diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts index def69bad..8554b04b 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts @@ -2,12 +2,21 @@ import { UploadMeta } from "@cosmjs/cosmwasm-launchpad"; import { sha256 } from "@cosmjs/crypto"; import { toHex } from "@cosmjs/encoding"; -import { coin, coins, GasPrice } from "@cosmjs/launchpad"; -import { DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing"; -import { assertIsBroadcastTxSuccess, codec } from "@cosmjs/stargate"; +import { + coin, + coins, + GasPrice, + MsgDelegate as LaunchpadMsgDelegate, + Secp256k1HdWallet, +} from "@cosmjs/launchpad"; +import { Coin, cosmosField, DirectSecp256k1HdWallet, registered, Registry } from "@cosmjs/proto-signing"; +import { AminoTypes, assertIsBroadcastTxSuccess, codec } from "@cosmjs/stargate"; import { assert, sleep } from "@cosmjs/utils"; import Long from "long"; +import pako from "pako"; +import { Message } from "protobufjs"; +import { cosmwasm } from "./codec"; import { PrivateSigningCosmWasmClient, SigningCosmWasmClient } from "./signingcosmwasmclient"; import { alice, @@ -15,12 +24,18 @@ import { makeRandomAddress, makeWasmClient, ModifyingDirectSecp256k1HdWallet, + ModifyingSecp256k1HdWallet, pendingWithoutWasmd, unused, validator, wasmd, } from "./testutils.spec"; +type IMsgSend = codec.cosmos.bank.v1beta1.IMsgSend; +type IMsgDelegate = codec.cosmos.staking.v1beta1.IMsgDelegate; +type IMsgStoreCode = cosmwasm.wasm.v1beta1.IMsgStoreCode; + +const { MsgSend } = codec.cosmos.bank.v1beta1; const { MsgDelegate } = codec.cosmos.staking.v1beta1; const { Tx } = codec.cosmos.tx.v1beta1; @@ -33,6 +48,17 @@ describe("SigningCosmWasmClient", () => { expect(client).toBeTruthy(); }); + it("can be constructed with custom registry", async () => { + pendingWithoutWasmd(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic); + const registry = new Registry(); + registry.register("/custom.MsgCustom", MsgSend); + const options = { registry: registry }; + const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options); + const openedClient = (client as unknown) as PrivateSigningCosmWasmClient; + expect(openedClient.registry.lookupType("/custom.MsgCustom")).toEqual(MsgSend); + }); + it("can be constructed with custom gas price", async () => { pendingWithoutWasmd(); const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); @@ -411,7 +437,7 @@ describe("SigningCosmWasmClient", () => { }); describe("sendTokens", () => { - it("works", async () => { + it("works with direct signer", async () => { pendingWithoutWasmd(); const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet); @@ -434,6 +460,30 @@ describe("SigningCosmWasmClient", () => { assert(after); expect(after).toEqual(transferAmount[0]); }); + + it("works with legacy Amino signer", async () => { + pendingWithoutWasmd(); + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); + const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet); + + const transferAmount = coins(7890, "ucosm"); + const beneficiaryAddress = makeRandomAddress(); + const memo = "for dinner"; + + // no tokens here + const before = await client.getBalance(beneficiaryAddress, "ucosm"); + expect(before).toBeNull(); + + // send + const result = await client.sendTokens(alice.address0, beneficiaryAddress, transferAmount, memo); + assertIsBroadcastTxSuccess(result); + expect(result.rawLog).toBeTruthy(); + + // got tokens + const after = await client.getBalance(beneficiaryAddress, "ucosm"); + assert(after); + expect(after).toEqual(transferAmount[0]); + }); }); describe("signAndBroadcast", () => { @@ -506,5 +556,188 @@ describe("SigningCosmWasmClient", () => { expect(tx.authInfo!.fee!.gasLimit!.toNumber()).toEqual(333333); }); }); + + describe("legacy Amino mode", () => { + // NOTE: One custom registry shared between tests + // See https://github.com/protobufjs/protobuf.js#using-decorators + // > Decorated types reside in protobuf.roots["decorated"] using a flat structure, so no duplicate names. + const customRegistry = new Registry(); + const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; + + @registered(customRegistry, msgDelegateTypeUrl) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + class CustomMsgDelegate extends Message { + @cosmosField.string(1) + public readonly custom_delegator_address?: string; + @cosmosField.string(2) + public readonly custom_validator_address?: string; + @cosmosField.message(3, Coin) + public readonly custom_amount?: Coin; + } + + it("works with bank MsgSend", async () => { + pendingWithoutWasmd(); + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); + const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet); + + const msgSend: IMsgSend = { + fromAddress: alice.address0, + toAddress: makeRandomAddress(), + amount: coins(1234, "ucosm"), + }; + const msgAny = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "Use your tokens wisely"; + const result = await client.signAndBroadcast(alice.address0, [msgAny], fee, memo); + assertIsBroadcastTxSuccess(result); + }); + + it("works with staking MsgDelegate", async () => { + pendingWithoutWasmd(); + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); + const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet); + + const msgDelegate: IMsgDelegate = { + delegatorAddress: alice.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }; + const msgAny = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: msgDelegate, + }; + const fee = { + amount: coins(2000, "ustake"), + gas: "200000", + }; + const memo = "Use your tokens wisely"; + const result = await client.signAndBroadcast(alice.address0, [msgAny], fee, memo); + assertIsBroadcastTxSuccess(result); + }); + + it("works with wasm MsgStoreCode", async () => { + pendingWithoutWasmd(); + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); + const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet); + const { data, builder, source } = getHackatom(); + + const msgStoreCode: IMsgStoreCode = { + sender: alice.address0, + wasmByteCode: pako.gzip(data), + source: source, + builder: builder, + }; + const msgAny = { + typeUrl: "/cosmwasm.wasm.v1beta1.MsgStoreCode", + value: msgStoreCode, + }; + const fee = { + amount: coins(2000, "ustake"), + gas: "1500000", + }; + const memo = "Use your tokens wisely"; + const result = await client.signAndBroadcast(alice.address0, [msgAny], fee, memo); + assertIsBroadcastTxSuccess(result); + }); + + it("works with a custom registry and custom message", async () => { + pendingWithoutWasmd(); + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); + const customAminoTypes = new AminoTypes({ + additions: { + "/cosmos.staking.v1beta1.MsgDelegate": { + aminoType: "cosmos-sdk/MsgDelegate", + toAmino: ({ + custom_delegator_address, + custom_validator_address, + custom_amount, + }: CustomMsgDelegate): LaunchpadMsgDelegate["value"] => { + assert(custom_delegator_address, "missing custom_delegator_address"); + assert(custom_validator_address, "missing validator_address"); + assert(custom_amount, "missing amount"); + assert(custom_amount.amount, "missing amount.amount"); + assert(custom_amount.denom, "missing amount.denom"); + return { + delegator_address: custom_delegator_address, + validator_address: custom_validator_address, + amount: { + amount: custom_amount.amount, + denom: custom_amount.denom, + }, + }; + }, + fromAmino: ({ + delegator_address, + validator_address, + amount, + }: LaunchpadMsgDelegate["value"]): CustomMsgDelegate => + CustomMsgDelegate.create({ + custom_delegator_address: delegator_address, + custom_validator_address: validator_address, + custom_amount: Coin.create(amount), + }), + }, + }, + }); + const options = { registry: customRegistry, aminoTypes: customAminoTypes }; + const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options); + + const msg = { + custom_delegator_address: alice.address0, + custom_validator_address: validator.validatorAddress, + custom_amount: coin(1234, "ustake"), + }; + const msgAny = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "Use your power wisely"; + const result = await client.signAndBroadcast(alice.address0, [msgAny], fee, memo); + assertIsBroadcastTxSuccess(result); + }); + + it("works with a modifying signer", async () => { + pendingWithoutWasmd(); + const wallet = await ModifyingSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, "wasm"); + const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet); + + const msg = { + delegatorAddress: alice.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(alice.address0, [msgAny], fee, memo); + assertIsBroadcastTxSuccess(result); + + await sleep(1000); + + const searchResult = await client.getTx(result.transactionHash); + assert(searchResult, "Must find transaction"); + const tx = Tx.decode(searchResult.tx); + // From ModifyingSecp256k1HdWallet + expect(tx.body!.memo).toEqual("This was modified"); + expect({ ...tx.authInfo!.fee!.amount![0] }).toEqual(coin(3000, "ucosm")); + expect(tx.authInfo!.fee!.gasLimit!.toNumber()).toEqual(333333); + }); + }); }); }); diff --git a/packages/cosmwasm-stargate/src/testutils.spec.ts b/packages/cosmwasm-stargate/src/testutils.spec.ts index 5f778c73..e29980f8 100644 --- a/packages/cosmwasm-stargate/src/testutils.spec.ts +++ b/packages/cosmwasm-stargate/src/testutils.spec.ts @@ -1,7 +1,13 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Bip39, EnglishMnemonic, Random, Secp256k1, Slip10, Slip10Curve } from "@cosmjs/crypto"; import { Bech32, fromBase64 } from "@cosmjs/encoding"; -import { coins, makeCosmoshubPath } from "@cosmjs/launchpad"; +import { + AminoSignResponse, + coins, + makeCosmoshubPath, + Secp256k1HdWallet, + StdSignDoc, +} from "@cosmjs/launchpad"; import { DirectSecp256k1HdWallet, DirectSignResponse, makeAuthInfoBytes } from "@cosmjs/proto-signing"; import { AuthExtension, @@ -205,6 +211,41 @@ export async function makeWasmClient( return QueryClient.withExtensions(tmClient, setupAuthExtension, setupBankExtension, setupWasmExtension); } +/** + * A class for testing clients using an Amino signer which modifies the transaction it receives before signing + */ +export class ModifyingSecp256k1HdWallet extends Secp256k1HdWallet { + public static async fromMnemonic( + mnemonic: string, + hdPath = makeCosmoshubPath(0), + prefix = "cosmos", + ): Promise { + const mnemonicChecked = new EnglishMnemonic(mnemonic); + const seed = await Bip39.mnemonicToSeed(mnemonicChecked); + const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath); + const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; + return new ModifyingSecp256k1HdWallet( + mnemonicChecked, + hdPath, + privkey, + Secp256k1.compressPubkey(uncompressed), + prefix, + ); + } + + public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise { + const modifiedSignDoc = { + ...signDoc, + fee: { + amount: coins(3000, "ucosm"), + gas: "333333", + }, + memo: "This was modified", + }; + return super.signAmino(signerAddress, modifiedSignDoc); + } +} + /** * A class for testing clients using a direct signer which modifies the transaction it receives before signing */