From 00fe264f220aa247907dfadda992714e3c4b86c3 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Mon, 9 Mar 2020 18:51:34 +0000 Subject: [PATCH] REF: single-address wallets now work through electrum --- class/abstract-wallet.js | 9 +- class/legacy-wallet.js | 417 ++++++---------------- screen/settings/about.js | 8 +- tests/integration/App.test.js | 110 +----- tests/integration/LegacyWallet.test.js | 154 ++++++++ tests/integration/WatchOnlyWallet.test.js | 76 ++-- 6 files changed, 330 insertions(+), 444 deletions(-) create mode 100644 tests/integration/LegacyWallet.test.js diff --git a/class/abstract-wallet.js b/class/abstract-wallet.js index 8d09df9e1..4d1a3473f 100644 --- a/class/abstract-wallet.js +++ b/class/abstract-wallet.js @@ -105,7 +105,7 @@ export class AbstractWallet { } weOwnAddress(address) { - return this._address === address; + throw Error('not implemented'); } /** @@ -128,7 +128,12 @@ export class AbstractWallet { } setSecret(newSecret) { - this.secret = newSecret.trim(); + this.secret = newSecret + .trim() + .replace('bitcoin:', '') + .replace('BITCOIN:', ''); + + if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase(); try { const parsedSecret = JSON.parse(this.secret); diff --git a/class/legacy-wallet.js b/class/legacy-wallet.js index ee9afcb21..6a76d08c3 100644 --- a/class/legacy-wallet.js +++ b/class/legacy-wallet.js @@ -1,7 +1,5 @@ import { AbstractWallet } from './abstract-wallet'; -import { SegwitBech32Wallet } from './'; -import { useBlockcypherTokens } from './constants'; -import Frisbee from 'frisbee'; +import { HDSegwitBech32Wallet } from './'; import { NativeModules } from 'react-native'; const bitcoin = require('bitcoinjs-lib'); const { RNRandomBytes } = NativeModules; @@ -37,7 +35,7 @@ export class LegacyWallet extends AbstractWallet { * @return {boolean} */ timeToRefreshTransaction() { - for (let tx of this.transactions) { + for (let tx of this.getTransactions()) { if (tx.confirmations < 7) { return true; } @@ -104,24 +102,13 @@ export class LegacyWallet extends AbstractWallet { */ async fetchBalance() { try { - const api = new Frisbee({ - baseURI: 'https://api.blockcypher.com/v1/btc/main/addrs/', - }); - - let response = await api.get( - this.getAddress() + '/balance' + ((useBlockcypherTokens && '?token=' + this.getRandomBlockcypherToken()) || ''), - ); - let json = response.body; - if (typeof json === 'undefined' || typeof json.final_balance === 'undefined') { - throw new Error('Could not fetch balance from API: ' + response.err + ' ' + JSON.stringify(response.body)); - } - - this.balance = Number(json.final_balance); - this.unconfirmed_balance = new BigNumber(json.unconfirmed_balance); - this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1; + let balance = await BlueElectrum.getBalanceByAddress(this.getAddress()); + this.balance = Number(balance.confirmed); + this.unconfirmed_balance = new BigNumber(balance.unconfirmed); + this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1; // wtf this._lastBalanceFetch = +new Date(); - } catch (err) { - console.warn(err); + } catch (Error) { + console.warn(Error); } } @@ -131,230 +118,116 @@ export class LegacyWallet extends AbstractWallet { * @return {Promise.} */ async fetchUtxo() { - const api = new Frisbee({ - baseURI: 'https://api.blockcypher.com/v1/btc/main/addrs/', - }); - - let response; try { - let maxHeight = 0; - this.utxo = []; - let json; + let utxos = await BlueElectrum.multiGetUtxoByAddress([this.getAddress()]); + for (let arr of Object.values(utxos)) { + this.utxo = this.utxo.concat(arr); + } + } catch (Error) { + console.warn(Error); + } - do { - response = await api.get( - this.getAddress() + - '?limit=2000&after=' + - maxHeight + - ((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || ''), - ); - json = response.body; - if (typeof json === 'undefined' || typeof json.final_balance === 'undefined') { - throw new Error('Could not fetch UTXO from API' + response.err); - } - json.txrefs = json.txrefs || []; // case when source address is empty (or maxheight too high, no txs) - - for (let txref of json.txrefs) { - maxHeight = Math.max(maxHeight, txref.block_height) + 1; - if (typeof txref.spent !== 'undefined' && txref.spent === false) { - this.utxo.push(txref); - } - } - } while (json.txrefs.length); - - json.unconfirmed_txrefs = json.unconfirmed_txrefs || []; - this.utxo = this.utxo.concat(json.unconfirmed_txrefs); - } catch (err) { - console.warn(err); + // backward compatibility + for (let u of this.utxo) { + u.tx_output_n = u.vout; + u.tx_hash = u.txId; + u.confirmations = u.height ? 1 : 0; } } + getUtxo() { + return this.utxo; + } + /** - * Fetches transactions via API. Returns VOID. - * Use getter to get the actual list. + * Fetches transactions via Electrum. Returns VOID. + * Use getter to get the actual list. * + * @see AbstractHDElectrumWallet.fetchTransactions() * * @return {Promise.} */ async fetchTransactions() { - try { - const api = new Frisbee({ - baseURI: 'https://api.blockcypher.com/', - }); + // Below is a simplified copypaste from HD electrum wallet + this._txs_by_external_index = []; + let addresses2fetch = [this.getAddress()]; - let after = 0; - let before = 100500100; - - for (let oldTx of this.getTransactions()) { - if (oldTx.block_height && oldTx.confirmations < 7) { - after = Math.max(after, oldTx.block_height); - } + // first: batch fetch for all addresses histories + let histories = await BlueElectrum.multiGetHistoryByAddress(addresses2fetch); + let txs = {}; + for (let history of Object.values(histories)) { + for (let tx of history) { + txs[tx.tx_hash] = tx; } - - while (1) { - let response = await api.get( - 'v1/btc/main/addrs/' + - this.getAddress() + - '/full?after=' + - after + - '&before=' + - before + - '&limit=50' + - ((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || ''), - ); - let json = response.body; - if (typeof json === 'undefined' || !json.txs) { - throw new Error('Could not fetch transactions from API:' + response.err); - } - - let alreadyFetchedTransactions = this.transactions; - this.transactions = json.txs; - this._lastTxFetch = +new Date(); - - // now, calculating value per each transaction... - for (let tx of this.transactions) { - if (tx.block_height) { - before = Math.min(before, tx.block_height); // so next time we fetch older TXs - } - - // now, if we dont have enough outputs or inputs in response we should collect them from API: - if (tx.next_outputs) { - let newOutputs = await this._fetchAdditionalOutputs(tx.next_outputs); - tx.outputs = tx.outputs.concat(newOutputs); - } - if (tx.next_inputs) { - let newInputs = await this._fetchAdditionalInputs(tx.next_inputs); - tx.inputs = tx.inputs.concat(newInputs); - } - - // how much came in... - let value = 0; - for (let out of tx.outputs) { - if (out && out.addresses && out.addresses.indexOf(this.getAddress()) !== -1) { - // found our address in outs of this TX - value += out.value; - } - } - tx.value = value; - // end - - // how much came out - value = 0; - for (let inp of tx.inputs) { - if (!inp.addresses) { - // console.log('inp.addresses empty'); - // console.log('got witness', inp.witness); // TODO - - inp.addresses = []; - if (inp.witness && inp.witness[1]) { - let address = SegwitBech32Wallet.witnessToAddress(inp.witness[1]); - inp.addresses.push(address); - } else { - inp.addresses.push('???'); - } - } - if (inp && inp.addresses && inp.addresses.indexOf(this.getAddress()) !== -1) { - // found our address in outs of this TX - value -= inp.output_value; - } - } - tx.value += value; - // end - } - - this.transactions = alreadyFetchedTransactions.concat(this.transactions); - - let txsUnconf = []; - let txs = []; - let hashPresent = {}; - // now, rearranging TXs. unconfirmed go first: - for (let tx of this.transactions.reverse()) { - if (hashPresent[tx.hash]) continue; - hashPresent[tx.hash] = 1; - if (tx.block_height && tx.block_height === -1) { - // unconfirmed - console.log(tx); - if (+new Date(tx.received) < +new Date() - 3600 * 24 * 1000) { - // nop, too old unconfirmed tx - skipping it - } else { - txsUnconf.push(tx); - } - } else { - txs.push(tx); - } - } - this.transactions = txsUnconf.reverse().concat(txs.reverse()); - // all reverses needed so freshly fetched TXs replace same old TXs - - this.transactions = this.transactions.sort((a, b) => { - return a.received < b.received; - }); - - if (json.txs.length < 50) { - // final batch, so it has les than max txs - break; - } - } - } catch (err) { - console.warn(err); } + + // next, batch fetching each txid we got + let txdatas = await BlueElectrum.multiGetTransactionByTxid(Object.keys(txs)); + + // now, tricky part. we collect all transactions from inputs (vin), and batch fetch them too. + // then we combine all this data (we need inputs to see source addresses and amounts) + let vinTxids = []; + for (let txdata of Object.values(txdatas)) { + for (let vin of txdata.vin) { + vinTxids.push(vin.txid); + } + } + let vintxdatas = await BlueElectrum.multiGetTransactionByTxid(vinTxids); + + // fetched all transactions from our inputs. now we need to combine it. + // iterating all _our_ transactions: + for (let txid of Object.keys(txdatas)) { + // iterating all inputs our our single transaction: + for (let inpNum = 0; inpNum < txdatas[txid].vin.length; inpNum++) { + let inpTxid = txdatas[txid].vin[inpNum].txid; + let inpVout = txdatas[txid].vin[inpNum].vout; + // got txid and output number of _previous_ transaction we shoud look into + if (vintxdatas[inpTxid] && vintxdatas[inpTxid].vout[inpVout]) { + // extracting amount & addresses from previous output and adding it to _our_ input: + txdatas[txid].vin[inpNum].addresses = vintxdatas[inpTxid].vout[inpVout].scriptPubKey.addresses; + txdatas[txid].vin[inpNum].value = vintxdatas[inpTxid].vout[inpVout].value; + } + } + } + + // now, we need to put transactions in all relevant `cells` of internal hashmaps: this.transactions_by_internal_index && this.transactions_by_external_index + + for (let tx of Object.values(txdatas)) { + for (let vin of tx.vin) { + if (vin.addresses && vin.addresses.indexOf(this.getAddress()) !== -1) { + // this TX is related to our address + let clonedTx = Object.assign({}, tx); + clonedTx.inputs = tx.vin.slice(0); + clonedTx.outputs = tx.vout.slice(0); + delete clonedTx.vin; + delete clonedTx.vout; + + this._txs_by_external_index.push(clonedTx); + } + } + for (let vout of tx.vout) { + if (vout.scriptPubKey.addresses.indexOf(this.getAddress()) !== -1) { + // this TX is related to our address + let clonedTx = Object.assign({}, tx); + clonedTx.inputs = tx.vin.slice(0); + clonedTx.outputs = tx.vout.slice(0); + delete clonedTx.vin; + delete clonedTx.vout; + + this._txs_by_external_index.push(clonedTx); + } + } + } + + this._lastTxFetch = +new Date(); } - async _fetchAdditionalOutputs(nextOutputs) { - let outputs = []; - let baseURI = nextOutputs.split('/'); - baseURI = baseURI[0] + '/' + baseURI[1] + '/' + baseURI[2] + '/'; - const api = new Frisbee({ - baseURI: baseURI, - }); + getTransactions() { + // a hacky code reuse from electrum HD wallet: + this._txs_by_external_index = this._txs_by_external_index || []; + this._txs_by_internal_index = []; - do { - await (() => new Promise(resolve => setTimeout(resolve, 1000)))(); - nextOutputs = nextOutputs.replace(baseURI, ''); - - let response = await api.get(nextOutputs + ((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || '')); - let json = response.body; - if (typeof json === 'undefined') { - throw new Error('Could not fetch transactions from API:' + response.err); - } - - if (json.outputs && json.outputs.length) { - outputs = outputs.concat(json.outputs); - nextOutputs = json.next_outputs; - } else { - break; - } - } while (1); - - return outputs; - } - - async _fetchAdditionalInputs(nextInputs) { - let inputs = []; - let baseURI = nextInputs.split('/'); - baseURI = baseURI[0] + '/' + baseURI[1] + '/' + baseURI[2] + '/'; - const api = new Frisbee({ - baseURI: baseURI, - }); - - do { - await (() => new Promise(resolve => setTimeout(resolve, 1000)))(); - nextInputs = nextInputs.replace(baseURI, ''); - - let response = await api.get(nextInputs + ((useBlockcypherTokens && '&token=' + this.getRandomBlockcypherToken()) || '')); - let json = response.body; - if (typeof json === 'undefined') { - throw new Error('Could not fetch transactions from API:' + response.err); - } - - if (json.inputs && json.inputs.length) { - inputs = inputs.concat(json.inputs); - nextInputs = json.next_inputs; - } else { - break; - } - } while (1); - - return inputs; + let hd = new HDSegwitBech32Wallet(); + return hd.getTransactions.apply(this); } async broadcastTx(txhex) { @@ -366,66 +239,8 @@ export class LegacyWallet extends AbstractWallet { } } - async _broadcastTxBtczen(txhex) { - const api = new Frisbee({ - baseURI: 'https://btczen.com', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - }); - - let res = await api.get('/broadcast/' + txhex); - console.log('response btczen', res.body); - return res.body; - } - - async _broadcastTxChainso(txhex) { - const api = new Frisbee({ - baseURI: 'https://chain.so', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - }); - - let res = await api.post('/api/v2/send_tx/BTC', { - body: { tx_hex: txhex }, - }); - return res.body; - } - - async _broadcastTxSmartbit(txhex) { - const api = new Frisbee({ - baseURI: 'https://api.smartbit.com.au', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - }); - - let res = await api.post('/v1/blockchain/pushtx', { - body: { hex: txhex }, - }); - return res.body; - } - - async _broadcastTxBlockcypher(txhex) { - const api = new Frisbee({ - baseURI: 'https://api.blockcypher.com', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - }); - - let res = await api.post('/v1/btc/main/txs/push', { body: { tx: txhex } }); - // console.log('blockcypher response', res); - return res.body; - } - /** - * Takes UTXOs (as presented by blockcypher api), transforms them into + * Takes UTXOs, transforms them into * format expected by signer module, creates tx and returns signed string txhex. * * @param utxos Unspent outputs, expects blockcypher format @@ -462,22 +277,12 @@ export class LegacyWallet extends AbstractWallet { return new Date(max).toString(); } - getRandomBlockcypherToken() { - return (array => { - for (let i = array.length - 1; i > 0; i--) { - let j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array[0]; - })([ - '0326b7107b4149559d18ce80612ef812', - 'a133eb7ccacd4accb80cb1225de4b155', - '7c2b1628d27b4bd3bf8eaee7149c577f', - 'f1e5a02b9ec84ec4bc8db2349022e5f5', - 'e5926dbeb57145979153adc41305b183', - ]); - } - + /** + * Validates any address, including legacy, p2sh and bech32 + * + * @param address + * @returns {boolean} + */ isAddressValid(address) { try { bitcoin.address.toOutputScript(address); @@ -486,4 +291,8 @@ export class LegacyWallet extends AbstractWallet { return false; } } + + weOwnAddress(address) { + return this.getAddress() === address || this._address === address; + } } diff --git a/screen/settings/about.js b/screen/settings/about.js index 0d52d974c..fc75ff32b 100644 --- a/screen/settings/about.js +++ b/screen/settings/about.js @@ -121,12 +121,10 @@ const About = () => { Built with awesome: * React Native - * Bitcoinjs-lib - * blockcypher.com API + * bitcoinjs-lib * Nodejs - * react-native-elements - * rn-nodeify - * bignumber.js + * Electrum server + diff --git a/tests/integration/App.test.js b/tests/integration/App.test.js index 4372ddbad..59b3f788c 100644 --- a/tests/integration/App.test.js +++ b/tests/integration/App.test.js @@ -1,6 +1,6 @@ /* global describe, it, expect, jest, jasmine */ import React from 'react'; -import { LegacyWallet, SegwitP2SHWallet, AppStorage } from '../../class'; +import { AppStorage } from '../../class'; import TestRenderer from 'react-test-renderer'; import Settings from '../../screen/settings/settings'; import Selftest from '../../screen/selftest'; @@ -49,28 +49,6 @@ jest.mock('ScrollView', () => { return ScrollView; }); -describe('unit - LegacyWallet', function() { - it('serialize and unserialize work correctly', () => { - let a = new LegacyWallet(); - a.setLabel('my1'); - let key = JSON.stringify(a); - - let b = LegacyWallet.fromJson(key); - assert(key === JSON.stringify(b)); - - assert.strictEqual(key, JSON.stringify(b)); - }); - - it('can validate addresses', () => { - let w = new LegacyWallet(); - assert.ok(w.isAddressValid('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); - assert.ok(!w.isAddressValid('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2j')); - assert.ok(w.isAddressValid('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2')); - assert.ok(!w.isAddressValid('3BDsBDxDimYgNZzsqszNZobqQq3yeUo')); - assert.ok(!w.isAddressValid('12345')); - }); -}); - it('BlueHeader works', () => { const rendered = TestRenderer.create().toJSON(); expect(rendered).toBeTruthy(); @@ -105,92 +83,6 @@ it('Selftest work', () => { assert.ok(okFound, 'OK not found. Got: ' + allTests.join('; ')); }); -it('Wallet can fetch UTXO', async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; - let w = new SegwitP2SHWallet(); - w._address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; - await w.fetchUtxo(); - assert.ok(w.utxo.length > 0, 'unexpected empty UTXO'); -}); - -it('SegwitP2SHWallet can generate segwit P2SH address from WIF', async () => { - let l = new SegwitP2SHWallet(); - l.setSecret('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct'); - assert.ok(l.getAddress() === '34AgLJhwXrvmkZS1o5TrcdeevMt22Nar53', 'expected ' + l.getAddress()); - assert.ok(l.getAddress() === (await l.getAddressAsync())); -}); - -it('Wallet can fetch balance', async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; - let w = new LegacyWallet(); - w._address = '115fUy41sZkAG14CmdP1VbEKcNRZJWkUWG'; // hack internals - assert.ok(w.getBalance() === 0); - assert.ok(w.getUnconfirmedBalance() === 0); - assert.ok(w._lastBalanceFetch === 0); - await w.fetchBalance(); - assert.ok(w.getBalance() === 18262000); - assert.ok(w.getUnconfirmedBalance() === 0); - assert.ok(w._lastBalanceFetch > 0); -}); - -it('Wallet can fetch TXs', async () => { - let w = new LegacyWallet(); - w._address = '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'; - await w.fetchTransactions(); - assert.strictEqual(w.getTransactions().length, 2); - - let tx0 = w.getTransactions()[0]; - let txExpected = { - block_hash: '0000000000000000000d05c54a592db8532f134e12b4c3ae0821ce582fad3566', - block_height: 530933, - block_index: 1587, - hash: '4924f3a29acdee007ebcf6084d2c9e1752c4eb7f26f7d1a06ef808780bf5fe6d', - addresses: ['12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG', '3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'], - total: 800, - fees: 200, - size: 190, - preference: 'low', - relayed_by: '18.197.135.148:8333', - confirmed: '2018-07-07T20:05:30Z', - received: '2018-07-07T20:02:01.637Z', - ver: 1, - double_spend: false, - vin_sz: 1, - vout_sz: 1, - confirmations: 593, - confidence: 1, - inputs: [ - { - prev_hash: 'd0432027a86119c63a0be8fa453275c2333b59067f1e559389cd3e0e377c8b96', - output_index: 1, - script: - '483045022100e443784abe25b6d39e01c95900834bf4eeaa82505ac0eb84c08e11c287d467de02203327c2b1136f4976f755ed7631b427d66db2278414e7faf1268eedf44c034e0c012103c69b905f7242b3688122f06951339a1ee00da652f6ecc6527ea6632146cace62', - output_value: 1000, - sequence: 4294967295, - addresses: ['12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'], - script_type: 'pay-to-pubkey-hash', - age: 530926, - }, - ], - outputs: [ - { - value: 800, - script: 'a914688eb9af71aab8ca221f4e6171a45fc46ea8743b87', - spent_by: '009c6219deeac341833642193e4a3b72e511105a61b48e375c5025b1bcbd6fb5', - addresses: ['3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'], - script_type: 'pay-to-script-hash', - }, - ], - value: -1000, - }; - - delete tx0.confirmations; - delete txExpected.confirmations; - delete tx0.preference; // that bs is not always the same - delete txExpected.preference; - assert.deepStrictEqual(tx0, txExpected); -}); - describe('currency', () => { it('fetches exchange rate and saves to AsyncStorage', async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; diff --git a/tests/integration/LegacyWallet.test.js b/tests/integration/LegacyWallet.test.js new file mode 100644 index 000000000..858dd1b9e --- /dev/null +++ b/tests/integration/LegacyWallet.test.js @@ -0,0 +1,154 @@ +/* global describe, it, jasmine, afterAll, beforeAll */ +import { LegacyWallet, SegwitP2SHWallet, SegwitBech32Wallet } from '../../class'; +let assert = require('assert'); +global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js +let BlueElectrum = require('../../BlueElectrum'); // so it connects ASAP + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; + +afterAll(async () => { + // after all tests we close socket so the test suite can actually terminate + BlueElectrum.forceDisconnect(); + return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination +}); + +beforeAll(async () => { + // awaiting for Electrum to be connected. For RN Electrum would naturally connect + // while app starts up, but for tests we need to wait for it + await BlueElectrum.waitTillConnected(); +}); + +describe('LegacyWallet', function() { + it('can serialize and unserialize correctly', () => { + let a = new LegacyWallet(); + a.setLabel('my1'); + let key = JSON.stringify(a); + + let b = LegacyWallet.fromJson(key); + assert(key === JSON.stringify(b)); + + assert.strictEqual(key, JSON.stringify(b)); + }); + + it('can validate addresses', () => { + let w = new LegacyWallet(); + assert.ok(w.isAddressValid('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); + assert.ok(!w.isAddressValid('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2j')); + assert.ok(w.isAddressValid('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2')); + assert.ok(!w.isAddressValid('3BDsBDxDimYgNZzsqszNZobqQq3yeUo')); + assert.ok(!w.isAddressValid('12345')); + assert.ok(w.isAddressValid('bc1quuafy8htjjj263cvpj7md84magzmc8svmh8lrm')); + assert.ok(w.isAddressValid('BC1QH6TF004TY7Z7UN2V5NTU4MKF630545GVHS45U7')); + }); + + it('can fetch balance', async () => { + let w = new LegacyWallet(); + w._address = '115fUy41sZkAG14CmdP1VbEKcNRZJWkUWG'; // hack internals + assert.ok(w.weOwnAddress('115fUy41sZkAG14CmdP1VbEKcNRZJWkUWG')); + assert.ok(!w.weOwnAddress('aaa')); + assert.ok(w.getBalance() === 0); + assert.ok(w.getUnconfirmedBalance() === 0); + assert.ok(w._lastBalanceFetch === 0); + await w.fetchBalance(); + assert.ok(w.getBalance() === 18262000); + assert.ok(w.getUnconfirmedBalance() === 0); + assert.ok(w._lastBalanceFetch > 0); + }); + + it('can fetch TXs', async () => { + let w = new LegacyWallet(); + w._address = '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'; + await w.fetchTransactions(); + assert.strictEqual(w.getTransactions().length, 2); + + for (let tx of w.getTransactions()) { + assert.ok(tx.hash); + assert.ok(tx.value); + assert.ok(tx.received); + assert.ok(tx.confirmations > 1); + } + }); + + it('can fetch UTXO', async () => { + let w = new LegacyWallet(); + w._address = '12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX'; + await w.fetchUtxo(); + assert.ok(w.utxo.length > 0, 'unexpected empty UTXO'); + assert.ok(w.getUtxo().length > 0, 'unexpected empty UTXO'); + + assert.ok(w.getUtxo()[0]['value']); + assert.ok(w.getUtxo()[0]['tx_output_n'] === 0 || w.getUtxo()[0]['tx_output_n'] === 1, JSON.stringify(w.getUtxo()[0])); + assert.ok(w.getUtxo()[0]['tx_hash']); + assert.ok(w.getUtxo()[0]['confirmations']); + }); +}); + +describe('SegwitP2SHWallet', function() { + it('can generate segwit P2SH address from WIF', async () => { + let l = new SegwitP2SHWallet(); + l.setSecret('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct'); + assert.ok(l.getAddress() === '34AgLJhwXrvmkZS1o5TrcdeevMt22Nar53', 'expected ' + l.getAddress()); + assert.ok(l.getAddress() === (await l.getAddressAsync())); + assert.ok(l.weOwnAddress('34AgLJhwXrvmkZS1o5TrcdeevMt22Nar53')); + }); +}); + +describe('SegwitBech32Wallet', function() { + it('can fetch balance', async () => { + let w = new SegwitBech32Wallet(); + w._address = 'bc1qn887fmetaytw4vj68vsh529ft408q8j9x3dndc'; + assert.ok(w.weOwnAddress('bc1qn887fmetaytw4vj68vsh529ft408q8j9x3dndc')); + await w.fetchBalance(); + assert.strictEqual(w.getBalance(), 100000); + }); + + it('can fetch UTXO', async () => { + let w = new SegwitBech32Wallet(); + w._address = 'bc1qn887fmetaytw4vj68vsh529ft408q8j9x3dndc'; + await w.fetchUtxo(); + assert.ok(w.getUtxo().length > 0, 'unexpected empty UTXO'); + + assert.ok(w.getUtxo()[0]['value']); + assert.ok(w.getUtxo()[0]['tx_output_n'] === 0); + assert.ok(w.getUtxo()[0]['tx_hash']); + assert.ok(w.getUtxo()[0]['confirmations']); + }); + + it('can fetch TXs', async () => { + let w = new LegacyWallet(); + w._address = 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv'; + await w.fetchTransactions(); + assert.strictEqual(w.getTransactions().length, 2); + + for (let tx of w.getTransactions()) { + assert.ok(tx.hash); + assert.ok(tx.value); + assert.ok(tx.received); + assert.ok(tx.confirmations > 1); + } + + assert.strictEqual(w.getTransactions()[0].value, -892111); + assert.strictEqual(w.getTransactions()[1].value, 892111); + }); + + it('can fetch TXs', async () => { + let w = new LegacyWallet(); + w._address = 'bc1qn887fmetaytw4vj68vsh529ft408q8j9x3dndc'; + assert.ok(w.weOwnAddress('bc1qn887fmetaytw4vj68vsh529ft408q8j9x3dndc')); + await w.fetchTransactions(); + assert.strictEqual(w.getTransactions().length, 1); + + for (let tx of w.getTransactions()) { + assert.ok(tx.hash); + assert.strictEqual(tx.value, 100000); + assert.ok(tx.received); + assert.ok(tx.confirmations > 1); + } + + let tx0 = w.getTransactions()[0]; + assert.ok(tx0['inputs']); + assert.ok(tx0['inputs'].length === 1); + assert.ok(tx0['outputs']); + assert.ok(tx0['outputs'].length === 3); + }); +}); diff --git a/tests/integration/WatchOnlyWallet.test.js b/tests/integration/WatchOnlyWallet.test.js index 94ff55257..7552d2eae 100644 --- a/tests/integration/WatchOnlyWallet.test.js +++ b/tests/integration/WatchOnlyWallet.test.js @@ -16,6 +16,8 @@ beforeAll(async () => { await BlueElectrum.waitTillConnected(); }); +jasmine.DEFAULT_TIMEOUT_INTERVAL = 500 * 1000; + describe('Watch only wallet', () => { it('can fetch balance', async () => { let w = new WatchOnlyWallet(); @@ -25,12 +27,11 @@ describe('Watch only wallet', () => { }); it('can fetch tx', async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000; let w = new WatchOnlyWallet(); - w.setSecret('167zK5iZrs1U6piDqubD3FjRqUTM2CZnb8'); await w.fetchTransactions(); - assert.strictEqual(w.getTransactions().length, 233); + assert.ok(w.getTransactions().length >= 215); + // should be 233 but electrum server cant return huge transactions >.< w = new WatchOnlyWallet(); w.setSecret('1BiJW1jyUaxcJp2JWwbPLPzB1toPNWTFJV'); @@ -42,8 +43,33 @@ describe('Watch only wallet', () => { assert.strictEqual(w.getTransactions().length, 2); }); + it('can fetch TXs with values', async () => { + let w = new WatchOnlyWallet(); + for (let sec of [ + 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv', + 'BC1QUHNVE8Q4TK3UNHMJTS7YMXV8CD6W9XV8WY29UV', + 'bitcoin:bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv', + 'BITCOIN:BC1QUHNVE8Q4TK3UNHMJTS7YMXV8CD6W9XV8WY29UV', + ]) { + w.setSecret(sec); + assert.strictEqual(w.getAddress(), 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv'); + assert.strictEqual(await w.getAddressAsync(), 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv'); + assert.ok(w.weOwnAddress('bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv')); + await w.fetchTransactions(); + + for (let tx of w.getTransactions()) { + assert.ok(tx.hash); + assert.ok(tx.value); + assert.ok(tx.received); + assert.ok(tx.confirmations > 1); + } + + assert.strictEqual(w.getTransactions()[0].value, -892111); + assert.strictEqual(w.getTransactions()[1].value, 892111); + } + }); + it('can fetch complex TXs', async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 120 * 1000; let w = new WatchOnlyWallet(); w.setSecret('3NLnALo49CFEF4tCRhCvz45ySSfz3UktZC'); await w.fetchTransactions(); @@ -54,28 +80,34 @@ describe('Watch only wallet', () => { it('can validate address', async () => { let w = new WatchOnlyWallet(); - w.setSecret('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'); - assert.ok(w.valid()); - assert.strictEqual(w.isHd(), false); - w.setSecret('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'); - assert.ok(w.valid()); - assert.strictEqual(w.isHd(), false); + for (let secret of [ + 'bc1quhnve8q4tk3unhmjts7ymxv8cd6w9xv8wy29uv', + '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG', + '3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2', + 'BC1QUHNVE8Q4TK3UNHMJTS7YMXV8CD6W9XV8WY29UV', + ]) { + w.setSecret(secret); + assert.ok(w.valid()); + assert.strictEqual(w.isHd(), false); + } + w.setSecret('not valid'); assert.ok(!w.valid()); - w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps'); - assert.ok(w.valid()); - w.setSecret('ypub6XRzrn3HB1tjhhvrHbk1vnXCecZEdXohGzCk3GXwwbDoJ3VBzZ34jNGWbC6WrS7idXrYjjXEzcPDX5VqnHEnuNf5VAXgLfSaytMkJ2rwVqy'); - assert.ok(w.valid()); - w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP'); - assert.ok(w.valid()); - assert.strictEqual(w.isHd(), true); - assert.strictEqual(w.getMasterFingerprint(), false); - assert.strictEqual(w.getMasterFingerprintHex(), '00000000'); + for (let secret of [ + 'xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps', + 'ypub6XRzrn3HB1tjhhvrHbk1vnXCecZEdXohGzCk3GXwwbDoJ3VBzZ34jNGWbC6WrS7idXrYjjXEzcPDX5VqnHEnuNf5VAXgLfSaytMkJ2rwVqy', + 'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP', + ]) { + w.setSecret(secret); + assert.ok(w.valid()); + assert.strictEqual(w.isHd(), true); + assert.strictEqual(w.getMasterFingerprint(), false); + assert.strictEqual(w.getMasterFingerprintHex(), '00000000'); + } }); it('can fetch balance & transactions from zpub HD', async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; let w = new WatchOnlyWallet(); w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP'); await w.fetchBalance(); @@ -117,7 +149,6 @@ describe('Watch only wallet', () => { }); it('can import coldcard/electrum compatible JSON skeleton wallet, and create a tx with master fingerprint', async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 120 * 1000; const skeleton = '{"keystore": {"ckcc_xpub": "xpub661MyMwAqRbcGmUDQVKxmhEESB5xTk8hbsdTSV3Pmhm3HE9Fj3s45R9Y8LwyaQWjXXPytZjuhTKSyCBPeNrB1VVWQq1HCvjbEZ27k44oNmg", "xpub": "zpub6rFDtF1nuXZ9PUL4XzKURh3vJBW6Kj6TUrYL4qPtFNtDXtcTVfiqjQDyrZNwjwzt5HS14qdqo3Co2282Lv3Re6Y5wFZxAVuMEpeygnnDwfx", "label": "Coldcard Import 168DD603", "ckcc_xfp": 64392470, "type": "hardware", "hw_type": "coldcard", "derivation": "m/84\'/0\'/0\'"}, "wallet_type": "standard", "use_encryption": false, "seed_version": 17}'; let w = new WatchOnlyWallet(); @@ -175,7 +206,6 @@ describe('Watch only wallet', () => { }); it('can fetch balance & transactions from ypub HD', async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; let w = new WatchOnlyWallet(); w.setSecret('ypub6Y9u3QCRC1HkZv3stNxcQVwmw7vC7KX5Ldz38En5P88RQbesP2oy16hNyQocVCfYRQPxdHcd3pmu9AFhLv7NdChWmw5iNLryZ2U6EEHdnfo'); await w.fetchBalance(); @@ -186,7 +216,6 @@ describe('Watch only wallet', () => { }); it('can fetch balance & transactions from xpub HD', async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; let w = new WatchOnlyWallet(); w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps'); await w.fetchBalance(); @@ -197,7 +226,6 @@ describe('Watch only wallet', () => { }); it('can fetch large HD', async () => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 500 * 1000; let w = new WatchOnlyWallet(); w.setSecret('ypub6WnnYxkQCGeowv4BXq9Y9PHaXgHMJg9TkFaDJkunhcTAfbDw8z3LvV9kFNHGjeVaEoGdsSJgaMWpUBvYvpYGMJd43gTK5opecVVkvLwKttx'); await w.fetchBalance();