diff --git a/CHANGELOG.md b/CHANGELOG.md index 44bbe903..7a9eed7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,14 @@ 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. 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. +- @cosmjs/stargate: Add `makeMultisignedTx` which allows you to assemble a + transaction signed by a multisig account. ### Changed 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..b04c1673 100644 --- a/packages/proto-signing/src/pubkey.ts +++ b/packages/proto-signing/src/pubkey.ts @@ -1,36 +1,73 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { encodeSecp256k1Pubkey, SinglePubkey as AminoPubKey } from "@cosmjs/amino"; +import { + encodeSecp256k1Pubkey, + isMultisigThresholdPubkey, + isSecp256k1Pubkey, + MultisigThresholdPubkey, + 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 { - 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`); } diff --git a/packages/stargate/src/index.ts b/packages/stargate/src/index.ts index 384f3d82..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, @@ -34,6 +35,7 @@ export { } from "./stargateclient"; export { defaultRegistryTypes, + SignerData, SigningStargateClient, SigningStargateClientOptions, } from "./signingstargateclient"; diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts new file mode 100644 index 00000000..d6859597 --- /dev/null +++ b/packages/stargate/src/multisignature.spec.ts @@ -0,0 +1,266 @@ +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 { makeCompactBitArray, makeMultisignedTx } from "./multisignature"; +import { SignerData, SigningStargateClient } from "./signingstargateclient"; +import { assertIsBroadcastTxSuccess, StargateClient } 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(); + const multisigAccountAddress = "cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"; + + // 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: 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(), + }; + + return { + accountNumber: accountOnChain.accountNumber, + sequence: accountOnChain.sequence, + chainId: await client.getChainId(), + msgs: [msg], + fee: fee, + memo: "Use your tokens wisely", + }; + })(); + + 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. + { + 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); + } + }); + }); +}); diff --git a/packages/stargate/src/multisignature.ts b/packages/stargate/src/multisignature.ts new file mode 100644 index 00000000..08b579c8 --- /dev/null +++ b/packages/stargate/src/multisignature.ts @@ -0,0 +1,74 @@ +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 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, + fee: StdFee, + bodyBytes: Uint8Array, + signatures: Map, +): TxRaw { + const addresses = Array.from(signatures.keys()); + const prefix = Bech32.decode(addresses[0]).prefix; + + 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) { + signers[i] = true; + signaturesList.push(signature); + } + } + + const signerInfo: SignerInfo = { + publicKey: encodePubkey(multisigPubkey), + modeInfo: { + multi: { + bitarray: makeCompactBitArray(signers), + 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/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/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 869c4f12..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"; @@ -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 = { @@ -429,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 49b30cba..c16d5a2d 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"; @@ -97,6 +98,17 @@ function createDefaultRegistry(): Registry { return new Registry(defaultRegistryTypes); } +/** + * 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; +} + /** Use for testing only */ export interface PrivateSigningStargateClient { readonly fees: CosmosFeeTable; @@ -127,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, ) { @@ -168,6 +196,54 @@ export class SigningStargateClient extends StargateClient { fee: StdFee, memo = "", ): Promise { + 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. + * + * 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 + * (See the SigningStargateClient.offline constructor). + */ + public async sign( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + explicitSignerData?: SignerData, + ): Promise { + 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 }; + } + + return isOfflineDirectSigner(this.signer) + ? this.signDirect(signerAddress, messages, fee, memo, signerData) + : this.signAmino(signerAddress, messages, fee, memo, signerData); + } + + private async signAmino( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + { accountNumber, sequence, chainId }: SignerData, + ): Promise { + assert(!isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( (account) => account.address === signerAddress, ); @@ -175,36 +251,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, - }; - 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); @@ -226,12 +272,44 @@ 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 this.broadcastTx(signedTx); + } + + private async signDirect( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + { accountNumber, sequence, chainId }: SignerData, + ): 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 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); + return TxRaw.fromPartial({ + bodyBytes: signed.bodyBytes, + authInfoBytes: signed.authInfoBytes, + signatures: [fromBase64(signature.signature)], + }); } } 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, diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index f98fc7fc..8b093eb3 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 = { @@ -54,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 */ 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": [