From 5f32796bfc9b6784d2b47c839321119131b0e5de Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 11:06:16 +0100 Subject: [PATCH] Pull out TokenManager --- packages/faucet/src/actions/start/start.ts | 2 +- packages/faucet/src/faucet.spec.ts | 128 ----------------- packages/faucet/src/faucet.ts | 71 ++-------- packages/faucet/src/tokenmanager.spec.ts | 157 +++++++++++++++++++++ packages/faucet/src/tokenmanager.ts | 67 +++++++++ 5 files changed, 234 insertions(+), 191 deletions(-) create mode 100644 packages/faucet/src/tokenmanager.spec.ts create mode 100644 packages/faucet/src/tokenmanager.ts diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index deb80177..ec0a4f2c 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -134,7 +134,7 @@ export async function start(args: ReadonlyArray): Promise { const job: SendJob = { sender: sender, recipient: address, - amount: faucet.creditAmount(ticker), + amount: faucet.tokenManager.creditAmount(ticker), tokenTicker: ticker, }; logSendJob(job); diff --git a/packages/faucet/src/faucet.spec.ts b/packages/faucet/src/faucet.spec.ts index 6d45e334..5da32df9 100644 --- a/packages/faucet/src/faucet.spec.ts +++ b/packages/faucet/src/faucet.spec.ts @@ -1,5 +1,4 @@ import { TokenConfiguration } from "@cosmwasm/bcp"; -import { TokenTicker } from "@iov/bcp"; import { Faucet } from "./faucet"; @@ -27,131 +26,4 @@ describe("Faucet", () => { expect(faucet).toBeTruthy(); }); }); - - describe("creditAmount", () => { - const faucet = new Faucet(dummyConfig); - - it("returns 10 tokens by default", () => { - expect(faucet.creditAmount("TOKENZ" as TokenTicker)).toEqual({ - quantity: "10000000", - fractionalDigits: 6, - tokenTicker: "TOKENZ", - }); - expect(faucet.creditAmount("TRASH" as TokenTicker)).toEqual({ - quantity: "10000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - }); - - it("returns value from env variable when set", () => { - process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22"; - expect(faucet.creditAmount("TRASH" as TokenTicker)).toEqual({ - quantity: "22000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; - }); - - it("returns default when env variable is set to empty", () => { - process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; - expect(faucet.creditAmount("TRASH" as TokenTicker)).toEqual({ - quantity: "10000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; - }); - }); - - describe("refillAmount", () => { - const faucet = new Faucet(dummyConfig); - - beforeEach(() => { - process.env.FAUCET_REFILL_FACTOR = ""; - process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; - }); - - it("returns 20*10 + '000' by default", () => { - expect(faucet.refillAmount("TRASH" as TokenTicker)).toEqual({ - quantity: "200000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - }); - - it("returns 20*22 + '000' when credit amount is 22", () => { - process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22"; - expect(faucet.refillAmount("TRASH" as TokenTicker)).toEqual({ - quantity: "440000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - }); - - it("returns 30*10 + '000' when refill factor is 30", () => { - process.env.FAUCET_REFILL_FACTOR = "30"; - expect(faucet.refillAmount("TRASH" as TokenTicker)).toEqual({ - quantity: "300000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - }); - - it("returns 30*22 + '000' when refill factor is 30 and credit amount is 22", () => { - process.env.FAUCET_REFILL_FACTOR = "30"; - process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22"; - expect(faucet.refillAmount("TRASH" as TokenTicker)).toEqual({ - quantity: "660000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - }); - }); - - describe("refillThreshold", () => { - const faucet = new Faucet(dummyConfig); - - beforeEach(() => { - process.env.FAUCET_REFILL_THRESHOLD = ""; - process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; - }); - - it("returns 8*10 + '000' by default", () => { - expect(faucet.refillThreshold("TRASH" as TokenTicker)).toEqual({ - quantity: "80000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - }); - - it("returns 8*22 + '000' when credit amount is 22", () => { - process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22"; - expect(faucet.refillThreshold("TRASH" as TokenTicker)).toEqual({ - quantity: "176000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - }); - - it("returns 5*10 + '000' when refill threshold is 5", () => { - process.env.FAUCET_REFILL_THRESHOLD = "5"; - expect(faucet.refillThreshold("TRASH" as TokenTicker)).toEqual({ - quantity: "50000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - }); - - it("returns 5*22 + '000' when refill threshold is 5 and credit amount is 22", () => { - process.env.FAUCET_REFILL_THRESHOLD = "5"; - process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22"; - expect(faucet.refillThreshold("TRASH" as TokenTicker)).toEqual({ - quantity: "110000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - }); - }); }); diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index 42d6cec8..7a29e7b7 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -1,6 +1,5 @@ import { TokenConfiguration } from "@cosmwasm/bcp"; -import { Account, Amount, BlockchainConnection, TokenTicker, TxCodec } from "@iov/bcp"; -import { Decimal, Uint53 } from "@iov/encoding"; +import { BlockchainConnection, TxCodec } from "@iov/bcp"; import { UserProfile } from "@iov/keycontrol"; import { sleep } from "@iov/utils"; @@ -12,45 +11,15 @@ import { loadTokenTickers, send, } from "./multichainhelpers"; +import { TokenManager } from "./tokenmanager"; import { SendJob } from "./types"; -/** Send `factor` times credit amount on refilling */ -const defaultRefillFactor = 20; - -/** refill when balance gets below `factor` times credit amount */ -const defaultRefillThresholdFactor = 8; - export class Faucet { - private readonly config: TokenConfiguration; + /** will be private soon */ + public readonly tokenManager: TokenManager; public constructor(config: TokenConfiguration) { - this.config = config; - } - - /** The amount of tokens that will be sent to the user */ - public creditAmount(token: TokenTicker, factor: Uint53 = new Uint53(1)): Amount { - const amountFromEnv = process.env[`FAUCET_CREDIT_AMOUNT_${token}`]; - const amount = amountFromEnv ? Uint53.fromString(amountFromEnv).toNumber() : 10; - const value = new Uint53(amount * factor.toNumber()); - - const fractionalDigits = this.getFractionalDigits(token); - return { - quantity: value.toString() + "0".repeat(fractionalDigits), - fractionalDigits: fractionalDigits, - tokenTicker: token, - }; - } - - public refillAmount(token: TokenTicker): Amount { - const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_FACTOR || "0", 10) || undefined; - const factor = new Uint53(factorFromEnv || defaultRefillFactor); - return this.creditAmount(token, factor); - } - - public refillThreshold(token: TokenTicker): Amount { - const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_THRESHOLD || "0", 10) || undefined; - const factor = new Uint53(factorFromEnv || defaultRefillThresholdFactor); - return this.creditAmount(token, factor); + this.tokenManager = new TokenManager(config); } public async refill(profile: UserProfile, connection: BlockchainConnection, codec: TxCodec): Promise { @@ -70,7 +39,9 @@ export class Faucet { const jobs: SendJob[] = []; for (const token of availableTokens) { - const refillDistibutors = distributorAccounts.filter(account => this.needsRefill(account, token)); + const refillDistibutors = distributorAccounts.filter(account => + this.tokenManager.needsRefill(account, token), + ); console.info(`Refilling ${token} of:`); console.info( refillDistibutors.length ? refillDistibutors.map(r => ` ${debugAccount(r)}`).join("\n") : " none", @@ -80,7 +51,7 @@ export class Faucet { sender: holderIdentity, recipient: refillDistibutor.address, tokenTicker: token, - amount: this.refillAmount(token), + amount: this.tokenManager.refillAmount(token), }); } } @@ -97,28 +68,4 @@ export class Faucet { console.info("Nothing to be done. Anyways, thanks for checking."); } } - - /** true iff the distributor account needs a refill */ - public needsRefill(account: Account, token: TokenTicker): boolean { - const balanceAmount = account.balance.find(b => b.tokenTicker === token); - - const balance = balanceAmount - ? Decimal.fromAtomics(balanceAmount.quantity, balanceAmount.fractionalDigits) - : Decimal.fromAtomics("0", 0); - - const thresholdAmount = this.refillThreshold(token); - const threshold = Decimal.fromAtomics(thresholdAmount.quantity, thresholdAmount.fractionalDigits); - - // TODO: perform < operation on Decimal type directly - // https://github.com/iov-one/iov-core/issues/1375 - return balance.toFloatApproximation() < threshold.toFloatApproximation(); - } - - private getFractionalDigits(ticker: TokenTicker): number { - const match = [...this.config.bankTokens, ...(this.config.erc20Tokens || [])].find( - token => token.ticker === ticker, - ); - if (!match) throw new Error(`No token found for ticker symbol: ${ticker}`); - return match.fractionalDigits; - } } diff --git a/packages/faucet/src/tokenmanager.spec.ts b/packages/faucet/src/tokenmanager.spec.ts new file mode 100644 index 00000000..7801c462 --- /dev/null +++ b/packages/faucet/src/tokenmanager.spec.ts @@ -0,0 +1,157 @@ +import { TokenConfiguration } from "@cosmwasm/bcp"; +import { TokenTicker } from "@iov/bcp"; + +import { TokenManager } from "./tokenmanager"; + +const dummyConfig: TokenConfiguration = { + bankTokens: [ + { + ticker: "TOKENZ", + name: "The tokenz", + fractionalDigits: 6, + denom: "utokenz", + }, + { + ticker: "TRASH", + name: "Trash token", + fractionalDigits: 3, + denom: "mtrash", + }, + ], +}; + +describe("TokenManager", () => { + describe("constructor", () => { + it("can be constructed", () => { + const tm = new TokenManager(dummyConfig); + expect(tm).toBeTruthy(); + }); + }); + + describe("creditAmount", () => { + const tm = new TokenManager(dummyConfig); + + it("returns 10 tokens by default", () => { + expect(tm.creditAmount("TOKENZ" as TokenTicker)).toEqual({ + quantity: "10000000", + fractionalDigits: 6, + tokenTicker: "TOKENZ", + }); + expect(tm.creditAmount("TRASH" as TokenTicker)).toEqual({ + quantity: "10000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + }); + + it("returns value from env variable when set", () => { + process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22"; + expect(tm.creditAmount("TRASH" as TokenTicker)).toEqual({ + quantity: "22000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; + }); + + it("returns default when env variable is set to empty", () => { + process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; + expect(tm.creditAmount("TRASH" as TokenTicker)).toEqual({ + quantity: "10000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; + }); + }); + + describe("refillAmount", () => { + const tm = new TokenManager(dummyConfig); + + beforeEach(() => { + process.env.FAUCET_REFILL_FACTOR = ""; + process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; + }); + + it("returns 20*10 + '000' by default", () => { + expect(tm.refillAmount("TRASH" as TokenTicker)).toEqual({ + quantity: "200000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + }); + + it("returns 20*22 + '000' when credit amount is 22", () => { + process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22"; + expect(tm.refillAmount("TRASH" as TokenTicker)).toEqual({ + quantity: "440000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + }); + + it("returns 30*10 + '000' when refill factor is 30", () => { + process.env.FAUCET_REFILL_FACTOR = "30"; + expect(tm.refillAmount("TRASH" as TokenTicker)).toEqual({ + quantity: "300000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + }); + + it("returns 30*22 + '000' 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" as TokenTicker)).toEqual({ + quantity: "660000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + }); + }); + + describe("refillThreshold", () => { + const tm = new TokenManager(dummyConfig); + + beforeEach(() => { + process.env.FAUCET_REFILL_THRESHOLD = ""; + process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; + }); + + it("returns 8*10 + '000' by default", () => { + expect(tm.refillThreshold("TRASH" as TokenTicker)).toEqual({ + quantity: "80000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + }); + + it("returns 8*22 + '000' when credit amount is 22", () => { + process.env.FAUCET_CREDIT_AMOUNT_TRASH = "22"; + expect(tm.refillThreshold("TRASH" as TokenTicker)).toEqual({ + quantity: "176000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + }); + + it("returns 5*10 + '000' when refill threshold is 5", () => { + process.env.FAUCET_REFILL_THRESHOLD = "5"; + expect(tm.refillThreshold("TRASH" as TokenTicker)).toEqual({ + quantity: "50000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + }); + + it("returns 5*22 + '000' 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" as TokenTicker)).toEqual({ + quantity: "110000", + fractionalDigits: 3, + tokenTicker: "TRASH", + }); + }); + }); +}); diff --git a/packages/faucet/src/tokenmanager.ts b/packages/faucet/src/tokenmanager.ts new file mode 100644 index 00000000..b91685d9 --- /dev/null +++ b/packages/faucet/src/tokenmanager.ts @@ -0,0 +1,67 @@ +import { TokenConfiguration } from "@cosmwasm/bcp"; +import { Account, Amount, TokenTicker } from "@iov/bcp"; +import { Decimal, Uint53 } from "@iov/encoding"; + +/** Send `factor` times credit amount on refilling */ +const defaultRefillFactor = 20; + +/** refill when balance gets below `factor` times credit amount */ +const defaultRefillThresholdFactor = 8; + +export class TokenManager { + private readonly config: TokenConfiguration; + + public constructor(config: TokenConfiguration) { + this.config = config; + } + + /** The amount of tokens that will be sent to the user */ + public creditAmount(token: TokenTicker, factor: Uint53 = new Uint53(1)): Amount { + const amountFromEnv = process.env[`FAUCET_CREDIT_AMOUNT_${token}`]; + const amount = amountFromEnv ? Uint53.fromString(amountFromEnv).toNumber() : 10; + const value = new Uint53(amount * factor.toNumber()); + + const fractionalDigits = this.getFractionalDigits(token); + return { + quantity: value.toString() + "0".repeat(fractionalDigits), + fractionalDigits: fractionalDigits, + tokenTicker: token, + }; + } + + public refillAmount(token: TokenTicker): Amount { + const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_FACTOR || "0", 10) || undefined; + const factor = new Uint53(factorFromEnv || defaultRefillFactor); + return this.creditAmount(token, factor); + } + + public refillThreshold(token: TokenTicker): Amount { + const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_THRESHOLD || "0", 10) || undefined; + const factor = new Uint53(factorFromEnv || defaultRefillThresholdFactor); + return this.creditAmount(token, factor); + } + + /** true iff the distributor account needs a refill */ + public needsRefill(account: Account, token: TokenTicker): boolean { + const balanceAmount = account.balance.find(b => b.tokenTicker === token); + + const balance = balanceAmount + ? Decimal.fromAtomics(balanceAmount.quantity, balanceAmount.fractionalDigits) + : Decimal.fromAtomics("0", 0); + + const thresholdAmount = this.refillThreshold(token); + const threshold = Decimal.fromAtomics(thresholdAmount.quantity, thresholdAmount.fractionalDigits); + + // TODO: perform < operation on Decimal type directly + // https://github.com/iov-one/iov-core/issues/1375 + return balance.toFloatApproximation() < threshold.toFloatApproximation(); + } + + private getFractionalDigits(ticker: TokenTicker): number { + const match = [...this.config.bankTokens, ...(this.config.erc20Tokens || [])].find( + token => token.ticker === ticker, + ); + if (!match) throw new Error(`No token found for ticker symbol: ${ticker}`); + return match.fractionalDigits; + } +}