From 651af7d6537202b3e9951c980b1c42f073333fbf Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 24 Mar 2021 15:31:50 +0100 Subject: [PATCH] amino: Transfer signdoc functions from launchpad --- packages/amino/src/index.ts | 12 +-- packages/amino/src/signdoc.spec.ts | 135 +++++++++++++++++++++++++++++ packages/amino/src/signdoc.ts | 76 ++++++++++++++++ packages/amino/src/signer.ts | 30 +------ 4 files changed, 214 insertions(+), 39 deletions(-) create mode 100644 packages/amino/src/signdoc.spec.ts create mode 100644 packages/amino/src/signdoc.ts diff --git a/packages/amino/src/index.ts b/packages/amino/src/index.ts index 42ca9252..c45fd5bb 100644 --- a/packages/amino/src/index.ts +++ b/packages/amino/src/index.ts @@ -20,13 +20,5 @@ export { } from "./pubkeys"; export { createMultisigThresholdPubkey } from "./multisig"; export { decodeSignature, encodeSecp256k1Signature, StdSignature } from "./signature"; -export { - AccountData, - Algo, - AminoMsg, - AminoSignResponse, - Coin, - OfflineAminoSigner, - StdFee, - StdSignDoc, -} from "./signer"; +export { AminoMsg, Coin, makeSignDoc, serializeSignDoc, StdFee, StdSignDoc } from "./signdoc"; +export { AccountData, Algo, AminoSignResponse, OfflineAminoSigner } from "./signer"; diff --git a/packages/amino/src/signdoc.spec.ts b/packages/amino/src/signdoc.spec.ts new file mode 100644 index 00000000..e23b705e --- /dev/null +++ b/packages/amino/src/signdoc.spec.ts @@ -0,0 +1,135 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Random } from "@cosmjs/crypto"; +import { Bech32 } from "@cosmjs/encoding"; + +import { AminoMsg, makeSignDoc, sortedJsonStringify } from "./signdoc"; + +function makeRandomAddress(): string { + return Bech32.encode("cosmos", Random.getBytes(20)); +} +const testAddress = "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6"; +const testValidatorAddress = "cosmosvaloper1yfkkk04ve8a0sugj4fe6q6zxuvmvza8r3arurr"; + +describe("encoding", () => { + describe("sortedJsonStringify", () => { + it("leaves non-objects unchanged", () => { + expect(sortedJsonStringify(true)).toEqual(`true`); + expect(sortedJsonStringify(false)).toEqual(`false`); + expect(sortedJsonStringify("aabbccdd")).toEqual(`"aabbccdd"`); + expect(sortedJsonStringify(75)).toEqual(`75`); + expect(sortedJsonStringify(null)).toEqual(`null`); + expect(sortedJsonStringify([5, 6, 7, 1])).toEqual(`[5,6,7,1]`); + expect(sortedJsonStringify([5, ["a", "b"], true, null, 1])).toEqual(`[5,["a","b"],true,null,1]`); + }); + + it("sorts objects by key", () => { + // already sorted + expect(sortedJsonStringify({})).toEqual(`{}`); + expect(sortedJsonStringify({ a: 3 })).toEqual(`{"a":3}`); + expect(sortedJsonStringify({ a: 3, b: 2, c: 1 })).toEqual(`{"a":3,"b":2,"c":1}`); + + // not yet sorted + expect(sortedJsonStringify({ b: 2, a: 3, c: 1 })).toEqual(`{"a":3,"b":2,"c":1}`); + expect(sortedJsonStringify({ aaa: true, aa: true, a: true })).toEqual( + `{"a":true,"aa":true,"aaa":true}`, + ); + }); + + it("sorts nested objects", () => { + // already sorted + expect(sortedJsonStringify({ x: { y: { z: null } } })).toEqual(`{"x":{"y":{"z":null}}}`); + + // not yet sorted + expect(sortedJsonStringify({ b: { z: true, x: true, y: true }, a: true, c: true })).toEqual( + `{"a":true,"b":{"x":true,"y":true,"z":true},"c":true}`, + ); + }); + + it("sorts objects in arrays", () => { + // already sorted + expect(sortedJsonStringify([1, 2, { x: { y: { z: null } } }, 4])).toEqual( + `[1,2,{"x":{"y":{"z":null}}},4]`, + ); + + // not yet sorted + expect(sortedJsonStringify([1, 2, { b: { z: true, x: true, y: true }, a: true, c: true }, 4])).toEqual( + `[1,2,{"a":true,"b":{"x":true,"y":true,"z":true},"c":true},4]`, + ); + }); + }); + + describe("makeSignDoc", () => { + it("works", () => { + const chainId = "testspace-12"; + const msg1: AminoMsg = { + type: "cosmos-sdk/MsgDelegate", + value: { + delegator_address: testAddress, + validator_address: testValidatorAddress, + amount: { amount: "1234", denom: "ustake" }, + }, + }; + const msg2: AminoMsg = { + type: "cosmos-sdk/MsgSend", + value: { + from_address: testAddress, + to_address: makeRandomAddress(), + amount: [{ amount: "1234567", denom: "ucosm" }], + }, + }; + const fee = { + amount: [{ amount: "2000", denom: "ucosm" }], + gas: "180000", // 180k + }; + const memo = "Use your power wisely"; + const accountNumber = 15; + const sequence = 16; + + const signDoc = makeSignDoc([msg1, msg2], fee, chainId, memo, accountNumber, sequence); + expect(signDoc).toEqual({ + msgs: [msg1, msg2], + fee: fee, + chain_id: chainId, + account_number: accountNumber.toString(), + sequence: sequence.toString(), + memo: memo, + }); + }); + + it("works with undefined memo", () => { + const chainId = "testspace-12"; + const msg1: AminoMsg = { + type: "cosmos-sdk/MsgDelegate", + value: { + delegator_address: testAddress, + validator_address: testValidatorAddress, + amount: { amount: "1234", denom: "ustake" }, + }, + }; + const msg2: AminoMsg = { + type: "cosmos-sdk/MsgSend", + value: { + from_address: testAddress, + to_address: makeRandomAddress(), + amount: [{ amount: "1234567", denom: "ucosm" }], + }, + }; + const fee = { + amount: [{ amount: "2000", denom: "ucosm" }], + gas: "180000", // 180k + }; + const accountNumber = 15; + const sequence = 16; + + const signDoc = makeSignDoc([msg1, msg2], fee, chainId, undefined, accountNumber, sequence); + expect(signDoc).toEqual({ + msgs: [msg1, msg2], + fee: fee, + chain_id: chainId, + account_number: accountNumber.toString(), + sequence: sequence.toString(), + memo: "", + }); + }); + }); +}); diff --git a/packages/amino/src/signdoc.ts b/packages/amino/src/signdoc.ts new file mode 100644 index 00000000..bfd75dce --- /dev/null +++ b/packages/amino/src/signdoc.ts @@ -0,0 +1,76 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { toUtf8 } from "@cosmjs/encoding"; +import { Uint53 } from "@cosmjs/math"; + +export interface AminoMsg { + readonly type: string; + readonly value: any; +} + +export interface Coin { + readonly denom: string; + readonly amount: string; +} + +export interface StdFee { + readonly amount: readonly Coin[]; + readonly gas: string; +} + +/** + * The document to be signed + * + * @see https://docs.cosmos.network/master/modules/auth/03_types.html#stdsigndoc + */ +export interface StdSignDoc { + readonly chain_id: string; + readonly account_number: string; + readonly sequence: string; + readonly fee: StdFee; + readonly msgs: readonly AminoMsg[]; + readonly memo: string; +} + +function sortedObject(obj: any): any { + if (typeof obj !== "object" || obj === null) { + return obj; + } + if (Array.isArray(obj)) { + return obj.map(sortedObject); + } + const sortedKeys = Object.keys(obj).sort(); + const result: Record = {}; + // NOTE: Use forEach instead of reduce for performance with large objects eg Wasm code + sortedKeys.forEach((key) => { + result[key] = sortedObject(obj[key]); + }); + return result; +} + +/** Returns a JSON string with objects sorted by key */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function sortedJsonStringify(obj: any): string { + return JSON.stringify(sortedObject(obj)); +} + +export function makeSignDoc( + msgs: readonly AminoMsg[], + fee: StdFee, + chainId: string, + memo: string | undefined, + accountNumber: number | string, + sequence: number | string, +): StdSignDoc { + return { + chain_id: chainId, + account_number: Uint53.fromString(accountNumber.toString()).toString(), + sequence: Uint53.fromString(sequence.toString()).toString(), + fee: fee, + msgs: msgs, + memo: memo || "", + }; +} + +export function serializeSignDoc(signDoc: StdSignDoc): Uint8Array { + return toUtf8(sortedJsonStringify(signDoc)); +} diff --git a/packages/amino/src/signer.ts b/packages/amino/src/signer.ts index cb1abc38..0059cbc1 100644 --- a/packages/amino/src/signer.ts +++ b/packages/amino/src/signer.ts @@ -1,34 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { StdSignature } from "./signature"; - -export interface AminoMsg { - readonly type: string; - readonly value: any; -} - -export interface Coin { - readonly denom: string; - readonly amount: string; -} - -export interface StdFee { - readonly amount: readonly Coin[]; - readonly gas: string; -} - -/** - * The document to be signed - * - * @see https://docs.cosmos.network/master/modules/auth/03_types.html#stdsigndoc - */ -export interface StdSignDoc { - readonly chain_id: string; - readonly account_number: string; - readonly sequence: string; - readonly fee: StdFee; - readonly msgs: readonly AminoMsg[]; - readonly memo: string; -} +import { StdSignDoc } from "./signdoc"; export type Algo = "secp256k1" | "ed25519" | "sr25519";