From b3840b1c34b2a2babc931ef903d9ea8f166adaaf Mon Sep 17 00:00:00 2001 From: Olexandr Berezan <116814585+prxgr4mm3r@users.noreply.github.com> Date: Mon, 12 Feb 2024 22:18:33 +0200 Subject: [PATCH] feat: Add faucet for Swanky node (#60) Co-authored-by: Igor Papandinas --- src/commands/account/balance.ts | 63 +++++++++++++++++++ src/commands/account/create.ts | 9 ++- src/commands/account/faucet.ts | 23 +++++++ src/commands/account/swankyAccountCommands.ts | 45 +++++++++++++ src/commands/contract/deploy.ts | 10 +-- src/commands/init/index.ts | 9 +-- src/lib/account.ts | 3 +- src/lib/consts.ts | 7 ++- src/lib/contractCall.ts | 10 +-- src/lib/substrate-api.ts | 34 +++++++++- src/lib/swankyCommand.ts | 15 ++++- 11 files changed, 199 insertions(+), 29 deletions(-) create mode 100644 src/commands/account/balance.ts create mode 100644 src/commands/account/faucet.ts create mode 100644 src/commands/account/swankyAccountCommands.ts diff --git a/src/commands/account/balance.ts b/src/commands/account/balance.ts new file mode 100644 index 00000000..4a9caf85 --- /dev/null +++ b/src/commands/account/balance.ts @@ -0,0 +1,63 @@ +import { Args } from "@oclif/core"; +import { ApiPromise } from "@polkadot/api"; +import type { AccountInfo, Balance as BalanceType } from "@polkadot/types/interfaces"; +import { ChainApi, resolveNetworkUrl } from "../../lib/index.js"; +import { SwankyCommand } from "../../lib/swankyCommand.js"; +import { InputError } from "../../lib/errors.js"; +import { formatBalance } from "@polkadot/util"; + +export class Balance extends SwankyCommand { + static description = "Balance of an account"; + + static args = { + alias: Args.string({ + name: "alias", + description: "Alias of account to be used", + }), + }; + async run(): Promise { + const { args } = await this.parse(Balance); + + if (!args.alias) { + throw new InputError( + "Missing argument! Please provide an alias account to get the balance from. Example usage: `swanky account balance `" + ); + } + + const accountData = this.findAccountByAlias(args.alias); + const networkUrl = resolveNetworkUrl(this.swankyConfig, ""); + + const api = (await this.spinner.runCommand(async () => { + const api = await ChainApi.create(networkUrl); + await api.start(); + return api.apiInst; + }, "Connecting to node")) as ApiPromise; + + const decimals = api.registry.chainDecimals[0]; + formatBalance.setDefaults({ unit: "UNIT", decimals }); + + const { nonce, data: balance } = await api.query.system.account( + accountData.address + ); + const { free, reserved, miscFrozen, feeFrozen } = balance; + + let frozen: BalanceType; + if (feeFrozen.gt(miscFrozen)) { + frozen = feeFrozen; + } else { + frozen = miscFrozen; + } + + const transferrableBalance = free.sub(frozen); + const totalBalance = free.add(reserved); + + console.log("Transferrable Balance:", formatBalance(transferrableBalance)); + if (!transferrableBalance.eq(totalBalance)) { + console.log("Total Balance:", formatBalance(totalBalance)); + console.log("Raw Balances:", balance.toHuman()); + } + console.log("Account Nonce:", nonce.toHuman()); + + await api.disconnect(); + } +} diff --git a/src/commands/account/create.ts b/src/commands/account/create.ts index 5c4fc4c4..a9000c4a 100644 --- a/src/commands/account/create.ts +++ b/src/commands/account/create.ts @@ -3,8 +3,9 @@ import chalk from "chalk"; import { ChainAccount, encrypt } from "../../lib/index.js"; import { AccountData } from "../../types/index.js"; import inquirer from "inquirer"; -import { SwankyCommand } from "../../lib/swankyCommand.js"; -export class CreateAccount extends SwankyCommand { +import { SwankyAccountCommand } from "./swankyAccountCommands.js"; + +export class CreateAccount extends SwankyAccountCommand { static description = "Create a new dev account in config"; static flags = { @@ -35,7 +36,7 @@ export class CreateAccount extends SwankyCommand { ); } - let tmpMnemonic = ""; + let tmpMnemonic: string; if (flags.generate) { tmpMnemonic = ChainAccount.generate(); console.log( @@ -84,5 +85,7 @@ export class CreateAccount extends SwankyCommand { accountData.alias )} stored to config` ); + + await this.performFaucetTransfer(accountData, true); } } diff --git a/src/commands/account/faucet.ts b/src/commands/account/faucet.ts new file mode 100644 index 00000000..02b6b965 --- /dev/null +++ b/src/commands/account/faucet.ts @@ -0,0 +1,23 @@ +import { Args } from "@oclif/core"; +import { SwankyAccountCommand } from "./swankyAccountCommands.js"; + +export class Faucet extends SwankyAccountCommand { + static description = "Transfer some tokens from faucet to an account"; + + static aliases = [`account:faucet`]; + + static args = { + alias: Args.string({ + name: "alias", + required: true, + description: "Alias of account to be used", + }), + }; + + async run(): Promise { + const { args } = await this.parse(Faucet); + + const accountData = this.findAccountByAlias(args.alias); + await this.performFaucetTransfer(accountData); + } +} diff --git a/src/commands/account/swankyAccountCommands.ts b/src/commands/account/swankyAccountCommands.ts new file mode 100644 index 00000000..b884f203 --- /dev/null +++ b/src/commands/account/swankyAccountCommands.ts @@ -0,0 +1,45 @@ +import { Command } from "@oclif/core"; +import chalk from "chalk"; +import { AccountData, ChainApi, resolveNetworkUrl } from "../../index.js"; +import { LOCAL_FAUCET_AMOUNT } from "../../lib/consts.js"; +import { SwankyCommand } from "../../lib/swankyCommand.js"; +import { ApiError } from "../../lib/errors.js"; + +export abstract class SwankyAccountCommand extends SwankyCommand { + async performFaucetTransfer(accountData: AccountData, canBeSkipped = false) { + let api: ChainApi | null = null; + try { + api = (await this.spinner.runCommand(async () => { + const networkUrl = resolveNetworkUrl(this.swankyConfig, ""); + const api = await ChainApi.create(networkUrl); + await api.start(); + return api; + }, "Connecting to node")) as ChainApi; + + if (api) + await this.spinner.runCommand( + async () => { + if (api) await api.faucet(accountData); + }, + `Transferring ${LOCAL_FAUCET_AMOUNT} units from faucet account to ${accountData.alias}`, + `Transferred ${LOCAL_FAUCET_AMOUNT} units from faucet account to ${accountData.alias}`, + `Failed to transfer ${LOCAL_FAUCET_AMOUNT} units from faucet account to ${accountData.alias}`, + true + ); + } catch (cause) { + if (cause instanceof Error) { + if (cause.message.includes('ECONNREFUSED') && canBeSkipped) { + this.warn(`Unable to connect to the node. Skipping faucet transfer for ${chalk.yellowBright(accountData.alias)}.`); + } else { + throw new ApiError("Error transferring tokens from faucet account", { cause }); + } + } else { + throw new ApiError("An unknown error occurred during faucet transfer", { cause: new Error(String(cause)) }); + } + } finally { + if (api) { + await api.disconnect(); + } + } + } +} diff --git a/src/commands/contract/deploy.ts b/src/commands/contract/deploy.ts index 7b2e5482..a0b2ca0a 100644 --- a/src/commands/contract/deploy.ts +++ b/src/commands/contract/deploy.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { writeJSON } from "fs-extra/esm"; import { cryptoWaitReady } from "@polkadot/util-crypto/crypto"; import { resolveNetworkUrl, ChainApi, ChainAccount, decrypt, AbiType } from "../../lib/index.js"; -import { AccountData, Encrypted } from "../../types/index.js"; +import { Encrypted } from "../../types/index.js"; import inquirer from "inquirer"; import chalk from "chalk"; import { Contract } from "../../lib/contract.js"; @@ -70,13 +70,7 @@ export class DeployContract extends SwankyCommand { ); } - const accountData = this.swankyConfig.accounts.find( - (account: AccountData) => account.alias === flags.account - ); - if (!accountData) { - throw new ConfigError("Provided account alias not found in swanky.config.json"); - } - + const accountData = this.findAccountByAlias(flags.account); const mnemonic = accountData.isDev ? (accountData.mnemonic as string) : decrypt( diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index e7141b40..21712eba 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -18,6 +18,7 @@ import { getTemplates, swankyNodeVersions, } from "../../lib/index.js"; import { + ALICE_URI, BOB_URI, DEFAULT_ASTAR_NETWORK_URL, DEFAULT_NETWORK_URL, DEFAULT_NODE_INFO, DEFAULT_SHIBUYA_NETWORK_URL, @@ -191,15 +192,15 @@ export class Init extends SwankyCommand { this.configBuilder.accounts = [ { alias: "alice", - mnemonic: "//Alice", + mnemonic: ALICE_URI, isDev: true, - address: new ChainAccount("//Alice").pair.address, + address: new ChainAccount(ALICE_URI).pair.address, }, { alias: "bob", - mnemonic: "//Bob", + mnemonic: BOB_URI, isDev: true, - address: new ChainAccount("//Bob").pair.address, + address: new ChainAccount(BOB_URI).pair.address, }, ]; diff --git a/src/lib/account.ts b/src/lib/account.ts index 04897451..dc529cb0 100644 --- a/src/lib/account.ts +++ b/src/lib/account.ts @@ -2,6 +2,7 @@ import { mnemonicGenerate } from "@polkadot/util-crypto"; import { Keyring } from "@polkadot/keyring"; import { KeyringPair } from "@polkadot/keyring/types"; import { ChainProperty, KeypairType } from "../types/index.js"; +import { KEYPAIR_TYPE } from "./consts.js"; interface IChainAccount { pair: KeyringPair; @@ -17,7 +18,7 @@ export class ChainAccount implements IChainAccount { return mnemonicGenerate(); } - constructor(mnemonic: string, type: KeypairType = "sr25519") { + constructor(mnemonic: string, type: KeypairType = KEYPAIR_TYPE) { this._keyringType = type; this._keyring = new Keyring({ type: type }); this._mnemonic = mnemonic; diff --git a/src/lib/consts.ts b/src/lib/consts.ts index ed9e063b..b9391283 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -1,5 +1,7 @@ import { swankyNodeVersions } from "./nodeInfo.js"; +export const DEFAULT_NODE_INFO = swankyNodeVersions.get("1.6.0")!; + export const DEFAULT_NETWORK_URL = "ws://127.0.0.1:9944"; export const DEFAULT_ASTAR_NETWORK_URL = "wss://rpc.astar.network"; export const DEFAULT_SHIDEN_NETWORK_URL = "wss://rpc.shiden.astar.network"; @@ -8,4 +10,7 @@ export const DEFAULT_SHIBUYA_NETWORK_URL = "wss://shibuya.public.blastapi.io"; export const ARTIFACTS_PATH = "artifacts"; export const TYPED_CONTRACTS_PATH = "typedContracts"; -export const DEFAULT_NODE_INFO = swankyNodeVersions.get("1.6.0")!; +export const LOCAL_FAUCET_AMOUNT = 100; +export const KEYPAIR_TYPE = "sr25519"; +export const ALICE_URI = "//Alice"; +export const BOB_URI = "//Bob"; diff --git a/src/lib/contractCall.ts b/src/lib/contractCall.ts index 246e97d0..9c9e0b2f 100644 --- a/src/lib/contractCall.ts +++ b/src/lib/contractCall.ts @@ -1,5 +1,5 @@ import { AbiType, ChainAccount, ChainApi, decrypt, resolveNetworkUrl } from "./index.js"; -import { AccountData, ContractData, DeploymentData, Encrypted } from "../types/index.js"; +import { ContractData, DeploymentData, Encrypted } from "../types/index.js"; import { Args, Command, Flags, Interfaces } from "@oclif/core"; import inquirer from "inquirer"; import chalk from "chalk"; @@ -77,13 +77,7 @@ export abstract class ContractCall extends SwankyComma this.deploymentInfo = deploymentData; - const accountData = this.swankyConfig.accounts.find( - (account: AccountData) => account.alias === flags.account || "alice" - ); - if (!accountData) { - throw new ConfigError("Provided account alias not found in swanky.config.json"); - } - + const accountData = this.findAccountByAlias(flags.account || "alice"); const networkUrl = resolveNetworkUrl(this.swankyConfig, flags.network ?? ""); const api = await ChainApi.create(networkUrl); this.api = api; diff --git a/src/lib/substrate-api.ts b/src/lib/substrate-api.ts index 343d35f4..42bef37c 100644 --- a/src/lib/substrate-api.ts +++ b/src/lib/substrate-api.ts @@ -1,5 +1,5 @@ import { ApiPromise } from "@polkadot/api/promise"; -import { WsProvider } from "@polkadot/api"; +import { Keyring, WsProvider } from "@polkadot/api"; import { SignerOptions } from "@polkadot/api/types"; import { Codec, ITuple } from "@polkadot/types-codec/types"; import { ISubmittableResult } from "@polkadot/types/types"; @@ -7,11 +7,13 @@ import { TypeRegistry } from "@polkadot/types"; import { DispatchError, BlockHash } from "@polkadot/types/interfaces"; import { ChainAccount } from "./account.js"; import BN from "bn.js"; -import { ChainProperty, ExtrinsicPayload } from "../types/index.js"; +import { ChainProperty, ExtrinsicPayload, AccountData } from "../types/index.js"; import { KeyringPair } from "@polkadot/keyring/types"; import { Abi, CodePromise } from "@polkadot/api-contract"; import { ApiError, UnknownError } from "./errors.js"; +import { ALICE_URI, KEYPAIR_TYPE, LOCAL_FAUCET_AMOUNT } from "./consts.js"; +import { BN_TEN } from "@polkadot/util"; export type AbiType = Abi; // const AUTO_CONNECT_MS = 10_000; // [ms] @@ -101,6 +103,10 @@ export class ChainApi { return this._registry; } + public async disconnect(): Promise { + await this._provider.disconnect(); + } + public async start(): Promise { const chainProperties = await this._api.rpc.system.properties(); @@ -210,7 +216,6 @@ export class ChainApi { if (handler) handler(result); }); } - public async deploy( abi: Abi, wasm: Buffer, @@ -247,4 +252,27 @@ export class ChainApi { }); }); } + + public async faucet(accountData: AccountData): Promise { + const keyring = new Keyring({ type: KEYPAIR_TYPE }); + const alicePair = keyring.addFromUri(ALICE_URI); + + const chainDecimals = this._api.registry.chainDecimals[0]; + const amount = new BN(LOCAL_FAUCET_AMOUNT).mul(BN_TEN.pow(new BN(chainDecimals))); + + const tx = this._api.tx.balances.transfer(accountData.address, amount); + + return new Promise((resolve, reject) => { + this.signAndSend(alicePair, tx, {}, ({ status, events }) => { + if (status.isInBlock || status.isFinalized) { + const transferEvent = events.find(({ event }) => event?.method === "Transfer"); + if (!transferEvent) { + reject(); + return; + } + resolve(); + } + }).catch((error) => reject(error)); + }); + } } diff --git a/src/lib/swankyCommand.ts b/src/lib/swankyCommand.ts index b2d6c625..4f2b5a73 100644 --- a/src/lib/swankyCommand.ts +++ b/src/lib/swankyCommand.ts @@ -1,6 +1,7 @@ import { Command, Flags, Interfaces } from "@oclif/core"; +import chalk from "chalk"; import { getSwankyConfig, Spinner } from "./index.js"; -import { SwankyConfig } from "../types/index.js"; +import { AccountData, SwankyConfig } from "../types/index.js"; import { writeJSON } from "fs-extra/esm"; import { BaseError, ConfigError, UnknownError } from "./errors.js"; import { swankyLogger } from "./logger.js"; @@ -51,6 +52,18 @@ export abstract class SwankyCommand extends Command { Full command: ${JSON.stringify(process.argv)}`); } + protected findAccountByAlias(alias: string): AccountData { + const accountData = this.swankyConfig.accounts.find( + (account: AccountData) => account.alias === alias + ); + + if (!accountData) { + throw new ConfigError(`Provided account alias ${chalk.yellowBright(alias)} not found in swanky.config.json`); + } + + return accountData; + } + protected async storeConfig() { await writeJSON("swanky.config.json", this.swankyConfig, { spaces: 2 }); }