diff --git a/CHANGELOG.md b/CHANGELOG.md index f982d02b..f00817ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,16 +27,21 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil - new: 'src/flows/user-program-management/view.ts' - service file for pure functions of viewing user programs - new: 'src/flows/user-program-management/helpers/utils.ts' - utility helper file for user program management specific methods - new: './src/flows/user-program-management/remove.ts' - service file for removing user program pure function -- new: './src/common/base-commands.ts' - base abstract class for new command classes +- new: './src/common/entropy-base.ts' - base abstract class for new command classes - new: './src/balance' - new file structure for our CLI/TUI flows - - new: './src/balance/command.ts' - main entry file for balance command for tui/cli + - new: './src/balance/main.ts' - main entry file for balance command for tui/cli - new: './src/balance/utils.ts' - utilities and helper methods for all things balance - new: './src/transfer' - new file structure for our CLI/TUI flows - - new: './src/transfer/command.ts' - main entry file for transfer command for tui/cli + - new: './src/transfer/main.ts' - main entry file for transfer command for tui/cli - new: './src/transfer/utils.ts' - utilities and helper methods for all things transfer -- new: './src/accounts' - new file structure for our CLI/TUI flows - - new: './src/accounts/command.ts' - main entry file for accounts command for tui/cli - - new: './src/accounts/utils.ts' - utilities and helper methods for all things accounts +- new: './src/account' - new file structure for our CLI/TUI flows + - new: './src/account/main.ts' - main entry file for accounts command for tui/cli + - new: './src/account/utils.ts' - utilities and helper methods for all things accounts +- new: './src/faucet' - new file structure for our CLI/TUI flows + - new: './src/faucet/main.ts' - main entry file for faucet methods used for command and interaction files + - new: './src/faucet/utils.ts' - utilities and helper methods for all things faucet + - new: './src/faucet/interaction.ts' - main entrypoint for TUI flows + - new: './src/faucet/command.ts' - main entrypoint for CLI flows ### Changed @@ -48,6 +53,7 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil - removed flows/entropyTransfer/*.ts directory with file restructure - removed flows/manage-accounts/*/*.ts directory with file restructure - removed flows/register/*.ts directory with file restructure +- removed flow/entropyFaucet/*.ts directory with file restructure ### Broke diff --git a/src/balance/command.ts b/src/balance/command.ts index 2ff7daa5..4d0b023a 100644 --- a/src/balance/command.ts +++ b/src/balance/command.ts @@ -1,4 +1,5 @@ import { Command } from "commander"; +import Entropy from "@entropyxyz/sdk"; import { cliWrite, endpointOption, loadEntropy, passwordOption } from "src/common/utils-cli"; import { EntropyBalance } from "./main"; @@ -10,7 +11,7 @@ export function entropyBalanceCommand () { .addOption(passwordOption()) .addOption(endpointOption()) .action(async (address, opts) => { - const entropy = await loadEntropy(address, opts.endpoint) + const entropy: Entropy = await loadEntropy(address, opts.endpoint) const BalanceService = new EntropyBalance(entropy, opts.endpoint) const balance = await BalanceService.getBalance(address) cliWrite(`${balance.toLocaleString('en-US')} BITS`) diff --git a/src/common/entropy-base.ts b/src/common/entropy-base.ts index 5f604ba5..e24c9479 100644 --- a/src/common/entropy-base.ts +++ b/src/common/entropy-base.ts @@ -4,9 +4,11 @@ import { EntropyLogger } from "./logger"; export abstract class EntropyBase { protected logger: EntropyLogger protected entropy: Entropy + protected endpoint: string constructor ({ entropy, endpoint, flowContext }: { entropy: Entropy, endpoint: string, flowContext: string }) { this.logger = new EntropyLogger(flowContext, endpoint) this.entropy = entropy + this.endpoint = endpoint } } diff --git a/src/flows/entropyFaucet/signer.ts b/src/faucet/helpers/signer.ts similarity index 100% rename from src/flows/entropyFaucet/signer.ts rename to src/faucet/helpers/signer.ts index bb492291..d3a9140a 100644 --- a/src/flows/entropyFaucet/signer.ts +++ b/src/faucet/helpers/signer.ts @@ -2,8 +2,8 @@ import Entropy from "@entropyxyz/sdk"; import type { Signer, SignerResult } from "@polkadot/api/types"; import { Registry, SignerPayloadJSON } from "@polkadot/types/types"; import { u8aToHex } from "@polkadot/util"; -import { stripHexPrefix } from "../../common/utils"; import { blake2AsHex, decodeAddress, encodeAddress, signatureVerify } from "@polkadot/util-crypto"; +import { stripHexPrefix } from "../../common/utils"; let id = 0 export default class FaucetSigner implements Signer { diff --git a/src/faucet/interaction.ts b/src/faucet/interaction.ts new file mode 100644 index 00000000..54cc647b --- /dev/null +++ b/src/faucet/interaction.ts @@ -0,0 +1,44 @@ +import Entropy from "@entropyxyz/sdk" +import { EntropyLogger } from '../common/logger' +import { TESTNET_PROGRAM_HASH } from "./utils" +import { EntropyFaucet } from "./main" +import { print } from "src/common/utils" + +let chosenVerifyingKeys = [] +const amount = "10000000000" +// context for logging file +const FLOW_CONTEXT = 'ENTROPY_FAUCET_INTERACTION' +export async function entropyFaucet (entropy: Entropy, options, logger: EntropyLogger) { + const { endpoint } = options + if (!entropy.registrationManager.signer.pair) { + throw new Error("Keys are undefined") + } + const faucetService = new EntropyFaucet(entropy, endpoint) + const verifyingKeys = await faucetService.getAllFaucetVerifyingKeys() + // @ts-expect-error + return sendMoneyFromRandomFaucet(entropy, options.endpoint, verifyingKeys, logger) +} + +// Method that takes in the initial list of verifying keys (to avoid multiple calls to the rpc) and recursively retries each faucet until +// a successful transfer is made +async function sendMoneyFromRandomFaucet (entropy: Entropy, endpoint: string, verifyingKeys: string[], logger: EntropyLogger) { + const faucetService = new EntropyFaucet(entropy, endpoint) + const selectedAccountAddress = entropy.keyring.accounts.registration.address + const { chosenVerifyingKey, faucetAddress } = faucetService.getRandomFaucet(chosenVerifyingKeys, verifyingKeys) + try { + await faucetService.sendMoney({ amount, addressToSendTo: selectedAccountAddress, faucetAddress, chosenVerifyingKey, faucetProgramPointer: TESTNET_PROGRAM_HASH }) + // reset chosen keys after successful transfer + chosenVerifyingKeys = [] + print(`Account: ${selectedAccountAddress} has been successfully funded with ${parseInt(amount).toLocaleString('en-US')} BITS`) + } catch (error) { + logger.error('Error issuing funds through faucet', error, FLOW_CONTEXT) + chosenVerifyingKeys.push(chosenVerifyingKey) + if (error.message.includes('FaucetError') || chosenVerifyingKeys.length === verifyingKeys.length) { + console.error('ERR::', error.message) + return + } else { + // Check for non faucet errors (FaucetError) and retry faucet + await sendMoneyFromRandomFaucet(entropy, endpoint, verifyingKeys, logger) + } + } +} \ No newline at end of file diff --git a/src/faucet/main.ts b/src/faucet/main.ts new file mode 100644 index 00000000..2933ce35 --- /dev/null +++ b/src/faucet/main.ts @@ -0,0 +1,101 @@ +import Entropy from "@entropyxyz/sdk"; +import { EntropyBase } from "../common/entropy-base"; +import { blake2AsHex, encodeAddress } from "@polkadot/util-crypto"; +import { FAUCET_PROGRAM_MOD_KEY, TESTNET_PROGRAM_HASH } from "./utils"; +import { EntropyBalance } from "src/balance/main"; +import { EntropyProgram } from "src/program/main"; +import FaucetSigner from "./helpers/signer"; +import { SendMoneyParams } from "./types"; + +const FLOW_CONTEXT = 'ENTROPY-FAUCET' + +export class EntropyFaucet extends EntropyBase { + constructor (entropy: Entropy, endpoint: string) { + super({ entropy, endpoint, flowContext: FLOW_CONTEXT }) + } + + // Method used to sign and send the transfer request (transfer request = call argument) using the custom signer + // created to overwrite how we sign the payload that is sent up chain + async faucetSignAndSend (call: any, amount: number, senderAddress: string, chosenVerifyingKey: any): Promise { + const api = this.entropy.substrate + const faucetSigner = new FaucetSigner(api.registry, this.entropy, amount, chosenVerifyingKey) + + const sig = await call.signAsync(senderAddress, { + signer: faucetSigner, + }); + return new Promise((resolve, reject) => { + sig.send(({ status, dispatchError }: any) => { + // status would still be set, but in the case of error we can shortcut + // to just check it (so an error would indicate InBlock or Finalized) + if (dispatchError) { + let msg: string + if (dispatchError.isModule) { + // for module errors, we have the section indexed, lookup + const decoded = api.registry.findMetaError(dispatchError.asModule); + // @ts-ignore + const { documentation, method, section } = decoded; + + msg = `${section}.${method}: ${documentation.join(' ')}` + } else { + // Other, CannotLookup, BadOrigin, no extra info + msg = dispatchError.toString() + } + return reject(Error(msg)) + } + if (status.isFinalized) resolve(status) + }) + }) + } + + async getAllFaucetVerifyingKeys (programModKey = FAUCET_PROGRAM_MOD_KEY) { + const modifiableKeys = await this.entropy.substrate.query.registry.modifiableKeys(programModKey) + return modifiableKeys.toJSON() + } + + // To handle overloading the individual faucet, multiple faucet accounts have been generated, and here is + // where we choose one of those faucet's at random + getRandomFaucet (previousVerifyingKeys: string[] = [], allVerifyingKeys: string[] = []) { + if (allVerifyingKeys.length === previousVerifyingKeys.length) { + throw new Error('FaucetError: There are no more faucets to choose from') + } + let chosenVerifyingKey = allVerifyingKeys[Math.floor(Math.random() * allVerifyingKeys.length)] + if (previousVerifyingKeys.length && previousVerifyingKeys.includes(chosenVerifyingKey)) { + const filteredVerifyingKeys = allVerifyingKeys.filter((key: string) => !previousVerifyingKeys.includes(key)) + chosenVerifyingKey = filteredVerifyingKeys[Math.floor(Math.random() * filteredVerifyingKeys.length)] + } + const hashedKey = blake2AsHex(chosenVerifyingKey) + const faucetAddress = encodeAddress(hashedKey, 42).toString() + + return { chosenVerifyingKey, faucetAddress } + } + + async sendMoney ( + { + amount, + addressToSendTo, + faucetAddress, + chosenVerifyingKey, + faucetProgramPointer = TESTNET_PROGRAM_HASH + }: SendMoneyParams + ): Promise { + const balanceService = new EntropyBalance(this.entropy, this.endpoint) + const programService = new EntropyProgram(this.entropy, this.endpoint) + // check balance of faucet address + const balance = await balanceService.getBalance(faucetAddress) + if (balance <= 0) throw new Error('FundsError: Faucet Account does not have funds') + // check verifying key for only one program matching the program hash + const programs = await programService.list({ verifyingKey: chosenVerifyingKey }) + if (programs.length) { + if (programs.length > 1) throw new Error('ProgramsError: Faucet Account has too many programs attached, expected less') + if (programs.length === 1 && programs[0].program_pointer !== faucetProgramPointer) { + throw new Error('ProgramsError: Faucet Account does not possess Faucet program') + } + } else { + throw new Error('ProgramsError: Faucet Account has no programs attached') + } + + const transfer = this.entropy.substrate.tx.balances.transferAllowDeath(addressToSendTo, BigInt(amount)); + const transferStatus = await this.faucetSignAndSend(transfer, parseInt(amount), faucetAddress, chosenVerifyingKey) + if (transferStatus.isFinalized) return transferStatus + } +} \ No newline at end of file diff --git a/src/faucet/types.ts b/src/faucet/types.ts new file mode 100644 index 00000000..9e874085 --- /dev/null +++ b/src/faucet/types.ts @@ -0,0 +1,7 @@ +export interface SendMoneyParams { + amount: string + addressToSendTo: string + faucetAddress: string + chosenVerifyingKey: string + faucetProgramPointer: string +} \ No newline at end of file diff --git a/src/flows/entropyFaucet/constants.ts b/src/faucet/utils.ts similarity index 100% rename from src/flows/entropyFaucet/constants.ts rename to src/faucet/utils.ts diff --git a/src/flows/entropyFaucet/faucet.ts b/src/flows/entropyFaucet/faucet.ts deleted file mode 100644 index 3e0d0c17..00000000 --- a/src/flows/entropyFaucet/faucet.ts +++ /dev/null @@ -1,98 +0,0 @@ -// check verifying key has the balance and proper program hash - -import Entropy from "@entropyxyz/sdk"; -import { blake2AsHex, encodeAddress } from "@polkadot/util-crypto"; -import { EntropyProgram } from '../../program/main' -import FaucetSigner from "./signer"; -import { FAUCET_PROGRAM_MOD_KEY, TESTNET_PROGRAM_HASH } from "./constants"; -import { EntropyBalance } from "src/balance/main"; - -// only the faucet program should be on the key -async function faucetSignAndSend (call: any, entropy: Entropy, amount: number, senderAddress: string, chosenVerifyingKey: any): Promise { - const api = entropy.substrate - const faucetSigner = new FaucetSigner(api.registry, entropy, amount, chosenVerifyingKey) - - const sig = await call.signAsync(senderAddress, { - signer: faucetSigner, - }); - return new Promise((resolve, reject) => { - sig.send(({ status, dispatchError }: any) => { - // status would still be set, but in the case of error we can shortcut - // to just check it (so an error would indicate InBlock or Finalized) - if (dispatchError) { - let msg: string - if (dispatchError.isModule) { - // for module errors, we have the section indexed, lookup - const decoded = api.registry.findMetaError(dispatchError.asModule); - // @ts-ignore - const { documentation, method, section } = decoded; - - msg = `${section}.${method}: ${documentation.join(' ')}` - } else { - // Other, CannotLookup, BadOrigin, no extra info - msg = dispatchError.toString() - } - return reject(Error(msg)) - } - if (status.isFinalized) resolve(status) - }) - }) -} - -export async function getRandomFaucet (entropy: Entropy, previousVerifyingKeys: string[] = [], programModKey = FAUCET_PROGRAM_MOD_KEY) { - const modifiableKeys = await entropy.substrate.query.registry.modifiableKeys(programModKey) - const verifyingKeys = JSON.parse(JSON.stringify(modifiableKeys.toJSON())) - - // Choosing one of the 5 verifiying keys at random to be used as the faucet sender - if (verifyingKeys.length === previousVerifyingKeys.length) { - throw new Error('FaucetError: There are no more faucets to choose from') - } - let chosenVerifyingKey = verifyingKeys[Math.floor(Math.random() * verifyingKeys.length)] - if (previousVerifyingKeys.length && previousVerifyingKeys.includes(chosenVerifyingKey)) { - const filteredVerifyingKeys = verifyingKeys.filter((key: string) => !previousVerifyingKeys.includes(key)) - chosenVerifyingKey = filteredVerifyingKeys[Math.floor(Math.random() * filteredVerifyingKeys.length)] - } - const hashedKey = blake2AsHex(chosenVerifyingKey) - const faucetAddress = encodeAddress(hashedKey, 42).toString() - - return { chosenVerifyingKey, faucetAddress, verifyingKeys } -} - -export async function sendMoney ( - entropy: Entropy, - endpoint: string, - { - amount, - addressToSendTo, - faucetAddress, - chosenVerifyingKey, - faucetProgramPointer = TESTNET_PROGRAM_HASH - }: { - amount: string, - addressToSendTo: string, - faucetAddress: string, - chosenVerifyingKey: string, - faucetProgramPointer: string - } -): Promise { - const balanceService = new EntropyBalance(entropy, endpoint) - const programService = new EntropyProgram(entropy, endpoint) - - // check balance of faucet address - const balance = await balanceService.getBalance(faucetAddress) - if (balance <= 0) throw new Error('FundsError: Faucet Account does not have funds') - // check verifying key for only one program matching the program hash - const programs = await programService.list({ verifyingKey: chosenVerifyingKey }) - if (programs.length) { - if (programs.length > 1) throw new Error('ProgramsError: Faucet Account has too many programs attached, expected less') - if (programs.length === 1 && programs[0].program_pointer !== faucetProgramPointer) { - throw new Error('ProgramsError: Faucet Account does not possess Faucet program') - } - } else { - throw new Error('ProgramsError: Faucet Account has no programs attached') - } - - const transfer = entropy.substrate.tx.balances.transferAllowDeath(addressToSendTo, BigInt(amount)); - const transferStatus = await faucetSignAndSend(transfer, entropy, parseInt(amount), faucetAddress, chosenVerifyingKey ) - if (transferStatus.isFinalized) return transferStatus -} diff --git a/src/flows/entropyFaucet/index.ts b/src/flows/entropyFaucet/index.ts deleted file mode 100644 index 9d5c4c95..00000000 --- a/src/flows/entropyFaucet/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import Entropy from "@entropyxyz/sdk" -import { findAccountByAddressOrName, print } from "../../common/utils" -import { initializeEntropy } from "../../common/initializeEntropy" -import { EntropyLogger } from '../../common/logger' -import { getRandomFaucet, sendMoney } from "./faucet" -import { TESTNET_PROGRAM_HASH } from "./constants" - -let chosenVerifyingKeys = [] -export async function entropyFaucet ({ accounts, selectedAccount: selectedAccountAddress }, options, logger: EntropyLogger) { - const FLOW_CONTEXT = 'ENTROPY_FAUCET' - let faucetAddress - let chosenVerifyingKey - let entropy: Entropy - let verifyingKeys: string[] = [] - const amount = "10000000000" - const { endpoint } = options - const selectedAccount = findAccountByAddressOrName(accounts, selectedAccountAddress) - logger.log(`selectedAccount::`, FLOW_CONTEXT) - logger.log(selectedAccount, FLOW_CONTEXT) - try { - // @ts-ignore (see TODO on aliceAccount) - entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint }) - - if (!entropy.registrationManager.signer.pair) { - throw new Error("Keys are undefined") - } - - ({ chosenVerifyingKey, faucetAddress, verifyingKeys } = await getRandomFaucet(entropy, chosenVerifyingKeys)) - - await sendMoney(entropy, options.endpoint, { amount, addressToSendTo: selectedAccountAddress, faucetAddress, chosenVerifyingKey, faucetProgramPointer: TESTNET_PROGRAM_HASH }) - // reset chosen keys after successful transfer - chosenVerifyingKeys = [] - print(`Account: ${selectedAccountAddress} has been successfully funded with ${parseInt(amount).toLocaleString('en-US')} BITS`) - } catch (error) { - logger.error('Error issuing funds through faucet', error, FLOW_CONTEXT) - chosenVerifyingKeys.push(chosenVerifyingKey) - if (error.message.includes('FaucetError') || chosenVerifyingKeys.length === verifyingKeys.length) { - console.error('ERR::', error.message) - return - } else { - // Check for non faucet errors (FaucetError) and retry faucet - await entropyFaucet({ accounts, selectedAccount: selectedAccountAddress }, options, logger) - } - } -} diff --git a/src/flows/index.ts b/src/flows/index.ts deleted file mode 100644 index 1c2277d1..00000000 --- a/src/flows/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { entropyFaucet } from './entropyFaucet' diff --git a/src/tui.ts b/src/tui.ts index 85e3fc1a..ef6c2279 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,7 +1,6 @@ import inquirer from 'inquirer' import Entropy from '@entropyxyz/sdk' import * as config from './config' -import * as flows from './flows' import { EntropyTuiOptions } from './types' import { logo } from './common/ascii' import { print } from './common/utils' @@ -12,6 +11,7 @@ import { entropyAccount, entropyRegister } from './account/interaction' import { entropySign } from './sign/interaction' import { entropyBalance } from './balance/interaction' import { entropyTransfer } from './transfer/interaction' +import { entropyFaucet } from './faucet/interaction' import { entropyProgram, entropyProgramDev } from './program/interaction' async function setupConfig () { @@ -46,14 +46,13 @@ export default function tui (entropy: Entropy, options: EntropyTuiOptions) { // TODO: design programs in TUI (merge deploy+user programs) 'Deploy Program': () => {}, 'User Programs': () => {}, - 'Entropy Faucet': flows.entropyFaucet, } - // const devChoices = { - // // 'Entropy Faucet': flows.entropyFaucet, - // } + const devChoices = { + 'Entropy Faucet': () => {}, + } - // if (options.dev) Object.assign(choices, devChoices) + if (options.dev) Object.assign(choices, devChoices) // assign exit so its last Object.assign(choices, { 'Exit': async () => {} }) @@ -116,6 +115,14 @@ async function main (entropy: Entropy, choices, options, logger: EntropyLogger) .catch(err => console.error('There was an issue with signing', err)) break } + case 'Entropy Faucet': { + try { + await entropyFaucet(entropy, options, logger) + } catch (error) { + console.error('There was an issue with running the faucet', error); + } + break + } case 'User Programs': { await entropyProgram(entropy, options.endpoint) .catch(err => console.error('There was an error with programs', err)) diff --git a/tests/faucet.test.ts b/tests/faucet.test.ts index 08c37eb9..3a151fe8 100644 --- a/tests/faucet.test.ts +++ b/tests/faucet.test.ts @@ -4,17 +4,18 @@ import { stripHexPrefix } from '../src/common/utils' import { readFileSync } from 'fs' import { EntropyBalance } from '../src/balance/main' import { EntropyTransfer } from '../src/transfer/main' -import { getRandomFaucet, sendMoney } from '../src/flows/entropyFaucet/faucet' -import { LOCAL_PROGRAM_HASH } from '../src/flows/entropyFaucet/constants' +import { EntropyFaucet } from '../src/faucet/main' +import { LOCAL_PROGRAM_HASH } from '../src/faucet/utils' import { EntropyAccount } from '../src/account/main' test('Faucet Tests', async t => { const { run, entropy, endpoint } = await setupTest(t, { seed: charlieStashSeed }) const { entropy: naynayEntropy } = await setupTest(t) - const accountService = new EntropyAccount(entropy, endpoint) const balanceService = new EntropyBalance(entropy, endpoint) const transferService = new EntropyTransfer(entropy, endpoint) + const faucetService = new EntropyFaucet(naynayEntropy, endpoint) + const accountService = new EntropyAccount(entropy, endpoint) const faucetProgram = readFileSync('tests/programs/faucet_program.wasm') @@ -52,8 +53,9 @@ test('Faucet Tests', async t => { programData: [{ program_pointer: faucetProgramPointer, program_config: userConfig }] } )) - - const { chosenVerifyingKey, faucetAddress } = await getRandomFaucet(entropy, [], entropy.keyring.accounts.registration.address) + const verifyingKeys = await faucetService.getAllFaucetVerifyingKeys(entropy.keyring.accounts.registration.address) + // @ts-expect-error + const { chosenVerifyingKey, faucetAddress } = faucetService.getRandomFaucet([], verifyingKeys) // adding funds to faucet address entropyBalance = await balanceService.getBalance(entropy.keyring.accounts.registration.address) const faucetAddressBalance = await balanceService.getBalance(faucetAddress) @@ -63,9 +65,7 @@ test('Faucet Tests', async t => { await run('Transfer funds to faucet address', transferService.transfer(faucetAddress, "1000")) - const transferStatus = await sendMoney( - naynayEntropy, - endpoint, + const transferStatus = await faucetService.sendMoney( { amount: "10000000000", addressToSendTo: naynayEntropy.keyring.accounts.registration.address,