faucet: Refactor faucet to use denom

Still accepts ticker via credit API request
This commit is contained in:
willclarktech 2020-08-19 15:51:12 +01:00
parent 0bd8d5ecf2
commit 795451ee14
No known key found for this signature in database
GPG Key ID: 551A86E2E398ADF7
5 changed files with 88 additions and 43 deletions

View File

@ -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.");
@ -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,
};
}
}

View File

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

View File

@ -1,5 +1,4 @@
import { Coin } from "@cosmjs/launchpad";
import { Decimal } from "@cosmjs/math";
import { TokenConfiguration } from "./tokenmanager";
import { MinimalAccount, SendJob } from "./types";
@ -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 */

View File

@ -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 {
@ -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);
}
/**
@ -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);
@ -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")
@ -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),
});
}
}

View File

@ -4,6 +4,8 @@ import { Decimal, Uint53 } from "@cosmjs/math";
import { BankTokenMeta } from "./tokens";
import { MinimalAccount } from "./types";
const defaultCreditAmount = 10_000_000;
/** Send `factor` times credit amount on refilling */
const defaultRefillFactor = 20;
@ -23,46 +25,52 @@ export class TokenManager {
}
/** The amount of tokens that will be sent to the user */
public creditAmount(tickerSymbol: string, factor: Uint53 = new Uint53(1)): Coin {
const amountFromEnv = process.env[`FAUCET_CREDIT_AMOUNT_${tickerSymbol}`];
const amount = amountFromEnv ? Uint53.fromString(amountFromEnv).toNumber() : 10;
public creditAmount(denom: string, factor: Uint53 = new Uint53(1)): Coin {
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.getTokenMeta(tickerSymbol);
const meta = this.getTokenMetaForDenom(denom);
return {
amount: value.toString() + "0".repeat(meta.fractionalDigits),
amount: value.toString(),
denom: meta.denom,
};
}
public refillAmount(tickerSymbol: string): Coin {
public refillAmount(denom: string): Coin {
const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_FACTOR || "0", 10) || undefined;
const factor = new Uint53(factorFromEnv || defaultRefillFactor);
return this.creditAmount(tickerSymbol, factor);
return this.creditAmount(denom, factor);
}
public refillThreshold(tickerSymbol: string): Coin {
public refillThreshold(denom: string): Coin {
const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_THRESHOLD || "0", 10) || undefined;
const factor = new Uint53(factorFromEnv || defaultRefillThresholdFactor);
return this.creditAmount(tickerSymbol, factor);
return this.creditAmount(denom, factor);
}
/** true iff the distributor account needs a refill */
public needsRefill(account: MinimalAccount, tickerSymbol: string): boolean {
const meta = this.getTokenMeta(tickerSymbol);
public needsRefill(account: MinimalAccount, denom: string): boolean {
const meta = this.getTokenMetaForDenom(denom);
const balanceAmount = account.balance.find((b) => b.denom === meta.denom);
const balance = Decimal.fromAtomics(balanceAmount ? balanceAmount.amount : "0", meta.fractionalDigits);
const thresholdAmount = this.refillThreshold(tickerSymbol);
const thresholdAmount = this.refillThreshold(denom);
const threshold = Decimal.fromAtomics(thresholdAmount.amount, meta.fractionalDigits);
return balance.isLessThan(threshold);
}
private getTokenMeta(tickerSymbol: string): BankTokenMeta {
const match = this.config.bankTokens.find((token) => token.tickerSymbol === tickerSymbol);
if (!match) throw new Error(`No token found for ticker symbol: ${tickerSymbol}`);
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;
}
}