Skip to content

Commit

Permalink
balance testing (#130)
Browse files Browse the repository at this point in the history
* wip: created pure functions for balance

* WIP: add testing infra for unit tests

* wip: got tests to pass, adding new ones

* added test for all bad addresses

* updated user flows to use the new pure function to grab balance

* updated changelog

* Update src/common/initializeEntropy.ts

Co-authored-by: Frankie <frankie.diamond@gmail.com>

* Update src/common/initializeEntropy.ts

Co-authored-by: Frankie <frankie.diamond@gmail.com>

* adding actions

* updated rc for sdk package

* removing docker hub from actions

---------

Co-authored-by: Nayyir Jutha <nayyir@entropy.xyz>
Co-authored-by: Nayyir Jutha <nayyir.jutha@gmail.com>
Co-authored-by: Frankie <frankie.diamond@gmail.com>
  • Loading branch information
4 people authored Jun 24, 2024
1 parent 93c3d05 commit 47ae2ca
Show file tree
Hide file tree
Showing 13 changed files with 289 additions and 26 deletions.
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
}

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) {
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)

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

0 comments on commit 47ae2ca

Please sign in to comment.