diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b4903c..d3647ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - @cosmjs/launchpad-ledger: `LedgerSigner.sign` method renamed `signAmino`. - @cosmjs/proto-signing: Add new package for handling transaction signing with protobuf encoding. +- @cosmjs/proto-signing: Expose `DirectSignResponse` interface. - @cosmjs/stargate: Add new package for Cosmos SDK Stargate support. - @cosmjs/tendermint-rpc: Make `Client.detectVersion` private and let it return a version instead of a client. diff --git a/packages/launchpad/src/secp256k1hdwallet.ts b/packages/launchpad/src/secp256k1hdwallet.ts index 2f4d94ca..acdbd10f 100644 --- a/packages/launchpad/src/secp256k1hdwallet.ts +++ b/packages/launchpad/src/secp256k1hdwallet.ts @@ -223,7 +223,7 @@ export class Secp256k1HdWallet implements OfflineSigner { private readonly pubkey: Uint8Array; private readonly privkey: Uint8Array; - private constructor( + protected constructor( mnemonic: EnglishMnemonic, hdPath: HdPath, privkey: Uint8Array, diff --git a/packages/launchpad/types/secp256k1hdwallet.d.ts b/packages/launchpad/types/secp256k1hdwallet.d.ts index 9a3be009..40441395 100644 --- a/packages/launchpad/types/secp256k1hdwallet.d.ts +++ b/packages/launchpad/types/secp256k1hdwallet.d.ts @@ -1,4 +1,4 @@ -import { HdPath } from "@cosmjs/crypto"; +import { EnglishMnemonic, HdPath } from "@cosmjs/crypto"; import { StdSignDoc } from "./encoding"; import { AccountData, AminoSignResponse, OfflineSigner } from "./signer"; import { EncryptionConfiguration, KdfConfiguration } from "./wallet"; @@ -66,7 +66,13 @@ export declare class Secp256k1HdWallet implements OfflineSigner { /** Derived data */ private readonly pubkey; private readonly privkey; - private constructor(); + protected constructor( + mnemonic: EnglishMnemonic, + hdPath: HdPath, + privkey: Uint8Array, + pubkey: Uint8Array, + prefix: string, + ); get mnemonic(): string; private get address(); getAccounts(): Promise; diff --git a/packages/proto-signing/src/directsecp256k1hdwallet.ts b/packages/proto-signing/src/directsecp256k1hdwallet.ts index 1b869c4b..0e0e7f6c 100644 --- a/packages/proto-signing/src/directsecp256k1hdwallet.ts +++ b/packages/proto-signing/src/directsecp256k1hdwallet.ts @@ -80,7 +80,7 @@ export class DirectSecp256k1HdWallet implements OfflineDirectSigner { private readonly pubkey: Uint8Array; private readonly privkey: Uint8Array; - private constructor( + protected constructor( mnemonic: EnglishMnemonic, hdPath: HdPath, privkey: Uint8Array, diff --git a/packages/proto-signing/src/index.ts b/packages/proto-signing/src/index.ts index c6b8cc1f..f710c8c9 100644 --- a/packages/proto-signing/src/index.ts +++ b/packages/proto-signing/src/index.ts @@ -4,5 +4,5 @@ export { EncodeObject, Registry } from "./registry"; export { DirectSecp256k1HdWallet } from "./directsecp256k1hdwallet"; export { DirectSecp256k1Wallet } from "./directsecp256k1wallet"; export { decodePubkey, encodePubkey } from "./pubkey"; -export { isOfflineDirectSigner, OfflineDirectSigner, OfflineSigner } from "./signer"; +export { DirectSignResponse, isOfflineDirectSigner, OfflineDirectSigner, OfflineSigner } from "./signer"; export { makeAuthInfoBytes, makeSignBytes, makeSignDoc } from "./signing"; diff --git a/packages/proto-signing/types/directsecp256k1hdwallet.d.ts b/packages/proto-signing/types/directsecp256k1hdwallet.d.ts index f557689d..be0944d8 100644 --- a/packages/proto-signing/types/directsecp256k1hdwallet.d.ts +++ b/packages/proto-signing/types/directsecp256k1hdwallet.d.ts @@ -1,4 +1,4 @@ -import { HdPath } from "@cosmjs/crypto"; +import { EnglishMnemonic, HdPath } from "@cosmjs/crypto"; import { AccountData } from "@cosmjs/launchpad"; import { cosmos } from "./codec"; import { DirectSignResponse, OfflineDirectSigner } from "./signer"; @@ -31,7 +31,13 @@ export declare class DirectSecp256k1HdWallet implements OfflineDirectSigner { /** Derived data */ private readonly pubkey; private readonly privkey; - private constructor(); + protected constructor( + mnemonic: EnglishMnemonic, + hdPath: HdPath, + privkey: Uint8Array, + pubkey: Uint8Array, + prefix: string, + ); get mnemonic(): string; private get address(); getAccounts(): Promise; diff --git a/packages/proto-signing/types/index.d.ts b/packages/proto-signing/types/index.d.ts index c6b8cc1f..f710c8c9 100644 --- a/packages/proto-signing/types/index.d.ts +++ b/packages/proto-signing/types/index.d.ts @@ -4,5 +4,5 @@ export { EncodeObject, Registry } from "./registry"; export { DirectSecp256k1HdWallet } from "./directsecp256k1hdwallet"; export { DirectSecp256k1Wallet } from "./directsecp256k1wallet"; export { decodePubkey, encodePubkey } from "./pubkey"; -export { isOfflineDirectSigner, OfflineDirectSigner, OfflineSigner } from "./signer"; +export { DirectSignResponse, isOfflineDirectSigner, OfflineDirectSigner, OfflineSigner } from "./signer"; export { makeAuthInfoBytes, makeSignBytes, makeSignDoc } from "./signing"; diff --git a/packages/stargate/src/encoding.ts b/packages/stargate/src/encoding.ts index 8b238a79..71ffe880 100644 --- a/packages/stargate/src/encoding.ts +++ b/packages/stargate/src/encoding.ts @@ -1,27 +1,36 @@ +const typeRegister: Record = { + "/cosmos.bank.v1beta1.MsgSend": "cosmos-sdk/MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend": "cosmos-sdk/MsgMultiSend", + "/cosmos.crisis.v1beta1.MsgVerifyInvariant": "cosmos-sdk/MsgVerifyInvariant", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress": "cosmos-sdk/MsgSetWithdrawAddress", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward": "cosmos-sdk/MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgWithdrawValidatorComission": "cosmos-sdk/MsgWithdrawValidatorComission", + "/cosmos.distribution.v1beta1.MsgFundCommunityPool": "cosmos-sdk/MsgFundCommunityPool", + "/cosmos.evidence.v1beta1.MsgSubmitEvidence": "cosmos-sdk/MsgSubmitEvidence", + "/cosmos.gov.v1beta1.MsgSubmitProposal": "cosmos-sdk/MsgSubmitProposal", + "/cosmos.gov.v1beta1.MsgVote": "cosmos-sdk/MsgVote", + "/cosmos.gov.v1beta1.MsgDeposit": "cosmos-sdk/MsgDeposit", + "/cosmos.slashing.v1beta1.MsgUnjail": "cosmos-sdk/MsgUnjail", + "/cosmos.staking.v1beta1.MsgCreateValidator": "cosmos-sdk/MsgCreateValidator", + "/cosmos.staking.v1beta1.MsgEditValidator": "cosmos-sdk/MsgEditValidator", + "/cosmos.staking.v1beta1.MsgDelegate": "cosmos-sdk/MsgDelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate": "cosmos-sdk/MsgBeginRedelegate", + "/cosmos.staking.v1beta1.MsgUndelegate": "cosmos-sdk/MsgUndelegate", + "/cosmos.vesting.v1beta1.MsgCreateVestingAccount": "cosmos-sdk/MsgCreateVestingAccount", +}; + export function getMsgType(typeUrl: string): string { - const typeRegister: Record = { - "/cosmos.bank.v1beta1.MsgSend": "cosmos-sdk/MsgSend", - "/cosmos.bank.v1beta1.MsgMultiSend": "cosmos-sdk/MsgMultiSend", - "/cosmos.crisis.v1beta1.MsgVerifyInvariant": "cosmos-sdk/MsgVerifyInvariant", - "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress": "cosmos-sdk/MsgSetWithdrawAddress", - "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward": "cosmos-sdk/MsgWithdrawDelegatorReward", - "/cosmos.distribution.v1beta1.MsgWithdrawValidatorComission": "cosmos-sdk/MsgWithdrawValidatorComission", - "/cosmos.distribution.v1beta1.MsgFundCommunityPool": "cosmos-sdk/MsgFundCommunityPool", - "/cosmos.evidence.v1beta1.MsgSubmitEvidence": "cosmos-sdk/MsgSubmitEvidence", - "/cosmos.gov.v1beta1.MsgSubmitProposal": "cosmos-sdk/MsgSubmitProposal", - "/cosmos.gov.v1beta1.MsgVote": "cosmos-sdk/MsgVote", - "/cosmos.gov.v1beta1.MsgDeposit": "cosmos-sdk/MsgDeposit", - "/cosmos.slashing.v1beta1.MsgUnjail": "cosmos-sdk/MsgUnjail", - "/cosmos.staking.v1beta1.MsgCreateValidator": "cosmos-sdk/MsgCreateValidator", - "/cosmos.staking.v1beta1.MsgEditValidator": "cosmos-sdk/MsgEditValidator", - "/cosmos.staking.v1beta1.MsgDelegate": "cosmos-sdk/MsgDelegate", - "/cosmos.staking.v1beta1.MsgBeginRedelegate": "cosmos-sdk/MsgBeginRedelegate", - "/cosmos.staking.v1beta1.MsgUndelegate": "cosmos-sdk/MsgUndelegate", - "/cosmos.vesting.v1beta1.MsgCreateVestingAccount": "cosmos-sdk/MsgCreateVestingAccount", - }; const type = typeRegister[typeUrl]; if (!type) { throw new Error("Type URL not known"); } return type; } + +export function getMsgTypeUrl(type: string): string { + const [typeUrl] = Object.entries(typeRegister).find(([_typeUrl, value]) => value === type) ?? []; + if (!typeUrl) { + throw new Error("Type not known"); + } + return typeUrl; +} diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 5cce5cb9..a15aca9e 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -1,13 +1,25 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { coin, coins, GasPrice, Secp256k1HdWallet } from "@cosmjs/launchpad"; import { Coin, cosmosField, DirectSecp256k1HdWallet, registered, Registry } from "@cosmjs/proto-signing"; -import { assert } from "@cosmjs/utils"; +import { assert, sleep } from "@cosmjs/utils"; import { Message } from "protobufjs"; import { cosmos } from "./codec"; import { PrivateSigningStargateClient, SigningStargateClient } from "./signingstargateclient"; import { assertIsBroadcastTxSuccess } from "./stargateclient"; -import { faucet, makeRandomAddress, pendingWithoutSimapp, simapp, validator } from "./testutils.spec"; +import { + faucet, + makeRandomAddress, + ModifyingDirectSecp256k1HdWallet, + ModifyingSecp256k1HdWallet, + pendingWithoutSimapp, + simapp, + validator, +} from "./testutils.spec"; + +const { MsgSend } = cosmos.bank.v1beta1; +const { MsgDelegate } = cosmos.staking.v1beta1; +const { Tx } = cosmos.tx.v1beta1; describe("SigningStargateClient", () => { describe("constructor", () => { @@ -33,11 +45,11 @@ describe("SigningStargateClient", () => { pendingWithoutSimapp(); const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); const registry = new Registry(); - registry.register("/custom.MsgCustom", cosmos.bank.v1beta1.MsgSend); + registry.register("/custom.MsgCustom", MsgSend); const options = { registry: registry }; const client = await SigningStargateClient.connectWithWallet(simapp.tendermintUrl, wallet, options); const openedClient = (client as unknown) as PrivateSigningStargateClient; - expect(openedClient.registry.lookupType("/custom.MsgCustom")).toEqual(cosmos.bank.v1beta1.MsgSend); + expect(openedClient.registry.lookupType("/custom.MsgCustom")).toEqual(MsgSend); }); it("can be constructed with custom gas price", async () => { @@ -133,44 +145,81 @@ describe("SigningStargateClient", () => { }); describe("signAndBroadcast", () => { - it("works with direct mode", async () => { - pendingWithoutSimapp(); - const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); - const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; - const registry = new Registry(); - registry.register(msgDelegateTypeUrl, cosmos.staking.v1beta1.MsgDelegate); - const options = { registry: registry }; - const client = await SigningStargateClient.connectWithWallet(simapp.tendermintUrl, wallet, options); + describe("direct mode", () => { + it("works", async () => { + pendingWithoutSimapp(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; + const registry = new Registry(); + registry.register(msgDelegateTypeUrl, MsgDelegate); + const options = { registry: registry }; + const client = await SigningStargateClient.connectWithWallet(simapp.tendermintUrl, wallet, options); - const msg = cosmos.staking.v1beta1.MsgDelegate.create({ - delegatorAddress: faucet.address0, - validatorAddress: validator.validatorAddress, - amount: coin(1234, "ustake"), + const msg = MsgDelegate.create({ + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }); + const msgAny = { + typeUrl: msgDelegateTypeUrl, + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "180000", // 180k + }; + const memo = "Use your power wisely"; + const result = await client.signAndBroadcast(faucet.address0, [msgAny], fee, memo); + assertIsBroadcastTxSuccess(result); + }); + + it("works with a modifying signer", async () => { + pendingWithoutSimapp(); + const wallet = await ModifyingDirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; + const registry = new Registry(); + registry.register(msgDelegateTypeUrl, MsgDelegate); + const options = { registry: registry }; + const client = await SigningStargateClient.connectWithWallet(simapp.tendermintUrl, wallet, options); + + const msg = MsgDelegate.create({ + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }); + const msgAny = { + typeUrl: msgDelegateTypeUrl, + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "180000", // 180k + }; + const memo = "Use your power wisely"; + const result = await client.signAndBroadcast(faucet.address0, [msgAny], fee, memo); + assertIsBroadcastTxSuccess(result); + + await sleep(1000); + + const searchResult = await client.searchTx({ id: result.transactionHash }); + const tx = Tx.decode(searchResult[0].tx); + // From ModifyingDirectSecp256k1HdWallet + expect(tx.body!.memo).toEqual("This was modified"); + expect({ ...tx.authInfo!.fee!.amount![0] }).toEqual(coin(3000, "ucosm")); + expect(tx.authInfo!.fee!.gasLimit!.toNumber()).toEqual(333333); }); - const msgAny = { - typeUrl: msgDelegateTypeUrl, - value: msg, - }; - const fee = { - amount: coins(2000, "ucosm"), - gas: "180000", // 180k - }; - const memo = "Use your power wisely"; - const result = await client.signAndBroadcast(faucet.address0, [msgAny], fee, memo); - assertIsBroadcastTxSuccess(result); }); - it("works with legacy Amino mode", async () => { - pendingWithoutSimapp(); - const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); - const coinTypeUrl = "/cosmos.base.v1beta1.Coin"; - const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; + describe("legacy Amino mode", () => { + // NOTE: One registry shared between tests + // See https://github.com/protobufjs/protobuf.js#using-decorators + // > Decorated types reside in protobuf.roots["decorated"] using a flat structure, so no duplicate names. const registry = new Registry(); - registry.register(coinTypeUrl, Coin); + const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; @registered(registry, msgDelegateTypeUrl) // eslint-disable-next-line @typescript-eslint/no-unused-vars - class MsgDelegate extends Message { + class CustomMsgDelegate extends Message { @cosmosField.string(1) public readonly delegator_address?: string; @cosmosField.string(2) @@ -179,25 +228,62 @@ describe("SigningStargateClient", () => { public readonly amount?: Coin; } - const options = { registry: registry }; - const client = await SigningStargateClient.connectWithWallet(simapp.tendermintUrl, wallet, options); + it("works", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const options = { registry: registry }; + const client = await SigningStargateClient.connectWithWallet(simapp.tendermintUrl, wallet, options); - const msg = { - delegator_address: faucet.address0, - validator_address: validator.validatorAddress, - amount: coin(1234, "ustake"), - }; - const msgAny = { - typeUrl: msgDelegateTypeUrl, - value: msg, - }; - const fee = { - amount: coins(2000, "ucosm"), - gas: "200000", - }; - const memo = "Use your power wisely"; - const result = await client.signAndBroadcast(faucet.address0, [msgAny], fee, memo); - assertIsBroadcastTxSuccess(result); + const msg = { + delegator_address: faucet.address0, + validator_address: validator.validatorAddress, + amount: coin(1234, "ustake"), + }; + const msgAny = { + typeUrl: msgDelegateTypeUrl, + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "Use your power wisely"; + const result = await client.signAndBroadcast(faucet.address0, [msgAny], fee, memo); + assertIsBroadcastTxSuccess(result); + }); + + it("works with a modifying signer", async () => { + pendingWithoutSimapp(); + const wallet = await ModifyingSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const options = { registry: registry }; + const client = await SigningStargateClient.connectWithWallet(simapp.tendermintUrl, wallet, options); + + const msg = { + delegator_address: faucet.address0, + validator_address: validator.validatorAddress, + amount: coin(1234, "ustake"), + }; + const msgAny = { + typeUrl: msgDelegateTypeUrl, + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "Use your power wisely"; + const result = await client.signAndBroadcast(faucet.address0, [msgAny], fee, memo); + assertIsBroadcastTxSuccess(result); + + await sleep(1000); + + const searchResult = await client.searchTx({ id: result.transactionHash }); + const tx = Tx.decode(searchResult[0].tx); + // From ModifyingSecp256k1HdWallet + expect(tx.body!.memo).toEqual("This was modified"); + expect({ ...tx.authInfo!.fee!.amount![0] }).toEqual(coin(3000, "ucosm")); + expect(tx.authInfo!.fee!.gasLimit!.toNumber()).toEqual(333333); + }); }); }); }); diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index c93c822b..67a84ab5 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -24,7 +24,7 @@ import { import { adaptor34, Client as TendermintClient } from "@cosmjs/tendermint-rpc"; import { cosmos } from "./codec"; -import { getMsgType } from "./encoding"; +import { getMsgType, getMsgTypeUrl } from "./encoding"; import { BroadcastTxResponse, StargateClient } from "./stargateclient"; const { TxRaw } = cosmos.tx.v1beta1; @@ -123,10 +123,10 @@ export class SigningStargateClient extends StargateClient { if (isOfflineDirectSigner(this.signer)) { const authInfoBytes = makeAuthInfoBytes([pubkeyAny], fee.amount, gasLimit, sequence); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); - const { signature } = await this.signer.signDirect(address, signDoc); + const { signature, signed } = await this.signer.signDirect(address, signDoc); const txRaw = TxRaw.create({ - bodyBytes: txBodyBytes, - authInfoBytes: authInfoBytes, + bodyBytes: signed.bodyBytes, + authInfoBytes: signed.authInfoBytes, signatures: [fromBase64(signature.signature)], }); const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish()); @@ -135,17 +135,36 @@ export class SigningStargateClient extends StargateClient { // Amino signer const signMode = cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_LEGACY_AMINO_JSON; - const authInfoBytes = makeAuthInfoBytes([pubkeyAny], fee.amount, gasLimit, sequence, signMode); const msgs = messages.map((msg) => ({ type: getMsgType(msg.typeUrl), value: msg.value, })); const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence); - const signResponse = await this.signer.signAmino(address, signDoc); + const { signature, signed } = await this.signer.signAmino(address, signDoc); + const signedTxBody = { + messages: signed.msgs.map((msg) => ({ + typeUrl: getMsgTypeUrl(msg.type), + value: msg.value, + })), + memo: signed.memo, + }; + const signedTxBodyBytes = this.registry.encode({ + typeUrl: "/cosmos.tx.v1beta1.TxBody", + value: signedTxBody, + }); + const signedGasLimit = Int53.fromString(signed.fee.gas).toNumber(); + const signedSequence = Int53.fromString(signed.sequence).toNumber(); + const signedAuthInfoBytes = makeAuthInfoBytes( + [pubkeyAny], + signed.fee.amount, + signedGasLimit, + signedSequence, + signMode, + ); const txRaw = TxRaw.create({ - bodyBytes: txBodyBytes, - authInfoBytes: authInfoBytes, - signatures: [fromBase64(signResponse.signature.signature)], + bodyBytes: signedTxBodyBytes, + authInfoBytes: signedAuthInfoBytes, + signatures: [fromBase64(signature.signature)], }); const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish()); return this.broadcastTx(signedTx); diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index fa9524ad..21497e27 100644 --- a/packages/stargate/src/testutils.spec.ts +++ b/packages/stargate/src/testutils.spec.ts @@ -1,5 +1,18 @@ -import { Random } from "@cosmjs/crypto"; +/* eslint-disable @typescript-eslint/naming-convention */ +import { Bip39, EnglishMnemonic, Random, Secp256k1, Slip10, Slip10Curve } from "@cosmjs/crypto"; import { Bech32 } from "@cosmjs/encoding"; +import { + AminoSignResponse, + coins, + makeCosmoshubPath, + Secp256k1HdWallet, + StdSignDoc, +} from "@cosmjs/launchpad"; +import { DirectSecp256k1HdWallet, DirectSignResponse, makeAuthInfoBytes } from "@cosmjs/proto-signing"; + +import { cosmos } from "./codec"; + +const { AuthInfo, TxBody } = cosmos.tx.v1beta1; export function simappEnabled(): boolean { return !!process.env.SIMAPP_ENABLED; @@ -70,3 +83,86 @@ export const nonExistentAddress = "cosmos1p79apjaufyphcmsn4g07cynqf0wyjuezqu84hd export const nonNegativeIntegerMatcher = /^[0-9]+$/; export const tendermintIdMatcher = /^[0-9A-F]{64}$/; + +/** + * A class for testing clients using an Amino signer which modifies the transaction it receives before signing + */ +export class ModifyingSecp256k1HdWallet extends Secp256k1HdWallet { + public static async fromMnemonic( + mnemonic: string, + hdPath = makeCosmoshubPath(0), + prefix = "cosmos", + ): Promise { + const mnemonicChecked = new EnglishMnemonic(mnemonic); + const seed = await Bip39.mnemonicToSeed(mnemonicChecked); + const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath); + const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; + return new ModifyingSecp256k1HdWallet( + mnemonicChecked, + hdPath, + privkey, + Secp256k1.compressPubkey(uncompressed), + prefix, + ); + } + + public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise { + const modifiedSignDoc = { + ...signDoc, + fee: { + amount: coins(3000, "ucosm"), + gas: "333333", + }, + memo: "This was modified", + }; + return super.signAmino(signerAddress, modifiedSignDoc); + } +} + +/** + * A class for testing clients using a direct signer which modifies the transaction it receives before signing + */ +export class ModifyingDirectSecp256k1HdWallet extends DirectSecp256k1HdWallet { + public static async fromMnemonic( + mnemonic: string, + hdPath = makeCosmoshubPath(0), + prefix = "cosmos", + ): Promise { + const mnemonicChecked = new EnglishMnemonic(mnemonic); + const seed = await Bip39.mnemonicToSeed(mnemonicChecked); + const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath); + const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey; + return new ModifyingDirectSecp256k1HdWallet( + mnemonicChecked, + hdPath, + privkey, + Secp256k1.compressPubkey(uncompressed), + prefix, + ); + } + + public async signDirect(address: string, signDoc: cosmos.tx.v1beta1.ISignDoc): Promise { + const txBody = TxBody.decode(signDoc.bodyBytes!); + const modifiedTxBody = TxBody.create({ + ...txBody, + memo: "This was modified", + }); + const authInfo = AuthInfo.decode(signDoc.authInfoBytes!); + const pubkeys = authInfo.signerInfos.map((signerInfo) => signerInfo.publicKey!); + const sequence = authInfo.signerInfos[0].sequence!.toNumber(); + const modifiedFeeAmount = coins(3000, "ucosm"); + const modifiedGasLimit = 333333; + const modifiedSignDoc = { + ...signDoc, + bodyBytes: Uint8Array.from(TxBody.encode(modifiedTxBody).finish()), + authInfoBytes: makeAuthInfoBytes( + pubkeys, + modifiedFeeAmount, + modifiedGasLimit, + sequence, + cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_DIRECT, + ), + }; + return super.signDirect(address, modifiedSignDoc); + } +} diff --git a/packages/stargate/types/encoding.d.ts b/packages/stargate/types/encoding.d.ts index 6404c1ca..af3b9e44 100644 --- a/packages/stargate/types/encoding.d.ts +++ b/packages/stargate/types/encoding.d.ts @@ -1 +1,2 @@ export declare function getMsgType(typeUrl: string): string; +export declare function getMsgTypeUrl(type: string): string;