diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts new file mode 100644 index 00000000..11135f6e --- /dev/null +++ b/packages/stargate/src/signingstargateclient.ts @@ -0,0 +1,135 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { fromBase64 } from "@cosmjs/encoding"; +import { + buildFeeTable, + Coin, + CosmosFeeTable, + encodeSecp256k1Pubkey, + GasLimits, + GasPrice, + StdFee, +} from "@cosmjs/launchpad"; +import { Int53 } from "@cosmjs/math"; +import { + EncodeObject, + encodePubkey, + isOfflineDirectSigner, + makeAuthInfoBytes, + makeSignDoc, + OfflineSigner, + Registry, +} from "@cosmjs/proto-signing"; +import { Client as TendermintClient } from "@cosmjs/tendermint-rpc"; + +import { cosmos } from "./codec"; +import { BroadcastTxResponse, StargateClient } from "./stargateclient"; + +const { TxRaw } = cosmos.tx.v1beta1; + +const defaultGasPrice = GasPrice.fromString("0.025ucosm"); +const defaultGasLimits: GasLimits = { send: 80000 }; + +/** Use for testing only */ +export interface PrivateSigningStargateClient { + readonly fees: CosmosFeeTable; + readonly registry: Registry; +} + +export interface SigningStargateClientOptions { + readonly registry?: Registry; + readonly gasPrice?: GasPrice; + readonly gasLimits?: GasLimits; +} + +export class SigningStargateClient extends StargateClient { + private readonly fees: CosmosFeeTable; + private readonly registry: Registry; + private readonly signer: OfflineSigner; + + public static async connectWithWallet( + endpoint: string, + signer: OfflineSigner, + options: SigningStargateClientOptions = {}, + ): Promise { + const tmClient = await TendermintClient.connect(endpoint); + return new SigningStargateClient(tmClient, signer, options); + } + + private constructor( + tmClient: TendermintClient, + signer: OfflineSigner, + options: SigningStargateClientOptions, + ) { + super(tmClient); + const { registry = new Registry(), gasPrice = defaultGasPrice, gasLimits = defaultGasLimits } = options; + this.fees = buildFeeTable(gasPrice, defaultGasLimits, gasLimits); + this.registry = registry; + this.signer = signer; + } + + public async sendTokens( + senderAddress: string, + recipientAddress: string, + transferAmount: readonly Coin[], + memo = "", + ): Promise { + const sendMsg = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: senderAddress, + toAddress: recipientAddress, + amount: transferAmount, + }, + }; + return this.signAndBroadcast(senderAddress, [sendMsg], this.fees.send, memo); + } + + public async signAndBroadcast( + address: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo = "", + ): Promise { + if (!isOfflineDirectSigner(this.signer)) { + throw new Error("Amino signer not yet supported"); + } + + const accountFromSigner = (await this.signer.getAccounts()).find( + (account) => account.address === address, + ); + if (!accountFromSigner) { + throw new Error("Failed to retrieve account from signer"); + } + const pubkey = encodeSecp256k1Pubkey(accountFromSigner.pubkey); + const accountFromChain = await this.getAccount(address); + if (!accountFromChain) { + throw new Error("Account not found"); + } + const { accountNumber, sequence } = accountFromChain; + if (!pubkey) { + throw new Error("Pubkey not known"); + } + const chainId = await this.getChainId(); + const pubkeyAny = encodePubkey(pubkey); + const txBody = { + messages: messages, + memo: memo, + }; + const txBodyBytes = this.registry.encode({ + typeUrl: "/cosmos.tx.v1beta1.TxBody", + value: txBody, + }); + const gasLimit = Int53.fromString(fee.gas).toNumber(); + const authInfoBytes = makeAuthInfoBytes([pubkeyAny], fee.amount, gasLimit, sequence); + + const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); + const signResponse = await this.signer.signDirect(address, signDoc); + const txRaw = TxRaw.create({ + bodyBytes: txBodyBytes, + authInfoBytes: authInfoBytes, + signatures: [fromBase64(signResponse.signature.signature)], + }); + const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish()); + return this.broadcastTx(signedTx); + } +} diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 2defde96..e8a9da2e 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -123,7 +123,7 @@ export class StargateClient { return new StargateClient(tmClient); } - private constructor(tmClient: TendermintClient) { + protected constructor(tmClient: TendermintClient) { this.tmClient = tmClient; this.queryClient = QueryClient.withExtensions(tmClient, setupAuthExtension, setupBankExtension); } diff --git a/packages/stargate/types/signingstargateclient.d.ts b/packages/stargate/types/signingstargateclient.d.ts new file mode 100644 index 00000000..e782543b --- /dev/null +++ b/packages/stargate/types/signingstargateclient.d.ts @@ -0,0 +1,36 @@ +import { Coin, CosmosFeeTable, GasLimits, GasPrice, StdFee } from "@cosmjs/launchpad"; +import { EncodeObject, OfflineSigner, Registry } from "@cosmjs/proto-signing"; +import { BroadcastTxResponse, StargateClient } from "./stargateclient"; +/** Use for testing only */ +export interface PrivateSigningStargateClient { + readonly fees: CosmosFeeTable; + readonly registry: Registry; +} +export interface SigningStargateClientOptions { + readonly registry?: Registry; + readonly gasPrice?: GasPrice; + readonly gasLimits?: GasLimits; +} +export declare class SigningStargateClient extends StargateClient { + private readonly fees; + private readonly registry; + private readonly signer; + static connectWithWallet( + endpoint: string, + signer: OfflineSigner, + options?: SigningStargateClientOptions, + ): Promise; + private constructor(); + sendTokens( + senderAddress: string, + recipientAddress: string, + transferAmount: readonly Coin[], + memo?: string, + ): Promise; + signAndBroadcast( + address: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo?: string, + ): Promise; +} diff --git a/packages/stargate/types/stargateclient.d.ts b/packages/stargate/types/stargateclient.d.ts index b8c8e460..c2408b1d 100644 --- a/packages/stargate/types/stargateclient.d.ts +++ b/packages/stargate/types/stargateclient.d.ts @@ -52,7 +52,7 @@ export declare class StargateClient { private readonly queryClient; private chainId; static connect(endpoint: string): Promise; - private constructor(); + protected constructor(tmClient: TendermintClient); getChainId(): Promise; getHeight(): Promise; getAccount(searchAddress: string): Promise;