Merge pull request #407 from CosmWasm/391-faucet-base-tokens
Use denom in faucet
This commit is contained in:
commit
f94e4f692d
@ -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
|
||||
|
||||
@ -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>"
|
||||
],
|
||||
@ -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",
|
||||
|
||||
@ -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" });
|
||||
});
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
|
||||
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,33 +27,33 @@ describe("TokenManager", () => {
|
||||
describe("creditAmount", () => {
|
||||
const tm = new TokenManager(dummyConfig);
|
||||
|
||||
it("returns 10 tokens by default", () => {
|
||||
expect(tm.creditAmount("TOKENZ")).toEqual({
|
||||
it("returns 10_000_000 base tokens by default", () => {
|
||||
expect(tm.creditAmount("utokenz")).toEqual({
|
||||
amount: "10000000",
|
||||
denom: "utokenz",
|
||||
});
|
||||
expect(tm.creditAmount("TRASH")).toEqual({
|
||||
amount: "10000",
|
||||
expect(tm.creditAmount("mtrash")).toEqual({
|
||||
amount: "10000000",
|
||||
denom: "mtrash",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns value from env variable when set", () => {
|
||||
process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22";
|
||||
expect(tm.creditAmount("TRASH")).toEqual({
|
||||
amount: "22000",
|
||||
process.env.FAUCET_CREDIT_AMOUNT_MTRASH = "22";
|
||||
expect(tm.creditAmount("mtrash")).toEqual({
|
||||
amount: "22",
|
||||
denom: "mtrash",
|
||||
});
|
||||
process.env.FAUCET_CREDIT_AMOUNT_TRASH = "";
|
||||
process.env.FAUCET_CREDIT_AMOUNT_MTRASH = "";
|
||||
});
|
||||
|
||||
it("returns default when env variable is set to empty", () => {
|
||||
process.env.FAUCET_CREDIT_AMOUNT_TRASH = "";
|
||||
expect(tm.creditAmount("TRASH")).toEqual({
|
||||
amount: "10000",
|
||||
expect(tm.creditAmount("mtrash")).toEqual({
|
||||
amount: "10000000",
|
||||
denom: "mtrash",
|
||||
});
|
||||
process.env.FAUCET_CREDIT_AMOUNT_TRASH = "";
|
||||
process.env.FAUCET_CREDIT_AMOUNT_MTRASH = "";
|
||||
});
|
||||
});
|
||||
|
||||
@ -62,37 +62,37 @@ describe("TokenManager", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.FAUCET_REFILL_FACTOR = "";
|
||||
process.env.FAUCET_CREDIT_AMOUNT_TRASH = "";
|
||||
process.env.FAUCET_CREDIT_AMOUNT_MTRASH = "";
|
||||
});
|
||||
|
||||
it("returns 20*10 + '000' by default", () => {
|
||||
expect(tm.refillAmount("TRASH")).toEqual({
|
||||
amount: "200000",
|
||||
it("returns 20*10_000_000' by default", () => {
|
||||
expect(tm.refillAmount("mtrash")).toEqual({
|
||||
amount: "200000000",
|
||||
denom: "mtrash",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 20*22 + '000' when credit amount is 22", () => {
|
||||
process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22";
|
||||
expect(tm.refillAmount("TRASH")).toEqual({
|
||||
amount: "440000",
|
||||
it("returns 20*22 when credit amount is 22", () => {
|
||||
process.env.FAUCET_CREDIT_AMOUNT_MTRASH = "22";
|
||||
expect(tm.refillAmount("mtrash")).toEqual({
|
||||
amount: "440",
|
||||
denom: "mtrash",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 30*10 + '000' when refill factor is 30", () => {
|
||||
it("returns 30*10_000_000' when refill factor is 30", () => {
|
||||
process.env.FAUCET_REFILL_FACTOR = "30";
|
||||
expect(tm.refillAmount("TRASH")).toEqual({
|
||||
amount: "300000",
|
||||
expect(tm.refillAmount("mtrash")).toEqual({
|
||||
amount: "300000000",
|
||||
denom: "mtrash",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 30*22 + '000' when refill factor is 30 and credit amount is 22", () => {
|
||||
it("returns 30*22 when refill factor is 30 and credit amount is 22", () => {
|
||||
process.env.FAUCET_REFILL_FACTOR = "30";
|
||||
process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22";
|
||||
expect(tm.refillAmount("TRASH")).toEqual({
|
||||
amount: "660000",
|
||||
process.env.FAUCET_CREDIT_AMOUNT_MTRASH = "22";
|
||||
expect(tm.refillAmount("mtrash")).toEqual({
|
||||
amount: "660",
|
||||
denom: "mtrash",
|
||||
});
|
||||
});
|
||||
@ -103,37 +103,37 @@ describe("TokenManager", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.FAUCET_REFILL_THRESHOLD = "";
|
||||
process.env.FAUCET_CREDIT_AMOUNT_TRASH = "";
|
||||
process.env.FAUCET_CREDIT_AMOUNT_MTRASH = "";
|
||||
});
|
||||
|
||||
it("returns 8*10 + '000' by default", () => {
|
||||
expect(tm.refillThreshold("TRASH")).toEqual({
|
||||
amount: "80000",
|
||||
it("returns 8*10_000_000 by default", () => {
|
||||
expect(tm.refillThreshold("mtrash")).toEqual({
|
||||
amount: "80000000",
|
||||
denom: "mtrash",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 8*22 + '000' when credit amount is 22", () => {
|
||||
process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22";
|
||||
expect(tm.refillThreshold("TRASH")).toEqual({
|
||||
amount: "176000",
|
||||
it("returns 8*22 when credit amount is 22", () => {
|
||||
process.env.FAUCET_CREDIT_AMOUNT_MTRASH = "22";
|
||||
expect(tm.refillThreshold("mtrash")).toEqual({
|
||||
amount: "176",
|
||||
denom: "mtrash",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 5*10 + '000' when refill threshold is 5", () => {
|
||||
it("returns 5*10_000_000 when refill threshold is 5", () => {
|
||||
process.env.FAUCET_REFILL_THRESHOLD = "5";
|
||||
expect(tm.refillThreshold("TRASH")).toEqual({
|
||||
amount: "50000",
|
||||
expect(tm.refillThreshold("mtrash")).toEqual({
|
||||
amount: "50000000",
|
||||
denom: "mtrash",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 5*22 + '000' when refill threshold is 5 and credit amount is 22", () => {
|
||||
it("returns 5*22 when refill threshold is 5 and credit amount is 22", () => {
|
||||
process.env.FAUCET_REFILL_THRESHOLD = "5";
|
||||
process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22";
|
||||
expect(tm.refillThreshold("TRASH")).toEqual({
|
||||
amount: "110000",
|
||||
process.env.FAUCET_CREDIT_AMOUNT_MTRASH = "22";
|
||||
expect(tm.refillThreshold("mtrash")).toEqual({
|
||||
amount: "110",
|
||||
denom: "mtrash",
|
||||
});
|
||||
});
|
||||
@ -161,8 +161,8 @@ describe("TokenManager", () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(tm.needsRefill(brokeAccount, "TOKENZ")).toEqual(true);
|
||||
expect(tm.needsRefill(richAccount, "TOKENZ")).toEqual(false);
|
||||
expect(tm.needsRefill(brokeAccount, "utokenz")).toEqual(true);
|
||||
expect(tm.needsRefill(richAccount, "utokenz")).toEqual(false);
|
||||
});
|
||||
|
||||
it("works for missing balance", () => {
|
||||
@ -170,7 +170,7 @@ describe("TokenManager", () => {
|
||||
address: "cosmos1rtfrpqt3yd7c8g73m9rsaen7fft0h52m3v9v5a",
|
||||
balance: [],
|
||||
};
|
||||
expect(tm.needsRefill(emptyAccount, "TOKENZ")).toEqual(true);
|
||||
expect(tm.needsRefill(emptyAccount, "utokenz")).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user