diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index a4afe2de7c..77548ca86a 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 72fc670f37..932ba7a936 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 251fba1257..66d406b23a 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 11577eca38..c7e28ddbf8 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]; }