Merge pull request #373 from CosmWasm/272-stargate-broadcast-tx

Add broadcastTx method to Stargate client
This commit is contained in:
Simon Warta 2020-08-12 13:38:44 +02:00 committed by GitHub
commit f969e661e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 219 additions and 16 deletions

View File

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

View File

@ -1,3 +1,6 @@
export { omitDefaults } from "./adr27";
export { decodeAny } from "./any";
export { Coin } from "./msgs";
export { cosmosField } from "./decorator";
export { Registry } from "./registry";
export { makeSignBytes } from "./signing";

View File

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

View File

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

View File

@ -1,3 +1,6 @@
export { omitDefaults } from "./adr27";
export { decodeAny } from "./any";
export { Coin } from "./msgs";
export { cosmosField } from "./decorator";
export { Registry } from "./registry";
export { makeSignBytes } from "./signing";

View File

@ -0,0 +1,2 @@
import { cosmos } from "./generated/codecimpl";
export declare function makeSignBytes(signDoc: cosmos.tx.ISignDoc): Uint8Array;

View File

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

View File

@ -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<BroadcastTxResponse> {
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<Uint8Array> {
const response = await this.tmClient.abciQuery({
// we need the StoreKey for the module, not the module name

View File

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

View File

@ -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<readonly Coin[]>;
disconnect(): void;
broadcastTx(tx: Uint8Array): Promise<BroadcastTxResponse>;
private queryVerified;
private queryUnverified;
}

View File

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

View File

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

View File

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