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

balance testing #130

Merged
merged 13 commits into from
Jun 24, 2024
33 changes: 33 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
name: Build, lint, test
run-name: Test JS CLI (started by @${{ github.triggering_actor }})

on:
pull_request:
types:
- opened
- synchronize
- reopened

jobs:
build_test_lint:
name: Build, test, and lint
permissions:
contents: read
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.10.0
- name: Install
run: yarn --network-timeout 180000
- name: Typecheck
run: yarn run test:types
- name: Build
run: yarn run build
- name: Add TSS server host mappings
run: |
echo "127.0.0.1 alice-tss-server bob-tss-server" | sudo tee -a /etc/hosts
- name: Test
run: yarn run test
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil
## [UNRELEASED]

### Added

- new: './src/flows/balance/balance.ts' - service file separated out of main flow containing the pure functions to perform balance requests for one or multiple addresses
- new: './tests/balance.test.ts' - new unit tests file for balance pure functions
### Fixed

- keyring retrieval method was incorrectly returning the default keyring when no keyring was found, which is not the intended flow
### Changed

- conditional when initializing entropy object to only error if no seed AND admin account is not found in the account data, new unit test caught bug with using OR condition
### Broke

### Meta/Dev
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
},
"homepage": "https://github.com/entropyxyz/cli#readme",
"dependencies": {
"@entropyxyz/sdk": "^0.2.1",
"@entropyxyz/sdk": "^0.2.2-0",
"@polkadot/util": "^12.6.2",
"@types/node": "^20.12.12",
"ansi-colors": "^4.1.3",
Expand All @@ -67,6 +67,7 @@
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"pinst": "^3.0.0",
"readline": "^1.3.0",
"regenerator-runtime": "^0.14.1",
"tap-spec": "^5.0.0",
"tape": "^5.7.5",
Expand Down
8 changes: 5 additions & 3 deletions src/common/initializeEntropy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const keyrings = {
export function getKeyring (address) {
if (!address && keyrings.default) return keyrings.default
if (address && keyrings[address]) return keyrings[address]
return keyrings.default
// explicitly return undefined so there is no confusion around what is selected
return undefined
rh0delta marked this conversation as resolved.
Show resolved Hide resolved
}

interface InitializeEntropyOpts {
Expand All @@ -38,7 +39,8 @@ export const initializeEntropy = async ({ keyMaterial, password, endpoint }: Ini
await wasmGlobalsReady()

const { accountData, password: successfulPassword } = await getAccountDataAndPassword(keyMaterial, password)
if (!accountData.seed || !accountData.admin) {
// check if there is no admin account and no seed so that we can throw an error
if (!accountData.seed && !accountData.admin) {
rh0delta marked this conversation as resolved.
Show resolved Hide resolved
throw new Error("Data format is not recognized as either encrypted or unencrypted")
}

Expand All @@ -63,7 +65,7 @@ export const initializeEntropy = async ({ keyMaterial, password, endpoint }: Ini
}

let selectedAccount
const storedKeyring = getKeyring(accountData.admin.address)
const storedKeyring = getKeyring(accountData?.admin?.address)
rh0delta marked this conversation as resolved.
Show resolved Hide resolved

if(!storedKeyring) {
const keyring = new Keyring({ ...accountData, debug: true })
Expand Down
36 changes: 36 additions & 0 deletions src/flows/balance/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Entropy from "@entropyxyz/sdk";
import { BalanceInfo } from "./types";

const hexToBigInt = (hexString: string) => BigInt(hexString)

export async function getBalance (entropy: Entropy, address: string): Promise<number> {
try {
const accountInfo = (await entropy.substrate.query.system.account(address)) as any

return parseInt(hexToBigInt(accountInfo.data.free).toString())
} catch (error) {
// console.error(`There was an error getting balance for [acct = ${address}]`, error);
throw new Error(error.message)
}
}

export async function getBalances (entropy: Entropy, addresses: string[]): Promise<BalanceInfo> {
const balanceInfo: BalanceInfo = {}
try {
await Promise.all(addresses.map(async address => {
try {
const balance = await getBalance(entropy, address)

balanceInfo[address] = { balance }
} catch (error) {
// console.error(`Error retrieving balance for ${address}`, error);
balanceInfo[address] = { error: error.message }
}
}))

return balanceInfo
} catch (error) {
// console.error(`There was an error getting balances for [${addresses}]`, error);
throw new Error(error.message)
}
}
9 changes: 3 additions & 6 deletions src/flows/balance/cli.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { initializeEntropy } from '../../common/initializeEntropy'
import * as config from '../../config'
import { debug } from '../../common/utils'

const hexToBigInt = (hexString: string) => BigInt(hexString)
import { getBalance } from './balance'

export async function cliGetBalance ({ address, password, endpoint }) {
const storedConfig = await config.get()
Expand All @@ -17,10 +16,8 @@ export async function cliGetBalance ({ address, password, endpoint }) {
}

const entropy = await initializeEntropy({ keyMaterial: account.data, password, endpoint })
const balance = await getBalance(entropy, address)

const accountInfo = (await entropy.substrate.query.system.account(address)) as any
debug('accountInfo', accountInfo)

return hexToBigInt(accountInfo.data.free).toString()
return balance
}

10 changes: 4 additions & 6 deletions src/flows/balance/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { initializeEntropy } from "../../common/initializeEntropy"
import { print, debug, getSelectedAccount } from "../../common/utils"
import { getBalance } from "./balance";

const hexToBigInt = (hexString: string) => BigInt(hexString)


// TO-DO setup flow method to provide options to allow users to select account,
// use external address, or get balances for all accounts in config
export async function checkBalance ({ accounts, selectedAccount: selectedAccountAddress }, options) {
const { endpoint } = options
debug('endpoint', endpoint);

const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress)
const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint });
const accountAddress = selectedAccountAddress
// @ts-ignore
const accountInfo = (await entropy.substrate.query.system.account(accountAddress)) as any
const freeBalance = hexToBigInt(accountInfo.data.free)
const freeBalance = await getBalance(entropy, accountAddress)
print(`Address ${accountAddress} has a balance of: ${freeBalance.toLocaleString('en-US')} BITS`)
}
8 changes: 8 additions & 0 deletions src/flows/balance/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type BalanceInfoWithError = {
balance?: number
error?: Error
}

export interface BalanceInfo {
[address: string]: BalanceInfoWithError
}
4 changes: 2 additions & 2 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ export interface EntropyAccountConfig {
}

export interface EntropyAccountData {
debug: boolean
debug?: boolean
seed: string
admin: EntropyAccount
admin?: EntropyAccount
registration?: EntropyAccount
deviceKey?: EntropyAccount
programDev?: EntropyAccount
Expand Down
85 changes: 85 additions & 0 deletions tests/balance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import test from 'tape'
import Entropy, { wasmGlobalsReady } from '@entropyxyz/sdk'
// WIP: I'm seeing problems importing this?
import Keyring from '@entropyxyz/sdk/dist/keys/index'
import {
makeSeed,
promiseRunner,
sleep,
spinNetworkUp,
spinNetworkDown
} from './testing-utils'

import { getBalance, getBalances } from '../src/flows/balance/balance'
import { initializeEntropy } from 'src/common/initializeEntropy'

const networkType = 'two-nodes'

// TODO: export charlieStashSeed
const richAddress = '5Ck5SLSHYac6WFt5UZRSsdJjwmpSZq85fd5TRNAdZQVzEAPT'

test('getBalance + getBalances', async (t) => {
/* Setup */
const run = promiseRunner(t)
await run('wasm', wasmGlobalsReady())
await run('network up', spinNetworkUp(networkType))
// this gets called after all tests are run
t.teardown(async () => {
await entropy.close()
await spinNetworkDown(networkType).catch((error) =>
console.error('Error while spinning network down', error.message)
)
})
await sleep(process.env.GITHUB_WORKSPACE ? 30_000 : 5_000)

const newSeed = makeSeed()
const entropy = await initializeEntropy({ keyMaterial: { seed: newSeed, debug: true }, endpoint: 'ws://127.0.0.1:9944', })
const newAddress = entropy.keyring.accounts.registration.address

await run('entropy ready', entropy.ready)

/* getBalance */
const newAddressBalance = await run(
'getBalance (newSeed)',
getBalance(entropy, newAddress)
)

t.equal(newAddressBalance, 0, 'newSeed balance = 0')

const richAddressBalance = await run(
'getBalance (richAddress)',
getBalance(entropy, richAddress)
)

t.true(richAddressBalance > BigInt(10e10), 'richAddress balance >>> 0')

/* getBalances */

const balances = await run(
'getBalances',
getBalances(entropy, [newAddress, richAddress])
)

t.deepEqual(
balances,
{
[newAddress]: {balance: newAddressBalance},
[richAddress]: {balance: richAddressBalance}
},
'getBalances works'
)

const badAddresses = ['5Cz6BfUaxxXCA3jninzxdan4JdmC1NVpgkiRPYhXbhr', '5Cz6BfUaxxXCA3jninzxdan4JdmC1NVpgkiRPYhXbhrfnD']
const balancesWithNoGoodAddress = await run(
'getBalances::one good address',
getBalances(entropy, badAddresses)
)

badAddresses.forEach(addr => {
t.true(!!balancesWithNoGoodAddress[addr].error, `error field is populated for ${addr}`)
})
// TODO:
// - test getBalances with 1 good address, 1 bung seed

t.end()
})
93 changes: 93 additions & 0 deletions tests/testing-utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { spinNetworkUp, spinNetworkDown, } from "@entropyxyz/sdk/dev/testing-utils.mjs"
// import { spinNetworkUp, spinNetworkDown, } from "@entropyxyz/sdk/testing"
import * as readline from 'readline'
import { randomBytes } from 'crypto'

export {
spinNetworkUp,
spinNetworkDown,
}

/* Helper for wrapping promises which makes it super clear in logging if the promise
* resolves or threw.
*
* @param {any} t - an instance to tape runner
* @param {boolean} keepThrowing - toggle throwing
*/
export function promiseRunner(t: any, keepThrowing = false) {
// NOTE: this function swallows errors
return async function run(
message: string,
promise: Promise<any>
): Promise<any> {
if (promise.constructor !== Promise) {
t.pass(message)
return Promise.resolve(promise)
}

const startTime = Date.now()
return promise
.then((result) => {
const time = (Date.now() - startTime) / 1000
const pad = Array(40 - message.length)
.fill('-')
.join('')
t.pass(`${message} ${pad} ${time}s`)
return result
})
.catch((err) => {
t.error(err, message)
if (keepThrowing) throw err
})
}
}

const SLEEP_DONE = '▓'
const SLEEP_TODO = '░'

export function sleep(durationInMs: number) {
return new Promise((resolve) => {
let count = 0

readline.cursorTo(process.stdout, 2)

const steps = Math.min(Math.round(durationInMs / 1000), 80)
const stepLength = durationInMs / steps

console.log('') // write blank link to overwrite
const interval = setInterval(step, stepLength)

function step() {
count++

if (count >= steps) {
clearInterval(interval)

undoLastLine()
console.log(`sleep (${durationInMs / 1000}s)`)
resolve('DONE')
return
}

undoLastLine()
console.log(
[
'sleep ',
...Array(count).fill(SLEEP_DONE),
...Array(steps - count).fill(SLEEP_TODO),
'\n',
].join('')
)
}
})
}
function undoLastLine() {
readline.moveCursor(process.stdout, 0, -1)
readline.cursorTo(process.stdout, 0)
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 4) // indent
}

export function makeSeed () {
return '0x' + randomBytes(32).toString('hex')
}
Loading