bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
346 lines (302 loc) • 12.5 kB
text/typescript
import { Wallet } from 'bitcore-client';
import { ParseApiStream } from 'bitcore-client';
import { expect } from 'chai';
import * as io from 'socket.io-client';
import config from '../../src/config';
import { MongoBound } from '../../src/models/base';
import { CoinStorage, ICoin } from '../../src/models/coin';
import { TransactionStorage } from '../../src/models/transaction';
import { IWallet, WalletStorage } from '../../src/models/wallet';
import { WalletAddressStorage } from '../../src/models/walletAddress';
import { BitcoinP2PWorker } from '../../src/modules/bitcoin/p2p';
import { AsyncRPC } from '../../src/rpc';
import { Api } from '../../src/services/api';
import { Event } from '../../src/services/event';
import { IUtxoNetworkConfig } from '../../src/types/Config';
import { wait } from '../../src/utils';
import { createWallet } from '../benchmark/wallet-benchmark';
import { resetDatabase } from '../helpers';
import { intAfterHelper, intBeforeHelper } from '../helpers/integration';
const chain = 'BTC';
const network = 'regtest';
const chainConfig = config.chains[chain][network] as IUtxoNetworkConfig;
const creds = chainConfig.rpc;
const rpc = new AsyncRPC(creds.username, creds.password, creds.host, creds.port);
async function checkWalletExists(pubKey, expectedAddress) {
// Check the database for the first wallet
const dbWallet = await WalletStorage.collection.findOne({
chain,
network,
pubKey
});
// Verify the addresses match
const foundAddresses = await WalletAddressStorage.collection
.find({
chain,
network,
wallet: dbWallet!._id
})
.toArray();
expect(foundAddresses.length).to.eq(1);
expect(foundAddresses[0].address).to.eq(expectedAddress);
return dbWallet;
}
async function getWalletUtxos(wallet: Wallet) {
const utxos = new Array<MongoBound<ICoin>>();
return new Promise<Array<MongoBound<ICoin>>>(resolve =>
wallet
.getUtxos()
.pipe(new ParseApiStream())
.on('data', (utxo: MongoBound<ICoin>) => {
utxos.push(utxo);
})
.on('end', () => resolve(utxos))
);
}
async function checkWalletUtxos(wallet: Wallet, expectedAddress: string) {
const utxos = await getWalletUtxos(wallet);
expect(utxos.length).to.eq(1);
expect(utxos[0].address).to.eq(expectedAddress);
return utxos;
}
async function verifyCoinSpent(coin: MongoBound<ICoin>, spentTxid: string, wallet: IWallet) {
const wallet1Coin = await CoinStorage.collection.findOne({
chain: coin.chain,
network: coin.network,
mintTxid: coin.mintTxid,
mintIndex: coin.mintIndex,
});
expect(wallet1Coin!.spentTxid).to.eq(spentTxid);
expect(wallet1Coin!.wallets[0].toHexString()).to.eq(wallet!._id!.toHexString());
}
async function checkWalletReceived(receivingWallet: IWallet, txid: string, address: string, sendingWallet: IWallet) {
const broadcastedOutput = await CoinStorage.collection.findOne({
chain,
network,
mintTxid: txid,
address
});
expect(broadcastedOutput!.address).to.eq(address);
expect(broadcastedOutput!.wallets.length).to.eq(1);
expect(broadcastedOutput!.wallets[0].toHexString()).to.eq(receivingWallet!._id!.toHexString());
const broadcastedTransaction = await TransactionStorage.collection.findOne({ chain, network, txid });
expect(broadcastedTransaction!.txid).to.eq(txid);
expect(broadcastedTransaction!.fee).gt(0);
const txWallets = broadcastedTransaction!.wallets.map(w => w.toHexString());
expect(txWallets.length).to.eq(2);
expect(txWallets).to.include(receivingWallet!._id!.toHexString());
expect(txWallets).to.include(sendingWallet!._id!.toHexString());
}
describe('Wallet Benchmark', function() {
const suite = this;
this.timeout(5000000);
let p2pWorker: BitcoinP2PWorker;
before(async () => {
await intBeforeHelper();
await Event.start();
await Api.start();
});
after(async () => {
await Event.stop();
await Api.stop();
await intAfterHelper(suite);
});
beforeEach(async () => {
await resetDatabase();
});
afterEach(async () => {
if (p2pWorker) {
await p2pWorker.stop();
}
});
describe('Wallet import', () => {
it('should be able to create two wallets and have them interact', async () => {
const seenCoins = new Set();
const socket = io.connect('http://localhost:3000', { transports: ['websocket'] });
const connected = new Promise<void>(r => {
socket.on('connect', () => {
const room = `/${chain}/${network}/inv`;
socket.emit('room', room);
console.log('Connected to socket');
r();
});
});
await connected;
socket.on('coin', (coin: ICoin) => {
seenCoins.add(coin.mintTxid);
});
p2pWorker = new BitcoinP2PWorker({
chain,
network,
chainConfig
});
await p2pWorker.start();
const address1 = await rpc.getnewaddress('');
const address2 = await rpc.getnewaddress('');
const anAddress = 'mkzAfSHtmTh5Xsc352jf6TBPj55Lne5g21';
try {
await rpc.call('generatetoaddress', [1, address1]);
await rpc.call('generatetoaddress', [1, address2]);
await rpc.call('generatetoaddress', [100, anAddress]);
await p2pWorker.syncDone();
const wallet1 = await createWallet([address1], 0, network);
const wallet2 = await createWallet([address2], 1, network);
const dbWallet1 = await checkWalletExists(wallet1.authPubKey, address1);
const dbWallet2 = await checkWalletExists(wallet2.authPubKey, address2);
const utxos = await checkWalletUtxos(wallet1, address1);
await checkWalletUtxos(wallet2, address2);
const tx = await rpc.call('createrawtransaction', [
utxos.map(utxo => ({ txid: utxo.mintTxid, vout: utxo.mintIndex })),
{ [address1]: 0.1, [address2]: 0.1 }
]);
const fundedTx = await rpc.call('fundrawtransaction', [tx]);
const signedTx = await rpc.signrawtx(fundedTx.hex);
const broadcastedTx = await rpc.call('sendrawtransaction', [signedTx.hex]);
while (!seenCoins.has(broadcastedTx)) {
console.log('...WAITING...'); // TODO
await wait(1000);
}
await verifyCoinSpent(utxos[0], broadcastedTx, dbWallet1!);
await checkWalletReceived(dbWallet1!, broadcastedTx, address1, dbWallet2!);
await checkWalletReceived(dbWallet2!, broadcastedTx, address2, dbWallet1!);
await wait(1000);
await socket.disconnect();
await p2pWorker.stop();
} catch (e) {
console.log('Error : ', e);
expect(e).to.be.undefined;
}
});
it('should be able to create two wallets and have them interact, while syncing', async () => {
const seenCoins = new Set();
const socket = io.connect('http://localhost:3000', { transports: ['websocket'] });
const connected = new Promise<void>(r => {
socket.on('connect', () => {
const room = `/${chain}/${network}/inv`;
socket.emit('room', room);
console.log('Connected to socket');
r();
});
});
await connected;
socket.on('coin', (coin: ICoin) => {
seenCoins.add(coin.mintTxid);
});
p2pWorker = new BitcoinP2PWorker({
chain,
network,
chainConfig
});
await p2pWorker.start();
const address1 = await rpc.getnewaddress('');
const address2 = await rpc.getnewaddress('');
const anAddress = 'mkzAfSHtmTh5Xsc352jf6TBPj55Lne5g21';
try {
await rpc.call('generatetoaddress', [1, address1]);
await rpc.call('generatetoaddress', [1, address2]);
// mature coins
await rpc.call('generatetoaddress', [100, anAddress]);
await p2pWorker.syncDone();
const wallet1 = await createWallet([address1], 2, network);
const wallet2 = await createWallet([address2], 3, network);
const dbWallet1 = await checkWalletExists(wallet1.authPubKey, address1);
const dbWallet2 = await checkWalletExists(wallet2.authPubKey, address2);
const utxos = await checkWalletUtxos(wallet1, address1);
await checkWalletUtxos(wallet2, address2);
const tx = await rpc.call('createrawtransaction', [
utxos.map(utxo => ({ txid: utxo.mintTxid, vout: utxo.mintIndex })),
{ [address1]: 0.1, [address2]: 0.1 }
]);
const fundedTx = await rpc.call('fundrawtransaction', [tx]);
const signedTx = await rpc.signrawtx(fundedTx.hex);
await rpc.call('generatetoaddress', [100, anAddress]);
p2pWorker.sync();
expect(p2pWorker.isSyncing).to.be.true;
// Generate some blocks for the node to process
const broadcastedTx = await rpc.call('sendrawtransaction', [signedTx.hex]);
expect(p2pWorker.isSyncing).to.be.true;
while (!seenCoins.has(broadcastedTx)) {
console.log('...WAITING...'); // TODO
await wait(1000);
}
await verifyCoinSpent(utxos[0], broadcastedTx, dbWallet1!);
await checkWalletReceived(dbWallet1!, broadcastedTx, address1, dbWallet2!);
await checkWalletReceived(dbWallet2!, broadcastedTx, address2, dbWallet1!);
await wait(1000);
await socket.disconnect();
await p2pWorker.stop();
} catch (e) {
console.log('Error : ', e);
expect(e).to.be.undefined;
}
});
it('should import all addresses and verify in database while below 300 mb of heapUsed memory', async () => {
let smallAddressBatch = new Array<string>();
let mediumAddressBatch = new Array<string>();
let largeAddressBatch = new Array<string>();
console.log('Generating small batch of addresses');
for (let i = 0; i < 10; i++) {
let address = await rpc.getnewaddress('');
smallAddressBatch.push(address);
}
console.log('Generating medium batch of addresses');
expect(smallAddressBatch.length).to.deep.equal(10);
for (let i = 0; i < 100; i++) {
let address = await rpc.getnewaddress('');
mediumAddressBatch.push(address);
}
expect(mediumAddressBatch.length).to.deep.equal(100);
console.log('Generating large batch of addresses');
for (let i = 0; i < 1000; i++) {
let address = await rpc.getnewaddress('');
largeAddressBatch.push(address);
}
expect(largeAddressBatch.length).to.deep.equal(1000);
console.log('Checking');
const importedWallet1 = await createWallet(smallAddressBatch, 0, network);
const importedWallet2 = await createWallet(mediumAddressBatch, 1, network);
const importedWallet3 = await createWallet(largeAddressBatch, 2, network);
expect(importedWallet1).to.not.be.null;
expect(importedWallet2).to.not.be.null;
expect(importedWallet3).to.not.be.null;
const foundSmallAddressBatch = await WalletAddressStorage.collection
.find({
chain,
network,
address: { $in: smallAddressBatch }
})
.toArray();
const smallAddresses = foundSmallAddressBatch.map(wa => wa.address);
for (let address of smallAddressBatch) {
expect(smallAddresses.includes(address)).to.be.true;
}
expect(foundSmallAddressBatch.length).to.have.deep.equal(smallAddressBatch.length);
const foundMediumAddressBatch = await WalletAddressStorage.collection
.find({
chain,
network,
address: { $in: mediumAddressBatch }
})
.toArray();
const mediumAddresses = foundMediumAddressBatch.map(wa => wa.address);
for (let address of mediumAddressBatch) {
expect(mediumAddresses.includes(address)).to.be.true;
}
expect(foundMediumAddressBatch.length).to.have.deep.equal(mediumAddressBatch.length);
const foundLargeAddressBatch = await WalletAddressStorage.collection
.find({
chain,
network,
address: { $in: largeAddressBatch }
})
.toArray();
const largeAddresses = foundLargeAddressBatch.map(wa => wa.address);
for (let address of largeAddressBatch) {
expect(largeAddresses.includes(address)).to.be.true;
}
expect(foundLargeAddressBatch.length).to.have.deep.equal(largeAddressBatch.length);
const { heapUsed } = process.memoryUsage();
expect(heapUsed).to.be.below(3e8);
});
});
});