Pull out TokenManager

This commit is contained in:
Simon Warta 2020-02-10 11:06:16 +01:00
parent a3b6ab5bd8
commit 5f32796bfc
5 changed files with 234 additions and 191 deletions

View File

@ -134,7 +134,7 @@ export async function start(args: ReadonlyArray<string>): Promise<void> {
const job: SendJob = {
sender: sender,
recipient: address,
amount: faucet.creditAmount(ticker),
amount: faucet.tokenManager.creditAmount(ticker),
tokenTicker: ticker,
};
logSendJob(job);

View File

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

View File

@ -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<void> {
@ -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;
}
}

View File

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

View File

@ -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;
}
}