diff --git a/CHANGELOG.md b/CHANGELOG.md index 1768da89..f5305260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,13 @@ - @cosmjs/sdk38: Remove `Pen` type in favour of `OfflineSigner` and remove `Secp256k1Pen` class in favour of `Secp256k1Wallet` which takes an `OfflineSigner` instead of a `SigningCallback`. +- @cosmjs/sdk38: Rename `CosmosSdkAccount` to `BaseAccount` and export the type. +- @cosmjs/sdk38: `BaseAccount` now uses `number | string` as the type for + `account_number` and `sequence`. The new helpers `uint64ToNumber` and + `uint64ToString` allow you to normalize the mixed input. +- @cosmjs/sdk38: `BaseAccount` now uses `string | PubKey | null` as the type for + `public_key`. The new helper `normalizePubkey` allows you to normalize the + mixed input. - @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 diff --git a/packages/cosmwasm/src/cosmwasmclient.ts b/packages/cosmwasm/src/cosmwasmclient.ts index f2543c97..c16a8965 100644 --- a/packages/cosmwasm/src/cosmwasmclient.ts +++ b/packages/cosmwasm/src/cosmwasmclient.ts @@ -6,13 +6,14 @@ import { BroadcastMode, Coin, CosmosSdkTx, - decodeBech32Pubkey, IndexedTx, LcdClient, + normalizePubkey, PostTxResult, PubKey, setupAuthExtension, StdTx, + uint64ToNumber, } from "@cosmjs/sdk38"; import { setupWasmExtension, WasmExtension } from "./lcdapi/wasm"; @@ -233,9 +234,9 @@ export class CosmWasmClient { return { address: value.address, balance: value.coins, - pubkey: value.public_key ? decodeBech32Pubkey(value.public_key) : undefined, - accountNumber: value.account_number, - sequence: value.sequence, + pubkey: normalizePubkey(value.public_key) || undefined, + accountNumber: uint64ToNumber(value.account_number), + sequence: uint64ToNumber(value.sequence), }; } } diff --git a/packages/sdk38/src/cosmosclient.ts b/packages/sdk38/src/cosmosclient.ts index 34ee4fe0..555afd43 100644 --- a/packages/sdk38/src/cosmosclient.ts +++ b/packages/sdk38/src/cosmosclient.ts @@ -3,9 +3,15 @@ import { fromBase64, fromHex, toHex } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; import { Coin } from "./coins"; -import { AuthExtension, BroadcastMode, LcdClient, setupAuthExtension } from "./lcdapi"; +import { + AuthExtension, + BroadcastMode, + LcdClient, + normalizePubkey, + setupAuthExtension, + uint64ToNumber, +} from "./lcdapi"; import { Log, parseLogs } from "./logs"; -import { decodeBech32Pubkey } from "./pubkey"; import { CosmosSdkTx, PubKey, StdTx } from "./types"; export interface GetSequenceResult { @@ -234,9 +240,9 @@ export class CosmosClient { return { address: value.address, balance: value.coins, - pubkey: value.public_key ? decodeBech32Pubkey(value.public_key) : undefined, - accountNumber: value.account_number, - sequence: value.sequence, + pubkey: normalizePubkey(value.public_key) || undefined, + accountNumber: uint64ToNumber(value.account_number), + sequence: uint64ToNumber(value.sequence), }; } } diff --git a/packages/sdk38/src/encoding.ts b/packages/sdk38/src/encoding.ts index 70ef1f23..17f8de77 100644 --- a/packages/sdk38/src/encoding.ts +++ b/packages/sdk38/src/encoding.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { toUtf8 } from "@cosmjs/encoding"; +import { uint64ToString } from "./lcdapi"; import { Msg } from "./msgs"; import { StdFee } from "./types"; @@ -41,16 +42,16 @@ export function makeSignBytes( fee: StdFee, chainId: string, memo: string, - accountNumber: number, - sequence: number, + accountNumber: number | string, + sequence: number | string, ): Uint8Array { const signDoc: StdSignDoc = { - account_number: accountNumber.toString(), + account_number: uint64ToString(accountNumber), chain_id: chainId, fee: fee, memo: memo, msgs: msgs, - sequence: sequence.toString(), + sequence: uint64ToString(sequence), }; const sortedSignDoc = sortJson(signDoc); return toUtf8(JSON.stringify(sortedSignDoc)); diff --git a/packages/sdk38/src/index.ts b/packages/sdk38/src/index.ts index 4afe6c26..05adf9ce 100644 --- a/packages/sdk38/src/index.ts +++ b/packages/sdk38/src/index.ts @@ -30,6 +30,7 @@ export { AuthExtension, BankBalancesResponse, BankExtension, + BaseAccount, BlockResponse, BroadcastMode, DistributionCommunityPoolResponse, @@ -60,6 +61,7 @@ export { MintParametersResponse, NodeInfoResponse, normalizeLcdApiArray, + normalizePubkey, PostTxsResponse, SearchTxsResponse, setupAuthExtension, @@ -78,6 +80,8 @@ export { StakingPoolResponse, SupplyExtension, TxsResponse, + uint64ToNumber, + uint64ToString, } from "./lcdapi"; export { isMsgDelegate, isMsgSend, Msg, MsgDelegate, MsgSend } from "./msgs"; export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; diff --git a/packages/sdk38/src/lcdapi/auth.ts b/packages/sdk38/src/lcdapi/auth.ts index 942d117b..6b316466 100644 --- a/packages/sdk38/src/lcdapi/auth.ts +++ b/packages/sdk38/src/lcdapi/auth.ts @@ -1,22 +1,58 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Coin } from "../coins"; +import { PubKey } from "../types"; import { LcdClient } from "./lcdclient"; -export interface CosmosSdkAccount { +/** + * A Cosmos SDK base account. + * + * This type describes the base account representation as returned + * by the Cosmos SDK 0.37–0.39 LCD API. + * + * @see https://docs.cosmos.network/master/modules/auth/02_state.html#base-account + */ +export interface BaseAccount { /** Bech32 account address */ readonly address: string; readonly coins: readonly Coin[]; - /** Bech32 encoded pubkey */ - readonly public_key: string; - readonly account_number: number; - readonly sequence: number; + /** + * The public key of the account. This is not available on-chain as long as the account + * did not send a transaction. + * + * This was a type/value object in Cosmos SDK 0.37, changed to bech32 in Cosmos SDK 0.38 ([1]) + * and changed back to type/value object in Cosmos SDK 0.39 ([2]). + * + * [1]: https://github.com/cosmos/cosmos-sdk/pull/5280 + * [2]: https://github.com/cosmos/cosmos-sdk/pull/6749 + */ + readonly public_key: string | PubKey | null; + /** + * The account number assigned by the blockchain. + * + * This was string encoded in Cosmos SDK 0.37, changed to number in Cosmos SDK 0.38 ([1]) + * and changed back to string in Cosmos SDK 0.39 ([2]). + * + * [1]: https://github.com/cosmos/cosmos-sdk/pull/5280 + * [2]: https://github.com/cosmos/cosmos-sdk/pull/6749 + */ + readonly account_number: number | string; + /** + * The sequence number for replay protection. + * + * This was string encoded in Cosmos SDK 0.37, changed to number in Cosmos SDK 0.38 ([1]) + * and changed back to string in Cosmos SDK 0.39 ([2]). + * + * [1]: https://github.com/cosmos/cosmos-sdk/pull/5280 + * [2]: https://github.com/cosmos/cosmos-sdk/pull/6749 + */ + readonly sequence: number | string; } export interface AuthAccountsResponse { readonly height: string; readonly result: { readonly type: "cosmos-sdk/Account"; - readonly value: CosmosSdkAccount; + readonly value: BaseAccount; }; } diff --git a/packages/sdk38/src/lcdapi/index.ts b/packages/sdk38/src/lcdapi/index.ts index 5a3fc580..db3bdb73 100644 --- a/packages/sdk38/src/lcdapi/index.ts +++ b/packages/sdk38/src/lcdapi/index.ts @@ -2,7 +2,7 @@ // Standard modules (see tracking issue https://github.com/CosmWasm/cosmjs/issues/276) // -export { AuthExtension, AuthAccountsResponse, setupAuthExtension } from "./auth"; +export { AuthExtension, AuthAccountsResponse, BaseAccount, setupAuthExtension } from "./auth"; export { BankBalancesResponse, BankExtension, setupBankExtension } from "./bank"; export { DistributionCommunityPoolResponse, @@ -77,3 +77,8 @@ export { TxsResponse, } from "./base"; export { LcdApiArray, LcdClient, normalizeLcdApiArray } from "./lcdclient"; + +// +// Utils for interacting with the client/API +// +export { normalizePubkey, uint64ToNumber, uint64ToString } from "./utils"; diff --git a/packages/sdk38/src/lcdapi/utils.spec.ts b/packages/sdk38/src/lcdapi/utils.spec.ts new file mode 100644 index 00000000..ace1c60e --- /dev/null +++ b/packages/sdk38/src/lcdapi/utils.spec.ts @@ -0,0 +1,94 @@ +import { PubKey } from "../types"; +import { normalizePubkey, uint64ToNumber, uint64ToString } from "./utils"; + +describe("utils", () => { + describe("uint64ToNumber", () => { + it("works for numeric inputs", () => { + expect(uint64ToNumber(0)).toEqual(0); + expect(uint64ToNumber(1)).toEqual(1); + expect(uint64ToNumber(Number.MAX_SAFE_INTEGER)).toEqual(Number.MAX_SAFE_INTEGER); + }); + + it("works for string inputs", () => { + expect(uint64ToNumber("0")).toEqual(0); + expect(uint64ToNumber("1")).toEqual(1); + expect(uint64ToNumber("9007199254740991")).toEqual(Number.MAX_SAFE_INTEGER); + }); + + it("throws for invalid numbers", () => { + expect(() => uint64ToNumber(NaN)).toThrow(); + expect(() => uint64ToNumber(1.1)).toThrow(); + expect(() => uint64ToNumber(-1)).toThrow(); + expect(() => uint64ToNumber(Number.MAX_SAFE_INTEGER + 1)).toThrow(); + }); + + it("throws for invalid strings", () => { + expect(() => uint64ToNumber("")).toThrow(); + expect(() => uint64ToNumber("0x22")).toThrow(); + expect(() => uint64ToNumber("-1")).toThrow(); + expect(() => uint64ToNumber("1.1")).toThrow(); + expect(() => uint64ToNumber("9007199254740992")).toThrow(); + }); + }); + + describe("uint64ToString", () => { + it("works for numeric inputs", () => { + expect(uint64ToString(0)).toEqual("0"); + expect(uint64ToString(1)).toEqual("1"); + expect(uint64ToString(Number.MAX_SAFE_INTEGER)).toEqual("9007199254740991"); + }); + + it("works for string inputs", () => { + expect(uint64ToString("0")).toEqual("0"); + expect(uint64ToString("1")).toEqual("1"); + expect(uint64ToString("9007199254740991")).toEqual("9007199254740991"); + }); + + it("works for large string values", () => { + // for the string -> string version, the full uint64 range is supported + expect(uint64ToString("9007199254740992")).toEqual("9007199254740992"); + expect(uint64ToString("18446744073709551615")).toEqual("18446744073709551615"); + }); + + it("throws for invalid numbers", () => { + expect(() => uint64ToString(NaN)).toThrow(); + expect(() => uint64ToString(1.1)).toThrow(); + expect(() => uint64ToString(-1)).toThrow(); + expect(() => uint64ToString(Number.MAX_SAFE_INTEGER + 1)).toThrow(); + }); + + it("throws for invalid strings", () => { + expect(() => uint64ToString("")).toThrow(); + expect(() => uint64ToString("0x22")).toThrow(); + expect(() => uint64ToString("-1")).toThrow(); + expect(() => uint64ToString("1.1")).toThrow(); + expect(() => uint64ToString("18446744073709551616")).toThrow(); + }); + }); + + describe("normalizePubkey", () => { + it("interprets empty bech32 string as unset", () => { + expect(normalizePubkey("")).toBeNull(); + }); + + it("decodes bech32 pubkey", () => { + const input = "cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"; + expect(normalizePubkey(input)).toEqual({ + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }); + }); + + it("interprets null as unset", () => { + expect(normalizePubkey(null)).toBeNull(); + }); + + it("passes PubKey unchanged", () => { + const original: PubKey = { + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }; + expect(original).toEqual(original); + }); + }); +}); diff --git a/packages/sdk38/src/lcdapi/utils.ts b/packages/sdk38/src/lcdapi/utils.ts new file mode 100644 index 00000000..5f797746 --- /dev/null +++ b/packages/sdk38/src/lcdapi/utils.ts @@ -0,0 +1,38 @@ +import { Uint64 } from "@cosmjs/math"; + +import { decodeBech32Pubkey } from "../pubkey"; +import { PubKey } from "../types"; + +/** + * Converts an integer expressed as number or string to a number. + * Throws if input is not a valid uint64 or if the value exceeds MAX_SAFE_INTEGER. + * + * This is needed for supporting Comsos SDK 0.37/0.38/0.39 with one client. + */ +export function uint64ToNumber(input: number | string): number { + const value = typeof input === "number" ? Uint64.fromNumber(input) : Uint64.fromString(input); + return value.toNumber(); +} + +/** + * Converts an integer expressed as number or string to a string. + * Throws if input is not a valid uint64. + * + * This is needed for supporting Comsos SDK 0.37/0.38/0.39 with one client. + */ +export function uint64ToString(input: number | string): string { + const value = typeof input === "number" ? Uint64.fromNumber(input) : Uint64.fromString(input); + return value.toString(); +} + +/** + * Normalizes a pubkey as in `BaseAccount.public_key` to allow supporting + * Comsos SDK 0.37–0.39. + * + * Returns null when unset. + */ +export function normalizePubkey(input: string | PubKey | null): PubKey | null { + if (!input) return null; + if (typeof input === "string") return decodeBech32Pubkey(input); + return input; +} diff --git a/packages/sdk38/types/encoding.d.ts b/packages/sdk38/types/encoding.d.ts index 6eece8ca..35885e4d 100644 --- a/packages/sdk38/types/encoding.d.ts +++ b/packages/sdk38/types/encoding.d.ts @@ -5,6 +5,6 @@ export declare function makeSignBytes( fee: StdFee, chainId: string, memo: string, - accountNumber: number, - sequence: number, + accountNumber: number | string, + sequence: number | string, ): Uint8Array; diff --git a/packages/sdk38/types/index.d.ts b/packages/sdk38/types/index.d.ts index d84be9f2..b136e0d0 100644 --- a/packages/sdk38/types/index.d.ts +++ b/packages/sdk38/types/index.d.ts @@ -28,6 +28,7 @@ export { AuthExtension, BankBalancesResponse, BankExtension, + BaseAccount, BlockResponse, BroadcastMode, DistributionCommunityPoolResponse, @@ -58,6 +59,7 @@ export { MintParametersResponse, NodeInfoResponse, normalizeLcdApiArray, + normalizePubkey, PostTxsResponse, SearchTxsResponse, setupAuthExtension, @@ -76,6 +78,8 @@ export { StakingPoolResponse, SupplyExtension, TxsResponse, + uint64ToNumber, + uint64ToString, } from "./lcdapi"; export { isMsgDelegate, isMsgSend, Msg, MsgDelegate, MsgSend } from "./msgs"; export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; diff --git a/packages/sdk38/types/lcdapi/auth.d.ts b/packages/sdk38/types/lcdapi/auth.d.ts index 95f404d3..e20b7102 100644 --- a/packages/sdk38/types/lcdapi/auth.d.ts +++ b/packages/sdk38/types/lcdapi/auth.d.ts @@ -1,19 +1,55 @@ import { Coin } from "../coins"; +import { PubKey } from "../types"; import { LcdClient } from "./lcdclient"; -export interface CosmosSdkAccount { +/** + * A Cosmos SDK base account. + * + * This type describes the base account representation as returned + * by the Cosmos SDK 0.37–0.39 LCD API. + * + * @see https://docs.cosmos.network/master/modules/auth/02_state.html#base-account + */ +export interface BaseAccount { /** Bech32 account address */ readonly address: string; readonly coins: readonly Coin[]; - /** Bech32 encoded pubkey */ - readonly public_key: string; - readonly account_number: number; - readonly sequence: number; + /** + * The public key of the account. This is not available on-chain as long as the account + * did not send a transaction. + * + * This was a type/value object in Cosmos SDK 0.37, changed to bech32 in Cosmos SDK 0.38 ([1]) + * and changed back to type/value object in Cosmos SDK 0.39 ([2]). + * + * [1]: https://github.com/cosmos/cosmos-sdk/pull/5280 + * [2]: https://github.com/cosmos/cosmos-sdk/pull/6749 + */ + readonly public_key: string | PubKey | null; + /** + * The account number assigned by the blockchain. + * + * This was string encoded in Cosmos SDK 0.37, changed to number in Cosmos SDK 0.38 ([1]) + * and changed back to string in Cosmos SDK 0.39 ([2]). + * + * [1]: https://github.com/cosmos/cosmos-sdk/pull/5280 + * [2]: https://github.com/cosmos/cosmos-sdk/pull/6749 + */ + readonly account_number: number | string; + /** + * The sequence number for replay protection. + * + * This was string encoded in Cosmos SDK 0.37, changed to number in Cosmos SDK 0.38 ([1]) + * and changed back to string in Cosmos SDK 0.39 ([2]). + * + * [1]: https://github.com/cosmos/cosmos-sdk/pull/5280 + * [2]: https://github.com/cosmos/cosmos-sdk/pull/6749 + */ + readonly sequence: number | string; } export interface AuthAccountsResponse { readonly height: string; readonly result: { readonly type: "cosmos-sdk/Account"; - readonly value: CosmosSdkAccount; + readonly value: BaseAccount; }; } export interface AuthExtension { diff --git a/packages/sdk38/types/lcdapi/index.d.ts b/packages/sdk38/types/lcdapi/index.d.ts index 6a06331d..07b83300 100644 --- a/packages/sdk38/types/lcdapi/index.d.ts +++ b/packages/sdk38/types/lcdapi/index.d.ts @@ -1,4 +1,4 @@ -export { AuthExtension, AuthAccountsResponse, setupAuthExtension } from "./auth"; +export { AuthExtension, AuthAccountsResponse, BaseAccount, setupAuthExtension } from "./auth"; export { BankBalancesResponse, BankExtension, setupBankExtension } from "./bank"; export { DistributionCommunityPoolResponse, @@ -68,3 +68,4 @@ export { TxsResponse, } from "./base"; export { LcdApiArray, LcdClient, normalizeLcdApiArray } from "./lcdclient"; +export { normalizePubkey, uint64ToNumber, uint64ToString } from "./utils"; diff --git a/packages/sdk38/types/lcdapi/utils.d.ts b/packages/sdk38/types/lcdapi/utils.d.ts new file mode 100644 index 00000000..d3268564 --- /dev/null +++ b/packages/sdk38/types/lcdapi/utils.d.ts @@ -0,0 +1,22 @@ +import { PubKey } from "../types"; +/** + * Converts an integer expressed as number or string to a number. + * Throws if input is not a valid uint64 or if the value exceeds MAX_SAFE_INTEGER. + * + * This is needed for supporting Comsos SDK 0.37/0.38/0.39 with one client. + */ +export declare function uint64ToNumber(input: number | string): number; +/** + * Converts an integer expressed as number or string to a string. + * Throws if input is not a valid uint64. + * + * This is needed for supporting Comsos SDK 0.37/0.38/0.39 with one client. + */ +export declare function uint64ToString(input: number | string): string; +/** + * Normalizes a pubkey as in `BaseAccount.public_key` to allow supporting + * Comsos SDK 0.37–0.39. + * + * Returns null when unset. + */ +export declare function normalizePubkey(input: string | PubKey | null): PubKey | null;