diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index c99be923..cb6a7fe4 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -1,4 +1,4 @@ -import { coin, coins, GasPrice } from "@cosmjs/launchpad"; +import { coin, coins, GasPrice, Secp256k1HdWallet } from "@cosmjs/launchpad"; import { DirectSecp256k1Wallet, Registry } from "@cosmjs/proto-signing"; import { assert } from "@cosmjs/utils"; @@ -126,7 +126,7 @@ describe("SigningStargateClient", () => { }); describe("signAndBroadcast", () => { - it("works", async () => { + it("works with direct mode", async () => { pendingWithoutSimapp(); const wallet = await DirectSecp256k1Wallet.fromMnemonic(faucet.mnemonic); const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; @@ -152,5 +152,32 @@ describe("SigningStargateClient", () => { const result = await client.signAndBroadcast(faucet.address0, [msgAny], fee, memo); assertIsBroadcastTxSuccess(result); }); + + it("works with legacy Amino mode", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; + const registry = new Registry(); + registry.register(msgDelegateTypeUrl, cosmos.staking.v1beta1.MsgDelegate); + const options = { registry: registry }; + const client = await SigningStargateClient.connectWithWallet(simapp.tendermintUrl, wallet, options); + + const msg = cosmos.staking.v1beta1.MsgDelegate.create({ + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }); + const msgAny = { + typeUrl: msgDelegateTypeUrl, + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "Use your power wisely"; + const result = await client.signAndBroadcast(faucet.address0, [msgAny], fee, memo); + assertIsBroadcastTxSuccess(result); + }); }); }); diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index 11135f6e..e7203f49 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -1,13 +1,17 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { fromBase64 } from "@cosmjs/encoding"; import { + AccountData, buildFeeTable, Coin, CosmosFeeTable, encodeSecp256k1Pubkey, GasLimits, GasPrice, + makeSignDoc as makeSignDocAmino, + Msg, StdFee, + StdSignDoc, } from "@cosmjs/launchpad"; import { Int53 } from "@cosmjs/math"; import { @@ -26,6 +30,40 @@ import { BroadcastTxResponse, StargateClient } from "./stargateclient"; const { TxRaw } = cosmos.tx.v1beta1; +function snakifyMsgValue(obj: Msg): Msg { + return { + ...obj, + value: Object.entries(obj.value).reduce( + (snakified, [key, value]) => ({ + ...snakified, + [key + .split(/(?=[A-Z])/) + .join("_") + .toLowerCase()]: value, + }), + {}, + ), + }; +} + +function snakifyForAmino(signDoc: StdSignDoc): StdSignDoc { + return { + ...signDoc, + msgs: signDoc.msgs.map(snakifyMsgValue), + }; +} + +function getMsgType(typeUrl: string): string { + const typeRegister: Record = { + "/cosmos.staking.v1beta1.MsgDelegate": "cosmos-sdk/MsgDelegate", + }; + const type = typeRegister[typeUrl]; + if (!type) { + throw new Error("Type URL not known"); + } + return type; +} + const defaultGasPrice = GasPrice.fromString("0.025ucosm"); const defaultGasLimits: GasLimits = { send: 80000 }; @@ -90,12 +128,8 @@ export class SigningStargateClient extends StargateClient { 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, + (account: AccountData) => account.address === address, ); if (!accountFromSigner) { throw new Error("Failed to retrieve account from signer"); @@ -120,10 +154,29 @@ export class SigningStargateClient extends StargateClient { 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); + if (isOfflineDirectSigner(this.signer)) { + 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); + } + + // Amino signer + const signMode = cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_LEGACY_AMINO_JSON; + const authInfoBytes = makeAuthInfoBytes([pubkeyAny], fee.amount, gasLimit, sequence, signMode); + const msgs = messages.map((msg) => ({ + type: getMsgType(msg.typeUrl), + value: msg.value, + })); + const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence); + const signResponse = await this.signer.signAmino(address, snakifyForAmino(signDoc)); const txRaw = TxRaw.create({ bodyBytes: txBodyBytes, authInfoBytes: authInfoBytes,