diff --git a/packages/neuron-wallet/src/database/chain/entities/transaction.ts b/packages/neuron-wallet/src/database/chain/entities/transaction.ts index 5404b78097..865825c048 100644 --- a/packages/neuron-wallet/src/database/chain/entities/transaction.ts +++ b/packages/neuron-wallet/src/database/chain/entities/transaction.ts @@ -83,6 +83,12 @@ export default class Transaction extends BaseEntity { }) updatedAt!: string + // only used for check fork in indexer mode + @Column({ + type: 'boolean', + }) + confirmed: boolean = false + @OneToMany(_type => InputEntity, input => input.transaction) inputs!: InputEntity[] diff --git a/packages/neuron-wallet/src/database/chain/migrations/1562038960990-AddStatusToTx.ts b/packages/neuron-wallet/src/database/chain/migrations/1562038960990-AddStatusToTx.ts index b8f91ce94f..233ab2c59c 100644 --- a/packages/neuron-wallet/src/database/chain/migrations/1562038960990-AddStatusToTx.ts +++ b/packages/neuron-wallet/src/database/chain/migrations/1562038960990-AddStatusToTx.ts @@ -1,21 +1,33 @@ -import {MigrationInterface, QueryRunner, TableColumn, getConnection} from "typeorm"; +import {MigrationInterface, QueryRunner, TableColumn, getConnection, In} from "typeorm"; import TransactionEntity from '../entities/transaction' import { OutputStatus } from '../../../services/tx/params' import { TransactionStatus } from '../../../types/cell-types' +import OutputEntity from 'database/chain/entities/output' export class AddStatusToTx1562038960990 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE 'transaction' ADD COLUMN 'status' varchar NOT NULL DEFAULT '';`) + // TransactionStatus.Success = 'success' + await queryRunner.query(`ALTER TABLE 'transaction' ADD COLUMN 'status' varchar NOT NULL DEFAULT 'success';`) + + const pendingTxHashes: string[] = (await getConnection() + .getRepository(OutputEntity) + .createQueryBuilder('output') + .select(`output.outPointTxHash`, 'txHash') + .where({ + status: OutputStatus.Sent + }) + .getRawMany()) + .filter(output => output.txHash) + await getConnection() + .createQueryBuilder() + .update(TransactionEntity) + .set({ status: TransactionStatus.Pending }) + .where({ + hash: In(pendingTxHashes) + }) + .execute() - const txs = await getConnection() - .getRepository(TransactionEntity) - .find({ relations: ['inputs', 'outputs'] }) - const updatedTxs = txs.map(tx => { - tx.status = tx.outputs[0].status === OutputStatus.Sent ? TransactionStatus.Pending : TransactionStatus.Success - return tx - }) - await getConnection().manager.save(updatedTxs) await queryRunner.changeColumn('transaction', 'status', new TableColumn({ name: 'status', type: 'varchar', diff --git a/packages/neuron-wallet/src/database/chain/migrations/1565693320664-AddConfirmed.ts b/packages/neuron-wallet/src/database/chain/migrations/1565693320664-AddConfirmed.ts new file mode 100644 index 0000000000..69448a80d7 --- /dev/null +++ b/packages/neuron-wallet/src/database/chain/migrations/1565693320664-AddConfirmed.ts @@ -0,0 +1,13 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class AddConfirmed1565693320664 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE 'transaction' ADD COLUMN 'confirmed' boolean NOT NULL DEFAULT false;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('transaction', 'confirmed') + } + +} diff --git a/packages/neuron-wallet/src/database/chain/ormconfig.ts b/packages/neuron-wallet/src/database/chain/ormconfig.ts index da39c8fc03..98f2d75937 100644 --- a/packages/neuron-wallet/src/database/chain/ormconfig.ts +++ b/packages/neuron-wallet/src/database/chain/ormconfig.ts @@ -12,6 +12,7 @@ import SyncInfo from './entities/sync-info' import { InitMigration1561695143591 } from './migrations/1561695143591-InitMigration' import { AddStatusToTx1562038960990 } from './migrations/1562038960990-AddStatusToTx' +import { AddConfirmed1565693320664 } from './migrations/1565693320664-AddConfirmed' export const CONNECTION_NOT_FOUND_NAME = 'ConnectionNotFoundError' @@ -31,7 +32,7 @@ const connectOptions = async (genesisBlockHash: string): Promise { + return this.core.rpc.deindexLockHash(lockHash) + } + + public indexLockHash = async (lockHash: string, indexFrom = '0') => { + return this.core.rpc.indexLockHash(lockHash, indexFrom) + } + + public getTransactionByLockHash = async ( + lockHash: string, + page: string, + per: string, + reverseOrder: boolean = false + ) => { + const result = await this.core.rpc.getTransactionsByLockHash(lockHash, page, per, reverseOrder) + return result + } + + public getLockHashIndexStates = async () => { + return this.core.rpc.getLockHashIndexStates() + } + + public getLiveCellsByLockHash = async ( + lockHash: string, + page: string, + per: string, + reverseOrder: boolean = false + ) => { + const result = await this.core.rpc.getLiveCellsByLockHash(lockHash, page, per, reverseOrder) + return result + } +} diff --git a/packages/neuron-wallet/src/services/indexer/queue.ts b/packages/neuron-wallet/src/services/indexer/queue.ts new file mode 100644 index 0000000000..eebf7c5dc3 --- /dev/null +++ b/packages/neuron-wallet/src/services/indexer/queue.ts @@ -0,0 +1,221 @@ +import { Subject, Subscription } from 'rxjs' +import Utils from 'services/sync/utils' +import logger from 'utils/logger' +import GetBlocks from 'services/sync/get-blocks' +import { Transaction } from 'types/cell-types' +import TypeConvert from 'types/type-convert' +import BlockNumber from 'services/sync/block-number' +import AddressesUsedSubject from 'models/subjects/addresses-used-subject' +import LockUtils from 'models/lock-utils' +import TransactionPersistor from 'services/tx/transaction-persistor' +import IndexerTransaction from 'services/tx/indexer-transaction' + +import IndexerRPC from './indexer-rpc' + +enum TxPointType { + CreatedBy = 'createdBy', + ConsumedBy = 'consumedBy', +} + +export default class IndexerQueue { + private lockHashes: string[] + private indexerRPC: IndexerRPC + private getBlocksService: GetBlocks + private per = 50 + private interval = 1000 + private blockNumberService: BlockNumber + private tipNumberListener: Subscription + private tipBlockNumber: bigint = BigInt(-1) + + private stopped = false + private indexed = false + + private inProcess = false + + private resetFlag = false + + constructor(url: string, lockHashes: string[], tipNumberSubject: Subject) { + this.lockHashes = lockHashes + this.indexerRPC = new IndexerRPC(url) + this.getBlocksService = new GetBlocks() + this.blockNumberService = new BlockNumber() + this.tipNumberListener = tipNumberSubject.subscribe(async (num: string) => { + if (num) { + this.tipBlockNumber = BigInt(num) + } + }) + } + + public setLockHashes = (lockHashes: string[]): void => { + this.lockHashes = lockHashes + this.indexed = false + } + + public reset = () => { + this.resetFlag = true + } + + /* eslint no-await-in-loop: "off" */ + /* eslint no-restricted-syntax: "off" */ + public start = async () => { + while (!this.stopped) { + try { + this.inProcess = true + if (this.resetFlag) { + await this.blockNumberService.updateCurrent(BigInt(0)) + this.resetFlag = false + } + const { lockHashes } = this + const currentBlockNumber: bigint = await this.blockNumberService.getCurrent() + if (!this.indexed || currentBlockNumber !== this.tipBlockNumber) { + if (!this.indexed) { + await this.indexLockHashes(lockHashes) + this.indexed = true + } + const minBlockNumber = await this.getCurrentBlockNumber(lockHashes) + for (const lockHash of lockHashes) { + await this.pipeline(lockHash, TxPointType.CreatedBy, currentBlockNumber) + } + for (const lockHash of lockHashes) { + await this.pipeline(lockHash, TxPointType.ConsumedBy, currentBlockNumber) + } + if (minBlockNumber) { + await this.blockNumberService.updateCurrent(minBlockNumber) + } + } + await this.yield(this.interval) + } catch (err) { + if (err.message.startsWith('connect ECONNREFUSED')) { + logger.debug('sync indexer error:', err) + } else { + logger.error('sync indexer error:', err) + } + } finally { + await this.yield() + this.inProcess = false + } + } + } + + public processFork = async () => { + while (!this.stopped) { + try { + const tip = this.tipBlockNumber + const txs = await IndexerTransaction.txHashes() + for (const tx of txs) { + const result = await this.getBlocksService.getTransaction(tx.hash) + if (!result) { + await IndexerTransaction.deleteTxWhenFork(tx.hash) + } else if (tip - BigInt(tx.blockNumber) >= 1000) { + await IndexerTransaction.confirm(tx.hash) + } + } + } catch (err) { + logger.error(`indexer delete forked tx:`, err) + } finally { + await this.yield(10000) + } + } + } + + public getCurrentBlockNumber = async (lockHashes: string[]) => { + // get lock hash indexer status + const lockHashIndexStates = await this.indexerRPC.getLockHashIndexStates() + const blockNumbers = lockHashIndexStates + .filter(state => lockHashes.includes(state.lockHash)) + .map(state => state.blockNumber) + const uniqueBlockNumbers = [...new Set(blockNumbers)] + const blockNumbersBigInt = uniqueBlockNumbers.map(num => BigInt(num)) + const minBlockNumber = Utils.min(blockNumbersBigInt) + return minBlockNumber + } + + public indexLockHashes = async (lockHashes: string[]) => { + const lockHashIndexStates = await this.indexerRPC.getLockHashIndexStates() + const indexedLockHashes: string[] = lockHashIndexStates.map(state => state.lockHash) + const nonIndexedLockHashes = lockHashes.filter(i => !indexedLockHashes.includes(i)) + + await Utils.mapSeries(nonIndexedLockHashes, async (lockHash: string) => { + await this.indexerRPC.indexLockHash(lockHash) + }) + } + + // type: 'createdBy' | 'consumedBy' + public pipeline = async (lockHash: string, type: TxPointType, startBlockNumber: bigint) => { + let page = 0 + let stopped = false + while (!stopped) { + const txs = await this.indexerRPC.getTransactionByLockHash(lockHash, page.toString(), this.per.toString()) + if (txs.length < this.per) { + stopped = true + } + for (const tx of txs) { + let txPoint: CKBComponents.TransactionPoint | null = null + if (type === TxPointType.CreatedBy) { + txPoint = tx.createdBy + } else if (type === TxPointType.ConsumedBy) { + txPoint = tx.consumedBy + } + + if ( + txPoint && + (BigInt(txPoint.blockNumber) >= startBlockNumber || this.tipBlockNumber - BigInt(txPoint.blockNumber) < 1000) + ) { + const transactionWithStatus = await this.getBlocksService.getTransaction(txPoint.txHash) + const ckbTransaction: CKBComponents.Transaction = transactionWithStatus.transaction + const transaction: Transaction = TypeConvert.toTransaction(ckbTransaction) + // tx timestamp / blockNumber / blockHash + const { blockHash } = transactionWithStatus.txStatus + if (blockHash) { + const blockHeader = await this.getBlocksService.getHeader(blockHash) + transaction.blockHash = blockHash + transaction.blockNumber = blockHeader.number + transaction.timestamp = blockHeader.timestamp + } + // broadcast address used + const txEntity = await TransactionPersistor.saveFetchTx(transaction) + + let address: string | undefined + if (type === TxPointType.CreatedBy) { + address = LockUtils.lockScriptToAddress(transaction.outputs![+txPoint.index].lock) + } else if (type === TxPointType.ConsumedBy) { + const input = txEntity.inputs[+txPoint.index] + const output = await IndexerTransaction.updateInputLockHash(input.outPointTxHash!, input.outPointIndex!) + if (output) { + address = LockUtils.lockScriptToAddress(output.lock) + } + } + if (address) { + AddressesUsedSubject.getSubject().next([address]) + } + } + } + page += 1 + } + } + + public stop = () => { + this.tipNumberListener.unsubscribe() + this.stopped = true + } + + public waitForDrained = async (timeout: number = 5000) => { + const startAt: number = +new Date() + while (this.inProcess) { + const now: number = +new Date() + if (now - startAt > timeout) { + return + } + await this.yield(50) + } + } + + public stopAndWait = async () => { + this.stop() + await this.waitForDrained() + } + + private yield = async (millisecond: number = 1) => { + await Utils.sleep(millisecond) + } +} diff --git a/packages/neuron-wallet/src/services/sync/get-blocks.ts b/packages/neuron-wallet/src/services/sync/get-blocks.ts index 9c8ebda8a4..34656c4d38 100644 --- a/packages/neuron-wallet/src/services/sync/get-blocks.ts +++ b/packages/neuron-wallet/src/services/sync/get-blocks.ts @@ -1,6 +1,6 @@ import Core from '@nervosnetwork/ckb-sdk-core' -import { Block } from 'types/cell-types' +import { Block, BlockHeader } from 'types/cell-types' import TypeConvert from 'types/type-convert' import { NetworkWithID } from 'services/networks' import CheckAndSave from './check-and-save' @@ -55,6 +55,16 @@ export default class GetBlocks { return block } + public getTransaction = async (hash: string): Promise => { + const tx = await core.rpc.getTransaction(hash) + return tx + } + + public getHeader = async (hash: string): Promise => { + const result = await core.rpc.getHeader(hash) + return TypeConvert.toBlockHeader(result) + } + public static getBlockByNumber = async (num: string): Promise => { const block = await core.rpc.getBlockByNumber(num) return TypeConvert.toBlock(block) diff --git a/packages/neuron-wallet/src/services/sync/utils.ts b/packages/neuron-wallet/src/services/sync/utils.ts index f52fd1d5b0..eeb6d3cd1f 100644 --- a/packages/neuron-wallet/src/services/sync/utils.ts +++ b/packages/neuron-wallet/src/services/sync/utils.ts @@ -48,4 +48,20 @@ export default class Utils { } return result } + + public static min = (array: bigint[]): bigint | undefined => { + let minValue = array[0] + if (!minValue) { + return undefined + } + + for (let i = 1; i < array.length; ++i) { + const value = array[i] + if (value < minValue) { + minValue = value + } + } + + return minValue + } } diff --git a/packages/neuron-wallet/src/services/tx/indexer-transaction.ts b/packages/neuron-wallet/src/services/tx/indexer-transaction.ts new file mode 100644 index 0000000000..5beb9393d7 --- /dev/null +++ b/packages/neuron-wallet/src/services/tx/indexer-transaction.ts @@ -0,0 +1,96 @@ +import { getConnection } from 'typeorm' +import TransactionEntity from 'database/chain/entities/transaction' +import Utils from 'services/sync/utils' +import InputEntity from 'database/chain/entities/input' +import OutputEntity from 'database/chain/entities/output' +import { TransactionStatus } from 'types/cell-types' +import { OutputStatus } from './params' + +export default class IndexerTransaction { + public static txHashes = async () => { + const txs = await getConnection() + .getRepository(TransactionEntity) + .createQueryBuilder('tx') + .where({ + confirmed: false, + status: TransactionStatus.Success, + }) + .getMany() + + return txs + } + + public static confirm = async (hash: string) => { + await getConnection().manager.update(TransactionEntity, hash, { confirmed: true }) + } + + public static deleteTxWhenFork = async (hash: string) => { + const tx = await getConnection() + .getRepository(TransactionEntity) + .findOne(hash, { relations: ['inputs', 'outputs'] }) + + if (!tx) { + return + } + + // reset previous output to OutputStatus.Live + await getConnection().transaction(async transactionalEntityManager => { + await Utils.mapSeries(tx.inputs, async (input: InputEntity) => { + if (!input.lockHash) { + return + } + + await transactionalEntityManager.update( + OutputEntity, + { + outPointTxHash: input.outPointTxHash, + outPointIndex: input.outPointIndex, + }, + { status: OutputStatus.Live } + ) + }) + + await transactionalEntityManager.remove([tx, ...tx.inputs, ...tx.outputs]) + }) + } + + public static updateInputLockHash = async (txHash: string, index: string) => { + const output = await getConnection() + .getRepository(OutputEntity) + .createQueryBuilder('output') + .where({ + outPointTxHash: txHash, + outPointIndex: index, + }) + .getOne() + + if (output) { + await getConnection().manager.update( + InputEntity, + { + outPointTxHash: txHash, + outPointIndex: index, + }, + { + lockHash: output.lockHash, + capacity: output.capacity, + } + ) + output.status = OutputStatus.Dead + await getConnection().manager.save(output) + + const tx = await getConnection() + .getRepository(TransactionEntity) + .createQueryBuilder('tx') + .where({ + hash: output.outPointTxHash, + }) + .getOne() + if (tx) { + tx.emitUpdate() + } + } + + return output + } +} diff --git a/packages/neuron-wallet/src/startup/sync-block-task/indexer.ts b/packages/neuron-wallet/src/startup/sync-block-task/indexer.ts new file mode 100644 index 0000000000..79ad26e4a2 --- /dev/null +++ b/packages/neuron-wallet/src/startup/sync-block-task/indexer.ts @@ -0,0 +1,55 @@ +import { remote } from 'electron' +import AddressService from 'services/addresses' +import LockUtils from 'models/lock-utils' +import IndexerQueue from 'services/indexer/queue' + +import { initDatabase } from './init-database' + +const { nodeService, addressDbChangedSubject, walletCreatedSubject } = remote.require( + './startup/sync-block-task/params' +) + +// maybe should call this every time when new address generated +// load all addresses and convert to lockHashes +export const loadAddressesAndConvert = async (): Promise => { + const addresses: string[] = (await AddressService.allAddresses()).map(addr => addr.address) + const lockHashes: string[] = await LockUtils.addressesToAllLockHashes(addresses) + return lockHashes +} + +// call this after network switched +let indexerQueue: IndexerQueue | undefined +export const switchNetwork = async (nodeURL: string) => { + // stop all blocks service + if (indexerQueue) { + await indexerQueue.stopAndWait() + } + + // disconnect old connection and connect to new database + await initDatabase() + // load lockHashes + const lockHashes: string[] = await loadAddressesAndConvert() + // start sync blocks service + indexerQueue = new IndexerQueue(nodeURL, lockHashes, nodeService.tipNumberSubject) + + addressDbChangedSubject.subscribe(async (event: string) => { + // ignore update and remove + if (event === 'AfterInsert') { + const hashes: string[] = await loadAddressesAndConvert() + if (indexerQueue) { + indexerQueue.setLockHashes(hashes) + } + } + }) + + walletCreatedSubject.subscribe(async (type: string) => { + if (type === 'import') { + if (indexerQueue) { + indexerQueue.reset() + } + } + }) + + indexerQueue.start() + indexerQueue.processFork() +} diff --git a/packages/neuron-wallet/src/startup/sync-block-task/sync.ts b/packages/neuron-wallet/src/startup/sync-block-task/sync.ts new file mode 100644 index 0000000000..bbf26d2bd0 --- /dev/null +++ b/packages/neuron-wallet/src/startup/sync-block-task/sync.ts @@ -0,0 +1,65 @@ +import { remote } from 'electron' +import AddressService from 'services/addresses' +import LockUtils from 'models/lock-utils' +import BlockListener from 'services/sync/block-listener' + +import { initDatabase } from './init-database' + +const { nodeService, addressDbChangedSubject, walletCreatedSubject } = remote.require( + './startup/sync-block-task/params' +) + +// pass to task a main process subject +// AddressesUsedSubject.setSubject(addressesUsedSubject) + +// maybe should call this every time when new address generated +// load all addresses and convert to lockHashes +export const loadAddressesAndConvert = async (): Promise => { + const addresses: string[] = (await AddressService.allAddresses()).map(addr => addr.address) + const lockHashes: string[] = await LockUtils.addressesToAllLockHashes(addresses) + return lockHashes +} + +// call this after network switched +let blockListener: BlockListener | undefined +export const switchNetwork = async () => { + // stop all blocks service + if (blockListener) { + await blockListener.stopAndWait() + } + + // disconnect old connection and connect to new database + await initDatabase() + // load lockHashes + const lockHashes: string[] = await loadAddressesAndConvert() + // start sync blocks service + blockListener = new BlockListener(lockHashes, nodeService.tipNumberSubject) + + addressDbChangedSubject.subscribe(async (event: string) => { + // ignore update and remove + if (event === 'AfterInsert') { + const hashes: string[] = await loadAddressesAndConvert() + if (blockListener) { + blockListener.setLockHashes(hashes) + } + } + }) + + const regenerateListener = async () => { + if (blockListener) { + await blockListener.stopAndWait() + } + // wait former queue to be drained + const hashes: string[] = await loadAddressesAndConvert() + blockListener = new BlockListener(hashes, nodeService.tipNumberSubject) + await blockListener.start(true) + } + + walletCreatedSubject.subscribe(async (type: string) => { + if (type === 'import') { + await regenerateListener() + } + }) + + blockListener.start() +} diff --git a/packages/neuron-wallet/src/startup/sync-block-task/task.ts b/packages/neuron-wallet/src/startup/sync-block-task/task.ts index 94559ef9a4..80def9a331 100644 --- a/packages/neuron-wallet/src/startup/sync-block-task/task.ts +++ b/packages/neuron-wallet/src/startup/sync-block-task/task.ts @@ -1,86 +1,45 @@ import { remote } from 'electron' import { initConnection as initAddressConnection } from 'database/address/ormconfig' -import AddressService from 'services/addresses' -import LockUtils from 'models/lock-utils' import AddressesUsedSubject from 'models/subjects/addresses-used-subject' -import BlockListener from 'services/sync/block-listener' import { NetworkWithID } from 'services/networks' import { register as registerTxStatusListener } from 'listeners/tx-status' import { register as registerAddressListener } from 'listeners/address' +import IndexerRPC from 'services/indexer/indexer-rpc' +import Utils from 'services/sync/utils' -import { initDatabase } from './init-database' +import { switchNetwork as syncSwitchNetwork } from './sync' +import { switchNetwork as indexerSwitchNetwork } from './indexer' // register to listen address updates registerAddressListener() -const { - nodeService, - addressDbChangedSubject, - addressesUsedSubject, - databaseInitSubject, - walletCreatedSubject, -} = remote.require('./startup/sync-block-task/params') +const { addressesUsedSubject, databaseInitSubject } = remote.require('./startup/sync-block-task/params') // pass to task a main process subject AddressesUsedSubject.setSubject(addressesUsedSubject) -// maybe should call this every time when new address generated -// load all addresses and convert to lockHashes -export const loadAddressesAndConvert = async (): Promise => { - const addresses: string[] = (await AddressService.allAddresses()).map(addr => addr.address) - const lockHashes: string[] = await LockUtils.addressesToAllLockHashes(addresses) - return lockHashes -} - -// call this after network switched -let blockListener: BlockListener | undefined -export const switchNetwork = async () => { - // stop all blocks service - if (blockListener) { - await blockListener.stopAndWait() +export const testIndexer = async (url: string): Promise => { + const indexerRPC = new IndexerRPC(url) + try { + await Utils.retry(3, 100, () => { + return indexerRPC.getLockHashIndexStates() + }) + return true + } catch { + return false } - - // disconnect old connection and connect to new database - await initDatabase() - // load lockHashes - const lockHashes: string[] = await loadAddressesAndConvert() - // start sync blocks service - blockListener = new BlockListener(lockHashes, nodeService.tipNumberSubject) - - addressDbChangedSubject.subscribe(async (event: string) => { - // ignore update and remove - if (event === 'AfterInsert') { - const hashes: string[] = await loadAddressesAndConvert() - if (blockListener) { - blockListener.setLockHashes(hashes) - } - } - }) - - const regenerateListener = async () => { - if (blockListener) { - await blockListener.stopAndWait() - } - // wait former queue to be drained - const hashes: string[] = await loadAddressesAndConvert() - blockListener = new BlockListener(hashes, nodeService.tipNumberSubject) - await blockListener.start(true) - } - - walletCreatedSubject.subscribe(async (type: string) => { - if (type === 'import') { - await regenerateListener() - } - }) - - blockListener.start() } export const run = async () => { await initAddressConnection() databaseInitSubject.subscribe(async (network: NetworkWithID | undefined) => { if (network) { - await switchNetwork() + const indexerEnabled = await testIndexer(network.remote) + if (indexerEnabled) { + await indexerSwitchNetwork(network.remote) + } else { + await syncSwitchNetwork() + } } }) registerTxStatusListener() diff --git a/packages/neuron-wallet/tests/services/sync/utils.test.ts b/packages/neuron-wallet/tests/services/sync/utils.test.ts new file mode 100644 index 0000000000..212b19b815 --- /dev/null +++ b/packages/neuron-wallet/tests/services/sync/utils.test.ts @@ -0,0 +1,24 @@ +import Utils from '../../../src/services/sync/utils' + +describe('Key tests', () => { + describe('min', () => { + it('with empty array', () => { + const arr: bigint[] = [] + const minValue = Utils.min(arr) + expect(minValue).toBe(undefined) + }) + + it('only one value', () => { + const value = BigInt(1) + const arr = [value] + const minValue = Utils.min(arr) + expect(minValue).toEqual(value) + }) + + it('two value', () => { + const arr = [BigInt(38050), BigInt(4058)] + const minValue = Utils.min(arr) + expect(minValue).toEqual(BigInt(4058)) + }) + }) +})