From 3cb3bf14a3ab5fdd843a76911ef981fcce1f09ce Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 11 Aug 2020 18:01:27 +0200 Subject: [PATCH] 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; }