REF: single-address wallets now work through electrum

This commit is contained in:
Overtorment 2020-03-09 18:51:34 +00:00
parent fbd9ba7739
commit 00fe264f22
6 changed files with 330 additions and 444 deletions

View file

@ -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);

View file

@ -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.<void>}
*/
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.<void>}
*/
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;
}
}

View file

@ -121,12 +121,10 @@ const About = () => {
<BlueText h3>Built with awesome:</BlueText>
<BlueSpacing20 />
<BlueText h4>* React Native</BlueText>
<BlueText h4>* Bitcoinjs-lib</BlueText>
<BlueText h4>* blockcypher.com API</BlueText>
<BlueText h4>* bitcoinjs-lib</BlueText>
<BlueText h4>* Nodejs</BlueText>
<BlueText h4>* react-native-elements</BlueText>
<BlueText h4>* rn-nodeify</BlueText>
<BlueText h4>* bignumber.js</BlueText>
<BlueText h4>* Electrum server</BlueText>
<BlueSpacing20 />
<BlueSpacing20 />
<BlueButton onPress={handleOnReleaseNotesPress} title="Release notes" />

View file

@ -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(<BlueHeader />).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;

View file

@ -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);
});
});

View file

@ -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();