mirror of
https://github.com/cosmos/cosmjs.git
synced 2025-03-10 21:49:15 +00:00
Merge pull request #425 from CosmWasm/remove-ticker-from-faucet
Remove ticker/fractional digits from faucet
This commit is contained in:
commit
e499b46bfd
@ -23,9 +23,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/faucet: `/credit` API now expects `denom` (base token) instead of
|
||||
`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
|
||||
|
@ -19,8 +19,8 @@ yarn dev-start
|
||||
Advanced users that want to provide their custom config can start as follows:
|
||||
|
||||
```
|
||||
FAUCET_CREDIT_AMOUNT_COSM=10 \
|
||||
FAUCET_CREDIT_AMOUNT_STAKE=5 \
|
||||
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"
|
||||
@ -53,11 +53,10 @@ 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".
|
||||
FAUCET_TOKENS A comma separated list of tokens configs in the format
|
||||
{DISPLAY}=10^{DIGITS}{base}, e.g.
|
||||
"ATOM=10^6uatom" or "COSM = 10^6ucosm, STAKE = 10^3mstake".
|
||||
FAUCET_TOKENS A comma separated list of token denoms, e.g.
|
||||
"uatom" or "ucosm, mstake".
|
||||
FAUCET_CREDIT_AMOUNT_TKN Send this amount of TKN to a user requesting TKN. TKN is
|
||||
a placeholder for the token ticker. Defaults to 10.
|
||||
a placeholder for the token's denom. Defaults to 10000000.
|
||||
FAUCET_REFILL_FACTOR Send factor times credit amount on refilling. Defauls to 8.
|
||||
FAUCET_REFILL_THRESHOLD Refill when balance gets below factor times credit amount.
|
||||
Defaults to 20.
|
||||
@ -124,7 +123,7 @@ situation is different.
|
||||
```
|
||||
curl --header "Content-Type: application/json" \
|
||||
--request POST \
|
||||
--data '{"ticker":"ISA","address":"cosmos1yre6ac7qfgyfgvh58ph0rgw627rhw766y430qq"}' \
|
||||
--data '{"denom":"ucosm","address":"cosmos1yre6ac7qfgyfgvh58ph0rgw627rhw766y430qq"}' \
|
||||
http://localhost:8000/credit
|
||||
```
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
||||
"test": "yarn build-or-skip && yarn test-node",
|
||||
"coverage": "nyc --reporter=text --reporter=lcov yarn test --quiet",
|
||||
"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\""
|
||||
"start-coralnet": "FAUCET_ADDRESS_PREFIX=coral FAUCET_TOKENS=\"ushell,ureef\" 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.3",
|
||||
|
@ -26,11 +26,10 @@ 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".
|
||||
FAUCET_TOKENS A comma separated list of tokens configs in the format
|
||||
{DISPLAY}=10^{DIGITS}{base}, e.g.
|
||||
"ATOM=10^6uatom" or "COSM = 10^6ucosm, STAKE = 10^3mstake".
|
||||
FAUCET_TOKENS A comma separated list of token denoms, e.g.
|
||||
"uatom" or "ucosm, mstake".
|
||||
FAUCET_CREDIT_AMOUNT_TKN Send this amount of TKN to a user requesting TKN. TKN is
|
||||
a placeholder for the token ticker. Defaults to 10.
|
||||
a placeholder for the token's denom. Defaults to 10000000.
|
||||
FAUCET_REFILL_FACTOR Send factor times credit amount on refilling. Defauls to 8.
|
||||
FAUCET_REFILL_THRESHOLD Refill when balance gets below factor times credit amount.
|
||||
Defaults to 20.
|
||||
|
@ -28,10 +28,10 @@ export async function start(args: readonly string[]): Promise<void> {
|
||||
constants.concurrency,
|
||||
true,
|
||||
);
|
||||
const chainTokens = faucet.loadTokenTickers();
|
||||
const chainTokens = faucet.configuredTokens();
|
||||
console.info("Chain tokens:", chainTokens);
|
||||
const accounts = await faucet.loadAccounts();
|
||||
logAccountsState(accounts, constants.tokenConfig);
|
||||
logAccountsState(accounts);
|
||||
let availableTokens = await faucet.availableTokens();
|
||||
console.info("Available tokens:", availableTokens);
|
||||
setInterval(async () => {
|
||||
|
@ -6,9 +6,16 @@ describe("RequestParser", () => {
|
||||
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" });
|
||||
it("throws helpful error message when ticker is found", () => {
|
||||
const oldBody = { address: "abc", ticker: "TKN" };
|
||||
expect(() => RequestParser.parseCreditBody(oldBody)).toThrowError(
|
||||
/The 'ticker' field was removed in CosmJS 0.23. Please use 'denom' instead./i,
|
||||
);
|
||||
|
||||
const confusedBody = { address: "abc", ticker: "TKN", denom: "utkn" };
|
||||
expect(() => RequestParser.parseCreditBody(confusedBody)).toThrowError(
|
||||
/The 'ticker' field was removed in CosmJS 0.23. Please use 'denom' instead./i,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws for invalid credit requests", () => {
|
||||
@ -26,44 +33,32 @@ describe("RequestParser", () => {
|
||||
|
||||
// address unset
|
||||
{
|
||||
const body = { ticker: "TKN" };
|
||||
const body = { denom: "utkn" };
|
||||
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'address' must be a string/i);
|
||||
}
|
||||
|
||||
// address wrong type
|
||||
{
|
||||
const body = { address: true, ticker: "TKN" };
|
||||
const body = { address: true, denom: "utkn" };
|
||||
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'address' must be a string/i);
|
||||
}
|
||||
|
||||
// address empty
|
||||
{
|
||||
const body = { address: "", ticker: "TKN" };
|
||||
const body = { address: "", denom: "utkn" };
|
||||
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'address' must not be empty/i);
|
||||
}
|
||||
|
||||
// denom and ticker unset
|
||||
// denom unset
|
||||
{
|
||||
const body = { address: "abc" };
|
||||
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,
|
||||
);
|
||||
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'denom' 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,
|
||||
);
|
||||
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'denom' must be a string/i);
|
||||
}
|
||||
|
||||
// denom empty
|
||||
@ -71,19 +66,5 @@ describe("RequestParser", () => {
|
||||
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(
|
||||
/Exactly one of properties 'denom' or 'ticker' must be a string/i,
|
||||
);
|
||||
}
|
||||
|
||||
// ticker empty
|
||||
{
|
||||
const body = { address: "abc", ticker: "" };
|
||||
expect(() => RequestParser.parseCreditBody(body)).toThrowError(/Property 'ticker' must not be empty/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import { isNonNullObject } from "@cosmjs/utils";
|
||||
|
||||
import { HttpError } from "./httperror";
|
||||
|
||||
export interface CreditRequestBodyDataWithDenom {
|
||||
export interface CreditRequestBodyData {
|
||||
/** The base denomination */
|
||||
readonly denom: string;
|
||||
/** The recipient address */
|
||||
@ -16,14 +16,6 @@ export interface CreditRequestBodyDataWithTicker {
|
||||
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)) {
|
||||
@ -32,6 +24,10 @@ export class RequestParser {
|
||||
|
||||
const { address, denom, ticker } = body as any;
|
||||
|
||||
if (typeof ticker !== "undefined") {
|
||||
throw new HttpError(400, "The 'ticker' field was removed in CosmJS 0.23. Please use 'denom' instead.");
|
||||
}
|
||||
|
||||
if (typeof address !== "string") {
|
||||
throw new HttpError(400, "Property 'address' must be a string.");
|
||||
}
|
||||
@ -40,29 +36,17 @@ export class RequestParser {
|
||||
throw new HttpError(400, "Property 'address' must not be empty.");
|
||||
}
|
||||
|
||||
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 (typeof denom !== "string") {
|
||||
throw new HttpError(400, "Property 'denom' must be a string.");
|
||||
}
|
||||
|
||||
if (typeof ticker === "string" && ticker.length === 0) {
|
||||
throw new HttpError(400, "Property 'ticker' must not be empty.");
|
||||
}
|
||||
|
||||
if (typeof denom === "string" && denom.length === 0) {
|
||||
if (denom.length === 0) {
|
||||
throw new HttpError(400, "Property 'denom' must not be empty.");
|
||||
}
|
||||
|
||||
return denom
|
||||
? {
|
||||
address: address,
|
||||
denom: denom,
|
||||
}
|
||||
: {
|
||||
address: address,
|
||||
ticker: ticker,
|
||||
};
|
||||
return {
|
||||
address: address,
|
||||
denom: denom,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { isValidAddress } from "../addresses";
|
||||
import * as constants from "../constants";
|
||||
import { Faucet } from "../faucet";
|
||||
import { HttpError } from "./httperror";
|
||||
import { isCreditRequestBodyDataWithDenom, RequestParser } from "./requestparser";
|
||||
import { RequestParser } from "./requestparser";
|
||||
|
||||
/** This will be passed 1:1 to the user */
|
||||
export interface ChainConstants {
|
||||
@ -35,7 +35,7 @@ export class Webserver {
|
||||
case "/status": {
|
||||
const [holder, ...distributors] = await faucet.loadAccounts();
|
||||
const availableTokens = await faucet.availableTokens();
|
||||
const chainTokens = faucet.loadTokenTickers();
|
||||
const chainTokens = faucet.configuredTokens();
|
||||
context.response.body = {
|
||||
status: "ok",
|
||||
...chainConstants,
|
||||
@ -59,30 +59,20 @@ export class Webserver {
|
||||
const requestBody = context.request.body;
|
||||
const creditBody = RequestParser.parseCreditBody(requestBody);
|
||||
|
||||
const { address } = creditBody;
|
||||
let denom: string | undefined;
|
||||
let ticker: string | undefined;
|
||||
if (isCreditRequestBodyDataWithDenom(creditBody)) {
|
||||
({ denom } = creditBody);
|
||||
} else {
|
||||
({ ticker } = creditBody);
|
||||
}
|
||||
const { address, denom } = 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();
|
||||
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}`);
|
||||
const matchingDenom = availableTokens.find((availableDenom) => availableDenom === denom);
|
||||
if (matchingDenom === undefined) {
|
||||
throw new HttpError(422, `Token is not available. Available tokens are: ${availableTokens}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await faucet.credit(address, matchingToken.denom);
|
||||
await faucet.credit(address, matchingDenom);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new HttpError(500, "Sending tokens failed");
|
||||
|
@ -14,5 +14,5 @@ export const port: number = Number.parseInt(process.env.FAUCET_PORT || "", 10) |
|
||||
export const mnemonic: string | undefined = process.env.FAUCET_MNEMONIC;
|
||||
export const addressPrefix = process.env.FAUCET_ADDRESS_PREFIX || "cosmos";
|
||||
export const tokenConfig: TokenConfiguration = {
|
||||
bankTokens: parseBankTokens(process.env.FAUCET_TOKENS || "COSM=10^6ucosm, STAKE=10^6ustake"),
|
||||
bankTokens: parseBankTokens(process.env.FAUCET_TOKENS || "ucosm, ustake"),
|
||||
};
|
||||
|
@ -1,38 +1,35 @@
|
||||
import { Coin } from "@cosmjs/launchpad";
|
||||
|
||||
import { TokenConfiguration } from "./tokenmanager";
|
||||
import { MinimalAccount, SendJob } from "./types";
|
||||
|
||||
/** A string representation of a coin in a human-readable format that can change at any time */
|
||||
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}`);
|
||||
return `${coin.amount} ${meta?.denom}`;
|
||||
function debugCoin(coin: Coin): string {
|
||||
return `${coin.amount} ${coin.denom}`;
|
||||
}
|
||||
|
||||
/** A string representation of a balance in a human-readable format that can change at any time */
|
||||
export function debugBalance(data: readonly Coin[], tokens: TokenConfiguration): string {
|
||||
return `[${data.map((b) => debugCoin(b, tokens)).join(", ")}]`;
|
||||
export function debugBalance(data: readonly Coin[]): string {
|
||||
return `[${data.map((b) => debugCoin(b)).join(", ")}]`;
|
||||
}
|
||||
|
||||
/** A string representation of an account in a human-readable format that can change at any time */
|
||||
export function debugAccount(account: MinimalAccount, tokens: TokenConfiguration): string {
|
||||
return `${account.address}: ${debugBalance(account.balance, tokens)}`;
|
||||
export function debugAccount(account: MinimalAccount): string {
|
||||
return `${account.address}: ${debugBalance(account.balance)}`;
|
||||
}
|
||||
|
||||
export function logAccountsState(accounts: readonly MinimalAccount[], tokens: TokenConfiguration): void {
|
||||
export function logAccountsState(accounts: readonly MinimalAccount[]): void {
|
||||
if (accounts.length < 2) {
|
||||
throw new Error("List of accounts must contain at least one token holder and one distributor");
|
||||
}
|
||||
const holder = accounts[0];
|
||||
const distributors = accounts.slice(1);
|
||||
console.info("Holder:\n" + ` ${debugAccount(holder, tokens)}`);
|
||||
console.info("Distributors:\n" + distributors.map((r) => ` ${debugAccount(r, tokens)}`).join("\n"));
|
||||
console.info("Holder:\n" + ` ${debugAccount(holder)}`);
|
||||
console.info("Distributors:\n" + distributors.map((r) => ` ${debugAccount(r)}`).join("\n"));
|
||||
}
|
||||
|
||||
export function logSendJob(job: SendJob, tokens: TokenConfiguration): void {
|
||||
export function logSendJob(job: SendJob): void {
|
||||
const from = job.sender;
|
||||
const to = job.recipient;
|
||||
const amount = debugCoin(job.amount, tokens);
|
||||
const amount = debugCoin(job.amount);
|
||||
console.info(`Sending ${amount} from ${from} to ${to} ...`);
|
||||
}
|
||||
|
@ -14,18 +14,7 @@ function pendingWithoutWasmd(): void {
|
||||
|
||||
const httpUrl = "http://localhost:1317";
|
||||
const defaultTokenConfig: TokenConfiguration = {
|
||||
bankTokens: [
|
||||
{
|
||||
fractionalDigits: 6,
|
||||
tickerSymbol: "COSM",
|
||||
denom: "ucosm",
|
||||
},
|
||||
{
|
||||
fractionalDigits: 6,
|
||||
tickerSymbol: "STAKE",
|
||||
denom: "ustake",
|
||||
},
|
||||
],
|
||||
bankTokens: ["ucosm", "ustake"],
|
||||
};
|
||||
const defaultAddressPrefix = "cosmos";
|
||||
|
||||
@ -57,10 +46,7 @@ describe("Faucet", () => {
|
||||
pendingWithoutWasmd();
|
||||
const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3);
|
||||
const tickers = await faucet.availableTokens();
|
||||
expect(tickers).toEqual([
|
||||
{ denom: "ucosm", tickerSymbol: "COSM", fractionalDigits: 6 },
|
||||
{ denom: "ustake", tickerSymbol: "STAKE", fractionalDigits: 6 },
|
||||
]);
|
||||
expect(tickers).toEqual(["ucosm", "ustake"]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -147,12 +133,12 @@ describe("Faucet", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadTokenTickers", () => {
|
||||
describe("configuredTokens", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3);
|
||||
const tickers = faucet.loadTokenTickers();
|
||||
expect(tickers).toEqual(["COSM", "STAKE"]);
|
||||
const tickers = faucet.configuredTokens();
|
||||
expect(tickers).toEqual(["ucosm", "ustake"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -10,7 +10,6 @@ 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 {
|
||||
@ -73,15 +72,15 @@ export class Faucet {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of ticker symbols of tokens owned by the the holder and configured in the faucet
|
||||
* Returns a list of denoms of tokens owned by the the holder and configured in the faucet
|
||||
*/
|
||||
public async availableTokens(): Promise<readonly BankTokenMeta[]> {
|
||||
public async availableTokens(): Promise<string[]> {
|
||||
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))
|
||||
.map((b) => this.tokenConfig.bankTokens.find((token) => token == b.denom))
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
@ -103,12 +102,13 @@ export class Faucet {
|
||||
recipient: recipient,
|
||||
amount: this.tokenManager.creditAmount(denom),
|
||||
};
|
||||
if (this.logging) logSendJob(job, this.tokenConfig);
|
||||
if (this.logging) logSendJob(job);
|
||||
await this.send(job);
|
||||
}
|
||||
|
||||
public loadTokenTickers(): readonly string[] {
|
||||
return this.tokenConfig.bankTokens.map((token) => token.tickerSymbol);
|
||||
/** Returns a list to token denoms which are configured */
|
||||
public configuredTokens(): string[] {
|
||||
return Array.from(this.tokenConfig.bankTokens);
|
||||
}
|
||||
|
||||
public async loadAccounts(): Promise<readonly MinimalAccount[]> {
|
||||
@ -134,14 +134,14 @@ export class Faucet {
|
||||
public async refill(): Promise<void> {
|
||||
if (this.logging) {
|
||||
console.info(`Connected to network: ${await this.readOnlyClient.getChainId()}`);
|
||||
console.info(`Tokens on network: ${this.loadTokenTickers().join(", ")}`);
|
||||
console.info(`Tokens on network: ${this.configuredTokens().join(", ")}`);
|
||||
}
|
||||
|
||||
const accounts = await this.loadAccounts();
|
||||
if (this.logging) logAccountsState(accounts, this.tokenConfig);
|
||||
if (this.logging) logAccountsState(accounts);
|
||||
const [_, ...distributorAccounts] = accounts;
|
||||
|
||||
const availableTokenDenoms = (await this.availableTokens()).map((token) => token.denom);
|
||||
const availableTokenDenoms = await this.availableTokens();
|
||||
if (this.logging) console.info("Available tokens:", availableTokenDenoms);
|
||||
|
||||
const jobs: SendJob[] = [];
|
||||
@ -154,7 +154,7 @@ export class Faucet {
|
||||
console.info(`Refilling ${denom} of:`);
|
||||
console.info(
|
||||
refillDistibutors.length
|
||||
? refillDistibutors.map((r) => ` ${debugAccount(r, this.tokenConfig)}`).join("\n")
|
||||
? refillDistibutors.map((r) => ` ${debugAccount(r)}`).join("\n")
|
||||
: " none",
|
||||
);
|
||||
}
|
||||
@ -168,7 +168,7 @@ export class Faucet {
|
||||
}
|
||||
if (jobs.length > 0) {
|
||||
for (const job of jobs) {
|
||||
if (this.logging) logSendJob(job, this.tokenConfig);
|
||||
if (this.logging) logSendJob(job);
|
||||
// don't crash faucet when one send fails
|
||||
try {
|
||||
await this.send(job);
|
||||
@ -180,7 +180,7 @@ export class Faucet {
|
||||
|
||||
if (this.logging) {
|
||||
console.info("Done refilling accounts.");
|
||||
logAccountsState(await this.loadAccounts(), this.tokenConfig);
|
||||
logAccountsState(await this.loadAccounts());
|
||||
}
|
||||
} else {
|
||||
if (this.logging) {
|
||||
|
@ -2,18 +2,7 @@ import { TokenConfiguration, TokenManager } from "./tokenmanager";
|
||||
import { MinimalAccount } from "./types";
|
||||
|
||||
const dummyConfig: TokenConfiguration = {
|
||||
bankTokens: [
|
||||
{
|
||||
tickerSymbol: "TOKENZ",
|
||||
fractionalDigits: 6,
|
||||
denom: "utokenz",
|
||||
},
|
||||
{
|
||||
tickerSymbol: "TRASH",
|
||||
fractionalDigits: 3,
|
||||
denom: "mtrash",
|
||||
},
|
||||
],
|
||||
bankTokens: ["utokenz", "mtrash"],
|
||||
};
|
||||
|
||||
describe("TokenManager", () => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Coin } from "@cosmjs/launchpad";
|
||||
import { Decimal, Uint53 } from "@cosmjs/math";
|
||||
|
||||
import { BankTokenMeta } from "./tokens";
|
||||
import { MinimalAccount } from "./types";
|
||||
|
||||
const defaultCreditAmount = 10_000_000;
|
||||
@ -14,7 +13,7 @@ const defaultRefillThresholdFactor = 8;
|
||||
|
||||
export interface TokenConfiguration {
|
||||
/** Supported tokens of the Cosmos SDK bank module */
|
||||
readonly bankTokens: readonly BankTokenMeta[];
|
||||
readonly bankTokens: readonly string[];
|
||||
}
|
||||
|
||||
export class TokenManager {
|
||||
@ -29,11 +28,9 @@ export class TokenManager {
|
||||
const amountFromEnv = process.env[`FAUCET_CREDIT_AMOUNT_${denom.toUpperCase()}`];
|
||||
const amount = amountFromEnv ? Uint53.fromString(amountFromEnv).toNumber() : defaultCreditAmount;
|
||||
const value = new Uint53(amount * factor.toNumber());
|
||||
|
||||
const meta = this.getTokenMetaForDenom(denom);
|
||||
return {
|
||||
amount: value.toString(),
|
||||
denom: meta.denom,
|
||||
denom: denom,
|
||||
};
|
||||
}
|
||||
|
||||
@ -51,26 +48,12 @@ export class TokenManager {
|
||||
|
||||
/** true iff the distributor account needs a refill */
|
||||
public needsRefill(account: MinimalAccount, denom: string): boolean {
|
||||
const meta = this.getTokenMetaForDenom(denom);
|
||||
const balanceAmount = account.balance.find((b) => b.denom === denom);
|
||||
|
||||
const balanceAmount = account.balance.find((b) => b.denom === meta.denom);
|
||||
|
||||
const balance = Decimal.fromAtomics(balanceAmount ? balanceAmount.amount : "0", meta.fractionalDigits);
|
||||
const balance = Decimal.fromAtomics(balanceAmount ? balanceAmount.amount : "0", 0);
|
||||
const thresholdAmount = this.refillThreshold(denom);
|
||||
const threshold = Decimal.fromAtomics(thresholdAmount.amount, meta.fractionalDigits);
|
||||
const threshold = Decimal.fromAtomics(thresholdAmount.amount, 0);
|
||||
|
||||
return balance.isLessThan(threshold);
|
||||
}
|
||||
|
||||
private getTokenMetaForDenom(denom: string): BankTokenMeta {
|
||||
const match = this.config.bankTokens.find((token) => token.denom === denom);
|
||||
if (!match) throw new Error(`No token found for denom: ${denom}`);
|
||||
return match;
|
||||
}
|
||||
|
||||
private getTokenMetaForTicker(ticker: string): BankTokenMeta {
|
||||
const match = this.config.bankTokens.find((token) => token.tickerSymbol === ticker);
|
||||
if (!match) throw new Error(`No token found for ticker: ${ticker}`);
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
@ -3,76 +3,29 @@ import { parseBankToken, parseBankTokens } from "./tokens";
|
||||
describe("tokens", () => {
|
||||
describe("parseBankToken", () => {
|
||||
it("works", () => {
|
||||
expect(parseBankToken("COSM=10^6ucosm")).toEqual({
|
||||
tickerSymbol: "COSM",
|
||||
fractionalDigits: 6,
|
||||
denom: "ucosm",
|
||||
});
|
||||
expect(parseBankToken("ucosm")).toEqual("ucosm");
|
||||
});
|
||||
|
||||
it("allows using whitespace", () => {
|
||||
expect(parseBankToken("COSM = 10^6 ucosm")).toEqual({
|
||||
tickerSymbol: "COSM",
|
||||
fractionalDigits: 6,
|
||||
denom: "ucosm",
|
||||
});
|
||||
expect(parseBankToken(" ucosm\n")).toEqual("ucosm");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseBankTokens", () => {
|
||||
it("works for one", () => {
|
||||
expect(parseBankTokens("COSM=10^6ucosm")).toEqual([
|
||||
{
|
||||
tickerSymbol: "COSM",
|
||||
fractionalDigits: 6,
|
||||
denom: "ucosm",
|
||||
},
|
||||
]);
|
||||
expect(parseBankTokens("ucosm")).toEqual(["ucosm"]);
|
||||
});
|
||||
|
||||
it("works for two", () => {
|
||||
expect(parseBankTokens("COSM=10^6ucosm,STAKE=10^3mstake")).toEqual([
|
||||
{
|
||||
tickerSymbol: "COSM",
|
||||
fractionalDigits: 6,
|
||||
denom: "ucosm",
|
||||
},
|
||||
{
|
||||
tickerSymbol: "STAKE",
|
||||
fractionalDigits: 3,
|
||||
denom: "mstake",
|
||||
},
|
||||
]);
|
||||
expect(parseBankTokens("ucosm,mstake")).toEqual(["ucosm", "mstake"]);
|
||||
});
|
||||
|
||||
it("ignores whitespace", () => {
|
||||
expect(parseBankTokens("COSM=10^6ucosm, STAKE=10^3mstake\n")).toEqual([
|
||||
{
|
||||
tickerSymbol: "COSM",
|
||||
fractionalDigits: 6,
|
||||
denom: "ucosm",
|
||||
},
|
||||
{
|
||||
tickerSymbol: "STAKE",
|
||||
fractionalDigits: 3,
|
||||
denom: "mstake",
|
||||
},
|
||||
]);
|
||||
expect(parseBankTokens("ucosm, mstake\n")).toEqual(["ucosm", "mstake"]);
|
||||
});
|
||||
|
||||
it("ignores empty elements", () => {
|
||||
expect(parseBankTokens("COSM=10^6ucosm,STAKE=10^3mstake,")).toEqual([
|
||||
{
|
||||
tickerSymbol: "COSM",
|
||||
fractionalDigits: 6,
|
||||
denom: "ucosm",
|
||||
},
|
||||
{
|
||||
tickerSymbol: "STAKE",
|
||||
fractionalDigits: 3,
|
||||
denom: "mstake",
|
||||
},
|
||||
]);
|
||||
expect(parseBankTokens("ucosm,mstake,")).toEqual(["ucosm", "mstake"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,38 +1,14 @@
|
||||
import { Uint53 } from "@cosmjs/math";
|
||||
const parseBankTokenPattern = /^([a-zA-Z]{2,20})$/;
|
||||
|
||||
export interface BankTokenMeta {
|
||||
readonly denom: string;
|
||||
/**
|
||||
* The token ticker symbol, e.g. ATOM or ETH.
|
||||
*/
|
||||
readonly tickerSymbol: string;
|
||||
/**
|
||||
* The number of fractional digits the token supports.
|
||||
*
|
||||
* A quantity is expressed as atomic units. 10^fractionalDigits of those
|
||||
* atomic units make up 1 token.
|
||||
*
|
||||
* E.g. in Ethereum 10^18 wei are 1 ETH and from the quantity 123000000000000000000
|
||||
* the last 18 digits are the fractional part and the rest the wole part.
|
||||
*/
|
||||
readonly fractionalDigits: number;
|
||||
}
|
||||
|
||||
const parseBankTokenPattern = /^([a-zA-Z]{2,20})=10\^([0-9]+)([a-zA-Z]{2,20})$/;
|
||||
|
||||
export function parseBankToken(input: string): BankTokenMeta {
|
||||
export function parseBankToken(input: string): string {
|
||||
const match = input.replace(/\s/g, "").match(parseBankTokenPattern);
|
||||
if (!match) {
|
||||
throw new Error("Token could not be parsed. Format: {DISPLAY}=10^{DIGITS}{base}, e.g. ATOM=10^6uatom");
|
||||
}
|
||||
return {
|
||||
tickerSymbol: match[1],
|
||||
fractionalDigits: Uint53.fromString(match[2]).toNumber(),
|
||||
denom: match[3],
|
||||
};
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export function parseBankTokens(input: string): BankTokenMeta[] {
|
||||
export function parseBankTokens(input: string): string[] {
|
||||
return input
|
||||
.trim()
|
||||
.split(",")
|
||||
|
Loading…
x
Reference in New Issue
Block a user