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