Merge pull request #723 from cosmos/allow-sign-without-breadcast

Implement and demo usage of multisig account
This commit is contained in:
mergify[bot] 2021-03-24 15:51:45 +00:00 committed by GitHub
commit bd7644332b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1073 additions and 76 deletions

View File

@ -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

View File

@ -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" \

View File

@ -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>): 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> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;

View File

@ -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`);
}

View File

@ -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";

View File

@ -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<string, Uint8Array>([
[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);
}
});
});
});

View File

@ -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<string, Uint8Array>,
): 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<Uint8Array>();
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;
}

View File

@ -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,
});

View File

@ -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>): 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);
});
});
});
});

View File

@ -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<SigningStargateClient> {
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<BroadcastTxResponse> {
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<TxRaw> {
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<TxRaw> {
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<TxRaw> {
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)],
});
}
}

View File

@ -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<StargateClient> {
@ -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<string> {
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<number> {
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<Account | null> {
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<Account | null> {
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<Block> {
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<Coin | null> {
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<readonly Coin[]> {
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<BroadcastTxResponse> {
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<readonly IndexedTx[]> {
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,

View File

@ -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 */

View File

@ -8,3 +8,59 @@ Run the following:
cd scripts/simapp
./start.sh
```
## Preset accounts
1. **Faucet**<br>
economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone<br>
Address 0: cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6<br>
Address 1: cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5<br>
Address 2: cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k<br>
Address 3: cosmos142u9fgcjdlycfcez3lw8x6x5h7rfjlnfhpw2lx<br>
Address 4: cosmos1hsm76p4ahyhl5yh3ve9ur49r5kemhp2r0dcjvx<br>
Pubkey 0: A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ<br>
Pubkey 1: AiDosfIbBi54XJ1QjCeApumcy/FjdtF+YhywPf3DKTx7<br>
Pubkey 2: AzQg33JZqH7vSsm09esZY5bZvmzYwE/SY78cA0iLxpD7<br>
Pubkey 3: A3gOAlB6aiRTCPvWMQg2+ZbGYNsLd8qlvV28m8p2UhY2<br>
Pubkey 4: Aum2063ub/ErUnIUB36sK55LktGUStgcbSiaAnL1wadu
2. **Alice**: Test account for the cosmwasm package that can run in parallel with faucet without sequence conflicts<br>
enlist hip relief stomach skate base shallow young switch frequent cry park<br>
Address 0: cosmos14qemq0vw6y3gc3u3e0aty2e764u4gs5le3hada<br>
Address 1: cosmos1hhg2rlu9jscacku2wwckws7932qqqu8x3gfgw0<br>
Address 2: cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5<br>
Address 3: cosmos17yg9mssjenmc3jkqth6ulcwj9cxujrxxzezwta<br>
Address 4: cosmos1f7j7ryulwjfe9ljplvhtcaxa6wqgula3etktce<br>
Pubkey 0: A9cXhWb8ZpqCzkA8dQCPV29KdeRLV3rUYxrkHudLbQtS<br>
Pubkey 1: A4XluzvcUx0ViLF0DjYW5/noArGwpltDstoUUZo+g1b0<br>
Pubkey 2: A5TKr1NKc/MKRJ7+EHDD9PlzmGaPD/di/6hzZyBwxoy5<br>
Pubkey 3: A/HSABDUqMB2qDy+PA7fiuuuA+hfrco2VwwiThMiTzUx<br>
Pubkey 4: A7usTiqgqfxL/WKhoephDUSCHBQlLagtwI/qTmEteTRM
3. **Bob**: Test account (unused for now)<br>
remain fragile remove stamp quiz bus country dress critic mammal office need<br>
Address 0: cosmos1lvrwcvrqlc5ktzp2c4t22xgkx29q3y83lktgzl<br>
Address 1: cosmos1vkv9sfwaak76weyamqx0flmng2vuquxqcuqukh<br>
Address 2: cosmos106jwym4s9aujcmes26myzzwqsccw09sdm0v5au<br>
Address 3: cosmos1c7wpeen2uv8thayf7g8q2rgpm29clj0dgrdtzw<br>
Address 4: cosmos1mjxpv9ft30wer7ma7kwfxhm42l379xutplrdk6<br>
Pubkey 0: A0d/GxY+UALE+miWJP0qyq4/EayG1G6tsg24v+cbD6By<br>
Pubkey 1: Agqd6njsVEQD1CR+F2aqEb8hil5NXZ06mjKgetaNC12t<br>
Pubkey 2: A6e9ElvKaM0DKWh1bIdK3bgB14dyEDgIXYMA0Lbs1GoQ<br>
Pubkey 3: AkAK5PQaucieWMb0+tTRY01feYI+upRnoNK556eD0Ibb<br>
Pubkey 4: A5HMVEAJsupdQWItbZv5Z1xZifDixQi6tjU/hJpZY1bF
4. **Unused**: for testing account state; this account never changes balances or sequences<br>
oyster design unusual machine spread century engine gravity focus cave carry slot<br>
ArkCaFUJ/IH+vKBmNRCdUVl3mCAhbopk9jjW4Ko4OfRQ<br>
cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u
5. **Guest**: account for manual testing<br>
degree tackle suggest window test behind mesh extra cover prepare oak script<br>
Am/+YV0LaeqQPu7BDJuDHV7J8y68ptkGs10YS+9s71Nq<br>
cosmos17d0jcz59jf68g52vq38tuuncmwwjk42u6mcxej
### Multisig accounts
1. 1/5 threshold multisig of the first 5 faucet accounts<br>
cosmos1v75snhly7wfndk83ud4daygh397vcmkta8rlzq
2. 2/5 threshold multisig of the first 5 faucet accounts<br>
cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9
3. 3/5 threshold multisig of the first 5 faucet accounts<br>
cosmos1d2mg2euvus3tu2tprfwrfzeal4xu7kygugjxjc

View File

@ -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

View File

@ -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": [