Skip to content

Commit

Permalink
Merge pull request #29 from cosmology-tech/trim-and-docs
Browse files Browse the repository at this point in the history
Trim and docs
  • Loading branch information
Zetazzz authored Aug 14, 2024
2 parents 159b95b + 8fecc1e commit aaf9e3d
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 37 deletions.
91 changes: 82 additions & 9 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
<<interface>>
+string algo
+string hdPath
+IKey getPublicKey(isCompressed: boolean)
}
class ByteAuth {
<<interface>>
+ISignatureWraper~Sig~ sign(data: Uint8Array)
}
class DocAuth {
<<interface>>
+string address
+SignDocResponse~Doc~ signDoc(doc: Doc)
}
ByteAuth --|> Auth
DocAuth --|> Auth
BaseDocAuth ..|> DocAuth
class BaseDocAuth {
<<abstract>>
+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" */
Expand All @@ -38,12 +104,19 @@ export interface Auth {

export interface Wallet<Account, SignDoc> {
...,
sign: (doc: SignDoc) => Promise<SignResponse<SignDoc>>;
async signDirect(
signerAddress: string,
signDoc: CosmosDirectDoc
): Promise<DirectSignResponse>;
async signAmino(
signerAddress: string,
signDoc: CosmosAminoDoc
): Promise<AminoSignResponse>;
}
```

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).
144 changes: 134 additions & 10 deletions docs/signer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
<<interface>>
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 {
<<abstract>>
+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 {
<<interface>>
+buildSignature(doc: Doc): Sig | Promise<Sig>
}
class ITxBuilder {
<<interface>>
+buildSignedTxDoc(args: SignArgs): Promise<SignResp>
}
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.

Expand All @@ -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("<MNEMONIC_WORDS>", "cosmos");
const [auth] = Secp256k1Auth.fromMnemonic("<MNEMONIC_WORDS>", [
HDPath.cosmos().toString(),
]);
const signer = new DirectSigner(auth, [toEncoder(MsgSend)], <RPC_ENDPOINT>);
```

## 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.

Expand All @@ -44,18 +166,20 @@ 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("<MNEMONIC_WORDS>", [
{
prefix: commonPrefix,
hdPath: cosmosHdPath,
},
]);
const signer = await DirectSigner.fromWallet(wallet, [toEncoder(MsgSend)], <RPC_ENDPOINT>);
```

> Tips: `interchainjs` also provides helper methods to easily construct `Wallet` for each `Signer`. See [details](/docs/wallet.md#easy-to-construct-wallet).
## UniSigner Interface

There are 3 signing methods in `UniSigner`
There are 3 main signing methods in `UniSigner`

```ts
/** you can import { UniSigner } from "@interchainjs/types" */
Expand Down
2 changes: 1 addition & 1 deletion docs/wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ classDiagram
OfflineDirectSigner <|.. Secp256k1HDWallet
OfflineAminoSigner <|.. Secp256k1HDWallet
Secp256k1HDWallet <|.. ICosmosWallet
ICosmosWallet <|.. Secp256k1HDWallet
style OfflineDirectSigner fill:#f9f,stroke:#333,stroke-width:2px
style OfflineAminoSigner fill:#f9f,stroke:#333,stroke-width:2px
Expand Down
33 changes: 25 additions & 8 deletions libs/interchainjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,38 @@ 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(<rpc-endpoint>, <offline signer>);
const result = await client.signAndBroadcast(<ADDRESS>, <MESSAGE>[], "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(<ADDRESS>, <MESSAGE>[], "auto");
// or you can use helper functions to do `signAndBroadcast`. taking send tokens as example
const result = await client.helpers.send(<ADDRESS>, <MsgSend message>, "auto", "");
const result = await signingClient.helpers.send(<ADDRESS>, <MsgSend message>, "auto", "");

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("<MNEMONIC_WORDS>", { prefix: "<prefix>" });
const directOfflineSigner = wallet.toOfflineDirectSigner();
const directWallet = Secp256k1HDWallet.fromMnemonic(generateMnemonic(), [
{
prefix: commonPrefix,
hdPath: cosmosHdPath,
},
]);

const directSigner = directWallet.toOfflineDirectSigner();
```

## Implementations
Expand All @@ -50,7 +67,7 @@ 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

Expand Down
1 change: 1 addition & 0 deletions networks/cosmos/src/base/base-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export abstract class CosmosBaseSigner<SignDoc>
return this._queryClient;
}


/**
* convert relative timeoutHeight to absolute timeoutHeight
*/
Expand Down
Loading

0 comments on commit aaf9e3d

Please sign in to comment.