Skip to content

Commit

Permalink
feat: add tests for receiving funds
Browse files Browse the repository at this point in the history
  • Loading branch information
limpbrains committed Nov 25, 2024
1 parent b5dfd39 commit 8c51787
Show file tree
Hide file tree
Showing 4 changed files with 290 additions and 2 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions tests/boost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
223 changes: 223 additions & 0 deletions tests/receive.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
60 changes: 59 additions & 1 deletion tests/utils.ts
Original file line number Diff line number Diff line change
@@ -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:[email protected]:43782';
export const electrumHost = '127.0.0.1';
Expand Down Expand Up @@ -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<K extends TMessageKeys>(
messageKey: K,
timeout = 20000
): Promise<TMessageDataMap[K]> {
// 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 = [];
}
}

0 comments on commit 8c51787

Please sign in to comment.