/* eslint-env mocha */ /* eslint prefer-arrow-callback: "off" */ 'use strict'; const {BloomFilter} = require('bfilter'); const assert = require('./util/assert'); const consensus = require('../lib/protocol/consensus'); const Address = require('../lib/primitives/address'); const Script = require('../lib/script/script'); const Outpoint = require('../lib/primitives/outpoint'); const MTX = require('../lib/primitives/mtx'); const FullNode = require('../lib/node/fullnode'); const ChainEntry = require('../lib/blockchain/chainentry'); const pkg = require('../lib/pkg'); if (process.browser) return; const ports = { p2p: 49331, node: 49332, wallet: 49333 }; const node = new FullNode({ network: 'regtest', apiKey: 'foo', walletAuth: true, memory: true, workers: true, plugins: [require('../lib/wallet/plugin')], port: ports.p2p, httpPort: ports.node, env: { 'BCOIN_WALLET_HTTP_PORT': ports.wallet.toString() }}); const {NodeClient, WalletClient} = require('bclient'); const nclient = new NodeClient({ port: ports.node, apiKey: 'foo' }); const wclient = new WalletClient({ port: ports.wallet, apiKey: 'foo' }); let wallet = null; const {wdb} = node.require('walletdb'); let addr = null; let hash = null; let blocks = null; describe('HTTP', function() { this.timeout(15000); // m/44'/1'/0'/0/{0,1} const pubkeys = [ Buffer.from('02a7451395735369f2ecdfc829c0f' + '774e88ef1303dfe5b2f04dbaab30a535dfdd6', 'hex'), Buffer.from('03589ae7c835ce76e23cf8feb32f1a' + 'df4a7f2ba0ed2ad70801802b0bcd70e99c1c', 'hex') ]; it('should open node', async () => { consensus.COINBASE_MATURITY = 0; await node.open(); await nclient.open(); await wclient.open(); }); it('should create wallet', async () => { const info = await wclient.createWallet('test'); assert.strictEqual(info.id, 'test'); wallet = wclient.wallet('test', info.token); await wallet.open(); }); it('should get info', async () => { const info = await nclient.getInfo(); assert.strictEqual(info.network, node.network.type); assert.strictEqual(info.version, pkg.version); assert.typeOf(info.pool, 'object'); assert.strictEqual(info.pool.agent, node.pool.options.agent); assert.typeOf(info.chain, 'object'); assert.strictEqual(info.chain.height, 0); assert.typeOf(info.indexes, 'object'); assert.typeOf(info.indexes.addr, 'object'); assert.equal(info.indexes.addr.enabled, false); assert.equal(info.indexes.addr.height, 0); assert.typeOf(info.indexes.tx, 'object'); assert.equal(info.indexes.addr.enabled, false); assert.equal(info.indexes.tx.height, 0); }); it('should get wallet info', async () => { const info = await wallet.getInfo(); assert.strictEqual(info.id, 'test'); const acct = await wallet.getAccount('default'); const str = acct.receiveAddress; assert.typeOf(str, 'string'); addr = Address.fromString(str, node.network); }); it('should fill with funds', async () => { const mtx = new MTX(); mtx.addOutpoint(new Outpoint(consensus.ZERO_HASH, 0)); mtx.addOutput(addr, 50460); mtx.addOutput(addr, 50460); mtx.addOutput(addr, 50460); mtx.addOutput(addr, 50460); const tx = mtx.toTX(); let balance = null; wallet.once('balance', (b) => { balance = b; }); let receive = null; wallet.once('address', (r) => { receive = r[0]; }); let details = null; wallet.once('tx', (d) => { details = d; }); await wdb.addTX(tx); await new Promise(r => setTimeout(r, 300)); assert(receive); assert.strictEqual(receive.name, 'default'); assert.strictEqual(receive.type, 'pubkeyhash'); assert.strictEqual(receive.branch, 0); assert(balance); assert.strictEqual(balance.confirmed, 0); assert.strictEqual(balance.unconfirmed, 201840); assert(details); assert.strictEqual(details.hash, tx.txid()); }); it('should get balance', async () => { const balance = await wallet.getBalance(); assert.strictEqual(balance.confirmed, 0); assert.strictEqual(balance.unconfirmed, 201840); }); it('should send a tx', async () => { const options = { rate: 10000, outputs: [{ value: 10000, address: addr.toString(node.network) }] }; const tx = await wallet.send(options); assert(tx); assert.strictEqual(tx.inputs.length, 1); assert.strictEqual(tx.outputs.length, 2); let value = 0; value += tx.outputs[0].value; value += tx.outputs[1].value; assert.strictEqual(value, 48190); hash = tx.hash; }); it('should get a tx', async () => { const tx = await wallet.getTX(hash); assert(tx); assert.strictEqual(tx.hash, hash); }); it('should generate new api key', async () => { const old = wallet.token.toString('hex'); const result = await wallet.retoken(null); assert.strictEqual(result.token.length, 64); assert.notStrictEqual(result.token, old); }); it('should get balance', async () => { const balance = await wallet.getBalance(); assert.strictEqual(balance.unconfirmed, 199570); }); it('should execute an rpc call', async () => { const info = await nclient.execute('getblockchaininfo', []); assert.strictEqual(info.blocks, 0); }); it('should execute an rpc call with bool parameter', async () => { const info = await nclient.execute('getrawmempool', [true]); assert.deepStrictEqual(info, {}); }); it('should create account', async () => { const info = await wallet.createAccount('foo1'); assert(info); assert(info.initialized); assert.strictEqual(info.name, 'foo1'); assert.strictEqual(info.accountIndex, 1); assert.strictEqual(info.m, 1); assert.strictEqual(info.n, 1); }); it('should create account', async () => { const info = await wallet.createAccount('foo2', { type: 'multisig', m: 1, n: 2 }); assert(info); assert(!info.initialized); assert.strictEqual(info.name, 'foo2'); assert.strictEqual(info.accountIndex, 2); assert.strictEqual(info.m, 1); assert.strictEqual(info.n, 2); }); it('should get a block template', async () => { const json = await nclient.execute('getblocktemplate', [{ rules: ['segwit'] }]); assert.deepStrictEqual(json, { capabilities: ['proposal'], mutable: ['time', 'transactions', 'prevblock'], version: 536870912, rules: ['!segwit'], vbavailable: {}, vbrequired: 0, height: 1, previousblockhash: '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206', target: '7fffff0000000000000000000000000000000000000000000000000000000000', bits: '207fffff', noncerange: '00000000ffffffff', curtime: json.curtime, mintime: 1296688603, maxtime: json.maxtime, expires: json.expires, sigoplimit: 80000, sizelimit: 4000000, weightlimit: 4000000, longpollid: '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206' + '00000000', submitold: false, coinbaseaux: { flags: '6d696e65642062792062636f696e' }, coinbasevalue: 5000000000, transactions: [], default_witness_commitment: '6a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48beb' + 'd836974e8cf9' }); }); it('should send a block template proposal', async () => { const attempt = await node.miner.createBlock(); const block = attempt.toBlock(); const hex = block.toRaw().toString('hex'); const json = await nclient.execute('getblocktemplate', [{ mode: 'proposal', data: hex }]); assert.strictEqual(json, null); }); it('should validate an address', async () => { const json = await nclient.execute('validateaddress', [ addr.toString(node.network) ]); assert.deepStrictEqual(json, { isvalid: true, address: addr.toString(node.network), scriptPubKey: Script.fromAddress(addr).toRaw().toString('hex'), iswitness: false, isscript: false }); }); it('should not validate invalid address', async () => { // Valid Mainnet P2WPKH from // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki const json = await nclient.execute('validateaddress', [ 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4' ]); // Sending an address from the incorrect network // should result in an invalid address assert.deepStrictEqual(json, { isvalid: false }); }); it('should validate a p2wpkh address', async () => { const address = 'bcrt1q8gk5z3dy7zv9ywe7synlrk58elz4hrnegvpv6m'; const addr = Address.fromString(address); const script = Script.fromAddress(addr); const json = await nclient.execute('validateaddress', [ address ]); assert.deepStrictEqual(json, { isvalid: true, iswitness: true, address: address, isscript: addr.isScripthash(), scriptPubKey: script.toJSON(), witness_version: addr.version, witness_program: addr.hash.toString('hex') }); }); it('should validate a p2sh address', async () => { const script = Script.fromMultisig(2, 2, pubkeys); const address = Address.fromScript(script); // Test the valid case - render the address to the // correct network { const json = await nclient.execute('validateaddress', [ address.toString(node.network) ]); assert.deepEqual(json, { isvalid: true, address: address.toString(node.network), scriptPubKey: Script.fromAddress(address).toJSON(), isscript: true, iswitness: false }); } // Test the invalid case - render the address to the // incorrect network, making it an invalid address { const json = await nclient.execute('validateaddress', [ address.toString('main') ]); assert.deepEqual(json, { isvalid: false }); } }); it('should validate a p2wsh address', async () => { const script = Script.fromMultisig(2, 2, pubkeys); const scriptPubKey = script.forWitness(); const program = script.sha256(); const address = Address.fromProgram(0, program); const json = await nclient.execute('validateaddress', [ address.toString(node.network) ]); assert.deepEqual(json, { isvalid: true, address: address.toString(node.network), scriptPubKey: scriptPubKey.toJSON(), isscript: true, iswitness: true, witness_version: 0, witness_program: program.toString('hex') }); }); for (const template of [true, false]) { const suffix = template ? 'with template' : 'without template'; it(`should create and sign transaction ${suffix}`, async () => { const change = await wallet.createChange('default'); const tx = await wallet.createTX({ template: template, // should not matter, sign = true sign: true, outputs: [{ address: change.address, value: 50000 }] }); const mtx = MTX.fromJSON(tx); for (const input of tx.inputs) { const script = input.script; assert.notStrictEqual(script, '', 'Input must be signed.'); } assert.strictEqual(mtx.verify(), true, 'Transaction must be signed.'); }); } it('should create transaction without template', async () => { const change = await wallet.createChange('default'); const tx = await wallet.createTX({ sign: false, outputs: [{ address: change.address, value: 50000 }] }); for (const input of tx.inputs) { const script = input.script; assert.strictEqual(script.length, 0, 'Input must not be templated.'); } }); it('should create transaction with template', async () => { const change = await wallet.createChange('default'); const tx = await wallet.createTX({ sign: false, template: true, outputs: [{ address: change.address, value: 20000 }] }); for (const input of tx.inputs) { const script = Buffer.from(input.script, 'hex'); // p2pkh // 1 (OP_0 placeholder) + 1 (length) + 33 (pubkey) assert.strictEqual(script.length, 35); assert.strictEqual(script[0], 0x00, 'First item in stack must be a placeholder OP_0'); } }); it('should generate 10 blocks from RPC call', async () => { blocks = await nclient.execute( 'generatetoaddress', [10, addr.toString('regtest')] ); assert.strictEqual(blocks.length, 10); }); it('should initiate rescan from socket without a bloom filter', async () => { // Rescan from height 5. Without a filter loaded = no response, but no error const response = await nclient.call('rescan', 5); assert.strictEqual(null, response); }); it('should initiate rescan from socket WITH a bloom filter', async () => { // Create an SPV-standard Bloom filter and add one of our wallet addresses const filter = BloomFilter.fromRate(20000, 0.001, BloomFilter.flags.ALL); const walletAddr = addr.toString('regtest'); filter.add(walletAddr, 'ascii'); // Send Bloom filter to server await nclient.call('set filter', filter.filter); // `rescan` commands the node server to check blocks against a bloom filter. // When the server matches a transaction in a block to the filter, it // sends a socket call BACK to the client with the ChainEntry of the block, // and an array of matched transactions. Because of this callback, the // CLIENT MUST have a `block rescan` hook in place or the server will throw. const matchingBlocks = []; nclient.hook('block rescan', (entry, txs) => { // Coinbase transactions were mined to our watch address, matching filter. assert.strictEqual(txs.length, 1); const cbtx = MTX.fromRaw(txs[0]); assert.strictEqual( cbtx.outputs[0].getAddress().toString('regtest'), walletAddr ); // Blocks are returned as raw ChainEntry matchingBlocks.push( ChainEntry.fromRaw(entry).rhash().toString('hex') ); }); // Rescan from height 5 -- should return blocks 5 through 10, inclusive. await nclient.call('rescan', 5); assert.deepStrictEqual(matchingBlocks, blocks.slice(4)); }); it('should cleanup', async () => { consensus.COINBASE_MATURITY = 100; await wallet.close(); await wclient.close(); await nclient.close(); await node.close(); }); });