Skip to content

Commit

Permalink
Merge pull request #803 from cosmos/fromString-es6
Browse files Browse the repository at this point in the history
Avoid the use of es2018 named capture groups
  • Loading branch information
webmaster128 authored May 18, 2021
2 parents 9f242d3 + 8782f16 commit 01196d0
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 22 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ and this project adheres to
`CosmWasmClient.broadcastTx` and `StargateClient.broadcastTx` ([#800]). This
bug was introduced with the switch from broadcast mode "commit" to "sync" in
version 0.25.0.
- @cosmjs/launchpad, @cosmjs/stargate: Avoid the use of named capture groups in
`GasPrice.fromString` to restore ES2017 compatibility and make the library
work with Hermes ([#801]; thanks [@AlexBHarley]).
- @cosmjs/launchpad: Adapt `GasPrice.fromString` denom pattern to Cosmos SDK
0.39 rules: reduce denom length to 16 and allow digits in denom.
- @cosmjs/stargate: Adapt `GasPrice.fromString` denom pattern to Cosmos SDK 0.42
rules: allow lengths up to 128, allow upper case letters and digits.

[#800]: https://github.com/cosmos/cosmjs/issues/800
[#801]: https://github.com/cosmos/cosmjs/issues/801
[@alexbharley]: https://github.com/AlexBHarley

## [0.25.2] - 2021-05-11

Expand Down
44 changes: 38 additions & 6 deletions packages/launchpad/src/fee.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,44 @@ describe("GasPrice", () => {
});
});

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");
describe("fromString", () => {
it("works", () => {
const inputs: Record<string, { amount: string; denom: string }> = {
// Test amounts
"3.14utest": { amount: "3.14", denom: "utest" },
"3utest": { amount: "3", denom: "utest" },
"0.14utest": { amount: "0.14", denom: "utest" },
// Test denoms
"0.14sht": { amount: "0.14", denom: "sht" },
"0.14testtesttesttest": { amount: "0.14", denom: "testtesttesttest" },
"0.14ucoin2": { amount: "0.14", denom: "ucoin2" },
};
for (const [input, expected] of Object.entries(inputs)) {
const gasPrice = GasPrice.fromString(input);
expect(gasPrice.amount.toString()).withContext(`Input: ${input}`).toEqual(expected.amount);
expect(gasPrice.denom).withContext(`Input: ${input}`).toEqual(expected.denom);
}
});

it("errors for invalid gas price", () => {
// Checks basic format <amount><denom>
expect(() => GasPrice.fromString("")).toThrowError(/Invalid gas price string/i);
expect(() => GasPrice.fromString("utkn")).toThrowError(/Invalid gas price string/i);
expect(() => GasPrice.fromString("@utkn")).toThrowError(/Invalid gas price string/i);
expect(() => GasPrice.fromString("234")).toThrowError(/Invalid gas price string/i);
expect(() => GasPrice.fromString("-234tkn")).toThrowError(/Invalid gas price string/i);
// Checks details of <denom>
expect(() => GasPrice.fromString("234t")).toThrowError(/denom must be between 3 and 16 characters/i);
expect(() => GasPrice.fromString("234tt")).toThrowError(/denom must be between 3 and 16 characters/i);
expect(() => GasPrice.fromString("234ttttttttttttttttt")).toThrowError(
/denom must be between 3 and 16 characters/i,
);
expect(() => GasPrice.fromString("234ATOM")).toThrowError(
/denom must only contain lower case letters a-z and digits 0-9/i,
);
// Checks details of <amount>
expect(() => GasPrice.fromString("3.utkn")).toThrowError(/Fractional part missing/i);
expect(() => GasPrice.fromString("..utkn")).toThrowError(/More than one separator found/i);
});
});
});
34 changes: 29 additions & 5 deletions packages/launchpad/src/fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@ import { Decimal, Uint53 } from "@cosmjs/math";

export type FeeTable = Record<string, StdFee>;

/**
* Denom checker for the Cosmos SDK 0.39 denom pattern
* (https://github.com/cosmos/cosmos-sdk/blob/v0.39.3/types/coin.go#L597-L598).
*
* This is like a regexp but with helpful error messages.
*/
function checkDenom(denom: string): void {
if (denom.length < 3 || denom.length > 16) {
throw new Error("Denom must be between 3 and 16 characters");
}
if (denom.match(/[^a-z0-9]/)) {
throw new Error("Denom must only contain lower case letters a-z and digits 0-9");
}
}

/**
* A gas price, i.e. the price of a single unit of gas. This is typically a fraction of
* the smallest fee token unit, such as 0.012utoken.
*/
export class GasPrice {
public readonly amount: Decimal;
public readonly denom: string;
Expand All @@ -12,15 +31,20 @@ export class GasPrice {
this.denom = denom;
}

/**
* Parses a gas price formatted as `<amount><denom>`, e.g. `GasPrice.fromString("0.012utoken")`.
*
* The denom must match the Cosmos SDK 0.39 pattern (https://github.com/cosmos/cosmos-sdk/blob/v0.39.3/types/coin.go#L597-L598).
* See `GasPrice` in @cosmjs/stargate for a more generic matcher.
*/
public static fromString(gasPrice: string): GasPrice {
const matchResult = gasPrice.match(/^(?<amount>.+?)(?<denom>[a-z]+)$/);
// Use Decimal.fromUserInput and checkDenom for detailed checks and helpful error messages
const matchResult = gasPrice.match(/^([0-9.]+)([a-z][a-z0-9]*)$/i);
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 [_, amount, denom] = matchResult;
checkDenom(denom);
const fractionalDigits = 18;
const decimalAmount = Decimal.fromUserInput(amount, fractionalDigits);
return new GasPrice(decimalAmount, denom);
Expand Down
49 changes: 43 additions & 6 deletions packages/stargate/src/fee.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,49 @@ describe("GasPrice", () => {
});
});

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");
describe("fromString", () => {
it("works", () => {
const inputs: Record<string, { amount: string; denom: string }> = {
// Test amounts
"3.14utest": { amount: "3.14", denom: "utest" },
"3utest": { amount: "3", denom: "utest" },
"0.14utest": { amount: "0.14", denom: "utest" },
// Test denoms
"0.14sht": { amount: "0.14", denom: "sht" },
"0.14testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest": {
amount: "0.14",
denom:
"testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest",
},
"0.14ucoin2": { amount: "0.14", denom: "ucoin2" },
// eslint-disable-next-line @typescript-eslint/naming-convention
"0.14FOOBAR": { amount: "0.14", denom: "FOOBAR" },
};
for (const [input, expected] of Object.entries(inputs)) {
const gasPrice = GasPrice.fromString(input);
expect(gasPrice.amount.toString()).withContext(`Input: ${input}`).toEqual(expected.amount);
expect(gasPrice.denom).withContext(`Input: ${input}`).toEqual(expected.denom);
}
});

it("errors for invalid gas price", () => {
// Checks basic format <amount><denom>
expect(() => GasPrice.fromString("")).toThrowError(/Invalid gas price string/i);
expect(() => GasPrice.fromString("utkn")).toThrowError(/Invalid gas price string/i);
expect(() => GasPrice.fromString("@utkn")).toThrowError(/Invalid gas price string/i);
expect(() => GasPrice.fromString("234")).toThrowError(/Invalid gas price string/i);
expect(() => GasPrice.fromString("-234tkn")).toThrowError(/Invalid gas price string/i);
// Checks details of <denom>
expect(() => GasPrice.fromString("234t")).toThrowError(/denom must be between 3 and 128 characters/i);
expect(() => GasPrice.fromString("234tt")).toThrowError(/denom must be between 3 and 128 characters/i);
expect(() =>
GasPrice.fromString(
"234ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt",
),
).toThrowError(/denom must be between 3 and 128 characters/i);
// Checks details of <amount>
expect(() => GasPrice.fromString("3.utkn")).toThrowError(/Fractional part missing/i);
expect(() => GasPrice.fromString("..utkn")).toThrowError(/More than one separator found/i);
});
});
});
32 changes: 27 additions & 5 deletions packages/stargate/src/fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ import { coins } from "@cosmjs/proto-signing";
export type FeeTable = Record<string, StdFee>;

