Skip to content

Use denom in faucet #407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
- @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/faucet: `/credit` API now accepts either `denom` (base token) or as
before `ticker` (unit token). Environmental variables specifying credit
amounts now need to use uppercase denom.
- @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
Expand Down
6 changes: 3 additions & 3 deletions packages/faucet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@cosmjs/faucet",
"version": "0.22.2",
"description": "The faucet",
"contributors":[
"contributors": [
"Ethan Frey <ethanfrey@users.noreply.github.com>",
"Simon Warta <webmaster128@users.noreply.github.com>"
],
Expand Down Expand Up @@ -36,8 +36,8 @@
"test-node": "node jasmine-testrunner.js",
"test": "yarn build-or-skip && yarn test-node",
"coverage": "nyc --reporter=text --reporter=lcov yarn test --quiet",
"start-dev": "FAUCET_CREDIT_AMOUNT_COSM=10 FAUCET_CREDIT_AMOUNT_STAKE=5 FAUCET_CONCURRENCY=3 FAUCET_MNEMONIC=\"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone\" ./bin/cosmos-faucet start \"http://localhost:1317\"",
"start-coralnet": "FAUCET_ADDRESS_PREFIX=coral FAUCET_TOKENS=\"SHELL=10^6ushell, REEF=10^6ureef\" FAUCET_CREDIT_AMOUNT_SHELL=10 FAUCET_CREDIT_AMOUNT_REEF=2 FAUCET_CONCURRENCY=3 FAUCET_MNEMONIC=\"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone\" ./bin/cosmos-faucet start \"https://lcd.coralnet.cosmwasm.com\""
"start-dev": "FAUCET_CREDIT_AMOUNT_UCOSM=10000000 FAUCET_CREDIT_AMOUNT_USTAKE=5000000 FAUCET_CONCURRENCY=3 FAUCET_MNEMONIC=\"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone\" ./bin/cosmos-faucet start \"http://localhost:1317\"",
"start-coralnet": "FAUCET_ADDRESS_PREFIX=coral FAUCET_TOKENS=\"SHELL=10^6ushell, REEF=10^6ureef\" FAUCET_CREDIT_AMOUNT_USHELL=10000000 FAUCET_CREDIT_AMOUNT_UREEF=2000000 FAUCET_CONCURRENCY=3 FAUCET_MNEMONIC=\"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone\" ./bin/cosmos-faucet start \"https://lcd.coralnet.cosmwasm.com\""
},
"dependencies": {
"@cosmjs/crypto": "^0.22.2",
Expand Down
39 changes: 35 additions & 4 deletions packages/faucet/src/api/requestparser.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { RequestParser } from "./requestparser";

describe("RequestParser", () => {
it("can process valid credit request", () => {
it("can process valid credit request with denom", () => {
const body = { address: "abc", denom: "utkn" };
expect(RequestParser.parseCreditBody(body)).toEqual({ address: "abc", denom: "utkn" });
});

it("can process valid credit request with ticker", () => {
const body = { address: "abc", ticker: "TKN" };
expect(RequestParser.parseCreditBody(body)).toEqual({ address: "abc", ticker: "TKN" });
});
Expand Down Expand Up @@ -37,16 +42,42 @@ describe("RequestParser", () => {
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'address' must not be empty/i);
}

// ticker unset
// denom and ticker unset
{
const body = { address: "abc" };
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'ticker' must be a string/i);
expect(() => RequestParser.parseCreditBody(body)).toThrowError(
/Exactly one of properties 'denom' or 'ticker' must be a string/i,
);
}

// denom and ticker both set
{
const body = { address: "abc", denom: "ustake", ticker: "COSM" };
expect(() => RequestParser.parseCreditBody(body)).toThrowError(
/Exactly one of properties 'denom' or 'ticker' must be a string/i,
);
}

// denom wrong type
{
const body = { address: "abc", denom: true };
expect(() => RequestParser.parseCreditBody(body)).toThrowError(
/Exactly one of properties 'denom' or 'ticker' must be a string/i,
);
}

// denom empty
{
const body = { address: "abc", denom: "" };
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'denom' must not be empty/i);
}

// ticker wrong type
{
const body = { address: "abc", ticker: true };
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'ticker' must be a string/i);
expect(() => RequestParser.parseCreditBody(body)).toThrowError(
/Exactly one of properties 'denom' or 'ticker' must be a string/i,
);
}

// ticker empty
Expand Down
45 changes: 36 additions & 9 deletions packages/faucet/src/api/requestparser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,35 @@ import { isNonNullObject } from "@cosmjs/utils";

import { HttpError } from "./httperror";

export interface CreditRequestBodyData {
export interface CreditRequestBodyDataWithDenom {
/** The base denomination */
readonly denom: string;
/** The recipient address */
readonly address: string;
}

export interface CreditRequestBodyDataWithTicker {
/** The ticker symbol */
readonly ticker: string;
/** The recipient address */
readonly address: string;
}

export type CreditRequestBodyData = CreditRequestBodyDataWithDenom | CreditRequestBodyDataWithTicker;

export function isCreditRequestBodyDataWithDenom(
data: CreditRequestBodyData,
): data is CreditRequestBodyDataWithDenom {
return typeof (data as CreditRequestBodyDataWithDenom).denom === "string";
}

export class RequestParser {
public static parseCreditBody(body: unknown): CreditRequestBodyData {
if (!isNonNullObject(body) || Array.isArray(body)) {
throw new HttpError(400, "Request body must be a dictionary.");
}

const { address, ticker } = body as any;
const { address, denom, ticker } = body as any;

if (typeof address !== "string") {
throw new HttpError(400, "Property 'address' must be a string.");
Expand All @@ -25,17 +40,29 @@ export class RequestParser {
throw new HttpError(400, "Property 'address' must not be empty.");
}

if (typeof ticker !== "string") {
throw new HttpError(400, "Property 'ticker' must be a string");
if (
(typeof denom !== "string" && typeof ticker !== "string") ||
(typeof denom === "string" && typeof ticker === "string")
) {
throw new HttpError(400, "Exactly one of properties 'denom' or 'ticker' must be a string");
}

if (ticker.length === 0) {
if (typeof ticker === "string" && ticker.length === 0) {
throw new HttpError(400, "Property 'ticker' must not be empty.");
}

return {
address: address,
ticker: ticker,
};
if (typeof denom === "string" && denom.length === 0) {
throw new HttpError(400, "Property 'denom' must not be empty.");
}

return denom
? {
address: address,
denom: denom,
}
: {
address: address,
ticker: ticker,
};
}
}
20 changes: 16 additions & 4 deletions packages/faucet/src/api/webserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { isValidAddress } from "../addresses";
import * as constants from "../constants";
import { Faucet } from "../faucet";
import { HttpError } from "./httperror";
import { RequestParser } from "./requestparser";
import { isCreditRequestBodyDataWithDenom, RequestParser } from "./requestparser";

/** This will be passed 1:1 to the user */
export interface ChainConstants {
Expand Down Expand Up @@ -57,20 +57,32 @@ export class Webserver {

// context.request.body is set by the bodyParser() plugin
const requestBody = context.request.body;
const { address, ticker } = RequestParser.parseCreditBody(requestBody);
const creditBody = RequestParser.parseCreditBody(requestBody);

const { address } = creditBody;
let denom: string | undefined;
let ticker: string | undefined;
if (isCreditRequestBodyDataWithDenom(creditBody)) {
({ denom } = creditBody);
} else {
({ ticker } = creditBody);
}

if (!isValidAddress(address, constants.addressPrefix)) {
throw new HttpError(400, "Address is not in the expected format for this chain.");
}

const availableTokens = await faucet.availableTokens();
if (availableTokens.indexOf(ticker) === -1) {
const matchingToken = availableTokens.find(
(token) => token.denom === denom || token.tickerSymbol === ticker,
);
if (matchingToken === undefined) {
const tokens = JSON.stringify(availableTokens);
throw new HttpError(422, `Token is not available. Available tokens are: ${tokens}`);
}

try {
await faucet.credit(address, ticker);
await faucet.credit(address, matchingToken.denom);
} catch (e) {
console.error(e);
throw new HttpError(500, "Sending tokens failed");
Expand Down
4 changes: 1 addition & 3 deletions packages/faucet/src/debugging.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Coin } from "@cosmjs/launchpad";
import { Decimal } from "@cosmjs/math";

import { TokenConfiguration } from "./tokenmanager";
import { MinimalAccount, SendJob } from "./types";
Expand All @@ -8,8 +7,7 @@ import { MinimalAccount, SendJob } from "./types";
function debugCoin(coin: Coin, tokens: TokenConfiguration): string {
const meta = tokens.bankTokens.find((token) => token.denom == coin.denom);
if (!meta) throw new Error(`No token configuration found for denom ${coin.denom}`);
const value = Decimal.fromAtomics(coin.amount, meta.fractionalDigits).toString();
return `${value} ${meta?.tickerSymbol}`;
return `${coin.amount} ${meta?.denom}`;
}

/** A string representation of a balance in a human-readable format that can change at any time */
Expand Down
11 changes: 7 additions & 4 deletions packages/faucet/src/faucet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,14 @@ describe("Faucet", () => {
expect(tickers).toEqual([]);
});

it("is empty when no tokens are configured", async () => {
it("is not empty with default token config", async () => {
pendingWithoutWasmd();
const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3);
const tickers = await faucet.availableTokens();
expect(tickers).toEqual(["COSM", "STAKE"]);
expect(tickers).toEqual([
{ denom: "ucosm", tickerSymbol: "COSM", fractionalDigits: 6 },
{ denom: "ustake", tickerSymbol: "STAKE", fractionalDigits: 6 },
]);
});
});

Expand Down Expand Up @@ -113,7 +116,7 @@ describe("Faucet", () => {
pendingWithoutWasmd();
const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3);
const recipient = makeRandomAddress();
await faucet.credit(recipient, "COSM");
await faucet.credit(recipient, "ucosm");

const readOnlyClient = new CosmosClient(httpUrl);
const account = await readOnlyClient.getAccount(recipient);
Expand All @@ -130,7 +133,7 @@ describe("Faucet", () => {
pendingWithoutWasmd();
const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3);
const recipient = makeRandomAddress();
await faucet.credit(recipient, "STAKE");
await faucet.credit(recipient, "ustake");

const readOnlyClient = new CosmosClient(httpUrl);
const account = await readOnlyClient.getAccount(recipient);
Expand Down
24 changes: 12 additions & 12 deletions packages/faucet/src/faucet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as constants from "./constants";
import { debugAccount, logAccountsState, logSendJob } from "./debugging";
import { createWallets } from "./profile";
import { TokenConfiguration, TokenManager } from "./tokenmanager";
import { BankTokenMeta } from "./tokens";
import { MinimalAccount, SendJob } from "./types";

function isDefined<X>(value: X | undefined): value is X {
Expand Down Expand Up @@ -74,15 +75,14 @@ export class Faucet {
/**
* Returns a list of ticker symbols of tokens owned by the the holder and configured in the faucet
*/
public async availableTokens(): Promise<readonly string[]> {
public async availableTokens(): Promise<readonly BankTokenMeta[]> {
const holderAccount = await this.readOnlyClient.getAccount(this.holderAddress);
const balance = holderAccount ? holderAccount.balance : [];

return balance
.filter((b) => b.amount !== "0")
.map((b) => this.tokenConfig.bankTokens.find((token) => token.denom == b.denom))
.filter(isDefined)
.map((token) => token.tickerSymbol);
.filter(isDefined);
}

/**
Expand All @@ -94,14 +94,14 @@ export class Faucet {
assertIsBroadcastTxSuccess(result);
}

/** Use one of the distributor accounts to send tokend to user */
public async credit(recipient: string, tickerSymbol: string): Promise<void> {
/** Use one of the distributor accounts to send tokens to user */
public async credit(recipient: string, denom: string): Promise<void> {
if (this.distributorAddresses.length === 0) throw new Error("No distributor account available");
const sender = this.distributorAddresses[this.getCreditCount() % this.distributorAddresses.length];
const job: SendJob = {
sender: sender,
recipient: recipient,
amount: this.tokenManager.creditAmount(tickerSymbol),
amount: this.tokenManager.creditAmount(denom),
};
if (this.logging) logSendJob(job, this.tokenConfig);
await this.send(job);
Expand Down Expand Up @@ -141,17 +141,17 @@ export class Faucet {
if (this.logging) logAccountsState(accounts, this.tokenConfig);
const [_, ...distributorAccounts] = accounts;

const availableTokens = await this.availableTokens();
if (this.logging) console.info("Available tokens:", availableTokens);
const availableTokenDenoms = (await this.availableTokens()).map((token) => token.denom);
if (this.logging) console.info("Available tokens:", availableTokenDenoms);

const jobs: SendJob[] = [];
for (const tickerSymbol of availableTokens) {
for (const denom of availableTokenDenoms) {
const refillDistibutors = distributorAccounts.filter((account) =>
this.tokenManager.needsRefill(account, tickerSymbol),
this.tokenManager.needsRefill(account, denom),
);

if (this.logging) {
console.info(`Refilling ${tickerSymbol} of:`);
console.info(`Refilling ${denom} of:`);
console.info(
refillDistibutors.length
? refillDistibutors.map((r) => ` ${debugAccount(r, this.tokenConfig)}`).join("\n")
Expand All @@ -162,7 +162,7 @@ export class Faucet {
jobs.push({
sender: this.holderAddress,
recipient: refillDistibutor.address,
amount: this.tokenManager.refillAmount(tickerSymbol),
amount: this.tokenManager.refillAmount(denom),
});
}
}
Expand Down
Loading