From c126307bfd000e0e253342f21ffa384b5cb2d4ac Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 11 Aug 2020 17:52:52 +0200 Subject: [PATCH 1/5] launchpad: Improve test example --- packages/launchpad/src/cosmosclient.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/launchpad/src/cosmosclient.spec.ts b/packages/launchpad/src/cosmosclient.spec.ts index 032e3f30..562ca66c 100644 --- a/packages/launchpad/src/cosmosclient.spec.ts +++ b/packages/launchpad/src/cosmosclient.spec.ts @@ -198,7 +198,7 @@ describe("CosmosClient", () => { const [{ address: walletAddress }] = accounts; const client = new CosmosClient(wasmd.endpoint); - const memo = "My first contract on chain"; + const memo = "Test send"; const sendMsg: MsgSend = { type: "cosmos-sdk/MsgSend", value: { From f53425d7ff0b144f431bdf30308afd44655e5ea2 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 11 Aug 2020 17:53:21 +0200 Subject: [PATCH 2/5] proto-signing: Export omitDefaults and Registry from index --- packages/proto-signing/src/index.ts | 2 ++ packages/proto-signing/types/index.d.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/proto-signing/src/index.ts b/packages/proto-signing/src/index.ts index 28e7bc70..37d59ef4 100644 --- a/packages/proto-signing/src/index.ts +++ b/packages/proto-signing/src/index.ts @@ -1,3 +1,5 @@ +export { omitDefaults } from "./adr27"; export { decodeAny } from "./any"; export { Coin } from "./msgs"; export { cosmosField } from "./decorator"; +export { Registry } from "./registry"; diff --git a/packages/proto-signing/types/index.d.ts b/packages/proto-signing/types/index.d.ts index 28e7bc70..37d59ef4 100644 --- a/packages/proto-signing/types/index.d.ts +++ b/packages/proto-signing/types/index.d.ts @@ -1,3 +1,5 @@ +export { omitDefaults } from "./adr27"; export { decodeAny } from "./any"; export { Coin } from "./msgs"; export { cosmosField } from "./decorator"; +export { Registry } from "./registry"; From 53b216d1fbfb3fb96a45cae3bb5313bd3a654a03 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 12 Aug 2020 12:40:59 +0200 Subject: [PATCH 3/5] proto-signing: Add and export makeSignBytes helper function --- packages/proto-signing/src/index.ts | 1 + packages/proto-signing/src/signing.spec.ts | 3 ++- packages/proto-signing/src/signing.ts | 8 ++++++++ packages/proto-signing/types/index.d.ts | 1 + packages/proto-signing/types/signing.d.ts | 2 ++ 5 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 packages/proto-signing/src/signing.ts create mode 100644 packages/proto-signing/types/signing.d.ts diff --git a/packages/proto-signing/src/index.ts b/packages/proto-signing/src/index.ts index 37d59ef4..ce95215d 100644 --- a/packages/proto-signing/src/index.ts +++ b/packages/proto-signing/src/index.ts @@ -3,3 +3,4 @@ export { decodeAny } from "./any"; export { Coin } from "./msgs"; export { cosmosField } from "./decorator"; export { Registry } from "./registry"; +export { makeSignBytes } from "./signing"; diff --git a/packages/proto-signing/src/signing.spec.ts b/packages/proto-signing/src/signing.spec.ts index 2e9ebcc0..8f0b5614 100644 --- a/packages/proto-signing/src/signing.spec.ts +++ b/packages/proto-signing/src/signing.spec.ts @@ -6,6 +6,7 @@ import { omitDefaults } from "./adr27"; import { cosmos } from "./generated/codecimpl"; import { defaultRegistry } from "./msgs"; import { Registry, TxBodyValue } from "./registry"; +import { makeSignBytes } from "./signing"; const { AuthInfo, SignDoc, Tx, TxBody } = cosmos.tx; const { PublicKey } = cosmos.crypto; @@ -158,7 +159,7 @@ describe("signing demo", () => { accountSequence: sequenceNumber, }), ); - const signDocBytes = Uint8Array.from(SignDoc.encode(signDoc).finish()); + const signDocBytes = makeSignBytes(signDoc); expect(toHex(signDocBytes)).toEqual(signBytes); const signature = await wallet.sign(address, signDocBytes); diff --git a/packages/proto-signing/src/signing.ts b/packages/proto-signing/src/signing.ts new file mode 100644 index 00000000..1be4332e --- /dev/null +++ b/packages/proto-signing/src/signing.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { cosmos } from "./generated/codecimpl"; + +const { SignDoc } = cosmos.tx; + +export function makeSignBytes(signDoc: cosmos.tx.ISignDoc): Uint8Array { + return Uint8Array.from(SignDoc.encode(signDoc).finish()); +} diff --git a/packages/proto-signing/types/index.d.ts b/packages/proto-signing/types/index.d.ts index 37d59ef4..ce95215d 100644 --- a/packages/proto-signing/types/index.d.ts +++ b/packages/proto-signing/types/index.d.ts @@ -3,3 +3,4 @@ export { decodeAny } from "./any"; export { Coin } from "./msgs"; export { cosmosField } from "./decorator"; export { Registry } from "./registry"; +export { makeSignBytes } from "./signing"; diff --git a/packages/proto-signing/types/signing.d.ts b/packages/proto-signing/types/signing.d.ts new file mode 100644 index 00000000..a6b776ff --- /dev/null +++ b/packages/proto-signing/types/signing.d.ts @@ -0,0 +1,2 @@ +import { cosmos } from "./generated/codecimpl"; +export declare function makeSignBytes(signDoc: cosmos.tx.ISignDoc): Uint8Array; From d9520990dd09053172d584856d7ad3401ddc71fd Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 12 Aug 2020 13:19:19 +0200 Subject: [PATCH 4/5] tendermint-rpc: Update BroadcastTxCommitResponse type --- packages/tendermint-rpc/src/responses.ts | 10 +++++----- packages/tendermint-rpc/src/v0-33/responses.ts | 4 ++-- packages/tendermint-rpc/types/responses.d.ts | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/tendermint-rpc/src/responses.ts b/packages/tendermint-rpc/src/responses.ts index 049ad805..1eb25b6f 100644 --- a/packages/tendermint-rpc/src/responses.ts +++ b/packages/tendermint-rpc/src/responses.ts @@ -69,20 +69,20 @@ export function broadcastTxSyncSuccess(res: BroadcastTxSyncResponse): boolean { } export interface BroadcastTxCommitResponse { - readonly height?: number; + readonly height: number; readonly hash: TxHash; readonly checkTx: TxData; readonly deliverTx?: TxData; } /** - * Returns true iff transaction made it sucessfully into a block - * (i.e. sucess in `check_tx` and `deliver_tx` field) + * Returns true iff transaction made it successfully into a block + * (i.e. success in `check_tx` and `deliver_tx` field) */ -export function broadcastTxCommitSuccess(res: BroadcastTxCommitResponse): boolean { +export function broadcastTxCommitSuccess(response: BroadcastTxCommitResponse): boolean { // code must be 0 on success // deliverTx may be present but empty on failure - return res.checkTx.code === 0 && !!res.deliverTx && res.deliverTx.code === 0; + return response.checkTx.code === 0 && !!response.deliverTx && response.deliverTx.code === 0; } export interface CommitResponse { diff --git a/packages/tendermint-rpc/src/v0-33/responses.ts b/packages/tendermint-rpc/src/v0-33/responses.ts index 76e12b5a..8e78ddcc 100644 --- a/packages/tendermint-rpc/src/v0-33/responses.ts +++ b/packages/tendermint-rpc/src/v0-33/responses.ts @@ -345,7 +345,7 @@ function decodeBroadcastTxSync(data: RpcBroadcastTxSyncResponse): responses.Broa } interface RpcBroadcastTxCommitResponse { - readonly height?: IntegerString; + readonly height: IntegerString; readonly hash: HexString; readonly check_tx: RpcTxData; readonly deliver_tx?: RpcTxData; @@ -353,7 +353,7 @@ interface RpcBroadcastTxCommitResponse { function decodeBroadcastTxCommit(data: RpcBroadcastTxCommitResponse): responses.BroadcastTxCommitResponse { return { - height: may(Integer.parse, data.height), + height: Integer.parse(data.height), hash: fromHex(assertNotEmpty(data.hash)) as TxHash, checkTx: decodeTxData(assertObject(data.check_tx)), deliverTx: may(decodeTxData, data.deliver_tx), diff --git a/packages/tendermint-rpc/types/responses.d.ts b/packages/tendermint-rpc/types/responses.d.ts index ff55fb74..26446072 100644 --- a/packages/tendermint-rpc/types/responses.d.ts +++ b/packages/tendermint-rpc/types/responses.d.ts @@ -55,16 +55,16 @@ export interface BroadcastTxSyncResponse extends TxData { */ export declare function broadcastTxSyncSuccess(res: BroadcastTxSyncResponse): boolean; export interface BroadcastTxCommitResponse { - readonly height?: number; + readonly height: number; readonly hash: TxHash; readonly checkTx: TxData; readonly deliverTx?: TxData; } /** - * Returns true iff transaction made it sucessfully into a block - * (i.e. sucess in `check_tx` and `deliver_tx` field) + * Returns true iff transaction made it successfully into a block + * (i.e. success in `check_tx` and `deliver_tx` field) */ -export declare function broadcastTxCommitSuccess(res: BroadcastTxCommitResponse): boolean; +export declare function broadcastTxCommitSuccess(response: BroadcastTxCommitResponse): boolean; export interface CommitResponse { readonly header: Header; readonly commit: Commit; From 3cb3bf14a3ab5fdd843a76911ef981fcce1f09ce Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 11 Aug 2020 18:01:27 +0200 Subject: [PATCH 5/5] stargate: Add broadcastTx method to client --- packages/stargate/src/stargateclient.spec.ts | 95 +++++++++++++++++++- packages/stargate/src/stargateclient.ts | 58 +++++++++++- packages/stargate/src/testutils.spec.ts | 16 ++++ packages/stargate/types/stargateclient.d.ts | 23 +++++ 4 files changed, 189 insertions(+), 3 deletions(-) diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index 821c1fc2..c3f37ba6 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -1,7 +1,23 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Bech32, fromBase64 } from "@cosmjs/encoding"; +import { Secp256k1Wallet } from "@cosmjs/launchpad"; +import { makeSignBytes, omitDefaults, Registry } from "@cosmjs/proto-signing"; import { assert, sleep } from "@cosmjs/utils"; -import { PrivateStargateClient, StargateClient } from "./stargateclient"; -import { nonExistentAddress, pendingWithoutSimapp, simapp, unused, validator } from "./testutils.spec"; +import { cosmos } from "./generated/codecimpl"; +import { assertIsBroadcastTxSuccess, PrivateStargateClient, StargateClient } from "./stargateclient"; +import { + faucet, + makeRandomAddressBytes, + nonExistentAddress, + pendingWithoutSimapp, + simapp, + unused, + validator, +} from "./testutils.spec"; + +const { AuthInfo, SignDoc, Tx, TxBody } = cosmos.tx; +const { PublicKey } = cosmos.crypto; describe("StargateClient", () => { describe("connect", () => { @@ -182,4 +198,79 @@ describe("StargateClient", () => { expect(balances).toEqual([]); }); }); + + describe("broadcastTx", () => { + it("broadcasts a transaction", async () => { + pendingWithoutSimapp(); + const client = await StargateClient.connect(simapp.tendermintUrl); + const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); + const [{ address, pubkey: pubkeyBytes }] = await wallet.getAccounts(); + const publicKey = PublicKey.create({ secp256k1: pubkeyBytes }); + const registry = new Registry(); + const txBodyFields = { + typeUrl: "/cosmos.tx.TxBody", + value: { + messages: [ + { + typeUrl: "/cosmos.bank.MsgSend", + value: { + fromAddress: Bech32.decode(address).data, + toAddress: makeRandomAddressBytes(), + amount: [ + { + denom: "ucosm", + amount: "1234567", + }, + ], + }, + }, + ], + }, + }; + const txBodyBytes = registry.encode(txBodyFields); + const txBody = TxBody.decode(txBodyBytes); + const authInfo = { + signerInfos: [ + { + publicKey: publicKey, + modeInfo: { + single: { + mode: cosmos.tx.signing.SignMode.SIGN_MODE_DIRECT, + }, + }, + }, + ], + fee: { + gasLimit: 200000, + }, + }; + const authInfoBytes = Uint8Array.from(AuthInfo.encode(authInfo).finish()); + + const chainId = await client.getChainId(); + const { accountNumber, sequence } = (await client.getSequence(address))!; + const signDoc = SignDoc.create( + omitDefaults({ + bodyBytes: txBodyBytes, + authInfoBytes: authInfoBytes, + chainId: chainId, + accountNumber: accountNumber, + accountSequence: sequence, + }), + ); + const signDocBytes = makeSignBytes(signDoc); + const signature = await wallet.sign(address, signDocBytes); + const txRaw = Tx.create({ + body: txBody, + authInfo: authInfo, + signatures: [fromBase64(signature.signature)], + }); + const txRawBytes = Uint8Array.from(Tx.encode(txRaw).finish()); + const txResult = await client.broadcastTx(txRawBytes); + assertIsBroadcastTxSuccess(txResult); + + const { rawLog, transactionHash } = txResult; + expect(rawLog).toMatch(/{"key":"amount","value":"1234567ucosm"}/); + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + }); + }); }); diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 433fa794..14647007 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -3,7 +3,7 @@ import { Bech32, toAscii, toHex } from "@cosmjs/encoding"; import { Coin, decodeAminoPubkey, PubKey } from "@cosmjs/launchpad"; import { Uint64 } from "@cosmjs/math"; import { decodeAny } from "@cosmjs/proto-signing"; -import { Client as TendermintClient } from "@cosmjs/tendermint-rpc"; +import { broadcastTxCommitSuccess, Client as TendermintClient } from "@cosmjs/tendermint-rpc"; import { arrayContentEquals, assert, assertDefined } from "@cosmjs/utils"; import Long from "long"; @@ -22,6 +22,44 @@ export interface SequenceResponse { readonly sequence: number; } +export interface BroadcastTxFailure { + readonly height: number; + readonly code: number; + readonly transactionHash: string; + readonly rawLog?: string; + readonly data?: Uint8Array; +} + +export interface BroadcastTxSuccess { + readonly height: number; + readonly transactionHash: string; + readonly rawLog?: string; + readonly data?: Uint8Array; +} + +export type BroadcastTxResponse = BroadcastTxSuccess | BroadcastTxFailure; + +export function isBroadcastTxFailure(result: BroadcastTxResponse): result is BroadcastTxFailure { + return !!(result as BroadcastTxFailure).code; +} + +export function isBroadcastTxSuccess(result: BroadcastTxResponse): result is BroadcastTxSuccess { + return !isBroadcastTxFailure(result); +} + +/** + * Ensures the given result is a success. Throws a detailed error message otherwise. + */ +export function assertIsBroadcastTxSuccess( + result: BroadcastTxResponse, +): asserts result is BroadcastTxSuccess { + if (isBroadcastTxFailure(result)) { + throw new Error( + `Error when broadcasting tx ${result.transactionHash} at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}`, + ); + } +} + function uint64FromProto(input: number | Long): Uint64 { return Uint64.fromString(input.toString()); } @@ -155,6 +193,24 @@ export class StargateClient { this.tmClient.disconnect(); } + public async broadcastTx(tx: Uint8Array): Promise { + const response = await this.tmClient.broadcastTxCommit({ tx }); + return broadcastTxCommitSuccess(response) + ? { + height: response.height, + transactionHash: toHex(response.hash).toUpperCase(), + rawLog: response.deliverTx?.log, + data: response.deliverTx?.data, + } + : { + height: response.height, + code: response.checkTx.code, + transactionHash: toHex(response.hash).toUpperCase(), + rawLog: response.checkTx.log, + data: response.checkTx.data, + }; + } + private async queryVerified(store: string, key: Uint8Array): Promise { const response = await this.tmClient.abciQuery({ // we need the StoreKey for the module, not the module name diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index fa7ef050..ea501b86 100644 --- a/packages/stargate/src/testutils.spec.ts +++ b/packages/stargate/src/testutils.spec.ts @@ -1,9 +1,15 @@ +import { Random } from "@cosmjs/crypto"; + export function pendingWithoutSimapp(): void { if (!process.env.SIMAPP_ENABLED) { return pending("Set SIMAPP_ENABLED to enable Simapp based tests"); } } +export function makeRandomAddressBytes(): Uint8Array { + return Random.getBytes(20); +} + export const simapp = { tendermintUrl: "localhost:26657", chainId: "simd-testing", @@ -12,6 +18,16 @@ export const simapp = { blockTime: 1_000, // ms }; +export const faucet = { + mnemonic: + "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone", + pubkey0: { + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }, + address0: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", +}; + /** Unused account */ export const unused = { pubkey: { diff --git a/packages/stargate/types/stargateclient.d.ts b/packages/stargate/types/stargateclient.d.ts index a7d3781e..69123a7b 100644 --- a/packages/stargate/types/stargateclient.d.ts +++ b/packages/stargate/types/stargateclient.d.ts @@ -11,6 +11,28 @@ export interface SequenceResponse { readonly accountNumber: number; readonly sequence: number; } +export interface BroadcastTxFailure { + readonly height: number; + readonly code: number; + readonly transactionHash: string; + readonly rawLog?: string; + readonly data?: Uint8Array; +} +export interface BroadcastTxSuccess { + readonly height: number; + readonly transactionHash: string; + readonly rawLog?: string; + readonly data?: Uint8Array; +} +export declare type BroadcastTxResponse = BroadcastTxSuccess | BroadcastTxFailure; +export declare function isBroadcastTxFailure(result: BroadcastTxResponse): result is BroadcastTxFailure; +export declare function isBroadcastTxSuccess(result: BroadcastTxResponse): result is BroadcastTxSuccess; +/** + * Ensures the given result is a success. Throws a detailed error message otherwise. + */ +export declare function assertIsBroadcastTxSuccess( + result: BroadcastTxResponse, +): asserts result is BroadcastTxSuccess; /** Use for testing only */ export interface PrivateStargateClient { readonly tmClient: TendermintClient; @@ -33,6 +55,7 @@ export declare class StargateClient { */ getAllBalancesUnverified(address: string): Promise; disconnect(): void; + broadcastTx(tx: Uint8Array): Promise; private queryVerified; private queryUnverified; }