From 8c51787b62a938276b92cc22e0107dd873c33525 Mon Sep 17 00:00:00 2001 From: Ivan Vershigora Date: Mon, 25 Nov 2024 18:08:08 +0000 Subject: [PATCH] feat: add tests for receiving funds --- package.json | 3 +- tests/boost.test.ts | 6 ++ tests/receive.test.ts | 223 ++++++++++++++++++++++++++++++++++++++++++ tests/utils.ts | 60 +++++++++++- 4 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 tests/receive.test.ts diff --git a/package.json b/package.json index 0e718e7..efe8e8f 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,11 @@ "test": "yarn build && env mocha --exit -r ts-node/register 'tests/**/*.ts'", "test:boost": "yarn build && env mocha --exit -r ts-node/register 'tests/boost.test.ts'", "test:wallet": "yarn build && env mocha --exit -r ts-node/register 'tests/wallet.test.ts'", + "test:receive": "yarn build && env mocha --exit -r ts-node/register 'tests/receive.test.ts'", + "test:storage": "yarn build && env mocha --exit -r ts-node/register 'tests/storage.test.ts'", "test:electrum": "yarn build && env mocha --exit -r ts-node/register 'tests/electrum.test.ts'", "test:derivation": "yarn build && env mocha --exit -r ts-node/register 'tests/derivation.test.ts'", "test:transaction": "yarn build && env mocha --exit -r ts-node/register 'tests/transaction.test.ts'", - "test:storage": "yarn build && env mocha --exit -r ts-node/register 'tests/storage.test.ts'", "example": "ts-node example", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint:check": "eslint . --ext .js,.jsx,.ts,.tsx", diff --git a/tests/boost.test.ts b/tests/boost.test.ts index 3b784aa..9c0a2df 100644 --- a/tests/boost.test.ts +++ b/tests/boost.test.ts @@ -79,6 +79,12 @@ beforeEach(async function () { await wallet.refreshWallet({}); }); +afterEach(async function () { + if (wallet?.electrum) { + await wallet.electrum.disconnect(); + } +}); + describe('Boost', async function () { this.timeout(testTimeout); diff --git a/tests/receive.test.ts b/tests/receive.test.ts new file mode 100644 index 0000000..a1dff33 --- /dev/null +++ b/tests/receive.test.ts @@ -0,0 +1,223 @@ +import BitcoinJsonRpc from 'bitcoin-json-rpc'; +import { expect } from 'chai'; +import net from 'net'; +import tls from 'tls'; + +import { + EAddressType, + EAvailableNetworks, + EProtocol, + generateMnemonic, + sleep, + Wallet +} from '../src'; +import { + bitcoinURL, + electrumHost, + electrumPort, + initWaitForElectrumToSync, + MessageListener, + TWaitForElectrum +} from './utils'; + +const testTimeout = 60000; +let wallet: Wallet; +let waitForElectrum: TWaitForElectrum; +const rpc = new BitcoinJsonRpc(bitcoinURL); +const ml = new MessageListener(); + +beforeEach(async function () { + this.timeout(testTimeout); + ml.clear(); + + // Ensure sufficient balance in regtest + let balance = await rpc.getBalance(); + const address = await rpc.getNewAddress(); + await rpc.generateToAddress(1, address); + + while (balance < 10) { + await rpc.generateToAddress(10, address); + balance = await rpc.getBalance(); + } + + waitForElectrum = await initWaitForElectrumToSync( + { host: electrumHost, port: electrumPort }, + bitcoinURL + ); + + await waitForElectrum(); + + const mnemonic = generateMnemonic(); + + const res = await Wallet.create({ + mnemonic, + network: EAvailableNetworks.regtest, + addressType: EAddressType.p2wpkh, + electrumOptions: { + servers: [ + { + host: '127.0.0.1', + ssl: 60002, + tcp: 60001, + protocol: EProtocol.tcp + } + ], + net, + tls + }, + // reduce gap limit to speed up tests + gapLimitOptions: { + lookAhead: 2, + lookBehind: 2, + lookAheadChange: 2, + lookBehindChange: 2 + }, + onMessage: ml.onMessage + }); + if (res.isErr()) throw res.error; + wallet = res.value; + await wallet.refreshWallet({}); +}); + +afterEach(async function () { + if (wallet?.electrum) { + await wallet.electrum.disconnect(); + } +}); + +describe('Receive', async function () { + this.timeout(testTimeout); + + it('Should generate new receiving address', async () => { + const r = await wallet.getNextAvailableAddress(); + if (r.isErr()) throw r.error; + const address = r.value.addressIndex.address; + expect(address).to.be.a('string'); + expect(address).to.match(/^bcrt1/); // Regtest bech32 prefix + }); + + it('Should receive funds and update balance', async () => { + expect(wallet.data.balance).to.equal(0); + + const r = await wallet.getNextAvailableAddress(); + if (r.isErr()) throw r.error; + const address = r.value.addressIndex.address; + expect(address).to.be.a('string'); + expect(address).to.match(/^bcrt1/); // Regtest bech32 prefix + + const amount = 0.1; + const amountSats = amount * 10e7; + await rpc.sendToAddress(address, amount.toString()); + + await rpc.generateToAddress(1, await rpc.getNewAddress()); + await waitForElectrum(); + + await wallet.refreshWallet({}); + expect(wallet.getBalance()).to.equal(amountSats); + expect(wallet.balance).to.equal(amountSats); + expect(wallet.utxos.length).to.equal(1); + }); + + // failing, WIP + it.skip('Should track multiple receiving addresses', async () => { + const r1 = await wallet.getNextAvailableAddress(); + if (r1.isErr()) throw r1.error; + const address1 = r1.value.addressIndex.address; + const r2 = await wallet.getNextAvailableAddress(); + if (r2.isErr()) throw r2.error; + const address2 = r2.value.addressIndex.address; + + // Without any transactions addresses should match + expect(address1).to.equal(address2); + + // Send funds, get new address + await rpc.sendToAddress(address1, '0.1'); + await rpc.generateToAddress(1, await rpc.getNewAddress()); + await waitForElectrum(); + const r3 = await wallet.getNextAvailableAddress(); + if (r3.isErr()) throw r3.error; + const address3 = r3.value.addressIndex.address; + + // After a transaction, addresses should differ + expect(address1).to.not.equal(address3); + + const receivePromise = ml.waitFor('transactionReceived'); + + // Second transaction + await rpc.sendToAddress(address3, '0.2'); + await rpc.generateToAddress(1, await rpc.getNewAddress()); + await waitForElectrum(); + + // await sleep(1000); + await receivePromise; + while (wallet.isRefreshing) { + console.info('wallet.isRefreshing', wallet.isRefreshing); + await sleep(10); + } + // await wallet.refreshWallet(); + + console.log(wallet.data); + + // Check balances + expect(wallet.balance).to.equal(0.3 * 10e7); // 0.3 BTC in sats + + // Test getAddressBalance + const balance1 = await wallet.getAddressBalance(address1); + if (balance1.isErr()) throw balance1.error; + expect(balance1.value.confirmed).to.equal(0.1 * 10e7); + const balance3 = await wallet.getAddressBalance(address3); + if (balance3.isErr()) throw balance3.error; + expect(balance3.value.confirmed).to.equal(0.2 * 10e7); + + // Test getAddressesBalance + const combinedBalance = await wallet.getAddressesBalance([ + address1, + address3 + ]); + if (combinedBalance.isErr()) throw combinedBalance.error; + expect(combinedBalance.value).to.equal(0.3 * 10e7); + }); + + it('Should handle unconfirmed transactions', async () => { + const r = await wallet.getNextAvailableAddress(); + if (r.isErr()) throw r.error; + const address = r.value.addressIndex.address; + + await rpc.sendToAddress(address, '0.1'); + await waitForElectrum(); + + // Refresh wallet and check unconfirmed transactions + await wallet.refreshWallet({}); + expect(Object.keys(wallet.data.unconfirmedTransactions)).to.have.length(1); + expect(Object.keys(wallet.data.transactions)).to.have.length(1); + + // Generate blocks to confirm transaction + await rpc.generateToAddress(10, await rpc.getNewAddress()); + await waitForElectrum(); + + // Refresh and check confirmed status + await wallet.refreshWallet({}); + expect(Object.keys(wallet.data.unconfirmedTransactions)).to.have.length(0); + expect(Object.keys(wallet.data.transactions)).to.have.length(1); + }); + + it('Should receive transaction and emit correct messages', async () => { + const r = await wallet.getNextAvailableAddress(); + if (r.isErr()) throw r.error; + const address = r.value.addressIndex.address; + + // test transactionReceived message + const receivePromise = ml.waitFor('transactionReceived'); + const amount = 0.1; + await rpc.sendToAddress(address, amount.toString()); + const txReceivedMessage = await receivePromise; + expect(txReceivedMessage.transaction.value).to.equal(0.1); + + // test transactionConfirmed message + const confirmedPromise = ml.waitFor('transactionConfirmed'); + await rpc.generateToAddress(1, await rpc.getNewAddress()); + await waitForElectrum(); + const txConfirmedMessage = await confirmedPromise; + expect(txConfirmedMessage.transaction.height).to.be.greaterThan(0); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts index 555c142..71ab182 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,7 +1,13 @@ import net from 'net'; import BitcoinJsonRpc from 'bitcoin-json-rpc'; import ElectrumClient from 'bw-electrum-client'; -import { EProtocol, sleep } from '../src'; +import { + EProtocol, + sleep, + TMessageDataMap, + TMessageKeys, + TOnMessage +} from '../src'; export const bitcoinURL = 'http://polaruser:polarpass@127.0.0.1:43782'; export const electrumHost = '127.0.0.1'; @@ -82,3 +88,55 @@ export const initWaitForElectrumToSync = async ( return waitForElectrum; }; + +type TMessage = { + key: TMessageKeys; + data: TMessageDataMap[keyof TMessageDataMap]; + timestamp: number; +}; + +export class MessageListener { + public messages: TMessage[] = []; + private resolvers: ((message: TMessage) => void)[] = []; + + onMessage: TOnMessage = (key, data) => { + const message: TMessage = { + key, + data, + timestamp: Date.now() + }; + this.messages.push(message); + this.resolvers.forEach((resolve) => resolve(message)); + }; + + waitFor( + messageKey: K, + timeout = 20000 + ): Promise { + // Check if message already received + const existingMessage = this.messages.find((msg) => msg.key === messageKey); + if (existingMessage) { + return Promise.resolve(existingMessage.data as TMessageDataMap[K]); + } + + // Wait for new message + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error('Timeout waiting for message')); + }, timeout); + + const resolver = (msg: TMessage): void => { + if (msg.key === messageKey) { + clearTimeout(timer); + resolve(msg.data as TMessageDataMap[K]); + } + }; + this.resolvers.push(resolver); + }); + } + + clear(): void { + this.messages = []; + this.resolvers = []; + } +}