mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-22 23:08:07 +01:00
Merge branch 'master' of github.com:BlueWallet/BlueWallet
This commit is contained in:
commit
f44f86828e
14 changed files with 576 additions and 33 deletions
|
@ -1,4 +1,5 @@
|
|||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import { SegwitBech32Wallet } from './class';
|
||||
const ElectrumClient = require('electrum-client');
|
||||
let bitcoin = require('bitcoinjs-lib');
|
||||
let reverse = require('buffer-reverse');
|
||||
|
@ -19,7 +20,6 @@ const hardcodedPeers = [
|
|||
{ host: 'electrum2.bluewallet.io', tcp: '50001' },
|
||||
{ host: 'electrum3.bluewallet.io', tcp: '50001' },
|
||||
{ host: 'electrum3.bluewallet.io', tcp: '50001' }, // 2x weight
|
||||
{ host: 'electrum.coinop.cc', tcp: '50001' },
|
||||
];
|
||||
|
||||
let mainClient = false;
|
||||
|
@ -123,23 +123,47 @@ async function getTransactionsByAddress(address) {
|
|||
return history;
|
||||
}
|
||||
|
||||
async function getTransactionsFullByAddress(address) {
|
||||
let txs = await this.getTransactionsByAddress(address);
|
||||
let ret = [];
|
||||
for (let tx of txs) {
|
||||
let full = await mainClient.blockchainTransaction_get(tx.tx_hash, true);
|
||||
full.address = address;
|
||||
for (let vin of full.vin) {
|
||||
vin.address = SegwitBech32Wallet.witnessToAddress(vin.txinwitness[1]);
|
||||
// now we need to fetch previous TX where this VIN became an output, so we can see its amount
|
||||
let prevTxForVin = await mainClient.blockchainTransaction_get(vin.txid, true);
|
||||
if (prevTxForVin && prevTxForVin.vout && prevTxForVin.vout[vin.vout]) {
|
||||
vin.value = prevTxForVin.vout[vin.vout].value;
|
||||
}
|
||||
}
|
||||
delete full.hex; // compact
|
||||
delete full.hash; // compact
|
||||
ret.push(full);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param addresses {Array}
|
||||
* @returns {Promise<{balance: number, unconfirmed_balance: number}>}
|
||||
* @returns {Promise<{balance: number, unconfirmed_balance: number, addresses: object}>}
|
||||
*/
|
||||
async function multiGetBalanceByAddress(addresses) {
|
||||
if (!mainClient) throw new Error('Electrum client is not connected');
|
||||
let balance = 0;
|
||||
let unconfirmedBalance = 0;
|
||||
let addressesAssoc = {};
|
||||
for (let addr of addresses) {
|
||||
let b = await getBalanceByAddress(addr);
|
||||
|
||||
balance += b.confirmed;
|
||||
unconfirmedBalance += b.unconfirmed_balance;
|
||||
unconfirmedBalance += b.unconfirmed;
|
||||
addressesAssoc[addr] = b;
|
||||
}
|
||||
|
||||
return { balance, unconfirmed_balance: unconfirmedBalance };
|
||||
return { balance, unconfirmed_balance: unconfirmedBalance, addresses: addressesAssoc };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -187,6 +211,7 @@ async function broadcast(hex) {
|
|||
module.exports.getBalanceByAddress = getBalanceByAddress;
|
||||
module.exports.getTransactionsByAddress = getTransactionsByAddress;
|
||||
module.exports.multiGetBalanceByAddress = multiGetBalanceByAddress;
|
||||
module.exports.getTransactionsFullByAddress = getTransactionsFullByAddress;
|
||||
module.exports.waitTillConnected = waitTillConnected;
|
||||
module.exports.estimateFees = estimateFees;
|
||||
module.exports.broadcast = broadcast;
|
||||
|
|
|
@ -6,7 +6,8 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000;
|
|||
|
||||
afterAll(() => {
|
||||
// after all tests we close socket so the test suite can actually terminate
|
||||
return BlueElectrum.forceDisconnect();
|
||||
BlueElectrum.forceDisconnect();
|
||||
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
|
@ -60,18 +61,51 @@ describe('Electrum', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('BlueElectrum works', async function() {
|
||||
it('BlueElectrum can do getBalanceByAddress()', async function() {
|
||||
let address = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
|
||||
let balance = await BlueElectrum.getBalanceByAddress(address);
|
||||
assert.strictEqual(balance.confirmed, 51432);
|
||||
assert.strictEqual(balance.unconfirmed, 0);
|
||||
assert.strictEqual(balance.addr, address);
|
||||
});
|
||||
|
||||
let txs = await BlueElectrum.getTransactionsByAddress(address);
|
||||
it('BlueElectrum can do getTransactionsByAddress()', async function() {
|
||||
let txs = await BlueElectrum.getTransactionsByAddress('bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
|
||||
assert.strictEqual(txs.length, 1);
|
||||
assert.strictEqual(txs[0].tx_hash, 'ad00a92409d8982a1d7f877056dbed0c4337d2ebab70b30463e2802279fb936d');
|
||||
assert.strictEqual(txs[0].height, 563077);
|
||||
});
|
||||
|
||||
it.only('BlueElectrum can do getTransactionsFullByAddress()', async function() {
|
||||
let txs = await BlueElectrum.getTransactionsFullByAddress('bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
|
||||
for (let tx of txs) {
|
||||
assert.ok(tx.tx_hash);
|
||||
assert.ok(tx.height);
|
||||
assert.ok(tx.address === 'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
|
||||
assert.ok(tx.txid);
|
||||
assert.ok(tx.confirmations);
|
||||
assert.ok(tx.vin);
|
||||
assert.ok(tx.vout);
|
||||
assert.ok(tx.vout[0].value);
|
||||
assert.ok(tx.vout[0].scriptPubKey);
|
||||
}
|
||||
});
|
||||
|
||||
it('BlueElectrum can do multiGetBalanceByAddress()', async function() {
|
||||
let balances = await BlueElectrum.multiGetBalanceByAddress([
|
||||
'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh',
|
||||
'bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p',
|
||||
'bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r',
|
||||
'bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy',
|
||||
]);
|
||||
|
||||
assert.strictEqual(balances.balance, 200000);
|
||||
assert.strictEqual(balances.unconfirmed_balance, 0);
|
||||
assert.strictEqual(balances.addresses['bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'].confirmed, 50000);
|
||||
assert.strictEqual(balances.addresses['bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'].unconfirmed, 0);
|
||||
assert.strictEqual(balances.addresses['bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p'].confirmed, 50000);
|
||||
assert.strictEqual(balances.addresses['bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p'].unconfirmed, 0);
|
||||
assert.strictEqual(balances.addresses['bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r'].confirmed, 50000);
|
||||
assert.strictEqual(balances.addresses['bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r'].unconfirmed, 0);
|
||||
assert.strictEqual(balances.addresses['bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy'].confirmed, 50000);
|
||||
assert.strictEqual(balances.addresses['bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy'].unconfirmed, 0);
|
||||
});
|
||||
});
|
||||
|
|
145
HDBech32Wallet.test.js
Normal file
145
HDBech32Wallet.test.js
Normal file
|
@ -0,0 +1,145 @@
|
|||
/* global it, describe, jasmine, afterAll, beforeAll */
|
||||
import { HDSegwitBech32Wallet } from './class';
|
||||
global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment
|
||||
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
|
||||
|
||||
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();
|
||||
console.log('electrum connected');
|
||||
});
|
||||
|
||||
describe('Bech32 Segwit HD (BIP84)', () => {
|
||||
it('can create', async function() {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30 * 1000;
|
||||
let mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
let hd = new HDSegwitBech32Wallet();
|
||||
hd.setSecret(mnemonic);
|
||||
|
||||
assert.strictEqual(true, hd.validateMnemonic());
|
||||
assert.strictEqual(
|
||||
'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs',
|
||||
hd.getXpub(),
|
||||
);
|
||||
|
||||
assert.strictEqual(hd._getExternalWIFByIndex(0), 'KyZpNDKnfs94vbrwhJneDi77V6jF64PWPF8x5cdJb8ifgg2DUc9d');
|
||||
assert.strictEqual(hd._getExternalWIFByIndex(1), 'Kxpf5b8p3qX56DKEe5NqWbNUP9MnqoRFzZwHRtsFqhzuvUJsYZCy');
|
||||
assert.strictEqual(hd._getInternalWIFByIndex(0), 'KxuoxufJL5csa1Wieb2kp29VNdn92Us8CoaUG3aGtPtcF3AzeXvF');
|
||||
assert.ok(hd._getInternalWIFByIndex(0) !== hd._getInternalWIFByIndex(1));
|
||||
|
||||
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu');
|
||||
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g');
|
||||
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el');
|
||||
assert.ok(hd._getInternalAddressByIndex(0) !== hd._getInternalAddressByIndex(1));
|
||||
|
||||
assert.ok(hd._lastBalanceFetch === 0);
|
||||
await hd.fetchBalance();
|
||||
assert.strictEqual(hd.getBalance(), 0);
|
||||
assert.ok(hd._lastBalanceFetch > 0);
|
||||
|
||||
// checking that internal pointer and async address getter return the same address
|
||||
let freeAddress = await hd.getAddressAsync();
|
||||
assert.strictEqual(hd.next_free_address_index, 0);
|
||||
assert.strictEqual(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress);
|
||||
let freeChangeAddress = await hd.getChangeAddressAsync();
|
||||
assert.strictEqual(hd.next_free_change_address_index, 0);
|
||||
assert.strictEqual(hd._getInternalAddressByIndex(hd.next_free_change_address_index), freeChangeAddress);
|
||||
});
|
||||
|
||||
it('can fetch balance', async function() {
|
||||
if (!process.env.HD_MNEMONIC) {
|
||||
console.error('process.env.HD_MNEMONIC not set, skipped');
|
||||
return;
|
||||
}
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
|
||||
let hd = new HDSegwitBech32Wallet();
|
||||
hd.setSecret(process.env.HD_MNEMONIC);
|
||||
assert.ok(hd.validateMnemonic());
|
||||
|
||||
assert.strictEqual(
|
||||
'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP',
|
||||
hd.getXpub(),
|
||||
);
|
||||
|
||||
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p');
|
||||
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh');
|
||||
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy');
|
||||
assert.strictEqual(hd._getInternalAddressByIndex(1), 'bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r');
|
||||
|
||||
await hd.fetchBalance();
|
||||
assert.strictEqual(hd.getBalance(), 200000);
|
||||
assert.strictEqual(await hd.getAddressAsync(), hd._getExternalAddressByIndex(2));
|
||||
assert.strictEqual(await hd.getChangeAddressAsync(), hd._getInternalAddressByIndex(2));
|
||||
assert.strictEqual(hd.next_free_address_index, 2);
|
||||
assert.strictEqual(hd.next_free_change_address_index, 2);
|
||||
|
||||
// now, reset HD wallet, and find free addresses from scratch:
|
||||
hd = new HDSegwitBech32Wallet();
|
||||
hd.setSecret(process.env.HD_MNEMONIC);
|
||||
|
||||
assert.strictEqual(await hd.getAddressAsync(), hd._getExternalAddressByIndex(2));
|
||||
assert.strictEqual(await hd.getChangeAddressAsync(), hd._getInternalAddressByIndex(2));
|
||||
assert.strictEqual(hd.next_free_address_index, 2);
|
||||
assert.strictEqual(hd.next_free_change_address_index, 2);
|
||||
});
|
||||
|
||||
it('can fetch transactions', async function() {
|
||||
if (!process.env.HD_MNEMONIC) {
|
||||
console.error('process.env.HD_MNEMONIC not set, skipped');
|
||||
return;
|
||||
}
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
|
||||
let hd = new HDSegwitBech32Wallet();
|
||||
hd.setSecret(process.env.HD_MNEMONIC);
|
||||
assert.ok(hd.validateMnemonic());
|
||||
|
||||
await hd.fetchBalance();
|
||||
await hd.fetchTransactions();
|
||||
assert.strictEqual(hd.getTransactions().length, 4);
|
||||
// console.warn(JSON.stringify(hd.getTransactions(), null, 2));
|
||||
|
||||
for (let tx of hd.getTransactions()) {
|
||||
assert.ok(tx.hash);
|
||||
assert.strictEqual(tx.value, 50000);
|
||||
assert.ok(tx.timestamp);
|
||||
assert.ok(tx.confirmations > 1);
|
||||
}
|
||||
});
|
||||
|
||||
it('can generate addresses only via zpub', function() {
|
||||
let zpub = 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs';
|
||||
let hd = new HDSegwitBech32Wallet();
|
||||
hd._xpub = zpub;
|
||||
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu');
|
||||
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g');
|
||||
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el');
|
||||
assert.ok(hd._getInternalAddressByIndex(0) !== hd._getInternalAddressByIndex(1));
|
||||
});
|
||||
|
||||
it('can generate', async () => {
|
||||
let hd = new HDSegwitBech32Wallet();
|
||||
let hashmap = {};
|
||||
for (let c = 0; c < 1000; c++) {
|
||||
await hd.generate();
|
||||
let secret = hd.getSecret();
|
||||
if (hashmap[secret]) {
|
||||
throw new Error('Duplicate secret generated!');
|
||||
}
|
||||
hashmap[secret] = 1;
|
||||
assert.ok(secret.split(' ').length === 12 || secret.split(' ').length === 24);
|
||||
}
|
||||
|
||||
let hd2 = new HDSegwitBech32Wallet();
|
||||
hd2.setSecret(hd.getSecret());
|
||||
assert.ok(hd2.validateMnemonic());
|
||||
});
|
||||
});
|
|
@ -9,7 +9,8 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000;
|
|||
|
||||
afterAll(() => {
|
||||
// after all tests we close socket so the test suite can actually terminate
|
||||
return BlueElectrum.forceDisconnect();
|
||||
BlueElectrum.forceDisconnect();
|
||||
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
|
@ -79,7 +80,6 @@ it('HD (BIP49) can work with a gap', async function() {
|
|||
// console.log('external', c, hd._getExternalAddressByIndex(c));
|
||||
// }
|
||||
await hd.fetchTransactions();
|
||||
console.log('hd.transactions.length=', hd.transactions.length);
|
||||
assert.ok(hd.transactions.length >= 3);
|
||||
});
|
||||
|
||||
|
@ -90,7 +90,6 @@ it('Segwit HD (BIP49) can batch fetch many txs', async function() {
|
|||
await hd.fetchBalance();
|
||||
await hd.fetchTransactions();
|
||||
assert.ok(hd.transactions.length > 0);
|
||||
console.log('hd.transactions.length=', hd.transactions.length);
|
||||
});
|
||||
|
||||
it('Segwit HD (BIP49) can generate addressess only via ypub', function() {
|
||||
|
|
|
@ -102,7 +102,7 @@ android {
|
|||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "4.0.0"
|
||||
versionName "4.0.1"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
|
|
@ -416,28 +416,30 @@ export class AbstractHDWallet extends LegacyWallet {
|
|||
this.next_free_change_address_index = await binarySearchIterationForInternalAddress(100);
|
||||
this.next_free_address_index = await binarySearchIterationForExternalAddress(100);
|
||||
}
|
||||
}
|
||||
|
||||
this.usedAddresses = [];
|
||||
|
||||
// generating all involved addresses:
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
this.usedAddresses.push(this._getExternalAddressByIndex(c));
|
||||
}
|
||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||
this.usedAddresses.push(this._getInternalAddressByIndex(c));
|
||||
}
|
||||
} // end rescanning fresh wallet
|
||||
|
||||
// finally fetching balance
|
||||
let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses);
|
||||
this.balance = balance.balance;
|
||||
this.unconfirmed_balance = balance.unconfirmed_balance;
|
||||
this._lastBalanceFetch = +new Date();
|
||||
await this._fetchBalance();
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
async _fetchBalance() {
|
||||
this.usedAddresses = [];
|
||||
// generating all involved addresses:
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
this.usedAddresses.push(this._getExternalAddressByIndex(c));
|
||||
}
|
||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||
this.usedAddresses.push(this._getInternalAddressByIndex(c));
|
||||
}
|
||||
let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses);
|
||||
this.balance = balance.balance;
|
||||
this.unconfirmed_balance = balance.unconfirmed_balance;
|
||||
this._lastBalanceFetch = +new Date();
|
||||
}
|
||||
|
||||
async _fetchUtxoBatch(addresses) {
|
||||
const api = new Frisbee({
|
||||
baseURI: 'https://blockchain.info',
|
||||
|
|
|
@ -42,6 +42,10 @@ export class AbstractWallet {
|
|||
return this.label;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {number} Available to spend amount, int, in sats
|
||||
*/
|
||||
getBalance() {
|
||||
return this.balance;
|
||||
}
|
||||
|
|
333
class/hd-segwit-bech32-wallet.js
Normal file
333
class/hd-segwit-bech32-wallet.js
Normal file
|
@ -0,0 +1,333 @@
|
|||
import { AbstractHDWallet } from './abstract-hd-wallet';
|
||||
import { NativeModules } from 'react-native';
|
||||
import bitcoin from 'bitcoinjs-lib';
|
||||
import bip39 from 'bip39';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import b58 from 'bs58check';
|
||||
import signer from '../models/signer';
|
||||
const BlueElectrum = require('../BlueElectrum');
|
||||
|
||||
const { RNRandomBytes } = NativeModules;
|
||||
|
||||
/**
|
||||
* Converts zpub to xpub
|
||||
*
|
||||
* @param {String} zpub
|
||||
* @returns {String} xpub
|
||||
*/
|
||||
function _zpubToXpub(zpub) {
|
||||
let data = b58.decode(zpub);
|
||||
data = data.slice(4);
|
||||
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]);
|
||||
|
||||
return b58.encode(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Segwit Bech32 Bitcoin address
|
||||
*
|
||||
* @param hdNode
|
||||
* @returns {String}
|
||||
*/
|
||||
function _nodeToBech32SegwitAddress(hdNode) {
|
||||
const pubkeyBuf = hdNode.keyPair.getPublicKeyBuffer();
|
||||
var scriptPubKey = bitcoin.script.witnessPubKeyHash.output.encode(bitcoin.crypto.hash160(pubkeyBuf));
|
||||
var address = bitcoin.address.fromOutputScript(scriptPubKey);
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* HD Wallet (BIP39).
|
||||
* In particular, BIP84 (Bech32 Native Segwit)
|
||||
* @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
|
||||
*/
|
||||
export class HDSegwitBech32Wallet extends AbstractHDWallet {
|
||||
static type = 'HDsegwitBech32';
|
||||
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed
|
||||
this._balances_by_internal_index = {};
|
||||
|
||||
this._txs_by_external_index = {};
|
||||
this._txs_by_internal_index = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
getBalance() {
|
||||
let ret = 0;
|
||||
for (let bal of Object.values(this._balances_by_external_index)) {
|
||||
ret += bal.c;
|
||||
}
|
||||
for (let bal of Object.values(this._balances_by_internal_index)) {
|
||||
ret += bal.c;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
getUnconfirmedBalance() {
|
||||
let ret = 0;
|
||||
for (let bal of Object.values(this._balances_by_external_index)) {
|
||||
ret += bal.u;
|
||||
}
|
||||
for (let bal of Object.values(this._balances_by_internal_index)) {
|
||||
ret += bal.u;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
allowSend() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async generate() {
|
||||
let that = this;
|
||||
return new Promise(function(resolve) {
|
||||
if (typeof RNRandomBytes === 'undefined') {
|
||||
// CLI/CI environment
|
||||
// crypto should be provided globally by test launcher
|
||||
return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line
|
||||
if (err) throw err;
|
||||
that.secret = bip39.entropyToMnemonic(buf.toString('hex'));
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// RN environment
|
||||
RNRandomBytes.randomBytes(32, (err, bytes) => {
|
||||
if (err) throw new Error(err);
|
||||
let b = Buffer.from(bytes, 'base64').toString('hex');
|
||||
that.secret = bip39.entropyToMnemonic(b);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getExternalWIFByIndex(index) {
|
||||
return this._getWIFByIndex(false, index);
|
||||
}
|
||||
|
||||
_getInternalWIFByIndex(index) {
|
||||
return this._getWIFByIndex(true, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get internal/external WIF by wallet index
|
||||
* @param {Boolean} internal
|
||||
* @param {Number} index
|
||||
* @returns {*}
|
||||
* @private
|
||||
*/
|
||||
_getWIFByIndex(internal, index) {
|
||||
const mnemonic = this.secret;
|
||||
const seed = bip39.mnemonicToSeed(mnemonic);
|
||||
const root = bitcoin.HDNode.fromSeedBuffer(seed);
|
||||
const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`;
|
||||
const child = root.derivePath(path);
|
||||
|
||||
return child.keyPair.toWIF();
|
||||
}
|
||||
|
||||
_getNodeAddressByIndex(node, index) {
|
||||
index = index * 1; // cast to int
|
||||
if (node === 0) {
|
||||
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
|
||||
}
|
||||
|
||||
if (node === 1) {
|
||||
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
|
||||
}
|
||||
|
||||
const xpub = _zpubToXpub(this.getXpub());
|
||||
const hdNode = bitcoin.HDNode.fromBase58(xpub);
|
||||
const address = _nodeToBech32SegwitAddress(hdNode.derive(node).derive(index));
|
||||
|
||||
if (node === 0) {
|
||||
return (this.external_addresses_cache[index] = address);
|
||||
}
|
||||
|
||||
if (node === 1) {
|
||||
return (this.internal_addresses_cache[index] = address);
|
||||
}
|
||||
}
|
||||
|
||||
_getExternalAddressByIndex(index) {
|
||||
return this._getNodeAddressByIndex(0, index);
|
||||
}
|
||||
|
||||
_getInternalAddressByIndex(index) {
|
||||
return this._getNodeAddressByIndex(1, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returning zpub actually, not xpub. Keeping same method name
|
||||
* for compatibility.
|
||||
*
|
||||
* @return {String} zpub
|
||||
*/
|
||||
getXpub() {
|
||||
if (this._xpub) {
|
||||
return this._xpub; // cache hit
|
||||
}
|
||||
// first, getting xpub
|
||||
const mnemonic = this.secret;
|
||||
const seed = bip39.mnemonicToSeed(mnemonic);
|
||||
const root = bitcoin.HDNode.fromSeedBuffer(seed);
|
||||
|
||||
const path = "m/84'/0'/0'";
|
||||
const child = root.derivePath(path).neutered();
|
||||
const xpub = child.toBase58();
|
||||
|
||||
// bitcoinjs does not support zpub yet, so we just convert it from xpub
|
||||
let data = b58.decode(xpub);
|
||||
data = data.slice(4);
|
||||
data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]);
|
||||
this._xpub = b58.encode(data);
|
||||
|
||||
return this._xpub;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
async fetchTransactions() {
|
||||
// if txs are absent for some internal address in hierarchy - this is a sign
|
||||
// we should fetch txs for that address
|
||||
// OR if some address has unconfirmed balance - should fetch it's txs
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
if (!this._txs_by_external_index[c] || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) {
|
||||
this._txs_by_external_index[c] = await BlueElectrum.getTransactionsFullByAddress(this._getExternalAddressByIndex(c));
|
||||
}
|
||||
}
|
||||
for (let c = 0; c < this.next_free_change_address_index + 1 /* this.gap_limit */; c++) {
|
||||
if (!this._txs_by_internal_index[c] || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) {
|
||||
this._txs_by_internal_index[c] = await BlueElectrum.getTransactionsFullByAddress(this._getInternalAddressByIndex(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTransactions() {
|
||||
let txs = [];
|
||||
|
||||
for (let addressTxs of Object.values(this._txs_by_external_index)) {
|
||||
txs = txs.concat(addressTxs);
|
||||
}
|
||||
for (let addressTxs of Object.values(this._txs_by_internal_index)) {
|
||||
txs = txs.concat(addressTxs);
|
||||
}
|
||||
|
||||
let ret = [];
|
||||
for (let tx of txs) {
|
||||
tx.timestamp = tx.blocktime;
|
||||
tx.hash = tx.txid;
|
||||
tx.value = 0;
|
||||
|
||||
for (let vin of tx.vin) {
|
||||
// if input (spending) goes from our address - we are loosing!
|
||||
if (vin.address && this.weOwnAddress(vin.address)) {
|
||||
tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber();
|
||||
}
|
||||
}
|
||||
|
||||
for (let vout of tx.vout) {
|
||||
// when output goes to our address - this means we are gaining!
|
||||
if (
|
||||
vout.scriptPubKey &&
|
||||
vout.scriptPubKey.addresses &&
|
||||
vout.scriptPubKey.addresses[0] &&
|
||||
this.weOwnAddress(vout.scriptPubKey.addresses[0])
|
||||
) {
|
||||
tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber();
|
||||
}
|
||||
}
|
||||
ret.push(tx);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async _fetchBalance() {
|
||||
let addresses2fetch = [];
|
||||
|
||||
// generating all involved addresses.
|
||||
// if address is skipped in internal representation (`_balances_by_external_index` and `_balances_by_internal_index`)
|
||||
// then its a marker that this address should be fetched.
|
||||
// if it has unconfirmed balance - it is also a marker that it should be fetched
|
||||
// also it should be fetched if it is the last used address in hierarchy, just for any case,
|
||||
// or if it is next unused (plus several unused addressess according to gap limit)
|
||||
|
||||
// external
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
if (c >= this.next_free_address_index) {
|
||||
addresses2fetch.push(this._getExternalAddressByIndex(c));
|
||||
} else if (!this._balances_by_external_index[c]) {
|
||||
addresses2fetch.push(this._getExternalAddressByIndex(c));
|
||||
} else if (this._balances_by_external_index[c] && this._balances_by_external_index[c].u !== 0) {
|
||||
addresses2fetch.push(this._getExternalAddressByIndex(c));
|
||||
}
|
||||
}
|
||||
|
||||
// internal
|
||||
for (let c = 0; c < this.next_free_change_address_index + 1 /* this.gap_limit */; c++) {
|
||||
if (c >= this.next_free_change_address_index) {
|
||||
addresses2fetch.push(this._getInternalAddressByIndex(c));
|
||||
} else if (!this._balances_by_internal_index[c]) {
|
||||
addresses2fetch.push(this._getInternalAddressByIndex(c));
|
||||
} else if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].u !== 0) {
|
||||
addresses2fetch.push(this._getInternalAddressByIndex(c));
|
||||
}
|
||||
}
|
||||
|
||||
let balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch);
|
||||
|
||||
// converting to a more compact internal format
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
let addr = this._getExternalAddressByIndex(c);
|
||||
if (balances.addresses[addr]) {
|
||||
this._balances_by_external_index[c] = {
|
||||
c: balances.addresses[addr].confirmed,
|
||||
u: balances.addresses[addr].unconfirmed,
|
||||
};
|
||||
}
|
||||
}
|
||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||
let addr = this._getInternalAddressByIndex(c);
|
||||
if (balances.addresses[addr]) {
|
||||
this._balances_by_internal_index[c] = {
|
||||
c: balances.addresses[addr].confirmed,
|
||||
u: balances.addresses[addr].unconfirmed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this._lastBalanceFetch = +new Date();
|
||||
}
|
||||
|
||||
weOwnAddress(address) {
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
if (this._getExternalAddressByIndex(c) === address) return true;
|
||||
}
|
||||
for (let c = 0; c < this.next_free_change_address_index + 1 /* this.gap_limit */; c++) {
|
||||
if (this._getInternalAddressByIndex(c) === address) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
createTx(utxos, amount, fee, address) {
|
||||
for (let utxo of utxos) {
|
||||
utxo.wif = this._getWifForAddress(utxo.address);
|
||||
}
|
||||
|
||||
let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10));
|
||||
return signer.createHDSegwitTransaction(
|
||||
utxos,
|
||||
address,
|
||||
amountPlusFee,
|
||||
fee,
|
||||
this._getInternalAddressByIndex(this.next_free_change_address_index),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,3 +10,4 @@ export * from './hd-legacy-p2pkh-wallet';
|
|||
export * from './watch-only-wallet';
|
||||
export * from './lightning-custodian-wallet';
|
||||
export * from './abstract-hd-wallet';
|
||||
export * from './hd-segwit-bech32-wallet';
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.0.0</string>
|
||||
<string>4.0.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.0.0</string>
|
||||
<string>4.0.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>239</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.0.0</string>
|
||||
<string>4.0.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>239</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "BlueWallet",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "BlueWallet",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.1",
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
|
|
Loading…
Add table
Reference in a new issue