Merge pull request #723 from cosmos/allow-sign-without-breadcast
Implement and demo usage of multisig account
This commit is contained in:
commit
bd7644332b
@ -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
|
||||
|
||||
|
||||
@ -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" \
|
||||
|
||||
106
packages/proto-signing/src/codec/cosmos/crypto/multisig/keys.ts
Normal file
106
packages/proto-signing/src/codec/cosmos/crypto/multisig/keys.ts
Normal 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>;
|
||||
@ -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`);
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
266
packages/stargate/src/multisignature.spec.ts
Normal file
266
packages/stargate/src/multisignature.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
74
packages/stargate/src/multisignature.ts
Normal file
74
packages/stargate/src/multisignature.ts
Normal 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;
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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": [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user