From 144a051e39f7b18f8cf6ac5b054d8e5e729d064d Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 09:29:27 +0100 Subject: [PATCH 01/16] Rename module faucet to cli --- packages/faucet/bin/cosmwasm-faucet | 2 +- packages/faucet/src/{faucet.ts => cli.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/faucet/src/{faucet.ts => cli.ts} (100%) diff --git a/packages/faucet/bin/cosmwasm-faucet b/packages/faucet/bin/cosmwasm-faucet index c44ac640..97be62e7 100755 --- a/packages/faucet/bin/cosmwasm-faucet +++ b/packages/faucet/bin/cosmwasm-faucet @@ -2,5 +2,5 @@ const path = require("path"); // attempt to call in main file.... -const faucet = require(path.join(__dirname, "..", "build", "faucet.js")); +const faucet = require(path.join(__dirname, "..", "build", "cli.js")); faucet.main(process.argv.slice(2)); diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/cli.ts similarity index 100% rename from packages/faucet/src/faucet.ts rename to packages/faucet/src/cli.ts From 59d336e54aa1d56bd48fad6eefd5a6ed5fbf2c91 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 10:32:16 +0100 Subject: [PATCH 02/16] Pull out faucet class Closes #65 --- packages/faucet/package.json | 1 + packages/faucet/src/actions/start/start.ts | 14 +- packages/faucet/src/cashflow.spec.ts | 124 ---------------- packages/faucet/src/cashflow.ts | 64 --------- packages/faucet/src/codec.ts | 28 +--- packages/faucet/src/constants.ts | 19 +++ packages/faucet/src/faucet.spec.ts | 157 +++++++++++++++++++++ packages/faucet/src/faucet.ts | 124 ++++++++++++++++ packages/faucet/src/multichainhelpers.ts | 51 ------- 9 files changed, 313 insertions(+), 269 deletions(-) delete mode 100644 packages/faucet/src/cashflow.spec.ts delete mode 100644 packages/faucet/src/cashflow.ts create mode 100644 packages/faucet/src/faucet.spec.ts create mode 100644 packages/faucet/src/faucet.ts diff --git a/packages/faucet/package.json b/packages/faucet/package.json index af98acd7..09d31032 100644 --- a/packages/faucet/package.json +++ b/packages/faucet/package.json @@ -40,6 +40,7 @@ "@iov/crypto": "^2.0.0-alpha.7", "@iov/encoding": "^2.0.0-alpha.7", "@iov/keycontrol": "^2.0.0-alpha.7", + "@iov/utils": "^2.0.0-alpha.7", "@koa/cors": "^3.0.0", "axios": "^0.19.0", "fast-deep-equal": "^3.1.1", diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index 45cc23c6..13ade74d 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -3,16 +3,15 @@ import cors = require("@koa/cors"); import Koa from "koa"; import bodyParser from "koa-bodyparser"; -import { creditAmount, setFractionalDigits } from "../../cashflow"; -import { codecDefaultFractionalDigits, codecImplementation, establishConnection } from "../../codec"; +import { codecImplementation, establishConnection } from "../../codec"; import * as constants from "../../constants"; import { logAccountsState, logSendJob } from "../../debugging"; +import { Faucet } from "../../faucet"; import { availableTokensFromHolder, identitiesOfFirstWallet, loadAccounts, loadTokenTickers, - refill, send, } from "../../multichainhelpers"; import { setSecretAndCreateIdentities } from "../../profile"; @@ -48,7 +47,6 @@ export async function start(args: ReadonlyArray): Promise { const connectedChainId = connection.chainId(); console.info(`Connected to network: ${connectedChainId}`); - setFractionalDigits(codecDefaultFractionalDigits()); await setSecretAndCreateIdentities(profile, constants.mnemonic, connectedChainId); const chainTokens = await loadTokenTickers(connection); @@ -67,8 +65,10 @@ export async function start(args: ReadonlyArray): Promise { const distibutorIdentities = identitiesOfFirstWallet(profile).slice(1); - await refill(profile, connection); - setInterval(async () => refill(profile, connection), 60_000); // ever 60 seconds + const faucet = new Faucet(constants.tokenConfig); + + await faucet.refill(profile, connection); + setInterval(async () => faucet.refill(profile, connection), 60_000); // ever 60 seconds console.info("Creating webserver ..."); const api = new Koa(); @@ -127,7 +127,7 @@ export async function start(args: ReadonlyArray): Promise { const job: SendJob = { sender: sender, recipient: address, - amount: creditAmount(ticker), + amount: faucet.creditAmount(ticker), tokenTicker: ticker, }; logSendJob(job); diff --git a/packages/faucet/src/cashflow.spec.ts b/packages/faucet/src/cashflow.spec.ts deleted file mode 100644 index b90f6347..00000000 --- a/packages/faucet/src/cashflow.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { TokenTicker } from "@iov/bcp"; - -import { creditAmount, refillAmount, refillThreshold, setFractionalDigits } from "./cashflow"; - -describe("Cashflow", () => { - beforeAll(() => { - setFractionalDigits(3); - }); - - describe("creditAmount", () => { - it("returns '10' + '000' by default", () => { - expect(creditAmount("TOKENZ" as TokenTicker)).toEqual({ - quantity: "10000", - fractionalDigits: 3, - tokenTicker: "TOKENZ", - }); - expect(creditAmount("TRASH" as TokenTicker)).toEqual({ - quantity: "10000", - fractionalDigits: 3, - tokenTicker: "TRASH", - }); - }); - - it("returns value from env variable + '000' when set", () => { - process.env.FAUCET_CREDIT_AMOUNT_WTF = "22"; - expect(creditAmount("WTF" as TokenTicker)).toEqual({ - quantity: "22000", - fractionalDigits: 3, - tokenTicker: "WTF", - }); - }); - - it("returns default from env variable + '000' when set to empty", () => { - process.env.FAUCET_CREDIT_AMOUNT_WTF = ""; - expect(creditAmount("WTF" as TokenTicker)).toEqual({ - quantity: "10000", - fractionalDigits: 3, - tokenTicker: "WTF", - }); - }); - }); - - describe("refillAmount", () => { - beforeEach(() => { - process.env.FAUCET_REFILL_FACTOR = ""; - }); - it("returns 20*10 + '000' by default", () => { - expect(refillAmount("TOKENZ" as TokenTicker)).toEqual({ - quantity: "200000", - fractionalDigits: 3, - tokenTicker: "TOKENZ", - }); - }); - - it("returns 20*22 + '000' when credit amount is 22", () => { - process.env.FAUCET_CREDIT_AMOUNT_WTF = "22"; - expect(refillAmount("WTF" as TokenTicker)).toEqual({ - quantity: "440000", - fractionalDigits: 3, - tokenTicker: "WTF", - }); - }); - - it("returns 30*10 + '000' when refill factor is 30", () => { - process.env.FAUCET_REFILL_FACTOR = "30"; - expect(refillAmount("TOKENZ" as TokenTicker)).toEqual({ - quantity: "300000", - fractionalDigits: 3, - tokenTicker: "TOKENZ", - }); - }); - - 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_WTF = "22"; - expect(refillAmount("WTF" as TokenTicker)).toEqual({ - quantity: "660000", - fractionalDigits: 3, - tokenTicker: "WTF", - }); - }); - }); - - describe("refillThreshold", () => { - beforeEach(() => { - process.env.FAUCET_REFILL_THRESHOLD = ""; - }); - it("returns 8*10 + '000' by default", () => { - expect(refillThreshold("TOKENZ" as TokenTicker)).toEqual({ - quantity: "80000", - fractionalDigits: 3, - tokenTicker: "TOKENZ", - }); - }); - - it("returns 8*22 + '000' when credit amount is 22", () => { - process.env.FAUCET_CREDIT_AMOUNT_WTF = "22"; - expect(refillThreshold("WTF" as TokenTicker)).toEqual({ - quantity: "176000", - fractionalDigits: 3, - tokenTicker: "WTF", - }); - }); - - it("returns 5*10 + '000' when refill threshold is 5", () => { - process.env.FAUCET_REFILL_THRESHOLD = "5"; - expect(refillThreshold("TOKENZ" as TokenTicker)).toEqual({ - quantity: "50000", - fractionalDigits: 3, - tokenTicker: "TOKENZ", - }); - }); - - 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_WTF = "22"; - expect(refillThreshold("WTF" as TokenTicker)).toEqual({ - quantity: "110000", - fractionalDigits: 3, - tokenTicker: "WTF", - }); - }); - }); -}); diff --git a/packages/faucet/src/cashflow.ts b/packages/faucet/src/cashflow.ts deleted file mode 100644 index e95116fe..00000000 --- a/packages/faucet/src/cashflow.ts +++ /dev/null @@ -1,64 +0,0 @@ -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; - -// Load this from connection? -let globalFractionalDigits: number | undefined; - -export function setFractionalDigits(input: number): void { - globalFractionalDigits = input; -} - -export function getFractionalDigits(): number { - if (globalFractionalDigits === undefined) { - throw new Error("Fractional digits not set"); - } - return globalFractionalDigits; -} - -/** The amount of tokens that will be sent to the user */ -export function 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 = getFractionalDigits(); - return { - quantity: value.toString() + "0".repeat(fractionalDigits), - fractionalDigits: fractionalDigits, - tokenTicker: token, - }; -} - -export function refillAmount(token: TokenTicker): Amount { - const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_FACTOR || "0", 10) || undefined; - const factor = new Uint53(factorFromEnv || defaultRefillFactor); - return creditAmount(token, factor); -} - -export function refillThreshold(token: TokenTicker): Amount { - const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_THRESHOLD || "0", 10) || undefined; - const factor = new Uint53(factorFromEnv || defaultRefillThresholdFactor); - return creditAmount(token, factor); -} - -/** true iff the distributor account needs a refill */ -export function 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 = 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(); -} diff --git a/packages/faucet/src/codec.ts b/packages/faucet/src/codec.ts index f5d27e48..cffbfd67 100644 --- a/packages/faucet/src/codec.ts +++ b/packages/faucet/src/codec.ts @@ -1,32 +1,14 @@ -import { CosmWasmCodec, CosmWasmConnection, TokenConfiguration } from "@cosmwasm/bcp"; +import { CosmWasmCodec, CosmWasmConnection } from "@cosmwasm/bcp"; import { TxCodec } from "@iov/bcp"; +import { tokenConfig } from "./constants"; + const prefix = "cosmos"; -const config: TokenConfiguration = { - bankTokens: [ - { - fractionalDigits: 6, - name: "Fee Token", - ticker: "COSM", - denom: "cosm", - }, - { - fractionalDigits: 6, - name: "Staking Token", - ticker: "STAKE", - denom: "stake", - }, - ], -}; export async function establishConnection(url: string): Promise { - return CosmWasmConnection.establish(url, prefix, config); + return CosmWasmConnection.establish(url, prefix, tokenConfig); } export function codecImplementation(): TxCodec { - return new CosmWasmCodec(prefix, config.bankTokens); -} - -export function codecDefaultFractionalDigits(): number { - return 6; + return new CosmWasmCodec(prefix, tokenConfig.bankTokens); } diff --git a/packages/faucet/src/constants.ts b/packages/faucet/src/constants.ts index 616d1acd..1faaa975 100644 --- a/packages/faucet/src/constants.ts +++ b/packages/faucet/src/constants.ts @@ -1,4 +1,23 @@ +import { TokenConfiguration } from "@cosmwasm/bcp"; + export const binaryName = "cosmwasm-faucet"; export const concurrency: number = Number.parseInt(process.env.FAUCET_CONCURRENCY || "", 10) || 5; export const port: number = Number.parseInt(process.env.FAUCET_PORT || "", 10) || 8000; export const mnemonic: string | undefined = process.env.FAUCET_MNEMONIC; + +export const tokenConfig: TokenConfiguration = { + bankTokens: [ + { + fractionalDigits: 6, + name: "Fee Token", + ticker: "COSM", + denom: "cosm", + }, + { + fractionalDigits: 6, + name: "Staking Token", + ticker: "STAKE", + denom: "stake", + }, + ], +}; diff --git a/packages/faucet/src/faucet.spec.ts b/packages/faucet/src/faucet.spec.ts new file mode 100644 index 00000000..6d45e334 --- /dev/null +++ b/packages/faucet/src/faucet.spec.ts @@ -0,0 +1,157 @@ +import { TokenConfiguration } from "@cosmwasm/bcp"; +import { TokenTicker } from "@iov/bcp"; + +import { Faucet } from "./faucet"; + +const dummyConfig: TokenConfiguration = { + bankTokens: [ + { + ticker: "TOKENZ", + name: "The tokenz", + fractionalDigits: 6, + denom: "utokenz", + }, + { + ticker: "TRASH", + name: "Trash token", + fractionalDigits: 3, + denom: "mtrash", + }, + ], +}; + +describe("Faucet", () => { + describe("constructor", () => { + it("can be constructed", () => { + const faucet = new Faucet(dummyConfig); + 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 new file mode 100644 index 00000000..b0427fe1 --- /dev/null +++ b/packages/faucet/src/faucet.ts @@ -0,0 +1,124 @@ +import { TokenConfiguration } from "@cosmwasm/bcp"; +import { Account, Amount, BlockchainConnection, TokenTicker } from "@iov/bcp"; +import { Decimal, Uint53 } from "@iov/encoding"; +import { UserProfile } from "@iov/keycontrol"; +import { sleep } from "@iov/utils"; + +import { debugAccount, logAccountsState, logSendJob } from "./debugging"; +import { + availableTokensFromHolder, + identitiesOfFirstWallet, + loadAccounts, + loadTokenTickers, + send, +} from "./multichainhelpers"; +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; + + 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); + } + + public async refill(profile: UserProfile, connection: BlockchainConnection): Promise { + console.info(`Connected to network: ${connection.chainId()}`); + console.info(`Tokens on network: ${(await loadTokenTickers(connection)).join(", ")}`); + + const holderIdentity = identitiesOfFirstWallet(profile)[0]; + + const accounts = await loadAccounts(profile, connection); + logAccountsState(accounts); + const holderAccount = accounts[0]; + const distributorAccounts = accounts.slice(1); + + const availableTokens = availableTokensFromHolder(holderAccount); + console.info("Available tokens:", availableTokens); + + const jobs: SendJob[] = []; + + for (const token of availableTokens) { + const refillDistibutors = distributorAccounts.filter(account => this.needsRefill(account, token)); + console.info(`Refilling ${token} of:`); + console.info( + refillDistibutors.length ? refillDistibutors.map(r => ` ${debugAccount(r)}`).join("\n") : " none", + ); + for (const refillDistibutor of refillDistibutors) { + jobs.push({ + sender: holderIdentity, + recipient: refillDistibutor.address, + tokenTicker: token, + amount: this.refillAmount(token), + }); + } + } + if (jobs.length > 0) { + for (const job of jobs) { + logSendJob(job); + await send(profile, connection, job); + await sleep(50); + } + + console.info("Done refilling accounts."); + logAccountsState(await loadAccounts(profile, connection)); + } else { + 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/multichainhelpers.ts b/packages/faucet/src/multichainhelpers.ts index 57e2e554..3628e627 100644 --- a/packages/faucet/src/multichainhelpers.ts +++ b/packages/faucet/src/multichainhelpers.ts @@ -9,15 +9,9 @@ import { } from "@iov/bcp"; import { UserProfile } from "@iov/keycontrol"; -import { needsRefill, refillAmount } from "./cashflow"; import { codecImplementation } from "./codec"; -import { debugAccount, logAccountsState, logSendJob } from "./debugging"; import { SendJob } from "./types"; -async function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} - export function identitiesOfFirstWallet(profile: UserProfile): ReadonlyArray { const wallet = profile.wallets.value[0]; return profile.getIdentities(wallet.id); @@ -88,48 +82,3 @@ export async function send( export function availableTokensFromHolder(holderAccount: Account): ReadonlyArray { return holderAccount.balance.map(coin => coin.tokenTicker); } - -export async function refill(profile: UserProfile, connection: BlockchainConnection): Promise { - console.info(`Connected to network: ${connection.chainId()}`); - console.info(`Tokens on network: ${(await loadTokenTickers(connection)).join(", ")}`); - - const holderIdentity = identitiesOfFirstWallet(profile)[0]; - - const accounts = await loadAccounts(profile, connection); - logAccountsState(accounts); - const holderAccount = accounts[0]; - const distributorAccounts = accounts.slice(1); - - const availableTokens = availableTokensFromHolder(holderAccount); - console.info("Available tokens:", availableTokens); - - const jobs: SendJob[] = []; - - for (const token of availableTokens) { - const refillDistibutors = distributorAccounts.filter(account => needsRefill(account, token)); - console.info(`Refilling ${token} of:`); - console.info( - refillDistibutors.length ? refillDistibutors.map(r => ` ${debugAccount(r)}`).join("\n") : " none", - ); - for (const refillDistibutor of refillDistibutors) { - jobs.push({ - sender: holderIdentity, - recipient: refillDistibutor.address, - tokenTicker: token, - amount: refillAmount(token), - }); - } - } - if (jobs.length > 0) { - for (const job of jobs) { - logSendJob(job); - await send(profile, connection, job); - await sleep(50); - } - - console.info("Done refilling accounts."); - logAccountsState(await loadAccounts(profile, connection)); - } else { - console.info("Nothing to be done. Anyways, thanks for checking."); - } -} From a3b6ab5bd8e362d1b025bc6b0bd9b7b2e5657114 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 10:53:38 +0100 Subject: [PATCH 03/16] Improve codec handling --- packages/faucet/src/actions/start/start.ts | 19 +++++++++++++------ packages/faucet/src/addresses.ts | 17 +++++++++++++++++ packages/faucet/src/codec.spec.ts | 1 - packages/faucet/src/codec.ts | 14 -------------- packages/faucet/src/constants.ts | 1 + packages/faucet/src/debugging.ts | 4 ++-- packages/faucet/src/faucet.ts | 6 +++--- packages/faucet/src/multichainhelpers.ts | 9 ++++----- packages/faucet/src/profile.ts | 4 ++-- 9 files changed, 42 insertions(+), 33 deletions(-) create mode 100644 packages/faucet/src/addresses.ts delete mode 100644 packages/faucet/src/codec.spec.ts delete mode 100644 packages/faucet/src/codec.ts diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index 13ade74d..deb80177 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -1,9 +1,10 @@ import { UserProfile } from "@iov/keycontrol"; import cors = require("@koa/cors"); +import { createCosmWasmConnector } from "@cosmwasm/bcp"; import Koa from "koa"; import bodyParser from "koa-bodyparser"; -import { codecImplementation, establishConnection } from "../../codec"; +import { isValidAddress } from "../../addresses"; import * as constants from "../../constants"; import { logAccountsState, logSendJob } from "../../debugging"; import { Faucet } from "../../faucet"; @@ -41,8 +42,14 @@ export async function start(args: ReadonlyArray): Promise { if (!constants.mnemonic) { throw new Error("The FAUCET_MNEMONIC environment variable is not set"); } + + const connector = createCosmWasmConnector( + blockchainBaseUrl, + constants.addressPrefix, + constants.tokenConfig, + ); console.info(`Connecting to blockchain ${blockchainBaseUrl} ...`); - const connection = await establishConnection(blockchainBaseUrl); + const connection = await connector.establishConnection(); const connectedChainId = connection.chainId(); console.info(`Connected to network: ${connectedChainId}`); @@ -67,8 +74,8 @@ export async function start(args: ReadonlyArray): Promise { const faucet = new Faucet(constants.tokenConfig); - await faucet.refill(profile, connection); - setInterval(async () => faucet.refill(profile, connection), 60_000); // ever 60 seconds + await faucet.refill(profile, connection, connector.codec); + setInterval(async () => faucet.refill(profile, connection, connector.codec), 60_000); // ever 60 seconds console.info("Creating webserver ..."); const api = new Koa(); @@ -112,7 +119,7 @@ export async function start(args: ReadonlyArray): Promise { const requestBody = context.request.body; const { address, ticker } = RequestParser.parseCreditBody(requestBody); - if (!codecImplementation().isValidAddress(address)) { + if (!isValidAddress(address)) { throw new HttpError(400, "Address is not in the expected format for this chain."); } @@ -131,7 +138,7 @@ export async function start(args: ReadonlyArray): Promise { tokenTicker: ticker, }; logSendJob(job); - await send(profile, connection, job); + await send(profile, connection, connector.codec, job); } catch (e) { console.error(e); throw new HttpError(500, "Sending tokens failed"); diff --git a/packages/faucet/src/addresses.ts b/packages/faucet/src/addresses.ts new file mode 100644 index 00000000..2b749e62 --- /dev/null +++ b/packages/faucet/src/addresses.ts @@ -0,0 +1,17 @@ +import { CosmWasmCodec } from "@cosmwasm/bcp"; +import { Address, Identity, TxCodec } from "@iov/bcp"; + +import * as constants from "./constants"; + +const noTokensCodec: Pick = new CosmWasmCodec( + constants.addressPrefix, + [], +); + +export function identityToAddress(identity: Identity): Address { + return noTokensCodec.identityToAddress(identity); +} + +export function isValidAddress(input: string): boolean { + return noTokensCodec.isValidAddress(input); +} diff --git a/packages/faucet/src/codec.spec.ts b/packages/faucet/src/codec.spec.ts deleted file mode 100644 index 624bf357..00000000 --- a/packages/faucet/src/codec.spec.ts +++ /dev/null @@ -1 +0,0 @@ -describe("codec", () => {}); diff --git a/packages/faucet/src/codec.ts b/packages/faucet/src/codec.ts deleted file mode 100644 index cffbfd67..00000000 --- a/packages/faucet/src/codec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CosmWasmCodec, CosmWasmConnection } from "@cosmwasm/bcp"; -import { TxCodec } from "@iov/bcp"; - -import { tokenConfig } from "./constants"; - -const prefix = "cosmos"; - -export async function establishConnection(url: string): Promise { - return CosmWasmConnection.establish(url, prefix, tokenConfig); -} - -export function codecImplementation(): TxCodec { - return new CosmWasmCodec(prefix, tokenConfig.bankTokens); -} diff --git a/packages/faucet/src/constants.ts b/packages/faucet/src/constants.ts index 1faaa975..6eb5c861 100644 --- a/packages/faucet/src/constants.ts +++ b/packages/faucet/src/constants.ts @@ -5,6 +5,7 @@ export const concurrency: number = Number.parseInt(process.env.FAUCET_CONCURRENC export const port: number = Number.parseInt(process.env.FAUCET_PORT || "", 10) || 8000; export const mnemonic: string | undefined = process.env.FAUCET_MNEMONIC; +export const addressPrefix = "cosmos"; export const tokenConfig: TokenConfiguration = { bankTokens: [ { diff --git a/packages/faucet/src/debugging.ts b/packages/faucet/src/debugging.ts index a5d7cf64..97a50f5f 100644 --- a/packages/faucet/src/debugging.ts +++ b/packages/faucet/src/debugging.ts @@ -1,7 +1,7 @@ import { Account, Amount } from "@iov/bcp"; import { Decimal } from "@iov/encoding"; -import { codecImplementation } from "./codec"; +import { identityToAddress } from "./addresses"; import { SendJob } from "./types"; /** A string representation of a coin in a human-readable format that can change at any time */ @@ -31,7 +31,7 @@ export function logAccountsState(accounts: ReadonlyArray): void { } export function logSendJob(job: SendJob): void { - const from = codecImplementation().identityToAddress(job.sender); + const from = identityToAddress(job.sender); const to = job.recipient; const amount = debugAmount(job.amount); console.info(`Sending ${amount} from ${from} to ${to} ...`); diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index b0427fe1..42d6cec8 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -1,5 +1,5 @@ import { TokenConfiguration } from "@cosmwasm/bcp"; -import { Account, Amount, BlockchainConnection, TokenTicker } from "@iov/bcp"; +import { Account, Amount, BlockchainConnection, TokenTicker, TxCodec } from "@iov/bcp"; import { Decimal, Uint53 } from "@iov/encoding"; import { UserProfile } from "@iov/keycontrol"; import { sleep } from "@iov/utils"; @@ -53,7 +53,7 @@ export class Faucet { return this.creditAmount(token, factor); } - public async refill(profile: UserProfile, connection: BlockchainConnection): Promise { + public async refill(profile: UserProfile, connection: BlockchainConnection, codec: TxCodec): Promise { console.info(`Connected to network: ${connection.chainId()}`); console.info(`Tokens on network: ${(await loadTokenTickers(connection)).join(", ")}`); @@ -87,7 +87,7 @@ export class Faucet { if (jobs.length > 0) { for (const job of jobs) { logSendJob(job); - await send(profile, connection, job); + await send(profile, connection, codec, job); await sleep(50); } diff --git a/packages/faucet/src/multichainhelpers.ts b/packages/faucet/src/multichainhelpers.ts index 3628e627..7791c6b4 100644 --- a/packages/faucet/src/multichainhelpers.ts +++ b/packages/faucet/src/multichainhelpers.ts @@ -6,10 +6,11 @@ import { isBlockInfoPending, SendTransaction, TokenTicker, + TxCodec, } from "@iov/bcp"; import { UserProfile } from "@iov/keycontrol"; -import { codecImplementation } from "./codec"; +import { identityToAddress } from "./addresses"; import { SendJob } from "./types"; export function identitiesOfFirstWallet(profile: UserProfile): ReadonlyArray { @@ -21,8 +22,7 @@ export async function loadAccounts( profile: UserProfile, connection: BlockchainConnection, ): Promise> { - const codec = codecImplementation(); - const addresses = identitiesOfFirstWallet(profile).map(identity => codec.identityToAddress(identity)); + const addresses = identitiesOfFirstWallet(profile).map(identity => identityToAddress(identity)); const out: Account[] = []; for (const address of addresses) { @@ -55,10 +55,9 @@ export async function loadTokenTickers( export async function send( profile: UserProfile, connection: BlockchainConnection, + codec: TxCodec, job: SendJob, ): Promise { - const codec = codecImplementation(); - const sendWithFee = await connection.withDefaultFee({ kind: "bcp/send", chainId: connection.chainId(), diff --git a/packages/faucet/src/profile.ts b/packages/faucet/src/profile.ts index b5c4b766..dea697b7 100644 --- a/packages/faucet/src/profile.ts +++ b/packages/faucet/src/profile.ts @@ -1,7 +1,7 @@ import { ChainId } from "@iov/bcp"; import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; -import { codecImplementation } from "./codec"; +import { identityToAddress } from "./addresses"; import * as constants from "./constants"; import { debugPath } from "./hdpaths"; @@ -24,7 +24,7 @@ export async function setSecretAndCreateIdentities( // log const role = i === 0 ? "token holder " : `distributor ${i}`; - const address = codecImplementation().identityToAddress(identity); + const address = identityToAddress(identity); console.info(`Created ${role} (${debugPath(path)}): ${address}`); } } From 5f32796bfc9b6784d2b47c839321119131b0e5de Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 11:06:16 +0100 Subject: [PATCH 04/16] 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; + } +} From d378df6b8c88e9576bcb1f345163cd5553cfd862 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 11:12:51 +0100 Subject: [PATCH 05/16] Move connection and codec into Faucet --- packages/faucet/src/actions/start/start.ts | 6 +++--- packages/faucet/src/faucet.spec.ts | 21 +++++++++++++++++---- packages/faucet/src/faucet.ts | 19 ++++++++++++------- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index ec0a4f2c..57bb01f5 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -72,10 +72,10 @@ export async function start(args: ReadonlyArray): Promise { const distibutorIdentities = identitiesOfFirstWallet(profile).slice(1); - const faucet = new Faucet(constants.tokenConfig); + const faucet = new Faucet(constants.tokenConfig, connection, connector.codec); - await faucet.refill(profile, connection, connector.codec); - setInterval(async () => faucet.refill(profile, connection, connector.codec), 60_000); // ever 60 seconds + await faucet.refill(profile); + setInterval(async () => faucet.refill(profile), 60_000); // ever 60 seconds console.info("Creating webserver ..."); const api = new Koa(); diff --git a/packages/faucet/src/faucet.spec.ts b/packages/faucet/src/faucet.spec.ts index 5da32df9..86bc4e2d 100644 --- a/packages/faucet/src/faucet.spec.ts +++ b/packages/faucet/src/faucet.spec.ts @@ -1,8 +1,16 @@ -import { TokenConfiguration } from "@cosmwasm/bcp"; +import { CosmWasmCodec, CosmWasmConnection, TokenConfiguration } from "@cosmwasm/bcp"; +import { CosmosAddressBech32Prefix } from "@cosmwasm/sdk"; import { Faucet } from "./faucet"; -const dummyConfig: TokenConfiguration = { +function pendingWithoutCosmos(): void { + if (!process.env.COSMOS_ENABLED) { + return pending("Set COSMOS_ENABLED to enable Cosmos node-based tests"); + } +} + +const httpUrl = "http://localhost:1317"; +const defaultConfig: TokenConfiguration = { bankTokens: [ { ticker: "TOKENZ", @@ -18,12 +26,17 @@ const dummyConfig: TokenConfiguration = { }, ], }; +const defaultPrefix = "cosmos" as CosmosAddressBech32Prefix; +const codec = new CosmWasmCodec(defaultPrefix, defaultConfig.bankTokens); describe("Faucet", () => { describe("constructor", () => { - it("can be constructed", () => { - const faucet = new Faucet(dummyConfig); + it("can be constructed", async () => { + pendingWithoutCosmos(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); + const faucet = new Faucet(defaultConfig, connection, codec); expect(faucet).toBeTruthy(); + connection.disconnect(); }); }); }); diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index 7a29e7b7..70e4e90b 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -18,17 +18,22 @@ export class Faucet { /** will be private soon */ public readonly tokenManager: TokenManager; - public constructor(config: TokenConfiguration) { + private readonly connection: BlockchainConnection; + private readonly codec: TxCodec; + + public constructor(config: TokenConfiguration, connection: BlockchainConnection, codec: TxCodec) { this.tokenManager = new TokenManager(config); + this.connection = connection; + this.codec = codec; } - public async refill(profile: UserProfile, connection: BlockchainConnection, codec: TxCodec): Promise { - console.info(`Connected to network: ${connection.chainId()}`); - console.info(`Tokens on network: ${(await loadTokenTickers(connection)).join(", ")}`); + public async refill(profile: UserProfile): Promise { + console.info(`Connected to network: ${this.connection.chainId()}`); + console.info(`Tokens on network: ${(await loadTokenTickers(this.connection)).join(", ")}`); const holderIdentity = identitiesOfFirstWallet(profile)[0]; - const accounts = await loadAccounts(profile, connection); + const accounts = await loadAccounts(profile, this.connection); logAccountsState(accounts); const holderAccount = accounts[0]; const distributorAccounts = accounts.slice(1); @@ -58,12 +63,12 @@ export class Faucet { if (jobs.length > 0) { for (const job of jobs) { logSendJob(job); - await send(profile, connection, codec, job); + await send(profile, this.connection, this.codec, job); await sleep(50); } console.info("Done refilling accounts."); - logAccountsState(await loadAccounts(profile, connection)); + logAccountsState(await loadAccounts(profile, this.connection)); } else { console.info("Nothing to be done. Anyways, thanks for checking."); } From 940c41ba64e8eba1910c1009259abf013ffa92b1 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 11:31:17 +0100 Subject: [PATCH 06/16] Move send to Faucet; and test --- packages/faucet/src/actions/start/start.ts | 3 +- packages/faucet/src/faucet.spec.ts | 82 ++++++++++++++++++++-- packages/faucet/src/faucet.ts | 35 ++++++++- packages/faucet/src/multichainhelpers.ts | 41 +---------- 4 files changed, 109 insertions(+), 52 deletions(-) diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index 57bb01f5..ea38cbf4 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -13,7 +13,6 @@ import { identitiesOfFirstWallet, loadAccounts, loadTokenTickers, - send, } from "../../multichainhelpers"; import { setSecretAndCreateIdentities } from "../../profile"; import { SendJob } from "../../types"; @@ -138,7 +137,7 @@ export async function start(args: ReadonlyArray): Promise { tokenTicker: ticker, }; logSendJob(job); - await send(profile, connection, connector.codec, job); + await faucet.send(profile, job); } catch (e) { console.error(e); throw new HttpError(500, "Sending tokens failed"); diff --git a/packages/faucet/src/faucet.spec.ts b/packages/faucet/src/faucet.spec.ts index 86bc4e2d..d4af68df 100644 --- a/packages/faucet/src/faucet.spec.ts +++ b/packages/faucet/src/faucet.spec.ts @@ -1,5 +1,10 @@ import { CosmWasmCodec, CosmWasmConnection, TokenConfiguration } from "@cosmwasm/bcp"; import { CosmosAddressBech32Prefix } from "@cosmwasm/sdk"; +import { Address, ChainId, Identity, TokenTicker } from "@iov/bcp"; +import { Random } from "@iov/crypto"; +import { Bech32 } from "@iov/encoding"; +import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; +import { assert } from "@iov/utils"; import { Faucet } from "./faucet"; @@ -13,22 +18,55 @@ const httpUrl = "http://localhost:1317"; const defaultConfig: TokenConfiguration = { bankTokens: [ { - ticker: "TOKENZ", - name: "The tokenz", fractionalDigits: 6, - denom: "utokenz", + name: "Fee Token", + ticker: "COSM", + denom: "ucosm", }, { - ticker: "TRASH", - name: "Trash token", - fractionalDigits: 3, - denom: "mtrash", + fractionalDigits: 6, + name: "Staking Token", + ticker: "STAKE", + denom: "ustake", }, ], + erc20Tokens: [ + // { + // contractAddress: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", + // fractionalDigits: 5, + // ticker: "ASH", + // name: "Ash Token", + // }, + // { + // contractAddress: "cosmos1hqrdl6wstt8qzshwc6mrumpjk9338k0lr4dqxd", + // fractionalDigits: 0, + // ticker: "BASH", + // name: "Bash Token", + // }, + ], }; const defaultPrefix = "cosmos" as CosmosAddressBech32Prefix; +const defaultChainId = "cosmos:testing" as ChainId; const codec = new CosmWasmCodec(defaultPrefix, defaultConfig.bankTokens); +function makeRandomAddress(): Address { + return Bech32.encode(defaultPrefix, Random.getBytes(20)) as Address; +} + +const faucetMnemonic = + "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone"; +const faucetPath = HdPaths.cosmos(0); + +async function makeProfile(): Promise<{ readonly profile: UserProfile; readonly holder: Identity }> { + const profile = new UserProfile(); + const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic)); + const holder = await profile.createIdentity(wallet.id, defaultChainId, faucetPath); + return { + profile: profile, + holder: holder, + }; +} + describe("Faucet", () => { describe("constructor", () => { it("can be constructed", async () => { @@ -39,4 +77,34 @@ describe("Faucet", () => { connection.disconnect(); }); }); + + describe("send", () => { + it("can send", async () => { + pendingWithoutCosmos(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); + const { profile, holder } = await makeProfile(); + const faucet = new Faucet(defaultConfig, connection, codec); + const recipient = makeRandomAddress(); + await faucet.send(profile, { + amount: { + quantity: "23456", + fractionalDigits: 6, + tokenTicker: "COSM" as TokenTicker, + }, + tokenTicker: "COSM" as TokenTicker, + sender: holder, + recipient: recipient, + }); + const account = await connection.getAccount({ address: recipient }); + assert(account); + expect(account.balance).toEqual([ + { + quantity: "23456", + fractionalDigits: 6, + tokenTicker: "COSM" as TokenTicker, + }, + ]); + connection.disconnect(); + }); + }); }); diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index 70e4e90b..852cf766 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -1,5 +1,11 @@ import { TokenConfiguration } from "@cosmwasm/bcp"; -import { BlockchainConnection, TxCodec } from "@iov/bcp"; +import { + BlockchainConnection, + isBlockInfoFailed, + isBlockInfoPending, + SendTransaction, + TxCodec, +} from "@iov/bcp"; import { UserProfile } from "@iov/keycontrol"; import { sleep } from "@iov/utils"; @@ -9,7 +15,6 @@ import { identitiesOfFirstWallet, loadAccounts, loadTokenTickers, - send, } from "./multichainhelpers"; import { TokenManager } from "./tokenmanager"; import { SendJob } from "./types"; @@ -27,6 +32,30 @@ export class Faucet { this.codec = codec; } + /** + * Creates and posts a send transaction. Then waits until the transaction is in a block. + */ + public async send(profile: UserProfile, job: SendJob): Promise { + const sendWithFee = await this.connection.withDefaultFee({ + kind: "bcp/send", + chainId: this.connection.chainId(), + sender: this.codec.identityToAddress(job.sender), + senderPubkey: job.sender.pubkey, + recipient: job.recipient, + memo: "Make love, not war", + amount: job.amount, + }); + + const nonce = await this.connection.getNonce({ pubkey: job.sender.pubkey }); + const signed = await profile.signTransaction(job.sender, sendWithFee, this.codec, nonce); + + const post = await this.connection.postTx(this.codec.bytesToPost(signed)); + const blockInfo = await post.blockInfo.waitFor(info => !isBlockInfoPending(info)); + if (isBlockInfoFailed(blockInfo)) { + throw new Error(`Sending tokens failed. Code: ${blockInfo.code}, message: ${blockInfo.message}`); + } + } + public async refill(profile: UserProfile): Promise { console.info(`Connected to network: ${this.connection.chainId()}`); console.info(`Tokens on network: ${(await loadTokenTickers(this.connection)).join(", ")}`); @@ -63,7 +92,7 @@ export class Faucet { if (jobs.length > 0) { for (const job of jobs) { logSendJob(job); - await send(profile, this.connection, this.codec, job); + await this.send(profile, job); await sleep(50); } diff --git a/packages/faucet/src/multichainhelpers.ts b/packages/faucet/src/multichainhelpers.ts index 7791c6b4..2bfcfbb6 100644 --- a/packages/faucet/src/multichainhelpers.ts +++ b/packages/faucet/src/multichainhelpers.ts @@ -1,17 +1,7 @@ -import { - Account, - BlockchainConnection, - Identity, - isBlockInfoFailed, - isBlockInfoPending, - SendTransaction, - TokenTicker, - TxCodec, -} from "@iov/bcp"; +import { Account, BlockchainConnection, Identity, TokenTicker } from "@iov/bcp"; import { UserProfile } from "@iov/keycontrol"; import { identityToAddress } from "./addresses"; -import { SendJob } from "./types"; export function identitiesOfFirstWallet(profile: UserProfile): ReadonlyArray { const wallet = profile.wallets.value[0]; @@ -49,35 +39,6 @@ export async function loadTokenTickers( return (await connection.getAllTokens()).map(token => token.tokenTicker); } -/** - * Creates and posts a send transaction. Then waits until the transaction is in a block. - */ -export async function send( - profile: UserProfile, - connection: BlockchainConnection, - codec: TxCodec, - job: SendJob, -): Promise { - const sendWithFee = await connection.withDefaultFee({ - kind: "bcp/send", - chainId: connection.chainId(), - sender: codec.identityToAddress(job.sender), - senderPubkey: job.sender.pubkey, - recipient: job.recipient, - memo: "We ❤️ developers – iov.one", - amount: job.amount, - }); - - const nonce = await connection.getNonce({ pubkey: job.sender.pubkey }); - const signed = await profile.signTransaction(job.sender, sendWithFee, codec, nonce); - - const post = await connection.postTx(codec.bytesToPost(signed)); - const blockInfo = await post.blockInfo.waitFor(info => !isBlockInfoPending(info)); - if (isBlockInfoFailed(blockInfo)) { - throw new Error(`Sending tokens failed. Code: ${blockInfo.code}, message: ${blockInfo.message}`); - } -} - export function availableTokensFromHolder(holderAccount: Account): ReadonlyArray { return holderAccount.balance.map(coin => coin.tokenTicker); } From 3b1774794d63b917c96a4e464cfb3fa66c74ad92 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 11:32:15 +0100 Subject: [PATCH 07/16] Remove obsolete tokenTicker from SendJob --- packages/faucet/src/actions/start/start.ts | 1 - packages/faucet/src/faucet.spec.ts | 1 - packages/faucet/src/faucet.ts | 1 - packages/faucet/src/types.ts | 3 +-- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index ea38cbf4..68d73210 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -134,7 +134,6 @@ export async function start(args: ReadonlyArray): Promise { sender: sender, recipient: address, amount: faucet.tokenManager.creditAmount(ticker), - tokenTicker: ticker, }; logSendJob(job); await faucet.send(profile, job); diff --git a/packages/faucet/src/faucet.spec.ts b/packages/faucet/src/faucet.spec.ts index d4af68df..2b9029d4 100644 --- a/packages/faucet/src/faucet.spec.ts +++ b/packages/faucet/src/faucet.spec.ts @@ -91,7 +91,6 @@ describe("Faucet", () => { fractionalDigits: 6, tokenTicker: "COSM" as TokenTicker, }, - tokenTicker: "COSM" as TokenTicker, sender: holder, recipient: recipient, }); diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index 852cf766..862320a6 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -84,7 +84,6 @@ export class Faucet { jobs.push({ sender: holderIdentity, recipient: refillDistibutor.address, - tokenTicker: token, amount: this.tokenManager.refillAmount(token), }); } diff --git a/packages/faucet/src/types.ts b/packages/faucet/src/types.ts index 28186ee9..31334171 100644 --- a/packages/faucet/src/types.ts +++ b/packages/faucet/src/types.ts @@ -1,8 +1,7 @@ -import { Address, Amount, Identity, TokenTicker } from "@iov/bcp"; +import { Address, Amount, Identity } from "@iov/bcp"; export interface SendJob { readonly sender: Identity; readonly recipient: Address; - readonly tokenTicker: TokenTicker; readonly amount: Amount; } From 85cd71ddfebe36e0e42b555cc21f7ccf25d9b09f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 11:35:13 +0100 Subject: [PATCH 08/16] Move profile into faucet --- packages/faucet/src/actions/start/start.ts | 8 ++++---- packages/faucet/src/faucet.spec.ts | 7 ++++--- packages/faucet/src/faucet.ts | 23 ++++++++++++++-------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index 68d73210..29b6027a 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -71,10 +71,10 @@ export async function start(args: ReadonlyArray): Promise { const distibutorIdentities = identitiesOfFirstWallet(profile).slice(1); - const faucet = new Faucet(constants.tokenConfig, connection, connector.codec); + const faucet = new Faucet(constants.tokenConfig, connection, connector.codec, profile); - await faucet.refill(profile); - setInterval(async () => faucet.refill(profile), 60_000); // ever 60 seconds + await faucet.refill(); + setInterval(async () => faucet.refill(), 60_000); // ever 60 seconds console.info("Creating webserver ..."); const api = new Koa(); @@ -136,7 +136,7 @@ export async function start(args: ReadonlyArray): Promise { amount: faucet.tokenManager.creditAmount(ticker), }; logSendJob(job); - await faucet.send(profile, job); + await faucet.send(job); } catch (e) { console.error(e); throw new HttpError(500, "Sending tokens failed"); diff --git a/packages/faucet/src/faucet.spec.ts b/packages/faucet/src/faucet.spec.ts index 2b9029d4..fe5610b7 100644 --- a/packages/faucet/src/faucet.spec.ts +++ b/packages/faucet/src/faucet.spec.ts @@ -72,7 +72,8 @@ describe("Faucet", () => { it("can be constructed", async () => { pendingWithoutCosmos(); const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); - const faucet = new Faucet(defaultConfig, connection, codec); + const { profile } = await makeProfile(); + const faucet = new Faucet(defaultConfig, connection, codec, profile); expect(faucet).toBeTruthy(); connection.disconnect(); }); @@ -83,9 +84,9 @@ describe("Faucet", () => { pendingWithoutCosmos(); const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); const { profile, holder } = await makeProfile(); - const faucet = new Faucet(defaultConfig, connection, codec); + const faucet = new Faucet(defaultConfig, connection, codec, profile); const recipient = makeRandomAddress(); - await faucet.send(profile, { + await faucet.send({ amount: { quantity: "23456", fractionalDigits: 6, diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index 862320a6..ac8f1b8d 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -25,17 +25,24 @@ export class Faucet { private readonly connection: BlockchainConnection; private readonly codec: TxCodec; + private readonly profile: UserProfile; - public constructor(config: TokenConfiguration, connection: BlockchainConnection, codec: TxCodec) { + public constructor( + config: TokenConfiguration, + connection: BlockchainConnection, + codec: TxCodec, + profile: UserProfile, + ) { this.tokenManager = new TokenManager(config); this.connection = connection; this.codec = codec; + this.profile = profile; } /** * Creates and posts a send transaction. Then waits until the transaction is in a block. */ - public async send(profile: UserProfile, job: SendJob): Promise { + public async send(job: SendJob): Promise { const sendWithFee = await this.connection.withDefaultFee({ kind: "bcp/send", chainId: this.connection.chainId(), @@ -47,7 +54,7 @@ export class Faucet { }); const nonce = await this.connection.getNonce({ pubkey: job.sender.pubkey }); - const signed = await profile.signTransaction(job.sender, sendWithFee, this.codec, nonce); + const signed = await this.profile.signTransaction(job.sender, sendWithFee, this.codec, nonce); const post = await this.connection.postTx(this.codec.bytesToPost(signed)); const blockInfo = await post.blockInfo.waitFor(info => !isBlockInfoPending(info)); @@ -56,13 +63,13 @@ export class Faucet { } } - public async refill(profile: UserProfile): Promise { + public async refill(): Promise { console.info(`Connected to network: ${this.connection.chainId()}`); console.info(`Tokens on network: ${(await loadTokenTickers(this.connection)).join(", ")}`); - const holderIdentity = identitiesOfFirstWallet(profile)[0]; + const holderIdentity = identitiesOfFirstWallet(this.profile)[0]; - const accounts = await loadAccounts(profile, this.connection); + const accounts = await loadAccounts(this.profile, this.connection); logAccountsState(accounts); const holderAccount = accounts[0]; const distributorAccounts = accounts.slice(1); @@ -91,12 +98,12 @@ export class Faucet { if (jobs.length > 0) { for (const job of jobs) { logSendJob(job); - await this.send(profile, job); + await this.send(job); await sleep(50); } console.info("Done refilling accounts."); - logAccountsState(await loadAccounts(profile, this.connection)); + logAccountsState(await loadAccounts(this.profile, this.connection)); } else { console.info("Nothing to be done. Anyways, thanks for checking."); } From accef527feb0edd5dabf6501d8c4ead43db89b83 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 11:39:02 +0100 Subject: [PATCH 09/16] Move and test loadTokenTickers --- packages/faucet/src/actions/start/start.ts | 13 ++++--------- packages/faucet/src/faucet.spec.ts | 12 ++++++++++++ packages/faucet/src/faucet.ts | 14 +++++++------- packages/faucet/src/multichainhelpers.ts | 6 ------ 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index 29b6027a..90af7ca1 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -8,12 +8,7 @@ import { isValidAddress } from "../../addresses"; import * as constants from "../../constants"; import { logAccountsState, logSendJob } from "../../debugging"; import { Faucet } from "../../faucet"; -import { - availableTokensFromHolder, - identitiesOfFirstWallet, - loadAccounts, - loadTokenTickers, -} from "../../multichainhelpers"; +import { availableTokensFromHolder, identitiesOfFirstWallet, loadAccounts } from "../../multichainhelpers"; import { setSecretAndCreateIdentities } from "../../profile"; import { SendJob } from "../../types"; import { HttpError } from "./httperror"; @@ -55,7 +50,9 @@ export async function start(args: ReadonlyArray): Promise { await setSecretAndCreateIdentities(profile, constants.mnemonic, connectedChainId); - const chainTokens = await loadTokenTickers(connection); + const faucet = new Faucet(constants.tokenConfig, connection, connector.codec, profile); + + const chainTokens = await faucet.loadTokenTickers(); console.info("Chain tokens:", chainTokens); const accounts = await loadAccounts(profile, connection); @@ -71,8 +68,6 @@ export async function start(args: ReadonlyArray): Promise { const distibutorIdentities = identitiesOfFirstWallet(profile).slice(1); - const faucet = new Faucet(constants.tokenConfig, connection, connector.codec, profile); - await faucet.refill(); setInterval(async () => faucet.refill(), 60_000); // ever 60 seconds diff --git a/packages/faucet/src/faucet.spec.ts b/packages/faucet/src/faucet.spec.ts index fe5610b7..6526a504 100644 --- a/packages/faucet/src/faucet.spec.ts +++ b/packages/faucet/src/faucet.spec.ts @@ -107,4 +107,16 @@ describe("Faucet", () => { connection.disconnect(); }); }); + + describe("loadTokenTickers", () => { + it("works", async () => { + pendingWithoutCosmos(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); + const { profile } = await makeProfile(); + const faucet = new Faucet(defaultConfig, connection, codec, profile); + const tickers = await faucet.loadTokenTickers(); + expect(tickers).toEqual(["COSM", "STAKE"]); + connection.disconnect(); + }); + }); }); diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index ac8f1b8d..c8e49a8b 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -4,18 +4,14 @@ import { isBlockInfoFailed, isBlockInfoPending, SendTransaction, + TokenTicker, TxCodec, } from "@iov/bcp"; import { UserProfile } from "@iov/keycontrol"; import { sleep } from "@iov/utils"; import { debugAccount, logAccountsState, logSendJob } from "./debugging"; -import { - availableTokensFromHolder, - identitiesOfFirstWallet, - loadAccounts, - loadTokenTickers, -} from "./multichainhelpers"; +import { availableTokensFromHolder, identitiesOfFirstWallet, loadAccounts } from "./multichainhelpers"; import { TokenManager } from "./tokenmanager"; import { SendJob } from "./types"; @@ -63,9 +59,13 @@ export class Faucet { } } + public async loadTokenTickers(): Promise> { + return (await this.connection.getAllTokens()).map(token => token.tokenTicker); + } + public async refill(): Promise { console.info(`Connected to network: ${this.connection.chainId()}`); - console.info(`Tokens on network: ${(await loadTokenTickers(this.connection)).join(", ")}`); + console.info(`Tokens on network: ${(await this.loadTokenTickers()).join(", ")}`); const holderIdentity = identitiesOfFirstWallet(this.profile)[0]; diff --git a/packages/faucet/src/multichainhelpers.ts b/packages/faucet/src/multichainhelpers.ts index 2bfcfbb6..dc9e1164 100644 --- a/packages/faucet/src/multichainhelpers.ts +++ b/packages/faucet/src/multichainhelpers.ts @@ -33,12 +33,6 @@ export async function loadAccounts( return out; } -export async function loadTokenTickers( - connection: BlockchainConnection, -): Promise> { - return (await connection.getAllTokens()).map(token => token.tokenTicker); -} - export function availableTokensFromHolder(holderAccount: Account): ReadonlyArray { return holderAccount.balance.map(coin => coin.tokenTicker); } From 46ddff93ec19284d6e5614fce7662a906faa76f8 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 11:48:21 +0100 Subject: [PATCH 10/16] Move and test loadAccounts --- packages/faucet/src/actions/start/start.ts | 8 +++--- packages/faucet/src/faucet.spec.ts | 16 ++++++++++++ packages/faucet/src/faucet.ts | 30 +++++++++++++++++++--- packages/faucet/src/multichainhelpers.ts | 29 +-------------------- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index 90af7ca1..660e536b 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -8,7 +8,7 @@ import { isValidAddress } from "../../addresses"; import * as constants from "../../constants"; import { logAccountsState, logSendJob } from "../../debugging"; import { Faucet } from "../../faucet"; -import { availableTokensFromHolder, identitiesOfFirstWallet, loadAccounts } from "../../multichainhelpers"; +import { availableTokensFromHolder, identitiesOfFirstWallet } from "../../multichainhelpers"; import { setSecretAndCreateIdentities } from "../../profile"; import { SendJob } from "../../types"; import { HttpError } from "./httperror"; @@ -55,13 +55,13 @@ export async function start(args: ReadonlyArray): Promise { const chainTokens = await faucet.loadTokenTickers(); console.info("Chain tokens:", chainTokens); - const accounts = await loadAccounts(profile, connection); + const accounts = await faucet.loadAccounts(); logAccountsState(accounts); let availableTokens = availableTokensFromHolder(accounts[0]); console.info("Available tokens:", availableTokens); setInterval(async () => { - const updatedAccounts = await loadAccounts(profile, connection); + const updatedAccounts = await faucet.loadAccounts(); availableTokens = availableTokensFromHolder(updatedAccounts[0]); console.info("Available tokens:", availableTokens); }, 60_000); @@ -88,7 +88,7 @@ export async function start(args: ReadonlyArray): Promise { "See https://github.com/iov-one/iov-faucet for all further information.\n"; break; case "/status": { - const updatedAccounts = await loadAccounts(profile, connection); + const updatedAccounts = await faucet.loadAccounts(); context.response.body = { status: "ok", nodeUrl: blockchainBaseUrl, diff --git a/packages/faucet/src/faucet.spec.ts b/packages/faucet/src/faucet.spec.ts index 6526a504..72fc670f 100644 --- a/packages/faucet/src/faucet.spec.ts +++ b/packages/faucet/src/faucet.spec.ts @@ -119,4 +119,20 @@ describe("Faucet", () => { connection.disconnect(); }); }); + + describe("loadAccounts", () => { + it("works", async () => { + pendingWithoutCosmos(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); + const { profile, holder } = await makeProfile(); + const faucet = new Faucet(defaultConfig, connection, codec, profile); + const accounts = await faucet.loadAccounts(); + const expectedHolderAccount = await connection.getAccount({ pubkey: holder.pubkey }); + assert(expectedHolderAccount); + expect(accounts).toEqual([ + { address: expectedHolderAccount.address, balance: expectedHolderAccount.balance }, + ]); + connection.disconnect(); + }); + }); }); diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index c8e49a8b..eecd6c5e 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -1,5 +1,6 @@ import { TokenConfiguration } from "@cosmwasm/bcp"; import { + Account, BlockchainConnection, isBlockInfoFailed, isBlockInfoPending, @@ -10,8 +11,9 @@ import { import { UserProfile } from "@iov/keycontrol"; import { sleep } from "@iov/utils"; +import { identityToAddress } from "./addresses"; import { debugAccount, logAccountsState, logSendJob } from "./debugging"; -import { availableTokensFromHolder, identitiesOfFirstWallet, loadAccounts } from "./multichainhelpers"; +import { availableTokensFromHolder, identitiesOfFirstWallet } from "./multichainhelpers"; import { TokenManager } from "./tokenmanager"; import { SendJob } from "./types"; @@ -63,13 +65,35 @@ export class Faucet { return (await this.connection.getAllTokens()).map(token => token.tokenTicker); } + public async loadAccounts(): Promise>> { + const addresses = identitiesOfFirstWallet(this.profile).map(identity => identityToAddress(identity)); + + const out: Account[] = []; + for (const address of addresses) { + const response = await this.connection.getAccount({ address: address }); + if (response) { + out.push({ + address: response.address, + balance: response.balance, + }); + } else { + out.push({ + address: address, + balance: [], + }); + } + } + + return out; + } + public async refill(): Promise { console.info(`Connected to network: ${this.connection.chainId()}`); console.info(`Tokens on network: ${(await this.loadTokenTickers()).join(", ")}`); const holderIdentity = identitiesOfFirstWallet(this.profile)[0]; - const accounts = await loadAccounts(this.profile, this.connection); + const accounts = await this.loadAccounts(); logAccountsState(accounts); const holderAccount = accounts[0]; const distributorAccounts = accounts.slice(1); @@ -103,7 +127,7 @@ export class Faucet { } console.info("Done refilling accounts."); - logAccountsState(await loadAccounts(this.profile, this.connection)); + logAccountsState(await this.loadAccounts()); } else { console.info("Nothing to be done. Anyways, thanks for checking."); } diff --git a/packages/faucet/src/multichainhelpers.ts b/packages/faucet/src/multichainhelpers.ts index dc9e1164..26be6198 100644 --- a/packages/faucet/src/multichainhelpers.ts +++ b/packages/faucet/src/multichainhelpers.ts @@ -1,38 +1,11 @@ -import { Account, BlockchainConnection, Identity, TokenTicker } from "@iov/bcp"; +import { Account, Identity, TokenTicker } from "@iov/bcp"; import { UserProfile } from "@iov/keycontrol"; -import { identityToAddress } from "./addresses"; - export function identitiesOfFirstWallet(profile: UserProfile): ReadonlyArray { const wallet = profile.wallets.value[0]; return profile.getIdentities(wallet.id); } -export async function loadAccounts( - profile: UserProfile, - connection: BlockchainConnection, -): Promise> { - const addresses = identitiesOfFirstWallet(profile).map(identity => identityToAddress(identity)); - - const out: Account[] = []; - for (const address of addresses) { - const response = await connection.getAccount({ address: address }); - if (response) { - out.push({ - address: response.address, - balance: response.balance, - }); - } else { - out.push({ - address: address, - balance: [], - }); - } - } - - return out; -} - export function availableTokensFromHolder(holderAccount: Account): ReadonlyArray { return holderAccount.balance.map(coin => coin.tokenTicker); } From 6e53302d49d857e636baeacb79d37ef5cbcf8581 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 12:02:27 +0100 Subject: [PATCH 11/16] Sort some code --- packages/faucet/src/actions/generate.ts | 5 ++-- packages/faucet/src/actions/start/start.ts | 31 +++++++--------------- packages/faucet/src/faucet.ts | 11 +++++--- packages/faucet/src/profile.ts | 8 +++--- 4 files changed, 23 insertions(+), 32 deletions(-) diff --git a/packages/faucet/src/actions/generate.ts b/packages/faucet/src/actions/generate.ts index e99c0262..48a16450 100644 --- a/packages/faucet/src/actions/generate.ts +++ b/packages/faucet/src/actions/generate.ts @@ -1,6 +1,5 @@ import { ChainId } from "@iov/bcp"; import { Bip39, Random } from "@iov/crypto"; -import { UserProfile } from "@iov/keycontrol"; import * as constants from "../constants"; import { setSecretAndCreateIdentities } from "../profile"; @@ -17,6 +16,6 @@ export async function generate(args: ReadonlyArray): Promise { const mnemonic = Bip39.encode(Random.getBytes(16)).toString(); console.info(`FAUCET_MNEMONIC="${mnemonic}"`); - const profile = new UserProfile(); - await setSecretAndCreateIdentities(profile, mnemonic, chainId); + // Log the addresses + await setSecretAndCreateIdentities(mnemonic, chainId); } diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index 660e536b..a4afe2de 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -1,4 +1,3 @@ -import { UserProfile } from "@iov/keycontrol"; import cors = require("@koa/cors"); import { createCosmWasmConnector } from "@cosmwasm/bcp"; import Koa from "koa"; @@ -8,7 +7,7 @@ import { isValidAddress } from "../../addresses"; import * as constants from "../../constants"; import { logAccountsState, logSendJob } from "../../debugging"; import { Faucet } from "../../faucet"; -import { availableTokensFromHolder, identitiesOfFirstWallet } from "../../multichainhelpers"; +import { availableTokensFromHolder } from "../../multichainhelpers"; import { setSecretAndCreateIdentities } from "../../profile"; import { SendJob } from "../../types"; import { HttpError } from "./httperror"; @@ -28,15 +27,8 @@ export async function start(args: ReadonlyArray): Promise { ); } + // Connection const blockchainBaseUrl = args[0]; - - const port = constants.port; - - const profile = new UserProfile(); - if (!constants.mnemonic) { - throw new Error("The FAUCET_MNEMONIC environment variable is not set"); - } - const connector = createCosmWasmConnector( blockchainBaseUrl, constants.addressPrefix, @@ -44,20 +36,18 @@ export async function start(args: ReadonlyArray): Promise { ); console.info(`Connecting to blockchain ${blockchainBaseUrl} ...`); const connection = await connector.establishConnection(); + console.info(`Connected to network: ${connection.chainId()}`); - const connectedChainId = connection.chainId(); - console.info(`Connected to network: ${connectedChainId}`); - - await setSecretAndCreateIdentities(profile, constants.mnemonic, connectedChainId); + // Profile + if (!constants.mnemonic) throw new Error("The FAUCET_MNEMONIC environment variable is not set"); + const profile = await setSecretAndCreateIdentities(constants.mnemonic, connection.chainId()); + // Faucet const faucet = new Faucet(constants.tokenConfig, connection, connector.codec, profile); - const chainTokens = await faucet.loadTokenTickers(); console.info("Chain tokens:", chainTokens); - const accounts = await faucet.loadAccounts(); logAccountsState(accounts); - let availableTokens = availableTokensFromHolder(accounts[0]); console.info("Available tokens:", availableTokens); setInterval(async () => { @@ -66,8 +56,6 @@ export async function start(args: ReadonlyArray): Promise { console.info("Available tokens:", availableTokens); }, 60_000); - const distibutorIdentities = identitiesOfFirstWallet(profile).slice(1); - await faucet.refill(); setInterval(async () => faucet.refill(), 60_000); // ever 60 seconds @@ -92,7 +80,7 @@ export async function start(args: ReadonlyArray): Promise { context.response.body = { status: "ok", nodeUrl: blockchainBaseUrl, - chainId: connectedChainId, + chainId: connection.chainId(), chainTokens: chainTokens, availableTokens: availableTokens, holder: updatedAccounts[0], @@ -122,7 +110,7 @@ export async function start(args: ReadonlyArray): Promise { throw new HttpError(422, `Token is not available. Available tokens are: ${tokens}`); } - const sender = distibutorIdentities[getCount() % distibutorIdentities.length]; + const sender = faucet.distributors[getCount() % faucet.distributors.length]; try { const job: SendJob = { @@ -144,6 +132,7 @@ export async function start(args: ReadonlyArray): Promise { // koa sends 404 by default } }); + const port = constants.port; console.info(`Starting webserver on port ${port} ...`); api.listen(port); } diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index eecd6c5e..251fba12 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -2,6 +2,7 @@ import { TokenConfiguration } from "@cosmwasm/bcp"; import { Account, BlockchainConnection, + Identity, isBlockInfoFailed, isBlockInfoPending, SendTransaction, @@ -20,6 +21,12 @@ import { SendJob } from "./types"; export class Faucet { /** will be private soon */ public readonly tokenManager: TokenManager; + public get holder(): Identity { + return identitiesOfFirstWallet(this.profile)[0]; + } + public get distributors(): readonly Identity[] { + return identitiesOfFirstWallet(this.profile).slice(1); + } private readonly connection: BlockchainConnection; private readonly codec: TxCodec; @@ -91,8 +98,6 @@ export class Faucet { console.info(`Connected to network: ${this.connection.chainId()}`); console.info(`Tokens on network: ${(await this.loadTokenTickers()).join(", ")}`); - const holderIdentity = identitiesOfFirstWallet(this.profile)[0]; - const accounts = await this.loadAccounts(); logAccountsState(accounts); const holderAccount = accounts[0]; @@ -113,7 +118,7 @@ export class Faucet { ); for (const refillDistibutor of refillDistibutors) { jobs.push({ - sender: holderIdentity, + sender: this.holder, recipient: refillDistibutor.address, amount: this.tokenManager.refillAmount(token), }); diff --git a/packages/faucet/src/profile.ts b/packages/faucet/src/profile.ts index dea697b7..11577eca 100644 --- a/packages/faucet/src/profile.ts +++ b/packages/faucet/src/profile.ts @@ -5,11 +5,8 @@ import { identityToAddress } from "./addresses"; import * as constants from "./constants"; import { debugPath } from "./hdpaths"; -export async function setSecretAndCreateIdentities( - profile: UserProfile, - mnemonic: string, - chainId: ChainId, -): Promise { +export async function setSecretAndCreateIdentities(mnemonic: string, chainId: ChainId): Promise { + const profile = new UserProfile(); if (profile.wallets.value.length !== 0) { throw new Error("Profile already contains wallets"); } @@ -27,4 +24,5 @@ export async function setSecretAndCreateIdentities( const address = identityToAddress(identity); console.info(`Created ${role} (${debugPath(path)}): ${address}`); } + return profile; } From bf023748f1c4f4d4aae1f843b86177c4f0ee2ac3 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 12:40:53 +0100 Subject: [PATCH 12/16] Use radix rule from eslint --- .eslintrc.js | 1 + tslint.json | 1 + 2 files changed, 2 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 52a62083..52166b4d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,6 +25,7 @@ module.exports = { "no-param-reassign": "warn", "no-shadow": "warn", "prefer-const": "warn", + "radix": ["warn", "always"], "spaced-comment": ["warn", "always", { line: { markers: ["/ Date: Mon, 10 Feb 2020 12:43:06 +0100 Subject: [PATCH 13/16] Move credit function infot Faucet --- packages/faucet/src/actions/start/start.ts | 22 +----- packages/faucet/src/faucet.spec.ts | 80 ++++++++++++++++++++-- packages/faucet/src/faucet.ts | 61 ++++++++++++----- packages/faucet/src/profile.ts | 33 +++++---- 4 files changed, 141 insertions(+), 55 deletions(-) diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index a4afe2de..77548ca8 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -5,21 +5,13 @@ import bodyParser from "koa-bodyparser"; import { isValidAddress } from "../../addresses"; import * as constants from "../../constants"; -import { logAccountsState, logSendJob } from "../../debugging"; +import { logAccountsState } from "../../debugging"; import { Faucet } from "../../faucet"; import { availableTokensFromHolder } from "../../multichainhelpers"; import { setSecretAndCreateIdentities } from "../../profile"; -import { SendJob } from "../../types"; import { HttpError } from "./httperror"; import { RequestParser } from "./requestparser"; -let count = 0; - -/** returns an integer >= 0 that increments and is unique in module scope */ -function getCount(): number { - return count++; -} - export async function start(args: ReadonlyArray): Promise { if (args.length < 1) { throw Error( @@ -43,7 +35,7 @@ export async function start(args: ReadonlyArray): Promise { const profile = await setSecretAndCreateIdentities(constants.mnemonic, connection.chainId()); // Faucet - const faucet = new Faucet(constants.tokenConfig, connection, connector.codec, profile); + const faucet = new Faucet(constants.tokenConfig, connection, connector.codec, profile, true); const chainTokens = await faucet.loadTokenTickers(); console.info("Chain tokens:", chainTokens); const accounts = await faucet.loadAccounts(); @@ -110,16 +102,8 @@ export async function start(args: ReadonlyArray): Promise { throw new HttpError(422, `Token is not available. Available tokens are: ${tokens}`); } - const sender = faucet.distributors[getCount() % faucet.distributors.length]; - try { - const job: SendJob = { - sender: sender, - recipient: address, - amount: faucet.tokenManager.creditAmount(ticker), - }; - logSendJob(job); - await faucet.send(job); + await faucet.credit(address, ticker); } catch (e) { console.error(e); throw new HttpError(500, "Sending tokens failed"); diff --git a/packages/faucet/src/faucet.spec.ts b/packages/faucet/src/faucet.spec.ts index 72fc670f..932ba7a9 100644 --- a/packages/faucet/src/faucet.spec.ts +++ b/packages/faucet/src/faucet.spec.ts @@ -3,10 +3,11 @@ import { CosmosAddressBech32Prefix } from "@cosmwasm/sdk"; import { Address, ChainId, Identity, TokenTicker } from "@iov/bcp"; import { Random } from "@iov/crypto"; import { Bech32 } from "@iov/encoding"; -import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; +import { UserProfile } from "@iov/keycontrol"; import { assert } from "@iov/utils"; import { Faucet } from "./faucet"; +import { createUserProfile } from "./profile"; function pendingWithoutCosmos(): void { if (!process.env.COSMOS_ENABLED) { @@ -55,15 +56,15 @@ function makeRandomAddress(): Address { const faucetMnemonic = "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone"; -const faucetPath = HdPaths.cosmos(0); -async function makeProfile(): Promise<{ readonly profile: UserProfile; readonly holder: Identity }> { - const profile = new UserProfile(); - const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic)); - const holder = await profile.createIdentity(wallet.id, defaultChainId, faucetPath); +async function makeProfile( + distributors = 0, +): Promise<{ readonly profile: UserProfile; readonly holder: Identity; readonly distributors: Identity[] }> { + const [profile, identities] = await createUserProfile(faucetMnemonic, defaultChainId, distributors); return { profile: profile, - holder: holder, + holder: identities[0], + distributors: identities.slice(1), }; } @@ -108,6 +109,71 @@ describe("Faucet", () => { }); }); + describe("refill", () => { + it("works", async () => { + pendingWithoutCosmos(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); + const { profile, distributors } = await makeProfile(1); + const faucet = new Faucet(defaultConfig, connection, codec, profile); + await faucet.refill(); + const distributorBalance = (await connection.getAccount({ pubkey: distributors[0].pubkey }))?.balance; + assert(distributorBalance); + expect(distributorBalance).toEqual([ + jasmine.objectContaining({ + tokenTicker: "COSM", + fractionalDigits: 6, + }), + jasmine.objectContaining({ + tokenTicker: "STAKE", + fractionalDigits: 6, + }), + ]); + expect(Number.parseInt(distributorBalance[0].quantity, 10)).toBeGreaterThanOrEqual(80_000000); + expect(Number.parseInt(distributorBalance[1].quantity, 10)).toBeGreaterThanOrEqual(80_000000); + connection.disconnect(); + }); + }); + + describe("credit", () => { + it("works for fee token", async () => { + pendingWithoutCosmos(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); + const { profile } = await makeProfile(1); + const faucet = new Faucet(defaultConfig, connection, codec, profile); + const recipient = makeRandomAddress(); + await faucet.credit(recipient, "COSM" as TokenTicker); + const account = await connection.getAccount({ address: recipient }); + assert(account); + expect(account.balance).toEqual([ + { + quantity: "10000000", + fractionalDigits: 6, + tokenTicker: "COSM" as TokenTicker, + }, + ]); + connection.disconnect(); + }); + + it("works for stake token", async () => { + pendingWithoutCosmos(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); + const { profile } = await makeProfile(1); + const faucet = new Faucet(defaultConfig, connection, codec, profile); + const recipient = makeRandomAddress(); + await faucet.credit(recipient, "STAKE" as TokenTicker); + const account = await connection.getAccount({ address: recipient }); + assert(account); + expect(account.balance).toEqual([ + { + quantity: "10000000", + fractionalDigits: 6, + tokenTicker: "STAKE" as TokenTicker, + }, + ]); + connection.disconnect(); + }); + }); + describe("loadTokenTickers", () => { it("works", async () => { pendingWithoutCosmos(); diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index 251fba12..66d406b2 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -1,6 +1,7 @@ import { TokenConfiguration } from "@cosmwasm/bcp"; import { Account, + Address, BlockchainConnection, Identity, isBlockInfoFailed, @@ -19,8 +20,6 @@ import { TokenManager } from "./tokenmanager"; import { SendJob } from "./types"; export class Faucet { - /** will be private soon */ - public readonly tokenManager: TokenManager; public get holder(): Identity { return identitiesOfFirstWallet(this.profile)[0]; } @@ -28,20 +27,25 @@ export class Faucet { return identitiesOfFirstWallet(this.profile).slice(1); } + private readonly tokenManager: TokenManager; private readonly connection: BlockchainConnection; private readonly codec: TxCodec; private readonly profile: UserProfile; + private readonly logging: boolean; + private creditCount = 0; public constructor( config: TokenConfiguration, connection: BlockchainConnection, codec: TxCodec, profile: UserProfile, + logging = false, ) { this.tokenManager = new TokenManager(config); this.connection = connection; this.codec = codec; this.profile = profile; + this.logging = logging; } /** @@ -68,6 +72,19 @@ export class Faucet { } } + /** Use one of the distributor accounts to send tokend to user */ + public async credit(recipient: Address, ticker: TokenTicker): Promise { + if (this.distributors.length === 0) throw new Error("No distributor account available"); + const sender = this.distributors[this.getCreditCount() % this.distributors.length]; + const job: SendJob = { + sender: sender, + recipient: recipient, + amount: this.tokenManager.creditAmount(ticker), + }; + if (this.logging) logSendJob(job); + await this.send(job); + } + public async loadTokenTickers(): Promise> { return (await this.connection.getAllTokens()).map(token => token.tokenTicker); } @@ -95,27 +112,30 @@ export class Faucet { } public async refill(): Promise { - console.info(`Connected to network: ${this.connection.chainId()}`); - console.info(`Tokens on network: ${(await this.loadTokenTickers()).join(", ")}`); + if (this.logging) { + console.info(`Connected to network: ${this.connection.chainId()}`); + console.info(`Tokens on network: ${(await this.loadTokenTickers()).join(", ")}`); + } const accounts = await this.loadAccounts(); - logAccountsState(accounts); - const holderAccount = accounts[0]; - const distributorAccounts = accounts.slice(1); + if (this.logging) logAccountsState(accounts); + const [holderAccount, ...distributorAccounts] = accounts; const availableTokens = availableTokensFromHolder(holderAccount); - console.info("Available tokens:", availableTokens); + if (this.logging) console.info("Available tokens:", availableTokens); const jobs: SendJob[] = []; - for (const token of availableTokens) { 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", - ); + + if (this.logging) { + console.info(`Refilling ${token} of:`); + console.info( + refillDistibutors.length ? refillDistibutors.map(r => ` ${debugAccount(r)}`).join("\n") : " none", + ); + } for (const refillDistibutor of refillDistibutors) { jobs.push({ sender: this.holder, @@ -131,10 +151,19 @@ export class Faucet { await sleep(50); } - console.info("Done refilling accounts."); - logAccountsState(await this.loadAccounts()); + if (this.logging) { + console.info("Done refilling accounts."); + logAccountsState(await this.loadAccounts()); + } } else { - console.info("Nothing to be done. Anyways, thanks for checking."); + if (this.logging) { + console.info("Nothing to be done. Anyways, thanks for checking."); + } } } + + /** returns an integer >= 0 that increments and is unique for this instance */ + private getCreditCount(): number { + return this.creditCount++; + } } diff --git a/packages/faucet/src/profile.ts b/packages/faucet/src/profile.ts index 11577eca..c7e28ddb 100644 --- a/packages/faucet/src/profile.ts +++ b/packages/faucet/src/profile.ts @@ -1,28 +1,35 @@ -import { ChainId } from "@iov/bcp"; +import { ChainId, Identity } from "@iov/bcp"; import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; import { identityToAddress } from "./addresses"; import * as constants from "./constants"; import { debugPath } from "./hdpaths"; -export async function setSecretAndCreateIdentities(mnemonic: string, chainId: ChainId): Promise { +export async function createUserProfile( + mnemonic: string, + chainId: ChainId, + numberOfDistributors: number, + logging = false, +): Promise<[UserProfile, readonly Identity[]]> { const profile = new UserProfile(); - if (profile.wallets.value.length !== 0) { - throw new Error("Profile already contains wallets"); - } const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(mnemonic)); + const identities = new Array(); // first account is the token holder - const numberOfIdentities = 1 + constants.concurrency; + const numberOfIdentities = 1 + numberOfDistributors; for (let i = 0; i < numberOfIdentities; i++) { - // create const path = HdPaths.cosmos(i); const identity = await profile.createIdentity(wallet.id, chainId, path); - - // log - const role = i === 0 ? "token holder " : `distributor ${i}`; - const address = identityToAddress(identity); - console.info(`Created ${role} (${debugPath(path)}): ${address}`); + if (logging) { + const role = i === 0 ? "token holder " : `distributor ${i}`; + const address = identityToAddress(identity); + console.info(`Created ${role} (${debugPath(path)}): ${address}`); + } + identities.push(identity); } - return profile; + return [profile, identities]; +} + +export async function setSecretAndCreateIdentities(mnemonic: string, chainId: ChainId): Promise { + return (await createUserProfile(mnemonic, chainId, constants.concurrency, true))[0]; } From 3f8ab89877c73aed83195d49828432209cc6bec9 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 12:45:54 +0100 Subject: [PATCH 14/16] Remove obsolete setSecretAndCreateIdentities --- packages/faucet/src/actions/generate.ts | 4 ++-- packages/faucet/src/actions/start/start.ts | 9 +++++++-- packages/faucet/src/profile.ts | 5 ----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/faucet/src/actions/generate.ts b/packages/faucet/src/actions/generate.ts index 48a16450..2e5f9b49 100644 --- a/packages/faucet/src/actions/generate.ts +++ b/packages/faucet/src/actions/generate.ts @@ -2,7 +2,7 @@ import { ChainId } from "@iov/bcp"; import { Bip39, Random } from "@iov/crypto"; import * as constants from "../constants"; -import { setSecretAndCreateIdentities } from "../profile"; +import { createUserProfile } from "../profile"; export async function generate(args: ReadonlyArray): Promise { if (args.length < 1) { @@ -17,5 +17,5 @@ export async function generate(args: ReadonlyArray): Promise { console.info(`FAUCET_MNEMONIC="${mnemonic}"`); // Log the addresses - await setSecretAndCreateIdentities(mnemonic, chainId); + await createUserProfile(mnemonic, chainId, constants.concurrency, true); } diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index 77548ca8..0e96c9a2 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -8,7 +8,7 @@ import * as constants from "../../constants"; import { logAccountsState } from "../../debugging"; import { Faucet } from "../../faucet"; import { availableTokensFromHolder } from "../../multichainhelpers"; -import { setSecretAndCreateIdentities } from "../../profile"; +import { createUserProfile } from "../../profile"; import { HttpError } from "./httperror"; import { RequestParser } from "./requestparser"; @@ -32,7 +32,12 @@ export async function start(args: ReadonlyArray): Promise { // Profile if (!constants.mnemonic) throw new Error("The FAUCET_MNEMONIC environment variable is not set"); - const profile = await setSecretAndCreateIdentities(constants.mnemonic, connection.chainId()); + const [profile] = await createUserProfile( + constants.mnemonic, + connection.chainId(), + constants.concurrency, + true, + ); // Faucet const faucet = new Faucet(constants.tokenConfig, connection, connector.codec, profile, true); diff --git a/packages/faucet/src/profile.ts b/packages/faucet/src/profile.ts index c7e28ddb..ec540e01 100644 --- a/packages/faucet/src/profile.ts +++ b/packages/faucet/src/profile.ts @@ -2,7 +2,6 @@ import { ChainId, Identity } from "@iov/bcp"; import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; import { identityToAddress } from "./addresses"; -import * as constants from "./constants"; import { debugPath } from "./hdpaths"; export async function createUserProfile( @@ -29,7 +28,3 @@ export async function createUserProfile( } return [profile, identities]; } - -export async function setSecretAndCreateIdentities(mnemonic: string, chainId: ChainId): Promise { - return (await createUserProfile(mnemonic, chainId, constants.concurrency, true))[0]; -} From 1280c2f8cf48ad6dfd7e412f76b257fbfe154269 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 12:58:18 +0100 Subject: [PATCH 15/16] Pull out Webserver --- packages/faucet/src/actions/start/start.ts | 76 +-------------- .../{actions/start => api}/httperror.spec.ts | 0 .../src/{actions/start => api}/httperror.ts | 0 .../start => api}/requestparser.spec.ts | 0 .../{actions/start => api}/requestparser.ts | 0 packages/faucet/src/api/webserver.ts | 94 +++++++++++++++++++ 6 files changed, 97 insertions(+), 73 deletions(-) rename packages/faucet/src/{actions/start => api}/httperror.spec.ts (100%) rename packages/faucet/src/{actions/start => api}/httperror.ts (100%) rename packages/faucet/src/{actions/start => api}/requestparser.spec.ts (100%) rename packages/faucet/src/{actions/start => api}/requestparser.ts (100%) create mode 100644 packages/faucet/src/api/webserver.ts diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index 0e96c9a2..408203f5 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -1,16 +1,11 @@ -import cors = require("@koa/cors"); import { createCosmWasmConnector } from "@cosmwasm/bcp"; -import Koa from "koa"; -import bodyParser from "koa-bodyparser"; -import { isValidAddress } from "../../addresses"; +import { Webserver } from "../../api/webserver"; import * as constants from "../../constants"; import { logAccountsState } from "../../debugging"; import { Faucet } from "../../faucet"; import { availableTokensFromHolder } from "../../multichainhelpers"; import { createUserProfile } from "../../profile"; -import { HttpError } from "./httperror"; -import { RequestParser } from "./requestparser"; export async function start(args: ReadonlyArray): Promise { if (args.length < 1) { @@ -57,71 +52,6 @@ export async function start(args: ReadonlyArray): Promise { setInterval(async () => faucet.refill(), 60_000); // ever 60 seconds console.info("Creating webserver ..."); - const api = new Koa(); - api.use(cors()); - api.use(bodyParser()); - - api.use(async context => { - switch (context.path) { - case "/": - case "/healthz": - context.response.body = - "Welcome to the faucet!\n" + - "\n" + - "Check the full status via the /status endpoint.\n" + - "You can get tokens from here by POSTing to /credit.\n" + - "See https://github.com/iov-one/iov-faucet for all further information.\n"; - break; - case "/status": { - const updatedAccounts = await faucet.loadAccounts(); - context.response.body = { - status: "ok", - nodeUrl: blockchainBaseUrl, - chainId: connection.chainId(), - chainTokens: chainTokens, - availableTokens: availableTokens, - holder: updatedAccounts[0], - distributors: updatedAccounts.slice(1), - }; - break; - } - case "/credit": { - if (context.request.method !== "POST") { - throw new HttpError(405, "This endpoint requires a POST request"); - } - - if (context.request.type !== "application/json") { - throw new HttpError(415, "Content-type application/json expected"); - } - - // context.request.body is set by the bodyParser() plugin - const requestBody = context.request.body; - const { address, ticker } = RequestParser.parseCreditBody(requestBody); - - if (!isValidAddress(address)) { - throw new HttpError(400, "Address is not in the expected format for this chain."); - } - - if (availableTokens.indexOf(ticker) === -1) { - const tokens = JSON.stringify(availableTokens); - throw new HttpError(422, `Token is not available. Available tokens are: ${tokens}`); - } - - try { - await faucet.credit(address, ticker); - } catch (e) { - console.error(e); - throw new HttpError(500, "Sending tokens failed"); - } - - context.response.body = "ok"; - break; - } - default: - // koa sends 404 by default - } - }); - const port = constants.port; - console.info(`Starting webserver on port ${port} ...`); - api.listen(port); + const server = new Webserver(faucet, { nodeUrl: blockchainBaseUrl, chainId: connection.chainId() }); + server.start(constants.port); } diff --git a/packages/faucet/src/actions/start/httperror.spec.ts b/packages/faucet/src/api/httperror.spec.ts similarity index 100% rename from packages/faucet/src/actions/start/httperror.spec.ts rename to packages/faucet/src/api/httperror.spec.ts diff --git a/packages/faucet/src/actions/start/httperror.ts b/packages/faucet/src/api/httperror.ts similarity index 100% rename from packages/faucet/src/actions/start/httperror.ts rename to packages/faucet/src/api/httperror.ts diff --git a/packages/faucet/src/actions/start/requestparser.spec.ts b/packages/faucet/src/api/requestparser.spec.ts similarity index 100% rename from packages/faucet/src/actions/start/requestparser.spec.ts rename to packages/faucet/src/api/requestparser.spec.ts diff --git a/packages/faucet/src/actions/start/requestparser.ts b/packages/faucet/src/api/requestparser.ts similarity index 100% rename from packages/faucet/src/actions/start/requestparser.ts rename to packages/faucet/src/api/requestparser.ts diff --git a/packages/faucet/src/api/webserver.ts b/packages/faucet/src/api/webserver.ts new file mode 100644 index 00000000..5669ada6 --- /dev/null +++ b/packages/faucet/src/api/webserver.ts @@ -0,0 +1,94 @@ +import Koa from "koa"; +import cors = require("@koa/cors"); +import { ChainId } from "@iov/bcp"; +import bodyParser from "koa-bodyparser"; + +import { isValidAddress } from "../addresses"; +import { Faucet } from "../faucet"; +import { availableTokensFromHolder } from "../multichainhelpers"; +import { HttpError } from "./httperror"; +import { RequestParser } from "./requestparser"; + +/** This will be passed 1:1 to the user */ +export interface ChainConstants { + readonly nodeUrl: string; + readonly chainId: ChainId; +} + +export class Webserver { + private readonly api = new Koa(); + + constructor(faucet: Faucet, chainChinstants: ChainConstants) { + this.api.use(cors()); + this.api.use(bodyParser()); + + this.api.use(async context => { + switch (context.path) { + case "/": + case "/healthz": + context.response.body = + "Welcome to the faucet!\n" + + "\n" + + "Check the full status via the /status endpoint.\n" + + "You can get tokens from here by POSTing to /credit.\n" + + "See https://github.com/iov-one/iov-faucet for all further information.\n"; + break; + case "/status": { + const [holder, ...distributors] = await faucet.loadAccounts(); + const availableTokens = availableTokensFromHolder(holder); + const chainTokens = await faucet.loadTokenTickers(); + context.response.body = { + status: "ok", + ...chainChinstants, + chainTokens: chainTokens, + availableTokens: availableTokens, + holder: holder, + distributors: distributors, + }; + break; + } + case "/credit": { + if (context.request.method !== "POST") { + throw new HttpError(405, "This endpoint requires a POST request"); + } + + if (context.request.type !== "application/json") { + throw new HttpError(415, "Content-type application/json expected"); + } + + // context.request.body is set by the bodyParser() plugin + const requestBody = context.request.body; + const { address, ticker } = RequestParser.parseCreditBody(requestBody); + + if (!isValidAddress(address)) { + throw new HttpError(400, "Address is not in the expected format for this chain."); + } + + const [holder] = await faucet.loadAccounts(); + const availableTokens = availableTokensFromHolder(holder); + if (availableTokens.indexOf(ticker) === -1) { + const tokens = JSON.stringify(availableTokens); + throw new HttpError(422, `Token is not available. Available tokens are: ${tokens}`); + } + + try { + await faucet.credit(address, ticker); + } catch (e) { + console.error(e); + throw new HttpError(500, "Sending tokens failed"); + } + + context.response.body = "ok"; + break; + } + default: + // koa sends 404 by default + } + }); + } + + public start(port: number): void { + console.info(`Starting webserver on port ${port} ...`); + this.api.listen(port); + } +} From 062b003a4a1843caf2475810a234ce6d9ffce769 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 10 Feb 2020 14:20:20 +0100 Subject: [PATCH 16/16] Fix denom of COSM and STAKE --- packages/faucet/src/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/faucet/src/constants.ts b/packages/faucet/src/constants.ts index 6eb5c861..bea2d710 100644 --- a/packages/faucet/src/constants.ts +++ b/packages/faucet/src/constants.ts @@ -12,13 +12,13 @@ export const tokenConfig: TokenConfiguration = { fractionalDigits: 6, name: "Fee Token", ticker: "COSM", - denom: "cosm", + denom: "ucosm", }, { fractionalDigits: 6, name: "Staking Token", ticker: "STAKE", - denom: "stake", + denom: "ustake", }, ], };