From ca103bfedbb7b271357a13094ba6e66ac6ee9ba9 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 3 Jun 2020 12:49:16 +0200 Subject: [PATCH 1/2] Remove BCP types from faucet --- packages/faucet/package.json | 4 +- packages/faucet/src/actions/generate.ts | 7 +- packages/faucet/src/actions/start/start.ts | 35 ++-- packages/faucet/src/addresses.ts | 26 ++- packages/faucet/src/api/requestparser.ts | 12 +- packages/faucet/src/api/webserver.ts | 18 +- packages/faucet/src/constants.ts | 8 +- packages/faucet/src/debugging.ts | 33 ++-- packages/faucet/src/faucet.spec.ts | 148 ++++++++-------- packages/faucet/src/faucet.ts | 161 +++++++++--------- packages/faucet/src/multichainhelpers.spec.ts | 56 ------ packages/faucet/src/multichainhelpers.ts | 11 -- packages/faucet/src/profile.ts | 25 ++- packages/faucet/src/tokenmanager.spec.ts | 94 +++++----- packages/faucet/src/tokenmanager.ts | 44 ++--- packages/faucet/src/types.ts | 33 +++- 16 files changed, 316 insertions(+), 399 deletions(-) delete mode 100644 packages/faucet/src/multichainhelpers.spec.ts delete mode 100644 packages/faucet/src/multichainhelpers.ts diff --git a/packages/faucet/package.json b/packages/faucet/package.json index 88711c577a..941c11ccf0 100644 --- a/packages/faucet/package.json +++ b/packages/faucet/package.json @@ -35,11 +35,9 @@ "test": "yarn build-or-skip && yarn test-node" }, "dependencies": { - "@cosmwasm/bcp": "^0.8.0", - "@iov/bcp": "^2.1.0", + "@cosmwasm/sdk38": "^0.8.0", "@iov/crypto": "^2.1.0", "@iov/encoding": "^2.1.0", - "@iov/keycontrol": "^2.1.0", "@iov/utils": "^2.0.2", "@koa/cors": "^3.0.0", "axios": "^0.19.0", diff --git a/packages/faucet/src/actions/generate.ts b/packages/faucet/src/actions/generate.ts index 2e5f9b499b..465908389d 100644 --- a/packages/faucet/src/actions/generate.ts +++ b/packages/faucet/src/actions/generate.ts @@ -1,8 +1,7 @@ -import { ChainId } from "@iov/bcp"; import { Bip39, Random } from "@iov/crypto"; import * as constants from "../constants"; -import { createUserProfile } from "../profile"; +import { createPens } from "../profile"; export async function generate(args: ReadonlyArray): Promise { if (args.length < 1) { @@ -11,11 +10,11 @@ export async function generate(args: ReadonlyArray): Promise { ); } - const chainId = args[0] as ChainId; + const chainId = args[0]; const mnemonic = Bip39.encode(Random.getBytes(16)).toString(); console.info(`FAUCET_MNEMONIC="${mnemonic}"`); // Log the addresses - await createUserProfile(mnemonic, chainId, constants.concurrency, true); + await createPens(mnemonic, chainId, constants.concurrency, true); } diff --git a/packages/faucet/src/actions/start/start.ts b/packages/faucet/src/actions/start/start.ts index ea0382dc18..9333cc81c0 100644 --- a/packages/faucet/src/actions/start/start.ts +++ b/packages/faucet/src/actions/start/start.ts @@ -1,11 +1,9 @@ -import { createCosmosConnector } from "@cosmwasm/bcp"; +import { CosmosClient } from "@cosmwasm/sdk38"; 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"; export async function start(args: ReadonlyArray): Promise { if (args.length < 1) { @@ -16,35 +14,28 @@ export async function start(args: ReadonlyArray): Promise { // Connection const blockchainBaseUrl = args[0]; - const connector = createCosmosConnector( + console.info(`Connecting to blockchain ${blockchainBaseUrl} ...`); + const chainId = await new CosmosClient(blockchainBaseUrl).getChainId(); + console.info(`Connected to network: ${chainId}`); + + // Faucet + if (!constants.mnemonic) throw new Error("The FAUCET_MNEMONIC environment variable is not set"); + const faucet = await Faucet.make( blockchainBaseUrl, constants.addressPrefix, constants.developmentTokenConfig, - ); - console.info(`Connecting to blockchain ${blockchainBaseUrl} ...`); - const connection = await connector.establishConnection(); - console.info(`Connected to network: ${connection.chainId}`); - - // Profile - if (!constants.mnemonic) throw new Error("The FAUCET_MNEMONIC environment variable is not set"); - const [profile] = await createUserProfile( constants.mnemonic, - connection.chainId, constants.concurrency, true, ); - - // Faucet - const faucet = new Faucet(constants.developmentTokenConfig, connection, connector.codec, profile, true); - const chainTokens = await faucet.loadTokenTickers(); + const chainTokens = faucet.loadTokenTickers(); console.info("Chain tokens:", chainTokens); const accounts = await faucet.loadAccounts(); - logAccountsState(accounts); - let availableTokens = availableTokensFromHolder(accounts[0]); + logAccountsState(accounts, constants.developmentTokenConfig); + let availableTokens = await faucet.availableTokens(); console.info("Available tokens:", availableTokens); setInterval(async () => { - const updatedAccounts = await faucet.loadAccounts(); - availableTokens = availableTokensFromHolder(updatedAccounts[0]); + availableTokens = await faucet.availableTokens(); console.info("Available tokens:", availableTokens); }, 60_000); @@ -52,6 +43,6 @@ export async function start(args: ReadonlyArray): Promise { setInterval(async () => faucet.refill(), 60_000); // ever 60 seconds console.info("Creating webserver ..."); - const server = new Webserver(faucet, { nodeUrl: blockchainBaseUrl, chainId: connection.chainId }); + const server = new Webserver(faucet, { nodeUrl: blockchainBaseUrl, chainId: chainId }); server.start(constants.port); } diff --git a/packages/faucet/src/addresses.ts b/packages/faucet/src/addresses.ts index 7b1306dd71..6b1f8ddfe0 100644 --- a/packages/faucet/src/addresses.ts +++ b/packages/faucet/src/addresses.ts @@ -1,17 +1,13 @@ -import { CosmosCodec } from "@cosmwasm/bcp"; -import { Address, Identity, TxCodec } from "@iov/bcp"; +import { Bech32 } from "@iov/encoding"; -import * as constants from "./constants"; - -const noTokensCodec: Pick = new CosmosCodec( - constants.addressPrefix, - [], -); - -export function identityToAddress(identity: Identity): Address { - return noTokensCodec.identityToAddress(identity); -} - -export function isValidAddress(input: string): boolean { - return noTokensCodec.isValidAddress(input); +export function isValidAddress(input: string, requiredPrefix: string): boolean { + try { + const { prefix, data } = Bech32.decode(input); + if (prefix !== requiredPrefix) { + return false; + } + return data.length === 20; + } catch { + return false; + } } diff --git a/packages/faucet/src/api/requestparser.ts b/packages/faucet/src/api/requestparser.ts index 2c9cfb4acc..db938f7349 100644 --- a/packages/faucet/src/api/requestparser.ts +++ b/packages/faucet/src/api/requestparser.ts @@ -1,10 +1,10 @@ -import { Address, TokenTicker } from "@iov/bcp"; - import { HttpError } from "./httperror"; export interface CreditRequestBodyData { - readonly ticker: TokenTicker; - readonly address: Address; + /** The ticker symbol */ + readonly ticker: string; + /** The recipient address */ + readonly address: string; } export class RequestParser { @@ -28,8 +28,8 @@ export class RequestParser { } return { - address: address as Address, - ticker: ticker as TokenTicker, + address: address, + ticker: ticker, }; } } diff --git a/packages/faucet/src/api/webserver.ts b/packages/faucet/src/api/webserver.ts index e175237b68..844c149633 100644 --- a/packages/faucet/src/api/webserver.ts +++ b/packages/faucet/src/api/webserver.ts @@ -1,24 +1,23 @@ import Koa from "koa"; import cors = require("@koa/cors"); -import { ChainId } from "@iov/bcp"; import bodyParser from "koa-bodyparser"; import { isValidAddress } from "../addresses"; +import * as constants from "../constants"; 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; + readonly chainId: string; } export class Webserver { private readonly api = new Koa(); - constructor(faucet: Faucet, chainChinstants: ChainConstants) { + constructor(faucet: Faucet, chainConstants: ChainConstants) { this.api.use(cors()); this.api.use(bodyParser()); @@ -35,11 +34,11 @@ export class Webserver { break; case "/status": { const [holder, ...distributors] = await faucet.loadAccounts(); - const availableTokens = availableTokensFromHolder(holder); - const chainTokens = await faucet.loadTokenTickers(); + const availableTokens = await faucet.availableTokens(); + const chainTokens = faucet.loadTokenTickers(); context.response.body = { status: "ok", - ...chainChinstants, + ...chainConstants, chainTokens: chainTokens, availableTokens: availableTokens, holder: holder, @@ -60,12 +59,11 @@ export class Webserver { const requestBody = context.request.body; const { address, ticker } = RequestParser.parseCreditBody(requestBody); - if (!isValidAddress(address)) { + if (!isValidAddress(address, constants.addressPrefix)) { throw new HttpError(400, "Address is not in the expected format for this chain."); } - const [holder] = await faucet.loadAccounts(); - const availableTokens = availableTokensFromHolder(holder); + const availableTokens = await faucet.availableTokens(); if (availableTokens.indexOf(ticker) === -1) { const tokens = JSON.stringify(availableTokens); throw new HttpError(422, `Token is not available. Available tokens are: ${tokens}`); diff --git a/packages/faucet/src/constants.ts b/packages/faucet/src/constants.ts index 2ebfaac569..b952883370 100644 --- a/packages/faucet/src/constants.ts +++ b/packages/faucet/src/constants.ts @@ -1,4 +1,4 @@ -import { TokenConfiguration } from "@cosmwasm/bcp"; +import { TokenConfiguration } from "./types"; export const binaryName = "cosmwasm-faucet"; export const concurrency: number = Number.parseInt(process.env.FAUCET_CONCURRENCY || "", 10) || 5; @@ -12,14 +12,12 @@ export const developmentTokenConfig: TokenConfiguration = { bankTokens: [ { fractionalDigits: 6, - name: "Fee Token", - ticker: "COSM", + tickerSymbol: "COSM", denom: "ucosm", }, { fractionalDigits: 6, - name: "Staking Token", - ticker: "STAKE", + tickerSymbol: "STAKE", denom: "ustake", }, ], diff --git a/packages/faucet/src/debugging.ts b/packages/faucet/src/debugging.ts index a63932b285..9a85ede577 100644 --- a/packages/faucet/src/debugging.ts +++ b/packages/faucet/src/debugging.ts @@ -1,38 +1,39 @@ -import { Account, Amount } from "@iov/bcp"; +import { Coin } from "@cosmwasm/sdk38"; import { Decimal } from "@iov/encoding"; -import { identityToAddress } from "./addresses"; -import { SendJob } from "./types"; +import { MinimalAccount, SendJob, TokenConfiguration } from "./types"; /** A string representation of a coin in a human-readable format that can change at any time */ -function debugAmount(amount: Amount): string { - const value = Decimal.fromAtomics(amount.quantity, amount.fractionalDigits).toString(); - return `${value} ${amount.tokenTicker}`; +function debugCoin(coin: Coin, tokens: TokenConfiguration): string { + const meta = tokens.bankTokens.find((token) => token.denom == coin.denom); + if (!meta) throw new Error(`No token configuration found for denom ${coin.denom}`); + const value = Decimal.fromAtomics(coin.amount, meta.fractionalDigits).toString(); + return `${value} ${meta?.tickerSymbol}`; } /** A string representation of a balance in a human-readable format that can change at any time */ -export function debugBalance(data: ReadonlyArray): string { - return `[${data.map(debugAmount).join(", ")}]`; +export function debugBalance(data: readonly Coin[], tokens: TokenConfiguration): string { + return `[${data.map((b) => debugCoin(b, tokens)).join(", ")}]`; } /** A string representation of an account in a human-readable format that can change at any time */ -export function debugAccount(account: Account): string { - return `${account.address}: ${debugBalance(account.balance)}`; +export function debugAccount(account: MinimalAccount, tokens: TokenConfiguration): string { + return `${account.address}: ${debugBalance(account.balance, tokens)}`; } -export function logAccountsState(accounts: ReadonlyArray): void { +export function logAccountsState(accounts: ReadonlyArray, tokens: TokenConfiguration): void { if (accounts.length < 2) { throw new Error("List of accounts must contain at least one token holder and one distributor"); } const holder = accounts[0]; const distributors = accounts.slice(1); - console.info("Holder:\n" + ` ${debugAccount(holder)}`); - console.info("Distributors:\n" + distributors.map((r) => ` ${debugAccount(r)}`).join("\n")); + console.info("Holder:\n" + ` ${debugAccount(holder, tokens)}`); + console.info("Distributors:\n" + distributors.map((r) => ` ${debugAccount(r, tokens)}`).join("\n")); } -export function logSendJob(job: SendJob): void { - const from = identityToAddress(job.sender); +export function logSendJob(job: SendJob, tokens: TokenConfiguration): void { + const from = job.sender; const to = job.recipient; - const amount = debugAmount(job.amount); + const amount = debugCoin(job.amount, tokens); console.info(`Sending ${amount} from ${from} to ${to} ...`); } diff --git a/packages/faucet/src/faucet.spec.ts b/packages/faucet/src/faucet.spec.ts index bd5a160cc3..3bd2c884f7 100644 --- a/packages/faucet/src/faucet.spec.ts +++ b/packages/faucet/src/faucet.spec.ts @@ -1,12 +1,10 @@ -import { CosmosCodec, CosmosConnection, TokenConfiguration } from "@cosmwasm/bcp"; -import { Address, ChainId, Identity, TokenTicker } from "@iov/bcp"; +import { CosmosClient } from "@cosmwasm/sdk38"; import { Random } from "@iov/crypto"; import { Bech32 } from "@iov/encoding"; -import { UserProfile } from "@iov/keycontrol"; import { assert } from "@iov/utils"; import { Faucet } from "./faucet"; -import { createUserProfile } from "./profile"; +import { TokenConfiguration } from "./types"; function pendingWithoutWasmd(): void { if (!process.env.WASMD_ENABLED) { @@ -15,175 +13,165 @@ function pendingWithoutWasmd(): void { } const httpUrl = "http://localhost:1317"; -const defaultConfig: TokenConfiguration = { +const defaultTokenConfig: TokenConfiguration = { bankTokens: [ { fractionalDigits: 6, - name: "Fee Token", - ticker: "COSM", + tickerSymbol: "COSM", denom: "ucosm", }, { fractionalDigits: 6, - name: "Staking Token", - ticker: "STAKE", + tickerSymbol: "STAKE", denom: "ustake", }, ], }; const defaultAddressPrefix = "cosmos"; -const defaultChainId = "cosmos:testing" as ChainId; -const codec = new CosmosCodec(defaultAddressPrefix, defaultConfig.bankTokens); -function makeRandomAddress(): Address { - return Bech32.encode(defaultAddressPrefix, Random.getBytes(20)) as Address; +function makeRandomAddress(): string { + return Bech32.encode(defaultAddressPrefix, Random.getBytes(20)); } 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"; -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: identities[0], - distributors: identities.slice(1), - }; -} - describe("Faucet", () => { describe("constructor", () => { it("can be constructed", async () => { pendingWithoutWasmd(); - const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); - const { profile } = await makeProfile(); - const faucet = new Faucet(defaultConfig, connection, codec, profile); + const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3); expect(faucet).toBeTruthy(); - connection.disconnect(); + }); + }); + + describe("availableTokens", () => { + it("is empty when no tokens are configures", async () => { + const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, { bankTokens: [] }, faucetMnemonic, 3); + const tickers = await faucet.availableTokens(); + expect(tickers).toEqual([]); + }); + + it("is empty when no tokens are configures", async () => { + const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3); + const tickers = await faucet.availableTokens(); + expect(tickers).toEqual(["COSM", "STAKE"]); }); }); describe("send", () => { it("can send bank token", async () => { pendingWithoutWasmd(); - const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); - const { profile, holder } = await makeProfile(); - const faucet = new Faucet(defaultConfig, connection, codec, profile); + const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3); const recipient = makeRandomAddress(); await faucet.send({ amount: { - quantity: "23456", - fractionalDigits: 6, - tokenTicker: "COSM" as TokenTicker, + amount: "23456", + denom: "ucosm", }, - sender: holder, + sender: faucet.holderAddress, recipient: recipient, }); - const account = await connection.getAccount({ address: recipient }); + + const readOnlyClient = new CosmosClient(httpUrl); + const account = await readOnlyClient.getAccount(recipient); assert(account); expect(account.balance).toEqual([ { - quantity: "23456", - fractionalDigits: 6, - tokenTicker: "COSM" as TokenTicker, + amount: "23456", + denom: "ucosm", }, ]); - connection.disconnect(); }); }); describe("refill", () => { it("works", async () => { pendingWithoutWasmd(); - const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); - const { profile, distributors } = await makeProfile(1); - const faucet = new Faucet(defaultConfig, connection, codec, profile); + const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3); await faucet.refill(); - const distributorBalance = (await connection.getAccount({ pubkey: distributors[0].pubkey }))?.balance; + const readOnlyClient = new CosmosClient(httpUrl); + const distributorBalance = (await readOnlyClient.getAccount(faucet.distributorAddresses[0]))?.balance; assert(distributorBalance); expect(distributorBalance).toEqual([ jasmine.objectContaining({ - tokenTicker: "COSM", - fractionalDigits: 6, + denom: "ucosm", }), jasmine.objectContaining({ - tokenTicker: "STAKE", - fractionalDigits: 6, + denom: "ustake", }), ]); - expect(Number.parseInt(distributorBalance[0].quantity, 10)).toBeGreaterThanOrEqual(80_000000); - expect(Number.parseInt(distributorBalance[1].quantity, 10)).toBeGreaterThanOrEqual(80_000000); - connection.disconnect(); + expect(Number.parseInt(distributorBalance[0].amount, 10)).toBeGreaterThanOrEqual(80_000000); + expect(Number.parseInt(distributorBalance[1].amount, 10)).toBeGreaterThanOrEqual(80_000000); }); }); describe("credit", () => { it("works for fee token", async () => { pendingWithoutWasmd(); - const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); - const { profile } = await makeProfile(1); - const faucet = new Faucet(defaultConfig, connection, codec, profile); + const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3); const recipient = makeRandomAddress(); - await faucet.credit(recipient, "COSM" as TokenTicker); - const account = await connection.getAccount({ address: recipient }); + await faucet.credit(recipient, "COSM"); + + const readOnlyClient = new CosmosClient(httpUrl); + const account = await readOnlyClient.getAccount(recipient); assert(account); expect(account.balance).toEqual([ { - quantity: "10000000", - fractionalDigits: 6, - tokenTicker: "COSM" as TokenTicker, + amount: "10000000", + denom: "ucosm", }, ]); - connection.disconnect(); }); it("works for stake token", async () => { pendingWithoutWasmd(); - const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); - const { profile } = await makeProfile(1); - const faucet = new Faucet(defaultConfig, connection, codec, profile); + const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3); const recipient = makeRandomAddress(); - await faucet.credit(recipient, "STAKE" as TokenTicker); - const account = await connection.getAccount({ address: recipient }); + await faucet.credit(recipient, "STAKE"); + + const readOnlyClient = new CosmosClient(httpUrl); + const account = await readOnlyClient.getAccount(recipient); assert(account); expect(account.balance).toEqual([ { - quantity: "10000000", - fractionalDigits: 6, - tokenTicker: "STAKE" as TokenTicker, + amount: "10000000", + denom: "ustake", }, ]); - connection.disconnect(); }); }); describe("loadTokenTickers", () => { it("works", async () => { pendingWithoutWasmd(); - const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); - const { profile } = await makeProfile(); - const faucet = new Faucet(defaultConfig, connection, codec, profile); - const tickers = await faucet.loadTokenTickers(); + const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 3); + const tickers = faucet.loadTokenTickers(); expect(tickers).toEqual(["COSM", "STAKE"]); - connection.disconnect(); }); }); describe("loadAccounts", () => { it("works", async () => { pendingWithoutWasmd(); - const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); - const { profile, holder } = await makeProfile(); - const faucet = new Faucet(defaultConfig, connection, codec, profile); + const faucet = await Faucet.make(httpUrl, defaultAddressPrefix, defaultTokenConfig, faucetMnemonic, 1); const accounts = await faucet.loadAccounts(); - const expectedHolderAccount = await connection.getAccount({ pubkey: holder.pubkey }); + + const readOnlyClient = new CosmosClient(httpUrl); + const expectedHolderAccount = await readOnlyClient.getAccount(faucet.holderAddress); + const expectedDistributorAccount = await readOnlyClient.getAccount(faucet.distributorAddresses[0]); assert(expectedHolderAccount); + assert(expectedDistributorAccount); expect(accounts).toEqual([ - { address: expectedHolderAccount.address, balance: expectedHolderAccount.balance }, + jasmine.objectContaining({ + address: expectedHolderAccount.address, + balance: expectedHolderAccount.balance, + }), + jasmine.objectContaining({ + address: expectedDistributorAccount.address, + balance: expectedDistributorAccount.balance, + }), ]); - connection.disconnect(); }); }); }); diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index 3af0adbc06..f6d16a2a10 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -1,105 +1,112 @@ -import { TokenConfiguration } from "@cosmwasm/bcp"; -import { - Account, - Address, - BlockchainConnection, - Identity, - isBlockInfoFailed, - isBlockInfoPending, - SendTransaction, - TokenTicker, - TxCodec, -} from "@iov/bcp"; -import { UserProfile } from "@iov/keycontrol"; +import { CosmosClient, Pen, SigningCosmosClient } from "@cosmwasm/sdk38"; import { sleep } from "@iov/utils"; -import { identityToAddress } from "./addresses"; import { debugAccount, logAccountsState, logSendJob } from "./debugging"; -import { availableTokensFromHolder, identitiesOfFirstWallet } from "./multichainhelpers"; +import { createPens } from "./profile"; import { TokenManager } from "./tokenmanager"; -import { SendJob } from "./types"; +import { MinimalAccount, SendJob, TokenConfiguration } from "./types"; + +function isDefined(value: X | undefined): value is X { + return value !== undefined; +} export class Faucet { - public get holder(): Identity { - return identitiesOfFirstWallet(this.profile)[0]; - } - public get distributors(): readonly Identity[] { - return identitiesOfFirstWallet(this.profile).slice(1); + public static async make( + apiUrl: string, + addressPrefix: string, + config: TokenConfiguration, + mnemonic: string, + numberOfDistributors: number, + logging = false, + ): Promise { + const pens = await createPens(mnemonic, addressPrefix, numberOfDistributors, logging); + return new Faucet(apiUrl, addressPrefix, config, pens, logging); } + public readonly addressPrefix: string; + public readonly holderAddress: string; + public readonly distributorAddresses: readonly string[]; + + private readonly tokenConfig: TokenConfiguration; private readonly tokenManager: TokenManager; - private readonly connection: BlockchainConnection; - private readonly codec: TxCodec; - private readonly profile: UserProfile; + private readonly readOnlyClient: CosmosClient; + private readonly clients: { [senderAddress: string]: SigningCosmosClient }; private readonly logging: boolean; private creditCount = 0; - public constructor( + private constructor( + apiUrl: string, + addressPrefix: string, config: TokenConfiguration, - connection: BlockchainConnection, - codec: TxCodec, - profile: UserProfile, + pens: readonly [string, Pen][], logging = false, ) { + this.addressPrefix = addressPrefix; + this.tokenConfig = config; this.tokenManager = new TokenManager(config); - this.connection = connection; - this.codec = codec; - this.profile = profile; + + this.readOnlyClient = new CosmosClient(apiUrl); + + this.holderAddress = pens[0][0]; + this.distributorAddresses = pens.slice(1).map((pair) => pair[0]); + + // we need one client per sender + const clients: { [senderAddress: string]: SigningCosmosClient } = {}; + for (const [senderAddress, pen] of pens) { + clients[senderAddress] = new SigningCosmosClient(apiUrl, senderAddress, (signBytes) => + pen.sign(signBytes), + ); + } + this.clients = clients; this.logging = logging; } + /** + * Returns a list of ticker symbols of tokens owned by the the holder and configured in the faucet + */ + public async availableTokens(): Promise> { + const holderAccount = await this.readOnlyClient.getAccount(this.holderAddress); + const balance = holderAccount ? holderAccount.balance : []; + + return balance + .filter((b) => b.amount !== "0") + .map((b) => this.tokenConfig.bankTokens.find((token) => token.denom == b.denom)) + .filter(isDefined) + .map((token) => token.tickerSymbol); + } + /** * Creates and posts a send transaction. Then waits until the transaction is in a block. */ public async send(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 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)); - if (isBlockInfoFailed(blockInfo)) { - throw new Error(`Sending tokens failed. Code: ${blockInfo.code}, message: ${blockInfo.message}`); - } + await this.clients[job.sender].sendTokens(job.recipient, [job.amount], "Make love, not war"); } /** 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]; + public async credit(recipient: string, tickerSymbol: string): Promise { + if (this.distributorAddresses.length === 0) throw new Error("No distributor account available"); + const sender = this.distributorAddresses[this.getCreditCount() % this.distributorAddresses.length]; const job: SendJob = { sender: sender, recipient: recipient, - amount: this.tokenManager.creditAmount(ticker), + amount: this.tokenManager.creditAmount(tickerSymbol), }; - if (this.logging) logSendJob(job); + if (this.logging) logSendJob(job, this.tokenConfig); await this.send(job); } - public async loadTokenTickers(): Promise> { - return (await this.connection.getAllTokens()).map((token) => token.tokenTicker); + public loadTokenTickers(): readonly string[] { + return this.tokenConfig.bankTokens.map((token) => token.tickerSymbol); } - public async loadAccounts(): Promise>> { - const addresses = identitiesOfFirstWallet(this.profile).map((identity) => identityToAddress(identity)); + public async loadAccounts(): Promise> { + const addresses = [this.holderAddress, ...this.distributorAddresses]; - const out: Account[] = []; + const out: MinimalAccount[] = []; for (const address of addresses) { - const response = await this.connection.getAccount({ address: address }); + const response = await this.readOnlyClient.getAccount(address); if (response) { - out.push({ - address: response.address, - balance: response.balance, - }); + out.push(response); } else { out.push({ address: address, @@ -113,49 +120,49 @@ export class Faucet { public async refill(): Promise { if (this.logging) { - console.info(`Connected to network: ${this.connection.chainId}`); - console.info(`Tokens on network: ${(await this.loadTokenTickers()).join(", ")}`); + console.info(`Connected to network: ${this.readOnlyClient.getChainId()}`); + console.info(`Tokens on network: ${this.loadTokenTickers().join(", ")}`); } const accounts = await this.loadAccounts(); - if (this.logging) logAccountsState(accounts); - const [holderAccount, ...distributorAccounts] = accounts; + if (this.logging) logAccountsState(accounts, this.tokenConfig); + const [_, ...distributorAccounts] = accounts; - const availableTokens = availableTokensFromHolder(holderAccount); + const availableTokens = await this.availableTokens(); if (this.logging) console.info("Available tokens:", availableTokens); const jobs: SendJob[] = []; - for (const token of availableTokens) { + for (const tickerSymbol of availableTokens) { const refillDistibutors = distributorAccounts.filter((account) => - this.tokenManager.needsRefill(account, token), + this.tokenManager.needsRefill(account, tickerSymbol), ); if (this.logging) { - console.info(`Refilling ${token} of:`); + console.info(`Refilling ${tickerSymbol} of:`); console.info( refillDistibutors.length - ? refillDistibutors.map((r) => ` ${debugAccount(r)}`).join("\n") + ? refillDistibutors.map((r) => ` ${debugAccount(r, this.tokenConfig)}`).join("\n") : " none", ); } for (const refillDistibutor of refillDistibutors) { jobs.push({ - sender: this.holder, + sender: this.holderAddress, recipient: refillDistibutor.address, - amount: this.tokenManager.refillAmount(token), + amount: this.tokenManager.refillAmount(tickerSymbol), }); } } if (jobs.length > 0) { for (const job of jobs) { - if (this.logging) logSendJob(job); + if (this.logging) logSendJob(job, this.tokenConfig); await this.send(job); await sleep(50); } if (this.logging) { console.info("Done refilling accounts."); - logAccountsState(await this.loadAccounts()); + logAccountsState(await this.loadAccounts(), this.tokenConfig); } } else { if (this.logging) { diff --git a/packages/faucet/src/multichainhelpers.spec.ts b/packages/faucet/src/multichainhelpers.spec.ts deleted file mode 100644 index cd7172bb12..0000000000 --- a/packages/faucet/src/multichainhelpers.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Address, Algorithm, PubkeyBundle, PubkeyBytes, TokenTicker } from "@iov/bcp"; - -import { availableTokensFromHolder } from "./multichainhelpers"; - -describe("multichainhelpers", () => { - describe("availableTokensFromHolder", () => { - const defaultPubkey: PubkeyBundle = { - algo: Algorithm.Ed25519, - data: new Uint8Array([0, 1, 2, 3]) as PubkeyBytes, - }; - - it("works for an empty account", () => { - const tickers = availableTokensFromHolder({ - address: "aabbccdd" as Address, - pubkey: defaultPubkey, - balance: [], - }); - expect(tickers).toEqual([]); - }); - - it("works for one token", () => { - const tickers = availableTokensFromHolder({ - address: "aabbccdd" as Address, - pubkey: defaultPubkey, - balance: [ - { - quantity: "1", - fractionalDigits: 9, - tokenTicker: "JADE" as TokenTicker, - }, - ], - }); - expect(tickers).toEqual(["JADE"]); - }); - - it("works for two tokens", () => { - const tickers = availableTokensFromHolder({ - address: "aabbccdd" as Address, - pubkey: defaultPubkey, - balance: [ - { - quantity: "1", - fractionalDigits: 9, - tokenTicker: "JADE" as TokenTicker, - }, - { - quantity: "1", - fractionalDigits: 9, - tokenTicker: "TRASH" as TokenTicker, - }, - ], - }); - expect(tickers).toEqual(["JADE", "TRASH"]); - }); - }); -}); diff --git a/packages/faucet/src/multichainhelpers.ts b/packages/faucet/src/multichainhelpers.ts deleted file mode 100644 index d7adcf2e3c..0000000000 --- a/packages/faucet/src/multichainhelpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Account, Identity, TokenTicker } from "@iov/bcp"; -import { UserProfile } from "@iov/keycontrol"; - -export function identitiesOfFirstWallet(profile: UserProfile): ReadonlyArray { - const wallet = profile.wallets.value[0]; - return profile.getIdentities(wallet.id); -} - -export function availableTokensFromHolder(holderAccount: Account): ReadonlyArray { - return holderAccount.balance.map((coin) => coin.tokenTicker); -} diff --git a/packages/faucet/src/profile.ts b/packages/faucet/src/profile.ts index ec540e0116..a07d6ce7a0 100644 --- a/packages/faucet/src/profile.ts +++ b/packages/faucet/src/profile.ts @@ -1,30 +1,27 @@ -import { ChainId, Identity } from "@iov/bcp"; -import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; +import { makeCosmoshubPath, Pen, Secp256k1Pen } from "@cosmwasm/sdk38"; -import { identityToAddress } from "./addresses"; import { debugPath } from "./hdpaths"; -export async function createUserProfile( +export async function createPens( mnemonic: string, - chainId: ChainId, + addressPrefix: string, numberOfDistributors: number, logging = false, -): Promise<[UserProfile, readonly Identity[]]> { - const profile = new UserProfile(); - const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(mnemonic)); - const identities = new Array(); +): Promise { + const pens = new Array<[string, Pen]>(); // first account is the token holder const numberOfIdentities = 1 + numberOfDistributors; for (let i = 0; i < numberOfIdentities; i++) { - const path = HdPaths.cosmos(i); - const identity = await profile.createIdentity(wallet.id, chainId, path); + const path = makeCosmoshubPath(i); + const pen = await Secp256k1Pen.fromMnemonic(mnemonic, path); + const address = pen.address(addressPrefix); if (logging) { const role = i === 0 ? "token holder " : `distributor ${i}`; - const address = identityToAddress(identity); console.info(`Created ${role} (${debugPath(path)}): ${address}`); } - identities.push(identity); + pens.push([address, pen]); } - return [profile, identities]; + + return pens; } diff --git a/packages/faucet/src/tokenmanager.spec.ts b/packages/faucet/src/tokenmanager.spec.ts index 7801c462c0..3725a2f835 100644 --- a/packages/faucet/src/tokenmanager.spec.ts +++ b/packages/faucet/src/tokenmanager.spec.ts @@ -1,19 +1,15 @@ -import { TokenConfiguration } from "@cosmwasm/bcp"; -import { TokenTicker } from "@iov/bcp"; - import { TokenManager } from "./tokenmanager"; +import { TokenConfiguration } from "./types"; const dummyConfig: TokenConfiguration = { bankTokens: [ { - ticker: "TOKENZ", - name: "The tokenz", + tickerSymbol: "TOKENZ", fractionalDigits: 6, denom: "utokenz", }, { - ticker: "TRASH", - name: "Trash token", + tickerSymbol: "TRASH", fractionalDigits: 3, denom: "mtrash", }, @@ -32,34 +28,30 @@ describe("TokenManager", () => { 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("TOKENZ")).toEqual({ + amount: "10000000", + denom: "utokenz", }); - expect(tm.creditAmount("TRASH" as TokenTicker)).toEqual({ - quantity: "10000", - fractionalDigits: 3, - tokenTicker: "TRASH", + expect(tm.creditAmount("TRASH")).toEqual({ + amount: "10000", + denom: "mtrash", }); }); 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", + expect(tm.creditAmount("TRASH")).toEqual({ + amount: "22000", + denom: "mtrash", }); 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", + expect(tm.creditAmount("TRASH")).toEqual({ + amount: "10000", + denom: "mtrash", }); process.env.FAUCET_CREDIT_AMOUNT_TRASH = ""; }); @@ -74,38 +66,34 @@ describe("TokenManager", () => { }); it("returns 20*10 + '000' by default", () => { - expect(tm.refillAmount("TRASH" as TokenTicker)).toEqual({ - quantity: "200000", - fractionalDigits: 3, - tokenTicker: "TRASH", + expect(tm.refillAmount("TRASH")).toEqual({ + amount: "200000", + denom: "mtrash", }); }); 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", + expect(tm.refillAmount("TRASH")).toEqual({ + amount: "440000", + denom: "mtrash", }); }); 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", + expect(tm.refillAmount("TRASH")).toEqual({ + amount: "300000", + denom: "mtrash", }); }); 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", + expect(tm.refillAmount("TRASH")).toEqual({ + amount: "660000", + denom: "mtrash", }); }); }); @@ -119,38 +107,34 @@ describe("TokenManager", () => { }); it("returns 8*10 + '000' by default", () => { - expect(tm.refillThreshold("TRASH" as TokenTicker)).toEqual({ - quantity: "80000", - fractionalDigits: 3, - tokenTicker: "TRASH", + expect(tm.refillThreshold("TRASH")).toEqual({ + amount: "80000", + denom: "mtrash", }); }); 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", + expect(tm.refillThreshold("TRASH")).toEqual({ + amount: "176000", + denom: "mtrash", }); }); 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", + expect(tm.refillThreshold("TRASH")).toEqual({ + amount: "50000", + denom: "mtrash", }); }); 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", + expect(tm.refillThreshold("TRASH")).toEqual({ + amount: "110000", + denom: "mtrash", }); }); }); diff --git a/packages/faucet/src/tokenmanager.ts b/packages/faucet/src/tokenmanager.ts index c43d3502a5..cc987f5c9b 100644 --- a/packages/faucet/src/tokenmanager.ts +++ b/packages/faucet/src/tokenmanager.ts @@ -1,7 +1,8 @@ -import { TokenConfiguration } from "@cosmwasm/bcp"; -import { Account, Amount, TokenTicker } from "@iov/bcp"; +import { Coin } from "@cosmwasm/sdk38"; import { Decimal, Uint53 } from "@iov/encoding"; +import { BankTokenMeta, MinimalAccount, TokenConfiguration } from "./types"; + /** Send `factor` times credit amount on refilling */ const defaultRefillFactor = 20; @@ -16,50 +17,51 @@ export class TokenManager { } /** 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}`]; + public creditAmount(tickerSymbol: string, factor: Uint53 = new Uint53(1)): Coin { + const amountFromEnv = process.env[`FAUCET_CREDIT_AMOUNT_${tickerSymbol}`]; const amount = amountFromEnv ? Uint53.fromString(amountFromEnv).toNumber() : 10; const value = new Uint53(amount * factor.toNumber()); - const fractionalDigits = this.getFractionalDigits(token); + const meta = this.getTokenMeta(tickerSymbol); return { - quantity: value.toString() + "0".repeat(fractionalDigits), - fractionalDigits: fractionalDigits, - tokenTicker: token, + amount: value.toString() + "0".repeat(meta.fractionalDigits), + denom: meta.denom, }; } - public refillAmount(token: TokenTicker): Amount { + public refillAmount(tickerSymbol: string): Coin { const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_FACTOR || "0", 10) || undefined; const factor = new Uint53(factorFromEnv || defaultRefillFactor); - return this.creditAmount(token, factor); + return this.creditAmount(tickerSymbol, factor); } - public refillThreshold(token: TokenTicker): Amount { + public refillThreshold(tickerSymbol: string): Coin { const factorFromEnv = Number.parseInt(process.env.FAUCET_REFILL_THRESHOLD || "0", 10) || undefined; const factor = new Uint53(factorFromEnv || defaultRefillThresholdFactor); - return this.creditAmount(token, factor); + return this.creditAmount(tickerSymbol, 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); + public needsRefill(account: MinimalAccount, tickerSymbol: string): boolean { + const meta = this.getTokenMeta(tickerSymbol); + + const balanceAmount = account.balance.find((b) => b.denom === meta.denom); const balance = balanceAmount - ? Decimal.fromAtomics(balanceAmount.quantity, balanceAmount.fractionalDigits) + ? Decimal.fromAtomics(balanceAmount.amount, meta.fractionalDigits) : Decimal.fromAtomics("0", 0); - const thresholdAmount = this.refillThreshold(token); - const threshold = Decimal.fromAtomics(thresholdAmount.quantity, thresholdAmount.fractionalDigits); + const thresholdAmount = this.refillThreshold(tickerSymbol); + const threshold = Decimal.fromAtomics(thresholdAmount.amount, meta.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.find((token) => token.ticker === ticker); - if (!match) throw new Error(`No token found for ticker symbol: ${ticker}`); - return match.fractionalDigits; + private getTokenMeta(tickerSymbol: string): BankTokenMeta { + const match = this.config.bankTokens.find((token) => token.tickerSymbol === tickerSymbol); + if (!match) throw new Error(`No token found for ticker symbol: ${tickerSymbol}`); + return match; } } diff --git a/packages/faucet/src/types.ts b/packages/faucet/src/types.ts index 31334171cc..3ac00ac169 100644 --- a/packages/faucet/src/types.ts +++ b/packages/faucet/src/types.ts @@ -1,7 +1,32 @@ -import { Address, Amount, Identity } from "@iov/bcp"; +import { Account, Coin } from "@cosmwasm/sdk38"; export interface SendJob { - readonly sender: Identity; - readonly recipient: Address; - readonly amount: Amount; + readonly sender: string; + readonly recipient: string; + readonly amount: Coin; } + +export interface BankTokenMeta { + readonly denom: string; + /** + * The token ticker symbol, e.g. ATOM or ETH. + */ + readonly tickerSymbol: string; + /** + * The number of fractional digits the token supports. + * + * A quantity is expressed as atomic units. 10^fractionalDigits of those + * atomic units make up 1 token. + * + * E.g. in Ethereum 10^18 wei are 1 ETH and from the quantity 123000000000000000000 + * the last 18 digits are the fractional part and the rest the wole part. + */ + readonly fractionalDigits: number; +} + +export interface TokenConfiguration { + /** Supported tokens of the Cosmos SDK bank module */ + readonly bankTokens: ReadonlyArray; +} + +export type MinimalAccount = Pick; From 7acc0e4ef867ba54b3bc3ad5e7fb54c5d79248df Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 3 Jun 2020 12:53:18 +0200 Subject: [PATCH 2/2] Request accounts in parallel --- packages/faucet/src/faucet.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/faucet/src/faucet.ts b/packages/faucet/src/faucet.ts index f6d16a2a10..78239407e1 100644 --- a/packages/faucet/src/faucet.ts +++ b/packages/faucet/src/faucet.ts @@ -102,20 +102,21 @@ export class Faucet { public async loadAccounts(): Promise> { const addresses = [this.holderAddress, ...this.distributorAddresses]; - const out: MinimalAccount[] = []; - for (const address of addresses) { - const response = await this.readOnlyClient.getAccount(address); - if (response) { - out.push(response); - } else { - out.push({ - address: address, - balance: [], - }); - } - } - - return out; + return Promise.all( + addresses.map( + async (address): Promise => { + const response = await this.readOnlyClient.getAccount(address); + if (response) { + return response; + } else { + return { + address: address, + balance: [], + }; + } + }, + ), + ); } public async refill(): Promise {