diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ba34fd9d8..1bbafdc428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to - @cosmjs/tendermint-rpc: Add `HttpBatchClient`, which implements `RpcClient`, supporting batch RPC requests ([#1300]). +- @cosmjs/encoding: Add `lossy` parameter to `fromUtf8` allowing the use of a + replacement charater instead of throwing. +- @cosmjs/stargate: Add structured `Events`s to `IndexTx.events` and + `DeliverTxResponse.events`. +- @cosmjs/cosmwasm-stargate: Add structured `Events`s field to + `SigningCosmWasmClient`s transaction execution methods. ## [0.29.2] - 2022-10-13 diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index 1c5162c2c4..ba896663c3 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -10,6 +10,7 @@ import { BroadcastTxError, Coin, DeliverTxResponse, + fromTendermint34Event, IndexedTx, isSearchByHeightQuery, isSearchBySentFromOrToQuery, @@ -283,6 +284,7 @@ export class CosmWasmClient { height: result.height, rawLog: result.rawLog, transactionHash: txId, + events: result.events, gasUsed: result.gasUsed, gasWanted: result.gasWanted, } @@ -462,6 +464,7 @@ export class CosmWasmClient { height: tx.height, hash: toHex(tx.hash).toUpperCase(), code: tx.result.code, + events: tx.result.events.map(fromTendermint34Event), rawLog: tx.result.log || "", tx: tx.tx, gasUsed: tx.result.gasUsed, diff --git a/packages/cosmwasm-stargate/src/index.ts b/packages/cosmwasm-stargate/src/index.ts index 4b00e8988b..c41663d936 100644 --- a/packages/cosmwasm-stargate/src/index.ts +++ b/packages/cosmwasm-stargate/src/index.ts @@ -32,4 +32,5 @@ export { } from "./signingcosmwasmclient"; // Re-exported because this is part of the CosmWasmClient/SigningCosmWasmClient APIs +export { Attribute, DeliverTxResponse, Event, IndexedTx } from "@cosmjs/stargate"; export { HttpEndpoint } from "@cosmjs/tendermint-rpc"; diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index 93b455ea20..6abf5648a5 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -20,6 +20,7 @@ import { createBankAminoConverters, defaultRegistryTypes as defaultStargateTypes, DeliverTxResponse, + Event, GasPrice, isDeliverTxFailure, logs, @@ -76,6 +77,7 @@ export interface UploadResult { readonly height: number; /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ readonly transactionHash: string; + readonly events: readonly Event[]; readonly gasWanted: number; readonly gasUsed: number; } @@ -109,6 +111,7 @@ export interface InstantiateResult { readonly height: number; /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ readonly transactionHash: string; + readonly events: readonly Event[]; readonly gasWanted: number; readonly gasUsed: number; } @@ -122,6 +125,7 @@ export interface ChangeAdminResult { readonly height: number; /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ readonly transactionHash: string; + readonly events: readonly Event[]; readonly gasWanted: number; readonly gasUsed: number; } @@ -132,6 +136,7 @@ export interface MigrateResult { readonly height: number; /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ readonly transactionHash: string; + readonly events: readonly Event[]; readonly gasWanted: number; readonly gasUsed: number; } @@ -148,6 +153,7 @@ export interface ExecuteResult { readonly height: number; /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ readonly transactionHash: string; + readonly events: readonly Event[]; readonly gasWanted: number; readonly gasUsed: number; } @@ -271,6 +277,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { logs: parsedLogs, height: result.height, transactionHash: result.transactionHash, + events: result.events, gasWanted: result.gasWanted, gasUsed: result.gasUsed, }; @@ -306,6 +313,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { logs: parsedLogs, height: result.height, transactionHash: result.transactionHash, + events: result.events, gasWanted: result.gasWanted, gasUsed: result.gasUsed, }; @@ -334,6 +342,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { logs: logs.parseRawLog(result.rawLog), height: result.height, transactionHash: result.transactionHash, + events: result.events, gasWanted: result.gasWanted, gasUsed: result.gasUsed, }; @@ -360,6 +369,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { logs: logs.parseRawLog(result.rawLog), height: result.height, transactionHash: result.transactionHash, + events: result.events, gasWanted: result.gasWanted, gasUsed: result.gasUsed, }; @@ -390,6 +400,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { logs: logs.parseRawLog(result.rawLog), height: result.height, transactionHash: result.transactionHash, + events: result.events, gasWanted: result.gasWanted, gasUsed: result.gasUsed, }; @@ -437,6 +448,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { logs: logs.parseRawLog(result.rawLog), height: result.height, transactionHash: result.transactionHash, + events: result.events, gasWanted: result.gasWanted, gasUsed: result.gasUsed, }; diff --git a/packages/encoding/src/utf8.spec.ts b/packages/encoding/src/utf8.spec.ts index be52ef9d99..2f7af8bca6 100644 --- a/packages/encoding/src/utf8.spec.ts +++ b/packages/encoding/src/utf8.spec.ts @@ -1,3 +1,4 @@ +import { toAscii } from "./ascii"; import { fromUtf8, toUtf8 } from "./utf8"; describe("utf8", () => { @@ -59,4 +60,15 @@ describe("utf8", () => { // Broken UTF8 example from https://github.com/nodejs/node/issues/16894 expect(() => fromUtf8(new Uint8Array([0xf0, 0x80, 0x80]))).toThrow(); }); + + describe("fromUtf8", () => { + it("replaces characters in lossy mode", () => { + expect(fromUtf8(new Uint8Array([]), true)).toEqual(""); + expect(fromUtf8(new Uint8Array([0x61, 0x62, 0x63]), true)).toEqual("abc"); + // Example from https://doc.rust-lang.org/stable/std/string/struct.String.html#method.from_utf8_lossy + expect( + fromUtf8(new Uint8Array([...toAscii("Hello "), 0xf0, 0x90, 0x80, ...toAscii("World")]), true), + ).toEqual("Hello �World"); + }); + }); }); diff --git a/packages/encoding/src/utf8.ts b/packages/encoding/src/utf8.ts index 79380bba30..bc06300163 100644 --- a/packages/encoding/src/utf8.ts +++ b/packages/encoding/src/utf8.ts @@ -12,6 +12,13 @@ export function toUtf8(str: string): Uint8Array { return new TextEncoder().encode(str); } -export function fromUtf8(data: Uint8Array): string { - return new TextDecoder("utf-8", { fatal: true }).decode(data); +/** + * Takes UTF-8 data and decodes it to a string. + * + * In lossy mode, the replacement character � is used to substitude invalid + * encodings. By default lossy mode is off and invalid data will lead to exceptions. + */ +export function fromUtf8(data: Uint8Array, lossy = false): string { + const fatal = !lossy; + return new TextDecoder("utf-8", { fatal }).decode(data); } diff --git a/packages/stargate/src/events.ts b/packages/stargate/src/events.ts new file mode 100644 index 0000000000..b4b70cce29 --- /dev/null +++ b/packages/stargate/src/events.ts @@ -0,0 +1,46 @@ +import { fromUtf8 } from "@cosmjs/encoding"; +import { tendermint34 } from "@cosmjs/tendermint-rpc"; + +/** + * An event attribute. + * + * This is the same attribute type as tendermint34.Attribute and tendermint35.EventAttribute + * but `key` and `value` are unified to strings. The conversion + * from bytes to string in the Tendermint 0.34 case should be done by performing + * [lossy] UTF-8 decoding. + * + * [lossy]: https://doc.rust-lang.org/stable/std/string/struct.String.html#method.from_utf8_lossy + */ +export interface Attribute { + readonly key: string; + readonly value: string; +} + +/** + * The same event type as tendermint34.Event and tendermint35.Event + * but attribute keys and values are unified to strings. The conversion + * from bytes to string in the Tendermint 0.34 case should be done by performing + * [lossy] UTF-8 decoding. + * + * [lossy]: https://doc.rust-lang.org/stable/std/string/struct.String.html#method.from_utf8_lossy + */ +export interface Event { + readonly type: string; + readonly attributes: readonly Attribute[]; +} + +/** + * Takes a Tendemrint 0.34 event with binary encoded key and value + * and converts it into an `Event` with string attributes. + */ +export function fromTendermint34Event(event: tendermint34.Event): Event { + return { + type: event.type, + attributes: event.attributes.map( + (attr): Attribute => ({ + key: fromUtf8(attr.key, true), + value: fromUtf8(attr.value, true), + }), + ), + }; +} diff --git a/packages/stargate/src/index.ts b/packages/stargate/src/index.ts index 5b2145068f..87eac5ab87 100644 --- a/packages/stargate/src/index.ts +++ b/packages/stargate/src/index.ts @@ -1,5 +1,6 @@ export { Account, accountFromAny, AccountParser } from "./accounts"; export { AminoConverter, AminoConverters, AminoTypes } from "./aminotypes"; +export { Attribute, Event, fromTendermint34Event } from "./events"; export { calculateFee, GasPrice } from "./fee"; export * as logs from "./logs"; export { diff --git a/packages/stargate/src/logs.ts b/packages/stargate/src/logs.ts index 91be9109ac..f899c8fe0d 100644 --- a/packages/stargate/src/logs.ts +++ b/packages/stargate/src/logs.ts @@ -1,15 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { isNonNullObject } from "@cosmjs/utils"; -export interface Attribute { - readonly key: string; - readonly value: string; -} - -export interface Event { - readonly type: string; - readonly attributes: readonly Attribute[]; -} +import { Attribute, Event } from "./events"; export interface Log { readonly msg_index: number; diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index 408539dc52..a12fd53da9 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -42,6 +42,7 @@ const resultFailure = { rawLog: "failed to execute message; message index: 0: 1855527000ufct is smaller than 20000000000000000000000ufct: insufficient funds", transactionHash: "FDC4FB701AABD465935F7D04AE490D1EF5F2BD4B227601C4E98B57EB077D9B7D", + events: [], gasUsed: 54396, gasWanted: 200000, }; @@ -51,6 +52,7 @@ const resultSuccess = { rawLog: '[{"events":[{"type":"message","attributes":[{"key":"action","value":"send"},{"key":"sender","value":"firma1trqyle9m2nvyafc2n25frkpwed2504y6avgfzr"},{"key":"module","value":"bank"}]},{"type":"transfer","attributes":[{"key":"recipient","value":"firma12er8ls2sf5zess3jgjxz59xat9xtf8hz0hk6n4"},{"key":"sender","value":"firma1trqyle9m2nvyafc2n25frkpwed2504y6avgfzr"},{"key":"amount","value":"2000000ufct"}]}]}]', transactionHash: "C0B416CA868C55C2B8C1BBB8F3CFA233854F13A5CB15D3E9599F50CAF7B3D161", + events: [], gasUsed: 61556, gasWanted: 200000, }; diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index a4b05c0b9b..410af11198 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -10,6 +10,7 @@ import { QueryDelegatorDelegationsResponse } from "cosmjs-types/cosmos/staking/v import { DelegationResponse } from "cosmjs-types/cosmos/staking/v1beta1/staking"; import { Account, accountFromAny, AccountParser } from "./accounts"; +import { Event, fromTendermint34Event } from "./events"; import { AuthExtension, BankExtension, @@ -64,6 +65,14 @@ export interface IndexedTx { readonly hash: string; /** Transaction execution error code. 0 on success. */ readonly code: number; + readonly events: readonly Event[]; + /** + * A string-based log document. + * + * This currently seems to merge attributes of multiple events into one event per type + * (https://github.com/tendermint/tendermint/issues/9595). You might want to use the `events` + * field instead. + */ readonly rawLog: string; /** * Raw transaction bytes stored in Tendermint. @@ -98,6 +107,14 @@ export interface DeliverTxResponse { /** Error code. The transaction suceeded iff code is 0. */ readonly code: number; readonly transactionHash: string; + readonly events: readonly Event[]; + /** + * A string-based log document. + * + * This currently seems to merge attributes of multiple events into one event per type + * (https://github.com/tendermint/tendermint/issues/9595). You might want to use the `events` + * field instead. + */ readonly rawLog?: string; readonly data?: readonly MsgData[]; readonly gasUsed: number; @@ -417,6 +434,7 @@ export class StargateClient { ? { code: result.code, height: result.height, + events: result.events, rawLog: result.rawLog, transactionHash: txId, gasUsed: result.gasUsed, @@ -453,6 +471,7 @@ export class StargateClient { height: tx.height, hash: toHex(tx.hash).toUpperCase(), code: tx.result.code, + events: tx.result.events.map(fromTendermint34Event), rawLog: tx.result.log || "", tx: tx.tx, gasUsed: tx.result.gasUsed,