diff --git a/.github/workflows/network-browser-test.yml b/.github/workflows/network-browser-test.yml new file mode 100644 index 00000000..96da99a1 --- /dev/null +++ b/.github/workflows/network-browser-test.yml @@ -0,0 +1,28 @@ +name: network-browser test +on: [push, pull_request] +env: + ETH_PRIVATE_KEY: ${{ secrets.ETH_PRIVATE_KEY }} + MANAGER_TAG: "1.9.3-beta.0" +jobs: + test_network_browser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - uses: oven-sh/setup-bun@v1 + - name: Launch hardhat node + working-directory: hardhat-node + run: docker-compose up -d + - name: Deploy manager contracts + run: | + bash ./helper-scripts/deploy_test_manager.sh + docker rmi -f skalenetwork/skale-manager:${{ env.MANAGER_TAG }} + + - name: Install network-browser dependencies + working-directory: network-browser + run: bun i + + - name: Run network-browser tests + working-directory: network-browser + run: bash run_tests.sh \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 2e2c3428..99e85c7e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "IMA"] path = IMA url = https://github.com/skalenetwork/IMA.git +[submodule "hardhat-node"] + path = hardhat-node + url = https://github.com/skalenetwork/hardhat-node.git +[submodule "helper-scripts"] + path = helper-scripts + url = https://github.com/skalenetwork/helper-scripts.git diff --git a/hardhat-node b/hardhat-node new file mode 160000 index 00000000..8a4b03fd --- /dev/null +++ b/hardhat-node @@ -0,0 +1 @@ +Subproject commit 8a4b03fd1051960a3e0182280bf4bfdc43129997 diff --git a/helper-scripts b/helper-scripts new file mode 160000 index 00000000..34adbed6 --- /dev/null +++ b/helper-scripts @@ -0,0 +1 @@ +Subproject commit 34adbed6050a23aec351eb90501b2ac2846c0f4b diff --git a/network-browser/README.md b/network-browser/README.md index fb345048..98c28abd 100644 --- a/network-browser/README.md +++ b/network-browser/README.md @@ -20,6 +20,8 @@ Optional env variables: - `POST_ERROR_DELAY` - delay before retry if error happened in browser loop (seconds, default: `5`) - `NETWORK_BROWSER_DELAY` - delay between iterations of the network-browser (seconds, default: `10800`) - `NETWORK_BROWSER_TIMEOUT` - maximum amount of time allocated to the browse function (seconds, default: `1200`) +- `NETWORK_BROWSER_LOG_LEVEL` - log level (0: silly, 1: trace, 2: debug, 3: info, 4: warn, 5: error, 6: fatal, default: `1`) +- `NETWORK_BROWSER_LOG_PRETTY` - colored logs, (boolean, default: `false`) ## Development diff --git a/network-browser/bun.lockb b/network-browser/bun.lockb index 71e6221e..b9f92824 100755 Binary files a/network-browser/bun.lockb and b/network-browser/bun.lockb differ diff --git a/network-browser/index.ts b/network-browser/index.ts index b99527aa..6fac8be5 100644 --- a/network-browser/index.ts +++ b/network-browser/index.ts @@ -37,12 +37,14 @@ import { NETWORK_BROWSER_DELAY, MULTICALL, CONNECTED_ONLY, - SCHAIN_RPC_URL + SCHAIN_RPC_URL, + LOG_LEVEL, + LOG_PRETTY } from './src/constants' import { Logger, type ILogObj } from 'tslog' -const log = new Logger() +const log = new Logger({ minLevel: LOG_LEVEL, stylePrettyLogs: LOG_PRETTY }) async function safeNetworkBrowserLoop() { log.info(`Running network-browser...`) diff --git a/network-browser/package.json b/network-browser/package.json index 1532d47b..6fe9fcee 100644 --- a/network-browser/package.json +++ b/network-browser/package.json @@ -1,7 +1,7 @@ { "name": "network-browser", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "bun _fix && bun --hot index.ts", @@ -23,11 +23,13 @@ }, "devDependencies": { "@rollup/plugin-typescript": "^11.1.5", - "bun-types": "^1.0.11", + "@types/elliptic": "^6.4.18", + "bun-types": "^1.0.18-1", + "elliptic": "^6.5.4", "eslint": "^8.53.0", "eslint-config-standard-with-typescript": "^39.1.1", "prettier": "^3.1.0", "rollup": "^4.7.0", - "typescript": "^5.2.2" + "typescript": "5.3.3" } } \ No newline at end of file diff --git a/network-browser/rollup.config.js b/network-browser/rollup.config.js index cada37b1..626aecfc 100644 --- a/network-browser/rollup.config.js +++ b/network-browser/rollup.config.js @@ -7,5 +7,10 @@ export default { format: 'es', preserveModules: true }, - plugins: [typescript()] + plugins: [ + typescript({ + include: ['src/**', 'index.ts'], + exclude: ['**/tests', '**/build'] + }) + ] } diff --git a/network-browser/run_tests.sh b/network-browser/run_tests.sh new file mode 100644 index 00000000..03c1bd01 --- /dev/null +++ b/network-browser/run_tests.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +export IMA_NETWORK_BROWSER_DATA_PATH="$DIR/test_schainsData.json" +export SCHAIN_PROXY_PATH="$DIR/test_ima_schain.json" +export MANAGER_ABI_PATH="$DIR/../helper-scripts/contracts_data/manager.json" + +export MAINNET_RPC_URL="http://127.0.0.1:8545" +export SCHAIN_RPC_URL="http://127.0.0.1:8545" +export SCHAIN_NAME="test" +export CONNECTED_ONLY=false + +bun test \ No newline at end of file diff --git a/network-browser/src/browser.ts b/network-browser/src/browser.ts index 7f41f133..0a344679 100644 --- a/network-browser/src/browser.ts +++ b/network-browser/src/browser.ts @@ -33,10 +33,10 @@ import { filterConnectedHashes } from './schains' import { getNodesGroups } from './nodes' -import { CONNECTED_ONLY, IMA_NETWORK_BROWSER_DATA_PATH } from './constants' +import { CONNECTED_ONLY, IMA_NETWORK_BROWSER_DATA_PATH, LOG_LEVEL, LOG_PRETTY } from './constants' import { writeJson, currentTimestamp, chainIdInt } from './tools' -const log = new Logger() +const log = new Logger({ minLevel: LOG_LEVEL, stylePrettyLogs: LOG_PRETTY }) export async function browse(schainsInternal: Contract, nodes: Contract): Promise { log.info('Browse iteration started, collecting chains') diff --git a/network-browser/src/constants.ts b/network-browser/src/constants.ts index b76f3e79..f3843661 100644 --- a/network-browser/src/constants.ts +++ b/network-browser/src/constants.ts @@ -20,7 +20,7 @@ * @copyright SKALE Labs 2023-Present */ -import { requiredEnv, booleanEnv, secondsEnv } from './envTools' +import { requiredEnv, booleanEnv, secondsEnv, optionalEnvNumber } from './envTools' // internal @@ -53,3 +53,6 @@ export const CONNECTED_ONLY = booleanEnv('CONNECTED_ONLY', true) export const POST_ERROR_DELAY = secondsEnv(process.env.POST_ERROR_DELAY, 5) export const NETWORK_BROWSER_DELAY = secondsEnv(process.env.NETWORK_BROWSER_DELAY, 10800) export const NETWORK_BROWSER_TIMEOUT = secondsEnv(process.env.NETWORK_BROWSER_TIMEOUT, 1200) + +export const LOG_LEVEL = optionalEnvNumber('NETWORK_BROWSER_LOG_LEVEL', 1) +export const LOG_PRETTY = booleanEnv('NETWORK_BROWSER_LOG_PRETTY', false) diff --git a/network-browser/src/contracts.ts b/network-browser/src/contracts.ts index 9bc02dc9..2992c367 100644 --- a/network-browser/src/contracts.ts +++ b/network-browser/src/contracts.ts @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2023-Present */ -import { JsonRpcProvider, type Provider, Contract, type Network } from 'ethers' +import { JsonRpcProvider, type Provider, Contract, type Network, type ContractRunner } from 'ethers' import mc from 'ethers-multicall-provider' import { type SkaleManagerAbi, type SChainImaAbi } from './interfaces' import { readJson } from './tools' @@ -52,14 +52,14 @@ export function getSChainProvider(endpoint: string): Provider { return new JsonRpcProvider(endpoint) } -export function schainsInternalContract(abi: SkaleManagerAbi, provider: Provider): Contract { - return new Contract(abi.schains_internal_address, abi.schains_internal_abi, provider) +export function schainsInternalContract(abi: SkaleManagerAbi, runner: ContractRunner): Contract { + return new Contract(abi.schains_internal_address, abi.schains_internal_abi, runner) } -export function messageProxyContract(abi: SChainImaAbi, provider: Provider): Contract { - return new Contract(abi.message_proxy_chain_address, abi.message_proxy_chain_abi, provider) +export function messageProxyContract(abi: SChainImaAbi, runner: ContractRunner): Contract { + return new Contract(abi.message_proxy_chain_address, abi.message_proxy_chain_abi, runner) } -export function nodesContract(abi: SkaleManagerAbi, provider: Provider): Contract { - return new Contract(abi.nodes_address, abi.nodes_abi, provider) +export function nodesContract(abi: SkaleManagerAbi, runner: ContractRunner): Contract { + return new Contract(abi.nodes_address, abi.nodes_abi, runner) } diff --git a/network-browser/src/envTools.ts b/network-browser/src/envTools.ts index 332533d5..58ff6511 100644 --- a/network-browser/src/envTools.ts +++ b/network-browser/src/envTools.ts @@ -22,6 +22,10 @@ const MS_MULTIPLIER = 1000 +export function isValidNumber(str: string): boolean { + return !isNaN(+str) && str.trim().length > 0 +} + export function secondsEnv(envValue: string | undefined, defaultSeconds: number): number { return (envValue !== undefined ? Number(envValue) : defaultSeconds) * MS_MULTIPLIER } @@ -42,10 +46,18 @@ export function requiredEnv(name: string): string { return value } -export function optionalEnv(envVar: string, defaultValue: string): string { - const value = process.env[envVar] +export function optionalEnv(name: string, defaultValue: string): string { + const value = process.env[name] if (value === undefined) { return defaultValue } return value } + +export function optionalEnvNumber(name: string, defaultValue: number): number { + const value = process.env[name] + if (value === undefined || !isValidNumber(value)) { + return defaultValue + } + return Number(value) +} diff --git a/network-browser/src/interfaces.ts b/network-browser/src/interfaces.ts index 292f6dca..d0031ff7 100644 --- a/network-browser/src/interfaces.ts +++ b/network-browser/src/interfaces.ts @@ -89,6 +89,10 @@ export interface SkaleManagerAbi { schains_abi: InterfaceAbi schains_internal_address: string schains_internal_abi: InterfaceAbi + validator_service_address: string + validator_service_abi: InterfaceAbi + skale_manager_address: string + skale_manager_abi: InterfaceAbi } export interface SChainImaAbi { diff --git a/network-browser/src/nodes.ts b/network-browser/src/nodes.ts index af6cd7b8..d65f547d 100644 --- a/network-browser/src/nodes.ts +++ b/network-browser/src/nodes.ts @@ -78,7 +78,7 @@ async function getNodesRaw( ) } -function nodeStruct( +export function nodeStruct( nodeArray: NodeArray, domainName: string, schainHash?: string, diff --git a/network-browser/src/tools.ts b/network-browser/src/tools.ts index b394649c..d06fcea5 100644 --- a/network-browser/src/tools.ts +++ b/network-browser/src/tools.ts @@ -25,9 +25,9 @@ import { Logger, type ILogObj } from 'tslog' import { readFileSync, writeFileSync, renameSync } from 'fs' import { BrowserTimeoutError } from './errors' -import { DEFAULT_PING_DELAY, DEFAULT_PING_ITERATIONS } from './constants' +import { DEFAULT_PING_DELAY, DEFAULT_PING_ITERATIONS, LOG_LEVEL, LOG_PRETTY } from './constants' -const log = new Logger() +const log = new Logger({ minLevel: LOG_LEVEL, stylePrettyLogs: LOG_PRETTY }) export function stringifyBigInt(obj: any): string { return JSON.stringify( diff --git a/network-browser/tests/base.test.ts b/network-browser/tests/base.test.ts deleted file mode 100644 index 542ac50f..00000000 --- a/network-browser/tests/base.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { describe, beforeAll, test } from 'bun:test' - -describe('test group', () => { - beforeAll(() => { - console.log('bf') - }) - test('browse test', () => { - console.log('test') - }) -}) diff --git a/network-browser/tests/browser.test.ts b/network-browser/tests/browser.test.ts new file mode 100644 index 00000000..48f6afdf --- /dev/null +++ b/network-browser/tests/browser.test.ts @@ -0,0 +1,98 @@ +import { unlinkSync, existsSync } from 'node:fs' +import { describe, beforeAll, test, expect, beforeEach } from 'bun:test' +import { Contract, Wallet } from 'ethers' + +import { + getMainnetProvider, + nodesContract, + getMainnetManagerAbi, + schainsInternalContract +} from '../src/contracts' +import { browse } from '../src/browser' +import { getSChain } from '../src/schains' +import { readJson } from '../src/tools' +import { NetworkBrowserData } from '../src/interfaces' +import { MAINNET_RPC_URL, IMA_NETWORK_BROWSER_DATA_PATH } from '../src/constants' + +import { + ETH_PRIVATE_KEY, + NODES_IN_SCHAIN, + validatorsContract, + managerContract, + schainsContract, + addAllPermissions, + generateWallets, + initDefaultValidator, + linkNodes, + registerNodes, + addTestSchainTypes, + createSchain, + randomString +} from './testUtils' + +describe('browser module test', () => { + let nodes: Contract + let schainsInternal: Contract + let wallet: Wallet + const chainName = randomString() + + beforeAll(async () => { + console.log('initializing provider and contracts') + const provider = await getMainnetProvider(MAINNET_RPC_URL, false) + wallet = new Wallet(ETH_PRIVATE_KEY, provider) + + const managerAbi = getMainnetManagerAbi() + const validators = validatorsContract(managerAbi, wallet) + schainsInternal = schainsInternalContract(managerAbi, wallet) + const schains = schainsContract(managerAbi, wallet) + const manager = managerContract(managerAbi, wallet) + + nodes = nodesContract(managerAbi, provider) + + await addAllPermissions(validators, schainsInternal, schains, wallet) + await initDefaultValidator(validators) + const wallets = await generateWallets(provider, wallet, NODES_IN_SCHAIN) + await linkNodes(validators, wallet, wallets) + await registerNodes(nodes, wallets) + + await addTestSchainTypes(schainsInternal) + await createSchain(schains, chainName, wallet.address) + }) + + beforeEach(async () => { + if (existsSync(IMA_NETWORK_BROWSER_DATA_PATH)) { + console.log('removing browse results') + unlinkSync(IMA_NETWORK_BROWSER_DATA_PATH) + } + }) + + test('browse', async () => { + const schain = await getSChain(schainsInternal, chainName) + expect(schain.name).toBe(chainName) + expect(schain.mainnetOwner).toBe(wallet.address) + + expect(existsSync(IMA_NETWORK_BROWSER_DATA_PATH)).toBeFalse + await browse(schainsInternal, nodes) + expect(existsSync(IMA_NETWORK_BROWSER_DATA_PATH)).toBeTrue + + const nbData: NetworkBrowserData = readJson(IMA_NETWORK_BROWSER_DATA_PATH) + + expect(nbData.updatedAt).toBeNumber + expect(nbData.schains).toBeArray + expect(nbData.schains[0].name).toBeString + expect(nbData.schains[0].mainnetOwner).toBeString + expect(nbData.schains[0].indexInOwnerList).toBeString + expect(nbData.schains[0].partOfNode).toBeString + expect(nbData.schains[0].lifetime).toBeString + expect(nbData.schains[0].startBlock).toBeString + expect(nbData.schains[0].deposit).toBeString + expect(nbData.schains[0].index).toBeString + expect(nbData.schains[0].generation).toBeString + expect(nbData.schains[0].chainId).toBeNumber + expect(nbData.schains[0].nodes).toBeArrayOfSize(NODES_IN_SCHAIN) + if (nbData.schains[0].nodes) { + expect(nbData.schains[0].nodes[0].endpoints?.domain.https).toBeString + expect(nbData.schains[0].nodes[0].endpoints?.ip.ws).toBeString + } + }) +}) diff --git a/network-browser/tests/nodes.test.ts b/network-browser/tests/nodes.test.ts new file mode 100644 index 00000000..ec82b90b --- /dev/null +++ b/network-browser/tests/nodes.test.ts @@ -0,0 +1,82 @@ +import { describe, beforeAll, test, expect } from 'bun:test' +import { Contract, Wallet, id } from 'ethers' + +import { + getMainnetProvider, + nodesContract, + getMainnetManagerAbi, + schainsInternalContract +} from '../src/contracts' +import { getNodes } from '../src/nodes' +import { getNodeIdsInGroups } from '../src/schains' +import { MAINNET_RPC_URL } from '../src/constants' +import { Node } from '../src/interfaces' + +import { + ETH_PRIVATE_KEY, + NODES_IN_SCHAIN, + validatorsContract, + managerContract, + schainsContract, + addAllPermissions, + generateWallets, + initDefaultValidator, + linkNodes, + registerNodes, + addTestSchainTypes, + createSchain, + randomString, + nodeNamesToIds +} from './testUtils' + +describe('nodes module test', () => { + let nodes: Contract + let schainsInternal: Contract + let wallet: Wallet + let nodeNames: string[] + let nodeIds: number[] + const chainName = randomString() + + beforeAll(async () => { + console.log('initializing provider and contracts') + const provider = await getMainnetProvider(MAINNET_RPC_URL, false) + wallet = new Wallet(ETH_PRIVATE_KEY, provider) + + const managerAbi = getMainnetManagerAbi() + const validators = validatorsContract(managerAbi, wallet) + schainsInternal = schainsInternalContract(managerAbi, wallet) + const schains = schainsContract(managerAbi, wallet) + const manager = managerContract(managerAbi, wallet) + + nodes = nodesContract(managerAbi, provider) + + await addAllPermissions(validators, schainsInternal, schains, wallet) + await initDefaultValidator(validators) + const wallets = await generateWallets(provider, wallet, NODES_IN_SCHAIN) + await linkNodes(validators, wallet, wallets) + nodeNames = await registerNodes(nodes, wallets) + + nodeIds = await nodeNamesToIds(nodes, nodeNames) + + await addTestSchainTypes(schainsInternal) + await createSchain(schains, chainName, wallet.address) + }) + test('getNodes', async () => { + const chainHash = id(chainName) + const nodeIds = await getNodeIdsInGroups(schainsInternal, [chainHash]) + const nodesRes: Node[] = await getNodes(nodes, schainsInternal, nodeIds[0], chainHash) + + expect(nodesRes).toBeArrayOfSize(NODES_IN_SCHAIN) + + expect(nodesRes[0].endpoints).toBeDefined + expect(nodesRes[0].endpoints?.domain.http).toBeString + expect(nodesRes[0].endpoints?.domain.https).toBeString + expect(nodesRes[0].endpoints?.domain.ws).toBeString + expect(nodesRes[0].endpoints?.domain.wss).toBeString + + expect(nodesRes[0].endpoints?.ip.http).toBeString + expect(nodesRes[0].endpoints?.ip.https).toBeString + expect(nodesRes[0].endpoints?.ip.ws).toBeString + expect(nodesRes[0].endpoints?.ip.wss).toBeString + }) +}) diff --git a/network-browser/tests/testUtils.ts b/network-browser/tests/testUtils.ts new file mode 100644 index 00000000..f2c3cad1 --- /dev/null +++ b/network-browser/tests/testUtils.ts @@ -0,0 +1,238 @@ +import { + type Provider, + Contract, + Wallet, + TransactionReceipt, + parseEther, + Signer, + getBytes, + solidityPackedKeccak256, + BytesLike, + hexlify, + id, + zeroPadValue, + TransactionResponse +} from 'ethers' +import { ec } from 'elliptic' + +import { getMainnetManagerAbi } from '../src/contracts' + +const secp256k1EC = new ec('secp256k1') + +import { type SkaleManagerAbi } from '../src/interfaces' + +export const ETH_PRIVATE_KEY = process.env.ETH_PRIVATE_KEY! + +export const NODES_IN_SCHAIN = 16 +export const TEST_VALIDATOR_NAME = 'test_val' +const TEST_VALIDATOR_ID = 1n +const ETH_TRANSFER_AMOUNT = '0.1' +const CONFIRMATION_BLOCKS = 2 +const GAS_MULTIPLIER = 1.2 + +export function validatorsContract(abi: SkaleManagerAbi, wallet: Wallet): Contract { + return new Contract(abi.validator_service_address, abi.validator_service_abi, wallet) +} + +export function schainsContract(abi: SkaleManagerAbi, wallet: Wallet): Contract { + return new Contract(abi.schains_address, abi.schains_abi, wallet) +} + +export function managerContract(abi: SkaleManagerAbi, wallet: Wallet): Contract { + return new Contract(abi.skale_manager_address, abi.skale_manager_abi, wallet) +} + +export async function addAllPermissions( + validators: Contract, + schainsInternal: Contract, + schains: Contract, + wallet: Wallet +): Promise { + const VALIDATOR_MANAGER_ROLE = await validators.VALIDATOR_MANAGER_ROLE() + let hasRole = await validators.hasRole(VALIDATOR_MANAGER_ROLE, wallet.address) + if (!hasRole) { + console.log('granting ROLE: VALIDATOR_MANAGER_ROLE') + await sendTx(validators.grantRole, [VALIDATOR_MANAGER_ROLE, wallet.address]) + } + const SCHAIN_TYPE_MANAGER_ROLE = await schainsInternal.SCHAIN_TYPE_MANAGER_ROLE() + hasRole = await schainsInternal.hasRole(SCHAIN_TYPE_MANAGER_ROLE, wallet.address) + if (!hasRole) { + console.log('granting ROLE: SCHAIN_TYPE_MANAGER_ROLE') + await sendTx(schainsInternal.grantRole, [SCHAIN_TYPE_MANAGER_ROLE, wallet.address]) + } + const SCHAIN_CREATOR_ROLE = await schains.SCHAIN_CREATOR_ROLE() + hasRole = await schains.hasRole(SCHAIN_CREATOR_ROLE, wallet.address) + if (!hasRole) { + console.log('granting ROLE: SCHAIN_CREATOR_ROLE') + await sendTx(schains.grantRole, [SCHAIN_CREATOR_ROLE, wallet.address]) + } + console.log('all roles granted') +} + +export async function initDefaultValidator(validators: Contract): Promise { + let number = await validators.numberOfValidators() + if (number === 0n) { + console.log('going to register validator') + await sendTx(validators.registerValidator, [TEST_VALIDATOR_NAME, '', 10, 0]) + + number = await validators.numberOfValidators() + console.log(`number of active validators: ${number}`) + + console.log('going to enable validator') + await sendTx(validators.enableValidator, [1]) + console.log('validator registered and enabled') + } else { + console.log('validator already exist, skipping') + } +} + +export async function generateWallets( + provider: Provider, + adminWallet: Wallet, + num: number +): Promise { + const wallets: Promise[] = [] + const baseNonce = await adminWallet.getNonce() + for (let i = 0; i < num; i++) { + console.log(`${i + 1}/${num} generating new wallet...`) + wallets.push(generateWallet(provider, adminWallet, baseNonce + i)) + } + return await Promise.all(wallets) +} + +export async function generateWallet( + provider: Provider, + adminWallet: Wallet, + nonce?: number +): Promise { + const wallet = Wallet.createRandom() + wallet.connect(provider) + await sendEth(adminWallet, wallet.address, ETH_TRANSFER_AMOUNT, provider, nonce) + console.log(`new wallet generated: ${wallet.address}, eth transferred: ${ETH_TRANSFER_AMOUNT}`) + return new Wallet(wallet.privateKey, provider) +} + +async function sendEth( + senderWallet: Wallet, + recipientAddress: string, + amountEth: string, // Amount in ETH, e.g., "0.1" for 0.1 ETH + provider: Provider, + nonce?: number +): Promise { + senderWallet = senderWallet.connect(provider) + const amountWei = parseEther(amountEth) + const tx = { + to: recipientAddress, + value: amountWei, + nonce: nonce + } + const txResponse = await senderWallet.sendTransaction(tx) + return provider.waitForTransaction(txResponse.hash) +} + +async function getValidatorIdSignature(validatorId: bigint, signer: Signer) { + return await signer.signMessage(getBytes(solidityPackedKeccak256(['uint'], [validatorId]))) +} + +export async function linkNodes( + validators: Contract, + adminWallet: Wallet, + wallets: Wallet[] +): Promise { + const baseNonce = await adminWallet.getNonce() + const promises = wallets.map(async (wallet, i) => { + console.log(`linking node address: ${wallet.address}`) + const signature = await getValidatorIdSignature(TEST_VALIDATOR_ID, wallet) + await validators.linkNodeAddress(wallet.address, signature, { nonce: baseNonce + i }) + console.log(`linked node address: ${wallet.address}`) + }) + await Promise.all(promises) +} + +export async function registerNodes(nodes: Contract, wallets: Wallet[]): Promise { + const promises = wallets.map(async (wallet, i) => { + console.log(`${i + 1}/${wallets.length} registering node for: ${wallet.address}`) + const managerAbi = getMainnetManagerAbi() + const manager = managerContract(managerAbi, wallet) + + const { ip, port, name, domainName, publicIp } = generateNodeInfo(wallet.address, i) + + const skaleNonce = randNum(0, 10000) + const pkPartsBytes = getPublicKey(wallet) + + await sendTx(manager.createNode, [ + port, + skaleNonce, + ipToHex(ip), + ipToHex(publicIp), + pkPartsBytes, + name, + domainName + ]) + console.log(`new node created: ${ip}:${port} - ${name} - ${domainName}`) + return name + }) + return await Promise.all(promises) +} + +export async function nodeNamesToIds(nodes: Contract, nodeNames: string[]): Promise { + const promises = nodeNames.map(async (name, i) => { + return await nodes.nodesNameToIndex(id(name)) + }) + return await Promise.all(promises) +} + +export async function addTestSchainTypes(schains: Contract): Promise { + await sendTx(schains.addSchainType, [8, NODES_IN_SCHAIN]) + await sendTx(schains.addSchainType, [8, NODES_IN_SCHAIN]) +} + +export async function createSchain(schains: Contract, name: string, owner: string): Promise { + console.log(`creating new schain: ${name}, owner: ${owner}`) + await sendTx(schains.addSchainByFoundation, [1000, 1, 10, name, owner, owner, []]) + console.log(`schain created: ${name}`) +} + +function generateNodeInfo(address: string, seed: number): any { + return { + ip: getRandomIp(), + port: 10000 + seed * randNum(1, 10), + name: `node-${address}`, + domainName: `nd.${address}.com`, + publicIp: getRandomIp() + } +} + +function getRandomIp(): string { + return `${randNum(0, 255)}.${randNum(0, 255)}.${randNum(0, 255)}.${randNum(0, 255)}` +} + +function randNum(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +function getPublicKey(wallet: Wallet): [BytesLike, BytesLike] { + const publicKey = secp256k1EC.keyFromPrivate(wallet.privateKey.slice(2)).getPublic() + const pubA = zeroPadValue(hexlify(publicKey.getX().toBuffer()), 32) + const pubB = zeroPadValue(hexlify(publicKey.getY().toBuffer()), 32) + return [pubA, pubB] +} + +function ipToHex(ip: string): string { + const parts = ip.split('.') + const hexParts = parts.map((part) => parseInt(part).toString(16).padStart(2, '0')).join('') + const hexIp = `0x${hexParts}` + return hexIp +} + +export async function sendTx(func: any, args: any[]): Promise { + const estimatedGas = await func.estimateGas(...args) + const response: TransactionResponse = await func(...args, { + gasLimit: BigInt(Math.round(Number(estimatedGas) * GAS_MULTIPLIER)) + }) + return await response.wait(CONFIRMATION_BLOCKS) +} + +export function randomString(): string { + return (Math.random() + 1).toString(36).substring(7) +} diff --git a/network-browser/tests/tools.test.ts b/network-browser/tests/tools.test.ts new file mode 100644 index 00000000..207137d3 --- /dev/null +++ b/network-browser/tests/tools.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from 'bun:test' + +import { + stringifyBigInt, + hexToIp, + currentTimestamp, + delay, + withTimeout, + chainIdHex, + chainIdInt +} from '../src/tools' +import { isValidNumber } from '../src/envTools' +import { BrowserTimeoutError } from '../src/errors' + +describe('tools module test', () => { + test('stringifyBigInt', () => { + const res = stringifyBigInt({ a: 1000n }) + expect(res).toBe('{\n "a": "1000"\n}') + }) + test('hexToIp', () => { + expect(hexToIp('0x5E0C3880')).toBe('94.12.56.128') + expect(hexToIp('01010101')).toBe('1.1.1.1') + expect(hexToIp('0xFFFFFFFF')).toBe('255.255.255.255') + expect(hexToIp('0xFFFFFFFFFFFFFFFF')).toBe('255.255.255.255') + }) + test('currentTimestamp', () => { + const ts = currentTimestamp() + expect(typeof ts).toBe('number') + expect(ts.toString()).toHaveLength(10) + }) + + test('delay', () => { + expect(delay(10)).resolves + }) + + test('withTimeout', async () => { + expect(withTimeout(delay(10), 100)).resolves.toBe(undefined) + expect(withTimeout(delay(1000), 100)).rejects.toThrow( + new BrowserTimeoutError('Operation timed out') + ) + }) + + test('chainId', () => { + expect(chainIdHex('elated-tan-skat')).toBe('0x79f99296') + expect(chainIdInt('elated-tan-skat')).toBe(2046399126) + }) + + test('isValidNumber', () => { + expect(isValidNumber('123')).toBeTrue + expect(isValidNumber('12.34')).toBeTrue + expect(isValidNumber('-123')).toBeTrue + expect(isValidNumber('abc')).toBeFalse + expect(isValidNumber('123abc')).toBeFalse + expect(isValidNumber('')).toBeFalse + }) +}) diff --git a/network-browser/tsconfig.json b/network-browser/tsconfig.json index 79e6b511..da516b48 100644 --- a/network-browser/tsconfig.json +++ b/network-browser/tsconfig.json @@ -15,6 +15,5 @@ /* Advanced Options */ "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ - }, - "exclude": ["**/tests", "**/build"] + } }