/**
* Denom checker for the Cosmos SDK 0.42 denom pattern
* (https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/types/coin.go#L599-L601).
*
* This is like a regexp but with helpful error messages.
*/
function checkDenom(denom: string): void {
if (denom.length < 3 || denom.length > 128) {
throw new Error("Denom must be between 3 and 128 characters");
}
}

/**
* A gas price, i.e. the price of a single unit of gas. This is typically a fraction of
* the smallest fee token unit, such as 0.012utoken.
*
* This is the same as GasPrice from @cosmjs/launchpad but those might diverge in the future.
*/
export class GasPrice {
Expand All @@ -19,15 +34,22 @@ export class GasPrice {
this.denom = denom;
}

/**
* Parses a gas price formatted as `<amount><denom>`, e.g. `GasPrice.fromString("0.012utoken")`.
*
* The denom must match the Cosmos SDK 0.42 pattern (https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/types/coin.go#L599-L601).
* See `GasPrice` in @cosmjs/stargate for a more generic matcher.
*
* Separators are not yet supported.
*/
public static fromString(gasPrice: string): GasPrice {
const matchResult = gasPrice.match(/^(?<amount>.+?)(?<denom>[a-z]+)$/);
// Use Decimal.fromUserInput and checkDenom for detailed checks and helpful error messages
const matchResult = gasPrice.match(/^([0-9.]+)([a-z][a-z0-9]*)$/i);
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 [_, amount, denom] = matchResult;
checkDenom(denom);
const fractionalDigits = 18;
const decimalAmount = Decimal.fromUserInput(amount, fractionalDigits);
return new GasPrice(decimalAmount, denom);
Expand Down

0 comments on commit 01196d0

Please sign in to comment.