Skip to content

Commit f94e4f6

Browse files
authored
Merge pull request #407 from CosmWasm/391-faucet-base-tokens
Use denom in faucet
2 parents 7a3d5a9 + 7b84d7a commit f94e4f6

10 files changed

+180
-98
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
- @cosmjs/faucet: Environmental variable `FAUCET_FEE` renamed to
2121
`FAUCET_GAS_PRICE` and now only accepts one token. Environmental variable
2222
`FAUCET_GAS` renamed to `FAUCET_GAS_LIMIT`.
23+
- @cosmjs/faucet: `/credit` API now accepts either `denom` (base token) or as
24+
before `ticker` (unit token). Environmental variables specifying credit
25+
amounts now need to use uppercase denom.
2326
- @cosmjs/launchpad: Rename `FeeTable` type to `CosmosFeeTable` and export a new
2427
more generic type `FeeTable`.
2528
- @cosmjs/launchpad: Add new class `GasPrice`, new helper type `GasLimits` and

packages/faucet/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@cosmjs/faucet",
33
"version": "0.22.2",
44
"description": "The faucet",
5-
"contributors":[
5+
"contributors": [
66
"Ethan Frey <ethanfrey@users.noreply.github.com>",
77
"Simon Warta <webmaster128@users.noreply.github.com>"
88
],
@@ -36,8 +36,8 @@
3636
"test-node": "node jasmine-testrunner.js",
3737
"test": "yarn build-or-skip && yarn test-node",
3838
"coverage": "nyc --reporter=text --reporter=lcov yarn test --quiet",
39-
"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\"",
40-
"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\""
39+
"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\"",
40+
"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\""
4141
},
4242
"dependencies": {
4343
"@cosmjs/crypto": "^0.22.2",

packages/faucet/src/api/requestparser.spec.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { RequestParser } from "./requestparser";
22

33
describe("RequestParser", () => {
4-
it("can process valid credit request", () => {
4+
it("can process valid credit request with denom", () => {
5+
const body = { address: "abc", denom: "utkn" };
6+
expect(RequestParser.parseCreditBody(body)).toEqual({ address: "abc", denom: "utkn" });
7+
});
8+
9+
it("can process valid credit request with ticker", () => {
510
const body = { address: "abc", ticker: "TKN" };
611
expect(RequestParser.parseCreditBody(body)).toEqual({ address: "abc", ticker: "TKN" });
712
});
@@ -37,16 +42,42 @@ describe("RequestParser", () => {
3742
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'address' must not be empty/i);
3843
}
3944

40-
// ticker unset
45+
// denom and ticker unset
4146
{
4247
const body = { address: "abc" };
43-
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'ticker' must be a string/i);
48+
expect(() => RequestParser.parseCreditBody(body)).toThrowError(
49+
/Exactly one of properties 'denom' or 'ticker' must be a string/i,
50+
);
51+
}
52+
53+
// denom and ticker both set
54+
{
55+
const body = { address: "abc", denom: "ustake", ticker: "COSM" };
56+
expect(() => RequestParser.parseCreditBody(body)).toThrowError(
57+
/Exactly one of properties 'denom' or 'ticker' must be a string/i,
58+
);
59+
}
60+
61+
// denom wrong type
62+
{
63+
const body = { address: "abc", denom: true };
64+
expect(() => RequestParser.parseCreditBody(body)).toThrowError(
65+
/Exactly one of properties 'denom' or 'ticker' must be a string/i,
66+
);
67+
}
68+
69+
// denom empty
70+
{
71+
const body = { address: "abc", denom: "" };
72+
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'denom' must not be empty/i);
4473
}
4574

4675
// ticker wrong type
4776
{
4877
const body = { address: "abc", ticker: true };
49-
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'ticker' must be a string/i);
78+
expect(() => RequestParser.parseCreditBody(body)).toThrowError(
79+
/Exactly one of properties 'denom' or 'ticker' must be a string/i,
80+
);
5081
}
5182

5283
// ticker empty

packages/faucet/src/api/requestparser.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,35 @@ import { isNonNullObject } from "@cosmjs/utils";
22

33
import { HttpError } from "./httperror";
44

5-
export interface CreditRequestBodyData {
5+
export interface CreditRequestBodyDataWithDenom {
6+
/** The base denomination */
7+
readonly denom: string;
8+
/** The recipient address */
9+
readonly address: string;
10+
}
11+
12+
export interface CreditRequestBodyDataWithTicker {
613
/** The ticker symbol */
714
readonly ticker: string;
815
/** The recipient address */
916
readonly address: string;
1017
}
1118

19+
export type CreditRequestBodyData = CreditRequestBodyDataWithDenom | CreditRequestBodyDataWithTicker;
20+
21+
export function isCreditRequestBodyDataWithDenom(
22+
data: CreditRequestBodyData,
23+
): data is CreditRequestBodyDataWithDenom {
24+
return typeof (data as CreditRequestBodyDataWithDenom).denom === "string";
25+
}
26+
1227
export class RequestParser {
1328
public static parseCreditBody(body: unknown): CreditRequestBodyData {
1429
if (!isNonNullObject(body) || Array.isArray(body)) {
1530
throw new HttpError(400, "Request body must be a dictionary.");
1631
}
1732

18-
const { address, ticker } = body as any;
33+
const { address, denom, ticker } = body as any;
1934

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

28-
if (typeof ticker !== "string") {
29-
throw new HttpError(400, "Property 'ticker' must be a string");
43+
if (
44+
(typeof denom !== "string" && typeof ticker !== "string") ||
45+
(typeof denom === "string" && typeof ticker === "string")
46+
) {
47+
throw new HttpError(400, "Exactly one of properties 'denom' or 'ticker' must be a string");
3048
}
3149

32-
if (ticker.length === 0) {
50+
if (typeof ticker === "string" && ticker.length === 0) {
3351
throw new HttpError(400, "Property 'ticker' must not be empty.");
3452
}
3553

36-
return {
37-
address: address,
38-
ticker: ticker,
39-
};
54+
if (typeof denom === "string" && denom.length === 0) {
55+
throw new HttpError(400, "Property 'denom' must not be empty.");
56+
}
57+
58+
return denom
59+
? {
60+
address: address,
61+
denom: denom,
62+
}
63+
: {
64+
address: address,
65+
ticker: ticker,
66+
};
4067
}
4168
}

packages/faucet/src/api/webserver.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { isValidAddress } from "../addresses";
66
import * as constants from "../constants";
77
import { Faucet } from "../faucet";
88
import { HttpError } from "./httperror";
9-
import { RequestParser } from "./requestparser";
9+
import { isCreditRequestBodyDataWithDenom, RequestParser } from "./requestparser";
1010

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

5858
// context.request.body is set by the bodyParser() plugin
5959
const requestBody = context.request.body;
60-
const { address, ticker } = RequestParser.parseCreditBody(requestBody);
60+
const creditBody = RequestParser.parseCreditBody(requestBody);
61+
62+
const { address } = creditBody;
63+
let denom: string | undefined;
64+
let ticker: string | undefined;
65+
if (isCreditRequestBodyDataWithDenom(creditBody)) {
66+
({ denom } = creditBody);
67+
} else {
68+
({ ticker } = creditBody);
69+
}
6170

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

6675
const availableTokens = await faucet.availableTokens();
67-
if (availableTokens.indexOf(ticker) === -1) {
76+
const matchingToken = availableTokens.find(
77+
(token) => token.denom === denom || token.tickerSymbol === ticker,
78+
);
79+
if (matchingToken === undefined) {
6880
const tokens = JSON.stringify(availableTokens);
6981
throw new HttpError(422, `Token is not available. Available tokens are: ${tokens}`);
7082
}
7183

7284
try {
73-
await faucet.credit(address, ticker);
85+
await faucet.credit(address, matchingToken.denom);
7486
} catch (e) {
7587
console.error(e);
7688
throw new HttpError(500, "Sending tokens failed");

packages/faucet/src/debugging.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Coin } from "@cosmjs/launchpad";
2-
import { Decimal } from "@cosmjs/math";
32

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

1513
/** A string representation of a balance in a human-readable format that can change at any time */

packages/faucet/src/faucet.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,14 @@ describe("Faucet", () => {
5353
expect(tickers).toEqual([]);
5454
});
5555

56-
it("is empty when no tokens are configured", async () => {
56+
it("is not empty with default token config", async () => {
5757
pendingWithoutWasmd();
5858
const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3);
5959
const tickers = await faucet.availableTokens();
60-
expect(tickers).toEqual(["COSM", "STAKE"]);
60+
expect(tickers).toEqual([
61+
{ denom: "ucosm", tickerSymbol: "COSM", fractionalDigits: 6 },
62+
{ denom: "ustake", tickerSymbol: "STAKE", fractionalDigits: 6 },
63+
]);
6164
});
6265
});
6366

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

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

135138
const readOnlyClient = new CosmosClient(httpUrl);
136139
const account = await readOnlyClient.getAccount(recipient);

packages/faucet/src/faucet.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as constants from "./constants";
1010
import { debugAccount, logAccountsState, logSendJob } from "./debugging";
1111
import { createWallets } from "./profile";
1212
import { TokenConfiguration, TokenManager } from "./tokenmanager";
13+
import { BankTokenMeta } from "./tokens";
1314
import { MinimalAccount, SendJob } from "./types";
1415

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

8182
return balance
8283
.filter((b) => b.amount !== "0")
8384
.map((b) => this.tokenConfig.bankTokens.find((token) => token.denom == b.denom))
84-
.filter(isDefined)
85-
.map((token) => token.tickerSymbol);
85+
.filter(isDefined);
8686
}
8787

8888
/**
@@ -94,14 +94,14 @@ export class Faucet {
9494
assertIsBroadcastTxSuccess(result);
9595
}
9696

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

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

147147
const jobs: SendJob[] = [];
148-
for (const tickerSymbol of availableTokens) {
148+
for (const denom of availableTokenDenoms) {
149149
const refillDistibutors = distributorAccounts.filter((account) =>
150-
this.tokenManager.needsRefill(account, tickerSymbol),
150+
this.tokenManager.needsRefill(account, denom),
151151
);
152152

153153
if (this.logging) {
154-
console.info(`Refilling ${tickerSymbol} of:`);
154+
console.info(`Refilling ${denom} of:`);
155155
console.info(
156156
refillDistibutors.length
157157
? refillDistibutors.map((r) => ` ${debugAccount(r, this.tokenConfig)}`).join("\n")
@@ -162,7 +162,7 @@ export class Faucet {
162162
jobs.push({
163163
sender: this.holderAddress,
164164
recipient: refillDistibutor.address,
165-
amount: this.tokenManager.refillAmount(tickerSymbol),
165+
amount: this.tokenManager.refillAmount(denom),
166166
});
167167
}
168168
}

0 commit comments

Comments
 (0)