From 406c86426b3e081e621ce6daf85ec6c928439a7a Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 12:44:09 +0100 Subject: [PATCH 01/19] launchpad: Refactor SigningCosmosClient for fees/gas --- .../launchpad/src/lcdapi/distribution.spec.ts | 2 +- packages/launchpad/src/lcdapi/gov.spec.ts | 2 +- packages/launchpad/src/lcdapi/staking.spec.ts | 2 +- packages/launchpad/src/signingcosmosclient.ts | 55 ++++++++++++++++--- .../launchpad/types/signingcosmosclient.d.ts | 16 +++++- 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/packages/launchpad/src/lcdapi/distribution.spec.ts b/packages/launchpad/src/lcdapi/distribution.spec.ts index 38562109..66e7cd9b 100644 --- a/packages/launchpad/src/lcdapi/distribution.spec.ts +++ b/packages/launchpad/src/lcdapi/distribution.spec.ts @@ -33,7 +33,7 @@ describe("DistributionExtension", () => { beforeAll(async () => { if (wasmdEnabled()) { const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, wallet, {}); + const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, wallet); const chainId = await client.getChainId(); const msg: MsgDelegate = { diff --git a/packages/launchpad/src/lcdapi/gov.spec.ts b/packages/launchpad/src/lcdapi/gov.spec.ts index 44f79889..c6cd10d1 100644 --- a/packages/launchpad/src/lcdapi/gov.spec.ts +++ b/packages/launchpad/src/lcdapi/gov.spec.ts @@ -31,7 +31,7 @@ describe("GovExtension", () => { beforeAll(async () => { if (wasmdEnabled()) { const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, wallet, {}); + const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, wallet); const chainId = await client.getChainId(); const proposalMsg = { diff --git a/packages/launchpad/src/lcdapi/staking.spec.ts b/packages/launchpad/src/lcdapi/staking.spec.ts index 4580bf17..5127f65e 100644 --- a/packages/launchpad/src/lcdapi/staking.spec.ts +++ b/packages/launchpad/src/lcdapi/staking.spec.ts @@ -34,7 +34,7 @@ describe("StakingExtension", () => { beforeAll(async () => { if (wasmdEnabled()) { const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, wallet, {}); + const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, wallet); const chainId = await client.getChainId(); { diff --git a/packages/launchpad/src/signingcosmosclient.ts b/packages/launchpad/src/signingcosmosclient.ts index ea9a79e6..5160160b 100644 --- a/packages/launchpad/src/signingcosmosclient.ts +++ b/packages/launchpad/src/signingcosmosclient.ts @@ -8,19 +8,49 @@ import { StdFee, StdTx } from "./types"; import { OfflineSigner } from "./wallet"; /** - * Those fees are used by the higher level methods of SigningCosmosClient + * These fees are used by the higher level methods of SigningCosmosClient */ export interface FeeTable { readonly send: StdFee; } -const defaultFees: FeeTable = { - send: { - amount: coins(2000, "ucosm"), - gas: "80000", // 80k - }, +export class GasPrice { + readonly amount: number; + readonly denom: string; + + constructor(amount: number, denom: string) { + this.amount = amount; + this.denom = denom; + } +} + +export type GasLimits = { + readonly [key in keyof FeeTable]: number; }; +function calculateFee(gasLimit: number, denom: string, price: number): StdFee { + const amount = Math.ceil(price * gasLimit); + return { + amount: coins(amount, denom), + gas: gasLimit.toString(), + }; +} + +function buildFeeTable({ denom, amount }: GasPrice, gasLimits: GasLimits): FeeTable { + return Object.entries(gasLimits).reduce((feeTable, [type, gasLimit]) => { + if (gasLimit === undefined) { + return feeTable; + } + return { + ...feeTable, + [type]: calculateFee(gasLimit, denom, amount), + }; + }, {} as FeeTable); +} + +const defaultGasPrice: GasPrice = new GasPrice(0.025, "ucosm"); +const defaultGasLimits: GasLimits = { send: 80000 }; + export class SigningCosmosClient extends CosmosClient { public readonly senderAddress: string; @@ -36,14 +66,16 @@ export class SigningCosmosClient extends CosmosClient { * @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API) * @param senderAddress The address that will sign and send transactions using this instance * @param signer An implementation of OfflineSigner which can provide signatures for transactions, potentially requiring user input. - * @param customFees The fees that are paid for transactions + * @param gasPrice The price paid per unit of gas + * @param gasLimits Custom overrides for gas limits related to specific transaction types * @param broadcastMode Defines at which point of the transaction processing the broadcastTx method returns */ public constructor( apiUrl: string, senderAddress: string, signer: OfflineSigner, - customFees?: Partial, + gasPrice: GasPrice = defaultGasPrice, + gasLimits?: Partial, broadcastMode = BroadcastMode.Block, ) { super(apiUrl, broadcastMode); @@ -51,7 +83,12 @@ export class SigningCosmosClient extends CosmosClient { this.senderAddress = senderAddress; this.signer = signer; - this.fees = { ...defaultFees, ...(customFees || {}) }; + + const mergedGasLimits = { + ...defaultGasLimits, + ...gasLimits, + }; + this.fees = buildFeeTable(gasPrice, mergedGasLimits); } public async getSequence(address?: string): Promise { diff --git a/packages/launchpad/types/signingcosmosclient.d.ts b/packages/launchpad/types/signingcosmosclient.d.ts index 9e6062a2..d8974309 100644 --- a/packages/launchpad/types/signingcosmosclient.d.ts +++ b/packages/launchpad/types/signingcosmosclient.d.ts @@ -5,11 +5,19 @@ import { Msg } from "./msgs"; import { StdFee } from "./types"; import { OfflineSigner } from "./wallet"; /** - * Those fees are used by the higher level methods of SigningCosmosClient + * These fees are used by the higher level methods of SigningCosmosClient */ export interface FeeTable { readonly send: StdFee; } +export declare class GasPrice { + readonly amount: number; + readonly denom: string; + constructor(amount: number, denom: string); +} +export declare type GasLimits = { + readonly [key in keyof FeeTable]: number; +}; export declare class SigningCosmosClient extends CosmosClient { readonly senderAddress: string; private readonly signer; @@ -23,14 +31,16 @@ export declare class SigningCosmosClient extends CosmosClient { * @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API) * @param senderAddress The address that will sign and send transactions using this instance * @param signer An implementation of OfflineSigner which can provide signatures for transactions, potentially requiring user input. - * @param customFees The fees that are paid for transactions + * @param gasPrice The price paid per unit of gas + * @param gasLimits Custom overrides for gas limits related to specific transaction types * @param broadcastMode Defines at which point of the transaction processing the broadcastTx method returns */ constructor( apiUrl: string, senderAddress: string, signer: OfflineSigner, - customFees?: Partial, + gasPrice?: GasPrice, + gasLimits?: Partial, broadcastMode?: BroadcastMode, ); getSequence(address?: string): Promise; From 2e1380975b24c27a3b18ed19a0441ed5f21dcdd6 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 12:54:04 +0100 Subject: [PATCH 02/19] launchpad: Add tests for SigningCosmosClient fee refactor --- .../launchpad/src/signingcosmosclient.spec.ts | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/launchpad/src/signingcosmosclient.spec.ts b/packages/launchpad/src/signingcosmosclient.spec.ts index d657d140..2d93abba 100644 --- a/packages/launchpad/src/signingcosmosclient.spec.ts +++ b/packages/launchpad/src/signingcosmosclient.spec.ts @@ -5,7 +5,7 @@ import { Coin, coin, coins } from "./coins"; import { assertIsBroadcastTxSuccess, PrivateCosmosClient } from "./cosmosclient"; import { MsgDelegate } from "./msgs"; import { Secp256k1Wallet } from "./secp256k1wallet"; -import { SigningCosmosClient } from "./signingcosmosclient"; +import { GasPrice, SigningCosmosClient } from "./signingcosmosclient"; import { makeRandomAddress, pendingWithoutWasmd, validatorAddress } from "./testutils.spec"; const httpUrl = "http://localhost:1317"; @@ -27,6 +27,62 @@ describe("SigningCosmosClient", () => { const client = new SigningCosmosClient(httpUrl, faucet.address, wallet); expect(client).toBeTruthy(); }); + + it("can be constructed with custom gas price", async () => { + const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); + const gasPrice = new GasPrice(3.14, "utest"); + const client = new SigningCosmosClient(httpUrl, faucet.address, wallet, gasPrice); + expect((client as any).fees).toEqual({ + send: { + amount: [ + { + amount: "251200", // 3.14 * 80_000 + denom: "utest", + }, + ], + gas: "80000", + }, + }); + }); + + it("can be constructed with custom gas limits", async () => { + const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); + const gasLimits = { + send: 160000, + }; + const client = new SigningCosmosClient(httpUrl, faucet.address, wallet, undefined, gasLimits); + expect((client as any).fees).toEqual({ + send: { + amount: [ + { + amount: "4000", // 0.025 * 160_000 + denom: "ucosm", + }, + ], + gas: "160000", + }, + }); + }); + + it("can be constructed with custom gas price and gas limits", async () => { + const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); + const gasPrice = new GasPrice(3.14, "utest"); + const gasLimits = { + send: 160000, + }; + const client = new SigningCosmosClient(httpUrl, faucet.address, wallet, gasPrice, gasLimits); + expect((client as any).fees).toEqual({ + send: { + amount: [ + { + amount: "502400", // 3.14 * 160_000 + denom: "utest", + }, + ], + gas: "160000", + }, + }); + }); }); describe("getHeight", () => { From 4571b9cb197a2edfc38f1f21ff6dba597f65b54c Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 14:39:05 +0100 Subject: [PATCH 03/19] launchpad: Extract gas helpers from SigningCosmosClient file --- packages/launchpad/src/gas.ts | 42 +++++++++++++++++ packages/launchpad/src/index.ts | 3 +- .../launchpad/src/signingcosmosclient.spec.ts | 3 +- packages/launchpad/src/signingcosmosclient.ts | 46 ++----------------- packages/launchpad/types/gas.d.ts | 16 +++++++ packages/launchpad/types/index.d.ts | 3 +- .../launchpad/types/signingcosmosclient.d.ts | 17 ++----- 7 files changed, 71 insertions(+), 59 deletions(-) create mode 100644 packages/launchpad/src/gas.ts create mode 100644 packages/launchpad/types/gas.d.ts diff --git a/packages/launchpad/src/gas.ts b/packages/launchpad/src/gas.ts new file mode 100644 index 00000000..5f7251f3 --- /dev/null +++ b/packages/launchpad/src/gas.ts @@ -0,0 +1,42 @@ +import { coins } from "./coins"; +import { StdFee } from "./types"; + +/** + * These fees are used by the higher level methods of SigningCosmosClient + */ +export interface FeeTable { + readonly send: StdFee; +} + +export class GasPrice { + public readonly amount: number; + public readonly denom: string; + + constructor(amount: number, denom: string) { + this.amount = amount; + this.denom = denom; + } +} + +export type GasLimits = { + readonly [key in keyof FeeTable]: number; +}; + +function calculateFee(gasLimit: number, denom: string, price: number): StdFee { + const amount = Math.ceil(price * gasLimit); + return { + amount: coins(amount, denom), + gas: gasLimit.toString(), + }; +} + +export function buildFeeTable({ denom, amount }: GasPrice, gasLimits: GasLimits): FeeTable { + return Object.entries(gasLimits).reduce((feeTable, [type, gasLimit]) => { + return gasLimit === undefined + ? feeTable + : { + ...feeTable, + [type]: calculateFee(gasLimit, denom, amount), + }; + }, {} as FeeTable); +} diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index 671863ea..8d870a18 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -29,6 +29,7 @@ export { isSearchByTagsQuery, } from "./cosmosclient"; export { makeSignBytes } from "./encoding"; +export { buildFeeTable, FeeTable, GasPrice } from "./gas"; export { AuthAccountsResponse, AuthExtension, @@ -97,7 +98,7 @@ export { } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; -export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; +export { SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types"; export { AccountData, diff --git a/packages/launchpad/src/signingcosmosclient.spec.ts b/packages/launchpad/src/signingcosmosclient.spec.ts index 2d93abba..22537d77 100644 --- a/packages/launchpad/src/signingcosmosclient.spec.ts +++ b/packages/launchpad/src/signingcosmosclient.spec.ts @@ -3,9 +3,10 @@ import { assert } from "@cosmjs/utils"; import { Coin, coin, coins } from "./coins"; import { assertIsBroadcastTxSuccess, PrivateCosmosClient } from "./cosmosclient"; +import { GasPrice } from "./gas"; import { MsgDelegate } from "./msgs"; import { Secp256k1Wallet } from "./secp256k1wallet"; -import { GasPrice, SigningCosmosClient } from "./signingcosmosclient"; +import { SigningCosmosClient } from "./signingcosmosclient"; import { makeRandomAddress, pendingWithoutWasmd, validatorAddress } from "./testutils.spec"; const httpUrl = "http://localhost:1317"; diff --git a/packages/launchpad/src/signingcosmosclient.ts b/packages/launchpad/src/signingcosmosclient.ts index 5160160b..cbb8dcdb 100644 --- a/packages/launchpad/src/signingcosmosclient.ts +++ b/packages/launchpad/src/signingcosmosclient.ts @@ -1,54 +1,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { Coin, coins } from "./coins"; +import { Coin } from "./coins"; import { Account, BroadcastTxResult, CosmosClient, GetSequenceResult } from "./cosmosclient"; import { makeSignBytes } from "./encoding"; +import { buildFeeTable, FeeTable, GasLimits, GasPrice } from "./gas"; import { BroadcastMode } from "./lcdapi"; import { Msg, MsgSend } from "./msgs"; import { StdFee, StdTx } from "./types"; import { OfflineSigner } from "./wallet"; -/** - * These fees are used by the higher level methods of SigningCosmosClient - */ -export interface FeeTable { - readonly send: StdFee; -} - -export class GasPrice { - readonly amount: number; - readonly denom: string; - - constructor(amount: number, denom: string) { - this.amount = amount; - this.denom = denom; - } -} - -export type GasLimits = { - readonly [key in keyof FeeTable]: number; -}; - -function calculateFee(gasLimit: number, denom: string, price: number): StdFee { - const amount = Math.ceil(price * gasLimit); - return { - amount: coins(amount, denom), - gas: gasLimit.toString(), - }; -} - -function buildFeeTable({ denom, amount }: GasPrice, gasLimits: GasLimits): FeeTable { - return Object.entries(gasLimits).reduce((feeTable, [type, gasLimit]) => { - if (gasLimit === undefined) { - return feeTable; - } - return { - ...feeTable, - [type]: calculateFee(gasLimit, denom, amount), - }; - }, {} as FeeTable); -} - -const defaultGasPrice: GasPrice = new GasPrice(0.025, "ucosm"); +const defaultGasPrice = new GasPrice(0.025, "ucosm"); const defaultGasLimits: GasLimits = { send: 80000 }; export class SigningCosmosClient extends CosmosClient { diff --git a/packages/launchpad/types/gas.d.ts b/packages/launchpad/types/gas.d.ts new file mode 100644 index 00000000..4aeee124 --- /dev/null +++ b/packages/launchpad/types/gas.d.ts @@ -0,0 +1,16 @@ +import { StdFee } from "./types"; +/** + * These fees are used by the higher level methods of SigningCosmosClient + */ +export interface FeeTable { + readonly send: StdFee; +} +export declare class GasPrice { + readonly amount: number; + readonly denom: string; + constructor(amount: number, denom: string); +} +export declare type GasLimits = { + readonly [key in keyof FeeTable]: number; +}; +export declare function buildFeeTable({ denom, amount }: GasPrice, gasLimits: GasLimits): FeeTable; diff --git a/packages/launchpad/types/index.d.ts b/packages/launchpad/types/index.d.ts index 8c7d2c0c..4e93998a 100644 --- a/packages/launchpad/types/index.d.ts +++ b/packages/launchpad/types/index.d.ts @@ -27,6 +27,7 @@ export { isSearchByTagsQuery, } from "./cosmosclient"; export { makeSignBytes } from "./encoding"; +export { buildFeeTable, FeeTable, GasPrice } from "./gas"; export { AuthAccountsResponse, AuthExtension, @@ -95,7 +96,7 @@ export { } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; -export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; +export { SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types"; export { AccountData, diff --git a/packages/launchpad/types/signingcosmosclient.d.ts b/packages/launchpad/types/signingcosmosclient.d.ts index d8974309..ebfffc92 100644 --- a/packages/launchpad/types/signingcosmosclient.d.ts +++ b/packages/launchpad/types/signingcosmosclient.d.ts @@ -1,23 +1,14 @@ import { Coin } from "./coins"; import { Account, BroadcastTxResult, CosmosClient, GetSequenceResult } from "./cosmosclient"; +import { FeeTable, GasLimits, GasPrice } from "./gas"; import { BroadcastMode } from "./lcdapi"; import { Msg } from "./msgs"; import { StdFee } from "./types"; import { OfflineSigner } from "./wallet"; -/** - * These fees are used by the higher level methods of SigningCosmosClient - */ -export interface FeeTable { - readonly send: StdFee; +/** Use for testing only */ +export interface PrivateSigningCosmosClient { + readonly fees: FeeTable; } -export declare class GasPrice { - readonly amount: number; - readonly denom: string; - constructor(amount: number, denom: string); -} -export declare type GasLimits = { - readonly [key in keyof FeeTable]: number; -}; export declare class SigningCosmosClient extends CosmosClient { readonly senderAddress: string; private readonly signer; From aee57597a2ffaf540d1de1accf29b1af50475418 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 14:57:00 +0100 Subject: [PATCH 04/19] launchpad: Add PrivateSigningCosmosClient class to help tests --- packages/launchpad/src/signingcosmosclient.spec.ts | 11 +++++++---- packages/launchpad/src/signingcosmosclient.ts | 5 +++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/launchpad/src/signingcosmosclient.spec.ts b/packages/launchpad/src/signingcosmosclient.spec.ts index 22537d77..f9214389 100644 --- a/packages/launchpad/src/signingcosmosclient.spec.ts +++ b/packages/launchpad/src/signingcosmosclient.spec.ts @@ -6,7 +6,7 @@ import { assertIsBroadcastTxSuccess, PrivateCosmosClient } from "./cosmosclient" import { GasPrice } from "./gas"; import { MsgDelegate } from "./msgs"; import { Secp256k1Wallet } from "./secp256k1wallet"; -import { SigningCosmosClient } from "./signingcosmosclient"; +import { PrivateSigningCosmosClient, SigningCosmosClient } from "./signingcosmosclient"; import { makeRandomAddress, pendingWithoutWasmd, validatorAddress } from "./testutils.spec"; const httpUrl = "http://localhost:1317"; @@ -33,7 +33,8 @@ describe("SigningCosmosClient", () => { const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); const gasPrice = new GasPrice(3.14, "utest"); const client = new SigningCosmosClient(httpUrl, faucet.address, wallet, gasPrice); - expect((client as any).fees).toEqual({ + const openedClient = (client as unknown) as PrivateSigningCosmosClient; + expect(openedClient.fees).toEqual({ send: { amount: [ { @@ -52,7 +53,8 @@ describe("SigningCosmosClient", () => { send: 160000, }; const client = new SigningCosmosClient(httpUrl, faucet.address, wallet, undefined, gasLimits); - expect((client as any).fees).toEqual({ + const openedClient = (client as unknown) as PrivateSigningCosmosClient; + expect(openedClient.fees).toEqual({ send: { amount: [ { @@ -72,7 +74,8 @@ describe("SigningCosmosClient", () => { send: 160000, }; const client = new SigningCosmosClient(httpUrl, faucet.address, wallet, gasPrice, gasLimits); - expect((client as any).fees).toEqual({ + const openedClient = (client as unknown) as PrivateSigningCosmosClient; + expect(openedClient.fees).toEqual({ send: { amount: [ { diff --git a/packages/launchpad/src/signingcosmosclient.ts b/packages/launchpad/src/signingcosmosclient.ts index cbb8dcdb..1889fb15 100644 --- a/packages/launchpad/src/signingcosmosclient.ts +++ b/packages/launchpad/src/signingcosmosclient.ts @@ -11,6 +11,11 @@ import { OfflineSigner } from "./wallet"; const defaultGasPrice = new GasPrice(0.025, "ucosm"); const defaultGasLimits: GasLimits = { send: 80000 }; +/** Use for testing only */ +export interface PrivateSigningCosmosClient { + readonly fees: FeeTable; +} + export class SigningCosmosClient extends CosmosClient { public readonly senderAddress: string; From c87b0643f9c198f1f40b9da9712f99dcae521745 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 15:15:47 +0100 Subject: [PATCH 05/19] launchpad: Refactor fee table generation --- packages/launchpad/src/gas.ts | 36 +++++++++---------- packages/launchpad/src/index.ts | 4 +-- packages/launchpad/src/signingcosmosclient.ts | 21 +++++------ packages/launchpad/types/gas.d.ts | 16 ++++----- packages/launchpad/types/index.d.ts | 4 +-- .../launchpad/types/signingcosmosclient.d.ts | 10 ++++-- 6 files changed, 46 insertions(+), 45 deletions(-) diff --git a/packages/launchpad/src/gas.ts b/packages/launchpad/src/gas.ts index 5f7251f3..7f0fabe4 100644 --- a/packages/launchpad/src/gas.ts +++ b/packages/launchpad/src/gas.ts @@ -1,13 +1,6 @@ import { coins } from "./coins"; import { StdFee } from "./types"; -/** - * These fees are used by the higher level methods of SigningCosmosClient - */ -export interface FeeTable { - readonly send: StdFee; -} - export class GasPrice { public readonly amount: number; public readonly denom: string; @@ -18,25 +11,28 @@ export class GasPrice { } } -export type GasLimits = { - readonly [key in keyof FeeTable]: number; +export type GasLimits> = { + readonly [key in keyof T]: number; }; -function calculateFee(gasLimit: number, denom: string, price: number): StdFee { - const amount = Math.ceil(price * gasLimit); +function calculateFee(gasLimit: number, { denom, amount: gasPriceAmount }: GasPrice): StdFee { + const amount = Math.ceil(gasPriceAmount * gasLimit); return { amount: coins(amount, denom), gas: gasLimit.toString(), }; } -export function buildFeeTable({ denom, amount }: GasPrice, gasLimits: GasLimits): FeeTable { - return Object.entries(gasLimits).reduce((feeTable, [type, gasLimit]) => { - return gasLimit === undefined - ? feeTable - : { - ...feeTable, - [type]: calculateFee(gasLimit, denom, amount), - }; - }, {} as FeeTable); +export function buildFeeTable>( + gasPrice: GasPrice, + defaultGasLimits: GasLimits, + gasLimits: Partial>, +): T { + return Object.entries(defaultGasLimits).reduce( + (feeTable, [type, defaultGasLimit]) => ({ + ...feeTable, + [type]: calculateFee(gasLimits[type] || defaultGasLimit, gasPrice), + }), + {} as T, + ); } diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index 8d870a18..c1b63346 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -29,7 +29,7 @@ export { isSearchByTagsQuery, } from "./cosmosclient"; export { makeSignBytes } from "./encoding"; -export { buildFeeTable, FeeTable, GasPrice } from "./gas"; +export { buildFeeTable, GasPrice } from "./gas"; export { AuthAccountsResponse, AuthExtension, @@ -98,7 +98,7 @@ export { } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; -export { SigningCosmosClient } from "./signingcosmosclient"; +export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types"; export { AccountData, diff --git a/packages/launchpad/src/signingcosmosclient.ts b/packages/launchpad/src/signingcosmosclient.ts index 1889fb15..cfd75437 100644 --- a/packages/launchpad/src/signingcosmosclient.ts +++ b/packages/launchpad/src/signingcosmosclient.ts @@ -2,14 +2,21 @@ import { Coin } from "./coins"; import { Account, BroadcastTxResult, CosmosClient, GetSequenceResult } from "./cosmosclient"; import { makeSignBytes } from "./encoding"; -import { buildFeeTable, FeeTable, GasLimits, GasPrice } from "./gas"; +import { buildFeeTable, GasLimits, GasPrice } from "./gas"; import { BroadcastMode } from "./lcdapi"; import { Msg, MsgSend } from "./msgs"; import { StdFee, StdTx } from "./types"; import { OfflineSigner } from "./wallet"; +/** + * These fees are used by the higher level methods of SigningCosmosClient + */ +export interface FeeTable extends Record { + readonly send: StdFee; +} + const defaultGasPrice = new GasPrice(0.025, "ucosm"); -const defaultGasLimits: GasLimits = { send: 80000 }; +const defaultGasLimits: GasLimits = { send: 80000 }; /** Use for testing only */ export interface PrivateSigningCosmosClient { @@ -40,20 +47,14 @@ export class SigningCosmosClient extends CosmosClient { senderAddress: string, signer: OfflineSigner, gasPrice: GasPrice = defaultGasPrice, - gasLimits?: Partial, + gasLimits: Partial> = {}, broadcastMode = BroadcastMode.Block, ) { super(apiUrl, broadcastMode); this.anyValidAddress = senderAddress; - this.senderAddress = senderAddress; this.signer = signer; - - const mergedGasLimits = { - ...defaultGasLimits, - ...gasLimits, - }; - this.fees = buildFeeTable(gasPrice, mergedGasLimits); + this.fees = buildFeeTable(gasPrice, defaultGasLimits, gasLimits); } public async getSequence(address?: string): Promise { diff --git a/packages/launchpad/types/gas.d.ts b/packages/launchpad/types/gas.d.ts index 4aeee124..b4070f3e 100644 --- a/packages/launchpad/types/gas.d.ts +++ b/packages/launchpad/types/gas.d.ts @@ -1,16 +1,14 @@ import { StdFee } from "./types"; -/** - * These fees are used by the higher level methods of SigningCosmosClient - */ -export interface FeeTable { - readonly send: StdFee; -} export declare class GasPrice { readonly amount: number; readonly denom: string; constructor(amount: number, denom: string); } -export declare type GasLimits = { - readonly [key in keyof FeeTable]: number; +export declare type GasLimits> = { + readonly [key in keyof T]: number; }; -export declare function buildFeeTable({ denom, amount }: GasPrice, gasLimits: GasLimits): FeeTable; +export declare function buildFeeTable>( + gasPrice: GasPrice, + defaultGasLimits: GasLimits, + gasLimits: Partial>, +): T; diff --git a/packages/launchpad/types/index.d.ts b/packages/launchpad/types/index.d.ts index 4e93998a..3f08cbbc 100644 --- a/packages/launchpad/types/index.d.ts +++ b/packages/launchpad/types/index.d.ts @@ -27,7 +27,7 @@ export { isSearchByTagsQuery, } from "./cosmosclient"; export { makeSignBytes } from "./encoding"; -export { buildFeeTable, FeeTable, GasPrice } from "./gas"; +export { buildFeeTable, GasPrice } from "./gas"; export { AuthAccountsResponse, AuthExtension, @@ -96,7 +96,7 @@ export { } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; -export { SigningCosmosClient } from "./signingcosmosclient"; +export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types"; export { AccountData, diff --git a/packages/launchpad/types/signingcosmosclient.d.ts b/packages/launchpad/types/signingcosmosclient.d.ts index ebfffc92..c6a82eea 100644 --- a/packages/launchpad/types/signingcosmosclient.d.ts +++ b/packages/launchpad/types/signingcosmosclient.d.ts @@ -1,10 +1,16 @@ import { Coin } from "./coins"; import { Account, BroadcastTxResult, CosmosClient, GetSequenceResult } from "./cosmosclient"; -import { FeeTable, GasLimits, GasPrice } from "./gas"; +import { GasLimits, GasPrice } from "./gas"; import { BroadcastMode } from "./lcdapi"; import { Msg } from "./msgs"; import { StdFee } from "./types"; import { OfflineSigner } from "./wallet"; +/** + * These fees are used by the higher level methods of SigningCosmosClient + */ +export interface FeeTable extends Record { + readonly send: StdFee; +} /** Use for testing only */ export interface PrivateSigningCosmosClient { readonly fees: FeeTable; @@ -31,7 +37,7 @@ export declare class SigningCosmosClient extends CosmosClient { senderAddress: string, signer: OfflineSigner, gasPrice?: GasPrice, - gasLimits?: Partial, + gasLimits?: Partial>, broadcastMode?: BroadcastMode, ); getSequence(address?: string): Promise; From 04f8e06771fe4318a286dc92fb0ff77da5797dd9 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 15:23:06 +0100 Subject: [PATCH 06/19] launchpad: Add basic test for GasPrice --- packages/launchpad/src/gas.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/launchpad/src/gas.spec.ts diff --git a/packages/launchpad/src/gas.spec.ts b/packages/launchpad/src/gas.spec.ts new file mode 100644 index 00000000..bb29902a --- /dev/null +++ b/packages/launchpad/src/gas.spec.ts @@ -0,0 +1,9 @@ +import { GasPrice } from "./gas"; + +describe("GasPrice", () => { + it("can be constructed", () => { + const gasPrice = new GasPrice(3.14, "utest"); + expect(gasPrice.amount).toEqual(3.14); + expect(gasPrice.denom).toEqual("utest"); + }); +}); From b94825f806effb12e8a4b67080ee457035760b71 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 16:07:15 +0100 Subject: [PATCH 07/19] math: Add Decimal.multiply method and tests --- packages/math/src/decimal.spec.ts | 41 +++++++++++++++++++++++++++++++ packages/math/src/decimal.ts | 12 +++++++++ packages/math/types/decimal.d.ts | 6 +++++ 3 files changed, 59 insertions(+) diff --git a/packages/math/src/decimal.spec.ts b/packages/math/src/decimal.spec.ts index b0e1803b..b59e556b 100644 --- a/packages/math/src/decimal.spec.ts +++ b/packages/math/src/decimal.spec.ts @@ -211,6 +211,47 @@ describe("Decimal", () => { }); }); + describe("multiply", () => { + it("returns correct values", () => { + const zero = Decimal.fromUserInput("0", 5); + expect(zero.multiply(Decimal.fromUserInput("0", 5)).toString()).toEqual("0"); + expect(zero.multiply(Decimal.fromUserInput("1", 5)).toString()).toEqual("0"); + expect(zero.multiply(Decimal.fromUserInput("2", 5)).toString()).toEqual("0"); + expect(zero.multiply(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("0"); + expect(zero.multiply(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0"); + + const one = Decimal.fromUserInput("1", 5); + expect(one.multiply(Decimal.fromUserInput("0", 5)).toString()).toEqual("0"); + expect(one.multiply(Decimal.fromUserInput("1", 5)).toString()).toEqual("1"); + expect(one.multiply(Decimal.fromUserInput("2", 5)).toString()).toEqual("2"); + expect(one.multiply(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("2.8"); + expect(one.multiply(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0.12345"); + + const oneDotFive = Decimal.fromUserInput("1.5", 5); + expect(oneDotFive.multiply(Decimal.fromUserInput("0", 5)).toString()).toEqual("0"); + expect(oneDotFive.multiply(Decimal.fromUserInput("1", 5)).toString()).toEqual("1.5"); + expect(oneDotFive.multiply(Decimal.fromUserInput("2", 5)).toString()).toEqual("3"); + expect(oneDotFive.multiply(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("4.2"); + expect(oneDotFive.multiply(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0.18517"); + + // original value remain unchanged + expect(zero.toString()).toEqual("0"); + expect(one.toString()).toEqual("1"); + expect(oneDotFive.toString()).toEqual("1.5"); + }); + + it("throws for different fractional digits", () => { + const zero = Decimal.fromUserInput("0", 5); + expect(() => zero.multiply(Decimal.fromUserInput("1", 1))).toThrowError(/do not match/i); + expect(() => zero.multiply(Decimal.fromUserInput("1", 2))).toThrowError(/do not match/i); + expect(() => zero.multiply(Decimal.fromUserInput("1", 3))).toThrowError(/do not match/i); + expect(() => zero.multiply(Decimal.fromUserInput("1", 4))).toThrowError(/do not match/i); + + expect(() => zero.multiply(Decimal.fromUserInput("1", 6))).toThrowError(/do not match/i); + expect(() => zero.multiply(Decimal.fromUserInput("1", 7))).toThrowError(/do not match/i); + }); + }); + describe("equals", () => { it("returns correct values", () => { const zero = Decimal.fromUserInput("0", 5); diff --git a/packages/math/src/decimal.ts b/packages/math/src/decimal.ts index ffedd0ed..969f811c 100644 --- a/packages/math/src/decimal.ts +++ b/packages/math/src/decimal.ts @@ -124,6 +124,18 @@ export class Decimal { return new Decimal(sum.toString(), this.fractionalDigits); } + /** + * a.multiply(b) returns a*b. + * + * Both values need to have the same fractional digits. + */ + public multiply(b: Decimal): Decimal { + if (this.fractionalDigits !== b.fractionalDigits) throw new Error("Fractional digits do not match"); + const factor = new BN(10).pow(new BN(this.data.fractionalDigits)); + const product = this.data.atomics.mul(new BN(b.atomics)).div(factor); + return new Decimal(product.toString(), this.fractionalDigits); + } + public equals(b: Decimal): boolean { return Decimal.compare(this, b) === 0; } diff --git a/packages/math/types/decimal.d.ts b/packages/math/types/decimal.d.ts index 0642fa07..102498d4 100644 --- a/packages/math/types/decimal.d.ts +++ b/packages/math/types/decimal.d.ts @@ -24,6 +24,12 @@ export declare class Decimal { * Both values need to have the same fractional digits. */ plus(b: Decimal): Decimal; + /** + * a.multiply(b) returns a*b. + * + * Both values need to have the same fractional digits. + */ + multiply(b: Decimal): Decimal; equals(b: Decimal): boolean; isLessThan(b: Decimal): boolean; isLessThanOrEqual(b: Decimal): boolean; From 86e7afbb1ea709e3d168f097479e42e1210e048d Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 16:24:57 +0100 Subject: [PATCH 08/19] launchpad: Update GasPrice to hold Decimal --- packages/launchpad/src/gas.spec.ts | 20 +++++++++++++++++--- packages/launchpad/src/gas.ts | 23 ++++++++++++++++++++--- packages/launchpad/types/gas.d.ts | 6 ++++-- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/launchpad/src/gas.spec.ts b/packages/launchpad/src/gas.spec.ts index bb29902a..a92e4e08 100644 --- a/packages/launchpad/src/gas.spec.ts +++ b/packages/launchpad/src/gas.spec.ts @@ -1,9 +1,23 @@ +import { Decimal } from "@cosmjs/math"; + import { GasPrice } from "./gas"; describe("GasPrice", () => { it("can be constructed", () => { - const gasPrice = new GasPrice(3.14, "utest"); - expect(gasPrice.amount).toEqual(3.14); - expect(gasPrice.denom).toEqual("utest"); + const inputs = ["3.14", "3", "0.14"]; + inputs.forEach((input) => { + const gasPrice = new GasPrice(Decimal.fromUserInput(input, 18), "utest"); + expect(gasPrice.amount.toString()).toEqual(input); + expect(gasPrice.denom).toEqual("utest"); + }); + }); + + it("can be constructed from a config string", () => { + const inputs = ["3.14", "3", "0.14"]; + inputs.forEach((input) => { + const gasPrice = GasPrice.fromString(`${input}utest`); + expect(gasPrice.amount.toString()).toEqual(input); + expect(gasPrice.denom).toEqual("utest"); + }); }); }); diff --git a/packages/launchpad/src/gas.ts b/packages/launchpad/src/gas.ts index 7f0fabe4..cc48d5c6 100644 --- a/packages/launchpad/src/gas.ts +++ b/packages/launchpad/src/gas.ts @@ -1,14 +1,30 @@ +import { Decimal } from "@cosmjs/math"; + import { coins } from "./coins"; import { StdFee } from "./types"; export class GasPrice { - public readonly amount: number; + public readonly amount: Decimal; public readonly denom: string; - constructor(amount: number, denom: string) { + constructor(amount: Decimal, denom: string) { this.amount = amount; this.denom = denom; } + + public static fromString(gasPrice: string): GasPrice { + const matchResult = gasPrice.match(/^(?.+?)(?[a-z]+)$/); + if (!matchResult) { + throw new Error("Invalid gas price string"); + } + const { amount, denom } = matchResult.groups as { readonly amount: string; readonly denom: string }; + if (denom.length < 3 || denom.length > 127) { + throw new Error("Gas price denomination must be between 3 and 127 characters"); + } + const fractionalDigits = 18; + const decimalAmount = Decimal.fromUserInput(amount, fractionalDigits); + return new GasPrice(decimalAmount, denom); + } } export type GasLimits> = { @@ -16,7 +32,8 @@ export type GasLimits> = { }; function calculateFee(gasLimit: number, { denom, amount: gasPriceAmount }: GasPrice): StdFee { - const amount = Math.ceil(gasPriceAmount * gasLimit); + const gasLimitDecimal = Decimal.fromUserInput(gasLimit.toString(), gasPriceAmount.fractionalDigits); + const amount = Math.ceil(gasPriceAmount.multiply(gasLimitDecimal).toFloatApproximation()); return { amount: coins(amount, denom), gas: gasLimit.toString(), diff --git a/packages/launchpad/types/gas.d.ts b/packages/launchpad/types/gas.d.ts index b4070f3e..b20b96e2 100644 --- a/packages/launchpad/types/gas.d.ts +++ b/packages/launchpad/types/gas.d.ts @@ -1,8 +1,10 @@ +import { Decimal } from "@cosmjs/math"; import { StdFee } from "./types"; export declare class GasPrice { - readonly amount: number; + readonly amount: Decimal; readonly denom: string; - constructor(amount: number, denom: string); + constructor(amount: Decimal, denom: string); + static fromString(gasPrice: string): GasPrice; } export declare type GasLimits> = { readonly [key in keyof T]: number; From f30004db7303bcb9e0a3fe84a0c7530adfd5b5ad Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 16:29:30 +0100 Subject: [PATCH 09/19] launchpad: Update SigningCosmosClient to use GasPrice.fromString --- packages/launchpad/src/signingcosmosclient.spec.ts | 4 ++-- packages/launchpad/src/signingcosmosclient.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/launchpad/src/signingcosmosclient.spec.ts b/packages/launchpad/src/signingcosmosclient.spec.ts index f9214389..b9b64d0f 100644 --- a/packages/launchpad/src/signingcosmosclient.spec.ts +++ b/packages/launchpad/src/signingcosmosclient.spec.ts @@ -31,7 +31,7 @@ describe("SigningCosmosClient", () => { it("can be constructed with custom gas price", async () => { const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); - const gasPrice = new GasPrice(3.14, "utest"); + const gasPrice = GasPrice.fromString("3.14utest"); const client = new SigningCosmosClient(httpUrl, faucet.address, wallet, gasPrice); const openedClient = (client as unknown) as PrivateSigningCosmosClient; expect(openedClient.fees).toEqual({ @@ -69,7 +69,7 @@ describe("SigningCosmosClient", () => { it("can be constructed with custom gas price and gas limits", async () => { const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); - const gasPrice = new GasPrice(3.14, "utest"); + const gasPrice = GasPrice.fromString("3.14utest"); const gasLimits = { send: 160000, }; diff --git a/packages/launchpad/src/signingcosmosclient.ts b/packages/launchpad/src/signingcosmosclient.ts index cfd75437..5cc229ba 100644 --- a/packages/launchpad/src/signingcosmosclient.ts +++ b/packages/launchpad/src/signingcosmosclient.ts @@ -15,7 +15,7 @@ export interface FeeTable extends Record { readonly send: StdFee; } -const defaultGasPrice = new GasPrice(0.025, "ucosm"); +const defaultGasPrice = GasPrice.fromString("0.025ucosm"); const defaultGasLimits: GasLimits = { send: 80000 }; /** Use for testing only */ From 914f8d94bb1e41e8343e27f766053ea5991a6354 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 16:40:34 +0100 Subject: [PATCH 10/19] launchpad: Refactor FeeTable type --- packages/launchpad/src/gas.ts | 2 ++ packages/launchpad/src/index.ts | 4 ++-- packages/launchpad/src/signingcosmosclient.ts | 14 +++++++------- packages/launchpad/types/gas.d.ts | 1 + packages/launchpad/types/index.d.ts | 4 ++-- packages/launchpad/types/signingcosmosclient.d.ts | 8 ++++---- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/launchpad/src/gas.ts b/packages/launchpad/src/gas.ts index cc48d5c6..a888b94b 100644 --- a/packages/launchpad/src/gas.ts +++ b/packages/launchpad/src/gas.ts @@ -3,6 +3,8 @@ import { Decimal } from "@cosmjs/math"; import { coins } from "./coins"; import { StdFee } from "./types"; +export type FeeTable = Record; + export class GasPrice { public readonly amount: Decimal; public readonly denom: string; diff --git a/packages/launchpad/src/index.ts b/packages/launchpad/src/index.ts index c1b63346..49a0ea91 100644 --- a/packages/launchpad/src/index.ts +++ b/packages/launchpad/src/index.ts @@ -29,7 +29,7 @@ export { isSearchByTagsQuery, } from "./cosmosclient"; export { makeSignBytes } from "./encoding"; -export { buildFeeTable, GasPrice } from "./gas"; +export { buildFeeTable, FeeTable, GasLimits, GasPrice } from "./gas"; export { AuthAccountsResponse, AuthExtension, @@ -98,7 +98,7 @@ export { } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; -export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; +export { CosmosFeeTable, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types"; export { AccountData, diff --git a/packages/launchpad/src/signingcosmosclient.ts b/packages/launchpad/src/signingcosmosclient.ts index 5cc229ba..5ffbd236 100644 --- a/packages/launchpad/src/signingcosmosclient.ts +++ b/packages/launchpad/src/signingcosmosclient.ts @@ -2,7 +2,7 @@ import { Coin } from "./coins"; import { Account, BroadcastTxResult, CosmosClient, GetSequenceResult } from "./cosmosclient"; import { makeSignBytes } from "./encoding"; -import { buildFeeTable, GasLimits, GasPrice } from "./gas"; +import { buildFeeTable, FeeTable, GasLimits, GasPrice } from "./gas"; import { BroadcastMode } from "./lcdapi"; import { Msg, MsgSend } from "./msgs"; import { StdFee, StdTx } from "./types"; @@ -11,23 +11,23 @@ import { OfflineSigner } from "./wallet"; /** * These fees are used by the higher level methods of SigningCosmosClient */ -export interface FeeTable extends Record { +export interface CosmosFeeTable extends FeeTable { readonly send: StdFee; } const defaultGasPrice = GasPrice.fromString("0.025ucosm"); -const defaultGasLimits: GasLimits = { send: 80000 }; +const defaultGasLimits: GasLimits = { send: 80000 }; /** Use for testing only */ export interface PrivateSigningCosmosClient { - readonly fees: FeeTable; + readonly fees: CosmosFeeTable; } export class SigningCosmosClient extends CosmosClient { public readonly senderAddress: string; private readonly signer: OfflineSigner; - private readonly fees: FeeTable; + private readonly fees: CosmosFeeTable; /** * Creates a new client with signing capability to interact with a Cosmos SDK blockchain. This is the bigger brother of CosmosClient. @@ -47,14 +47,14 @@ export class SigningCosmosClient extends CosmosClient { senderAddress: string, signer: OfflineSigner, gasPrice: GasPrice = defaultGasPrice, - gasLimits: Partial> = {}, + gasLimits: Partial> = {}, broadcastMode = BroadcastMode.Block, ) { super(apiUrl, broadcastMode); this.anyValidAddress = senderAddress; this.senderAddress = senderAddress; this.signer = signer; - this.fees = buildFeeTable(gasPrice, defaultGasLimits, gasLimits); + this.fees = buildFeeTable(gasPrice, defaultGasLimits, gasLimits); } public async getSequence(address?: string): Promise { diff --git a/packages/launchpad/types/gas.d.ts b/packages/launchpad/types/gas.d.ts index b20b96e2..8fccc2bc 100644 --- a/packages/launchpad/types/gas.d.ts +++ b/packages/launchpad/types/gas.d.ts @@ -1,5 +1,6 @@ import { Decimal } from "@cosmjs/math"; import { StdFee } from "./types"; +export declare type FeeTable = Record; export declare class GasPrice { readonly amount: Decimal; readonly denom: string; diff --git a/packages/launchpad/types/index.d.ts b/packages/launchpad/types/index.d.ts index 3f08cbbc..17332cf2 100644 --- a/packages/launchpad/types/index.d.ts +++ b/packages/launchpad/types/index.d.ts @@ -27,7 +27,7 @@ export { isSearchByTagsQuery, } from "./cosmosclient"; export { makeSignBytes } from "./encoding"; -export { buildFeeTable, GasPrice } from "./gas"; +export { buildFeeTable, FeeTable, GasLimits, GasPrice } from "./gas"; export { AuthAccountsResponse, AuthExtension, @@ -96,7 +96,7 @@ export { } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; -export { FeeTable, SigningCosmosClient } from "./signingcosmosclient"; +export { CosmosFeeTable, SigningCosmosClient } from "./signingcosmosclient"; export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types"; export { AccountData, diff --git a/packages/launchpad/types/signingcosmosclient.d.ts b/packages/launchpad/types/signingcosmosclient.d.ts index c6a82eea..e1110ad9 100644 --- a/packages/launchpad/types/signingcosmosclient.d.ts +++ b/packages/launchpad/types/signingcosmosclient.d.ts @@ -1,6 +1,6 @@ import { Coin } from "./coins"; import { Account, BroadcastTxResult, CosmosClient, GetSequenceResult } from "./cosmosclient"; -import { GasLimits, GasPrice } from "./gas"; +import { FeeTable, GasLimits, GasPrice } from "./gas"; import { BroadcastMode } from "./lcdapi"; import { Msg } from "./msgs"; import { StdFee } from "./types"; @@ -8,12 +8,12 @@ import { OfflineSigner } from "./wallet"; /** * These fees are used by the higher level methods of SigningCosmosClient */ -export interface FeeTable extends Record { +export interface CosmosFeeTable extends FeeTable { readonly send: StdFee; } /** Use for testing only */ export interface PrivateSigningCosmosClient { - readonly fees: FeeTable; + readonly fees: CosmosFeeTable; } export declare class SigningCosmosClient extends CosmosClient { readonly senderAddress: string; @@ -37,7 +37,7 @@ export declare class SigningCosmosClient extends CosmosClient { senderAddress: string, signer: OfflineSigner, gasPrice?: GasPrice, - gasLimits?: Partial>, + gasLimits?: Partial>, broadcastMode?: BroadcastMode, ); getSequence(address?: string): Promise; From e4767ed2e207e5330fdcf586104b2b1173554074 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 16:47:36 +0100 Subject: [PATCH 11/19] launchpad: Add default fees to SigningCosmosClient construction test --- .../launchpad/src/signingcosmosclient.spec.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/launchpad/src/signingcosmosclient.spec.ts b/packages/launchpad/src/signingcosmosclient.spec.ts index b9b64d0f..6ab1c5e9 100644 --- a/packages/launchpad/src/signingcosmosclient.spec.ts +++ b/packages/launchpad/src/signingcosmosclient.spec.ts @@ -23,10 +23,21 @@ const faucet = { describe("SigningCosmosClient", () => { describe("makeReadOnly", () => { - it("can be constructed", async () => { + it("can be constructed with default fees", async () => { const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); const client = new SigningCosmosClient(httpUrl, faucet.address, wallet); - expect(client).toBeTruthy(); + const openedClient = (client as unknown) as PrivateSigningCosmosClient; + expect(openedClient.fees).toEqual({ + send: { + amount: [ + { + amount: "2000", + denom: "ucosm", + }, + ], + gas: "80000", + }, + }); }); it("can be constructed with custom gas price", async () => { From 8c1fd0fdca6cccf2fb19ac8f0894370457e0ebef Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 17:01:54 +0100 Subject: [PATCH 12/19] cosmwasm: Refactor FeeTable for SigningCosmWasmClient --- packages/cosmwasm/src/index.ts | 2 +- .../cosmwasm/src/signingcosmwasmclient.ts | 56 ++++++++----------- packages/cosmwasm/types/index.d.ts | 2 +- .../cosmwasm/types/signingcosmwasmclient.d.ts | 26 +++++++-- 4 files changed, 47 insertions(+), 39 deletions(-) diff --git a/packages/cosmwasm/src/index.ts b/packages/cosmwasm/src/index.ts index 52a495d9..f7900667 100644 --- a/packages/cosmwasm/src/index.ts +++ b/packages/cosmwasm/src/index.ts @@ -21,7 +21,7 @@ export { } from "./cosmwasmclient"; export { ExecuteResult, - FeeTable, + CosmWasmFeeTable, InstantiateOptions, InstantiateResult, MigrateResult, diff --git a/packages/cosmwasm/src/signingcosmwasmclient.ts b/packages/cosmwasm/src/signingcosmwasmclient.ts index 797686cf..0ab7b8c8 100644 --- a/packages/cosmwasm/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm/src/signingcosmwasmclient.ts @@ -5,8 +5,11 @@ import { BroadcastMode, BroadcastTxFailure, BroadcastTxResult, + buildFeeTable, Coin, - coins, + FeeTable, + GasLimits, + GasPrice, isBroadcastTxFailure, makeSignBytes, Msg, @@ -31,9 +34,9 @@ import { } from "./msgs"; /** - * Those fees are used by the higher level methods of SigningCosmWasmClient + * These fees are used by the higher level methods of SigningCosmWasmClient */ -export interface FeeTable { +export interface CosmWasmFeeTable extends FeeTable { readonly upload: StdFee; readonly init: StdFee; readonly exec: StdFee; @@ -52,31 +55,14 @@ function prepareBuilder(buider: string | undefined): string { } } -const defaultFees: FeeTable = { - upload: { - amount: coins(25000, "ucosm"), - gas: "1000000", // one million - }, - init: { - amount: coins(12500, "ucosm"), - gas: "500000", // 500k - }, - migrate: { - amount: coins(5000, "ucosm"), - gas: "200000", // 200k - }, - exec: { - amount: coins(5000, "ucosm"), - gas: "200000", // 200k - }, - send: { - amount: coins(2000, "ucosm"), - gas: "80000", // 80k - }, - changeAdmin: { - amount: coins(2000, "ucosm"), - gas: "80000", // 80k - }, +const defaultGasPrice = GasPrice.fromString("0.025ucosm"); +const defaultGasLimits: GasLimits = { + upload: 1000000, + init: 500000, + migrate: 200000, + exec: 200000, + send: 80000, + changeAdmin: 80000, }; export interface UploadMeta { @@ -158,6 +144,11 @@ function createBroadcastTxErrorMessage(result: BroadcastTxFailure): string { return `Error when broadcasting tx ${result.transactionHash} at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}`; } +/** Use for testing only */ +export interface PrivateSigningCosmWasmClient { + readonly fees: CosmWasmFeeTable; +} + export class SigningCosmWasmClient extends CosmWasmClient { public readonly senderAddress: string; @@ -173,22 +164,23 @@ export class SigningCosmWasmClient extends CosmWasmClient { * @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API) * @param senderAddress The address that will sign and send transactions using this instance * @param signer An implementation of OfflineSigner which can provide signatures for transactions, potentially requiring user input. - * @param customFees The fees that are paid for transactions + * @param gasPrice The price paid per unit of gas + * @param gasLimits Custom overrides for gas limits related to specific transaction types * @param broadcastMode Defines at which point of the transaction processing the broadcastTx method returns */ public constructor( apiUrl: string, senderAddress: string, signer: OfflineSigner, - customFees?: Partial, + gasPrice: GasPrice = defaultGasPrice, + gasLimits: Partial> = {}, broadcastMode = BroadcastMode.Block, ) { super(apiUrl, broadcastMode); this.anyValidAddress = senderAddress; - this.senderAddress = senderAddress; this.signer = signer; - this.fees = { ...defaultFees, ...(customFees || {}) }; + this.fees = buildFeeTable(gasPrice, defaultGasLimits, gasLimits); } public async getSequence(address?: string): Promise { diff --git a/packages/cosmwasm/types/index.d.ts b/packages/cosmwasm/types/index.d.ts index 1b875e29..f74c9ba5 100644 --- a/packages/cosmwasm/types/index.d.ts +++ b/packages/cosmwasm/types/index.d.ts @@ -20,7 +20,7 @@ export { } from "./cosmwasmclient"; export { ExecuteResult, - FeeTable, + CosmWasmFeeTable, InstantiateOptions, InstantiateResult, MigrateResult, diff --git a/packages/cosmwasm/types/signingcosmwasmclient.d.ts b/packages/cosmwasm/types/signingcosmwasmclient.d.ts index 8f03f640..58f2f6f4 100644 --- a/packages/cosmwasm/types/signingcosmwasmclient.d.ts +++ b/packages/cosmwasm/types/signingcosmwasmclient.d.ts @@ -1,10 +1,20 @@ -import { BroadcastMode, BroadcastTxResult, Coin, Msg, OfflineSigner, StdFee } from "@cosmjs/launchpad"; +import { + BroadcastMode, + BroadcastTxResult, + Coin, + FeeTable, + GasLimits, + GasPrice, + Msg, + OfflineSigner, + StdFee, +} from "@cosmjs/launchpad"; import { Account, CosmWasmClient, GetSequenceResult } from "./cosmwasmclient"; import { Log } from "./logs"; /** - * Those fees are used by the higher level methods of SigningCosmWasmClient + * These fees are used by the higher level methods of SigningCosmWasmClient */ -export interface FeeTable { +export interface CosmWasmFeeTable extends FeeTable { readonly upload: StdFee; readonly init: StdFee; readonly exec: StdFee; @@ -81,6 +91,10 @@ export interface ExecuteResult { /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ readonly transactionHash: string; } +/** Use for testing only */ +export interface PrivateSigningCosmWasmClient { + readonly fees: CosmWasmFeeTable; +} export declare class SigningCosmWasmClient extends CosmWasmClient { readonly senderAddress: string; private readonly signer; @@ -94,14 +108,16 @@ export declare class SigningCosmWasmClient extends CosmWasmClient { * @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API) * @param senderAddress The address that will sign and send transactions using this instance * @param signer An implementation of OfflineSigner which can provide signatures for transactions, potentially requiring user input. - * @param customFees The fees that are paid for transactions + * @param gasPrice The price paid per unit of gas + * @param gasLimits Custom overrides for gas limits related to specific transaction types * @param broadcastMode Defines at which point of the transaction processing the broadcastTx method returns */ constructor( apiUrl: string, senderAddress: string, signer: OfflineSigner, - customFees?: Partial, + gasPrice?: GasPrice, + gasLimits?: Partial>, broadcastMode?: BroadcastMode, ); getSequence(address?: string): Promise; From 03bb146d23b78e19cd6f2d1f7a432e94002dfc43 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 17:02:22 +0100 Subject: [PATCH 13/19] cosmwasm: Add tests for fee table in SigningCosmWasmClient --- .../src/signingcosmwasmclient.spec.ts | 197 +++++++++++++++++- 1 file changed, 196 insertions(+), 1 deletion(-) diff --git a/packages/cosmwasm/src/signingcosmwasmclient.spec.ts b/packages/cosmwasm/src/signingcosmwasmclient.spec.ts index 2adc6ebb..56c275a7 100644 --- a/packages/cosmwasm/src/signingcosmwasmclient.spec.ts +++ b/packages/cosmwasm/src/signingcosmwasmclient.spec.ts @@ -6,6 +6,7 @@ import { AuthExtension, coin, coins, + GasPrice, LcdClient, MsgDelegate, Secp256k1Wallet, @@ -15,7 +16,7 @@ import { assert } from "@cosmjs/utils"; import { PrivateCosmWasmClient } from "./cosmwasmclient"; import { setupWasmExtension, WasmExtension } from "./lcdapi/wasm"; -import { SigningCosmWasmClient, UploadMeta } from "./signingcosmwasmclient"; +import { PrivateSigningCosmWasmClient, SigningCosmWasmClient, UploadMeta } from "./signingcosmwasmclient"; import { alice, getHackatom, @@ -38,6 +39,200 @@ describe("SigningCosmWasmClient", () => { const client = new SigningCosmWasmClient(httpUrl, alice.address0, wallet); expect(client).toBeTruthy(); }); + + it("can be constructed with custom gas price", async () => { + const wallet = await Secp256k1Wallet.fromMnemonic(alice.mnemonic); + const gasPrice = GasPrice.fromString("3.14utest"); + const client = new SigningCosmWasmClient(httpUrl, alice.address0, wallet, gasPrice); + const openedClient = (client as unknown) as PrivateSigningCosmWasmClient; + expect(openedClient.fees).toEqual({ + upload: { + amount: [ + { + amount: "3140000", + denom: "utest", + }, + ], + gas: "1000000", + }, + init: { + amount: [ + { + amount: "1570000", + denom: "utest", + }, + ], + gas: "500000", + }, + migrate: { + amount: [ + { + amount: "628000", + denom: "utest", + }, + ], + gas: "200000", + }, + exec: { + amount: [ + { + amount: "628000", + denom: "utest", + }, + ], + gas: "200000", + }, + send: { + amount: [ + { + amount: "251200", + denom: "utest", + }, + ], + gas: "80000", + }, + changeAdmin: { + amount: [ + { + amount: "251200", + denom: "utest", + }, + ], + gas: "80000", + }, + }); + }); + + it("can be constructed with custom gas limits", async () => { + const wallet = await Secp256k1Wallet.fromMnemonic(alice.mnemonic); + const gasLimits = { + send: 160000, + }; + const client = new SigningCosmWasmClient(httpUrl, alice.address0, wallet, undefined, gasLimits); + const openedClient = (client as unknown) as PrivateSigningCosmWasmClient; + expect(openedClient.fees).toEqual({ + upload: { + amount: [ + { + amount: "25000", + denom: "ucosm", + }, + ], + gas: "1000000", + }, + init: { + amount: [ + { + amount: "12500", + denom: "ucosm", + }, + ], + gas: "500000", + }, + migrate: { + amount: [ + { + amount: "5000", + denom: "ucosm", + }, + ], + gas: "200000", + }, + exec: { + amount: [ + { + amount: "5000", + denom: "ucosm", + }, + ], + gas: "200000", + }, + send: { + amount: [ + { + amount: "4000", + denom: "ucosm", + }, + ], + gas: "160000", + }, + changeAdmin: { + amount: [ + { + amount: "2000", + denom: "ucosm", + }, + ], + gas: "80000", + }, + }); + }); + + it("can be constructed with custom gas price and gas limits", async () => { + const wallet = await Secp256k1Wallet.fromMnemonic(alice.mnemonic); + const gasPrice = GasPrice.fromString("3.14utest"); + const gasLimits = { + send: 160000, + }; + const client = new SigningCosmWasmClient(httpUrl, alice.address0, wallet, gasPrice, gasLimits); + const openedClient = (client as unknown) as PrivateSigningCosmWasmClient; + expect(openedClient.fees).toEqual({ + upload: { + amount: [ + { + amount: "3140000", + denom: "utest", + }, + ], + gas: "1000000", + }, + init: { + amount: [ + { + amount: "1570000", + denom: "utest", + }, + ], + gas: "500000", + }, + migrate: { + amount: [ + { + amount: "628000", + denom: "utest", + }, + ], + gas: "200000", + }, + exec: { + amount: [ + { + amount: "628000", + denom: "utest", + }, + ], + gas: "200000", + }, + send: { + amount: [ + { + amount: "502400", + denom: "utest", + }, + ], + gas: "160000", + }, + changeAdmin: { + amount: [ + { + amount: "251200", + denom: "utest", + }, + ], + gas: "80000", + }, + }); + }); }); describe("getHeight", () => { From 53b413d96b7c86ddd5cda57927f69a1742e8330e Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 17:09:04 +0100 Subject: [PATCH 14/19] Update CHANGELOG for fee table changes --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cf29564..3bb0d2f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## 0.23.0 (unreleased) - @cosmjs/cosmwasm: Rename `CosmWasmClient.postTx` method to `.broadcastTx`. +- @cosmjs/cosmwasm: Rename `FeeTable` type to `CosmWasmFeeTable`. +- @cosmjs/cosmwasm: `SigningCosmWasmClient` constructor now takes optional + arguments `gasPrice` and `gasLimits` instead of `customFees` for easier + customization. - @cosmjs/cosmwasm: Rename `SigningCosmWasmClient.signAndPost` method to `.signAndBroadcast`. - @cosmjs/cosmwasm: Use stricter type `Record` for smart query, @@ -13,7 +17,15 @@ - @cosmjs/encoding: Add `limit` parameter to `Bech32.encode` and `.decode`. The new default limit for decoding is infinity (was 90 before). Set it to 90 to create a strict decoder. +- @cosmjs/launchpad: Rename `FeeTable` type to `CosmosFeeTable` and export a new + more generic type `FeeTable`. +- @cosmjs/launchpad: Add new class `GasPrice`, new helper type `GasLimits` and + new helper function `buildFeeTable` for easier handling of gas prices and + fees. - @cosmjs/launchpad: Rename `CosmosClient.postTx` method to `.broadcastTx`. +- @cosmjs/launchpad: `SigningCosmosClient` constructor now takes optional + arguments `gasPrice` and `gasLimits` instead of `customFees` for easier + customization. - @cosmjs/launchpad: Rename `SigningCosmosClient.signAndPost` method to `.signAndBroadcast`. - @cosmjs/launchpad: Rename `PostTx`-related types to `BroadcastTxResult`, @@ -24,6 +36,7 @@ `isSearchBySentFromOrToQuery` and `isSearchByTagsQuery`. - @cosmjs/launchpad: Change type of `TxsResponse.logs` and `BroadcastTxsResponse.logs` to `unknown[]`. +- @cosmjs/math: Add `.multiply` method to `Decimal` class. - @cosmjs/tendermint-rpc: Make `BroadcastTxCommitResponse.height` non-optional. - @cosmjs/tendermint-rpc: Change type of `GenesisResponse.appState` to `Record | undefined`. From a71e8f350235dec5a2517744880de0777b53a67a Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 17:24:02 +0100 Subject: [PATCH 15/19] faucet: Update for fee table changes --- packages/faucet/README.md | 6 +++--- packages/faucet/src/actions/help.ts | 6 +++--- packages/faucet/src/constants.ts | 8 +++++--- packages/faucet/src/faucet.ts | 16 +++++++--------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/faucet/README.md b/packages/faucet/README.md index eda63418..c191adbf 100644 --- a/packages/faucet/README.md +++ b/packages/faucet/README.md @@ -47,9 +47,9 @@ Environment variables FAUCET_CONCURRENCY Number of distributor accounts. Defaults to 5. FAUCET_PORT Port of the webserver. Defaults to 8000. FAUCET_MEMO Memo for send transactions. Defaults to unset. -FAUCET_FEE Fee for send transactions as a comma separated list, - e.g. "200ushell,30ureef". Defaults to "2000ucosm". -FAUCET_GAS Gas for send transactions. Defaults to 80000. +FAUCET_GAS_PRICE Gas price for transactions as a comma separated list. + Defaults to "0.025ucosm". +FAUCET_GAS_LIMIT Gas limit for send transactions. Defaults to 80000. FAUCET_MNEMONIC Secret mnemonic that serves as the base secret for the faucet HD accounts FAUCET_ADDRESS_PREFIX The bech32 address prefix. Defaults to "cosmos". diff --git a/packages/faucet/src/actions/help.ts b/packages/faucet/src/actions/help.ts index 466c96c6..1c6a73b7 100644 --- a/packages/faucet/src/actions/help.ts +++ b/packages/faucet/src/actions/help.ts @@ -20,9 +20,9 @@ Environment variables FAUCET_CONCURRENCY Number of distributor accounts. Defaults to 5. FAUCET_PORT Port of the webserver. Defaults to 8000. FAUCET_MEMO Memo for send transactions. Defaults to unset. -FAUCET_FEE Fee for send transactions as a comma separated list, - e.g. "200ushell,30ureef". Defaults to "2000ucosm". -FAUCET_GAS Gas for send transactions. Defaults to 80000. +FAUCET_GAS_PRICE Gas price for transactions as a comma separated list. + Defaults to "0.025ucosm". +FAUCET_GAS_LIMIT Gas limit for send transactions. Defaults to 80000. FAUCET_MNEMONIC Secret mnemonic that serves as the base secret for the faucet HD accounts FAUCET_ADDRESS_PREFIX The bech32 address prefix. Defaults to "cosmos". diff --git a/packages/faucet/src/constants.ts b/packages/faucet/src/constants.ts index b20ef9af..32822e70 100644 --- a/packages/faucet/src/constants.ts +++ b/packages/faucet/src/constants.ts @@ -1,12 +1,14 @@ -import { Coin, parseCoins } from "@cosmjs/launchpad"; +import { CosmosFeeTable, GasLimits, GasPrice } from "@cosmjs/launchpad"; import { TokenConfiguration } from "./tokenmanager"; import { parseBankTokens } from "./tokens"; export const binaryName = "cosmos-faucet"; export const memo: string | undefined = process.env.FAUCET_MEMO; -export const fee: readonly Coin[] = parseCoins(process.env.FAUCET_FEE || "2000ucosm"); -export const gas: string = process.env.FAUCET_GAS || "80000"; +export const gasPrice = GasPrice.fromString(process.env.FAUCET_GAS_PRICE || "0.025ucosm"); +export const gasLimits: GasLimits = { + send: parseInt(process.env.FAUCET_GAS_LIMIT || "80000", 10), +}; export const concurrency: number = Number.parseInt(process.env.FAUCET_CONCURRENCY || "", 10) || 5; export const port: number = Number.parseInt(process.env.FAUCET_PORT || "", 10) || 8000; export const mnemonic: string | undefined = process.env.FAUCET_MNEMONIC; diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index c091ce5a..910b6569 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -1,7 +1,6 @@ import { assertIsBroadcastTxSuccess, CosmosClient, - FeeTable, OfflineSigner, SigningCosmosClient, } from "@cosmjs/launchpad"; @@ -57,17 +56,16 @@ export class Faucet { this.holderAddress = wallets[0][0]; this.distributorAddresses = wallets.slice(1).map((pair) => pair[0]); - const fees: Partial = { - send: { - amount: constants.fee, - gas: constants.gas, - }, - }; - // we need one client per sender const clients: { [senderAddress: string]: SigningCosmosClient } = {}; for (const [senderAddress, wallet] of wallets) { - clients[senderAddress] = new SigningCosmosClient(apiUrl, senderAddress, wallet, fees); + clients[senderAddress] = new SigningCosmosClient( + apiUrl, + senderAddress, + wallet, + constants.gasPrice, + constants.gasLimits, + ); } this.clients = clients; this.logging = logging; From 3e080e834b4c9f7aaa03d7de704e6a99a6094776 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 17:26:24 +0100 Subject: [PATCH 16/19] Update CHANGELOG for faucet fee table changes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb0d2f0..4a3191f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ - @cosmjs/encoding: Add `limit` parameter to `Bech32.encode` and `.decode`. The new default limit for decoding is infinity (was 90 before). Set it to 90 to create a strict decoder. +- @cosmjs/faucet: Environmental variable `FAUCET_FEE` renamed to + `FAUCET_GAS_PRICE` and now only accepts one token. Environmental variable + `FAUCET_GAS` renamed to `FAUCET_GAS_LIMIT`. - @cosmjs/launchpad: Rename `FeeTable` type to `CosmosFeeTable` and export a new more generic type `FeeTable`. - @cosmjs/launchpad: Add new class `GasPrice`, new helper type `GasLimits` and From 5630330c09d9665d82d00abe8c373c4cbff36fe6 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 18 Aug 2020 17:46:50 +0100 Subject: [PATCH 17/19] cli: Update cli for CosmWasmFeeTable change --- packages/cli/examples/helpers.ts | 20 ++++++++++---------- packages/cli/src/cli.ts | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/cli/examples/helpers.ts b/packages/cli/examples/helpers.ts index 7001abb3..b3898317 100644 --- a/packages/cli/examples/helpers.ts +++ b/packages/cli/examples/helpers.ts @@ -16,8 +16,8 @@ const defaultOptions: Options = { const defaultFaucetUrl = "https://faucet.demo-10.cosmwasm.com/credit"; -const buildFeeTable = (feeToken: string, gasPrice: number): FeeTable => { - const stdFee = (gas: number, denom: string, price: number) => { +const buildFeeTable = (feeToken: string, gasPrice: number): CosmWasmFeeTable => { + const calculateFee = (gas: number, denom: string, price: number) => { const amount = Math.floor(gas * price); return { amount: [{ amount: amount.toString(), denom: denom }], @@ -26,12 +26,12 @@ const buildFeeTable = (feeToken: string, gasPrice: number): FeeTable => { }; return { - upload: stdFee(1000000, feeToken, gasPrice), - init: stdFee(500000, feeToken, gasPrice), - migrate: stdFee(500000, feeToken, gasPrice), - exec: stdFee(200000, feeToken, gasPrice), - send: stdFee(80000, feeToken, gasPrice), - changeAdmin: stdFee(80000, feeToken, gasPrice), + upload: calculateFee(1000000, feeToken, gasPrice), + init: calculateFee(500000, feeToken, gasPrice), + migrate: calculateFee(500000, feeToken, gasPrice), + exec: calculateFee(200000, feeToken, gasPrice), + send: calculateFee(80000, feeToken, gasPrice), + changeAdmin: calculateFee(80000, feeToken, gasPrice), }; }; @@ -51,11 +51,11 @@ const connect = async ( address: string; }> => { const options: Options = { ...defaultOptions, ...opts }; - const feeTable = buildFeeTable(options.feeToken, options.gasPrice); + const gasPrice = GasPrice.fromString(`${options.gasPrice}${options.feeToken}`); const wallet = await Secp256k1Wallet.fromMnemonic(mnemonic); const [{ address }] = await wallet.getAccounts(); - const client = new SigningCosmWasmClient(options.httpUrl, address, wallet, feeTable); + const client = new SigningCosmWasmClient(options.httpUrl, address, wallet, gasPrice); return { client, address }; }; diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index bf937150..f72023af 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -57,7 +57,7 @@ export async function main(originalArgs: readonly string[]): Promise { "SearchTxFilter", // signingcosmwasmclient "ExecuteResult", - "FeeTable", + "CosmWasmFeeTable", "InstantiateResult", "SigningCosmWasmClient", "UploadMeta", @@ -102,6 +102,7 @@ export async function main(originalArgs: readonly string[]): Promise { "BroadcastTxResult", "Coin", "CosmosClient", + "GasPrice", "Msg", "MsgDelegate", "MsgSend", From 3cbe9e96766e1c0cda9142eafa7ab405372eaf86 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 19 Aug 2020 10:11:14 +0100 Subject: [PATCH 18/19] cosmwasm: Extend CosmosFeeTable in SigningCosmWasmClient --- packages/cosmwasm/src/signingcosmwasmclient.ts | 7 +++---- packages/cosmwasm/types/signingcosmwasmclient.d.ts | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/cosmwasm/src/signingcosmwasmclient.ts b/packages/cosmwasm/src/signingcosmwasmclient.ts index 0ab7b8c8..d0fff774 100644 --- a/packages/cosmwasm/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm/src/signingcosmwasmclient.ts @@ -7,7 +7,7 @@ import { BroadcastTxResult, buildFeeTable, Coin, - FeeTable, + CosmosFeeTable, GasLimits, GasPrice, isBroadcastTxFailure, @@ -36,12 +36,11 @@ import { /** * These fees are used by the higher level methods of SigningCosmWasmClient */ -export interface CosmWasmFeeTable extends FeeTable { +export interface CosmWasmFeeTable extends CosmosFeeTable { readonly upload: StdFee; readonly init: StdFee; readonly exec: StdFee; readonly migrate: StdFee; - readonly send: StdFee; /** Paid when setting the contract admin to a new address or unsetting it */ readonly changeAdmin: StdFee; } @@ -153,7 +152,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { public readonly senderAddress: string; private readonly signer: OfflineSigner; - private readonly fees: FeeTable; + private readonly fees: CosmWasmFeeTable; /** * Creates a new client with signing capability to interact with a CosmWasm blockchain. This is the bigger brother of CosmWasmClient. diff --git a/packages/cosmwasm/types/signingcosmwasmclient.d.ts b/packages/cosmwasm/types/signingcosmwasmclient.d.ts index 58f2f6f4..568c2017 100644 --- a/packages/cosmwasm/types/signingcosmwasmclient.d.ts +++ b/packages/cosmwasm/types/signingcosmwasmclient.d.ts @@ -2,7 +2,7 @@ import { BroadcastMode, BroadcastTxResult, Coin, - FeeTable, + CosmosFeeTable, GasLimits, GasPrice, Msg, @@ -14,12 +14,11 @@ import { Log } from "./logs"; /** * These fees are used by the higher level methods of SigningCosmWasmClient */ -export interface CosmWasmFeeTable extends FeeTable { +export interface CosmWasmFeeTable extends CosmosFeeTable { readonly upload: StdFee; readonly init: StdFee; readonly exec: StdFee; readonly migrate: StdFee; - readonly send: StdFee; /** Paid when setting the contract admin to a new address or unsetting it */ readonly changeAdmin: StdFee; } From 47018d2b6f87d859565cf4903b7ba66ef988afd6 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 19 Aug 2020 10:37:15 +0100 Subject: [PATCH 19/19] math: Change Decimal.multiply to operate on Uint classes --- packages/launchpad/src/gas.ts | 5 +- packages/math/src/decimal.spec.ts | 87 +++++++++++++++++++++++-------- packages/math/src/decimal.ts | 10 ++-- packages/math/types/decimal.d.ts | 5 +- 4 files changed, 74 insertions(+), 33 deletions(-) diff --git a/packages/launchpad/src/gas.ts b/packages/launchpad/src/gas.ts index a888b94b..8f27dfdc 100644 --- a/packages/launchpad/src/gas.ts +++ b/packages/launchpad/src/gas.ts @@ -1,4 +1,4 @@ -import { Decimal } from "@cosmjs/math"; +import { Decimal, Uint53 } from "@cosmjs/math"; import { coins } from "./coins"; import { StdFee } from "./types"; @@ -34,8 +34,7 @@ export type GasLimits> = { }; function calculateFee(gasLimit: number, { denom, amount: gasPriceAmount }: GasPrice): StdFee { - const gasLimitDecimal = Decimal.fromUserInput(gasLimit.toString(), gasPriceAmount.fractionalDigits); - const amount = Math.ceil(gasPriceAmount.multiply(gasLimitDecimal).toFloatApproximation()); + const amount = Math.ceil(gasPriceAmount.multiply(new Uint53(gasLimit)).toFloatApproximation()); return { amount: coins(amount, denom), gas: gasLimit.toString(), diff --git a/packages/math/src/decimal.spec.ts b/packages/math/src/decimal.spec.ts index b59e556b..9f9dee5b 100644 --- a/packages/math/src/decimal.spec.ts +++ b/packages/math/src/decimal.spec.ts @@ -1,4 +1,5 @@ import { Decimal } from "./decimal"; +import { Uint32, Uint53, Uint64 } from "./integers"; describe("Decimal", () => { describe("fromAtomics", () => { @@ -212,27 +213,24 @@ describe("Decimal", () => { }); describe("multiply", () => { - it("returns correct values", () => { + it("returns correct values for Uint32", () => { const zero = Decimal.fromUserInput("0", 5); - expect(zero.multiply(Decimal.fromUserInput("0", 5)).toString()).toEqual("0"); - expect(zero.multiply(Decimal.fromUserInput("1", 5)).toString()).toEqual("0"); - expect(zero.multiply(Decimal.fromUserInput("2", 5)).toString()).toEqual("0"); - expect(zero.multiply(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("0"); - expect(zero.multiply(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0"); + expect(zero.multiply(new Uint32(0)).toString()).toEqual("0"); + expect(zero.multiply(new Uint32(1)).toString()).toEqual("0"); + expect(zero.multiply(new Uint32(2)).toString()).toEqual("0"); + expect(zero.multiply(new Uint32(4294967295)).toString()).toEqual("0"); const one = Decimal.fromUserInput("1", 5); - expect(one.multiply(Decimal.fromUserInput("0", 5)).toString()).toEqual("0"); - expect(one.multiply(Decimal.fromUserInput("1", 5)).toString()).toEqual("1"); - expect(one.multiply(Decimal.fromUserInput("2", 5)).toString()).toEqual("2"); - expect(one.multiply(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("2.8"); - expect(one.multiply(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0.12345"); + expect(one.multiply(new Uint32(0)).toString()).toEqual("0"); + expect(one.multiply(new Uint32(1)).toString()).toEqual("1"); + expect(one.multiply(new Uint32(2)).toString()).toEqual("2"); + expect(one.multiply(new Uint32(4294967295)).toString()).toEqual("4294967295"); const oneDotFive = Decimal.fromUserInput("1.5", 5); - expect(oneDotFive.multiply(Decimal.fromUserInput("0", 5)).toString()).toEqual("0"); - expect(oneDotFive.multiply(Decimal.fromUserInput("1", 5)).toString()).toEqual("1.5"); - expect(oneDotFive.multiply(Decimal.fromUserInput("2", 5)).toString()).toEqual("3"); - expect(oneDotFive.multiply(Decimal.fromUserInput("2.8", 5)).toString()).toEqual("4.2"); - expect(oneDotFive.multiply(Decimal.fromUserInput("0.12345", 5)).toString()).toEqual("0.18517"); + expect(oneDotFive.multiply(new Uint32(0)).toString()).toEqual("0"); + expect(oneDotFive.multiply(new Uint32(1)).toString()).toEqual("1.5"); + expect(oneDotFive.multiply(new Uint32(2)).toString()).toEqual("3"); + expect(oneDotFive.multiply(new Uint32(4294967295)).toString()).toEqual("6442450942.5"); // original value remain unchanged expect(zero.toString()).toEqual("0"); @@ -240,15 +238,58 @@ describe("Decimal", () => { expect(oneDotFive.toString()).toEqual("1.5"); }); - it("throws for different fractional digits", () => { + it("returns correct values for Uint53", () => { const zero = Decimal.fromUserInput("0", 5); - expect(() => zero.multiply(Decimal.fromUserInput("1", 1))).toThrowError(/do not match/i); - expect(() => zero.multiply(Decimal.fromUserInput("1", 2))).toThrowError(/do not match/i); - expect(() => zero.multiply(Decimal.fromUserInput("1", 3))).toThrowError(/do not match/i); - expect(() => zero.multiply(Decimal.fromUserInput("1", 4))).toThrowError(/do not match/i); + expect(zero.multiply(new Uint53(0)).toString()).toEqual("0"); + expect(zero.multiply(new Uint53(1)).toString()).toEqual("0"); + expect(zero.multiply(new Uint53(2)).toString()).toEqual("0"); + expect(zero.multiply(new Uint53(9007199254740991)).toString()).toEqual("0"); - expect(() => zero.multiply(Decimal.fromUserInput("1", 6))).toThrowError(/do not match/i); - expect(() => zero.multiply(Decimal.fromUserInput("1", 7))).toThrowError(/do not match/i); + const one = Decimal.fromUserInput("1", 5); + expect(one.multiply(new Uint53(0)).toString()).toEqual("0"); + expect(one.multiply(new Uint53(1)).toString()).toEqual("1"); + expect(one.multiply(new Uint53(2)).toString()).toEqual("2"); + expect(one.multiply(new Uint53(9007199254740991)).toString()).toEqual("9007199254740991"); + + const oneDotFive = Decimal.fromUserInput("1.5", 5); + expect(oneDotFive.multiply(new Uint53(0)).toString()).toEqual("0"); + expect(oneDotFive.multiply(new Uint53(1)).toString()).toEqual("1.5"); + expect(oneDotFive.multiply(new Uint53(2)).toString()).toEqual("3"); + expect(oneDotFive.multiply(new Uint53(9007199254740991)).toString()).toEqual("13510798882111486.5"); + + // original value remain unchanged + expect(zero.toString()).toEqual("0"); + expect(one.toString()).toEqual("1"); + expect(oneDotFive.toString()).toEqual("1.5"); + }); + + it("returns correct values for Uint64", () => { + const zero = Decimal.fromUserInput("0", 5); + expect(zero.multiply(Uint64.fromString("0")).toString()).toEqual("0"); + expect(zero.multiply(Uint64.fromString("1")).toString()).toEqual("0"); + expect(zero.multiply(Uint64.fromString("2")).toString()).toEqual("0"); + expect(zero.multiply(Uint64.fromString("18446744073709551615")).toString()).toEqual("0"); + + const one = Decimal.fromUserInput("1", 5); + expect(one.multiply(Uint64.fromString("0")).toString()).toEqual("0"); + expect(one.multiply(Uint64.fromString("1")).toString()).toEqual("1"); + expect(one.multiply(Uint64.fromString("2")).toString()).toEqual("2"); + expect(one.multiply(Uint64.fromString("18446744073709551615")).toString()).toEqual( + "18446744073709551615", + ); + + const oneDotFive = Decimal.fromUserInput("1.5", 5); + expect(oneDotFive.multiply(Uint64.fromString("0")).toString()).toEqual("0"); + expect(oneDotFive.multiply(Uint64.fromString("1")).toString()).toEqual("1.5"); + expect(oneDotFive.multiply(Uint64.fromString("2")).toString()).toEqual("3"); + expect(oneDotFive.multiply(Uint64.fromString("18446744073709551615")).toString()).toEqual( + "27670116110564327422.5", + ); + + // original value remain unchanged + expect(zero.toString()).toEqual("0"); + expect(one.toString()).toEqual("1"); + expect(oneDotFive.toString()).toEqual("1.5"); }); }); diff --git a/packages/math/src/decimal.ts b/packages/math/src/decimal.ts index 969f811c..75ddf103 100644 --- a/packages/math/src/decimal.ts +++ b/packages/math/src/decimal.ts @@ -1,5 +1,7 @@ import BN from "bn.js"; +import { Uint32, Uint53, Uint64 } from "./integers"; + // Too large values lead to massive memory usage. Limit to something sensible. // The largest value we need is 18 (Ether). const maxFractionalDigits = 100; @@ -127,12 +129,10 @@ export class Decimal { /** * a.multiply(b) returns a*b. * - * Both values need to have the same fractional digits. + * We only allow multiplication by unsigned integers to avoid rounding errors. */ - public multiply(b: Decimal): Decimal { - if (this.fractionalDigits !== b.fractionalDigits) throw new Error("Fractional digits do not match"); - const factor = new BN(10).pow(new BN(this.data.fractionalDigits)); - const product = this.data.atomics.mul(new BN(b.atomics)).div(factor); + public multiply(b: Uint32 | Uint53 | Uint64): Decimal { + const product = this.data.atomics.mul(new BN(b.toString())); return new Decimal(product.toString(), this.fractionalDigits); } diff --git a/packages/math/types/decimal.d.ts b/packages/math/types/decimal.d.ts index 102498d4..e2828055 100644 --- a/packages/math/types/decimal.d.ts +++ b/packages/math/types/decimal.d.ts @@ -1,3 +1,4 @@ +import { Uint32, Uint53, Uint64 } from "./integers"; /** * A type for arbitrary precision, non-negative decimals. * @@ -27,9 +28,9 @@ export declare class Decimal { /** * a.multiply(b) returns a*b. * - * Both values need to have the same fractional digits. + * We only allow multiplication by unsigned integers to avoid rounding errors. */ - multiply(b: Decimal): Decimal; + multiply(b: Uint32 | Uint53 | Uint64): Decimal; equals(b: Decimal): boolean; isLessThan(b: Decimal): boolean; isLessThanOrEqual(b: Decimal): boolean;