Merge pull request #399 from CosmWasm/396-fee-table

Update Fee tables
This commit is contained in:
Simon Warta 2020-08-19 12:16:27 +02:00 committed by GitHub
commit 0bd8d5ecf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 596 additions and 98 deletions

View File

@ -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<string, unknown>` for smart query,
@ -13,7 +17,18 @@
- @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
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 +39,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<string, unknown> | undefined`.

View File

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

View File

@ -57,7 +57,7 @@ export async function main(originalArgs: readonly string[]): Promise<void> {
"SearchTxFilter",
// signingcosmwasmclient
"ExecuteResult",
"FeeTable",
"CosmWasmFeeTable",
"InstantiateResult",
"SigningCosmWasmClient",
"UploadMeta",
@ -102,6 +102,7 @@ export async function main(originalArgs: readonly string[]): Promise<void> {
"BroadcastTxResult",
"Coin",
"CosmosClient",
"GasPrice",
"Msg",
"MsgDelegate",
"MsgSend",

View File

@ -21,7 +21,7 @@ export {
} from "./cosmwasmclient";
export {
ExecuteResult,
FeeTable,
CosmWasmFeeTable,
InstantiateOptions,
InstantiateResult,
MigrateResult,

View File

@ -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", () => {

View File

@ -5,8 +5,11 @@ import {
BroadcastMode,
BroadcastTxFailure,
BroadcastTxResult,
buildFeeTable,
Coin,
coins,
CosmosFeeTable,
GasLimits,
GasPrice,
isBroadcastTxFailure,
makeSignBytes,
Msg,
@ -31,14 +34,13 @@ 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 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;
}
@ -52,31 +54,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<CosmWasmFeeTable> = {
upload: 1000000,
init: 500000,
migrate: 200000,
exec: 200000,
send: 80000,
changeAdmin: 80000,
};
export interface UploadMeta {
@ -158,11 +143,16 @@ 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;
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.
@ -173,22 +163,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<FeeTable>,
gasPrice: GasPrice = defaultGasPrice,
gasLimits: Partial<GasLimits<CosmWasmFeeTable>> = {},
broadcastMode = BroadcastMode.Block,
) {
super(apiUrl, broadcastMode);
this.anyValidAddress = senderAddress;
this.senderAddress = senderAddress;
this.signer = signer;
this.fees = { ...defaultFees, ...(customFees || {}) };
this.fees = buildFeeTable<CosmWasmFeeTable>(gasPrice, defaultGasLimits, gasLimits);
}
public async getSequence(address?: string): Promise<GetSequenceResult> {

View File

@ -20,7 +20,7 @@ export {
} from "./cosmwasmclient";
export {
ExecuteResult,
FeeTable,
CosmWasmFeeTable,
InstantiateOptions,
InstantiateResult,
MigrateResult,

View File

@ -1,15 +1,24 @@
import { BroadcastMode, BroadcastTxResult, Coin, Msg, OfflineSigner, StdFee } from "@cosmjs/launchpad";
import {
BroadcastMode,
BroadcastTxResult,
Coin,
CosmosFeeTable,
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 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;
}
@ -81,6 +90,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 +107,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<FeeTable>,
gasPrice?: GasPrice,
gasLimits?: Partial<GasLimits<CosmWasmFeeTable>>,
broadcastMode?: BroadcastMode,
);
getSequence(address?: string): Promise<GetSequenceResult>;

View File

@ -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".

View File

@ -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".

View File

@ -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<CosmosFeeTable> = {
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;

View File

@ -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<FeeTable> = {
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;

View File

@ -0,0 +1,23 @@
import { Decimal } from "@cosmjs/math";
import { GasPrice } from "./gas";
describe("GasPrice", () => {
it("can be constructed", () => {
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");
});
});
});

View File

@ -0,0 +1,56 @@
import { Decimal, Uint53 } from "@cosmjs/math";
import { coins } from "./coins";
import { StdFee } from "./types";
export type FeeTable = Record<string, StdFee>;
export class GasPrice {
public readonly amount: Decimal;
public readonly denom: string;
constructor(amount: Decimal, denom: string) {
this.amount = amount;
this.denom = denom;
}
public static fromString(gasPrice: string): GasPrice {
const matchResult = gasPrice.match(/^(?<amount>.+?)(?<denom>[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<T extends Record<string, StdFee>> = {
readonly [key in keyof T]: number;
};
function calculateFee(gasLimit: number, { denom, amount: gasPriceAmount }: GasPrice): StdFee {
const amount = Math.ceil(gasPriceAmount.multiply(new Uint53(gasLimit)).toFloatApproximation());
return {
amount: coins(amount, denom),
gas: gasLimit.toString(),
};
}
export function buildFeeTable<T extends Record<string, StdFee>>(
gasPrice: GasPrice,
defaultGasLimits: GasLimits<T>,
gasLimits: Partial<GasLimits<T>>,
): T {
return Object.entries(defaultGasLimits).reduce(
(feeTable, [type, defaultGasLimit]) => ({
...feeTable,
[type]: calculateFee(gasLimits[type] || defaultGasLimit, gasPrice),
}),
{} as T,
);
}

View File

@ -29,6 +29,7 @@ export {
isSearchByTagsQuery,
} from "./cosmosclient";
export { makeSignBytes } from "./encoding";
export { buildFeeTable, FeeTable, GasLimits, 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 { CosmosFeeTable, SigningCosmosClient } from "./signingcosmosclient";
export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types";
export {
AccountData,

View File

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

View File

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

View File

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

View File

@ -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 { SigningCosmosClient } from "./signingcosmosclient";
import { PrivateSigningCosmosClient, SigningCosmosClient } from "./signingcosmosclient";
import { makeRandomAddress, pendingWithoutWasmd, validatorAddress } from "./testutils.spec";
const httpUrl = "http://localhost:1317";
@ -22,10 +23,80 @@ 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 () => {
const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic);
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({
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);
const openedClient = (client as unknown) as PrivateSigningCosmosClient;
expect(openedClient.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 = GasPrice.fromString("3.14utest");
const gasLimits = {
send: 160000,
};
const client = new SigningCosmosClient(httpUrl, faucet.address, wallet, gasPrice, gasLimits);
const openedClient = (client as unknown) as PrivateSigningCosmosClient;
expect(openedClient.fees).toEqual({
send: {
amount: [
{
amount: "502400", // 3.14 * 160_000
denom: "utest",
},
],
gas: "160000",
},
});
});
});

View File

@ -1,31 +1,33 @@
/* 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";
/**
* 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 {
export interface CosmosFeeTable extends FeeTable {
readonly send: StdFee;
}
const defaultFees: FeeTable = {
send: {
amount: coins(2000, "ucosm"),
gas: "80000", // 80k
},
};
const defaultGasPrice = GasPrice.fromString("0.025ucosm");
const defaultGasLimits: GasLimits<CosmosFeeTable> = { send: 80000 };
/** Use for testing only */
export interface PrivateSigningCosmosClient {
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.
@ -36,22 +38,23 @@ 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<FeeTable>,
gasPrice: GasPrice = defaultGasPrice,
gasLimits: Partial<GasLimits<CosmosFeeTable>> = {},
broadcastMode = BroadcastMode.Block,
) {
super(apiUrl, broadcastMode);
this.anyValidAddress = senderAddress;
this.senderAddress = senderAddress;
this.signer = signer;
this.fees = { ...defaultFees, ...(customFees || {}) };
this.fees = buildFeeTable<CosmosFeeTable>(gasPrice, defaultGasLimits, gasLimits);
}
public async getSequence(address?: string): Promise<GetSequenceResult> {

17
packages/launchpad/types/gas.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import { Decimal } from "@cosmjs/math";
import { StdFee } from "./types";
export declare type FeeTable = Record<string, StdFee>;
export declare class GasPrice {
readonly amount: Decimal;
readonly denom: string;
constructor(amount: Decimal, denom: string);
static fromString(gasPrice: string): GasPrice;
}
export declare type GasLimits<T extends Record<string, StdFee>> = {
readonly [key in keyof T]: number;
};
export declare function buildFeeTable<T extends Record<string, StdFee>>(
gasPrice: GasPrice,
defaultGasLimits: GasLimits<T>,
gasLimits: Partial<GasLimits<T>>,
): T;

View File

@ -27,6 +27,7 @@ export {
isSearchByTagsQuery,
} from "./cosmosclient";
export { makeSignBytes } from "./encoding";
export { buildFeeTable, FeeTable, GasLimits, 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 { CosmosFeeTable, SigningCosmosClient } from "./signingcosmosclient";
export { isStdTx, pubkeyType, CosmosSdkTx, PubKey, StdFee, StdSignature, StdTx } from "./types";
export {
AccountData,

View File

@ -1,15 +1,20 @@
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";
/**
* 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 {
export interface CosmosFeeTable extends FeeTable {
readonly send: StdFee;
}
/** Use for testing only */
export interface PrivateSigningCosmosClient {
readonly fees: CosmosFeeTable;
}
export declare class SigningCosmosClient extends CosmosClient {
readonly senderAddress: string;
private readonly signer;
@ -23,14 +28,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<FeeTable>,
gasPrice?: GasPrice,
gasLimits?: Partial<GasLimits<CosmosFeeTable>>,
broadcastMode?: BroadcastMode,
);
getSequence(address?: string): Promise<GetSequenceResult>;

View File

@ -1,4 +1,5 @@
import { Decimal } from "./decimal";
import { Uint32, Uint53, Uint64 } from "./integers";
describe("Decimal", () => {
describe("fromAtomics", () => {
@ -211,6 +212,87 @@ describe("Decimal", () => {
});
});
describe("multiply", () => {
it("returns correct values for Uint32", () => {
const zero = Decimal.fromUserInput("0", 5);
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(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(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");
expect(one.toString()).toEqual("1");
expect(oneDotFive.toString()).toEqual("1.5");
});
it("returns correct values for Uint53", () => {
const zero = Decimal.fromUserInput("0", 5);
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");
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");
});
});
describe("equals", () => {
it("returns correct values", () => {
const zero = Decimal.fromUserInput("0", 5);

View File

@ -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;
@ -124,6 +126,16 @@ export class Decimal {
return new Decimal(sum.toString(), this.fractionalDigits);
}
/**
* a.multiply(b) returns a*b.
*
* We only allow multiplication by unsigned integers to avoid rounding errors.
*/
public multiply(b: Uint32 | Uint53 | Uint64): Decimal {
const product = this.data.atomics.mul(new BN(b.toString()));
return new Decimal(product.toString(), this.fractionalDigits);
}
public equals(b: Decimal): boolean {
return Decimal.compare(this, b) === 0;
}

View File

@ -1,3 +1,4 @@
import { Uint32, Uint53, Uint64 } from "./integers";
/**
* A type for arbitrary precision, non-negative decimals.
*
@ -24,6 +25,12 @@ export declare class Decimal {
* Both values need to have the same fractional digits.
*/
plus(b: Decimal): Decimal;
/**
* a.multiply(b) returns a*b.
*
* We only allow multiplication by unsigned integers to avoid rounding errors.
*/
multiply(b: Uint32 | Uint53 | Uint64): Decimal;
equals(b: Decimal): boolean;
isLessThan(b: Decimal): boolean;
isLessThanOrEqual(b: Decimal): boolean;