diff --git a/packages/auto-consensus/__test__/balances.test.ts b/packages/auto-consensus/__test__/balances.test.ts index 440903e9..56fb491c 100644 --- a/packages/auto-consensus/__test__/balances.test.ts +++ b/packages/auto-consensus/__test__/balances.test.ts @@ -1,37 +1,11 @@ -import type { NetworkInput } from '@autonomys/auto-utils' -import { - ActivateWalletInput, - activate, - activateWallet, - disconnect, - networks, -} from '@autonomys/auto-utils' +import { ActivateWalletInput, activateWallet } from '@autonomys/auto-utils' import { address } from '../src/address' import { balance, totalIssuance } from '../src/balances' +import { setup } from './helpers' describe('Verify balances functions', () => { - const isLocalhost = process.env.LOCALHOST === 'true' - - // Define the test network and its details - const TEST_NETWORK: NetworkInput = !isLocalhost - ? { networkId: networks[0].id } - : { networkId: 'autonomys-localhost' } - const TEST_INVALID_NETWORK = { networkId: 'invalid-network' } - - const TEST_MNEMONIC = 'test test test test test test test test test test test junk' - const TEST_ADDRESS = '5GmS1wtCfR4tK5SSgnZbVT4kYw5W8NmxmijcsxCQE6oLW6A8' - const ALICE_URI = '//Alice' - const ALICE_ADDRESS = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' - const BOB_URI = '//Bob' - const BOB_ADDRESS = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty' - - beforeAll(async () => { - await activate(TEST_NETWORK) - }) - - afterAll(async () => { - await disconnect() - }) + const { isLocalhost, TEST_NETWORK, TEST_MNEMONIC, TEST_ADDRESS, ALICE_URI, ALICE_ADDRESS } = + setup() describe('Test totalIssuance()', () => { test('Check totalIssuance return a number greater than zero', async () => { diff --git a/packages/auto-consensus/__test__/helpers/events.ts b/packages/auto-consensus/__test__/helpers/events.ts new file mode 100644 index 00000000..548afce7 --- /dev/null +++ b/packages/auto-consensus/__test__/helpers/events.ts @@ -0,0 +1,117 @@ +export type ActionEvents = string | string[] +export type Events = ActionEvents | ActionEvents[] + +// Enum for Event Types +const enum Type { + system = 'system', + balances = 'balances', + transactionPayment = 'transactionPayment', + domains = 'domains', + sudo = 'sudo', +} + +// Utility Function for Event Names +const eventName = (type: Type, event: string) => `${type}.${event}` + +// System Events +const system: { + [key: string]: string +} = { + failure: eventName(Type.system, 'ExtrinsicFailed'), + newAccount: eventName(Type.system, 'NewAccount'), + success: eventName(Type.system, 'ExtrinsicSuccess'), +} + +// Balances Events +const balances: { + [key: string]: string +} = { + deposit: eventName(Type.balances, 'Deposit'), + endowed: eventName(Type.balances, 'Endowed'), + transfer: eventName(Type.balances, 'Transfer'), + withdraw: eventName(Type.balances, 'Withdraw'), +} + +// Transaction Payment Events +const transactionPayment: { + [key: string]: string +} = { + feePaid: eventName(Type.transactionPayment, 'TransactionFeePaid'), +} + +// Domains Events +const domains: { + [key: string]: string +} = { + forceDomainEpochTransition: eventName(Type.domains, 'ForceDomainEpochTransition'), + fundsUnlocked: eventName(Type.domains, 'FundsUnlocked'), + operatorDeregistered: eventName(Type.domains, 'OperatorDeregistered'), + operatorNominated: eventName(Type.domains, 'OperatorNominated'), + operatorRegistered: eventName(Type.domains, 'OperatorRegistered'), + operatorUnlocked: eventName(Type.domains, 'OperatorUnlocked'), + storageFeeDeposited: eventName(Type.domains, 'StorageFeeDeposited'), + withdrawStake: eventName(Type.domains, 'WithdrewStake'), +} + +// Sudo Events +const sudo: { + [key: string]: string +} = { + sudid: eventName(Type.sudo, 'Sudid'), +} + +// Define specific extrinsic keys for events +type EventKeys = + | 'transfer' + | 'operatorRegistered' + | 'operatorNominated' + | 'operatorDeRegistered' + | 'withdrawStake' + | 'unlockFunds' + | 'forceDomainEpochTransition' + +// Events Mappings +export const events: { [key in EventKeys]: ActionEvents } = { + transfer: [balances.withdraw, balances.transfer, transactionPayment.feePaid, system.success], + operatorRegistered: [ + balances.withdraw, + domains.storageFeeDeposited, + domains.operatorRegistered, + transactionPayment.feePaid, + system.success, + ], + operatorNominated: [ + balances.withdraw, + balances.transfer, + domains.storageFeeDeposited, + domains.operatorNominated, + transactionPayment.feePaid, + system.success, + ], + operatorDeRegistered: [ + balances.withdraw, + domains.operatorDeregistered, + transactionPayment.feePaid, + system.success, + ], + withdrawStake: [ + balances.withdraw, + domains.withdrawStake, + transactionPayment.feePaid, + system.success, + ], + unlockFunds: [ + balances.withdraw, + domains.fundsUnlocked, + transactionPayment.feePaid, + system.success, + ], + forceDomainEpochTransition: [ + balances.withdraw, + domains.forceDomainEpochTransition, + sudo.sudid, + balances.deposit, + transactionPayment.feePaid, + system.success, + ], +} diff --git a/packages/auto-consensus/__test__/helpers/index.ts b/packages/auto-consensus/__test__/helpers/index.ts new file mode 100644 index 00000000..9443bec6 --- /dev/null +++ b/packages/auto-consensus/__test__/helpers/index.ts @@ -0,0 +1,5 @@ +export * from './events' +export * from './setup' +export * from './staking' +export * from './sudo' +export * from './tx' diff --git a/packages/auto-consensus/__test__/helpers/setup.ts b/packages/auto-consensus/__test__/helpers/setup.ts new file mode 100644 index 00000000..88759645 --- /dev/null +++ b/packages/auto-consensus/__test__/helpers/setup.ts @@ -0,0 +1,39 @@ +import type { NetworkInput } from '@autonomys/auto-utils' +import { activate, disconnect, networks } from '@autonomys/auto-utils' + +export const setup = () => { + const isLocalhost = process.env.LOCALHOST === 'true' + + // Define the test network and its details + const TEST_NETWORK: NetworkInput = !isLocalhost + ? { networkId: networks[0].id } + : { networkId: 'autonomys-localhost' } + const TEST_INVALID_NETWORK = { networkId: 'invalid-network' } + + const TEST_MNEMONIC = 'test test test test test test test test test test test junk' + const TEST_ADDRESS = '5GmS1wtCfR4tK5SSgnZbVT4kYw5W8NmxmijcsxCQE6oLW6A8' + const ALICE_URI = '//Alice' + const ALICE_ADDRESS = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' + const BOB_URI = '//Bob' + const BOB_ADDRESS = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty' + + beforeAll(async () => { + await activate(TEST_NETWORK) + }) + + afterAll(async () => { + await disconnect() + }) + + return { + isLocalhost, + TEST_NETWORK, + TEST_INVALID_NETWORK, + TEST_MNEMONIC, + TEST_ADDRESS, + ALICE_URI, + ALICE_ADDRESS, + BOB_URI, + BOB_ADDRESS, + } +} diff --git a/packages/auto-consensus/__test__/helpers/staking.ts b/packages/auto-consensus/__test__/helpers/staking.ts new file mode 100644 index 00000000..6dd2d6e2 --- /dev/null +++ b/packages/auto-consensus/__test__/helpers/staking.ts @@ -0,0 +1,79 @@ +import type { ApiPromise } from '@polkadot/api' +import { u8aToHex } from '@polkadot/util' +import { operator, operators, RegisterOperatorInput } from '../../src/staking' + +const STORAGE_FEE_DEPOSIT_PERCENTAGE = 20 // 20% + +export const parseBigInt = (operatorId: string | number | bigint): bigint => + typeof operatorId === 'bigint' ? operatorId : BigInt(operatorId) + +export const calculateStake = (input: RegisterOperatorInput) => { + const { amountToStake, nominationTax } = input + + return (parseBigInt(amountToStake) * BigInt(100 - STORAGE_FEE_DEPOSIT_PERCENTAGE)) / BigInt(100) + // To-Do: Add the nomination tax +} + +export const calculateStorageFee = (input: RegisterOperatorInput) => { + const { amountToStake } = input + + return (parseBigInt(amountToStake) * BigInt(STORAGE_FEE_DEPOSIT_PERCENTAGE)) / BigInt(100) +} + +export const verifyOperatorRegistration = async (input: RegisterOperatorInput) => { + const { api, Operator, domainId, minimumNominatorStake, nominationTax } = input + + const operatorsList = await operators(api) + const findOperator = operatorsList.find( + (o) => o.operatorDetails.signingKey === u8aToHex(Operator.publicKey), + ) + expect(findOperator).toBeDefined() + if (findOperator) { + expect(findOperator.operatorDetails.currentDomainId).toEqual(BigInt(domainId)) + expect(findOperator.operatorDetails.currentTotalStake).toEqual(BigInt(0)) + expect(findOperator.operatorDetails.minimumNominatorStake).toEqual( + BigInt(minimumNominatorStake), + ) + expect(findOperator.operatorDetails.nominationTax).toEqual(Number(nominationTax)) + expect(findOperator.operatorDetails.status).toEqual({ registered: null }) + const thisOperator = await operator(api, findOperator.operatorId) + expect(thisOperator.currentDomainId).toEqual(BigInt(domainId)) + expect(thisOperator.currentTotalStake).toEqual(BigInt(0)) + expect(thisOperator.minimumNominatorStake).toEqual(BigInt(minimumNominatorStake)) + expect(thisOperator.nominationTax).toEqual(Number(nominationTax)) + expect(thisOperator.status).toEqual({ registered: null }) + } + + return findOperator +} + +export const verifyOperatorRegistrationFinal = async (input: RegisterOperatorInput) => { + const { api, Operator, domainId, amountToStake, minimumNominatorStake, nominationTax } = input + + const operatorsList = await operators(api) + const findOperator = operatorsList.find( + (o) => o.operatorDetails.signingKey === u8aToHex(Operator.publicKey), + ) + expect(findOperator).toBeDefined() + if (findOperator) { + expect(findOperator.operatorDetails.currentDomainId).toEqual(BigInt(domainId)) + expect(findOperator.operatorDetails.currentTotalStake).toEqual( + (BigInt(amountToStake) / BigInt(100)) * BigInt(80), + ) + expect(findOperator.operatorDetails.minimumNominatorStake).toEqual( + BigInt(minimumNominatorStake), + ) + expect(findOperator.operatorDetails.nominationTax).toEqual(Number(nominationTax)) + expect(findOperator.operatorDetails.totalStorageFeeDeposit).toEqual( + (BigInt(amountToStake) / BigInt(100)) * BigInt(20), + ) + const thisOperator = await operator(api, findOperator.operatorId) + expect(thisOperator.currentDomainId).toEqual(BigInt(domainId)) + expect(thisOperator.currentTotalStake).toEqual(calculateStake(input)) + expect(thisOperator.minimumNominatorStake).toEqual(BigInt(minimumNominatorStake)) + expect(thisOperator.nominationTax).toEqual(Number(nominationTax)) + expect(thisOperator.totalStorageFeeDeposit).toEqual(calculateStorageFee(input)) + } + + return findOperator +} diff --git a/packages/auto-consensus/__test__/helpers/sudo.ts b/packages/auto-consensus/__test__/helpers/sudo.ts new file mode 100644 index 00000000..8a48f660 --- /dev/null +++ b/packages/auto-consensus/__test__/helpers/sudo.ts @@ -0,0 +1,13 @@ +import { ApiPromise } from '@polkadot/api' +import type { AddressOrPair, SubmittableExtrinsic } from '@polkadot/api/types' +import type { ISubmittableResult } from '@polkadot/types/types' +import type { Events } from './events' +import { signAndSendTx } from './tx' + +export const sudo = async ( + api: ApiPromise, + sender: AddressOrPair, + tx: SubmittableExtrinsic<'promise', ISubmittableResult>, + eventsExpected: Events = [], + log: boolean = true, +) => await signAndSendTx(sender, api.tx.sudo.sudo(tx), eventsExpected, log) diff --git a/packages/auto-consensus/__test__/helpers/tx.ts b/packages/auto-consensus/__test__/helpers/tx.ts new file mode 100644 index 00000000..1fe8bdfd --- /dev/null +++ b/packages/auto-consensus/__test__/helpers/tx.ts @@ -0,0 +1,68 @@ +import type { AddressOrPair, SubmittableExtrinsic } from '@polkadot/api/types' +import type { EventRecord } from '@polkadot/types/interfaces' +import type { ISubmittableResult } from '@polkadot/types/types' +import type { Events } from './events' + +const validateEvents = ( + events: EventRecord[], + eventsExpected: Events, + tx: string, + block: string, + log: boolean = true, +) => { + const _eventsExpected = + typeof eventsExpected === 'string' + ? [eventsExpected] + : eventsExpected.map((e: string | string[]) => (typeof e === 'string' ? [e] : e)).flat() + + events.forEach(({ event: { data, method, section } }) => { + // if (log) console.log(`${section}.${method}`, data.toString()) // Uncomment this line to log every events with their data + const index = _eventsExpected.indexOf(`${section}.${method}`) + if (index > -1) _eventsExpected.splice(index, 1) + else if (log) + console.log('Event not expected', `${section}.${method}`, 'tx', tx, 'block', block) + }) + if (_eventsExpected.length > 0) + console.log('Events not found', _eventsExpected, 'tx', tx, 'block', block) + + expect(_eventsExpected).toHaveLength(0) + + return _eventsExpected +} + +export const signAndSendTx = async ( + sender: AddressOrPair, + tx: SubmittableExtrinsic<'promise', ISubmittableResult>, + eventsExpected: Events = [], + log: boolean = true, +) => { + let txHashHex: string | undefined = undefined + let blockHash: string | undefined = undefined + await new Promise((resolve, reject) => { + tx.signAndSend(sender, ({ events, status, txHash }) => { + if (status.isInBlock) { + txHashHex = txHash.toHex() + blockHash = status.asInBlock.toHex() + if (log) console.log('Successful tx', txHashHex, 'in block', blockHash) + + if (eventsExpected.length > 0) { + eventsExpected = validateEvents(events, eventsExpected, txHashHex, blockHash, log) + if (eventsExpected.length === 0) resolve() + else reject(new Error('Events not found')) + } else resolve() + } else if ( + status.isRetracted || + status.isFinalityTimeout || + status.isDropped || + status.isInvalid + ) { + if (log) console.error('Transaction failed') + reject(new Error('Transaction failed')) + } + }) + }) + expect(txHashHex).toBeDefined() + expect(blockHash).toBeDefined() + + return { txHash: txHashHex, blockHash } +} diff --git a/packages/auto-consensus/__test__/info.test.ts b/packages/auto-consensus/__test__/info.test.ts index 016e907b..3a4779e0 100644 --- a/packages/auto-consensus/__test__/info.test.ts +++ b/packages/auto-consensus/__test__/info.test.ts @@ -1,14 +1,8 @@ -import { activate, disconnect } from '@autonomys/auto-utils' import { networkTimestamp } from '../src/info' +import { setup } from './helpers' describe('Verify info functions', () => { - beforeAll(async () => { - await activate() - }) - - afterAll(async () => { - await disconnect() - }) + setup() test('Check network timestamp return a number greater than zero', async () => { // totalIssuance is an async function that returns a hex number as a string diff --git a/packages/auto-consensus/__test__/staking.test.ts b/packages/auto-consensus/__test__/staking.test.ts new file mode 100644 index 00000000..65bd4dcd --- /dev/null +++ b/packages/auto-consensus/__test__/staking.test.ts @@ -0,0 +1,256 @@ +import { ActivateWalletInput, activateWallet } from '@autonomys/auto-utils' +import { mnemonicGenerate } from '@polkadot/util-crypto' +import { address } from '../src/address' +import { balance } from '../src/balances' +import { + deregisterOperator, + nominateOperator, + operator, + registerOperator, + unlockFunds, + withdrawStake, +} from '../src/staking' +import { transfer } from '../src/transfer' +import { + events, + setup, + signAndSendTx, + sudo, + verifyOperatorRegistration, + verifyOperatorRegistrationFinal, +} from './helpers' + +describe('Verify staking functions', () => { + const { isLocalhost, TEST_NETWORK, ALICE_URI, ALICE_ADDRESS } = setup() + + if (isLocalhost) { + describe('Test registerOperator()', () => { + test('Check Alice can register random wallet as an operator', async () => { + const mnemonicOperator = mnemonicGenerate() + const { api, accounts } = await activateWallet({ + ...TEST_NETWORK, + uri: ALICE_URI, + } as ActivateWalletInput) + const { accounts: operatorAccounts } = await activateWallet({ + ...TEST_NETWORK, + uri: mnemonicOperator, + } as ActivateWalletInput) + expect(accounts.length).toBeGreaterThan(0) + expect(accounts[0].address).toEqual(ALICE_ADDRESS) + + const sender = accounts[0] + const _balanceSenderStart = await balance(api, address(sender.address)) + expect(_balanceSenderStart.free).toBeGreaterThan(BigInt(0)) + + const domainId = '0' + const amountToStake = '100000000000000000000' + const minimumNominatorStake = '1000000000000000000' + const nominationTax = '5' + const txInput = { + api, + senderAddress: ALICE_ADDRESS, + Operator: operatorAccounts[0], + domainId, + amountToStake, + minimumNominatorStake, + nominationTax, + } + await signAndSendTx(sender, await registerOperator(txInput), [events.operatorRegistered]) + const findOperator = await verifyOperatorRegistration(txInput) + + const _balanceSenderEnd = await balance(api, address(sender.address)) + expect(_balanceSenderEnd.free).toBeLessThan( + _balanceSenderStart.free - BigInt(amountToStake), + ) + if (findOperator) { + await sudo(api, sender, await api.tx.domains.forceStakingEpochTransition(domainId), [ + events.forceDomainEpochTransition, + ]) + await verifyOperatorRegistrationFinal(txInput) + } + }, 30000) + }) + + describe('Test nominateOperator()', () => { + test('Check Alice can nominate OperatorId 1', async () => { + const { api, accounts } = await activateWallet({ + ...TEST_NETWORK, + uri: ALICE_URI, + } as ActivateWalletInput) + expect(accounts.length).toBeGreaterThan(0) + expect(accounts[0].address).toEqual(ALICE_ADDRESS) + + const sender = accounts[0] + const _balanceSenderStart = await balance(api, address(sender.address)) + expect(_balanceSenderStart.free).toBeGreaterThan(BigInt(0)) + + const amountToStake = '50000000000000000000' + await signAndSendTx( + sender, + await nominateOperator({ + api, + operatorId: '1', + amountToStake, + }), + [events.operatorNominated], + ) + }, 10000) + + test('Check Operator can addFunds after registration', async () => { + const mnemonicOperator = mnemonicGenerate() + const { api, accounts } = await activateWallet({ + ...TEST_NETWORK, + uri: ALICE_URI, + } as ActivateWalletInput) + const { accounts: operatorAccounts } = await activateWallet({ + ...TEST_NETWORK, + uri: mnemonicOperator, + } as ActivateWalletInput) + expect(accounts.length).toBeGreaterThan(0) + expect(accounts[0].address).toEqual(ALICE_ADDRESS) + + const sender = accounts[0] + + const _balanceSenderStart = await balance(api, address(sender.address)) + expect(_balanceSenderStart.free).toBeGreaterThan(BigInt(0)) + + const domainId = '0' + const amountToStake = '100000000000000000000' + const minimumNominatorStake = '1000000000000000000' + const nominationTax = '5' + const txInput = { + api, + senderAddress: ALICE_ADDRESS, + Operator: operatorAccounts[0], + domainId, + amountToStake, + minimumNominatorStake, + nominationTax, + } + await signAndSendTx(sender, await registerOperator(txInput), [events.operatorRegistered]) + await verifyOperatorRegistration(txInput) + + await sudo(api, sender, await api.tx.domains.forceStakingEpochTransition(domainId), [ + events.forceDomainEpochTransition, + ]) + const operator = await verifyOperatorRegistrationFinal(txInput) + + if (operator) { + const amountToAdd = '50000000000000000000' + await signAndSendTx( + sender, + await nominateOperator({ + api, + operatorId: operator.operatorId, + amountToStake: amountToAdd, + }), + [events.operatorNominated], + ) + } else throw new Error('Operator not found') + }, 180000) + }) + + describe('Test deregisterOperator()', () => { + test('Check Operator can deregisterOperator after registration', async () => { + const mnemonicOperator = mnemonicGenerate() + const { api, accounts } = await activateWallet({ + ...TEST_NETWORK, + uri: ALICE_URI, + } as ActivateWalletInput) + const { accounts: operatorAccounts } = await activateWallet({ + ...TEST_NETWORK, + uri: mnemonicOperator, + } as ActivateWalletInput) + expect(accounts.length).toBeGreaterThan(0) + expect(accounts[0].address).toEqual(ALICE_ADDRESS) + + const sender = accounts[0] + + const _balanceSenderStart = await balance(api, address(sender.address)) + expect(_balanceSenderStart.free).toBeGreaterThan(BigInt(0)) + + const domainId = '0' + const amountToStake = '100000000000000000000' + const minimumNominatorStake = '1000000000000000000' + const nominationTax = '5' + const txInput = { + api, + senderAddress: ALICE_ADDRESS, + Operator: operatorAccounts[0], + domainId, + amountToStake, + minimumNominatorStake, + nominationTax, + } + await signAndSendTx(sender, await registerOperator(txInput), [events.operatorRegistered]) + await verifyOperatorRegistration(txInput) + + await sudo(api, sender, await api.tx.domains.forceStakingEpochTransition(domainId), [ + events.forceDomainEpochTransition, + ]) + const findOperator = await verifyOperatorRegistrationFinal(txInput) + + if (findOperator) { + await signAndSendTx( + sender, + await deregisterOperator({ + api, + operatorId: findOperator.operatorId, + }), + [events.operatorDeRegistered], + ) + } + }, 60000) + }) + + describe('Test withdrawStake()', () => { + test('Check Alice can nominate OperatorId 1 and then withdrawStake', async () => { + const { api, accounts } = await activateWallet({ + ...TEST_NETWORK, + uri: ALICE_URI, + } as ActivateWalletInput) + expect(accounts.length).toBeGreaterThan(0) + expect(accounts[0].address).toEqual(ALICE_ADDRESS) + + const sender = accounts[0] + const _balanceSenderStart = await balance(api, address(sender.address)) + expect(_balanceSenderStart.free).toBeGreaterThan(BigInt(0)) + + const operatorId = '1' + const operatorDetails = await operator(api, operatorId) + + const amountToStake = '50000000000000000000' + await signAndSendTx( + sender, + await nominateOperator({ + api, + operatorId, + amountToStake, + }), + [events.operatorNominated], + ) + + await sudo( + api, + sender, + await api.tx.domains.forceStakingEpochTransition(operatorDetails.currentDomainId), + [events.forceDomainEpochTransition], + ) + + await signAndSendTx( + sender, + await withdrawStake({ + api, + operatorId, + shares: operatorDetails.currentTotalShares / BigInt(1000), + }), + [events.withdrawStake], + ) + }, 30000) + }) + } else { + test('Staking test only run on localhost', async () => { + expect(true).toBeTruthy() + }) + } +}) diff --git a/packages/auto-consensus/__test__/transfer.test.ts b/packages/auto-consensus/__test__/transfer.test.ts index 0b5454e9..762df324 100644 --- a/packages/auto-consensus/__test__/transfer.test.ts +++ b/packages/auto-consensus/__test__/transfer.test.ts @@ -1,38 +1,11 @@ -import type { NetworkInput } from '@autonomys/auto-utils' -import { - ActivateWalletInput, - activate, - activateWallet, - disconnect, - networks, -} from '@autonomys/auto-utils' +import { ActivateWalletInput, activateWallet } from '@autonomys/auto-utils' import { address } from '../src/address' import { balance } from '../src/balances' import { transfer } from '../src/transfer' +import { events, setup, signAndSendTx } from './helpers' describe('Verify transfer functions', () => { - const isLocalhost = process.env.LOCALHOST === 'true' - - // Define the test network and its details - const TEST_NETWORK: NetworkInput = !isLocalhost - ? { networkId: networks[0].id } - : { networkId: 'autonomys-localhost' } - const TEST_INVALID_NETWORK = { networkId: 'invalid-network' } - - const TEST_MNEMONIC = 'test test test test test test test test test test test junk' - const TEST_ADDRESS = '5GmS1wtCfR4tK5SSgnZbVT4kYw5W8NmxmijcsxCQE6oLW6A8' - const ALICE_URI = '//Alice' - const ALICE_ADDRESS = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' - const BOB_URI = '//Bob' - const BOB_ADDRESS = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty' - - beforeAll(async () => { - await activate(TEST_NETWORK) - }) - - afterAll(async () => { - await disconnect() - }) + const { isLocalhost, TEST_NETWORK, ALICE_URI, ALICE_ADDRESS, BOB_ADDRESS } = setup() if (isLocalhost) { describe('Test transfer()', () => { @@ -45,40 +18,19 @@ describe('Verify transfer functions', () => { expect(accounts[0].address).toEqual(ALICE_ADDRESS) const sender = accounts[0] - let txHash: string | undefined + let blockHash: string | undefined const _balanceSenderStart = await balance(api, address(sender.address)) const _balanceReceiverStart = await balance(api, address(BOB_ADDRESS)) expect(_balanceSenderStart.free).toBeGreaterThan(BigInt(0)) - const tx = await transfer(api, BOB_ADDRESS, 1) - - await new Promise((resolve, reject) => { - tx.signAndSend(sender, ({ status }) => { - if (status.isInBlock) { - txHash = status.asInBlock.toHex() - console.log('Successful transfer of 1 with hash ' + txHash) - resolve() - } else if ( - status.isRetracted || - status.isFinalityTimeout || - status.isDropped || - status.isInvalid - ) { - reject(new Error('Transaction failed')) - } else { - console.log('Status of transfer: ' + status.type) - } - }) - }) - - expect(txHash).toBeDefined() + await signAndSendTx(sender, await transfer(api, BOB_ADDRESS, 1), [events.transfer]) const _balanceSenderEnd = await balance(api, address(sender.address)) const _balanceReceiverEnd = await balance(api, address(BOB_ADDRESS)) expect(_balanceSenderEnd.free).toBeLessThan(_balanceSenderStart.free) expect(_balanceReceiverEnd.free).toBeGreaterThan(_balanceReceiverStart.free) - }) + }, 60000) }) } else { test('Transfer test only run on localhost', async () => { diff --git a/packages/auto-consensus/package.json b/packages/auto-consensus/package.json index b9ee477f..e0f77cfb 100644 --- a/packages/auto-consensus/package.json +++ b/packages/auto-consensus/package.json @@ -6,7 +6,7 @@ "build": "tsc", "clean": "rm -rf dist", "format": "prettier --write \"src/**/*.ts\"", - "test": "jest" + "test-local": "jest -i" }, "files": [ "dist", @@ -24,6 +24,7 @@ "@autonomys/auto-utils": "workspace:*" }, "devDependencies": { + "@polkadot/types-codec": "^11.2.1", "@types/jest": "^29.5.12", "eslint": "^8.57.0", "jest": "^29.7.0", diff --git a/packages/auto-consensus/src/balances.ts b/packages/auto-consensus/src/balances.ts index bdf50821..b1426117 100644 --- a/packages/auto-consensus/src/balances.ts +++ b/packages/auto-consensus/src/balances.ts @@ -1,5 +1,5 @@ import { activate } from '@autonomys/auto-utils' -import { ApiPromise } from '@polkadot/api' +import type { ApiPromise } from '@polkadot/api' import type { BN } from '@polkadot/util' type RawBalanceData = { @@ -9,9 +9,9 @@ type RawBalanceData = { flags: BN } type BalanceData = { - free: any - reserved: any - frozen: any + free: bigint + reserved: bigint + frozen: bigint } export const totalIssuance = async (networkId?: string) => { diff --git a/packages/auto-consensus/src/staking.ts b/packages/auto-consensus/src/staking.ts new file mode 100644 index 00000000..f6137dbb --- /dev/null +++ b/packages/auto-consensus/src/staking.ts @@ -0,0 +1,203 @@ +import type { ApiPromise } from '@polkadot/api' +import type { KeyringPair } from '@polkadot/keyring/types' +import type { StorageKey } from '@polkadot/types' +import { createType } from '@polkadot/types' +import type { AnyTuple, Codec } from '@polkadot/types-codec/types' +import { u8aToHex } from '@polkadot/util' + +type RawOperatorId = string[] +type RawOperatorDetails = { + signingKey: string + currentDomainId: number + nextDomainId: number + minimumNominatorStake: string + nominationTax: number + currentTotalStake: number + currentEpochRewards: number + currentTotalShares: number + status: object[] + depositsInEpoch: string + withdrawalsInEpoch: number + totalStorageFeeDeposit: string +} +export type OperatorDetails = { + signingKey: string + currentDomainId: bigint + nextDomainId: bigint + minimumNominatorStake: bigint + nominationTax: number + currentTotalStake: bigint + currentEpochRewards: bigint + currentTotalShares: bigint + status: object[] + depositsInEpoch: bigint + withdrawalsInEpoch: bigint + totalStorageFeeDeposit: bigint +} +export type Operator = { + operatorId: bigint + operatorDetails: OperatorDetails +} + +type StringNumberOrBigInt = string | number | bigint + +export type RegisterOperatorInput = { + api: ApiPromise + senderAddress: string + Operator: KeyringPair + domainId: StringNumberOrBigInt + amountToStake: StringNumberOrBigInt + minimumNominatorStake: StringNumberOrBigInt + nominationTax: StringNumberOrBigInt +} + +export type StakingInput = { + api: ApiPromise + operatorId: StringNumberOrBigInt +} + +export interface WithdrawStakeInput extends StakingInput { + shares: StringNumberOrBigInt +} + +export interface NominateOperatorInput extends StakingInput { + amountToStake: StringNumberOrBigInt +} + +const parseOperatorDetails = (operatorDetails: Codec): OperatorDetails => { + const rawOD = operatorDetails.toJSON() as RawOperatorDetails + return { + signingKey: rawOD.signingKey, + currentDomainId: BigInt(rawOD.currentDomainId), + nextDomainId: BigInt(rawOD.nextDomainId), + minimumNominatorStake: BigInt(rawOD.minimumNominatorStake), + nominationTax: rawOD.nominationTax, + currentTotalStake: BigInt(rawOD.currentTotalStake), + currentEpochRewards: BigInt(rawOD.currentEpochRewards), + currentTotalShares: BigInt(rawOD.currentTotalShares), + status: rawOD.status, + depositsInEpoch: BigInt(rawOD.depositsInEpoch), + withdrawalsInEpoch: BigInt(rawOD.withdrawalsInEpoch), + totalStorageFeeDeposit: BigInt(rawOD.totalStorageFeeDeposit), + } +} + +const parseOperator = (operator: [StorageKey, Codec]): Operator => { + return { + operatorId: BigInt((operator[0].toHuman() as RawOperatorId)[0]), + operatorDetails: parseOperatorDetails(operator[1]), + } +} + +const parseString = (operatorId: StringNumberOrBigInt): string => + typeof operatorId === 'string' ? operatorId : operatorId.toString() + +export const operators = async (api: ApiPromise) => { + try { + const _operators = await api.query.domains.operators.entries() + return _operators.map((o) => parseOperator(o)) + } catch (error) { + console.error('error', error) + throw new Error('Error querying operators list.' + error) + } +} + +export const operator = async (api: ApiPromise, operatorId: StringNumberOrBigInt) => { + try { + const _operator = await api.query.domains.operators(parseString(operatorId)) + return parseOperatorDetails(_operator) + } catch (error) { + console.error('error', error) + throw new Error(`Error querying operatorId: ${operatorId} with error: ${error}`) + } +} + +export const registerOperator = async (input: RegisterOperatorInput) => { + try { + const { + api, + senderAddress, + Operator, + domainId, + amountToStake, + minimumNominatorStake, + nominationTax, + } = input + + const message = createType(api.registry, 'AccountId', senderAddress).toU8a() + const signingKey = u8aToHex(Operator.publicKey) + const signature = Operator.sign(message) + + return await api.tx.domains.registerOperator( + parseString(domainId), + parseString(amountToStake), + { + signingKey, + minimumNominatorStake: parseString(minimumNominatorStake), + nominationTax: parseString(nominationTax), + }, + signature, + ) + } catch (error) { + console.error('error', error) + throw new Error('Error creating register operator tx.' + error) + } +} + +export const nominateOperator = async (input: NominateOperatorInput) => { + try { + const { api, operatorId, amountToStake } = input + + return await api.tx.domains.nominateOperator( + parseString(operatorId), + parseString(amountToStake), + ) + } catch (error) { + console.error('error', error) + throw new Error('Error creating nominate operator tx.' + error) + } +} + +export const withdrawStake = async (input: WithdrawStakeInput) => { + try { + const { api, operatorId, shares } = input + + return await api.tx.domains.withdrawStake(parseString(operatorId), parseString(shares)) + } catch (error) { + console.error('error', error) + throw new Error('Error creating withdraw stake tx.' + error) + } +} + +export const deregisterOperator = async (input: StakingInput) => { + try { + const { api, operatorId } = input + + return await api.tx.domains.deregisterOperator(parseString(operatorId)) + } catch (error) { + console.error('error', error) + throw new Error('Error creating de-register operator tx.' + error) + } +} + +export const unlockFunds = async (input: StakingInput) => { + try { + const { api, operatorId } = input + + return await api.tx.domains.unlockFunds(parseString(operatorId)) + } catch (error) { + console.error('error', error) + throw new Error('Error creating unlock funds tx.' + error) + } +} + +export const unlockNominator = async (input: StakingInput) => { + try { + const { api, operatorId } = input + + return await api.tx.domains.unlockNominator(parseString(operatorId)) + } catch (error) { + console.error('error', error) + throw new Error('Error creating unlock nominator tx.' + error) + } +} diff --git a/packages/auto-consensus/src/transfer.ts b/packages/auto-consensus/src/transfer.ts index f322b96f..b884f108 100644 --- a/packages/auto-consensus/src/transfer.ts +++ b/packages/auto-consensus/src/transfer.ts @@ -1,4 +1,4 @@ -import { ApiPromise } from '@polkadot/api' +import type { ApiPromise } from '@polkadot/api' export const transfer = async ( api: ApiPromise, diff --git a/packages/auto-utils/src/wallet.ts b/packages/auto-utils/src/wallet.ts index b5a14875..aa81e237 100644 --- a/packages/auto-utils/src/wallet.ts +++ b/packages/auto-utils/src/wallet.ts @@ -41,20 +41,11 @@ export const activateWallet = async (input: ActivateWalletInput) => { // Get the list of accounts from the extension const allAccounts = await web3Accounts() accounts.push(...allAccounts) - - // Attach the first account (or handle multiple accounts as needed) - if (allAccounts.length > 0) { - const selectedAccount = allAccounts[0] - console.log('Connected to account:', selectedAccount.address) - // You can now use selectedAccount for transactions - } else { - console.warn('No accounts found in the Polkadot.js extension') - } + if (allAccounts.length === 0) console.warn('No accounts found in the Polkadot.js extension') } else if ((input as Mnemonic).mnemonic || (input as URI).uri) { // Attach the wallet in a node environment const account = await setupWallet(input) accounts.push(account) - if (account) console.log('Wallet attached:', account.address) } else throw new Error('No wallet provided') return { api, accounts } diff --git a/scripts/localhost-run-test.sh b/scripts/localhost-run-test.sh index 80308a46..66d4d205 100644 --- a/scripts/localhost-run-test.sh +++ b/scripts/localhost-run-test.sh @@ -1,4 +1,5 @@ LOCALHOST="true" export LOCALHOST -yarn run test \ No newline at end of file +yarn run test +yarn workspace @autonomys/auto-consensus run test-local \ No newline at end of file diff --git a/scripts/run-farmer.sh b/scripts/run-farmer.sh index 0798e27a..83c2933a 100644 --- a/scripts/run-farmer.sh +++ b/scripts/run-farmer.sh @@ -2,6 +2,6 @@ # Run farmer echo "Running farmer..." -./executables/farmer farm path=executables/farmer-temp,size=2GB --reward-address 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY --node-rpc-url ws://127.0.0.1:9944 +./executables/farmer farm path=executables/farmer-temp,size=1GiB --reward-address 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY --node-rpc-url ws://127.0.0.1:9944 echo "Both node and farmer are running in parallel." \ No newline at end of file diff --git a/scripts/run-operator.sh b/scripts/run-operator.sh index d90d3196..37780314 100644 --- a/scripts/run-operator.sh +++ b/scripts/run-operator.sh @@ -17,4 +17,4 @@ echo -e "${YELLOW}You can change these variables at the top of the script.${NC}" # Run an operator echo "Running an operator..." -./executables/node run --dev --farmer --timekeeper --base-path "$BASE_PATH" --name "localhost-operator" --rpc-rate-limit 1000 --rpc-max-connections 10000 --state-pruning archive-canonical --blocks-pruning 512 --rpc-cors all --force-synced --force-authoring -- --domain-id 0 --operator-id 2 --state-pruning archive-canonical --blocks-pruning 512 --rpc-cors all \ No newline at end of file +./executables/node run --dev --farmer --timekeeper --base-path "$BASE_PATH" --name "localhost-operator" --rpc-rate-limit 1000 --rpc-max-connections 10000 --state-pruning archive-canonical --blocks-pruning archive-canonical --rpc-cors all --force-synced --force-authoring -- --domain-id 0 --operator-id 1 --state-pruning archive-canonical --blocks-pruning 512 --rpc-cors all \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d0d265f0..cf6008ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20,6 +20,7 @@ __metadata: resolution: "@autonomys/auto-consensus@workspace:packages/auto-consensus" dependencies: "@autonomys/auto-utils": "workspace:*" + "@polkadot/types-codec": "npm:^11.2.1" "@types/jest": "npm:^29.5.12" eslint: "npm:^8.57.0" jest: "npm:^29.7.0" @@ -1213,7 +1214,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/types-codec@npm:11.2.1": +"@polkadot/types-codec@npm:11.2.1, @polkadot/types-codec@npm:^11.2.1": version: 11.2.1 resolution: "@polkadot/types-codec@npm:11.2.1" dependencies: