diff --git a/CHANGELOG.md b/CHANGELOG.md index 720d38c5..1768da89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ - @cosmjs/cosmwasm: Rename `CosmWasmClient.getNonce` method to `.getSequence`. - @cosmjs/cosmwasm: Remove `RestClient` class in favour of new modular `LcdClient` class from @cosmjs/sdk38. +- @cosmjs/cosmwasm: Add `SigningCosmWasmClient.signAndPost` as a mid-level + abstraction between `SigningCosmWasmClient.upload`/`.instantiate`/`.execute` + and `.postTx`. +- @cosmjs/cosmwasm: Use `*PostTx*` types and helpers from @cosmjs/sdk38. Remove + exported `PostTxResult`. - @cosmjs/sdk38: Rename `CosmosClient.getNonce` method to `.getSequence`. - @cosmjs/sdk38: Remove `RestClient` class in favour of new modular `LcdClient` class. @@ -13,3 +18,7 @@ `OfflineSigner` instead of a `SigningCallback`. - @cosmjs/math: Add missing integer check to `Uint64.fromNumber`. Before `Uint64.fromNumber(1.1)` produced some result. +- @cosmjs/sdk38: Add `SigningCosmosClient.signAndPost` as a mid-level + abstraction between `SigningCosmosClient.sendTokens` and `.postTx`. +- @cosmjs/sdk38: Export `PostTxFailure`/`PostTxSuccess` and type checkers + `isPostTxFailure`/`isPostTxSuccess`; export `assertIsPostTxSuccess`. diff --git a/packages/cli/examples/delegate.ts b/packages/cli/examples/delegate.ts index f518989c..1a99e19e 100644 --- a/packages/cli/examples/delegate.ts +++ b/packages/cli/examples/delegate.ts @@ -17,7 +17,7 @@ const msg: MsgDelegate = { }; const fee = { amount: coins(2000, "ucosm"), - gas: "120000", // 120k + gas: "180000", // 180k }; const memo = "Use your power wisely"; diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index cb53e7b9..fa7f892f 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -42,7 +42,6 @@ export function main(originalArgs: readonly string[]): void { "ContractDetails", "CosmWasmClient", "GetSequenceResult", - "PostTxResult", "SearchByHeightQuery", "SearchByIdQuery", "SearchBySentFromOrToQuery", @@ -98,6 +97,7 @@ export function main(originalArgs: readonly string[]): void { "MsgSend", "LcdClient", "OfflineSigner", + "PostTxResult", "PubKey", "pubkeyToAddress", "Secp256k1Wallet", diff --git a/packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts b/packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts index 8fc6fcf0..6d902e95 100644 --- a/packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts +++ b/packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts @@ -4,6 +4,7 @@ import { coins, CosmosSdkTx, isMsgSend, + isPostTxFailure, LcdClient, makeSignBytes, MsgSend, @@ -11,7 +12,7 @@ import { } from "@cosmjs/sdk38"; import { assert, sleep } from "@cosmjs/utils"; -import { CosmWasmClient, isPostTxFailure } from "./cosmwasmclient"; +import { CosmWasmClient } from "./cosmwasmclient"; import { isMsgExecuteContract, isMsgInstantiateContract } from "./msgs"; import { SigningCosmWasmClient } from "./signingcosmwasmclient"; import { diff --git a/packages/cosmwasm/src/cosmwasmclient.spec.ts b/packages/cosmwasm/src/cosmwasmclient.spec.ts index 5f4121d6..8873281c 100644 --- a/packages/cosmwasm/src/cosmwasmclient.spec.ts +++ b/packages/cosmwasm/src/cosmwasmclient.spec.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Sha256 } from "@cosmjs/crypto"; import { Bech32, fromHex, fromUtf8, toAscii, toBase64 } from "@cosmjs/encoding"; -import { makeSignBytes, MsgSend, Secp256k1Wallet, StdFee } from "@cosmjs/sdk38"; +import { assertIsPostTxSuccess, makeSignBytes, MsgSend, Secp256k1Wallet, StdFee } from "@cosmjs/sdk38"; import { assert, sleep } from "@cosmjs/utils"; import { ReadonlyDate } from "readonly-date"; -import { Code, CosmWasmClient, isPostTxFailure, PrivateCosmWasmClient } from "./cosmwasmclient"; +import { Code, CosmWasmClient, PrivateCosmWasmClient } from "./cosmwasmclient"; import { findAttribute } from "./logs"; import { SigningCosmWasmClient } from "./signingcosmwasmclient"; import cosmoshub from "./testdata/cosmoshub.json"; @@ -243,7 +243,7 @@ describe("CosmWasmClient", () => { signatures: [signature], }; const result = await client.postTx(signedTx); - assert(!isPostTxFailure(result)); + assertIsPostTxSuccess(result); const { logs, transactionHash } = result; const amountAttr = findAttribute(logs, "transfer", "amount"); expect(amountAttr.value).toEqual("1234567ucosm"); diff --git a/packages/cosmwasm/src/cosmwasmclient.ts b/packages/cosmwasm/src/cosmwasmclient.ts index b22f6b94..f2543c97 100644 --- a/packages/cosmwasm/src/cosmwasmclient.ts +++ b/packages/cosmwasm/src/cosmwasmclient.ts @@ -9,13 +9,14 @@ import { decodeBech32Pubkey, IndexedTx, LcdClient, + PostTxResult, PubKey, setupAuthExtension, StdTx, } from "@cosmjs/sdk38"; import { setupWasmExtension, WasmExtension } from "./lcdapi/wasm"; -import { Log, parseLogs } from "./logs"; +import { parseLogs } from "./logs"; import { JsonObject } from "./types"; export interface GetSequenceResult { @@ -32,28 +33,6 @@ export interface Account { readonly sequence: number; } -export interface PostTxFailure { - /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ - readonly transactionHash: string; - readonly height: number; - readonly code: number; - readonly rawLog: string; -} - -export interface PostTxSuccess { - readonly logs: readonly Log[]; - readonly rawLog: string; - /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ - readonly transactionHash: string; - readonly data?: Uint8Array; -} - -export type PostTxResult = PostTxSuccess | PostTxFailure; - -export function isPostTxFailure(postTxResult: PostTxResult): postTxResult is PostTxFailure { - return !!(postTxResult as PostTxFailure).code; -} - export interface SearchByIdQuery { readonly id: string; } diff --git a/packages/cosmwasm/src/index.ts b/packages/cosmwasm/src/index.ts index 40161f60..e77ef6e9 100644 --- a/packages/cosmwasm/src/index.ts +++ b/packages/cosmwasm/src/index.ts @@ -12,7 +12,6 @@ export { ContractDetails, CosmWasmClient, GetSequenceResult, - PostTxResult, SearchByHeightQuery, SearchByIdQuery, SearchBySentFromOrToQuery, diff --git a/packages/cosmwasm/src/signingcosmwasmclient.spec.ts b/packages/cosmwasm/src/signingcosmwasmclient.spec.ts index 6c249071..46f70a83 100644 --- a/packages/cosmwasm/src/signingcosmwasmclient.spec.ts +++ b/packages/cosmwasm/src/signingcosmwasmclient.spec.ts @@ -1,13 +1,29 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Sha256 } from "@cosmjs/crypto"; import { toHex } from "@cosmjs/encoding"; -import { AuthExtension, coin, coins, LcdClient, Secp256k1Wallet, setupAuthExtension } from "@cosmjs/sdk38"; +import { + assertIsPostTxSuccess, + AuthExtension, + coin, + coins, + LcdClient, + MsgDelegate, + Secp256k1Wallet, + setupAuthExtension, +} from "@cosmjs/sdk38"; import { assert } from "@cosmjs/utils"; -import { isPostTxFailure, PrivateCosmWasmClient } from "./cosmwasmclient"; +import { PrivateCosmWasmClient } from "./cosmwasmclient"; import { setupWasmExtension, WasmExtension } from "./lcdapi/wasm"; import { SigningCosmWasmClient, UploadMeta } from "./signingcosmwasmclient"; -import { alice, getHackatom, makeRandomAddress, pendingWithoutWasmd, unused } from "./testutils.spec"; +import { + alice, + getHackatom, + makeRandomAddress, + pendingWithoutWasmd, + unused, + validatorAddress, +} from "./testutils.spec"; const httpUrl = "http://localhost:1317"; @@ -317,7 +333,7 @@ describe("SigningCosmWasmClient", () => { // send const result = await client.sendTokens(beneficiaryAddress, transferAmount, "for dinner"); - assert(!isPostTxFailure(result)); + assertIsPostTxSuccess(result); const [firstLog] = result.logs; expect(firstLog).toBeTruthy(); @@ -327,4 +343,27 @@ describe("SigningCosmWasmClient", () => { expect(after.balance).toEqual(transferAmount); }); }); + + describe("signAndPost", () => { + it("works", async () => { + pendingWithoutWasmd(); + const wallet = await Secp256k1Wallet.fromMnemonic(alice.mnemonic); + const client = new SigningCosmWasmClient(httpUrl, alice.address0, wallet); + + const msg: MsgDelegate = { + type: "cosmos-sdk/MsgDelegate", + value: { + delegator_address: alice.address0, + validator_address: validatorAddress, + amount: coin(1234, "ustake"), + }, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "180000", // 180k + }; + const result = await client.signAndPost([msg], fee, "Use your power wisely"); + assertIsPostTxSuccess(result); + }); + }); }); diff --git a/packages/cosmwasm/src/signingcosmwasmclient.ts b/packages/cosmwasm/src/signingcosmwasmclient.ts index 175176f6..83715b26 100644 --- a/packages/cosmwasm/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm/src/signingcosmwasmclient.ts @@ -6,9 +6,13 @@ import { BroadcastMode, Coin, coins, + isPostTxFailure, makeSignBytes, + Msg, MsgSend, OfflineSigner, + PostTxFailure, + PostTxResult, StdFee, StdSignature, StdTx, @@ -16,14 +20,7 @@ import { import pako from "pako"; import { isValidBuilder } from "./builder"; -import { - Account, - CosmWasmClient, - GetSequenceResult, - isPostTxFailure, - PostTxFailure, - PostTxResult, -} from "./cosmwasmclient"; +import { Account, CosmWasmClient, GetSequenceResult } from "./cosmwasmclient"; import { findAttribute, Log } from "./logs"; import { MsgClearAdmin, @@ -219,19 +216,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { builder: builder, }, }; - const fee = this.fees.upload; - const { accountNumber, sequence } = await this.getSequence(); - const chainId = await this.getChainId(); - const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo, accountNumber, sequence); - const signature = await this.signer.sign(this.senderAddress, signBytes); - const signedTx: StdTx = { - msg: [storeCodeMsg], - fee: fee, - memo: memo, - signatures: [signature], - }; - - const result = await this.postTx(signedTx); + const result = await this.signAndPost([storeCodeMsg], this.fees.upload, memo); if (isPostTxFailure(result)) { throw new Error(createPostTxErrorMessage(result)); } @@ -264,21 +249,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { admin: options.admin, }, }; - const memo = options.memo || ""; - const fee = this.fees.init; - const { accountNumber, sequence } = await this.getSequence(); - const chainId = await this.getChainId(); - const signBytes = makeSignBytes([instantiateMsg], fee, chainId, memo, accountNumber, sequence); - - const signature = await this.signer.sign(this.senderAddress, signBytes); - const signedTx: StdTx = { - msg: [instantiateMsg], - fee: fee, - memo: memo, - signatures: [signature], - }; - - const result = await this.postTx(signedTx); + const result = await this.signAndPost([instantiateMsg], this.fees.init, options.memo); if (isPostTxFailure(result)) { throw new Error(createPostTxErrorMessage(result)); } @@ -299,19 +270,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { new_admin: newAdmin, }, }; - const fee = this.fees.changeAdmin; - const { accountNumber, sequence } = await this.getSequence(); - const chainId = await this.getChainId(); - const signBytes = makeSignBytes([updateAdminMsg], fee, chainId, memo, accountNumber, sequence); - const signature = await this.signer.sign(this.senderAddress, signBytes); - const signedTx: StdTx = { - msg: [updateAdminMsg], - fee: fee, - memo: memo, - signatures: [signature], - }; - - const result = await this.postTx(signedTx); + const result = await this.signAndPost([updateAdminMsg], this.fees.changeAdmin, memo); if (isPostTxFailure(result)) { throw new Error(createPostTxErrorMessage(result)); } @@ -329,19 +288,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { contract: contractAddress, }, }; - const fee = this.fees.changeAdmin; - const { accountNumber, sequence } = await this.getSequence(); - const chainId = await this.getChainId(); - const signBytes = makeSignBytes([clearAdminMsg], fee, chainId, memo, accountNumber, sequence); - const signature = await this.signer.sign(this.senderAddress, signBytes); - const signedTx: StdTx = { - msg: [clearAdminMsg], - fee: fee, - memo: memo, - signatures: [signature], - }; - - const result = await this.postTx(signedTx); + const result = await this.signAndPost([clearAdminMsg], this.fees.changeAdmin, memo); if (isPostTxFailure(result)) { throw new Error(createPostTxErrorMessage(result)); } @@ -366,19 +313,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { msg: migrateMsg, }, }; - const fee = this.fees.migrate; - const { accountNumber, sequence } = await this.getSequence(); - const chainId = await this.getChainId(); - const signBytes = makeSignBytes([msg], fee, chainId, memo, accountNumber, sequence); - const signature = await this.signer.sign(this.senderAddress, signBytes); - const signedTx: StdTx = { - msg: [msg], - fee: fee, - memo: memo, - signatures: [signature], - }; - - const result = await this.postTx(signedTx); + const result = await this.signAndPost([msg], this.fees.migrate, memo); if (isPostTxFailure(result)) { throw new Error(createPostTxErrorMessage(result)); } @@ -403,19 +338,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { sent_funds: transferAmount || [], }, }; - const fee = this.fees.exec; - const { accountNumber, sequence } = await this.getSequence(); - const chainId = await this.getChainId(); - const signBytes = makeSignBytes([executeMsg], fee, chainId, memo, accountNumber, sequence); - const signature = await this.signer.sign(this.senderAddress, signBytes); - const signedTx: StdTx = { - msg: [executeMsg], - fee: fee, - memo: memo, - signatures: [signature], - }; - - const result = await this.postTx(signedTx); + const result = await this.signAndPost([executeMsg], this.fees.exec, memo); if (isPostTxFailure(result)) { throw new Error(createPostTxErrorMessage(result)); } @@ -438,18 +361,24 @@ export class SigningCosmWasmClient extends CosmWasmClient { amount: transferAmount, }, }; - const fee = this.fees.send; + return this.signAndPost([sendMsg], this.fees.send, memo); + } + + /** + * Gets account number and sequence from the API, creates a sign doc, + * creates a single signature, assembles the signed transaction and broadcasts it. + */ + public async signAndPost(msgs: readonly Msg[], fee: StdFee, memo = ""): Promise { const { accountNumber, sequence } = await this.getSequence(); const chainId = await this.getChainId(); - const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence); + const signBytes = makeSignBytes(msgs, fee, chainId, memo, accountNumber, sequence); const signature = await this.signer.sign(this.senderAddress, signBytes); const signedTx: StdTx = { - msg: [sendMsg], + msg: msgs, fee: fee, memo: memo, signatures: [signature], }; - return this.postTx(signedTx); } } diff --git a/packages/cosmwasm/src/testutils.spec.ts b/packages/cosmwasm/src/testutils.spec.ts index 5cfe24a4..c7ed6518 100644 --- a/packages/cosmwasm/src/testutils.spec.ts +++ b/packages/cosmwasm/src/testutils.spec.ts @@ -47,6 +47,8 @@ export const wasmd = { chainId: "testing", }; +export const validatorAddress = "cosmosvaloper1gjvanqxc774u6ed9thj4gpn9gj5zus5u32enqn"; + export const alice = { mnemonic: "enlist hip relief stomach skate base shallow young switch frequent cry park", pubkey0: { diff --git a/packages/cosmwasm/types/cosmwasmclient.d.ts b/packages/cosmwasm/types/cosmwasmclient.d.ts index ac245804..ed3b6689 100644 --- a/packages/cosmwasm/types/cosmwasmclient.d.ts +++ b/packages/cosmwasm/types/cosmwasmclient.d.ts @@ -5,11 +5,11 @@ import { CosmosSdkTx, IndexedTx, LcdClient, + PostTxResult, PubKey, StdTx, } from "@cosmjs/sdk38"; import { WasmExtension } from "./lcdapi/wasm"; -import { Log } from "./logs"; import { JsonObject } from "./types"; export interface GetSequenceResult { readonly accountNumber: number; @@ -23,22 +23,6 @@ export interface Account { readonly accountNumber: number; readonly sequence: number; } -export interface PostTxFailure { - /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ - readonly transactionHash: string; - readonly height: number; - readonly code: number; - readonly rawLog: string; -} -export interface PostTxSuccess { - readonly logs: readonly Log[]; - readonly rawLog: string; - /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ - readonly transactionHash: string; - readonly data?: Uint8Array; -} -export declare type PostTxResult = PostTxSuccess | PostTxFailure; -export declare function isPostTxFailure(postTxResult: PostTxResult): postTxResult is PostTxFailure; export interface SearchByIdQuery { readonly id: string; } diff --git a/packages/cosmwasm/types/index.d.ts b/packages/cosmwasm/types/index.d.ts index a6406556..517e261b 100644 --- a/packages/cosmwasm/types/index.d.ts +++ b/packages/cosmwasm/types/index.d.ts @@ -11,7 +11,6 @@ export { ContractDetails, CosmWasmClient, GetSequenceResult, - PostTxResult, SearchByHeightQuery, SearchByIdQuery, SearchBySentFromOrToQuery, diff --git a/packages/cosmwasm/types/signingcosmwasmclient.d.ts b/packages/cosmwasm/types/signingcosmwasmclient.d.ts index 6825aaa5..71de1463 100644 --- a/packages/cosmwasm/types/signingcosmwasmclient.d.ts +++ b/packages/cosmwasm/types/signingcosmwasmclient.d.ts @@ -1,5 +1,5 @@ -import { BroadcastMode, Coin, OfflineSigner, StdFee, StdSignature } from "@cosmjs/sdk38"; -import { Account, CosmWasmClient, GetSequenceResult, PostTxResult } from "./cosmwasmclient"; +import { BroadcastMode, Coin, Msg, OfflineSigner, PostTxResult, StdFee, StdSignature } from "@cosmjs/sdk38"; +import { Account, CosmWasmClient, GetSequenceResult } from "./cosmwasmclient"; import { Log } from "./logs"; export interface SigningCallback { (signBytes: Uint8Array): Promise; @@ -124,4 +124,9 @@ export declare class SigningCosmWasmClient extends CosmWasmClient { transferAmount?: readonly Coin[], ): Promise; sendTokens(recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise; + /** + * Gets account number and sequence from the API, creates a sign doc, + * creates a single signature, assembles the signed transaction and broadcasts it. + */ + signAndPost(msgs: readonly Msg[], fee: StdFee, memo?: string): Promise; } diff --git a/packages/sdk38/src/cosmosclient.spec.ts b/packages/sdk38/src/cosmosclient.spec.ts index 30e5d14c..6087e967 100644 --- a/packages/sdk38/src/cosmosclient.spec.ts +++ b/packages/sdk38/src/cosmosclient.spec.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { assert, sleep } from "@cosmjs/utils"; +import { sleep } from "@cosmjs/utils"; import { ReadonlyDate } from "readonly-date"; -import { CosmosClient, isPostTxFailure, PrivateCosmWasmClient } from "./cosmosclient"; +import { assertIsPostTxSuccess, CosmosClient, PrivateCosmWasmClient } from "./cosmosclient"; import { makeSignBytes } from "./encoding"; import { findAttribute } from "./logs"; import { MsgSend } from "./msgs"; @@ -234,7 +234,7 @@ describe("CosmosClient", () => { signatures: [signature], }; const txResult = await client.postTx(signedTx); - assert(!isPostTxFailure(txResult)); + assertIsPostTxSuccess(txResult); const { logs, transactionHash } = txResult; const amountAttr = findAttribute(logs, "transfer", "amount"); expect(amountAttr.value).toEqual("1234567ucosm"); diff --git a/packages/sdk38/src/cosmosclient.ts b/packages/sdk38/src/cosmosclient.ts index a447f1b9..34ee4fe0 100644 --- a/packages/sdk38/src/cosmosclient.ts +++ b/packages/sdk38/src/cosmosclient.ts @@ -40,8 +40,23 @@ export interface PostTxSuccess { export type PostTxResult = PostTxSuccess | PostTxFailure; -export function isPostTxFailure(postTxResult: PostTxResult): postTxResult is PostTxFailure { - return !!(postTxResult as PostTxFailure).code; +export function isPostTxFailure(result: PostTxResult): result is PostTxFailure { + return !!(result as PostTxFailure).code; +} + +export function isPostTxSuccess(result: PostTxResult): result is PostTxSuccess { + return !isPostTxFailure(result); +} + +/** + * Ensures the given result is a success. Throws a detailed error message otherwise. + */ +export function assertIsPostTxSuccess(result: PostTxResult): asserts result is PostTxSuccess { + if (isPostTxFailure(result)) { + throw new Error( + `Error when posting tx ${result.transactionHash} at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}`, + ); + } } export interface SearchByIdQuery { diff --git a/packages/sdk38/src/index.ts b/packages/sdk38/src/index.ts index 81ddccea..4afe6c26 100644 --- a/packages/sdk38/src/index.ts +++ b/packages/sdk38/src/index.ts @@ -6,12 +6,17 @@ export { Coin, coin, coins } from "./coins"; export { Account, + assertIsPostTxSuccess, Block, BlockHeader, CosmosClient, GetSequenceResult, IndexedTx, + isPostTxFailure, + isPostTxSuccess, + PostTxFailure, PostTxResult, + PostTxSuccess, SearchByHeightQuery, SearchByIdQuery, SearchBySentFromOrToQuery, diff --git a/packages/sdk38/src/lcdapi/distribution.spec.ts b/packages/sdk38/src/lcdapi/distribution.spec.ts index e57c386e..af63253f 100644 --- a/packages/sdk38/src/lcdapi/distribution.spec.ts +++ b/packages/sdk38/src/lcdapi/distribution.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Bech32 } from "@cosmjs/encoding"; -import { assert, sleep } from "@cosmjs/utils"; +import { sleep } from "@cosmjs/utils"; import { coin, coins } from "../coins"; -import { isPostTxFailure } from "../cosmosclient"; +import { assertIsPostTxSuccess } from "../cosmosclient"; import { makeSignBytes } from "../encoding"; import { MsgDelegate } from "../msgs"; import { SigningCosmosClient } from "../signingcosmosclient"; @@ -55,8 +55,8 @@ describe("DistributionExtension", () => { signatures: [signature], }; - const receipt = await client.postTx(tx); - assert(!isPostTxFailure(receipt)); + const result = await client.postTx(tx); + assertIsPostTxSuccess(result); await sleep(75); // wait until transactions are indexed } diff --git a/packages/sdk38/src/lcdapi/gov.spec.ts b/packages/sdk38/src/lcdapi/gov.spec.ts index ad379c45..3cb35162 100644 --- a/packages/sdk38/src/lcdapi/gov.spec.ts +++ b/packages/sdk38/src/lcdapi/gov.spec.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { assert, sleep } from "@cosmjs/utils"; +import { sleep } from "@cosmjs/utils"; import { coins } from "../coins"; -import { isPostTxFailure } from "../cosmosclient"; +import { assertIsPostTxSuccess } from "../cosmosclient"; import { makeSignBytes } from "../encoding"; import { SigningCosmosClient } from "../signingcosmosclient"; import { @@ -66,9 +66,9 @@ describe("GovExtension", () => { signatures: [proposalSignature], }; - const proposalReceipt = await client.postTx(proposalTx); - assert(!isPostTxFailure(proposalReceipt)); - proposalId = proposalReceipt.logs[0].events + const proposalResult = await client.postTx(proposalTx); + assertIsPostTxSuccess(proposalResult); + proposalId = proposalResult.logs[0].events .find(({ type }) => type === "submit_proposal")! .attributes.find(({ key }) => key === "proposal_id")!.value; diff --git a/packages/sdk38/src/lcdapi/staking.spec.ts b/packages/sdk38/src/lcdapi/staking.spec.ts index dfe91717..a3c28ace 100644 --- a/packages/sdk38/src/lcdapi/staking.spec.ts +++ b/packages/sdk38/src/lcdapi/staking.spec.ts @@ -2,7 +2,7 @@ import { assert, sleep } from "@cosmjs/utils"; import { coin, coins } from "../coins"; -import { isPostTxFailure } from "../cosmosclient"; +import { assertIsPostTxSuccess } from "../cosmosclient"; import { makeSignBytes } from "../encoding"; import { MsgDelegate, MsgUndelegate } from "../msgs"; import { SigningCosmosClient } from "../signingcosmosclient"; @@ -56,8 +56,8 @@ describe("StakingExtension", () => { signatures: [signature], }; - const receipt = await client.postTx(tx); - assert(!isPostTxFailure(receipt)); + const result = await client.postTx(tx); + assertIsPostTxSuccess(result); } { const msg: MsgUndelegate = { @@ -79,8 +79,8 @@ describe("StakingExtension", () => { signatures: [signature], }; - const receipt = await client.postTx(tx); - assert(!isPostTxFailure(receipt)); + const result = await client.postTx(tx); + assertIsPostTxSuccess(result); } await sleep(75); // wait until transactions are indexed @@ -402,7 +402,7 @@ describe("StakingExtension", () => { const response = await client.staking.validatorDelegations(validatorAddress); expect(response).toEqual({ height: jasmine.stringMatching(nonNegativeIntegerMatcher), - result: [ + result: jasmine.arrayContaining([ { delegator_address: faucet.address, validator_address: validatorAddress, @@ -415,7 +415,7 @@ describe("StakingExtension", () => { shares: "250000000.000000000000000000", balance: { denom: "ustake", amount: "250000000" }, }, - ], + ]), }); }); }); diff --git a/packages/sdk38/src/signingcosmosclient.spec.ts b/packages/sdk38/src/signingcosmosclient.spec.ts index ac5c6e5f..63952110 100644 --- a/packages/sdk38/src/signingcosmosclient.spec.ts +++ b/packages/sdk38/src/signingcosmosclient.spec.ts @@ -1,9 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import { assert } from "@cosmjs/utils"; -import { Coin } from "./coins"; -import { isPostTxFailure, PrivateCosmWasmClient } from "./cosmosclient"; +import { Coin, coin, coins } from "./coins"; +import { assertIsPostTxSuccess, PrivateCosmWasmClient } from "./cosmosclient"; +import { MsgDelegate } from "./msgs"; import { SigningCosmosClient } from "./signingcosmosclient"; -import { makeRandomAddress, pendingWithoutWasmd } from "./testutils.spec"; +import { makeRandomAddress, pendingWithoutWasmd, validatorAddress } from "./testutils.spec"; import { Secp256k1Wallet } from "./wallet"; const httpUrl = "http://localhost:1317"; @@ -66,7 +68,7 @@ describe("SigningCosmosClient", () => { // send const result = await client.sendTokens(beneficiaryAddress, transferAmount, "for dinner"); - assert(!isPostTxFailure(result)); + assertIsPostTxSuccess(result); const [firstLog] = result.logs; expect(firstLog).toBeTruthy(); @@ -76,4 +78,27 @@ describe("SigningCosmosClient", () => { expect(after.balance).toEqual(transferAmount); }); }); + + describe("signAndPost", () => { + it("works", async () => { + pendingWithoutWasmd(); + const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmosClient(httpUrl, faucet.address, wallet); + + const msg: MsgDelegate = { + type: "cosmos-sdk/MsgDelegate", + value: { + delegator_address: faucet.address, + validator_address: validatorAddress, + amount: coin(1234, "ustake"), + }, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "180000", // 180k + }; + const result = await client.signAndPost([msg], fee, "Use your power wisely"); + assertIsPostTxSuccess(result); + }); + }); }); diff --git a/packages/sdk38/src/signingcosmosclient.ts b/packages/sdk38/src/signingcosmosclient.ts index 23f0013b..d901425a 100644 --- a/packages/sdk38/src/signingcosmosclient.ts +++ b/packages/sdk38/src/signingcosmosclient.ts @@ -3,7 +3,7 @@ import { Coin, coins } from "./coins"; import { Account, CosmosClient, GetSequenceResult, PostTxResult } from "./cosmosclient"; import { makeSignBytes } from "./encoding"; import { BroadcastMode } from "./lcdapi"; -import { MsgSend } from "./msgs"; +import { Msg, MsgSend } from "./msgs"; import { StdFee, StdTx } from "./types"; import { OfflineSigner } from "./wallet"; @@ -87,18 +87,24 @@ export class SigningCosmosClient extends CosmosClient { amount: transferAmount, }, }; - const fee = this.fees.send; + return this.signAndPost([sendMsg], this.fees.send, memo); + } + + /** + * Gets account number and sequence from the API, creates a sign doc, + * creates a single signature, assembles the signed transaction and broadcasts it. + */ + public async signAndPost(msgs: readonly Msg[], fee: StdFee, memo = ""): Promise { const { accountNumber, sequence } = await this.getSequence(); const chainId = await this.getChainId(); - const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence); + const signBytes = makeSignBytes(msgs, fee, chainId, memo, accountNumber, sequence); const signature = await this.signer.sign(this.senderAddress, signBytes); const signedTx: StdTx = { - msg: [sendMsg], + msg: msgs, fee: fee, memo: memo, signatures: [signature], }; - return this.postTx(signedTx); } } diff --git a/packages/sdk38/types/cosmosclient.d.ts b/packages/sdk38/types/cosmosclient.d.ts index 57879a30..f6a28054 100644 --- a/packages/sdk38/types/cosmosclient.d.ts +++ b/packages/sdk38/types/cosmosclient.d.ts @@ -29,7 +29,12 @@ export interface PostTxSuccess { readonly data?: Uint8Array; } export declare type PostTxResult = PostTxSuccess | PostTxFailure; -export declare function isPostTxFailure(postTxResult: PostTxResult): postTxResult is PostTxFailure; +export declare function isPostTxFailure(result: PostTxResult): result is PostTxFailure; +export declare function isPostTxSuccess(result: PostTxResult): result is PostTxSuccess; +/** + * Ensures the given result is a success. Throws a detailed error message otherwise. + */ +export declare function assertIsPostTxSuccess(result: PostTxResult): asserts result is PostTxSuccess; export interface SearchByIdQuery { readonly id: string; } diff --git a/packages/sdk38/types/index.d.ts b/packages/sdk38/types/index.d.ts index 7aaafa40..d84be9f2 100644 --- a/packages/sdk38/types/index.d.ts +++ b/packages/sdk38/types/index.d.ts @@ -4,12 +4,17 @@ export { pubkeyToAddress, rawSecp256k1PubkeyToAddress } from "./address"; export { Coin, coin, coins } from "./coins"; export { Account, + assertIsPostTxSuccess, Block, BlockHeader, CosmosClient, GetSequenceResult, IndexedTx, + isPostTxFailure, + isPostTxSuccess, + PostTxFailure, PostTxResult, + PostTxSuccess, SearchByHeightQuery, SearchByIdQuery, SearchBySentFromOrToQuery, diff --git a/packages/sdk38/types/signingcosmosclient.d.ts b/packages/sdk38/types/signingcosmosclient.d.ts index 410e12db..d9947075 100644 --- a/packages/sdk38/types/signingcosmosclient.d.ts +++ b/packages/sdk38/types/signingcosmosclient.d.ts @@ -1,6 +1,7 @@ import { Coin } from "./coins"; import { Account, CosmosClient, GetSequenceResult, PostTxResult } from "./cosmosclient"; import { BroadcastMode } from "./lcdapi"; +import { Msg } from "./msgs"; import { StdFee } from "./types"; import { OfflineSigner } from "./wallet"; export interface FeeTable { @@ -35,4 +36,9 @@ export declare class SigningCosmosClient extends CosmosClient { getSequence(address?: string): Promise; getAccount(address?: string): Promise; sendTokens(recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise; + /** + * Gets account number and sequence from the API, creates a sign doc, + * creates a single signature, assembles the signed transaction and broadcasts it. + */ + signAndPost(msgs: readonly Msg[], fee: StdFee, memo?: string): Promise; }