From 5a84f91c589fedc3b408864e1dd30ea0eab4525f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 14:43:31 +0100 Subject: [PATCH 01/19] Inline msgDelegateTypeUrl --- packages/stargate/src/signingstargateclient.spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 869c4f12..0ba4e867 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -174,7 +174,6 @@ describe("SigningStargateClient", () => { it("works", async () => { pendingWithoutSimapp(); const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); - const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet); const msg = MsgDelegate.fromPartial({ @@ -183,7 +182,7 @@ describe("SigningStargateClient", () => { amount: coin(1234, "ustake"), }); const msgAny = { - typeUrl: msgDelegateTypeUrl, + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", value: msg, }; const fee = { @@ -198,7 +197,6 @@ describe("SigningStargateClient", () => { it("works with a modifying signer", async () => { pendingWithoutSimapp(); const wallet = await ModifyingDirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); - const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet); const msg = MsgDelegate.fromPartial({ @@ -207,7 +205,7 @@ describe("SigningStargateClient", () => { amount: coin(1234, "ustake"), }); const msgAny = { - typeUrl: msgDelegateTypeUrl, + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", value: msg, }; const fee = { From 1a0a5eddd7e633b0c82d5005673a8bb570bc4fea Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 14:23:16 +0100 Subject: [PATCH 02/19] Pull out signDirect/signAmino --- .../stargate/src/signingstargateclient.ts | 80 +++++++++++++------ 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index 49b30cba..b34b7d39 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -13,6 +13,7 @@ import { Registry, } from "@cosmjs/proto-signing"; import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { assert } from "@cosmjs/utils"; import { AminoTypes } from "./aminotypes"; import { MsgMultiSend } from "./codec/cosmos/bank/v1beta1/tx"; @@ -168,6 +169,19 @@ export class SigningStargateClient extends StargateClient { fee: StdFee, memo = "", ): Promise { + const signedTx = isOfflineDirectSigner(this.signer) + ? await this.signDirect(signerAddress, messages, fee, memo) + : await this.signAmino(signerAddress, messages, fee, memo); + return this.broadcastTx(signedTx); + } + + private async signAmino( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + ): Promise { + assert(!isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( (account) => account.address === signerAddress, ); @@ -181,30 +195,6 @@ export class SigningStargateClient extends StargateClient { } const { accountNumber, sequence } = accountFromChain; const chainId = await this.getChainId(); - 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(); - - if (isOfflineDirectSigner(this.signer)) { - const authInfoBytes = makeAuthInfoBytes([pubkey], fee.amount, gasLimit, sequence); - const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); - const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc); - const txRaw = TxRaw.fromPartial({ - bodyBytes: signed.bodyBytes, - authInfoBytes: signed.authInfoBytes, - signatures: [fromBase64(signature.signature)], - }); - const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish()); - return this.broadcastTx(signedTx); - } - - // Amino signer const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON; const msgs = messages.map((msg) => this.aminoTypes.toAmino(msg)); const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence); @@ -232,6 +222,46 @@ export class SigningStargateClient extends StargateClient { signatures: [fromBase64(signature.signature)], }); const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish()); - return this.broadcastTx(signedTx); + return signedTx; + } + + private async signDirect( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + ): Promise { + assert(isOfflineDirectSigner(this.signer)); + const accountFromSigner = (await this.signer.getAccounts()).find( + (account) => account.address === signerAddress, + ); + if (!accountFromSigner) { + throw new Error("Failed to retrieve account from signer"); + } + const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)); + const accountFromChain = await this.getAccountUnverified(signerAddress); + if (!accountFromChain) { + throw new Error("Account not found"); + } + const { accountNumber, sequence } = accountFromChain; + const chainId = await this.getChainId(); + 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([pubkey], fee.amount, gasLimit, sequence); + const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); + const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc); + const txRaw = TxRaw.fromPartial({ + bodyBytes: signed.bodyBytes, + authInfoBytes: signed.authInfoBytes, + signatures: [fromBase64(signature.signature)], + }); + return Uint8Array.from(TxRaw.encode(txRaw).finish()); } } From e89ad4f81b6783ad1565224636a3a5b2a5fb01f4 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 23:57:56 +0100 Subject: [PATCH 03/19] Create SigningStargateClient.sign --- CHANGELOG.md | 2 + .../src/signingstargateclient.spec.ts | 273 +++++++++++++++++- .../stargate/src/signingstargateclient.ts | 33 ++- 3 files changed, 297 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44bbe903..5aa30424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ and this project adheres to - @cosmjs/proto-signing: Added new `Coin`, `coin`, `coins` and `parseCoins` exports which have the same functionality as already existed in @cosmjs/launchpad. +- @cosmjs/stargate: Add `SigningStargateClient.sign`, which allows you to create + signed transactions without broadcasting them directly. ### Changed diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 0ba4e867..e826c554 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -8,7 +8,7 @@ import { AminoTypes } from "./aminotypes"; import { MsgSend } from "./codec/cosmos/bank/v1beta1/tx"; import { Coin } from "./codec/cosmos/base/v1beta1/coin"; import { DeepPartial, MsgDelegate } from "./codec/cosmos/staking/v1beta1/tx"; -import { Tx } from "./codec/cosmos/tx/v1beta1/tx"; +import { AuthInfo, Tx, TxBody, TxRaw } from "./codec/cosmos/tx/v1beta1/tx"; import { GasPrice } from "./fee"; import { PrivateSigningStargateClient, SigningStargateClient } from "./signingstargateclient"; import { assertIsBroadcastTxSuccess } from "./stargateclient"; @@ -427,4 +427,275 @@ describe("SigningStargateClient", () => { }); }); }); + + describe("sign", () => { + describe("direct mode", () => { + it("works", async () => { + pendingWithoutSimapp(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet); + + const msg = MsgDelegate.fromPartial({ + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }); + const msgAny = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "180000", // 180k + }; + const memo = "Use your power wisely"; + const signed = await client.sign(faucet.address0, [msgAny], fee, memo); + + // ensure signature is valid + const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); + assertIsBroadcastTxSuccess(result); + }); + + it("works with a modifying signer", async () => { + pendingWithoutSimapp(); + const wallet = await ModifyingDirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet); + + const msg = MsgDelegate.fromPartial({ + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }); + const msgAny = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "180000", // 180k + }; + const memo = "Use your power wisely"; + const signed = await client.sign(faucet.address0, [msgAny], fee, memo); + + const body = TxBody.decode(signed.bodyBytes); + const authInfo = AuthInfo.decode(signed.authInfoBytes); + // From ModifyingDirectSecp256k1HdWallet + expect(body.memo).toEqual("This was modified"); + expect({ ...authInfo.fee!.amount[0] }).toEqual(coin(3000, "ucosm")); + expect(authInfo.fee!.gasLimit.toNumber()).toEqual(333333); + + // ensure signature is valid + const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); + assertIsBroadcastTxSuccess(result); + }); + }); + + describe("legacy Amino mode", () => { + it("works with bank MsgSend", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet); + + const msgSend: MsgSend = { + fromAddress: faucet.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 signed = await client.sign(faucet.address0, [msgAny], fee, memo); + + // ensure signature is valid + const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); + assertIsBroadcastTxSuccess(result); + }); + + it("works with staking MsgDelegate", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet); + + const msgDelegate: MsgDelegate = { + delegatorAddress: faucet.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 signed = await client.sign(faucet.address0, [msgAny], fee, memo); + + // ensure signature is valid + const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); + assertIsBroadcastTxSuccess(result); + }); + + it("works with a custom registry and custom message", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + + const customRegistry = new Registry(); + const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; + interface CustomMsgDelegate { + customDelegatorAddress?: string; + customValidatorAddress?: string; + customAmount?: Coin; + } + const baseCustomMsgDelegate: CustomMsgDelegate = { + customDelegatorAddress: "", + customValidatorAddress: "", + }; + const CustomMsgDelegate = { + // Adapted from autogenerated MsgDelegate implementation + encode( + message: CustomMsgDelegate, + writer: protobuf.Writer = protobuf.Writer.create(), + ): protobuf.Writer { + writer.uint32(10).string(message.customDelegatorAddress ?? ""); + writer.uint32(18).string(message.customValidatorAddress ?? ""); + if (message.customAmount !== undefined && message.customAmount !== undefined) { + Coin.encode(message.customAmount, writer.uint32(26).fork()).ldelim(); + } + return writer; + }, + + decode(): CustomMsgDelegate { + throw new Error("decode method should not be required"); + }, + + fromJSON(): CustomMsgDelegate { + throw new Error("fromJSON method should not be required"); + }, + + fromPartial(object: DeepPartial): CustomMsgDelegate { + const message = { ...baseCustomMsgDelegate } as CustomMsgDelegate; + if (object.customDelegatorAddress !== undefined && object.customDelegatorAddress !== null) { + message.customDelegatorAddress = object.customDelegatorAddress; + } else { + message.customDelegatorAddress = ""; + } + if (object.customValidatorAddress !== undefined && object.customValidatorAddress !== null) { + message.customValidatorAddress = object.customValidatorAddress; + } else { + message.customValidatorAddress = ""; + } + if (object.customAmount !== undefined && object.customAmount !== null) { + message.customAmount = Coin.fromPartial(object.customAmount); + } else { + message.customAmount = undefined; + } + return message; + }, + + toJSON(): unknown { + throw new Error("toJSON method should not be required"); + }, + }; + customRegistry.register(msgDelegateTypeUrl, CustomMsgDelegate); + const customAminoTypes = new AminoTypes({ + additions: { + "/cosmos.staking.v1beta1.MsgDelegate": { + aminoType: "cosmos-sdk/MsgDelegate", + toAmino: ({ + customDelegatorAddress, + customValidatorAddress, + customAmount, + }: CustomMsgDelegate): LaunchpadMsgDelegate["value"] => { + assert(customDelegatorAddress, "missing customDelegatorAddress"); + assert(customValidatorAddress, "missing validatorAddress"); + assert(customAmount, "missing amount"); + assert(customAmount.amount, "missing amount.amount"); + assert(customAmount.denom, "missing amount.denom"); + return { + delegator_address: customDelegatorAddress, + validator_address: customValidatorAddress, + amount: { + amount: customAmount.amount, + denom: customAmount.denom, + }, + }; + }, + fromAmino: ({ + delegator_address, + validator_address, + amount, + }: LaunchpadMsgDelegate["value"]): CustomMsgDelegate => ({ + customDelegatorAddress: delegator_address, + customValidatorAddress: validator_address, + customAmount: Coin.fromPartial(amount), + }), + }, + }, + }); + const options = { registry: customRegistry, aminoTypes: customAminoTypes }; + const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet, options); + + const msg: CustomMsgDelegate = { + customDelegatorAddress: faucet.address0, + customValidatorAddress: validator.validatorAddress, + customAmount: 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 signed = await client.sign(faucet.address0, [msgAny], fee, memo); + + // ensure signature is valid + const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); + assertIsBroadcastTxSuccess(result); + }); + + it("works with a modifying signer", async () => { + pendingWithoutSimapp(); + const wallet = await ModifyingSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet); + + const msg: MsgDelegate = { + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + 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 signed = await client.sign(faucet.address0, [msgAny], fee, memo); + + const body = TxBody.decode(signed.bodyBytes); + const authInfo = AuthInfo.decode(signed.authInfoBytes); + // From ModifyingSecp256k1HdWallet + expect(body.memo).toEqual("This was modified"); + expect({ ...authInfo.fee!.amount[0] }).toEqual(coin(3000, "ucosm")); + expect(authInfo.fee!.gasLimit.toNumber()).toEqual(333333); + + // ensure signature is valid + const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); + assertIsBroadcastTxSuccess(result); + }); + }); + }); }); diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index b34b7d39..aaf4020d 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -169,18 +169,34 @@ export class SigningStargateClient extends StargateClient { fee: StdFee, memo = "", ): Promise { - const signedTx = isOfflineDirectSigner(this.signer) - ? await this.signDirect(signerAddress, messages, fee, memo) - : await this.signAmino(signerAddress, messages, fee, memo); + const txRaw = await this.sign(signerAddress, messages, fee, memo); + const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish()); return this.broadcastTx(signedTx); } + /** + * Gets account number and sequence from the API, creates a sign doc, + * creates a single signature and assembles the signed transaction. + * + * The sign mode (SIGN_MODE_DIRECT or SIGN_MODE_LEGACY_AMINO_JSON) is determined by this client's signer. + */ + public async sign( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + ): Promise { + return isOfflineDirectSigner(this.signer) + ? this.signDirect(signerAddress, messages, fee, memo) + : this.signAmino(signerAddress, messages, fee, memo); + } + private async signAmino( signerAddress: string, messages: readonly EncodeObject[], fee: StdFee, memo: string, - ): Promise { + ): Promise { assert(!isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( (account) => account.address === signerAddress, @@ -216,13 +232,11 @@ export class SigningStargateClient extends StargateClient { signedSequence, signMode, ); - const txRaw = TxRaw.fromPartial({ + return TxRaw.fromPartial({ bodyBytes: signedTxBodyBytes, authInfoBytes: signedAuthInfoBytes, signatures: [fromBase64(signature.signature)], }); - const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish()); - return signedTx; } private async signDirect( @@ -230,7 +244,7 @@ export class SigningStargateClient extends StargateClient { messages: readonly EncodeObject[], fee: StdFee, memo: string, - ): Promise { + ): Promise { assert(isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( (account) => account.address === signerAddress, @@ -257,11 +271,10 @@ export class SigningStargateClient extends StargateClient { const authInfoBytes = makeAuthInfoBytes([pubkey], fee.amount, gasLimit, sequence); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc); - const txRaw = TxRaw.fromPartial({ + return TxRaw.fromPartial({ bodyBytes: signed.bodyBytes, authInfoBytes: signed.authInfoBytes, signatures: [fromBase64(signature.signature)], }); - return Uint8Array.from(TxRaw.encode(txRaw).finish()); } } From 531a77780eac0295ab76fb27c21c482da47e6a34 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 18:49:40 +0100 Subject: [PATCH 04/19] Add 3 multisig accounts to simapp setup --- packages/stargate/src/queries/bank.spec.ts | 4 +- packages/stargate/src/testutils.spec.ts | 1 + scripts/simapp/README.md | 56 +++++++++++++++++ scripts/simapp/generate_template.sh | 3 +- .../template/.simapp/config/genesis.json | 60 +++++++++++++++++++ 5 files changed, 121 insertions(+), 3 deletions(-) diff --git a/packages/stargate/src/queries/bank.spec.ts b/packages/stargate/src/queries/bank.spec.ts index 0dc39eae..8806fa3d 100644 --- a/packages/stargate/src/queries/bank.spec.ts +++ b/packages/stargate/src/queries/bank.spec.ts @@ -144,7 +144,7 @@ describe("BankExtension", () => { const response = await client.bank.unverified.totalSupply(); expect(response).toEqual([ { - amount: "18000000000", + amount: simapp.totalSupply.toString(), denom: simapp.denomFee, }, { @@ -164,7 +164,7 @@ describe("BankExtension", () => { const response = await client.bank.unverified.supplyOf(simapp.denomFee); expect(response).toEqual({ - amount: "18000000000", + amount: simapp.totalSupply.toString(), denom: simapp.denomFee, }); diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index f98fc7fc..5688a2fb 100644 --- a/packages/stargate/src/testutils.spec.ts +++ b/packages/stargate/src/testutils.spec.ts @@ -45,6 +45,7 @@ export const simapp = { denomStaking: "ustake", denomFee: "ucosm", blockTime: 1_000, // ms + totalSupply: 21000000000, }; export const faucet = { diff --git a/scripts/simapp/README.md b/scripts/simapp/README.md index 63311c8f..39d36c0b 100644 --- a/scripts/simapp/README.md +++ b/scripts/simapp/README.md @@ -8,3 +8,59 @@ Run the following: cd scripts/simapp ./start.sh ``` + +## Preset accounts + +1. **Faucet**
+ economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone
+ Address 0: cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6
+ Address 1: cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5
+ Address 2: cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k
+ Address 3: cosmos142u9fgcjdlycfcez3lw8x6x5h7rfjlnfhpw2lx
+ Address 4: cosmos1hsm76p4ahyhl5yh3ve9ur49r5kemhp2r0dcjvx
+ Pubkey 0: A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ
+ Pubkey 1: AiDosfIbBi54XJ1QjCeApumcy/FjdtF+YhywPf3DKTx7
+ Pubkey 2: AzQg33JZqH7vSsm09esZY5bZvmzYwE/SY78cA0iLxpD7
+ Pubkey 3: A3gOAlB6aiRTCPvWMQg2+ZbGYNsLd8qlvV28m8p2UhY2
+ Pubkey 4: Aum2063ub/ErUnIUB36sK55LktGUStgcbSiaAnL1wadu +2. **Alice**: Test account for the cosmwasm package that can run in parallel with faucet without sequence conflicts
+ enlist hip relief stomach skate base shallow young switch frequent cry park
+ Address 0: cosmos14qemq0vw6y3gc3u3e0aty2e764u4gs5le3hada
+ Address 1: cosmos1hhg2rlu9jscacku2wwckws7932qqqu8x3gfgw0
+ Address 2: cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5
+ Address 3: cosmos17yg9mssjenmc3jkqth6ulcwj9cxujrxxzezwta
+ Address 4: cosmos1f7j7ryulwjfe9ljplvhtcaxa6wqgula3etktce
+ Pubkey 0: A9cXhWb8ZpqCzkA8dQCPV29KdeRLV3rUYxrkHudLbQtS
+ Pubkey 1: A4XluzvcUx0ViLF0DjYW5/noArGwpltDstoUUZo+g1b0
+ Pubkey 2: A5TKr1NKc/MKRJ7+EHDD9PlzmGaPD/di/6hzZyBwxoy5
+ Pubkey 3: A/HSABDUqMB2qDy+PA7fiuuuA+hfrco2VwwiThMiTzUx
+ Pubkey 4: A7usTiqgqfxL/WKhoephDUSCHBQlLagtwI/qTmEteTRM +3. **Bob**: Test account (unused for now)
+ remain fragile remove stamp quiz bus country dress critic mammal office need
+ Address 0: cosmos1lvrwcvrqlc5ktzp2c4t22xgkx29q3y83lktgzl
+ Address 1: cosmos1vkv9sfwaak76weyamqx0flmng2vuquxqcuqukh
+ Address 2: cosmos106jwym4s9aujcmes26myzzwqsccw09sdm0v5au
+ Address 3: cosmos1c7wpeen2uv8thayf7g8q2rgpm29clj0dgrdtzw
+ Address 4: cosmos1mjxpv9ft30wer7ma7kwfxhm42l379xutplrdk6
+ Pubkey 0: A0d/GxY+UALE+miWJP0qyq4/EayG1G6tsg24v+cbD6By
+ Pubkey 1: Agqd6njsVEQD1CR+F2aqEb8hil5NXZ06mjKgetaNC12t
+ Pubkey 2: A6e9ElvKaM0DKWh1bIdK3bgB14dyEDgIXYMA0Lbs1GoQ
+ Pubkey 3: AkAK5PQaucieWMb0+tTRY01feYI+upRnoNK556eD0Ibb
+ Pubkey 4: A5HMVEAJsupdQWItbZv5Z1xZifDixQi6tjU/hJpZY1bF +4. **Unused**: for testing account state; this account never changes balances or sequences
+ oyster design unusual machine spread century engine gravity focus cave carry slot
+ ArkCaFUJ/IH+vKBmNRCdUVl3mCAhbopk9jjW4Ko4OfRQ
+ cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u +5. **Guest**: account for manual testing
+ degree tackle suggest window test behind mesh extra cover prepare oak script
+ Am/+YV0LaeqQPu7BDJuDHV7J8y68ptkGs10YS+9s71Nq
+ cosmos17d0jcz59jf68g52vq38tuuncmwwjk42u6mcxej + +### Multisig accounts + +1. 1/5 threshold multisig of the first 5 faucet accounts
+ cosmos1v75snhly7wfndk83ud4daygh397vcmkta8rlzq +2. 2/5 threshold multisig of the first 5 faucet accounts
+ cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9 +3. 3/5 threshold multisig of the first 5 faucet accounts
+ cosmos1d2mg2euvus3tu2tprfwrfzeal4xu7kygugjxjc diff --git a/scripts/simapp/generate_template.sh b/scripts/simapp/generate_template.sh index fdfb9385..aed23d74 100755 --- a/scripts/simapp/generate_template.sh +++ b/scripts/simapp/generate_template.sh @@ -24,7 +24,8 @@ docker run --rm \ cosmos14qemq0vw6y3gc3u3e0aty2e764u4gs5le3hada cosmos1hhg2rlu9jscacku2wwckws7932qqqu8x3gfgw0 cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5 cosmos17yg9mssjenmc3jkqth6ulcwj9cxujrxxzezwta cosmos1f7j7ryulwjfe9ljplvhtcaxa6wqgula3etktce \ cosmos1lvrwcvrqlc5ktzp2c4t22xgkx29q3y83lktgzl cosmos1vkv9sfwaak76weyamqx0flmng2vuquxqcuqukh cosmos106jwym4s9aujcmes26myzzwqsccw09sdm0v5au cosmos1c7wpeen2uv8thayf7g8q2rgpm29clj0dgrdtzw cosmos1mjxpv9ft30wer7ma7kwfxhm42l379xutplrdk6 \ cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u \ - cosmos17d0jcz59jf68g52vq38tuuncmwwjk42u6mcxej + cosmos17d0jcz59jf68g52vq38tuuncmwwjk42u6mcxej \ + cosmos1v75snhly7wfndk83ud4daygh397vcmkta8rlzq cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9 cosmos1d2mg2euvus3tu2tprfwrfzeal4xu7kygugjxjc # The ./template folder is created by the docker daemon's user (root on Linux, current user # when using Docker Desktop on macOS), let's make it ours if needed diff --git a/scripts/simapp/template/.simapp/config/genesis.json b/scripts/simapp/template/.simapp/config/genesis.json index 034c2f68..3ff26e8a 100644 --- a/scripts/simapp/template/.simapp/config/genesis.json +++ b/scripts/simapp/template/.simapp/config/genesis.json @@ -128,6 +128,27 @@ "address": "cosmos17d0jcz59jf68g52vq38tuuncmwwjk42u6mcxej", "pub_key": null, "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "account_number": "0", + "address": "cosmos1v75snhly7wfndk83ud4daygh397vcmkta8rlzq", + "pub_key": null, + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "account_number": "0", + "address": "cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9", + "pub_key": null, + "sequence": "0" + }, + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "account_number": "0", + "address": "cosmos1d2mg2euvus3tu2tprfwrfzeal4xu7kygugjxjc", + "pub_key": null, + "sequence": "0" } ], "params": { @@ -205,6 +226,32 @@ } ] }, + { + "address": "cosmos1v75snhly7wfndk83ud4daygh397vcmkta8rlzq", + "coins": [ + { + "amount": "1000000000", + "denom": "ucosm" + }, + { + "amount": "10000000", + "denom": "ustake" + } + ] + }, + { + "address": "cosmos1d2mg2euvus3tu2tprfwrfzeal4xu7kygugjxjc", + "coins": [ + { + "amount": "1000000000", + "denom": "ucosm" + }, + { + "amount": "10000000", + "denom": "ustake" + } + ] + }, { "address": "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", "coins": [ @@ -257,6 +304,19 @@ } ] }, + { + "address": "cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9", + "coins": [ + { + "amount": "1000000000", + "denom": "ucosm" + }, + { + "amount": "10000000", + "denom": "ustake" + } + ] + }, { "address": "cosmos1hsm76p4ahyhl5yh3ve9ur49r5kemhp2r0dcjvx", "coins": [ From 63d4f3e6b945fe0884676017c9769bd42bc2e7c1 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 18:50:15 +0100 Subject: [PATCH 05/19] List faucet addresses 1,2,3,4 in testutils --- packages/stargate/src/testutils.spec.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index 5688a2fb..8b093eb3 100644 --- a/packages/stargate/src/testutils.spec.ts +++ b/packages/stargate/src/testutils.spec.ts @@ -55,7 +55,27 @@ export const faucet = { type: "tendermint/PubKeySecp256k1", value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", }, + pubkey1: { + type: "tendermint/PubKeySecp256k1", + value: "AiDosfIbBi54XJ1QjCeApumcy/FjdtF+YhywPf3DKTx7", + }, + pubkey2: { + type: "tendermint/PubKeySecp256k1", + value: "AzQg33JZqH7vSsm09esZY5bZvmzYwE/SY78cA0iLxpD7", + }, + pubkey3: { + type: "tendermint/PubKeySecp256k1", + value: "A3gOAlB6aiRTCPvWMQg2+ZbGYNsLd8qlvV28m8p2UhY2", + }, + pubkey4: { + type: "tendermint/PubKeySecp256k1", + value: "Aum2063ub/ErUnIUB36sK55LktGUStgcbSiaAnL1wadu", + }, address0: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", + address1: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + address2: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k", + address3: "cosmos142u9fgcjdlycfcez3lw8x6x5h7rfjlnfhpw2lx", + address4: "cosmos1hsm76p4ahyhl5yh3ve9ur49r5kemhp2r0dcjvx", }; /** Unused account */ From bc0e7f533117c6dce07b53b2a4cbfa765feca66e Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 18:51:00 +0100 Subject: [PATCH 06/19] Add support for MultisigThresholdPubkey to encodePubkey --- .../proto-signing/scripts/define-proto.sh | 1 + .../src/codec/cosmos/crypto/multisig/keys.ts | 106 ++++++++++++++++++ packages/proto-signing/src/pubkey.ts | 45 +++++--- 3 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 packages/proto-signing/src/codec/cosmos/crypto/multisig/keys.ts diff --git a/packages/proto-signing/scripts/define-proto.sh b/packages/proto-signing/scripts/define-proto.sh index ed906572..3b5017f3 100755 --- a/packages/proto-signing/scripts/define-proto.sh +++ b/packages/proto-signing/scripts/define-proto.sh @@ -20,6 +20,7 @@ protoc \ "$COSMOS_PROTO_DIR/cosmos/bank/v1beta1/bank.proto" \ "$COSMOS_PROTO_DIR/cosmos/bank/v1beta1/tx.proto" \ "$COSMOS_PROTO_DIR/cosmos/crypto/multisig/v1beta1/multisig.proto" \ + "$COSMOS_PROTO_DIR/cosmos/crypto/multisig/keys.proto" \ "$COSMOS_PROTO_DIR/cosmos/crypto/secp256k1/keys.proto" \ "$COSMOS_PROTO_DIR/cosmos/tx/v1beta1/tx.proto" \ "$COSMOS_PROTO_DIR/cosmos/tx/signing/v1beta1/signing.proto" \ diff --git a/packages/proto-signing/src/codec/cosmos/crypto/multisig/keys.ts b/packages/proto-signing/src/codec/cosmos/crypto/multisig/keys.ts new file mode 100644 index 00000000..6d692d92 --- /dev/null +++ b/packages/proto-signing/src/codec/cosmos/crypto/multisig/keys.ts @@ -0,0 +1,106 @@ +/* eslint-disable */ +import { Any } from "../../../google/protobuf/any"; +import Long from "long"; +import _m0 from "protobufjs/minimal"; + +export const protobufPackage = "cosmos.crypto.multisig"; + +/** + * LegacyAminoPubKey specifies a public key type + * which nests multiple public keys and a threshold, + * it uses legacy amino address rules. + */ +export interface LegacyAminoPubKey { + threshold: number; + publicKeys: Any[]; +} + +const baseLegacyAminoPubKey: object = { threshold: 0 }; + +export const LegacyAminoPubKey = { + encode(message: LegacyAminoPubKey, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.threshold !== 0) { + writer.uint32(8).uint32(message.threshold); + } + for (const v of message.publicKeys) { + Any.encode(v!, writer.uint32(18).fork()).ldelim(); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): LegacyAminoPubKey { + const reader = input instanceof Uint8Array ? new _m0.Reader(input) : input; + let end = length === undefined ? reader.len : reader.pos + length; + const message = { ...baseLegacyAminoPubKey } as LegacyAminoPubKey; + message.publicKeys = []; + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.threshold = reader.uint32(); + break; + case 2: + message.publicKeys.push(Any.decode(reader, reader.uint32())); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): LegacyAminoPubKey { + const message = { ...baseLegacyAminoPubKey } as LegacyAminoPubKey; + message.publicKeys = []; + if (object.threshold !== undefined && object.threshold !== null) { + message.threshold = Number(object.threshold); + } else { + message.threshold = 0; + } + if (object.publicKeys !== undefined && object.publicKeys !== null) { + for (const e of object.publicKeys) { + message.publicKeys.push(Any.fromJSON(e)); + } + } + return message; + }, + + toJSON(message: LegacyAminoPubKey): unknown { + const obj: any = {}; + message.threshold !== undefined && (obj.threshold = message.threshold); + if (message.publicKeys) { + obj.publicKeys = message.publicKeys.map((e) => (e ? Any.toJSON(e) : undefined)); + } else { + obj.publicKeys = []; + } + return obj; + }, + + fromPartial(object: DeepPartial): LegacyAminoPubKey { + const message = { ...baseLegacyAminoPubKey } as LegacyAminoPubKey; + message.publicKeys = []; + if (object.threshold !== undefined && object.threshold !== null) { + message.threshold = object.threshold; + } else { + message.threshold = 0; + } + if (object.publicKeys !== undefined && object.publicKeys !== null) { + for (const e of object.publicKeys) { + message.publicKeys.push(Any.fromPartial(e)); + } + } + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | undefined | Long; +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; diff --git a/packages/proto-signing/src/pubkey.ts b/packages/proto-signing/src/pubkey.ts index faa88ac4..ebf9f0c5 100644 --- a/packages/proto-signing/src/pubkey.ts +++ b/packages/proto-signing/src/pubkey.ts @@ -1,27 +1,42 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { encodeSecp256k1Pubkey, SinglePubkey as AminoPubKey } from "@cosmjs/amino"; +import { + encodeSecp256k1Pubkey, + isMultisigThresholdPubkey, + isSecp256k1Pubkey, + Pubkey, + SinglePubkey, +} from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; +import { Uint53 } from "@cosmjs/math"; +import { LegacyAminoPubKey } from "./codec/cosmos/crypto/multisig/keys"; import { PubKey } from "./codec/cosmos/crypto/secp256k1/keys"; import { Any } from "./codec/google/protobuf/any"; -export function encodePubkey(pubkey: AminoPubKey): Any { - switch (pubkey.type) { - case "tendermint/PubKeySecp256k1": { - const pubkeyProto = PubKey.fromPartial({ - key: fromBase64(pubkey.value), - }); - return Any.fromPartial({ - typeUrl: "/cosmos.crypto.secp256k1.PubKey", - value: Uint8Array.from(PubKey.encode(pubkeyProto).finish()), - }); - } - default: - throw new Error(`Pubkey type ${pubkey.type} not recognized`); +export function encodePubkey(pubkey: Pubkey): Any { + if (isSecp256k1Pubkey(pubkey)) { + const pubkeyProto = PubKey.fromPartial({ + key: fromBase64(pubkey.value), + }); + return Any.fromPartial({ + typeUrl: "/cosmos.crypto.secp256k1.PubKey", + value: Uint8Array.from(PubKey.encode(pubkeyProto).finish()), + }); + } else if (isMultisigThresholdPubkey(pubkey)) { + const pubkeyProto = LegacyAminoPubKey.fromPartial({ + threshold: Uint53.fromString(pubkey.value.threshold).toNumber(), + publicKeys: pubkey.value.pubkeys.map(encodePubkey), + }); + return Any.fromPartial({ + typeUrl: "/cosmos.crypto.multisig.LegacyAminoPubKey", + value: Uint8Array.from(LegacyAminoPubKey.encode(pubkeyProto).finish()), + }); + } else { + throw new Error(`Pubkey type ${pubkey.type} not recognized`); } } -export function decodePubkey(pubkey?: Any | null): AminoPubKey | null { +export function decodePubkey(pubkey?: Any | null): SinglePubkey | null { if (!pubkey || !pubkey.value) { return null; } From c3470dc90aab05a30021bd2e9b1e0fed65339e82 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 23:43:07 +0100 Subject: [PATCH 07/19] Allow using signDirect/signAmino directly and add SignData --- .../stargate/src/signingstargateclient.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index aaf4020d..1b8f4783 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -98,6 +98,12 @@ function createDefaultRegistry(): Registry { return new Registry(defaultRegistryTypes); } +export interface SignData { + readonly accountNumber: number; + readonly sequence: number; + readonly chainId: string; +} + /** Use for testing only */ export interface PrivateSigningStargateClient { readonly fees: CosmosFeeTable; @@ -186,16 +192,25 @@ export class SigningStargateClient extends StargateClient { fee: StdFee, memo: string, ): Promise { + const accountFromChain = await this.getAccountUnverified(signerAddress); + if (!accountFromChain) { + throw new Error("Account not found"); + } + const { accountNumber, sequence } = accountFromChain; + const chainId = await this.getChainId(); + const signData: SignData = { accountNumber, sequence, chainId }; + return isOfflineDirectSigner(this.signer) - ? this.signDirect(signerAddress, messages, fee, memo) - : this.signAmino(signerAddress, messages, fee, memo); + ? this.signDirect(signerAddress, messages, fee, memo, signData) + : this.signAmino(signerAddress, messages, fee, memo, signData); } - private async signAmino( + public async signAmino( signerAddress: string, messages: readonly EncodeObject[], fee: StdFee, memo: string, + { accountNumber, sequence, chainId }: SignData, ): Promise { assert(!isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( @@ -205,12 +220,6 @@ export class SigningStargateClient extends StargateClient { throw new Error("Failed to retrieve account from signer"); } const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)); - const accountFromChain = await this.getAccountUnverified(signerAddress); - if (!accountFromChain) { - throw new Error("Account not found"); - } - const { accountNumber, sequence } = accountFromChain; - const chainId = await this.getChainId(); const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON; const msgs = messages.map((msg) => this.aminoTypes.toAmino(msg)); const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence); @@ -239,11 +248,12 @@ export class SigningStargateClient extends StargateClient { }); } - private async signDirect( + public async signDirect( signerAddress: string, messages: readonly EncodeObject[], fee: StdFee, memo: string, + { accountNumber, sequence, chainId }: SignData, ): Promise { assert(isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( @@ -253,12 +263,6 @@ export class SigningStargateClient extends StargateClient { throw new Error("Failed to retrieve account from signer"); } const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)); - const accountFromChain = await this.getAccountUnverified(signerAddress); - if (!accountFromChain) { - throw new Error("Account not found"); - } - const { accountNumber, sequence } = accountFromChain; - const chainId = await this.getChainId(); const txBody = { messages: messages, memo: memo, From b8570f83b3e89ca9e7f7ec5ce615685540123d42 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 18:51:11 +0100 Subject: [PATCH 08/19] Add multisig test --- .../signingstargateclient.multisig.spec.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 packages/stargate/src/signingstargateclient.multisig.spec.ts diff --git a/packages/stargate/src/signingstargateclient.multisig.spec.ts b/packages/stargate/src/signingstargateclient.multisig.spec.ts new file mode 100644 index 00000000..5b2b21dd --- /dev/null +++ b/packages/stargate/src/signingstargateclient.multisig.spec.ts @@ -0,0 +1,117 @@ +import { createMultisigThresholdPubkey, encodeSecp256k1Pubkey, pubkeyToAddress } from "@cosmjs/amino"; +import { coins, makeCosmoshubPath, Secp256k1HdWallet } from "@cosmjs/launchpad"; +import { encodePubkey } from "@cosmjs/proto-signing"; +import { MultiSignature } from "@cosmjs/proto-signing/build/codec/cosmos/crypto/multisig/v1beta1/multisig"; +import { assert } from "@cosmjs/utils"; +import Long from "long"; + +import { MsgSend } from "./codec/cosmos/bank/v1beta1/tx"; +import { CompactBitArray } from "./codec/cosmos/crypto/multisig/v1beta1/multisig"; +import { SignMode } from "./codec/cosmos/tx/signing/v1beta1/signing"; +import { AuthInfo, SignerInfo, TxRaw } from "./codec/cosmos/tx/v1beta1/tx"; +import { SigningStargateClient } from "./signingstargateclient"; +import { assertIsBroadcastTxSuccess } from "./stargateclient"; +import { faucet, makeRandomAddress, simapp } from "./testutils.spec"; + +describe("SigningStargateClient multisig", () => { + it("works", async () => { + const wallet0 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); + const wallet1 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); + const wallet2 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(2)); + const wallet3 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(3)); + const wallet4 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(4)); + const pubkey0 = encodeSecp256k1Pubkey((await wallet0.getAccounts())[0].pubkey); + const pubkey1 = encodeSecp256k1Pubkey((await wallet1.getAccounts())[0].pubkey); + const pubkey2 = encodeSecp256k1Pubkey((await wallet2.getAccounts())[0].pubkey); + const pubkey3 = encodeSecp256k1Pubkey((await wallet3.getAccounts())[0].pubkey); + const pubkey4 = encodeSecp256k1Pubkey((await wallet4.getAccounts())[0].pubkey); + const multisigPubkey = createMultisigThresholdPubkey([pubkey0, pubkey1, pubkey2, pubkey3, pubkey4], 2); + const multisigAddress = pubkeyToAddress(multisigPubkey, "cosmos"); + expect(multisigAddress).toEqual("cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"); + + const client0 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet0); + const client1 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet1); + const client2 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet2); + const client3 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet3); + const client4 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet4); + + const msgSend: MsgSend = { + fromAddress: multisigAddress, + toAddress: makeRandomAddress(), + amount: coins(1234, "ucosm"), + }; + const msg = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const gasLimit = 200000; + const fee = { + amount: coins(2000, "ucosm"), + gas: gasLimit.toString(), + }; + const memo = "Use your tokens wisely"; + + const multisigAccount = await client0.getAccount(multisigAddress); + assert(multisigAccount, "Account does not exist on chain"); + const signData = { + accountNumber: multisigAccount.accountNumber, + sequence: multisigAccount.sequence, + chainId: await client0.getChainId(), + }; + + const { + bodyBytes, + signatures: [signature0], + } = await client0.signAmino(faucet.address0, [msg], fee, memo, signData); + const { + signatures: [_signature1], + } = await client1.signAmino(faucet.address1, [msg], fee, memo, signData); + const { + signatures: [_signature2], + } = await client2.signAmino(faucet.address2, [msg], fee, memo, signData); + const { + signatures: [_signature3], + } = await client3.signAmino(faucet.address3, [msg], fee, memo, signData); + const { + signatures: [signature4], + } = await client4.signAmino(faucet.address4, [msg], fee, memo, signData); + + const multisignature = MultiSignature.fromPartial({ signatures: [signature0, signature4] }); + + const signerInfo: SignerInfo = { + publicKey: encodePubkey(multisigPubkey), + modeInfo: { + multi: { + bitarray: CompactBitArray.fromPartial({ + elems: new Uint8Array([0b10001000]), + extraBitsStored: 5, + }), + modeInfos: [ + { single: { mode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON } }, + { single: { mode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON } }, + ], + }, + }, + sequence: Long.fromNumber(multisigAccount.sequence), + }; + + const authInfo = { + signerInfos: [signerInfo], + fee: { + amount: [...fee.amount], + gasLimit: Long.fromNumber(gasLimit), + }, + }; + + const authInfoBytes = AuthInfo.encode(AuthInfo.fromPartial(authInfo)).finish(); + const signedTx = TxRaw.fromPartial({ + bodyBytes: bodyBytes, + authInfoBytes: authInfoBytes, + signatures: [MultiSignature.encode(multisignature).finish()], + }); + + // ensure signature is valid + const result = await client0.broadcastTx(Uint8Array.from(TxRaw.encode(signedTx).finish())); + assertIsBroadcastTxSuccess(result); + }); +}); From dc052d8229f805c9bb1eee36bf6e28242b8f331b Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 23 Mar 2021 23:52:28 +0100 Subject: [PATCH 09/19] Support LegacyAminoPubKey in decodePubkey --- packages/proto-signing/src/pubkey.ts | 32 +++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/proto-signing/src/pubkey.ts b/packages/proto-signing/src/pubkey.ts index ebf9f0c5..b04c1673 100644 --- a/packages/proto-signing/src/pubkey.ts +++ b/packages/proto-signing/src/pubkey.ts @@ -3,6 +3,7 @@ import { encodeSecp256k1Pubkey, isMultisigThresholdPubkey, isSecp256k1Pubkey, + MultisigThresholdPubkey, Pubkey, SinglePubkey, } from "@cosmjs/amino"; @@ -36,16 +37,37 @@ export function encodePubkey(pubkey: Pubkey): Any { } } -export function decodePubkey(pubkey?: Any | null): SinglePubkey | null { - if (!pubkey || !pubkey.value) { - return null; - } - +function decodeSinglePubkey(pubkey: Any): SinglePubkey { switch (pubkey.typeUrl) { case "/cosmos.crypto.secp256k1.PubKey": { const { key } = PubKey.decode(pubkey.value); return encodeSecp256k1Pubkey(key); } + default: + throw new Error(`Pubkey type_url ${pubkey.typeUrl} not recognized as single public key type`); + } +} + +export function decodePubkey(pubkey?: Any | null): Pubkey | null { + if (!pubkey || !pubkey.value) { + return null; + } + + switch (pubkey.typeUrl) { + case "/cosmos.crypto.secp256k1.PubKey": { + return decodeSinglePubkey(pubkey); + } + case "/cosmos.crypto.multisig.LegacyAminoPubKey": { + const { threshold, publicKeys } = LegacyAminoPubKey.decode(pubkey.value); + const out: MultisigThresholdPubkey = { + type: "tendermint/PubKeyMultisigThreshold", + value: { + threshold: threshold.toString(), + pubkeys: publicKeys.map(decodeSinglePubkey), + }, + }; + return out; + } default: throw new Error(`Pubkey type_url ${pubkey.typeUrl} not recognized`); } From 447b82b66f3824bf15336b82268db7b72a477183 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 24 Mar 2021 01:06:36 +0100 Subject: [PATCH 10/19] Pull out makeMultisignedTx --- .../signingstargateclient.multisig.spec.ts | 122 ++++++++++++------ 1 file changed, 81 insertions(+), 41 deletions(-) diff --git a/packages/stargate/src/signingstargateclient.multisig.spec.ts b/packages/stargate/src/signingstargateclient.multisig.spec.ts index 5b2b21dd..c1e7eb18 100644 --- a/packages/stargate/src/signingstargateclient.multisig.spec.ts +++ b/packages/stargate/src/signingstargateclient.multisig.spec.ts @@ -1,4 +1,10 @@ -import { createMultisigThresholdPubkey, encodeSecp256k1Pubkey, pubkeyToAddress } from "@cosmjs/amino"; +import { + createMultisigThresholdPubkey, + encodeSecp256k1Pubkey, + MultisigThresholdPubkey, + pubkeyToAddress, +} from "@cosmjs/amino"; +import { Bech32 } from "@cosmjs/encoding"; import { coins, makeCosmoshubPath, Secp256k1HdWallet } from "@cosmjs/launchpad"; import { encodePubkey } from "@cosmjs/proto-signing"; import { MultiSignature } from "@cosmjs/proto-signing/build/codec/cosmos/crypto/multisig/v1beta1/multisig"; @@ -9,9 +15,63 @@ import { MsgSend } from "./codec/cosmos/bank/v1beta1/tx"; import { CompactBitArray } from "./codec/cosmos/crypto/multisig/v1beta1/multisig"; import { SignMode } from "./codec/cosmos/tx/signing/v1beta1/signing"; import { AuthInfo, SignerInfo, TxRaw } from "./codec/cosmos/tx/v1beta1/tx"; +import { StdFee } from "./fee"; import { SigningStargateClient } from "./signingstargateclient"; import { assertIsBroadcastTxSuccess } from "./stargateclient"; -import { faucet, makeRandomAddress, simapp } from "./testutils.spec"; +import { faucet, simapp } from "./testutils.spec"; + +function makeMultisignedTx( + multisigPubkey: MultisigThresholdPubkey, + sequence: number, + fee: StdFee, + bodyBytes: Uint8Array, + signatures: Map, +): TxRaw { + const addresses = Array.from(signatures.keys()); + const prefix = Bech32.decode(addresses[0]).prefix; + + let bits = 0; + const signaturesList = new Array(); + for (let i = 0; i < multisigPubkey.value.pubkeys.length; i++) { + const signerAddress = pubkeyToAddress(multisigPubkey.value.pubkeys[i], prefix); + const signature = signatures.get(signerAddress); + if (signature) { + // eslint-disable-next-line no-bitwise + bits |= 0b1 << (8 - 1 - i); + signaturesList.push(signature); + } + } + + const signerInfo: SignerInfo = { + publicKey: encodePubkey(multisigPubkey), + modeInfo: { + multi: { + bitarray: CompactBitArray.fromPartial({ + elems: new Uint8Array([bits]), + extraBitsStored: multisigPubkey.value.pubkeys.length, + }), + modeInfos: signaturesList.map((_) => ({ single: { mode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON } })), + }, + }, + sequence: Long.fromNumber(sequence), + }; + + const authInfo = AuthInfo.fromPartial({ + signerInfos: [signerInfo], + fee: { + amount: [...fee.amount], + gasLimit: Long.fromString(fee.gas), + }, + }); + + const authInfoBytes = AuthInfo.encode(authInfo).finish(); + const signedTx = TxRaw.fromPartial({ + bodyBytes: bodyBytes, + authInfoBytes: authInfoBytes, + signatures: [MultiSignature.encode(MultiSignature.fromPartial({ signatures: signaturesList })).finish()], + }); + return signedTx; +} describe("SigningStargateClient multisig", () => { it("works", async () => { @@ -25,6 +85,11 @@ describe("SigningStargateClient multisig", () => { const pubkey2 = encodeSecp256k1Pubkey((await wallet2.getAccounts())[0].pubkey); const pubkey3 = encodeSecp256k1Pubkey((await wallet3.getAccounts())[0].pubkey); const pubkey4 = encodeSecp256k1Pubkey((await wallet4.getAccounts())[0].pubkey); + const address0 = (await wallet0.getAccounts())[0].address; + const address1 = (await wallet1.getAccounts())[0].address; + const address2 = (await wallet2.getAccounts())[0].address; + const address3 = (await wallet3.getAccounts())[0].address; + const address4 = (await wallet4.getAccounts())[0].address; const multisigPubkey = createMultisigThresholdPubkey([pubkey0, pubkey1, pubkey2, pubkey3, pubkey4], 2); const multisigAddress = pubkeyToAddress(multisigPubkey, "cosmos"); expect(multisigAddress).toEqual("cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"); @@ -37,7 +102,7 @@ describe("SigningStargateClient multisig", () => { const msgSend: MsgSend = { fromAddress: multisigAddress, - toAddress: makeRandomAddress(), + toAddress: "cosmos19rvl6ja9h0erq9dc2xxfdzypc739ej8k5esnhg", amount: coins(1234, "ucosm"), }; const msg = { @@ -64,54 +129,29 @@ describe("SigningStargateClient multisig", () => { signatures: [signature0], } = await client0.signAmino(faucet.address0, [msg], fee, memo, signData); const { - signatures: [_signature1], + signatures: [signature1], } = await client1.signAmino(faucet.address1, [msg], fee, memo, signData); const { - signatures: [_signature2], + signatures: [signature2], } = await client2.signAmino(faucet.address2, [msg], fee, memo, signData); const { - signatures: [_signature3], + signatures: [signature3], } = await client3.signAmino(faucet.address3, [msg], fee, memo, signData); const { signatures: [signature4], } = await client4.signAmino(faucet.address4, [msg], fee, memo, signData); - const multisignature = MultiSignature.fromPartial({ signatures: [signature0, signature4] }); - - const signerInfo: SignerInfo = { - publicKey: encodePubkey(multisigPubkey), - modeInfo: { - multi: { - bitarray: CompactBitArray.fromPartial({ - elems: new Uint8Array([0b10001000]), - extraBitsStored: 5, - }), - modeInfos: [ - { single: { mode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON } }, - { single: { mode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON } }, - ], - }, - }, - sequence: Long.fromNumber(multisigAccount.sequence), - }; - - const authInfo = { - signerInfos: [signerInfo], - fee: { - amount: [...fee.amount], - gasLimit: Long.fromNumber(gasLimit), - }, - }; - - const authInfoBytes = AuthInfo.encode(AuthInfo.fromPartial(authInfo)).finish(); - const signedTx = TxRaw.fromPartial({ - bodyBytes: bodyBytes, - authInfoBytes: authInfoBytes, - signatures: [MultiSignature.encode(multisignature).finish()], - }); + const signatures = new Map([ + [address0, signature0], + [address1, signature1], + [address2, signature2], + [address3, signature3], + [address4, signature4], + ]); + const signedTx = makeMultisignedTx(multisigPubkey, multisigAccount.sequence, fee, bodyBytes, signatures); // ensure signature is valid - const result = await client0.broadcastTx(Uint8Array.from(TxRaw.encode(signedTx).finish())); - assertIsBroadcastTxSuccess(result); + const result04 = await client0.broadcastTx(Uint8Array.from(TxRaw.encode(signedTx).finish())); + assertIsBroadcastTxSuccess(result04); }); }); From 22f25a0f9dc5a17101be22c2fd9b0264026dc4d9 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 24 Mar 2021 10:05:23 +0100 Subject: [PATCH 11/19] Rename type to SignerData and make it public --- packages/stargate/src/index.ts | 1 + .../src/signingstargateclient.multisig.spec.ts | 14 +++++++------- packages/stargate/src/signingstargateclient.ts | 17 +++++++++++------ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/stargate/src/index.ts b/packages/stargate/src/index.ts index 384f3d82..a7096ec8 100644 --- a/packages/stargate/src/index.ts +++ b/packages/stargate/src/index.ts @@ -34,6 +34,7 @@ export { } from "./stargateclient"; export { defaultRegistryTypes, + SignerData, SigningStargateClient, SigningStargateClientOptions, } from "./signingstargateclient"; diff --git a/packages/stargate/src/signingstargateclient.multisig.spec.ts b/packages/stargate/src/signingstargateclient.multisig.spec.ts index c1e7eb18..70b6d660 100644 --- a/packages/stargate/src/signingstargateclient.multisig.spec.ts +++ b/packages/stargate/src/signingstargateclient.multisig.spec.ts @@ -16,7 +16,7 @@ import { CompactBitArray } from "./codec/cosmos/crypto/multisig/v1beta1/multisig import { SignMode } from "./codec/cosmos/tx/signing/v1beta1/signing"; import { AuthInfo, SignerInfo, TxRaw } from "./codec/cosmos/tx/v1beta1/tx"; import { StdFee } from "./fee"; -import { SigningStargateClient } from "./signingstargateclient"; +import { SignerData, SigningStargateClient } from "./signingstargateclient"; import { assertIsBroadcastTxSuccess } from "./stargateclient"; import { faucet, simapp } from "./testutils.spec"; @@ -118,7 +118,7 @@ describe("SigningStargateClient multisig", () => { const multisigAccount = await client0.getAccount(multisigAddress); assert(multisigAccount, "Account does not exist on chain"); - const signData = { + const signerData: SignerData = { accountNumber: multisigAccount.accountNumber, sequence: multisigAccount.sequence, chainId: await client0.getChainId(), @@ -127,19 +127,19 @@ describe("SigningStargateClient multisig", () => { const { bodyBytes, signatures: [signature0], - } = await client0.signAmino(faucet.address0, [msg], fee, memo, signData); + } = await client0.signAmino(faucet.address0, [msg], fee, memo, signerData); const { signatures: [signature1], - } = await client1.signAmino(faucet.address1, [msg], fee, memo, signData); + } = await client1.signAmino(faucet.address1, [msg], fee, memo, signerData); const { signatures: [signature2], - } = await client2.signAmino(faucet.address2, [msg], fee, memo, signData); + } = await client2.signAmino(faucet.address2, [msg], fee, memo, signerData); const { signatures: [signature3], - } = await client3.signAmino(faucet.address3, [msg], fee, memo, signData); + } = await client3.signAmino(faucet.address3, [msg], fee, memo, signerData); const { signatures: [signature4], - } = await client4.signAmino(faucet.address4, [msg], fee, memo, signData); + } = await client4.signAmino(faucet.address4, [msg], fee, memo, signerData); const signatures = new Map([ [address0, signature0], diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index 1b8f4783..1c66ba19 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -98,7 +98,12 @@ function createDefaultRegistry(): Registry { return new Registry(defaultRegistryTypes); } -export interface SignData { +/** + * Signing information for a single signer that is not included in the transaction. + * + * @see https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/x/auth/signing/sign_mode_handler.go#L23-L37 + */ +export interface SignerData { readonly accountNumber: number; readonly sequence: number; readonly chainId: string; @@ -198,11 +203,11 @@ export class SigningStargateClient extends StargateClient { } const { accountNumber, sequence } = accountFromChain; const chainId = await this.getChainId(); - const signData: SignData = { accountNumber, sequence, chainId }; + const signerData: SignerData = { accountNumber, sequence, chainId }; return isOfflineDirectSigner(this.signer) - ? this.signDirect(signerAddress, messages, fee, memo, signData) - : this.signAmino(signerAddress, messages, fee, memo, signData); + ? this.signDirect(signerAddress, messages, fee, memo, signerData) + : this.signAmino(signerAddress, messages, fee, memo, signerData); } public async signAmino( @@ -210,7 +215,7 @@ export class SigningStargateClient extends StargateClient { messages: readonly EncodeObject[], fee: StdFee, memo: string, - { accountNumber, sequence, chainId }: SignData, + { accountNumber, sequence, chainId }: SignerData, ): Promise { assert(!isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( @@ -253,7 +258,7 @@ export class SigningStargateClient extends StargateClient { messages: readonly EncodeObject[], fee: StdFee, memo: string, - { accountNumber, sequence, chainId }: SignData, + { accountNumber, sequence, chainId }: SignerData, ): Promise { assert(isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( From 7eb53ea17c2bc450a7ef875122e7dd3a8ebc948d Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 24 Mar 2021 10:11:35 +0100 Subject: [PATCH 12/19] Support explicit signer data in SigningStargateClient.sign --- .../signingstargateclient.multisig.spec.ts | 10 +++---- .../stargate/src/signingstargateclient.ts | 26 +++++++++++++------ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/stargate/src/signingstargateclient.multisig.spec.ts b/packages/stargate/src/signingstargateclient.multisig.spec.ts index 70b6d660..117ea680 100644 --- a/packages/stargate/src/signingstargateclient.multisig.spec.ts +++ b/packages/stargate/src/signingstargateclient.multisig.spec.ts @@ -127,19 +127,19 @@ describe("SigningStargateClient multisig", () => { const { bodyBytes, signatures: [signature0], - } = await client0.signAmino(faucet.address0, [msg], fee, memo, signerData); + } = await client0.sign(faucet.address0, [msg], fee, memo, signerData); const { signatures: [signature1], - } = await client1.signAmino(faucet.address1, [msg], fee, memo, signerData); + } = await client1.sign(faucet.address1, [msg], fee, memo, signerData); const { signatures: [signature2], - } = await client2.signAmino(faucet.address2, [msg], fee, memo, signerData); + } = await client2.sign(faucet.address2, [msg], fee, memo, signerData); const { signatures: [signature3], - } = await client3.signAmino(faucet.address3, [msg], fee, memo, signerData); + } = await client3.sign(faucet.address3, [msg], fee, memo, signerData); const { signatures: [signature4], - } = await client4.signAmino(faucet.address4, [msg], fee, memo, signerData); + } = await client4.sign(faucet.address4, [msg], fee, memo, signerData); const signatures = new Map([ [address0, signature0], diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index 1c66ba19..c505b1f9 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -190,27 +190,37 @@ export class SigningStargateClient extends StargateClient { * creates a single signature and assembles the signed transaction. * * The sign mode (SIGN_MODE_DIRECT or SIGN_MODE_LEGACY_AMINO_JSON) is determined by this client's signer. + * + * You can pass signer data (account number, sequence and chain ID) explicitly instead of querying them + * from the chain. This is needed when signing for a multisig account, but it also allows for offline signing + * (which probably fails right now because in other places SigningStargateClient assumes you are online). */ public async sign( signerAddress: string, messages: readonly EncodeObject[], fee: StdFee, memo: string, + explicitSignerData?: SignerData, ): Promise { - const accountFromChain = await this.getAccountUnverified(signerAddress); - if (!accountFromChain) { - throw new Error("Account not found"); + let signerData: SignerData; + if (explicitSignerData) { + signerData = explicitSignerData; + } else { + const accountFromChain = await this.getAccountUnverified(signerAddress); + if (!accountFromChain) { + throw new Error("Account not found"); + } + const { accountNumber, sequence } = accountFromChain; + const chainId = await this.getChainId(); + signerData = { accountNumber, sequence, chainId }; } - const { accountNumber, sequence } = accountFromChain; - const chainId = await this.getChainId(); - const signerData: SignerData = { accountNumber, sequence, chainId }; return isOfflineDirectSigner(this.signer) ? this.signDirect(signerAddress, messages, fee, memo, signerData) : this.signAmino(signerAddress, messages, fee, memo, signerData); } - public async signAmino( + private async signAmino( signerAddress: string, messages: readonly EncodeObject[], fee: StdFee, @@ -253,7 +263,7 @@ export class SigningStargateClient extends StargateClient { }); } - public async signDirect( + private async signDirect( signerAddress: string, messages: readonly EncodeObject[], fee: StdFee, From 5b8d3637cee5bf0a2b155113ded3d844e721c5f7 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 24 Mar 2021 10:36:41 +0100 Subject: [PATCH 13/19] Move makeMultisignedTx into separate module --- packages/stargate/src/multisignature.spec.ts | 102 ++++++++++++ packages/stargate/src/multisignature.ts | 63 +++++++ .../signingstargateclient.multisig.spec.ts | 157 ------------------ 3 files changed, 165 insertions(+), 157 deletions(-) create mode 100644 packages/stargate/src/multisignature.spec.ts create mode 100644 packages/stargate/src/multisignature.ts delete mode 100644 packages/stargate/src/signingstargateclient.multisig.spec.ts diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts new file mode 100644 index 00000000..5805a95b --- /dev/null +++ b/packages/stargate/src/multisignature.spec.ts @@ -0,0 +1,102 @@ +import { createMultisigThresholdPubkey, encodeSecp256k1Pubkey, pubkeyToAddress } from "@cosmjs/amino"; +import { coins, makeCosmoshubPath, Secp256k1HdWallet } from "@cosmjs/launchpad"; +import { assert } from "@cosmjs/utils"; + +import { MsgSend } from "./codec/cosmos/bank/v1beta1/tx"; +import { TxRaw } from "./codec/cosmos/tx/v1beta1/tx"; +import { makeMultisignedTx } from "./multisignature"; +import { SignerData, SigningStargateClient } from "./signingstargateclient"; +import { assertIsBroadcastTxSuccess } from "./stargateclient"; +import { faucet, pendingWithoutSimapp, simapp } from "./testutils.spec"; + +describe("multisignature", () => { + describe("makeMultisignedTx", () => { + it("works", async () => { + pendingWithoutSimapp(); + const wallet0 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); + const wallet1 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); + const wallet2 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(2)); + const wallet3 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(3)); + const wallet4 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(4)); + const pubkey0 = encodeSecp256k1Pubkey((await wallet0.getAccounts())[0].pubkey); + const pubkey1 = encodeSecp256k1Pubkey((await wallet1.getAccounts())[0].pubkey); + const pubkey2 = encodeSecp256k1Pubkey((await wallet2.getAccounts())[0].pubkey); + const pubkey3 = encodeSecp256k1Pubkey((await wallet3.getAccounts())[0].pubkey); + const pubkey4 = encodeSecp256k1Pubkey((await wallet4.getAccounts())[0].pubkey); + const address0 = (await wallet0.getAccounts())[0].address; + const address1 = (await wallet1.getAccounts())[0].address; + const address2 = (await wallet2.getAccounts())[0].address; + const address3 = (await wallet3.getAccounts())[0].address; + const address4 = (await wallet4.getAccounts())[0].address; + const multisigPubkey = createMultisigThresholdPubkey([pubkey0, pubkey1, pubkey2, pubkey3, pubkey4], 2); + const multisigAddress = pubkeyToAddress(multisigPubkey, "cosmos"); + expect(multisigAddress).toEqual("cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"); + + const client0 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet0); + const client1 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet1); + const client2 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet2); + const client3 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet3); + const client4 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet4); + + const msgSend: MsgSend = { + fromAddress: multisigAddress, + toAddress: "cosmos19rvl6ja9h0erq9dc2xxfdzypc739ej8k5esnhg", + amount: coins(1234, "ucosm"), + }; + const msg = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const gasLimit = 200000; + const fee = { + amount: coins(2000, "ucosm"), + gas: gasLimit.toString(), + }; + const memo = "Use your tokens wisely"; + + const multisigAccount = await client0.getAccount(multisigAddress); + assert(multisigAccount, "Account does not exist on chain"); + const signerData: SignerData = { + accountNumber: multisigAccount.accountNumber, + sequence: multisigAccount.sequence, + chainId: await client0.getChainId(), + }; + + const { + bodyBytes, + signatures: [signature0], + } = await client0.sign(faucet.address0, [msg], fee, memo, signerData); + const { + signatures: [signature1], + } = await client1.sign(faucet.address1, [msg], fee, memo, signerData); + const { + signatures: [signature2], + } = await client2.sign(faucet.address2, [msg], fee, memo, signerData); + const { + signatures: [signature3], + } = await client3.sign(faucet.address3, [msg], fee, memo, signerData); + const { + signatures: [signature4], + } = await client4.sign(faucet.address4, [msg], fee, memo, signerData); + + const signatures = new Map([ + [address0, signature0], + [address1, signature1], + [address2, signature2], + [address3, signature3], + [address4, signature4], + ]); + const signedTx = makeMultisignedTx( + multisigPubkey, + multisigAccount.sequence, + fee, + bodyBytes, + signatures, + ); + + // ensure signature is valid + const result = await client0.broadcastTx(Uint8Array.from(TxRaw.encode(signedTx).finish())); + assertIsBroadcastTxSuccess(result); + }); + }); +}); diff --git a/packages/stargate/src/multisignature.ts b/packages/stargate/src/multisignature.ts new file mode 100644 index 00000000..de98e563 --- /dev/null +++ b/packages/stargate/src/multisignature.ts @@ -0,0 +1,63 @@ +import { Bech32 } from "@cosmjs/encoding"; +import { encodePubkey } from "@cosmjs/proto-signing"; +import Long from "long"; + +import { MultisigThresholdPubkey, pubkeyToAddress } from "../../amino/build"; +import { CompactBitArray, MultiSignature } from "./codec/cosmos/crypto/multisig/v1beta1/multisig"; +import { SignMode } from "./codec/cosmos/tx/signing/v1beta1/signing"; +import { AuthInfo, SignerInfo } from "./codec/cosmos/tx/v1beta1/tx"; +import { TxRaw } from "./codec/cosmos/tx/v1beta1/tx"; +import { StdFee } from "./fee"; + +export function makeMultisignedTx( + multisigPubkey: MultisigThresholdPubkey, + sequence: number, + fee: StdFee, + bodyBytes: Uint8Array, + signatures: Map, +): TxRaw { + const addresses = Array.from(signatures.keys()); + const prefix = Bech32.decode(addresses[0]).prefix; + + let bits = 0; + const signaturesList = new Array(); + for (let i = 0; i < multisigPubkey.value.pubkeys.length; i++) { + const signerAddress = pubkeyToAddress(multisigPubkey.value.pubkeys[i], prefix); + const signature = signatures.get(signerAddress); + if (signature) { + // eslint-disable-next-line no-bitwise + bits |= 0b1 << (8 - 1 - i); + signaturesList.push(signature); + } + } + + const signerInfo: SignerInfo = { + publicKey: encodePubkey(multisigPubkey), + modeInfo: { + multi: { + bitarray: CompactBitArray.fromPartial({ + elems: new Uint8Array([bits]), + extraBitsStored: multisigPubkey.value.pubkeys.length, + }), + modeInfos: signaturesList.map((_) => ({ single: { mode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON } })), + }, + }, + sequence: Long.fromNumber(sequence), + }; + + const authInfo = AuthInfo.fromPartial({ + signerInfos: [signerInfo], + fee: { + amount: [...fee.amount], + gasLimit: Long.fromString(fee.gas), + }, + }); + + const authInfoBytes = AuthInfo.encode(authInfo).finish(); + const signedTx = TxRaw.fromPartial({ + bodyBytes: bodyBytes, + authInfoBytes: authInfoBytes, + signatures: [MultiSignature.encode(MultiSignature.fromPartial({ signatures: signaturesList })).finish()], + }); + return signedTx; +} diff --git a/packages/stargate/src/signingstargateclient.multisig.spec.ts b/packages/stargate/src/signingstargateclient.multisig.spec.ts deleted file mode 100644 index 117ea680..00000000 --- a/packages/stargate/src/signingstargateclient.multisig.spec.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { - createMultisigThresholdPubkey, - encodeSecp256k1Pubkey, - MultisigThresholdPubkey, - pubkeyToAddress, -} from "@cosmjs/amino"; -import { Bech32 } from "@cosmjs/encoding"; -import { coins, makeCosmoshubPath, Secp256k1HdWallet } from "@cosmjs/launchpad"; -import { encodePubkey } from "@cosmjs/proto-signing"; -import { MultiSignature } from "@cosmjs/proto-signing/build/codec/cosmos/crypto/multisig/v1beta1/multisig"; -import { assert } from "@cosmjs/utils"; -import Long from "long"; - -import { MsgSend } from "./codec/cosmos/bank/v1beta1/tx"; -import { CompactBitArray } from "./codec/cosmos/crypto/multisig/v1beta1/multisig"; -import { SignMode } from "./codec/cosmos/tx/signing/v1beta1/signing"; -import { AuthInfo, SignerInfo, TxRaw } from "./codec/cosmos/tx/v1beta1/tx"; -import { StdFee } from "./fee"; -import { SignerData, SigningStargateClient } from "./signingstargateclient"; -import { assertIsBroadcastTxSuccess } from "./stargateclient"; -import { faucet, simapp } from "./testutils.spec"; - -function makeMultisignedTx( - multisigPubkey: MultisigThresholdPubkey, - sequence: number, - fee: StdFee, - bodyBytes: Uint8Array, - signatures: Map, -): TxRaw { - const addresses = Array.from(signatures.keys()); - const prefix = Bech32.decode(addresses[0]).prefix; - - let bits = 0; - const signaturesList = new Array(); - for (let i = 0; i < multisigPubkey.value.pubkeys.length; i++) { - const signerAddress = pubkeyToAddress(multisigPubkey.value.pubkeys[i], prefix); - const signature = signatures.get(signerAddress); - if (signature) { - // eslint-disable-next-line no-bitwise - bits |= 0b1 << (8 - 1 - i); - signaturesList.push(signature); - } - } - - const signerInfo: SignerInfo = { - publicKey: encodePubkey(multisigPubkey), - modeInfo: { - multi: { - bitarray: CompactBitArray.fromPartial({ - elems: new Uint8Array([bits]), - extraBitsStored: multisigPubkey.value.pubkeys.length, - }), - modeInfos: signaturesList.map((_) => ({ single: { mode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON } })), - }, - }, - sequence: Long.fromNumber(sequence), - }; - - const authInfo = AuthInfo.fromPartial({ - signerInfos: [signerInfo], - fee: { - amount: [...fee.amount], - gasLimit: Long.fromString(fee.gas), - }, - }); - - const authInfoBytes = AuthInfo.encode(authInfo).finish(); - const signedTx = TxRaw.fromPartial({ - bodyBytes: bodyBytes, - authInfoBytes: authInfoBytes, - signatures: [MultiSignature.encode(MultiSignature.fromPartial({ signatures: signaturesList })).finish()], - }); - return signedTx; -} - -describe("SigningStargateClient multisig", () => { - it("works", async () => { - const wallet0 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const wallet1 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); - const wallet2 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(2)); - const wallet3 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(3)); - const wallet4 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(4)); - const pubkey0 = encodeSecp256k1Pubkey((await wallet0.getAccounts())[0].pubkey); - const pubkey1 = encodeSecp256k1Pubkey((await wallet1.getAccounts())[0].pubkey); - const pubkey2 = encodeSecp256k1Pubkey((await wallet2.getAccounts())[0].pubkey); - const pubkey3 = encodeSecp256k1Pubkey((await wallet3.getAccounts())[0].pubkey); - const pubkey4 = encodeSecp256k1Pubkey((await wallet4.getAccounts())[0].pubkey); - const address0 = (await wallet0.getAccounts())[0].address; - const address1 = (await wallet1.getAccounts())[0].address; - const address2 = (await wallet2.getAccounts())[0].address; - const address3 = (await wallet3.getAccounts())[0].address; - const address4 = (await wallet4.getAccounts())[0].address; - const multisigPubkey = createMultisigThresholdPubkey([pubkey0, pubkey1, pubkey2, pubkey3, pubkey4], 2); - const multisigAddress = pubkeyToAddress(multisigPubkey, "cosmos"); - expect(multisigAddress).toEqual("cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"); - - const client0 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet0); - const client1 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet1); - const client2 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet2); - const client3 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet3); - const client4 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet4); - - const msgSend: MsgSend = { - fromAddress: multisigAddress, - toAddress: "cosmos19rvl6ja9h0erq9dc2xxfdzypc739ej8k5esnhg", - amount: coins(1234, "ucosm"), - }; - const msg = { - typeUrl: "/cosmos.bank.v1beta1.MsgSend", - value: msgSend, - }; - const gasLimit = 200000; - const fee = { - amount: coins(2000, "ucosm"), - gas: gasLimit.toString(), - }; - const memo = "Use your tokens wisely"; - - const multisigAccount = await client0.getAccount(multisigAddress); - assert(multisigAccount, "Account does not exist on chain"); - const signerData: SignerData = { - accountNumber: multisigAccount.accountNumber, - sequence: multisigAccount.sequence, - chainId: await client0.getChainId(), - }; - - const { - bodyBytes, - signatures: [signature0], - } = await client0.sign(faucet.address0, [msg], fee, memo, signerData); - const { - signatures: [signature1], - } = await client1.sign(faucet.address1, [msg], fee, memo, signerData); - const { - signatures: [signature2], - } = await client2.sign(faucet.address2, [msg], fee, memo, signerData); - const { - signatures: [signature3], - } = await client3.sign(faucet.address3, [msg], fee, memo, signerData); - const { - signatures: [signature4], - } = await client4.sign(faucet.address4, [msg], fee, memo, signerData); - - const signatures = new Map([ - [address0, signature0], - [address1, signature1], - [address2, signature2], - [address3, signature3], - [address4, signature4], - ]); - const signedTx = makeMultisignedTx(multisigPubkey, multisigAccount.sequence, fee, bodyBytes, signatures); - - // ensure signature is valid - const result04 = await client0.broadcastTx(Uint8Array.from(TxRaw.encode(signedTx).finish())); - assertIsBroadcastTxSuccess(result04); - }); -}); From 27f34273f01aa44a32a2399b4308a1721c451d43 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 24 Mar 2021 11:05:28 +0100 Subject: [PATCH 14/19] Pull out and test makeCompactBitArray --- packages/stargate/src/multisignature.spec.ts | 154 ++++++++++++++++++- packages/stargate/src/multisignature.ts | 25 ++- 2 files changed, 171 insertions(+), 8 deletions(-) diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index 5805a95b..1d4a090f 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -4,12 +4,164 @@ import { assert } from "@cosmjs/utils"; import { MsgSend } from "./codec/cosmos/bank/v1beta1/tx"; import { TxRaw } from "./codec/cosmos/tx/v1beta1/tx"; -import { makeMultisignedTx } from "./multisignature"; +import { makeCompactBitArray, makeMultisignedTx } from "./multisignature"; import { SignerData, SigningStargateClient } from "./signingstargateclient"; import { assertIsBroadcastTxSuccess } from "./stargateclient"; import { faucet, pendingWithoutSimapp, simapp } from "./testutils.spec"; describe("multisignature", () => { + describe("makeCompactBitArray", () => { + it("works for 0 bits of different lengths", () => { + expect(makeCompactBitArray([])).toEqual({ elems: new Uint8Array([]), extraBitsStored: 0 }); + expect(makeCompactBitArray([false])).toEqual({ + elems: new Uint8Array([0b00000000]), + extraBitsStored: 1, + }); + expect(makeCompactBitArray([false, false])).toEqual({ + elems: new Uint8Array([0b00000000]), + extraBitsStored: 2, + }); + expect(makeCompactBitArray([false, false, false])).toEqual({ + elems: new Uint8Array([0b00000000]), + extraBitsStored: 3, + }); + expect(makeCompactBitArray([false, false, false, false])).toEqual({ + elems: new Uint8Array([0b00000000]), + extraBitsStored: 4, + }); + expect(makeCompactBitArray([false, false, false, false, false])).toEqual({ + elems: new Uint8Array([0b00000000]), + extraBitsStored: 5, + }); + expect(makeCompactBitArray([false, false, false, false, false, false])).toEqual({ + elems: new Uint8Array([0b00000000]), + extraBitsStored: 6, + }); + expect(makeCompactBitArray([false, false, false, false, false, false, false])).toEqual({ + elems: new Uint8Array([0b00000000]), + extraBitsStored: 7, + }); + expect(makeCompactBitArray([false, false, false, false, false, false, false, false])).toEqual({ + elems: new Uint8Array([0b00000000]), + extraBitsStored: 0, + }); + expect(makeCompactBitArray([false, false, false, false, false, false, false, false, false])).toEqual({ + elems: new Uint8Array([0b00000000, 0b00000000]), + extraBitsStored: 1, + }); + expect( + makeCompactBitArray([false, false, false, false, false, false, false, false, false, false]), + ).toEqual({ elems: new Uint8Array([0b00000000, 0b00000000]), extraBitsStored: 2 }); + }); + + it("works for 1 bits of different lengths", () => { + expect(makeCompactBitArray([])).toEqual({ elems: new Uint8Array([]), extraBitsStored: 0 }); + expect(makeCompactBitArray([true])).toEqual({ + elems: new Uint8Array([0b10000000]), + extraBitsStored: 1, + }); + expect(makeCompactBitArray([true, true])).toEqual({ + elems: new Uint8Array([0b11000000]), + extraBitsStored: 2, + }); + expect(makeCompactBitArray([true, true, true])).toEqual({ + elems: new Uint8Array([0b11100000]), + extraBitsStored: 3, + }); + expect(makeCompactBitArray([true, true, true, true])).toEqual({ + elems: new Uint8Array([0b11110000]), + extraBitsStored: 4, + }); + expect(makeCompactBitArray([true, true, true, true, true])).toEqual({ + elems: new Uint8Array([0b11111000]), + extraBitsStored: 5, + }); + expect(makeCompactBitArray([true, true, true, true, true, true])).toEqual({ + elems: new Uint8Array([0b11111100]), + extraBitsStored: 6, + }); + expect(makeCompactBitArray([true, true, true, true, true, true, true])).toEqual({ + elems: new Uint8Array([0b11111110]), + extraBitsStored: 7, + }); + expect(makeCompactBitArray([true, true, true, true, true, true, true, true])).toEqual({ + elems: new Uint8Array([0b11111111]), + extraBitsStored: 0, + }); + expect(makeCompactBitArray([true, true, true, true, true, true, true, true, true])).toEqual({ + elems: new Uint8Array([0b11111111, 0b10000000]), + extraBitsStored: 1, + }); + expect(makeCompactBitArray([true, true, true, true, true, true, true, true, true, true])).toEqual({ + elems: new Uint8Array([0b11111111, 0b11000000]), + extraBitsStored: 2, + }); + }); + + it("works for 1 bit in different places", () => { + expect( + makeCompactBitArray([true, false, false, false, false, false, false, false, false, false]), + ).toEqual({ + elems: new Uint8Array([0b10000000, 0b00000000]), + extraBitsStored: 2, + }); + expect( + makeCompactBitArray([false, true, false, false, false, false, false, false, false, false]), + ).toEqual({ + elems: new Uint8Array([0b01000000, 0b00000000]), + extraBitsStored: 2, + }); + expect( + makeCompactBitArray([false, false, true, false, false, false, false, false, false, false]), + ).toEqual({ + elems: new Uint8Array([0b00100000, 0b00000000]), + extraBitsStored: 2, + }); + expect( + makeCompactBitArray([false, false, false, true, false, false, false, false, false, false]), + ).toEqual({ + elems: new Uint8Array([0b00010000, 0b00000000]), + extraBitsStored: 2, + }); + expect( + makeCompactBitArray([false, false, false, false, true, false, false, false, false, false]), + ).toEqual({ + elems: new Uint8Array([0b00001000, 0b00000000]), + extraBitsStored: 2, + }); + expect( + makeCompactBitArray([false, false, false, false, false, true, false, false, false, false]), + ).toEqual({ + elems: new Uint8Array([0b00000100, 0b00000000]), + extraBitsStored: 2, + }); + expect( + makeCompactBitArray([false, false, false, false, false, false, true, false, false, false]), + ).toEqual({ + elems: new Uint8Array([0b00000010, 0b00000000]), + extraBitsStored: 2, + }); + expect( + makeCompactBitArray([false, false, false, false, false, false, false, true, false, false]), + ).toEqual({ + elems: new Uint8Array([0b00000001, 0b00000000]), + extraBitsStored: 2, + }); + expect( + makeCompactBitArray([false, false, false, false, false, false, false, false, true, false]), + ).toEqual({ + elems: new Uint8Array([0b00000000, 0b10000000]), + extraBitsStored: 2, + }); + expect( + makeCompactBitArray([false, false, false, false, false, false, false, false, false, true]), + ).toEqual({ + elems: new Uint8Array([0b00000000, 0b01000000]), + extraBitsStored: 2, + }); + }); + }); + describe("makeMultisignedTx", () => { it("works", async () => { pendingWithoutSimapp(); diff --git a/packages/stargate/src/multisignature.ts b/packages/stargate/src/multisignature.ts index de98e563..08b579c8 100644 --- a/packages/stargate/src/multisignature.ts +++ b/packages/stargate/src/multisignature.ts @@ -9,6 +9,21 @@ import { AuthInfo, SignerInfo } from "./codec/cosmos/tx/v1beta1/tx"; import { TxRaw } from "./codec/cosmos/tx/v1beta1/tx"; import { StdFee } from "./fee"; +export function makeCompactBitArray(bits: readonly boolean[]): CompactBitArray { + const byteCount = Math.ceil(bits.length / 8); + const extraBits = bits.length - Math.floor(bits.length / 8) * 8; + const bytes = new Uint8Array(byteCount); // zero-filled + + bits.forEach((value, index) => { + const bytePos = Math.floor(index / 8); + const bitPos = index % 8; + // eslint-disable-next-line no-bitwise + if (value) bytes[bytePos] |= 0b1 << (8 - 1 - bitPos); + }); + + return CompactBitArray.fromPartial({ elems: bytes, extraBitsStored: extraBits }); +} + export function makeMultisignedTx( multisigPubkey: MultisigThresholdPubkey, sequence: number, @@ -19,14 +34,13 @@ export function makeMultisignedTx( const addresses = Array.from(signatures.keys()); const prefix = Bech32.decode(addresses[0]).prefix; - let bits = 0; + const signers: boolean[] = Array(multisigPubkey.value.pubkeys.length).fill(false); const signaturesList = new Array(); for (let i = 0; i < multisigPubkey.value.pubkeys.length; i++) { const signerAddress = pubkeyToAddress(multisigPubkey.value.pubkeys[i], prefix); const signature = signatures.get(signerAddress); if (signature) { - // eslint-disable-next-line no-bitwise - bits |= 0b1 << (8 - 1 - i); + signers[i] = true; signaturesList.push(signature); } } @@ -35,10 +49,7 @@ export function makeMultisignedTx( publicKey: encodePubkey(multisigPubkey), modeInfo: { multi: { - bitarray: CompactBitArray.fromPartial({ - elems: new Uint8Array([bits]), - extraBitsStored: multisigPubkey.value.pubkeys.length, - }), + bitarray: makeCompactBitArray(signers), modeInfos: signaturesList.map((_) => ({ single: { mode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON } })), }, }, From c5b33c415ab8b6e83dac4d08fc8e31559a88bf69 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 24 Mar 2021 13:38:46 +0100 Subject: [PATCH 15/19] Add SignerData to CHANGELOG --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aa30424..20708cff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,9 @@ and this project adheres to exports which have the same functionality as already existed in @cosmjs/launchpad. - @cosmjs/stargate: Add `SigningStargateClient.sign`, which allows you to create - signed transactions without broadcasting them directly. + signed transactions without broadcasting them directly. The new type + `SignerData` can be passed into `.sign` to skip querying account number, + sequence and chain ID. ### Changed From 40b1daf381830d5718cb2fbf6cfe95ac90435f9b Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 24 Mar 2021 13:30:14 +0100 Subject: [PATCH 16/19] Allow SigningStargateClient to work offline --- CHANGELOG.md | 2 + .../stargate/src/signingstargateclient.ts | 20 +++++++- packages/stargate/src/stargateclient.ts | 48 +++++++++++++------ 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20708cff..877e7d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ and this project adheres to signed transactions without broadcasting them directly. The new type `SignerData` can be passed into `.sign` to skip querying account number, sequence and chain ID. +- @cosmjs/stargate: Add constructor `SigningStargateClient.offline` which does + not connect to Tendermint. This allows offline signing. ### Changed diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index c505b1f9..c16d5a2d 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -139,8 +139,24 @@ export class SigningStargateClient extends StargateClient { return new SigningStargateClient(tmClient, signer, options); } + /** + * Creates a client in offline mode. + * + * This should only be used in niche cases where you know exactly what you're doing, + * e.g. when building an offline signing application. + * + * When you try to use online functionality with such a signer, an + * exception will be raised. + */ + public static async offline( + signer: OfflineSigner, + options: SigningStargateClientOptions = {}, + ): Promise { + return new SigningStargateClient(undefined, signer, options); + } + private constructor( - tmClient: Tendermint34Client, + tmClient: Tendermint34Client | undefined, signer: OfflineSigner, options: SigningStargateClientOptions, ) { @@ -193,7 +209,7 @@ export class SigningStargateClient extends StargateClient { * * You can pass signer data (account number, sequence and chain ID) explicitly instead of querying them * from the chain. This is needed when signing for a multisig account, but it also allows for offline signing - * (which probably fails right now because in other places SigningStargateClient assumes you are online). + * (See the SigningStargateClient.offline constructor). */ public async sign( signerAddress: string, diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index be1426d6..6ba5914a 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -90,8 +90,8 @@ export interface PrivateStargateClient { } export class StargateClient { - private readonly tmClient: Tendermint34Client; - private readonly queryClient: QueryClient & AuthExtension & BankExtension; + private readonly tmClient: Tendermint34Client | undefined; + private readonly queryClient: (QueryClient & AuthExtension & BankExtension) | undefined; private chainId: string | undefined; public static async connect(endpoint: string): Promise { @@ -99,14 +99,32 @@ export class StargateClient { return new StargateClient(tmClient); } - protected constructor(tmClient: Tendermint34Client) { - this.tmClient = tmClient; - this.queryClient = QueryClient.withExtensions(tmClient, setupAuthExtension, setupBankExtension); + protected constructor(tmClient: Tendermint34Client | undefined) { + if (tmClient) { + this.tmClient = tmClient; + this.queryClient = QueryClient.withExtensions(tmClient, setupAuthExtension, setupBankExtension); + } + } + + protected forceGetTmClient(): Tendermint34Client { + if (!this.tmClient) { + throw new Error( + "Tendermint client not available. You cannot use online functionality in offline mode.", + ); + } + return this.tmClient; + } + + protected forceGetQueryClient(): QueryClient & AuthExtension & BankExtension { + if (!this.queryClient) { + throw new Error("Query client not available. You cannot use online functionality in offline mode."); + } + return this.queryClient; } public async getChainId(): Promise { if (!this.chainId) { - const response = await this.tmClient.status(); + const response = await this.forceGetTmClient().status(); const chainId = response.nodeInfo.network; if (!chainId) throw new Error("Chain ID must not be empty"); this.chainId = chainId; @@ -116,20 +134,20 @@ export class StargateClient { } public async getHeight(): Promise { - const status = await this.tmClient.status(); + const status = await this.forceGetTmClient().status(); return status.syncInfo.latestBlockHeight; } // this is nice to display data to the user, but is slower public async getAccount(searchAddress: string): Promise { - const account = await this.queryClient.auth.account(searchAddress); + const account = await this.forceGetQueryClient().auth.account(searchAddress); return account ? accountFromAny(account) : null; } // if we just need to get the sequence for signing a transaction, let's make this faster // (no need to wait a block before submitting) public async getAccountUnverified(searchAddress: string): Promise { - const account = await this.queryClient.auth.unverified.account(searchAddress); + const account = await this.forceGetQueryClient().auth.unverified.account(searchAddress); return account ? accountFromAny(account) : null; } @@ -146,7 +164,7 @@ export class StargateClient { } public async getBlock(height?: number): Promise { - const response = await this.tmClient.block(height); + const response = await this.forceGetTmClient().block(height); return { id: toHex(response.blockId.hash).toUpperCase(), header: { @@ -163,7 +181,7 @@ export class StargateClient { } public async getBalance(address: string, searchDenom: string): Promise { - const balance = await this.queryClient.bank.balance(address, searchDenom); + const balance = await this.forceGetQueryClient().bank.balance(address, searchDenom); return balance ? coinFromProto(balance) : null; } @@ -174,7 +192,7 @@ export class StargateClient { * proofs from such a method. */ public async getAllBalancesUnverified(address: string): Promise { - const balances = await this.queryClient.bank.unverified.allBalances(address); + const balances = await this.forceGetQueryClient().bank.unverified.allBalances(address); return balances.map(coinFromProto); } @@ -222,11 +240,11 @@ export class StargateClient { } public disconnect(): void { - this.tmClient.disconnect(); + if (this.tmClient) this.tmClient.disconnect(); } public async broadcastTx(tx: Uint8Array): Promise { - const response = await this.tmClient.broadcastTxCommit({ tx }); + const response = await this.forceGetTmClient().broadcastTxCommit({ tx }); if (broadcastTxCommitSuccess(response)) { return { height: response.height, @@ -253,7 +271,7 @@ export class StargateClient { } private async txsQuery(query: string): Promise { - const results = await this.tmClient.txSearchAll({ query: query }); + const results = await this.forceGetTmClient().txSearchAll({ query: query }); return results.txs.map((tx) => { return { height: tx.height, From 6d361e2c88a278499579f10a252f5d43cb02d41b Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 24 Mar 2021 14:08:49 +0100 Subject: [PATCH 17/19] Restructure makeMultisignedTx test to use offline signers --- packages/stargate/src/multisignature.spec.ts | 246 +++++++++++++------ 1 file changed, 167 insertions(+), 79 deletions(-) diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index 1d4a090f..42695155 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -6,7 +6,7 @@ import { MsgSend } from "./codec/cosmos/bank/v1beta1/tx"; import { TxRaw } from "./codec/cosmos/tx/v1beta1/tx"; import { makeCompactBitArray, makeMultisignedTx } from "./multisignature"; import { SignerData, SigningStargateClient } from "./signingstargateclient"; -import { assertIsBroadcastTxSuccess } from "./stargateclient"; +import { assertIsBroadcastTxSuccess, StargateClient } from "./stargateclient"; import { faucet, pendingWithoutSimapp, simapp } from "./testutils.spec"; describe("multisignature", () => { @@ -165,90 +165,178 @@ describe("multisignature", () => { describe("makeMultisignedTx", () => { it("works", async () => { pendingWithoutSimapp(); - const wallet0 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const wallet1 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); - const wallet2 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(2)); - const wallet3 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(3)); - const wallet4 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(4)); - const pubkey0 = encodeSecp256k1Pubkey((await wallet0.getAccounts())[0].pubkey); - const pubkey1 = encodeSecp256k1Pubkey((await wallet1.getAccounts())[0].pubkey); - const pubkey2 = encodeSecp256k1Pubkey((await wallet2.getAccounts())[0].pubkey); - const pubkey3 = encodeSecp256k1Pubkey((await wallet3.getAccounts())[0].pubkey); - const pubkey4 = encodeSecp256k1Pubkey((await wallet4.getAccounts())[0].pubkey); - const address0 = (await wallet0.getAccounts())[0].address; - const address1 = (await wallet1.getAccounts())[0].address; - const address2 = (await wallet2.getAccounts())[0].address; - const address3 = (await wallet3.getAccounts())[0].address; - const address4 = (await wallet4.getAccounts())[0].address; - const multisigPubkey = createMultisigThresholdPubkey([pubkey0, pubkey1, pubkey2, pubkey3, pubkey4], 2); - const multisigAddress = pubkeyToAddress(multisigPubkey, "cosmos"); - expect(multisigAddress).toEqual("cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"); + const multisigAccountAddress = "cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"; - const client0 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet0); - const client1 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet1); - const client2 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet2); - const client3 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet3); - const client4 = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet4); + // On the composer's machine signing instructions are created. + // The composer does not need to be one of the signers. + const signingInstruction = await (async () => { + const client = await StargateClient.connect(simapp.tendermintUrl); + const accountOnChain = await client.getAccount(multisigAccountAddress); + assert(accountOnChain, "Account does not exist on chain"); - const msgSend: MsgSend = { - fromAddress: multisigAddress, - toAddress: "cosmos19rvl6ja9h0erq9dc2xxfdzypc739ej8k5esnhg", - amount: coins(1234, "ucosm"), - }; - const msg = { - typeUrl: "/cosmos.bank.v1beta1.MsgSend", - value: msgSend, - }; - const gasLimit = 200000; - const fee = { - amount: coins(2000, "ucosm"), - gas: gasLimit.toString(), - }; - const memo = "Use your tokens wisely"; + const msgSend: MsgSend = { + fromAddress: multisigAccountAddress, + toAddress: "cosmos19rvl6ja9h0erq9dc2xxfdzypc739ej8k5esnhg", + amount: coins(1234, "ucosm"), + }; + const msg = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const gasLimit = 200000; + const fee = { + amount: coins(2000, "ucosm"), + gas: gasLimit.toString(), + }; - const multisigAccount = await client0.getAccount(multisigAddress); - assert(multisigAccount, "Account does not exist on chain"); - const signerData: SignerData = { - accountNumber: multisigAccount.accountNumber, - sequence: multisigAccount.sequence, - chainId: await client0.getChainId(), - }; + return { + accountNumber: accountOnChain.accountNumber, + sequence: accountOnChain.sequence, + chainId: await client.getChainId(), + msgs: [msg], + fee: fee, + memo: "Use your tokens wisely", + }; + })(); - const { - bodyBytes, - signatures: [signature0], - } = await client0.sign(faucet.address0, [msg], fee, memo, signerData); - const { - signatures: [signature1], - } = await client1.sign(faucet.address1, [msg], fee, memo, signerData); - const { - signatures: [signature2], - } = await client2.sign(faucet.address2, [msg], fee, memo, signerData); - const { - signatures: [signature3], - } = await client3.sign(faucet.address3, [msg], fee, memo, signerData); - const { - signatures: [signature4], - } = await client4.sign(faucet.address4, [msg], fee, memo, signerData); + // Signing environment 0 + const [pubkey0, signature0, bodyBytes] = await (async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); + const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); + const address = (await wallet.getAccounts())[0].address; + const signingClient = await SigningStargateClient.offline(wallet); + const signerData: SignerData = { + accountNumber: signingInstruction.accountNumber, + sequence: signingInstruction.sequence, + chainId: signingInstruction.chainId, + }; + const { bodyBytes: bb, signatures } = await signingClient.sign( + address, + signingInstruction.msgs, + signingInstruction.fee, + signingInstruction.memo, + signerData, + ); + return [pubkey, signatures[0], bb] as const; + })(); - const signatures = new Map([ - [address0, signature0], - [address1, signature1], - [address2, signature2], - [address3, signature3], - [address4, signature4], - ]); - const signedTx = makeMultisignedTx( - multisigPubkey, - multisigAccount.sequence, - fee, - bodyBytes, - signatures, - ); + // Signing environment 1 + const [pubkey1, signature1] = await (async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); + const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); + const address = (await wallet.getAccounts())[0].address; + const signingClient = await SigningStargateClient.offline(wallet); + const signerData: SignerData = { + accountNumber: signingInstruction.accountNumber, + sequence: signingInstruction.sequence, + chainId: signingInstruction.chainId, + }; + const { signatures } = await signingClient.sign( + address, + signingInstruction.msgs, + signingInstruction.fee, + signingInstruction.memo, + signerData, + ); + return [pubkey, signatures[0]] as const; + })(); - // ensure signature is valid - const result = await client0.broadcastTx(Uint8Array.from(TxRaw.encode(signedTx).finish())); - assertIsBroadcastTxSuccess(result); + // Signing environment 2 + const [pubkey2, signature2] = await (async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(2)); + const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); + const address = (await wallet.getAccounts())[0].address; + const signingClient = await SigningStargateClient.offline(wallet); + const signerData: SignerData = { + accountNumber: signingInstruction.accountNumber, + sequence: signingInstruction.sequence, + chainId: signingInstruction.chainId, + }; + const { signatures } = await signingClient.sign( + address, + signingInstruction.msgs, + signingInstruction.fee, + signingInstruction.memo, + signerData, + ); + return [pubkey, signatures[0]] as const; + })(); + + // Signing environment 3 + const [pubkey3, signature3] = await (async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(3)); + const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); + const address = (await wallet.getAccounts())[0].address; + const signingClient = await SigningStargateClient.offline(wallet); + const signerData: SignerData = { + accountNumber: signingInstruction.accountNumber, + sequence: signingInstruction.sequence, + chainId: signingInstruction.chainId, + }; + const { signatures } = await signingClient.sign( + address, + signingInstruction.msgs, + signingInstruction.fee, + signingInstruction.memo, + signerData, + ); + return [pubkey, signatures[0]] as const; + })(); + + // Signing environment 4 + const [pubkey4, signature4] = await (async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(4)); + const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); + const address = (await wallet.getAccounts())[0].address; + const signingClient = await SigningStargateClient.offline(wallet); + const signerData: SignerData = { + accountNumber: signingInstruction.accountNumber, + sequence: signingInstruction.sequence, + chainId: signingInstruction.chainId, + }; + const { signatures } = await signingClient.sign( + address, + signingInstruction.msgs, + signingInstruction.fee, + signingInstruction.memo, + signerData, + ); + return [pubkey, signatures[0]] as const; + })(); + + // From here on, no private keys are required anymore. Any anonymous entity + // can collect, assemble and broadcast. + { + const multisigPubkey = createMultisigThresholdPubkey( + [pubkey0, pubkey1, pubkey2, pubkey3, pubkey4], + 2, + ); + expect(pubkeyToAddress(multisigPubkey, "cosmos")).toEqual(multisigAccountAddress); + + const address0 = pubkeyToAddress(pubkey0, "cosmos"); + const address1 = pubkeyToAddress(pubkey1, "cosmos"); + const address2 = pubkeyToAddress(pubkey2, "cosmos"); + const address3 = pubkeyToAddress(pubkey3, "cosmos"); + const address4 = pubkeyToAddress(pubkey4, "cosmos"); + + const broadcaster = await StargateClient.connect(simapp.tendermintUrl); + const signedTx = makeMultisignedTx( + multisigPubkey, + signingInstruction.sequence, + signingInstruction.fee, + bodyBytes, + new Map([ + [address0, signature0], + [address1, signature1], + [address2, signature2], + [address3, signature3], + [address4, signature4], + ]), + ); + // ensure signature is valid + const result = await broadcaster.broadcastTx(Uint8Array.from(TxRaw.encode(signedTx).finish())); + assertIsBroadcastTxSuccess(result); + } }); }); }); From fc103ea31e8e6a28b3aebba4ada5193e169bdb13 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 24 Mar 2021 14:14:48 +0100 Subject: [PATCH 18/19] Use map for signing environments --- packages/stargate/src/multisignature.spec.ts | 132 ++++--------------- 1 file changed, 28 insertions(+), 104 deletions(-) diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index 42695155..d6859597 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -199,110 +199,34 @@ describe("multisignature", () => { }; })(); - // Signing environment 0 - const [pubkey0, signature0, bodyBytes] = await (async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); - const address = (await wallet.getAccounts())[0].address; - const signingClient = await SigningStargateClient.offline(wallet); - const signerData: SignerData = { - accountNumber: signingInstruction.accountNumber, - sequence: signingInstruction.sequence, - chainId: signingInstruction.chainId, - }; - const { bodyBytes: bb, signatures } = await signingClient.sign( - address, - signingInstruction.msgs, - signingInstruction.fee, - signingInstruction.memo, - signerData, - ); - return [pubkey, signatures[0], bb] as const; - })(); - - // Signing environment 1 - const [pubkey1, signature1] = await (async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); - const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); - const address = (await wallet.getAccounts())[0].address; - const signingClient = await SigningStargateClient.offline(wallet); - const signerData: SignerData = { - accountNumber: signingInstruction.accountNumber, - sequence: signingInstruction.sequence, - chainId: signingInstruction.chainId, - }; - const { signatures } = await signingClient.sign( - address, - signingInstruction.msgs, - signingInstruction.fee, - signingInstruction.memo, - signerData, - ); - return [pubkey, signatures[0]] as const; - })(); - - // Signing environment 2 - const [pubkey2, signature2] = await (async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(2)); - const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); - const address = (await wallet.getAccounts())[0].address; - const signingClient = await SigningStargateClient.offline(wallet); - const signerData: SignerData = { - accountNumber: signingInstruction.accountNumber, - sequence: signingInstruction.sequence, - chainId: signingInstruction.chainId, - }; - const { signatures } = await signingClient.sign( - address, - signingInstruction.msgs, - signingInstruction.fee, - signingInstruction.memo, - signerData, - ); - return [pubkey, signatures[0]] as const; - })(); - - // Signing environment 3 - const [pubkey3, signature3] = await (async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(3)); - const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); - const address = (await wallet.getAccounts())[0].address; - const signingClient = await SigningStargateClient.offline(wallet); - const signerData: SignerData = { - accountNumber: signingInstruction.accountNumber, - sequence: signingInstruction.sequence, - chainId: signingInstruction.chainId, - }; - const { signatures } = await signingClient.sign( - address, - signingInstruction.msgs, - signingInstruction.fee, - signingInstruction.memo, - signerData, - ); - return [pubkey, signatures[0]] as const; - })(); - - // Signing environment 4 - const [pubkey4, signature4] = await (async () => { - const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(4)); - const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); - const address = (await wallet.getAccounts())[0].address; - const signingClient = await SigningStargateClient.offline(wallet); - const signerData: SignerData = { - accountNumber: signingInstruction.accountNumber, - sequence: signingInstruction.sequence, - chainId: signingInstruction.chainId, - }; - const { signatures } = await signingClient.sign( - address, - signingInstruction.msgs, - signingInstruction.fee, - signingInstruction.memo, - signerData, - ); - return [pubkey, signatures[0]] as const; - })(); + const [ + [pubkey0, signature0, bodyBytes], + [pubkey1, signature1], + [pubkey2, signature2], + [pubkey3, signature3], + [pubkey4, signature4], + ] = await Promise.all( + [0, 1, 2, 3, 4].map(async (i) => { + // Signing environment + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(i)); + const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); + const address = (await wallet.getAccounts())[0].address; + const signingClient = await SigningStargateClient.offline(wallet); + const signerData: SignerData = { + accountNumber: signingInstruction.accountNumber, + sequence: signingInstruction.sequence, + chainId: signingInstruction.chainId, + }; + const { bodyBytes: bb, signatures } = await signingClient.sign( + address, + signingInstruction.msgs, + signingInstruction.fee, + signingInstruction.memo, + signerData, + ); + return [pubkey, signatures[0], bb] as const; + }), + ); // From here on, no private keys are required anymore. Any anonymous entity // can collect, assemble and broadcast. From f82b1755066a59a1684342c662ff8440c7e82673 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 24 Mar 2021 15:53:02 +0100 Subject: [PATCH 19/19] Export makeMultisignedTx --- CHANGELOG.md | 2 ++ packages/stargate/src/index.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 877e7d3c..7a9eed7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ and this project adheres to sequence and chain ID. - @cosmjs/stargate: Add constructor `SigningStargateClient.offline` which does not connect to Tendermint. This allows offline signing. +- @cosmjs/stargate: Add `makeMultisignedTx` which allows you to assemble a + transaction signed by a multisig account. ### Changed diff --git a/packages/stargate/src/index.ts b/packages/stargate/src/index.ts index a7096ec8..fe379222 100644 --- a/packages/stargate/src/index.ts +++ b/packages/stargate/src/index.ts @@ -4,6 +4,7 @@ export { Account, accountFromAny } from "./accounts"; export { AminoConverter, AminoTypes } from "./aminotypes"; export { buildFeeTable, FeeTable, GasLimits, GasPrice, StdFee } from "./fee"; export * as logs from "./logs"; +export { makeMultisignedTx } from "./multisignature"; export { AuthExtension, BankExtension,