Pull out TokenManager
This commit is contained in:
parent
a3b6ab5bd8
commit
5f32796bfc
@ -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);
|
||||
|
||||
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
157
packages/faucet/src/tokenmanager.spec.ts
Normal file
157
packages/faucet/src/tokenmanager.spec.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
67
packages/faucet/src/tokenmanager.ts
Normal file
67
packages/faucet/src/tokenmanager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user