Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faucet feature for SwankyNode #192

Merged
merged 11 commits into from
Feb 12, 2024
63 changes: 63 additions & 0 deletions src/commands/account/balance.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Balance> {
static description = "Balance of an account";

static args = {
alias: Args.string({
name: "alias",
description: "Alias of account to be used",
}),
};
async run(): Promise<void> {
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 <YourAliasAccount>`"
);
}

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<AccountInfo>(
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();
}
}
9 changes: 6 additions & 3 deletions src/commands/account/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof CreateAccount> {
import { SwankyAccountCommand } from "./swankyAccountCommands.js";

export class CreateAccount extends SwankyAccountCommand<typeof CreateAccount> {
static description = "Create a new dev account in config";

static flags = {
Expand Down Expand Up @@ -35,7 +36,7 @@ export class CreateAccount extends SwankyCommand<typeof CreateAccount> {
);
}

let tmpMnemonic = "";
let tmpMnemonic: string;
if (flags.generate) {
tmpMnemonic = ChainAccount.generate();
console.log(
Expand Down Expand Up @@ -84,5 +85,7 @@ export class CreateAccount extends SwankyCommand<typeof CreateAccount> {
accountData.alias
)} stored to config`
);

await this.performFaucetTransfer(accountData, true);
}
pmikolajczyk41 marked this conversation as resolved.
Show resolved Hide resolved
}
23 changes: 23 additions & 0 deletions src/commands/account/faucet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Args } from "@oclif/core";
import { SwankyAccountCommand } from "./swankyAccountCommands.js";

export class Faucet extends SwankyAccountCommand<typeof Faucet> {
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<void> {
const { args } = await this.parse(Faucet);

const accountData = this.findAccountByAlias(args.alias);
await this.performFaucetTransfer(accountData);
}
}
45 changes: 45 additions & 0 deletions src/commands/account/swankyAccountCommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Command } from "@oclif/core";
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ipapandinas Why is that class here? Isn't it more logical to put it into src/lib/..? When we use swanky account --help oclif thinks that it is a command.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea of having SwankyAccountCommand is to encapsulate common methods for account commands by inheriting this abstract class. But you are right we should move it, otherwise Oclif discovers it as a command.

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<T extends typeof Command> extends SwankyCommand<T> {
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();
}
}
}
}
10 changes: 2 additions & 8 deletions src/commands/contract/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -70,13 +70,7 @@ export class DeployContract extends SwankyCommand<typeof DeployContract> {
);
}

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(
Expand Down
9 changes: 5 additions & 4 deletions src/commands/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -191,15 +192,15 @@ export class Init extends SwankyCommand<typeof Init> {
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,
},
];

Expand Down
3 changes: 2 additions & 1 deletion src/lib/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/lib/consts.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
10 changes: 2 additions & 8 deletions src/lib/contractCall.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -77,13 +77,7 @@ export abstract class ContractCall<T extends typeof Command> 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;
Expand Down
34 changes: 31 additions & 3 deletions src/lib/substrate-api.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
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";
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]
Expand Down Expand Up @@ -101,6 +103,10 @@ export class ChainApi {
return this._registry;
}

public async disconnect(): Promise<void> {
await this._provider.disconnect();
}

public async start(): Promise<void> {
const chainProperties = await this._api.rpc.system.properties();

Expand Down Expand Up @@ -210,7 +216,6 @@ export class ChainApi {
if (handler) handler(result);
});
}

public async deploy(
abi: Abi,
wasm: Buffer,
Expand Down Expand Up @@ -247,4 +252,27 @@ export class ChainApi {
});
});
}

public async faucet(accountData: AccountData): Promise<void> {
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) {
ipapandinas marked this conversation as resolved.
Show resolved Hide resolved
const transferEvent = events.find(({ event }) => event?.method === "Transfer");
if (!transferEvent) {
reject();
return;
}
resolve();
}
}).catch((error) => reject(error));
});
}
}
15 changes: 14 additions & 1 deletion src/lib/swankyCommand.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -51,6 +52,18 @@ export abstract class SwankyCommand<T extends typeof Command> 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 });
}
Expand Down
Loading