diff --git a/docs/auth.md b/docs/auth.md index 0d61a81..dca9c28 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -2,31 +2,97 @@ The main purpose of the `@interchainjs/auth` is to offer developers a way to have different wallet algorithm implementations on Blockchain, including `secp256k1`, `ethSecp256k1`, etc. All of these algorithms implementations are exposing the same `Auth` interface which means that `Signer`s can just use these methods without the need to know the underlying implementation for specific algorithms as they are abstracted away. +```mermaid +classDiagram + class Auth { + <> + +string algo + +string hdPath + +IKey getPublicKey(isCompressed: boolean) + } + + class ByteAuth { + <> + +ISignatureWraper~Sig~ sign(data: Uint8Array) + } + + class DocAuth { + <> + +string address + +SignDocResponse~Doc~ signDoc(doc: Doc) + } + + ByteAuth --|> Auth + DocAuth --|> Auth + BaseDocAuth ..|> DocAuth + + class BaseDocAuth { + <> + +abstract Promise~SignDocResponse~ signDoc(doc: Doc) + } + + class AminoDocAuth { + +Promise~SignDocResponse~ signDoc(doc: StdSignDoc) + +static Promise~AminoDocAuth[]~ fromOfflineSigner(offlineSigner: OfflineAminoSigner) + } + + class DirectDocAuth { + +Promise~SignDocResponse~ signDoc(doc: SignDoc) + +static Promise~DirectDocAuth[]~ fromOfflineSigner(offlineSigner: OfflineDirectSigner) + } + + BaseDocAuth <|-- AminoDocAuth + BaseDocAuth <|-- DirectDocAuth + + class Secp256k1Auth { + +Key privateKey + +string algo + +string hdPath + +Secp256k1Auth(privateKey: Uint8Array | HDKey | Key, hdPath?: string) + +static Secp256k1Auth[] fromMnemonic(mnemonic: string, hdPaths: string[], options?: AuthOptions) + +Key getPublicKey(isCompressed?: boolean) + +ISignatureWraper~RecoveredSignatureType~ sign(data: Uint8Array) + } + + Secp256k1Auth ..|> ByteAuth + + style Auth fill:#f9f,stroke:#333,stroke-width:2px + style ByteAuth fill:#f9f,stroke:#333,stroke-width:2px + style DocAuth fill:#f9f,stroke:#333,stroke-width:2px +``` + To start, you have to make an instance of the `*Auth` (i.e. `Secp256k1Auth`) class which gives you the ability to use different algorithms out of the box. -Usually it can be instantiated from three static methods +Usually it can be instantiated from constructor or static methods. -- `fromMnemonic` makes an instance from a mnemonic words string. This instance can both `sign` and `verify`. -- `fromPrivateKey` makes an instance from a private key. This instance can both `sign` and `verify`. -- `fromPublicKey` makes an instance from a public key. This instance can only `verify` but no `sign` since the private key necessary for signing can not be derived from public key. +- `fromMnemonic` makes an instance from a mnemonic words string. This instance can both `sign`. Let's have a look at the properties and methods that `Auth` interface exposes and what they mean: - `algo` implies the algorithm name, i.e. `secp256k1`, `ed25519`. - `getPublicKey` gets the public key. This method returns the compressed or uncompressed public key according to the value of argument `isCompressed`. - `sign` signs binary data that can be any piece of information or message that needs to be digitally signed, and returns a `Signature` typed object. Note: this method itself usually does not inherently involve any hash method. -- `verify` verifies the authenticity of given signature, that is checking if the signature is valid for the provided binary data. Same with `sign`, this method itself usually doesn't apply any hash function to the signed data. -It's important to note that for a specific cryptographic algorithms, the corresponding `*Auth` class implements `Auth` interface in a way that can be universally applied on different networks. That's why both `sign` and `verify` methods usually don't apply any hash function to the targeted message data. Those various hashing processes will be configured in different `Signer`s. That is: +It's important to note that for a specific cryptographic algorithms, the corresponding `*Auth` class implements `Auth` interface in a way that can be universally applied on different networks. That's why `sign` method usually don't apply any hash function to the targeted message data. Those various hashing processes will be configured in different `Signer`s. That is: - `*Auth` classes differs across algorithms but independent of networks - `*Signer` classes differs across networks but independent of algorithms See [usage example](/docs/signer.md#signer--auth). +## ByteAuth vs. DocAuth + +### ByteAuth + +`ByteAuth` is an interface that extends the `Auth` interface and represents an authentication method that can sign arbitrary bytes. It is typically used for signing arbitrary data using specific algorithms like `secp256k1` or `eth_secp256k1`. The `sign` method in `ByteAuth` takes a `Uint8Array` of data and returns a signature wrapped in an `ISignatureWraper`. + +### DocAuth + +`DocAuth` is an interface that extends the `Auth` interface and represents an authentication method that can sign documents using offline signers. It is a wrapper for offline signers and is usually used by signers built from offline signers. The `signDoc` method in `DocAuth` takes a document of a specific type and returns a `SignDocResponse`. The `DocAuth` interface also includes an `address` property that represents the address associated with the authentication method. + ## Auth vs. Wallet -Both `Auth` and `Wallet` are interfaces that contains `sign` method. But they differs in the arguments. +Both `Auth` and `Wallet` are interfaces that contains `sign` method. ```ts /** you can import { Auth, Wallet } from "@interchainjs/types" */ @@ -38,12 +104,19 @@ export interface Auth { export interface Wallet { ..., - sign: (doc: SignDoc) => Promise>; + async signDirect( + signerAddress: string, + signDoc: CosmosDirectDoc + ): Promise; + async signAmino( + signerAddress: string, + signDoc: CosmosAminoDoc + ): Promise; } ``` As we can see above, the signing target of `Wallet` is can be any type (usually we set it as the sign document type) while in `Auth` it's limited to binary data. -For each `Signer` it always has a specific type of sign document type as the signing target to get signature (i.e. for `AminoSigner` it's `StdSignDoc` and for `DirectSigner` it's `SignDoc`). And for some Web3 wallet, they only expose signing methods of the sign document rather than the generalized binary data. Under this circumstanc, users are still abled to construct a `Signer` object via the `fromWallet` static method. This is why `Wallet` interface is created. +For each `Signer` it always has a specific type of sign document type as the signing target to get signature (i.e. for `AminoSigner` it's `StdSignDoc` and for `DirectSigner` it's `SignDoc`). And for some Web3 wallet, they only expose signing methods of the sign document rather than the generalized binary data. Under this circumstance, users are still abled to construct a `Signer` object via the `fromWallet` static method. This is why `Wallet` interface is created. See [usage example](/docs/signer.md#signer--wallet). diff --git a/docs/signer.md b/docs/signer.md index 5f6f620..3e0d57c 100644 --- a/docs/signer.md +++ b/docs/signer.md @@ -2,15 +2,135 @@ The main purpose of the `@interchainjs/cosmos`, `@interchainjs/ethereum`, `@interchainjs/ethermint` is to offer developers a way to have different `Signer` implementations on different types of Blockchains. All of these `Signer`s are implementing [`UniSigner` interface](#unisigner-interface) and extending the same `BaseSigner` class which with `Auth` object being utilized in construction. +Class diagram: + +```mermaid +classDiagram + class UniSigner { + <> + IKey publicKey + AddressResponse getAddress() + IKey | Promise~IKey~ signArbitrary(Uint8Array data) + SignDocResponse~Doc~ | Promise~SignDocResponse~Doc~~ signDoc(Doc doc) + Promise~BroadcastResponse~ broadcastArbitrary(Uint8Array data, BroadcastOptions options) + Promise~SignResponse~Tx, Doc, BroadcastResponse~~ sign(SignArgs args) + Promise~BroadcastResponse~ signAndBroadcast(SignArgs args, BroadcastOptions options) + Promise~BroadcastResponse~ broadcast(Tx tx, BroadcastOptions options) + } + + class BaseSigner { + <> + +Auth auth + +SignerConfig config + } + + class CosmosDocSigner { + +ISigBuilder txBuilder + +CosmosDocSigner(Auth auth, SignerConfig config) + +abstract ISigBuilder getTxBuilder() + +Promise~SignDocResponse~ signDoc(SignDoc doc) + } + + class CosmosBaseSigner { + +Encoder[] encoders + +string prefix + +IAccount account + +BaseCosmosTxBuilder txBuilder + +CosmosBaseSigner(Auth auth, Encoder[] encoders, string|HttpEndpoint endpoint, SignerOptions options) + +abstract Promise~IAccount~ getAccount() + +abstract BaseCosmosTxBuilder getTxBuilder() + +Promise~string~ getPrefix() + +Promise~string~ getAddress() + +void setEndpoint(string|HttpEndpoint endpoint) + +QueryClient get queryClient() + +Promise~SignResponse~ sign(CosmosSignArgs args) + +Promise~BroadcastResponse~ broadcast(TxRaw txRaw, BroadcastOptions options) + +Promise~BroadcastResponse~ broadcastArbitrary(Uint8Array message, BroadcastOptions options) + +Promise~BroadcastResponse~ signAndBroadcast(CosmosSignArgs args, BroadcastOptions options) + +Promise~SimulateResponse~ simulate(CosmosSignArgs args) + } + + class DirectSigner { + +auth: Auth + +encoders: Encoder[] + +endpoint: string | HttpEndpoint + +options: SignerOptions + +static fromWallet(signer: OfflineDirectSigner, encoders: Encoder[], endpoint?: string | HttpEndpoint, options?: SignerOptions): Promise~DirectSigner~ + +static fromWalletToSigners(signer: OfflineDirectSigner, encoders: Encoder[], endpoint?: string | HttpEndpoint, options?: SignerOptions): Promise~DirectSigner[]~ + } + + class AminoSigner { + +auth: Auth + +encoders: Encoder[] + +endpoint: string | HttpEndpoint + +options: SignerOptions + +static fromWallet(signer: OfflineDirectSigner, encoders: Encoder[], endpoint?: string | HttpEndpoint, options?: SignerOptions): Promise~DirectSigner~ + +static fromWalletToSigners(signer: OfflineDirectSigner, encoders: Encoder[], endpoint?: string | HttpEndpoint, options?: SignerOptions): Promise~DirectSigner[]~ + } + + class ISigBuilder { + <> + +buildSignature(doc: Doc): Sig | Promise + } + + class ITxBuilder { + <> + +buildSignedTxDoc(args: SignArgs): Promise + } + + class BaseCosmosTxBuilder { + +SignMode signMode + +BaseCosmosTxBuilderContext ctx + +buildDoc(args: CosmosSignArgs, txRaw: Partial~TxRaw~): Promise~SignDoc~ + +buildDocBytes(doc: SignDoc): Promise~Uint8Array~ + +buildTxRaw(args: CosmosSignArgs): Promise~Partial~TxRaw~ + +buildTxBody(args: CosmosSignArgs): Promise~TxBody~ + +buildSignerInfo(publicKey: EncodedMessage, sequence: bigint, signMode: SignMode): Promise~SignerInfo~ + +buildAuthInfo(signerInfos: SignerInfo[], fee: Fee): Promise~AuthInfo~ + +getFee(fee: StdFee, txBody: TxBody, signerInfos: SignerInfo[], options: DocOptions): Promise~StdFee~ + +buildSignedTxDoc(args: CosmosSignArgs): Promise~CosmosCreateDocResponse~SignDoc~~ + } + + BaseSigner <|-- CosmosDocSigner + CosmosDocSigner <|-- CosmosBaseSigner + CosmosBaseSigner <|-- DirectSigner + CosmosBaseSigner <|-- AminoSigner + UniSigner <|.. CosmosBaseSigner + + BaseCosmosTxBuilder --|> ITxBuilder + + CosmosDocSigner *-- ISigBuilder + CosmosBaseSigner *-- ITxBuilder + + style UniSigner fill:#f9f,stroke:#333,stroke-width:2px + style ISigBuilder fill:#f9f,stroke:#333,stroke-width:2px + style ITxBuilder fill:#f9f,stroke:#333,stroke-width:2px +``` + +Workflow: + +```mermaid +graph TD + A[Signer.sign] --> B[Create partial TxRaw by buildTxRaw] + B --> C[Call buildDoc] + C --> E[Sign the document by signDoc] + E -- isDocAuth --> F[auth.signDoc] + E -- isByteAuth --> G[txBuilder.buildSignature] + F --> H[Create signed TxRaw] + G --> H[Create signed TxRaw] + H --> I[Return CosmosCreateDocResponse] + I --> J[End] +``` + ```ts -import { UniSigner } from "@interchainjser/types"; -import { BaseSigner } from "@interchainjser/utils"; +import { UniSigner } from "@interchainjs/types"; +import { BaseSigner } from "@interchainjs/types"; ``` Need to note that there are 2 type parameters that indicates 2 types of document involved in signing and broadcasting process for interface `UniSigner`: - `SignDoc` is the document type as the signing target to get signature -- `Tx` is the transaction type to broadcast +- `Tx` is the signed transaction type to broadcast The `Signer` class is a way to sign and broadcast transactions on blockchains with ease. With it, you can just pass a Message that you want to be packed in a transaction and the transaction will be prepared, signed and broadcasted. @@ -26,13 +146,15 @@ import { toEncoder } from "@interchainjs/cosmos/utils"; import { Secp256k1Auth } from "@interchainjs/auth/secp256k1"; import { MsgSend } from "@interchainjs/cosmos-types/cosmos/bank/v1beta1/tx"; -const auth = Secp256k1Auth.fromMnemonic("", "cosmos"); +const [auth] = Secp256k1Auth.fromMnemonic("", [ + HDPath.cosmos().toString(), + ]); const signer = new DirectSigner(auth, [toEncoder(MsgSend)], ); ``` ## Signer + Wallet -As we know, `Wallet` object can be used to sign documents (See [details](/docs/auth.md#auth-vs-wallet)). However, some sign document is still not human-readable (i.e. for `DirectSigner`, the `SignDoc` type is an object with binary data type) +`Wallet` object can also be used to sign documents (See [details](/docs/auth.md#auth-vs-wallet)). However, some sign document is still not human-readable (i.e. for `DirectSigner`, the `SignDoc` type is an object with binary data types) However, combining with the `Signer` class allows you to sign human-readable messages or transactions using one function call. @@ -44,10 +166,12 @@ import { DirectWallet, SignDoc } from "@interchainjs/cosmos/types"; import { toEncoder } from "@interchainjs/cosmos/utils"; import { MsgSend } from "@interchainjs/cosmos-types/cosmos/bank/v1beta1/tx"; -const wallet: DirectWallet = { - async getAccount(){}, - async sign(doc: SignDoc){} -} +const directWallet = Secp256k1HDWallet.fromMnemonic("", [ + { + prefix: commonPrefix, + hdPath: cosmosHdPath, + }, +]); const signer = await DirectSigner.fromWallet(wallet, [toEncoder(MsgSend)], ); ``` @@ -55,7 +179,7 @@ const signer = await DirectSigner.fromWallet(wallet, [toEncoder(MsgSend)],

-Wrapper of `@interchainjs/auth` and `@interchainjs/cosmos` to fit corresponding interfaces in `@cosmjs` +Wrapper of `@interchainjs/auth` and `@interchainjs/cosmos` to fit corresponding interfaces in `@cosmjs` ## Usage @@ -26,10 +26,23 @@ To sign messages (taking `stargate` signing client as example) // import * from "interchainjs"; // Error: use sub-imports, to ensure small app size import { StargateSigningClient } from "interchainjs/stargate"; -const client = StargateSigningClient.connectWithSigner(, ); -const result = await client.signAndBroadcast(
, [], "auto"); +const directWallet = Secp256k1HDWallet.fromMnemonic(generateMnemonic(), [ + { + prefix: commonPrefix, + hdPath: cosmosHdPath, + }, +]); + +const directSigner = directWallet.toOfflineDirectSigner(); + +const signingClient = await StargateSigningClient.connectWithSigner( + await getRpcEndpoint(), + directSigner +); + +const result = await signingClient.signAndBroadcast(
, [], "auto"); // or you can use helper functions to do `signAndBroadcast`. taking send tokens as example -const result = await client.helpers.send(
, , "auto", ""); +const result = await signingClient.helpers.send(
, , "auto", ""); console.log(result.transactionHash); // the hash of TxRaw ``` @@ -37,10 +50,14 @@ console.log(result.transactionHash); // the hash of TxRaw To construct an offline signer (taking `direct` signer as example) ```ts -import { Secp256k1Wallet } from "interchainjs/wallets/secp256k1"; - -const wallet = Secp256k1Wallet.fromMnemonic("", { prefix: "" }); -const directOfflineSigner = wallet.toOfflineDirectSigner(); +const directWallet = Secp256k1HDWallet.fromMnemonic(generateMnemonic(), [ + { + prefix: commonPrefix, + hdPath: cosmosHdPath, + }, +]); + +const directSigner = directWallet.toOfflineDirectSigner(); ``` ## Implementations @@ -50,10 +67,10 @@ const directOfflineSigner = wallet.toOfflineDirectSigner(); - **stargate signing client** from `interchainjs/stargate` - **cosmwasm signing client** from `interchainjs/cosmwasm-stargate` - **wallet** - - **secp256k1 wallet** from `interchainjs/wallets/secp256k1` + - **secp256k1 wallet** from `@interchainjs/cosmos/wallets/secp256k1hd` ## License MIT License (MIT) & Apache License -Copyright (c) 2024 Cosmology (https://cosmology.zone/) \ No newline at end of file +Copyright (c) 2024 Cosmology (https://cosmology.zone/) diff --git a/networks/cosmos/src/base/base-signer.ts b/networks/cosmos/src/base/base-signer.ts index b685765..efe607d 100644 --- a/networks/cosmos/src/base/base-signer.ts +++ b/networks/cosmos/src/base/base-signer.ts @@ -217,6 +217,7 @@ export abstract class CosmosBaseSigner return this._queryClient; } + /** * convert relative timeoutHeight to absolute timeoutHeight */ diff --git a/packages/auth/README.md b/packages/auth/README.md index a0d082d..28bec44 100644 --- a/packages/auth/README.md +++ b/packages/auth/README.md @@ -26,33 +26,36 @@ Taking `secp256k1` as example. // import * from "@interchainjs/auth"; // Error: use sub-imports, to ensure small app size import { Secp256k1Auth } from "@interchainjs/auth/secp256k1"; -const auth = Secp256k1Auth.fromMnemonic("", ""); +const [directAuth] = Secp256k1Auth.fromMnemonic(generateMnemonic(), [ + "m/44'/118'/0'/0/0", +]); const signature = auth.sign(Uint8Array.from([1, 2, 3])); console.log(signature.toHex()); ``` -It's easy to derive _cosmos/injective/ethereum_ network HD path (taking `cosmos` as example) +It's easy to derive _cosmos/ethermint/ethereum_ network HD path (taking `cosmos` as example) ```ts // derive with Cosmos default HD path "m/44'/118'/0'/0/0" -const auth = Secp256k1Auth.fromMnemonic("", "cosmos"); +const [auth] = Secp256k1Auth.fromMnemonic("", [ + HDPath.cosmos().toString(), +]); // is identical to -const auth = Secp256k1Auth.fromMnemonic( - "", - "m/44'/118'/0'/0/0" -); +const [auth] = Secp256k1Auth.fromMnemonic("", [ + "m/44'/118'/0'/0/0", +]); ``` `Auth` objected can be utilized by different signers. See - [@interchainjs/cosmos](/networks/cosmos/README.md) - [@interchainjs/ethereum](/networks/ethereum/README.md) -- [@interchainjs/ethermint](/networks/injective/README.md) +- [@interchainjs/ethermint](/networks/ethermint/README.md) ## Implementations - **secp256k1 auth** from `@interchainjs/auth/secp256k1` -- **ed25519 auth** from `@interchainjs/auth/ed25519` (`Not fully implemented yet`) +- **ethSecp256k1 auth** from `@interchainjs/auth/ethSecp256k1` (`Not fully implemented yet`) ## License