Marcos Rodriguez
5 years ago
43 changed files with 2501 additions and 2450 deletions
@ -0,0 +1,891 @@ |
|||
import { NativeModules } from 'react-native'; |
|||
import bip39 from 'bip39'; |
|||
import BigNumber from 'bignumber.js'; |
|||
import b58 from 'bs58check'; |
|||
import { AbstractHDWallet } from './abstract-hd-wallet'; |
|||
const bitcoin = require('bitcoinjs-lib'); |
|||
const BlueElectrum = require('../BlueElectrum'); |
|||
const HDNode = require('bip32'); |
|||
const coinSelectAccumulative = require('coinselect/accumulative'); |
|||
const coinSelectSplit = require('coinselect/split'); |
|||
|
|||
const { RNRandomBytes } = NativeModules; |
|||
|
|||
export class AbstractHDElectrumWallet extends AbstractHDWallet { |
|||
static type = 'abstract'; |
|||
static typeReadable = 'abstract'; |
|||
static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68
|
|||
static finalRBFSequence = 4294967295; // 0xFFFFFFFF
|
|||
|
|||
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 = {}; |
|||
|
|||
this._utxo = []; |
|||
} |
|||
|
|||
/** |
|||
* @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 + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0); |
|||
} |
|||
|
|||
/** |
|||
* @inheritDoc |
|||
*/ |
|||
timeToRefreshTransaction() { |
|||
for (let tx of this.getTransactions()) { |
|||
if (tx.confirmations < 7) return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
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; |
|||
} |
|||
|
|||
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 {string|false} Either string WIF or FALSE if error happened |
|||
* @private |
|||
*/ |
|||
_getWIFByIndex(internal, index) { |
|||
if (!this.secret) return false; |
|||
const mnemonic = this.secret; |
|||
const seed = bip39.mnemonicToSeed(mnemonic); |
|||
const root = HDNode.fromSeed(seed); |
|||
const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`; |
|||
const child = root.derivePath(path); |
|||
|
|||
return child.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
|
|||
} |
|||
|
|||
if (node === 0 && !this._node0) { |
|||
const xpub = this.constructor._zpubToXpub(this.getXpub()); |
|||
const hdNode = HDNode.fromBase58(xpub); |
|||
this._node0 = hdNode.derive(node); |
|||
} |
|||
|
|||
if (node === 1 && !this._node1) { |
|||
const xpub = this.constructor._zpubToXpub(this.getXpub()); |
|||
const hdNode = HDNode.fromBase58(xpub); |
|||
this._node1 = hdNode.derive(node); |
|||
} |
|||
|
|||
let address; |
|||
if (node === 0) { |
|||
address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index)); |
|||
} |
|||
|
|||
if (node === 1) { |
|||
address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index)); |
|||
} |
|||
|
|||
if (node === 0) { |
|||
return (this.external_addresses_cache[index] = address); |
|||
} |
|||
|
|||
if (node === 1) { |
|||
return (this.internal_addresses_cache[index] = address); |
|||
} |
|||
} |
|||
|
|||
_getNodePubkeyByIndex(node, index) { |
|||
index = index * 1; // cast to int
|
|||
|
|||
if (node === 0 && !this._node0) { |
|||
const xpub = this.constructor._zpubToXpub(this.getXpub()); |
|||
const hdNode = HDNode.fromBase58(xpub); |
|||
this._node0 = hdNode.derive(node); |
|||
} |
|||
|
|||
if (node === 1 && !this._node1) { |
|||
const xpub = this.constructor._zpubToXpub(this.getXpub()); |
|||
const hdNode = HDNode.fromBase58(xpub); |
|||
this._node1 = hdNode.derive(node); |
|||
} |
|||
|
|||
if (node === 0) { |
|||
return this._node0.derive(index).publicKey; |
|||
} |
|||
|
|||
if (node === 1) { |
|||
return this._node1.derive(index).publicKey; |
|||
} |
|||
} |
|||
|
|||
_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 = HDNode.fromSeed(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
|
|||
// OR some tx for address is unconfirmed
|
|||
// OR some tx has < 7 confirmations
|
|||
|
|||
// fetching transactions in batch: first, getting batch history for all addresses,
|
|||
// then batch fetching all involved txids
|
|||
// finally, batch fetching txids of all inputs (needed to see amounts & addresses of those inputs)
|
|||
// then we combine it all together
|
|||
|
|||
let addresses2fetch = []; |
|||
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
// external addresses first
|
|||
let hasUnconfirmed = false; |
|||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; |
|||
for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; |
|||
|
|||
if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) { |
|||
addresses2fetch.push(this._getExternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
// next, internal addresses
|
|||
let hasUnconfirmed = false; |
|||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; |
|||
for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; |
|||
|
|||
if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) { |
|||
addresses2fetch.push(this._getInternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
// 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; |
|||
} |
|||
} |
|||
|
|||
// 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 purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid
|
|||
// or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations); |
|||
} |
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations); |
|||
} |
|||
|
|||
// now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index
|
|||
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
for (let tx of Object.values(txdatas)) { |
|||
for (let vin of tx.vin) { |
|||
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { |
|||
// this TX is related to our address
|
|||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; |
|||
let clonedTx = Object.assign({}, tx); |
|||
clonedTx.inputs = tx.vin.slice(0); |
|||
clonedTx.outputs = tx.vout.slice(0); |
|||
delete clonedTx.vin; |
|||
delete clonedTx.vout; |
|||
|
|||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|||
let replaced = false; |
|||
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { |
|||
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { |
|||
replaced = true; |
|||
this._txs_by_external_index[c][cc] = clonedTx; |
|||
} |
|||
} |
|||
if (!replaced) this._txs_by_external_index[c].push(clonedTx); |
|||
} |
|||
} |
|||
for (let vout of tx.vout) { |
|||
if (vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { |
|||
// this TX is related to our address
|
|||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; |
|||
let clonedTx = Object.assign({}, tx); |
|||
clonedTx.inputs = tx.vin.slice(0); |
|||
clonedTx.outputs = tx.vout.slice(0); |
|||
delete clonedTx.vin; |
|||
delete clonedTx.vout; |
|||
|
|||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|||
let replaced = false; |
|||
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { |
|||
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { |
|||
replaced = true; |
|||
this._txs_by_external_index[c][cc] = clonedTx; |
|||
} |
|||
} |
|||
if (!replaced) this._txs_by_external_index[c].push(clonedTx); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
for (let tx of Object.values(txdatas)) { |
|||
for (let vin of tx.vin) { |
|||
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { |
|||
// this TX is related to our address
|
|||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; |
|||
let clonedTx = Object.assign({}, tx); |
|||
clonedTx.inputs = tx.vin.slice(0); |
|||
clonedTx.outputs = tx.vout.slice(0); |
|||
delete clonedTx.vin; |
|||
delete clonedTx.vout; |
|||
|
|||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|||
let replaced = false; |
|||
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { |
|||
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { |
|||
replaced = true; |
|||
this._txs_by_internal_index[c][cc] = clonedTx; |
|||
} |
|||
} |
|||
if (!replaced) this._txs_by_internal_index[c].push(clonedTx); |
|||
} |
|||
} |
|||
for (let vout of tx.vout) { |
|||
if (vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { |
|||
// this TX is related to our address
|
|||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; |
|||
let clonedTx = Object.assign({}, tx); |
|||
clonedTx.inputs = tx.vin.slice(0); |
|||
clonedTx.outputs = tx.vout.slice(0); |
|||
delete clonedTx.vin; |
|||
delete clonedTx.vout; |
|||
|
|||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|||
let replaced = false; |
|||
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { |
|||
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { |
|||
replaced = true; |
|||
this._txs_by_internal_index[c][cc] = clonedTx; |
|||
} |
|||
} |
|||
if (!replaced) this._txs_by_internal_index[c].push(clonedTx); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
this._lastTxFetch = +new Date(); |
|||
} |
|||
|
|||
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.received = tx.blocktime * 1000; |
|||
if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed
|
|||
tx.confirmations = tx.confirmations || 0; // unconfirmed
|
|||
tx.hash = tx.txid; |
|||
tx.value = 0; |
|||
|
|||
for (let vin of tx.inputs) { |
|||
// if input (spending) goes from our address - we are loosing!
|
|||
if ((vin.address && this.weOwnAddress(vin.address)) || (vin.addresses && vin.addresses[0] && this.weOwnAddress(vin.addresses[0]))) { |
|||
tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber(); |
|||
} |
|||
} |
|||
|
|||
for (let vout of tx.outputs) { |
|||
// when output goes to our address - this means we are gaining!
|
|||
if (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); |
|||
} |
|||
|
|||
// now, deduplication:
|
|||
let usedTxIds = {}; |
|||
let ret2 = []; |
|||
for (let tx of ret) { |
|||
if (!usedTxIds[tx.txid]) ret2.push(tx); |
|||
usedTxIds[tx.txid] = 1; |
|||
} |
|||
|
|||
return ret2.sort(function(a, b) { |
|||
return b.received - a.received; |
|||
}); |
|||
} |
|||
|
|||
async _binarySearchIterationForInternalAddress(index) { |
|||
const gerenateChunkAddresses = chunkNum => { |
|||
let ret = []; |
|||
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { |
|||
ret.push(this._getInternalAddressByIndex(c)); |
|||
} |
|||
return ret; |
|||
}; |
|||
|
|||
let lastChunkWithUsedAddressesNum = null; |
|||
let lastHistoriesWithUsedAddresses = null; |
|||
for (let c = 0; c < Math.round(index / this.gap_limit); c++) { |
|||
let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); |
|||
if (this.constructor._getTransactionsFromHistories(histories).length > 0) { |
|||
// in this particular chunk we have used addresses
|
|||
lastChunkWithUsedAddressesNum = c; |
|||
lastHistoriesWithUsedAddresses = histories; |
|||
} else { |
|||
// empty chunk. no sense searching more chunks
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
let lastUsedIndex = 0; |
|||
|
|||
if (lastHistoriesWithUsedAddresses) { |
|||
// now searching for last used address in batch lastChunkWithUsedAddressesNum
|
|||
for ( |
|||
let c = lastChunkWithUsedAddressesNum * this.gap_limit; |
|||
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; |
|||
c++ |
|||
) { |
|||
let address = this._getInternalAddressByIndex(c); |
|||
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { |
|||
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
|
|||
} |
|||
} |
|||
} |
|||
|
|||
return lastUsedIndex; |
|||
} |
|||
|
|||
async _binarySearchIterationForExternalAddress(index) { |
|||
const gerenateChunkAddresses = chunkNum => { |
|||
let ret = []; |
|||
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { |
|||
ret.push(this._getExternalAddressByIndex(c)); |
|||
} |
|||
return ret; |
|||
}; |
|||
|
|||
let lastChunkWithUsedAddressesNum = null; |
|||
let lastHistoriesWithUsedAddresses = null; |
|||
for (let c = 0; c < Math.round(index / this.gap_limit); c++) { |
|||
let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); |
|||
if (this.constructor._getTransactionsFromHistories(histories).length > 0) { |
|||
// in this particular chunk we have used addresses
|
|||
lastChunkWithUsedAddressesNum = c; |
|||
lastHistoriesWithUsedAddresses = histories; |
|||
} else { |
|||
// empty chunk. no sense searching more chunks
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
let lastUsedIndex = 0; |
|||
|
|||
if (lastHistoriesWithUsedAddresses) { |
|||
// now searching for last used address in batch lastChunkWithUsedAddressesNum
|
|||
for ( |
|||
let c = lastChunkWithUsedAddressesNum * this.gap_limit; |
|||
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; |
|||
c++ |
|||
) { |
|||
let address = this._getExternalAddressByIndex(c); |
|||
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { |
|||
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
|
|||
} |
|||
} |
|||
} |
|||
|
|||
return lastUsedIndex; |
|||
} |
|||
|
|||
async fetchBalance() { |
|||
try { |
|||
if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) { |
|||
// doing binary search for last used address:
|
|||
this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000); |
|||
this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000); |
|||
} // end rescanning fresh wallet
|
|||
|
|||
// finally fetching balance
|
|||
await this._fetchBalance(); |
|||
} catch (err) { |
|||
console.warn(err); |
|||
} |
|||
} |
|||
|
|||
async _fetchBalance() { |
|||
// probing future addressess in hierarchy whether they have any transactions, in case
|
|||
// our 'next free addr' pointers are lagging behind
|
|||
let tryAgain = false; |
|||
let txs = await BlueElectrum.getTransactionsByAddress( |
|||
this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1), |
|||
); |
|||
if (txs.length > 0) { |
|||
// whoa, someone uses our wallet outside! better catch up
|
|||
this.next_free_address_index += this.gap_limit; |
|||
tryAgain = true; |
|||
} |
|||
|
|||
txs = await BlueElectrum.getTransactionsByAddress( |
|||
this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1), |
|||
); |
|||
if (txs.length > 0) { |
|||
this.next_free_change_address_index += this.gap_limit; |
|||
tryAgain = true; |
|||
} |
|||
|
|||
// FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ]
|
|||
|
|||
if (tryAgain) return this._fetchBalance(); |
|||
|
|||
// next, business as usuall. fetch balances
|
|||
|
|||
let addresses2fetch = []; |
|||
|
|||
// generating all involved addresses.
|
|||
// basically, refetch all from index zero to maximum. doesnt matter
|
|||
// since we batch them 100 per call
|
|||
|
|||
// external
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
addresses2fetch.push(this._getExternalAddressByIndex(c)); |
|||
} |
|||
|
|||
// internal
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
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]) { |
|||
// first, if balances differ from what we store - we delete transactions for that
|
|||
// address so next fetchTransactions() will refetch everything
|
|||
if (this._balances_by_external_index[c]) { |
|||
if ( |
|||
this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed || |
|||
this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed |
|||
) { |
|||
delete this._txs_by_external_index[c]; |
|||
} |
|||
} |
|||
// update local representation of balances on that address:
|
|||
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]) { |
|||
// first, if balances differ from what we store - we delete transactions for that
|
|||
// address so next fetchTransactions() will refetch everything
|
|||
if (this._balances_by_internal_index[c]) { |
|||
if ( |
|||
this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed || |
|||
this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed |
|||
) { |
|||
delete this._txs_by_internal_index[c]; |
|||
} |
|||
} |
|||
// update local representation of balances on that address:
|
|||
this._balances_by_internal_index[c] = { |
|||
c: balances.addresses[addr].confirmed, |
|||
u: balances.addresses[addr].unconfirmed, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
this._lastBalanceFetch = +new Date(); |
|||
} |
|||
|
|||
async fetchUtxo() { |
|||
// considering only confirmed balance
|
|||
let addressess = []; |
|||
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) { |
|||
addressess.push(this._getExternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) { |
|||
addressess.push(this._getInternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
this._utxo = []; |
|||
for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) { |
|||
this._utxo = this._utxo.concat(arr); |
|||
} |
|||
|
|||
// backward compatibility TODO: remove when we make sure `.utxo` is not used
|
|||
this.utxo = this._utxo; |
|||
// this belongs in `.getUtxo()`
|
|||
for (let u of this.utxo) { |
|||
u.txid = u.txId; |
|||
u.amount = u.value; |
|||
u.wif = this._getWifForAddress(u.address); |
|||
u.confirmations = u.height ? 1 : 0; |
|||
} |
|||
} |
|||
|
|||
getUtxo() { |
|||
return this._utxo; |
|||
} |
|||
|
|||
_getDerivationPathByAddress(address) { |
|||
const path = "m/84'/0'/0'"; |
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c; |
|||
} |
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
_getPubkeyByAddress(address) { |
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
if (this._getExternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(0, c); |
|||
} |
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
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 + this.gap_limit; c++) { |
|||
if (this._getInternalAddressByIndex(c) === address) return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* @deprecated |
|||
*/ |
|||
createTx(utxos, amount, fee, address) { |
|||
throw new Error('Deprecated'); |
|||
} |
|||
|
|||
/** |
|||
* |
|||
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos |
|||
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate) |
|||
* @param feeRate {Number} satoshi per byte |
|||
* @param changeAddress {String} Excessive coins will go back to that address |
|||
* @param sequence {Number} Used in RBF |
|||
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case |
|||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}} |
|||
*/ |
|||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) { |
|||
if (!changeAddress) throw new Error('No change address provided'); |
|||
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence; |
|||
|
|||
let algo = coinSelectAccumulative; |
|||
if (targets.length === 1 && targets[0] && !targets[0].value) { |
|||
// we want to send MAX
|
|||
algo = coinSelectSplit; |
|||
} |
|||
|
|||
let { inputs, outputs, fee } = algo(utxos, targets, feeRate); |
|||
|
|||
// .inputs and .outputs will be undefined if no solution was found
|
|||
if (!inputs || !outputs) { |
|||
throw new Error('Not enough balance. Try sending smaller amount'); |
|||
} |
|||
|
|||
let psbt = new bitcoin.Psbt(); |
|||
|
|||
let c = 0; |
|||
let keypairs = {}; |
|||
let values = {}; |
|||
|
|||
inputs.forEach(input => { |
|||
let keyPair; |
|||
if (!skipSigning) { |
|||
// skiping signing related stuff
|
|||
keyPair = bitcoin.ECPair.fromWIF(this._getWifForAddress(input.address)); |
|||
keypairs[c] = keyPair; |
|||
} |
|||
values[c] = input.value; |
|||
c++; |
|||
if (!skipSigning) { |
|||
// skiping signing related stuff
|
|||
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input'); |
|||
} |
|||
let pubkey = this._getPubkeyByAddress(input.address); |
|||
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); |
|||
// this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting
|
|||
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
|||
let path = this._getDerivationPathByAddress(input.address); |
|||
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); |
|||
psbt.addInput({ |
|||
hash: input.txId, |
|||
index: input.vout, |
|||
sequence, |
|||
bip32Derivation: [ |
|||
{ |
|||
masterFingerprint, |
|||
path, |
|||
pubkey, |
|||
}, |
|||
], |
|||
witnessUtxo: { |
|||
script: p2wpkh.output, |
|||
value: input.value, |
|||
}, |
|||
}); |
|||
}); |
|||
|
|||
outputs.forEach(output => { |
|||
// if output has no address - this is change output
|
|||
let change = false; |
|||
if (!output.address) { |
|||
change = true; |
|||
output.address = changeAddress; |
|||
} |
|||
|
|||
let path = this._getDerivationPathByAddress(output.address); |
|||
let pubkey = this._getPubkeyByAddress(output.address); |
|||
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); |
|||
// this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting
|
|||
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
|||
|
|||
let outputData = { |
|||
address: output.address, |
|||
value: output.value, |
|||
}; |
|||
|
|||
if (change) { |
|||
outputData['bip32Derivation'] = [ |
|||
{ |
|||
masterFingerprint, |
|||
path, |
|||
pubkey, |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
psbt.addOutput(outputData); |
|||
}); |
|||
|
|||
if (!skipSigning) { |
|||
// skiping signing related stuff
|
|||
for (let cc = 0; cc < c; cc++) { |
|||
psbt.signInput(cc, keypairs[cc]); |
|||
} |
|||
} |
|||
|
|||
let tx; |
|||
if (!skipSigning) { |
|||
tx = psbt.finalizeAllInputs().extractTransaction(); |
|||
} |
|||
return { tx, inputs, outputs, fee, psbt }; |
|||
} |
|||
|
|||
/** |
|||
* Combines 2 PSBTs into final transaction from which you can |
|||
* get HEX and broadcast |
|||
* |
|||
* @param base64one {string} |
|||
* @param base64two {string} |
|||
* @returns {Transaction} |
|||
*/ |
|||
combinePsbt(base64one, base64two) { |
|||
const final1 = bitcoin.Psbt.fromBase64(base64one); |
|||
const final2 = bitcoin.Psbt.fromBase64(base64two); |
|||
final1.combine(final2); |
|||
return final1.finalizeAllInputs().extractTransaction(); |
|||
} |
|||
|
|||
/** |
|||
* Creates Segwit Bech32 Bitcoin address |
|||
* |
|||
* @param hdNode |
|||
* @returns {String} |
|||
*/ |
|||
static _nodeToBech32SegwitAddress(hdNode) { |
|||
return bitcoin.payments.p2wpkh({ |
|||
pubkey: hdNode.publicKey, |
|||
}).address; |
|||
} |
|||
|
|||
/** |
|||
* Converts zpub to xpub |
|||
* |
|||
* @param {String} zpub |
|||
* @returns {String} xpub |
|||
*/ |
|||
static _zpubToXpub(zpub) { |
|||
let data = b58.decode(zpub); |
|||
data = data.slice(4); |
|||
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); |
|||
|
|||
return b58.encode(data); |
|||
} |
|||
|
|||
static _getTransactionsFromHistories(histories) { |
|||
let txs = []; |
|||
for (let history of Object.values(histories)) { |
|||
for (let tx of history) { |
|||
txs.push(tx); |
|||
} |
|||
} |
|||
return txs; |
|||
} |
|||
|
|||
/** |
|||
* Broadcast txhex. Can throw an exception if failed |
|||
* |
|||
* @param {String} txhex |
|||
* @returns {Promise<boolean>} |
|||
*/ |
|||
async broadcastTx(txhex) { |
|||
let broadcast = await BlueElectrum.broadcastV2(txhex); |
|||
console.log({ broadcast }); |
|||
if (broadcast.indexOf('successfully') !== -1) return true; |
|||
return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok
|
|||
} |
|||
} |
@ -0,0 +1,221 @@ |
|||
import { AppStorage, LightningCustodianWallet } from './'; |
|||
import AsyncStorage from '@react-native-community/async-storage'; |
|||
import BitcoinBIP70TransactionDecode from '../bip70/bip70'; |
|||
const bitcoin = require('bitcoinjs-lib'); |
|||
const BlueApp = require('../BlueApp'); |
|||
class DeeplinkSchemaMatch { |
|||
static hasSchema(schemaString) { |
|||
if (typeof schemaString !== 'string' || schemaString.length <= 0) return false; |
|||
const lowercaseString = schemaString.trim().toLowerCase(); |
|||
return ( |
|||
lowercaseString.startsWith('bitcoin:') || |
|||
lowercaseString.startsWith('lightning:') || |
|||
lowercaseString.startsWith('blue:') || |
|||
lowercaseString.startsWith('bluewallet:') || |
|||
lowercaseString.startsWith('lapp:') |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Examines the content of the event parameter. |
|||
* If the content is recognizable, create a dictionary with the respective |
|||
* navigation dictionary required by react-navigation |
|||
* @param {Object} event |
|||
* @param {void} completionHandler |
|||
*/ |
|||
static navigationRouteFor(event, completionHandler) { |
|||
if (event.url === null) { |
|||
return; |
|||
} |
|||
if (typeof event.url !== 'string') { |
|||
return; |
|||
} |
|||
let isBothBitcoinAndLightning; |
|||
try { |
|||
isBothBitcoinAndLightning = DeeplinkSchemaMatch.isBothBitcoinAndLightning(event.url); |
|||
} catch (e) { |
|||
console.log(e); |
|||
} |
|||
if (isBothBitcoinAndLightning) { |
|||
completionHandler({ |
|||
routeName: 'HandleOffchainAndOnChain', |
|||
params: { |
|||
onWalletSelect: this.isBothBitcoinAndLightningWalletSelect, |
|||
}, |
|||
}); |
|||
} else if (DeeplinkSchemaMatch.isBitcoinAddress(event.url) || BitcoinBIP70TransactionDecode.matchesPaymentURL(event.url)) { |
|||
completionHandler({ |
|||
routeName: 'SendDetails', |
|||
params: { |
|||
uri: event.url, |
|||
}, |
|||
}); |
|||
} else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) { |
|||
completionHandler({ |
|||
routeName: 'ScanLndInvoice', |
|||
params: { |
|||
uri: event.url, |
|||
}, |
|||
}); |
|||
} else if (DeeplinkSchemaMatch.isLnUrl(event.url)) { |
|||
completionHandler({ |
|||
routeName: 'LNDCreateInvoice', |
|||
params: { |
|||
uri: event.url, |
|||
}, |
|||
}); |
|||
} else if (DeeplinkSchemaMatch.isSafelloRedirect(event)) { |
|||
let urlObject = url.parse(event.url, true) // eslint-disable-line
|
|||
|
|||
const safelloStateToken = urlObject.query['safello-state-token']; |
|||
|
|||
completionHandler({ |
|||
routeName: 'BuyBitcoin', |
|||
params: { |
|||
uri: event.url, |
|||
safelloStateToken, |
|||
}, |
|||
}); |
|||
} else { |
|||
let urlObject = url.parse(event.url, true); // eslint-disable-line
|
|||
console.log('parsed', urlObject); |
|||
(async () => { |
|||
if (urlObject.protocol === 'bluewallet:' || urlObject.protocol === 'lapp:' || urlObject.protocol === 'blue:') { |
|||
switch (urlObject.host) { |
|||
case 'openlappbrowser': |
|||
console.log('opening LAPP', urlObject.query.url); |
|||
// searching for LN wallet:
|
|||
let haveLnWallet = false; |
|||
for (let w of BlueApp.getWallets()) { |
|||
if (w.type === LightningCustodianWallet.type) { |
|||
haveLnWallet = true; |
|||
} |
|||
} |
|||
|
|||
if (!haveLnWallet) { |
|||
// need to create one
|
|||
let w = new LightningCustodianWallet(); |
|||
w.setLabel(this.state.label || w.typeReadable); |
|||
|
|||
try { |
|||
let lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB); |
|||
if (lndhub) { |
|||
w.setBaseURI(lndhub); |
|||
w.init(); |
|||
} |
|||
await w.createAccount(); |
|||
await w.authorize(); |
|||
} catch (Err) { |
|||
// giving up, not doing anything
|
|||
return; |
|||
} |
|||
BlueApp.wallets.push(w); |
|||
await BlueApp.saveToDisk(); |
|||
} |
|||
|
|||
// now, opening lapp browser and navigating it to URL.
|
|||
// looking for a LN wallet:
|
|||
let lnWallet; |
|||
for (let w of BlueApp.getWallets()) { |
|||
if (w.type === LightningCustodianWallet.type) { |
|||
lnWallet = w; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (!lnWallet) { |
|||
// something went wrong
|
|||
return; |
|||
} |
|||
|
|||
this.navigator && |
|||
this.navigator.dispatch( |
|||
completionHandler({ |
|||
routeName: 'LappBrowser', |
|||
params: { |
|||
fromSecret: lnWallet.getSecret(), |
|||
fromWallet: lnWallet, |
|||
url: urlObject.query.url, |
|||
}, |
|||
}), |
|||
); |
|||
break; |
|||
} |
|||
} |
|||
})(); |
|||
} |
|||
} |
|||
|
|||
static isBitcoinAddress(address) { |
|||
address = address |
|||
.replace('bitcoin:', '') |
|||
.replace('bitcoin=', '') |
|||
.split('?')[0]; |
|||
let isValidBitcoinAddress = false; |
|||
try { |
|||
bitcoin.address.toOutputScript(address); |
|||
isValidBitcoinAddress = true; |
|||
} catch (err) { |
|||
isValidBitcoinAddress = false; |
|||
} |
|||
return isValidBitcoinAddress; |
|||
} |
|||
|
|||
static isLightningInvoice(invoice) { |
|||
let isValidLightningInvoice = false; |
|||
if (invoice.toLowerCase().startsWith('lightning:lnb') || invoice.toLowerCase().startsWith('lnb')) { |
|||
isValidLightningInvoice = true; |
|||
} |
|||
return isValidLightningInvoice; |
|||
} |
|||
|
|||
static isLnUrl(text) { |
|||
if (text.toLowerCase().startsWith('lightning:lnurl') || text.toLowerCase().startsWith('lnurl')) { |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
static isSafelloRedirect(event) { |
|||
let urlObject = url.parse(event.url, true) // eslint-disable-line
|
|||
|
|||
return !!urlObject.query['safello-state-token']; |
|||
} |
|||
|
|||
static isBothBitcoinAndLightning(url) { |
|||
if (url.includes('lightning') && url.includes('bitcoin')) { |
|||
const txInfo = url.split(/(bitcoin:|lightning:|lightning=|bitcoin=)+/); |
|||
let bitcoin; |
|||
let lndInvoice; |
|||
for (const [index, value] of txInfo.entries()) { |
|||
try { |
|||
// Inside try-catch. We dont wan't to crash in case of an out-of-bounds error.
|
|||
if (value.startsWith('bitcoin')) { |
|||
bitcoin = `bitcoin:${txInfo[index + 1]}`; |
|||
if (!DeeplinkSchemaMatch.isBitcoinAddress(bitcoin)) { |
|||
bitcoin = false; |
|||
break; |
|||
} |
|||
} else if (value.startsWith('lightning')) { |
|||
lndInvoice = `lightning:${txInfo[index + 1]}`; |
|||
if (!this.isLightningInvoice(lndInvoice)) { |
|||
lndInvoice = false; |
|||
break; |
|||
} |
|||
} |
|||
} catch (e) { |
|||
console.log(e); |
|||
} |
|||
if (bitcoin && lndInvoice) break; |
|||
} |
|||
if (bitcoin && lndInvoice) { |
|||
return { bitcoin, lndInvoice }; |
|||
} else { |
|||
return undefined; |
|||
} |
|||
} |
|||
return undefined; |
|||
} |
|||
} |
|||
|
|||
export default DeeplinkSchemaMatch; |
@ -1,898 +1,23 @@ |
|||
import { AbstractHDWallet } from './abstract-hd-wallet'; |
|||
import { NativeModules } from 'react-native'; |
|||
import bip39 from 'bip39'; |
|||
import BigNumber from 'bignumber.js'; |
|||
import b58 from 'bs58check'; |
|||
const bitcoin = require('bitcoinjs-lib'); |
|||
const BlueElectrum = require('../BlueElectrum'); |
|||
const HDNode = require('bip32'); |
|||
const coinSelectAccumulative = require('coinselect/accumulative'); |
|||
const coinSelectSplit = require('coinselect/split'); |
|||
|
|||
const { RNRandomBytes } = NativeModules; |
|||
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet'; |
|||
|
|||
/** |
|||
* 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 { |
|||
export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet { |
|||
static type = 'HDsegwitBech32'; |
|||
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)'; |
|||
static defaultRBFSequence = 2147483648; // 1 << 31, minimum for replaceable transactions as per BIP68
|
|||
static finalRBFSequence = 4294967295; // 0xFFFFFFFF
|
|||
|
|||
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 = {}; |
|||
|
|||
this._utxo = []; |
|||
allowSend() { |
|||
return true; |
|||
} |
|||
|
|||
allowBatchSend() { |
|||
return true; |
|||
} |
|||
|
|||
allowSendMax(): boolean { |
|||
allowSendMax() { |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* @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 + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0); |
|||
} |
|||
|
|||
/** |
|||
* @inheritDoc |
|||
*/ |
|||
timeToRefreshTransaction() { |
|||
for (let tx of this.getTransactions()) { |
|||
if (tx.confirmations < 7) return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
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 {string|false} Either string WIF or FALSE if error happened |
|||
* @private |
|||
*/ |
|||
_getWIFByIndex(internal, index) { |
|||
if (!this.secret) return false; |
|||
const mnemonic = this.secret; |
|||
const seed = bip39.mnemonicToSeed(mnemonic); |
|||
const root = HDNode.fromSeed(seed); |
|||
const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`; |
|||
const child = root.derivePath(path); |
|||
|
|||
return child.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
|
|||
} |
|||
|
|||
if (node === 0 && !this._node0) { |
|||
const xpub = this.constructor._zpubToXpub(this.getXpub()); |
|||
const hdNode = HDNode.fromBase58(xpub); |
|||
this._node0 = hdNode.derive(node); |
|||
} |
|||
|
|||
if (node === 1 && !this._node1) { |
|||
const xpub = this.constructor._zpubToXpub(this.getXpub()); |
|||
const hdNode = HDNode.fromBase58(xpub); |
|||
this._node1 = hdNode.derive(node); |
|||
} |
|||
|
|||
let address; |
|||
if (node === 0) { |
|||
address = this.constructor._nodeToBech32SegwitAddress(this._node0.derive(index)); |
|||
} |
|||
|
|||
if (node === 1) { |
|||
address = this.constructor._nodeToBech32SegwitAddress(this._node1.derive(index)); |
|||
} |
|||
|
|||
if (node === 0) { |
|||
return (this.external_addresses_cache[index] = address); |
|||
} |
|||
|
|||
if (node === 1) { |
|||
return (this.internal_addresses_cache[index] = address); |
|||
} |
|||
} |
|||
|
|||
_getNodePubkeyByIndex(node, index) { |
|||
index = index * 1; // cast to int
|
|||
|
|||
if (node === 0 && !this._node0) { |
|||
const xpub = this.constructor._zpubToXpub(this.getXpub()); |
|||
const hdNode = HDNode.fromBase58(xpub); |
|||
this._node0 = hdNode.derive(node); |
|||
} |
|||
|
|||
if (node === 1 && !this._node1) { |
|||
const xpub = this.constructor._zpubToXpub(this.getXpub()); |
|||
const hdNode = HDNode.fromBase58(xpub); |
|||
this._node1 = hdNode.derive(node); |
|||
} |
|||
|
|||
if (node === 0) { |
|||
return this._node0.derive(index).publicKey; |
|||
} |
|||
|
|||
if (node === 1) { |
|||
return this._node1.derive(index).publicKey; |
|||
} |
|||
} |
|||
|
|||
_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 = HDNode.fromSeed(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
|
|||
// OR some tx for address is unconfirmed
|
|||
// OR some tx has < 7 confirmations
|
|||
|
|||
// fetching transactions in batch: first, getting batch history for all addresses,
|
|||
// then batch fetching all involved txids
|
|||
// finally, batch fetching txids of all inputs (needed to see amounts & addresses of those inputs)
|
|||
// then we combine it all together
|
|||
|
|||
let addresses2fetch = []; |
|||
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
// external addresses first
|
|||
let hasUnconfirmed = false; |
|||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; |
|||
for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; |
|||
|
|||
if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) { |
|||
addresses2fetch.push(this._getExternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
// next, internal addresses
|
|||
let hasUnconfirmed = false; |
|||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; |
|||
for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || !tx.confirmations || tx.confirmations < 7; |
|||
|
|||
if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) { |
|||
addresses2fetch.push(this._getInternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
// 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; |
|||
} |
|||
} |
|||
|
|||
// 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 purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid
|
|||
// or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations); |
|||
} |
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations); |
|||
} |
|||
|
|||
// now, we need to put transactions in all relevant `cells` of internal hashmaps: this._txs_by_internal_index && this._txs_by_external_index
|
|||
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
for (let tx of Object.values(txdatas)) { |
|||
for (let vin of tx.vin) { |
|||
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { |
|||
// this TX is related to our address
|
|||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; |
|||
let clonedTx = Object.assign({}, tx); |
|||
clonedTx.inputs = tx.vin.slice(0); |
|||
clonedTx.outputs = tx.vout.slice(0); |
|||
delete clonedTx.vin; |
|||
delete clonedTx.vout; |
|||
|
|||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|||
let replaced = false; |
|||
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { |
|||
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { |
|||
replaced = true; |
|||
this._txs_by_external_index[c][cc] = clonedTx; |
|||
} |
|||
} |
|||
if (!replaced) this._txs_by_external_index[c].push(clonedTx); |
|||
} |
|||
} |
|||
for (let vout of tx.vout) { |
|||
if (vout.scriptPubKey.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) { |
|||
// this TX is related to our address
|
|||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; |
|||
let clonedTx = Object.assign({}, tx); |
|||
clonedTx.inputs = tx.vin.slice(0); |
|||
clonedTx.outputs = tx.vout.slice(0); |
|||
delete clonedTx.vin; |
|||
delete clonedTx.vout; |
|||
|
|||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|||
let replaced = false; |
|||
for (let cc = 0; cc < this._txs_by_external_index[c].length; cc++) { |
|||
if (this._txs_by_external_index[c][cc].txid === clonedTx.txid) { |
|||
replaced = true; |
|||
this._txs_by_external_index[c][cc] = clonedTx; |
|||
} |
|||
} |
|||
if (!replaced) this._txs_by_external_index[c].push(clonedTx); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
for (let tx of Object.values(txdatas)) { |
|||
for (let vin of tx.vin) { |
|||
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { |
|||
// this TX is related to our address
|
|||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; |
|||
let clonedTx = Object.assign({}, tx); |
|||
clonedTx.inputs = tx.vin.slice(0); |
|||
clonedTx.outputs = tx.vout.slice(0); |
|||
delete clonedTx.vin; |
|||
delete clonedTx.vout; |
|||
|
|||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|||
let replaced = false; |
|||
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { |
|||
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { |
|||
replaced = true; |
|||
this._txs_by_internal_index[c][cc] = clonedTx; |
|||
} |
|||
} |
|||
if (!replaced) this._txs_by_internal_index[c].push(clonedTx); |
|||
} |
|||
} |
|||
for (let vout of tx.vout) { |
|||
if (vout.scriptPubKey.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) { |
|||
// this TX is related to our address
|
|||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; |
|||
let clonedTx = Object.assign({}, tx); |
|||
clonedTx.inputs = tx.vin.slice(0); |
|||
clonedTx.outputs = tx.vout.slice(0); |
|||
delete clonedTx.vin; |
|||
delete clonedTx.vout; |
|||
|
|||
// trying to replace tx if it exists already (because it has lower confirmations, for example)
|
|||
let replaced = false; |
|||
for (let cc = 0; cc < this._txs_by_internal_index[c].length; cc++) { |
|||
if (this._txs_by_internal_index[c][cc].txid === clonedTx.txid) { |
|||
replaced = true; |
|||
this._txs_by_internal_index[c][cc] = clonedTx; |
|||
} |
|||
} |
|||
if (!replaced) this._txs_by_internal_index[c].push(clonedTx); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
this._lastTxFetch = +new Date(); |
|||
} |
|||
|
|||
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.received = tx.blocktime * 1000; |
|||
if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed
|
|||
tx.confirmations = tx.confirmations || 0; // unconfirmed
|
|||
tx.hash = tx.txid; |
|||
tx.value = 0; |
|||
|
|||
for (let vin of tx.inputs) { |
|||
// if input (spending) goes from our address - we are loosing!
|
|||
if ((vin.address && this.weOwnAddress(vin.address)) || (vin.addresses && vin.addresses[0] && this.weOwnAddress(vin.addresses[0]))) { |
|||
tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber(); |
|||
} |
|||
} |
|||
|
|||
for (let vout of tx.outputs) { |
|||
// when output goes to our address - this means we are gaining!
|
|||
if (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); |
|||
} |
|||
|
|||
// now, deduplication:
|
|||
let usedTxIds = {}; |
|||
let ret2 = []; |
|||
for (let tx of ret) { |
|||
if (!usedTxIds[tx.txid]) ret2.push(tx); |
|||
usedTxIds[tx.txid] = 1; |
|||
} |
|||
|
|||
return ret2.sort(function(a, b) { |
|||
return b.received - a.received; |
|||
}); |
|||
} |
|||
|
|||
async _binarySearchIterationForInternalAddress(index) { |
|||
const gerenateChunkAddresses = chunkNum => { |
|||
let ret = []; |
|||
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { |
|||
ret.push(this._getInternalAddressByIndex(c)); |
|||
} |
|||
return ret; |
|||
}; |
|||
|
|||
let lastChunkWithUsedAddressesNum = null; |
|||
let lastHistoriesWithUsedAddresses = null; |
|||
for (let c = 0; c < Math.round(index / this.gap_limit); c++) { |
|||
let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); |
|||
if (this.constructor._getTransactionsFromHistories(histories).length > 0) { |
|||
// in this particular chunk we have used addresses
|
|||
lastChunkWithUsedAddressesNum = c; |
|||
lastHistoriesWithUsedAddresses = histories; |
|||
} else { |
|||
// empty chunk. no sense searching more chunks
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
let lastUsedIndex = 0; |
|||
|
|||
if (lastHistoriesWithUsedAddresses) { |
|||
// now searching for last used address in batch lastChunkWithUsedAddressesNum
|
|||
for ( |
|||
let c = lastChunkWithUsedAddressesNum * this.gap_limit; |
|||
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; |
|||
c++ |
|||
) { |
|||
let address = this._getInternalAddressByIndex(c); |
|||
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { |
|||
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
|
|||
} |
|||
} |
|||
} |
|||
|
|||
return lastUsedIndex; |
|||
} |
|||
|
|||
async _binarySearchIterationForExternalAddress(index) { |
|||
const gerenateChunkAddresses = chunkNum => { |
|||
let ret = []; |
|||
for (let c = this.gap_limit * chunkNum; c < this.gap_limit * (chunkNum + 1); c++) { |
|||
ret.push(this._getExternalAddressByIndex(c)); |
|||
} |
|||
return ret; |
|||
}; |
|||
|
|||
let lastChunkWithUsedAddressesNum = null; |
|||
let lastHistoriesWithUsedAddresses = null; |
|||
for (let c = 0; c < Math.round(index / this.gap_limit); c++) { |
|||
let histories = await BlueElectrum.multiGetHistoryByAddress(gerenateChunkAddresses(c)); |
|||
if (this.constructor._getTransactionsFromHistories(histories).length > 0) { |
|||
// in this particular chunk we have used addresses
|
|||
lastChunkWithUsedAddressesNum = c; |
|||
lastHistoriesWithUsedAddresses = histories; |
|||
} else { |
|||
// empty chunk. no sense searching more chunks
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
let lastUsedIndex = 0; |
|||
|
|||
if (lastHistoriesWithUsedAddresses) { |
|||
// now searching for last used address in batch lastChunkWithUsedAddressesNum
|
|||
for ( |
|||
let c = lastChunkWithUsedAddressesNum * this.gap_limit; |
|||
c < lastChunkWithUsedAddressesNum * this.gap_limit + this.gap_limit; |
|||
c++ |
|||
) { |
|||
let address = this._getExternalAddressByIndex(c); |
|||
if (lastHistoriesWithUsedAddresses[address] && lastHistoriesWithUsedAddresses[address].length > 0) { |
|||
lastUsedIndex = Math.max(c, lastUsedIndex) + 1; // point to next, which is supposed to be unsued
|
|||
} |
|||
} |
|||
} |
|||
|
|||
return lastUsedIndex; |
|||
} |
|||
|
|||
async fetchBalance() { |
|||
try { |
|||
if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) { |
|||
// doing binary search for last used address:
|
|||
this.next_free_change_address_index = await this._binarySearchIterationForInternalAddress(1000); |
|||
this.next_free_address_index = await this._binarySearchIterationForExternalAddress(1000); |
|||
} // end rescanning fresh wallet
|
|||
|
|||
// finally fetching balance
|
|||
await this._fetchBalance(); |
|||
} catch (err) { |
|||
console.warn(err); |
|||
} |
|||
} |
|||
|
|||
async _fetchBalance() { |
|||
// probing future addressess in hierarchy whether they have any transactions, in case
|
|||
// our 'next free addr' pointers are lagging behind
|
|||
let tryAgain = false; |
|||
let txs = await BlueElectrum.getTransactionsByAddress( |
|||
this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1), |
|||
); |
|||
if (txs.length > 0) { |
|||
// whoa, someone uses our wallet outside! better catch up
|
|||
this.next_free_address_index += this.gap_limit; |
|||
tryAgain = true; |
|||
} |
|||
|
|||
txs = await BlueElectrum.getTransactionsByAddress( |
|||
this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1), |
|||
); |
|||
if (txs.length > 0) { |
|||
this.next_free_change_address_index += this.gap_limit; |
|||
tryAgain = true; |
|||
} |
|||
|
|||
// FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ]
|
|||
|
|||
if (tryAgain) return this._fetchBalance(); |
|||
|
|||
// next, business as usuall. fetch balances
|
|||
|
|||
let addresses2fetch = []; |
|||
|
|||
// generating all involved addresses.
|
|||
// basically, refetch all from index zero to maximum. doesnt matter
|
|||
// since we batch them 100 per call
|
|||
|
|||
// external
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
addresses2fetch.push(this._getExternalAddressByIndex(c)); |
|||
} |
|||
|
|||
// internal
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
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]) { |
|||
// first, if balances differ from what we store - we delete transactions for that
|
|||
// address so next fetchTransactions() will refetch everything
|
|||
if (this._balances_by_external_index[c]) { |
|||
if ( |
|||
this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed || |
|||
this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed |
|||
) { |
|||
delete this._txs_by_external_index[c]; |
|||
} |
|||
} |
|||
// update local representation of balances on that address:
|
|||
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]) { |
|||
// first, if balances differ from what we store - we delete transactions for that
|
|||
// address so next fetchTransactions() will refetch everything
|
|||
if (this._balances_by_internal_index[c]) { |
|||
if ( |
|||
this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed || |
|||
this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed |
|||
) { |
|||
delete this._txs_by_internal_index[c]; |
|||
} |
|||
} |
|||
// update local representation of balances on that address:
|
|||
this._balances_by_internal_index[c] = { |
|||
c: balances.addresses[addr].confirmed, |
|||
u: balances.addresses[addr].unconfirmed, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
this._lastBalanceFetch = +new Date(); |
|||
} |
|||
|
|||
async fetchUtxo() { |
|||
// considering only confirmed balance
|
|||
let addressess = []; |
|||
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) { |
|||
addressess.push(this._getExternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) { |
|||
addressess.push(this._getInternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
this._utxo = []; |
|||
for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) { |
|||
this._utxo = this._utxo.concat(arr); |
|||
} |
|||
} |
|||
|
|||
getUtxo() { |
|||
return this._utxo; |
|||
} |
|||
|
|||
_getDerivationPathByAddress(address) { |
|||
const path = "m/84'/0'/0'"; |
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c; |
|||
} |
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
_getPubkeyByAddress(address) { |
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
if (this._getExternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(0, c); |
|||
} |
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
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 + this.gap_limit; c++) { |
|||
if (this._getInternalAddressByIndex(c) === address) return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* @deprecated |
|||
*/ |
|||
createTx(utxos, amount, fee, address) { |
|||
throw new Error('Deprecated'); |
|||
} |
|||
|
|||
/** |
|||
* |
|||
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos |
|||
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate) |
|||
* @param feeRate {Number} satoshi per byte |
|||
* @param changeAddress {String} Excessive coins will go back to that address |
|||
* @param sequence {Number} Used in RBF |
|||
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case |
|||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}} |
|||
*/ |
|||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false) { |
|||
if (!changeAddress) throw new Error('No change address provided'); |
|||
sequence = sequence || HDSegwitBech32Wallet.defaultRBFSequence; |
|||
|
|||
let algo = coinSelectAccumulative; |
|||
if (targets.length === 1 && targets[0] && !targets[0].value) { |
|||
// we want to send MAX
|
|||
algo = coinSelectSplit; |
|||
} |
|||
|
|||
let { inputs, outputs, fee } = algo(utxos, targets, feeRate); |
|||
|
|||
// .inputs and .outputs will be undefined if no solution was found
|
|||
if (!inputs || !outputs) { |
|||
throw new Error('Not enough balance. Try sending smaller amount'); |
|||
} |
|||
|
|||
let psbt = new bitcoin.Psbt(); |
|||
|
|||
let c = 0; |
|||
let keypairs = {}; |
|||
let values = {}; |
|||
|
|||
inputs.forEach(input => { |
|||
let keyPair; |
|||
if (!skipSigning) { |
|||
// skiping signing related stuff
|
|||
keyPair = bitcoin.ECPair.fromWIF(this._getWifForAddress(input.address)); |
|||
keypairs[c] = keyPair; |
|||
} |
|||
values[c] = input.value; |
|||
c++; |
|||
if (!skipSigning) { |
|||
// skiping signing related stuff
|
|||
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input'); |
|||
} |
|||
let pubkey = this._getPubkeyByAddress(input.address); |
|||
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); |
|||
// this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting
|
|||
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
|||
let path = this._getDerivationPathByAddress(input.address); |
|||
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); |
|||
psbt.addInput({ |
|||
hash: input.txId, |
|||
index: input.vout, |
|||
sequence, |
|||
bip32Derivation: [ |
|||
{ |
|||
masterFingerprint, |
|||
path, |
|||
pubkey, |
|||
}, |
|||
], |
|||
witnessUtxo: { |
|||
script: p2wpkh.output, |
|||
value: input.value, |
|||
}, |
|||
}); |
|||
}); |
|||
|
|||
outputs.forEach(output => { |
|||
// if output has no address - this is change output
|
|||
let change = false; |
|||
if (!output.address) { |
|||
change = true; |
|||
output.address = changeAddress; |
|||
} |
|||
|
|||
let path = this._getDerivationPathByAddress(output.address); |
|||
let pubkey = this._getPubkeyByAddress(output.address); |
|||
let masterFingerprint = Buffer.from([0x00, 0x00, 0x00, 0x00]); |
|||
// this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting
|
|||
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
|
|||
|
|||
let outputData = { |
|||
address: output.address, |
|||
value: output.value, |
|||
}; |
|||
|
|||
if (change) { |
|||
outputData['bip32Derivation'] = [ |
|||
{ |
|||
masterFingerprint, |
|||
path, |
|||
pubkey, |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
psbt.addOutput(outputData); |
|||
}); |
|||
|
|||
if (!skipSigning) { |
|||
// skiping signing related stuff
|
|||
for (let cc = 0; cc < c; cc++) { |
|||
psbt.signInput(cc, keypairs[cc]); |
|||
} |
|||
} |
|||
|
|||
let tx; |
|||
if (!skipSigning) { |
|||
tx = psbt.finalizeAllInputs().extractTransaction(); |
|||
} |
|||
return { tx, inputs, outputs, fee, psbt }; |
|||
} |
|||
|
|||
/** |
|||
* Combines 2 PSBTs into final transaction from which you can |
|||
* get HEX and broadcast |
|||
* |
|||
* @param base64one {string} |
|||
* @param base64two {string} |
|||
* @returns {Transaction} |
|||
*/ |
|||
combinePsbt(base64one, base64two) { |
|||
const final1 = bitcoin.Psbt.fromBase64(base64one); |
|||
const final2 = bitcoin.Psbt.fromBase64(base64two); |
|||
final1.combine(final2); |
|||
return final1.finalizeAllInputs().extractTransaction(); |
|||
} |
|||
|
|||
/** |
|||
* Creates Segwit Bech32 Bitcoin address |
|||
* |
|||
* @param hdNode |
|||
* @returns {String} |
|||
*/ |
|||
static _nodeToBech32SegwitAddress(hdNode) { |
|||
return bitcoin.payments.p2wpkh({ |
|||
pubkey: hdNode.publicKey, |
|||
}).address; |
|||
} |
|||
|
|||
/** |
|||
* Converts zpub to xpub |
|||
* |
|||
* @param {String} zpub |
|||
* @returns {String} xpub |
|||
*/ |
|||
static _zpubToXpub(zpub) { |
|||
let data = b58.decode(zpub); |
|||
data = data.slice(4); |
|||
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); |
|||
|
|||
return b58.encode(data); |
|||
} |
|||
|
|||
static _getTransactionsFromHistories(histories) { |
|||
let txs = []; |
|||
for (let history of Object.values(histories)) { |
|||
for (let tx of history) { |
|||
txs.push(tx); |
|||
} |
|||
} |
|||
return txs; |
|||
} |
|||
|
|||
/** |
|||
* Broadcast txhex. Can throw an exception if failed |
|||
* |
|||
* @param {String} txhex |
|||
* @returns {Promise<boolean>} |
|||
*/ |
|||
async broadcastTx(txhex) { |
|||
let broadcast = await BlueElectrum.broadcastV2(txhex); |
|||
console.log({ broadcast }); |
|||
if (broadcast.indexOf('successfully') !== -1) return true; |
|||
return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok
|
|||
} |
|||
} |
|||
|
@ -0,0 +1,31 @@ |
|||
import { AbstractWallet } from './abstract-wallet'; |
|||
|
|||
export class PlaceholderWallet extends AbstractWallet { |
|||
static type = 'placeholder'; |
|||
static typeReadable = 'Placeholder'; |
|||
|
|||
constructor() { |
|||
super(); |
|||
this._isFailure = false; |
|||
} |
|||
|
|||
allowSend() { |
|||
return false; |
|||
} |
|||
|
|||
getLabel() { |
|||
return this.getIsFailure() ? 'Wallet Import' : 'Importing Wallet...'; |
|||
} |
|||
|
|||
allowReceive() { |
|||
return false; |
|||
} |
|||
|
|||
getIsFailure() { |
|||
return this._isFailure; |
|||
} |
|||
|
|||
setIsFailure(value) { |
|||
this._isFailure = value; |
|||
} |
|||
} |
@ -0,0 +1,229 @@ |
|||
/* global alert */ |
|||
import { |
|||
SegwitP2SHWallet, |
|||
LegacyWallet, |
|||
WatchOnlyWallet, |
|||
HDLegacyBreadwalletWallet, |
|||
HDSegwitP2SHWallet, |
|||
HDLegacyP2PKHWallet, |
|||
HDSegwitBech32Wallet, |
|||
LightningCustodianWallet, |
|||
PlaceholderWallet, |
|||
} from '../class'; |
|||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; |
|||
const EV = require('../events'); |
|||
const A = require('../analytics'); |
|||
/** @type {AppStorage} */ |
|||
const BlueApp = require('../BlueApp'); |
|||
const loc = require('../loc'); |
|||
|
|||
export default class WalletImport { |
|||
static async _saveWallet(w) { |
|||
try { |
|||
const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type); |
|||
if (wallet) { |
|||
alert('This wallet has been previously imported.'); |
|||
WalletImport.removePlaceholderWallet(); |
|||
} else { |
|||
alert(loc.wallets.import.success); |
|||
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); |
|||
w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable); |
|||
w.setUserHasSavedExport(true); |
|||
WalletImport.removePlaceholderWallet(); |
|||
BlueApp.wallets.push(w); |
|||
await BlueApp.saveToDisk(); |
|||
A(A.ENUM.CREATED_WALLET); |
|||
} |
|||
EV(EV.enum.WALLETS_COUNT_CHANGED); |
|||
} catch (_e) {} |
|||
} |
|||
|
|||
static removePlaceholderWallet() { |
|||
const placeholderWalletIndex = BlueApp.wallets.findIndex(wallet => wallet.type === PlaceholderWallet.type); |
|||
if (placeholderWalletIndex > -1) { |
|||
BlueApp.wallets.splice(placeholderWalletIndex, 1); |
|||
} |
|||
} |
|||
|
|||
static addPlaceholderWallet(importText, isFailure = false) { |
|||
const wallet = new PlaceholderWallet(); |
|||
wallet.setSecret(importText); |
|||
wallet.setIsFailure(isFailure); |
|||
BlueApp.wallets.push(wallet); |
|||
EV(EV.enum.WALLETS_COUNT_CHANGED); |
|||
return wallet; |
|||
} |
|||
|
|||
static isCurrentlyImportingWallet() { |
|||
return BlueApp.getWallets().some(wallet => wallet.type === PlaceholderWallet.type); |
|||
} |
|||
|
|||
static async processImportText(importText) { |
|||
if (WalletImport.isCurrentlyImportingWallet()) { |
|||
return; |
|||
} |
|||
const placeholderWallet = WalletImport.addPlaceholderWallet(importText); |
|||
// Plan:
|
|||
// 0. check if its HDSegwitBech32Wallet (BIP84)
|
|||
// 1. check if its HDSegwitP2SHWallet (BIP49)
|
|||
// 2. check if its HDLegacyP2PKHWallet (BIP44)
|
|||
// 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0")
|
|||
// 4. check if its Segwit WIF (P2SH)
|
|||
// 5. check if its Legacy WIF
|
|||
// 6. check if its address (watch-only wallet)
|
|||
// 7. check if its private key (segwit address P2SH) TODO
|
|||
// 7. check if its private key (legacy address) TODO
|
|||
|
|||
try { |
|||
// is it lightning custodian?
|
|||
if (importText.indexOf('blitzhub://') !== -1 || importText.indexOf('lndhub://') !== -1) { |
|||
let lnd = new LightningCustodianWallet(); |
|||
if (importText.includes('@')) { |
|||
const split = importText.split('@'); |
|||
lnd.setBaseURI(split[1]); |
|||
lnd.setSecret(split[0]); |
|||
} else { |
|||
lnd.setBaseURI(LightningCustodianWallet.defaultBaseUri); |
|||
lnd.setSecret(importText); |
|||
} |
|||
lnd.init(); |
|||
await lnd.authorize(); |
|||
await lnd.fetchTransactions(); |
|||
await lnd.fetchUserInvoices(); |
|||
await lnd.fetchPendingTransactions(); |
|||
await lnd.fetchBalance(); |
|||
return WalletImport._saveWallet(lnd); |
|||
} |
|||
|
|||
// trying other wallet types
|
|||
|
|||
let hd4 = new HDSegwitBech32Wallet(); |
|||
hd4.setSecret(importText); |
|||
if (hd4.validateMnemonic()) { |
|||
await hd4.fetchBalance(); |
|||
if (hd4.getBalance() > 0) { |
|||
await hd4.fetchTransactions(); |
|||
return WalletImport._saveWallet(hd4); |
|||
} |
|||
} |
|||
|
|||
let segwitWallet = new SegwitP2SHWallet(); |
|||
segwitWallet.setSecret(importText); |
|||
if (segwitWallet.getAddress()) { |
|||
// ok its a valid WIF
|
|||
|
|||
let legacyWallet = new LegacyWallet(); |
|||
legacyWallet.setSecret(importText); |
|||
|
|||
await legacyWallet.fetchBalance(); |
|||
if (legacyWallet.getBalance() > 0) { |
|||
// yep, its legacy we're importing
|
|||
await legacyWallet.fetchTransactions(); |
|||
return WalletImport._saveWallet(legacyWallet); |
|||
} else { |
|||
// by default, we import wif as Segwit P2SH
|
|||
await segwitWallet.fetchBalance(); |
|||
await segwitWallet.fetchTransactions(); |
|||
return WalletImport._saveWallet(segwitWallet); |
|||
} |
|||
} |
|||
|
|||
// case - WIF is valid, just has uncompressed pubkey
|
|||
|
|||
let legacyWallet = new LegacyWallet(); |
|||
legacyWallet.setSecret(importText); |
|||
if (legacyWallet.getAddress()) { |
|||
await legacyWallet.fetchBalance(); |
|||
await legacyWallet.fetchTransactions(); |
|||
return WalletImport._saveWallet(legacyWallet); |
|||
} |
|||
|
|||
// if we're here - nope, its not a valid WIF
|
|||
|
|||
let hd1 = new HDLegacyBreadwalletWallet(); |
|||
hd1.setSecret(importText); |
|||
if (hd1.validateMnemonic()) { |
|||
await hd1.fetchBalance(); |
|||
if (hd1.getBalance() > 0) { |
|||
await hd1.fetchTransactions(); |
|||
return WalletImport._saveWallet(hd1); |
|||
} |
|||
} |
|||
|
|||
let hd2 = new HDSegwitP2SHWallet(); |
|||
hd2.setSecret(importText); |
|||
if (hd2.validateMnemonic()) { |
|||
await hd2.fetchBalance(); |
|||
if (hd2.getBalance() > 0) { |
|||
await hd2.fetchTransactions(); |
|||
return WalletImport._saveWallet(hd2); |
|||
} |
|||
} |
|||
|
|||
let hd3 = new HDLegacyP2PKHWallet(); |
|||
hd3.setSecret(importText); |
|||
if (hd3.validateMnemonic()) { |
|||
await hd3.fetchBalance(); |
|||
if (hd3.getBalance() > 0) { |
|||
await hd3.fetchTransactions(); |
|||
return WalletImport._saveWallet(hd3); |
|||
} |
|||
} |
|||
|
|||
// no balances? how about transactions count?
|
|||
|
|||
if (hd1.validateMnemonic()) { |
|||
await hd1.fetchTransactions(); |
|||
if (hd1.getTransactions().length !== 0) { |
|||
return WalletImport._saveWallet(hd1); |
|||
} |
|||
} |
|||
if (hd2.validateMnemonic()) { |
|||
await hd2.fetchTransactions(); |
|||
if (hd2.getTransactions().length !== 0) { |
|||
return WalletImport._saveWallet(hd2); |
|||
} |
|||
} |
|||
if (hd3.validateMnemonic()) { |
|||
await hd3.fetchTransactions(); |
|||
if (hd3.getTransactions().length !== 0) { |
|||
return WalletImport._saveWallet(hd3); |
|||
} |
|||
} |
|||
if (hd4.validateMnemonic()) { |
|||
await hd4.fetchTransactions(); |
|||
if (hd4.getTransactions().length !== 0) { |
|||
return WalletImport._saveWallet(hd4); |
|||
} |
|||
} |
|||
|
|||
// is it even valid? if yes we will import as:
|
|||
if (hd4.validateMnemonic()) { |
|||
return WalletImport._saveWallet(hd4); |
|||
} |
|||
|
|||
// not valid? maybe its a watch-only address?
|
|||
|
|||
let watchOnly = new WatchOnlyWallet(); |
|||
watchOnly.setSecret(importText); |
|||
if (watchOnly.valid()) { |
|||
await watchOnly.fetchTransactions(); |
|||
await watchOnly.fetchBalance(); |
|||
return WalletImport._saveWallet(watchOnly); |
|||
} |
|||
|
|||
// nope?
|
|||
|
|||
// TODO: try a raw private key
|
|||
} catch (Err) { |
|||
WalletImport.removePlaceholderWallet(placeholderWallet); |
|||
EV(EV.enum.WALLETS_COUNT_CHANGED); |
|||
console.warn(Err); |
|||
} |
|||
WalletImport.removePlaceholderWallet(); |
|||
WalletImport.addPlaceholderWallet(importText, true); |
|||
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); |
|||
EV(EV.enum.WALLETS_COUNT_CHANGED); |
|||
alert(loc.wallets.import.error); |
|||
} |
|||
} |
@ -1,325 +1,125 @@ |
|||
/* global alert */ |
|||
import { |
|||
SegwitP2SHWallet, |
|||
LegacyWallet, |
|||
WatchOnlyWallet, |
|||
HDLegacyBreadwalletWallet, |
|||
HDSegwitP2SHWallet, |
|||
HDLegacyP2PKHWallet, |
|||
HDSegwitBech32Wallet, |
|||
} from '../../class'; |
|||
import React, { Component } from 'react'; |
|||
import React, { useEffect, useState } from 'react'; |
|||
import { KeyboardAvoidingView, Platform, Dimensions, View, TouchableWithoutFeedback, Keyboard } from 'react-native'; |
|||
import { |
|||
BlueFormMultiInput, |
|||
BlueButtonLink, |
|||
BlueFormLabel, |
|||
BlueLoading, |
|||
BlueDoneAndDismissKeyboardInputAccessory, |
|||
BlueButton, |
|||
SafeBlueArea, |
|||
BlueSpacing20, |
|||
BlueNavigationStyle, |
|||
} from '../../BlueComponents'; |
|||
import PropTypes from 'prop-types'; |
|||
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; |
|||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; |
|||
import Privacy from '../../Privacy'; |
|||
let EV = require('../../events'); |
|||
let A = require('../../analytics'); |
|||
/** @type {AppStorage} */ |
|||
let BlueApp = require('../../BlueApp'); |
|||
import { useNavigation, useNavigationParam } from 'react-navigation-hooks'; |
|||
import WalletImport from '../../class/walletImport'; |
|||
let loc = require('../../loc'); |
|||
const { width } = Dimensions.get('window'); |
|||
|
|||
export default class WalletsImport extends Component { |
|||
static navigationOptions = { |
|||
...BlueNavigationStyle(), |
|||
title: loc.wallets.import.title, |
|||
}; |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
isLoading: true, |
|||
isToolbarVisibleForAndroid: false, |
|||
}; |
|||
} |
|||
const WalletsImport = () => { |
|||
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false); |
|||
const [importText, setImportText] = useState(useNavigationParam('label') || ''); |
|||
const { navigate, dismiss } = useNavigation(); |
|||
|
|||
componentDidMount() { |
|||
this.setState({ |
|||
isLoading: false, |
|||
label: '', |
|||
}); |
|||
useEffect(() => { |
|||
Privacy.enableBlur(); |
|||
} |
|||
return () => Privacy.disableBlur(); |
|||
}); |
|||
|
|||
componentWillUnmount() { |
|||
Privacy.disableBlur(); |
|||
} |
|||
|
|||
async _saveWallet(w) { |
|||
if (BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret)) { |
|||
alert('This wallet has been previously imported.'); |
|||
} else { |
|||
alert(loc.wallets.import.success); |
|||
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); |
|||
w.setLabel(loc.wallets.import.imported + ' ' + w.typeReadable); |
|||
BlueApp.wallets.push(w); |
|||
await BlueApp.saveToDisk(); |
|||
EV(EV.enum.WALLETS_COUNT_CHANGED); |
|||
A(A.ENUM.CREATED_WALLET); |
|||
this.props.navigation.dismiss(); |
|||
const importButtonPressed = () => { |
|||
if (importText.trim().length === 0) { |
|||
return; |
|||
} |
|||
} |
|||
importMnemonic(importText); |
|||
}; |
|||
|
|||
async importMnemonic(text) { |
|||
const importMnemonic = importText => { |
|||
try { |
|||
// is it lightning custodian?
|
|||
if (text.indexOf('blitzhub://') !== -1 || text.indexOf('lndhub://') !== -1) { |
|||
let lnd = new LightningCustodianWallet(); |
|||
if (text.includes('@')) { |
|||
const split = text.split('@'); |
|||
lnd.setBaseURI(split[1]); |
|||
lnd.setSecret(split[0]); |
|||
} else { |
|||
lnd.setBaseURI(LightningCustodianWallet.defaultBaseUri); |
|||
lnd.setSecret(text); |
|||
} |
|||
lnd.init(); |
|||
await lnd.authorize(); |
|||
await lnd.fetchTransactions(); |
|||
await lnd.fetchUserInvoices(); |
|||
await lnd.fetchPendingTransactions(); |
|||
await lnd.fetchBalance(); |
|||
return this._saveWallet(lnd); |
|||
} |
|||
|
|||
// trying other wallet types
|
|||
|
|||
let hd4 = new HDSegwitBech32Wallet(); |
|||
hd4.setSecret(text); |
|||
if (hd4.validateMnemonic()) { |
|||
await hd4.fetchBalance(); |
|||
if (hd4.getBalance() > 0) { |
|||
await hd4.fetchTransactions(); |
|||
return this._saveWallet(hd4); |
|||
} |
|||
} |
|||
|
|||
let segwitWallet = new SegwitP2SHWallet(); |
|||
segwitWallet.setSecret(text); |
|||
if (segwitWallet.getAddress()) { |
|||
// ok its a valid WIF
|
|||
|
|||
let legacyWallet = new LegacyWallet(); |
|||
legacyWallet.setSecret(text); |
|||
|
|||
await legacyWallet.fetchBalance(); |
|||
if (legacyWallet.getBalance() > 0) { |
|||
// yep, its legacy we're importing
|
|||
await legacyWallet.fetchTransactions(); |
|||
return this._saveWallet(legacyWallet); |
|||
} else { |
|||
// by default, we import wif as Segwit P2SH
|
|||
await segwitWallet.fetchBalance(); |
|||
await segwitWallet.fetchTransactions(); |
|||
return this._saveWallet(segwitWallet); |
|||
} |
|||
} |
|||
|
|||
// case - WIF is valid, just has uncompressed pubkey
|
|||
|
|||
let legacyWallet = new LegacyWallet(); |
|||
legacyWallet.setSecret(text); |
|||
if (legacyWallet.getAddress()) { |
|||
await legacyWallet.fetchBalance(); |
|||
await legacyWallet.fetchTransactions(); |
|||
return this._saveWallet(legacyWallet); |
|||
} |
|||
|
|||
// if we're here - nope, its not a valid WIF
|
|||
|
|||
let hd1 = new HDLegacyBreadwalletWallet(); |
|||
hd1.setSecret(text); |
|||
if (hd1.validateMnemonic()) { |
|||
await hd1.fetchBalance(); |
|||
if (hd1.getBalance() > 0) { |
|||
await hd1.fetchTransactions(); |
|||
return this._saveWallet(hd1); |
|||
} |
|||
} |
|||
|
|||
let hd2 = new HDSegwitP2SHWallet(); |
|||
hd2.setSecret(text); |
|||
if (hd2.validateMnemonic()) { |
|||
await hd2.fetchBalance(); |
|||
if (hd2.getBalance() > 0) { |
|||
await hd2.fetchTransactions(); |
|||
return this._saveWallet(hd2); |
|||
} |
|||
} |
|||
|
|||
let hd3 = new HDLegacyP2PKHWallet(); |
|||
hd3.setSecret(text); |
|||
if (hd3.validateMnemonic()) { |
|||
await hd3.fetchBalance(); |
|||
if (hd3.getBalance() > 0) { |
|||
await hd3.fetchTransactions(); |
|||
return this._saveWallet(hd3); |
|||
} |
|||
} |
|||
|
|||
// no balances? how about transactions count?
|
|||
|
|||
if (hd1.validateMnemonic()) { |
|||
await hd1.fetchTransactions(); |
|||
if (hd1.getTransactions().length !== 0) { |
|||
return this._saveWallet(hd1); |
|||
} |
|||
} |
|||
if (hd2.validateMnemonic()) { |
|||
await hd2.fetchTransactions(); |
|||
if (hd2.getTransactions().length !== 0) { |
|||
return this._saveWallet(hd2); |
|||
} |
|||
} |
|||
if (hd3.validateMnemonic()) { |
|||
await hd3.fetchTransactions(); |
|||
if (hd3.getTransactions().length !== 0) { |
|||
return this._saveWallet(hd3); |
|||
} |
|||
} |
|||
if (hd4.validateMnemonic()) { |
|||
await hd4.fetchTransactions(); |
|||
if (hd4.getTransactions().length !== 0) { |
|||
return this._saveWallet(hd4); |
|||
} |
|||
} |
|||
|
|||
// is it even valid? if yes we will import as:
|
|||
if (hd4.validateMnemonic()) { |
|||
return this._saveWallet(hd4); |
|||
} |
|||
|
|||
// not valid? maybe its a watch-only address?
|
|||
|
|||
let watchOnly = new WatchOnlyWallet(); |
|||
watchOnly.setSecret(text); |
|||
if (watchOnly.valid()) { |
|||
await watchOnly.fetchTransactions(); |
|||
await watchOnly.fetchBalance(); |
|||
return this._saveWallet(watchOnly); |
|||
} |
|||
|
|||
// nope?
|
|||
|
|||
// TODO: try a raw private key
|
|||
} catch (Err) { |
|||
console.warn(Err); |
|||
} |
|||
|
|||
alert(loc.wallets.import.error); |
|||
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); |
|||
// Plan:
|
|||
// 0. check if its HDSegwitBech32Wallet (BIP84)
|
|||
// 1. check if its HDSegwitP2SHWallet (BIP49)
|
|||
// 2. check if its HDLegacyP2PKHWallet (BIP44)
|
|||
// 3. check if its HDLegacyBreadwalletWallet (no BIP, just "m/0")
|
|||
// 4. check if its Segwit WIF (P2SH)
|
|||
// 5. check if its Legacy WIF
|
|||
// 6. check if its address (watch-only wallet)
|
|||
// 7. check if its private key (segwit address P2SH) TODO
|
|||
// 7. check if its private key (legacy address) TODO
|
|||
} |
|||
|
|||
setLabel(text) { |
|||
this.setState({ |
|||
label: text, |
|||
}); /* also, a hack to make screen update new typed text */ |
|||
} |
|||
|
|||
render() { |
|||
if (this.state.isLoading) { |
|||
return ( |
|||
<View style={{ flex: 1, paddingTop: 20 }}> |
|||
<BlueLoading /> |
|||
</View> |
|||
); |
|||
WalletImport.processImportText(importText); |
|||
dismiss(); |
|||
} catch (error) { |
|||
alert(loc.wallets.import.error); |
|||
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}> |
|||
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}> |
|||
<KeyboardAvoidingView behavior="position" enabled> |
|||
<BlueFormLabel>{loc.wallets.import.explanation}</BlueFormLabel> |
|||
<BlueSpacing20 /> |
|||
<BlueFormMultiInput |
|||
value={this.state.label} |
|||
placeholder="" |
|||
contextMenuHidden |
|||
onChangeText={text => { |
|||
this.setLabel(text); |
|||
}} |
|||
inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID} |
|||
onFocus={() => this.setState({ isToolbarVisibleForAndroid: true })} |
|||
onBlur={() => this.setState({ isToolbarVisibleForAndroid: false })} |
|||
/> |
|||
{Platform.select({ |
|||
ios: ( |
|||
<BlueDoneAndDismissKeyboardInputAccessory |
|||
onClearTapped={() => this.setState({ label: '' }, () => Keyboard.dismiss())} |
|||
onPasteTapped={text => this.setState({ label: text }, () => Keyboard.dismiss())} |
|||
/> |
|||
), |
|||
android: this.state.isToolbarVisibleForAndroid && ( |
|||
<BlueDoneAndDismissKeyboardInputAccessory |
|||
onClearTapped={() => this.setState({ label: '' }, () => Keyboard.dismiss())} |
|||
onPasteTapped={text => this.setState({ label: text }, () => Keyboard.dismiss())} |
|||
/> |
|||
), |
|||
})} |
|||
</KeyboardAvoidingView> |
|||
</TouchableWithoutFeedback> |
|||
const onBarScanned = value => { |
|||
setImportText(value); |
|||
importMnemonic(value); |
|||
}; |
|||
|
|||
<BlueSpacing20 /> |
|||
<View |
|||
style={{ |
|||
alignItems: 'center', |
|||
}} |
|||
> |
|||
<BlueButton |
|||
disabled={!this.state.label} |
|||
title={loc.wallets.import.do_import} |
|||
buttonStyle={{ |
|||
width: width / 1.5, |
|||
}} |
|||
onPress={async () => { |
|||
if (!this.state.label) { |
|||
return; |
|||
} |
|||
this.setState({ isLoading: true }, async () => { |
|||
await this.importMnemonic(this.state.label.trim()); |
|||
this.setState({ isLoading: false }); |
|||
}); |
|||
}} |
|||
return ( |
|||
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}> |
|||
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}> |
|||
<KeyboardAvoidingView behavior="position" enabled> |
|||
<BlueFormLabel>{loc.wallets.import.explanation}</BlueFormLabel> |
|||
<BlueSpacing20 /> |
|||
<BlueFormMultiInput |
|||
value={importText} |
|||
contextMenuHidden |
|||
onChangeText={setImportText} |
|||
inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID} |
|||
onFocus={() => setIsToolbarVisibleForAndroid(true)} |
|||
onBlur={() => setIsToolbarVisibleForAndroid(false)} |
|||
/> |
|||
<BlueButtonLink |
|||
title={loc.wallets.import.scan_qr} |
|||
onPress={() => { |
|||
this.props.navigation.navigate('ScanQrWif'); |
|||
}} |
|||
/> |
|||
</View> |
|||
</SafeBlueArea> |
|||
); |
|||
} |
|||
} |
|||
{Platform.select({ |
|||
ios: ( |
|||
<BlueDoneAndDismissKeyboardInputAccessory |
|||
onClearTapped={() => { |
|||
setImportText(''); |
|||
Keyboard.dismiss(); |
|||
}} |
|||
onPasteTapped={text => { |
|||
setImportText(text); |
|||
Keyboard.dismiss(); |
|||
}} |
|||
/> |
|||
), |
|||
android: isToolbarVisibleForAndroid && ( |
|||
<BlueDoneAndDismissKeyboardInputAccessory |
|||
onClearTapped={() => { |
|||
setImportText(''); |
|||
Keyboard.dismiss(); |
|||
}} |
|||
onPasteTapped={text => { |
|||
setImportText(text); |
|||
Keyboard.dismiss(); |
|||
}} |
|||
/> |
|||
), |
|||
})} |
|||
</KeyboardAvoidingView> |
|||
</TouchableWithoutFeedback> |
|||
|
|||
<BlueSpacing20 /> |
|||
<View |
|||
style={{ |
|||
alignItems: 'center', |
|||
}} |
|||
> |
|||
<BlueButton |
|||
disabled={importText.trim().length === 0} |
|||
title={loc.wallets.import.do_import} |
|||
buttonStyle={{ |
|||
width: width / 1.5, |
|||
}} |
|||
onPress={importButtonPressed} |
|||
/> |
|||
<BlueButtonLink |
|||
title={loc.wallets.import.scan_qr} |
|||
onPress={() => { |
|||
navigate('ScanQrAddress', { onBarScanned }); |
|||
}} |
|||
/> |
|||
</View> |
|||
</SafeBlueArea> |
|||
); |
|||
}; |
|||
|
|||
WalletsImport.propTypes = { |
|||
navigation: PropTypes.shape({ |
|||
navigate: PropTypes.func, |
|||
goBack: PropTypes.func, |
|||
dismiss: PropTypes.func, |
|||
}), |
|||
WalletsImport.navigationOptions = { |
|||
...BlueNavigationStyle(), |
|||
title: loc.wallets.import.title, |
|||
}; |
|||
export default WalletsImport; |
|||
|
@ -0,0 +1,51 @@ |
|||
import React, { useState } from 'react'; |
|||
import { useNavigation, useNavigationParam } from 'react-navigation-hooks'; |
|||
import { View, Dimensions } from 'react-native'; |
|||
import { SafeBlueArea, BlueSpacing20, BlueCopyTextToClipboard, BlueButton, BlueCard, BlueTextCentered } from '../../BlueComponents'; |
|||
import QRCode from 'react-native-qrcode-svg'; |
|||
import { ScrollView } from 'react-native-gesture-handler'; |
|||
const { height, width } = Dimensions.get('window'); |
|||
const BlueApp = require('../../BlueApp'); |
|||
|
|||
const PleaseBackupLNDHub = () => { |
|||
const wallet = useNavigationParam('wallet'); |
|||
const navigation = useNavigation(); |
|||
const [qrCodeHeight, setQrCodeHeight] = useState(height > width ? width - 40 : width / 2); |
|||
|
|||
const onLayout = () => { |
|||
const { height } = Dimensions.get('window'); |
|||
setQrCodeHeight(height > width ? width - 40 : width / 2); |
|||
}; |
|||
|
|||
return ( |
|||
<SafeBlueArea style={{ flex: 1 }}> |
|||
<ScrollView centerContent contentContainerStyle={{ flexGrow: 1 }} onLayout={onLayout}> |
|||
<BlueCard> |
|||
<View> |
|||
<BlueTextCentered> |
|||
Please take a moment to save this LNDHub authentication. It's your backup you can use to restore the wallet on other device. |
|||
</BlueTextCentered> |
|||
</View> |
|||
<BlueSpacing20 /> |
|||
|
|||
<QRCode |
|||
value={wallet.secret} |
|||
logo={require('../../img/qr-code.png')} |
|||
logoSize={90} |
|||
size={qrCodeHeight} |
|||
color={BlueApp.settings.foregroundColor} |
|||
logoBackgroundColor={BlueApp.settings.brandingColor} |
|||
ecl={'H'} |
|||
/> |
|||
|
|||
<BlueSpacing20 /> |
|||
<BlueCopyTextToClipboard text={wallet.secret} /> |
|||
<BlueSpacing20 /> |
|||
<BlueButton onPress={navigation.dismiss} title="OK, I have saved it." /> |
|||
</BlueCard> |
|||
</ScrollView> |
|||
</SafeBlueArea> |
|||
); |
|||
}; |
|||
|
|||
export default PleaseBackupLNDHub; |
@ -1,344 +0,0 @@ |
|||
/* global alert */ |
|||
import React from 'react'; |
|||
import { ActivityIndicator, Image, View, TouchableOpacity } from 'react-native'; |
|||
import { BlueText, SafeBlueArea, BlueButton } from '../../BlueComponents'; |
|||
import { RNCamera } from 'react-native-camera'; |
|||
import { SegwitP2SHWallet, LegacyWallet, WatchOnlyWallet, HDLegacyP2PKHWallet, HDSegwitBech32Wallet } from '../../class'; |
|||
import PropTypes from 'prop-types'; |
|||
import { HDSegwitP2SHWallet } from '../../class/hd-segwit-p2sh-wallet'; |
|||
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; |
|||
import bip21 from 'bip21'; |
|||
/** @type {AppStorage} */ |
|||
let BlueApp = require('../../BlueApp'); |
|||
let EV = require('../../events'); |
|||
let bip38 = require('../../bip38'); |
|||
let wif = require('wif'); |
|||
let prompt = require('../../prompt'); |
|||
let loc = require('../../loc'); |
|||
|
|||
export default class ScanQrWif extends React.Component { |
|||
static navigationOptions = { |
|||
header: null, |
|||
}; |
|||
|
|||
state = { isLoading: false }; |
|||
|
|||
onBarCodeScanned = async ret => { |
|||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview(); |
|||
if (+new Date() - this.lastTimeIveBeenHere < 6000) { |
|||
this.lastTimeIveBeenHere = +new Date(); |
|||
return; |
|||
} |
|||
this.lastTimeIveBeenHere = +new Date(); |
|||
this.setState({ isLoading: true }); |
|||
if (ret.data[0] === '6') { |
|||
// password-encrypted, need to ask for password and decrypt
|
|||
console.log('trying to decrypt...'); |
|||
|
|||
this.setState({ |
|||
message: loc.wallets.scanQrWif.decoding, |
|||
}); |
|||
shold_stop_bip38 = undefined; // eslint-disable-line
|
|||
let password = await prompt(loc.wallets.scanQrWif.input_password, loc.wallets.scanQrWif.password_explain); |
|||
if (!password) { |
|||
return; |
|||
} |
|||
let that = this; |
|||
try { |
|||
let decryptedKey = await bip38.decrypt(ret.data, password, function(status) { |
|||
that.setState({ |
|||
message: loc.wallets.scanQrWif.decoding + '... ' + status.percent.toString().substr(0, 4) + ' %', |
|||
}); |
|||
}); |
|||
ret.data = wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed); |
|||
} catch (e) { |
|||
console.log(e.message); |
|||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); |
|||
this.setState({ message: false, isLoading: false }); |
|||
return alert(loc.wallets.scanQrWif.bad_password); |
|||
} |
|||
|
|||
this.setState({ message: false, isLoading: false }); |
|||
} |
|||
|
|||
for (let w of BlueApp.wallets) { |
|||
if (w.getSecret() === ret.data) { |
|||
// lookig for duplicates
|
|||
this.setState({ isLoading: false }); |
|||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); |
|||
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
|
|||
} |
|||
} |
|||
|
|||
// is it HD BIP49 mnemonic?
|
|||
let hd = new HDSegwitP2SHWallet(); |
|||
hd.setSecret(ret.data); |
|||
if (hd.validateMnemonic()) { |
|||
for (let w of BlueApp.wallets) { |
|||
if (w.getSecret() === hd.getSecret()) { |
|||
// lookig for duplicates
|
|||
this.setState({ isLoading: false }); |
|||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); |
|||
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
|
|||
} |
|||
} |
|||
this.setState({ isLoading: true }); |
|||
hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable); |
|||
await hd.fetchBalance(); |
|||
if (hd.getBalance() !== 0) { |
|||
await hd.fetchTransactions(); |
|||
BlueApp.wallets.push(hd); |
|||
await BlueApp.saveToDisk(); |
|||
alert(loc.wallets.import.success); |
|||
this.props.navigation.popToTop(); |
|||
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); |
|||
this.setState({ isLoading: false }); |
|||
return; |
|||
} |
|||
} |
|||
// nope
|
|||
|
|||
// is it HD legacy (BIP44) mnemonic?
|
|||
hd = new HDLegacyP2PKHWallet(); |
|||
hd.setSecret(ret.data); |
|||
if (hd.validateMnemonic()) { |
|||
for (let w of BlueApp.wallets) { |
|||
if (w.getSecret() === hd.getSecret()) { |
|||
// lookig for duplicates
|
|||
this.setState({ isLoading: false }); |
|||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); |
|||
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
|
|||
} |
|||
} |
|||
await hd.fetchTransactions(); |
|||
if (hd.getTransactions().length !== 0) { |
|||
await hd.fetchBalance(); |
|||
hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable); |
|||
BlueApp.wallets.push(hd); |
|||
await BlueApp.saveToDisk(); |
|||
alert(loc.wallets.import.success); |
|||
this.props.navigation.popToTop(); |
|||
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); |
|||
this.setState({ isLoading: false }); |
|||
return; |
|||
} |
|||
} |
|||
// nope
|
|||
|
|||
// is it HD BIP49 mnemonic?
|
|||
hd = new HDSegwitBech32Wallet(); |
|||
hd.setSecret(ret.data); |
|||
if (hd.validateMnemonic()) { |
|||
for (let w of BlueApp.wallets) { |
|||
if (w.getSecret() === hd.getSecret()) { |
|||
// lookig for duplicates
|
|||
this.setState({ isLoading: false }); |
|||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); |
|||
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
|
|||
} |
|||
} |
|||
this.setState({ isLoading: true }); |
|||
hd.setLabel(loc.wallets.import.imported + ' ' + hd.typeReadable); |
|||
BlueApp.wallets.push(hd); |
|||
await hd.fetchBalance(); |
|||
await hd.fetchTransactions(); |
|||
await BlueApp.saveToDisk(); |
|||
alert(loc.wallets.import.success); |
|||
this.props.navigation.popToTop(); |
|||
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); |
|||
this.setState({ isLoading: false }); |
|||
return; |
|||
} |
|||
// nope
|
|||
|
|||
// is it lndhub?
|
|||
if (ret.data.indexOf('blitzhub://') !== -1 || ret.data.indexOf('lndhub://') !== -1) { |
|||
this.setState({ isLoading: true }); |
|||
let lnd = new LightningCustodianWallet(); |
|||
lnd.setSecret(ret.data); |
|||
if (ret.data.includes('@')) { |
|||
const split = ret.data.split('@'); |
|||
lnd.setBaseURI(split[1]); |
|||
lnd.init(); |
|||
lnd.setSecret(split[0]); |
|||
} |
|||
|
|||
try { |
|||
await lnd.authorize(); |
|||
await lnd.fetchTransactions(); |
|||
await lnd.fetchUserInvoices(); |
|||
await lnd.fetchPendingTransactions(); |
|||
await lnd.fetchBalance(); |
|||
} catch (Err) { |
|||
console.log(Err); |
|||
this.setState({ isLoading: false }); |
|||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); |
|||
alert(Err.message); |
|||
return; |
|||
} |
|||
|
|||
BlueApp.wallets.push(lnd); |
|||
lnd.setLabel(loc.wallets.import.imported + ' ' + lnd.typeReadable); |
|||
this.props.navigation.popToTop(); |
|||
alert(loc.wallets.import.success); |
|||
await BlueApp.saveToDisk(); |
|||
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); |
|||
this.setState({ isLoading: false }); |
|||
return; |
|||
} |
|||
// nope
|
|||
|
|||
// is it just address..?
|
|||
let watchOnly = new WatchOnlyWallet(); |
|||
let watchAddr = ret.data; |
|||
|
|||
// Is it BIP21 format?
|
|||
if (ret.data.indexOf('bitcoin:') === 0 || ret.data.indexOf('BITCOIN:') === 0) { |
|||
try { |
|||
watchAddr = bip21.decode(ret.data).address; |
|||
} catch (err) { |
|||
console.log(err); |
|||
} |
|||
} |
|||
|
|||
if (watchOnly.setSecret(watchAddr) && watchOnly.valid()) { |
|||
watchOnly.setLabel(loc.wallets.scanQrWif.imported_watchonly); |
|||
BlueApp.wallets.push(watchOnly); |
|||
alert(loc.wallets.scanQrWif.imported_watchonly + loc.wallets.scanQrWif.with_address + watchOnly.getAddress()); |
|||
await watchOnly.fetchBalance(); |
|||
await watchOnly.fetchTransactions(); |
|||
await BlueApp.saveToDisk(); |
|||
this.props.navigation.popToTop(); |
|||
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); |
|||
this.setState({ isLoading: false }); |
|||
return; |
|||
} |
|||
// nope
|
|||
|
|||
let newWallet = new SegwitP2SHWallet(); |
|||
newWallet.setSecret(ret.data); |
|||
let newLegacyWallet = new LegacyWallet(); |
|||
newLegacyWallet.setSecret(ret.data); |
|||
|
|||
if (newWallet.getAddress() === false && newLegacyWallet.getAddress() === false) { |
|||
alert(loc.wallets.scanQrWif.bad_wif); |
|||
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.resumePreview(); |
|||
this.setState({ isLoading: false }); |
|||
return; |
|||
} |
|||
|
|||
if (newWallet.getAddress() === false && newLegacyWallet.getAddress() !== false) { |
|||
// case - WIF is valid, just has uncompressed pubkey
|
|||
newLegacyWallet.setLabel(loc.wallets.scanQrWif.imported_legacy); |
|||
BlueApp.wallets.push(newLegacyWallet); |
|||
alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newLegacyWallet.getAddress()); |
|||
await newLegacyWallet.fetchBalance(); |
|||
await newLegacyWallet.fetchTransactions(); |
|||
await BlueApp.saveToDisk(); |
|||
this.props.navigation.popToTop(); |
|||
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); |
|||
return; |
|||
} |
|||
|
|||
this.setState({ isLoading: true }); |
|||
await newLegacyWallet.fetchBalance(); |
|||
console.log('newLegacyWallet == ', newLegacyWallet.getBalance()); |
|||
|
|||
if (newLegacyWallet.getBalance()) { |
|||
newLegacyWallet.setLabel(loc.wallets.scanQrWif.imported_legacy); |
|||
BlueApp.wallets.push(newLegacyWallet); |
|||
alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newLegacyWallet.getAddress()); |
|||
await newLegacyWallet.fetchTransactions(); |
|||
} else { |
|||
await newWallet.fetchBalance(); |
|||
await newWallet.fetchTransactions(); |
|||
newWallet.setLabel(loc.wallets.scanQrWif.imported_segwit); |
|||
BlueApp.wallets.push(newWallet); |
|||
alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newWallet.getAddress()); |
|||
} |
|||
await BlueApp.saveToDisk(); |
|||
this.props.navigation.popToTop(); |
|||
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500); |
|||
}; // end
|
|||
|
|||
render() { |
|||
if (this.state.isLoading) { |
|||
return ( |
|||
<View style={{ flex: 1, paddingTop: 20, justifyContent: 'center', alignContent: 'center' }}> |
|||
<ActivityIndicator /> |
|||
</View> |
|||
); |
|||
} |
|||
return ( |
|||
<View style={{ flex: 1 }}> |
|||
{(() => { |
|||
if (this.state.message) { |
|||
return ( |
|||
<SafeBlueArea> |
|||
<View |
|||
style={{ |
|||
flex: 1, |
|||
flexDirection: 'column', |
|||
justifyContent: 'center', |
|||
alignItems: 'center', |
|||
}} |
|||
> |
|||
<BlueText>{this.state.message}</BlueText> |
|||
<BlueButton |
|||
icon={{ name: 'ban', type: 'font-awesome' }} |
|||
onPress={async () => { |
|||
this.setState({ message: false }); |
|||
shold_stop_bip38 = true; // eslint-disable-line
|
|||
}} |
|||
title={loc.wallets.scanQrWif.cancel} |
|||
/> |
|||
</View> |
|||
</SafeBlueArea> |
|||
); |
|||
} else { |
|||
return ( |
|||
<SafeBlueArea style={{ flex: 1 }}> |
|||
<RNCamera |
|||
captureAudio={false} |
|||
androidCameraPermissionOptions={{ |
|||
title: 'Permission to use camera', |
|||
message: 'We need your permission to use your camera', |
|||
buttonPositive: 'OK', |
|||
buttonNegative: 'Cancel', |
|||
}} |
|||
style={{ flex: 1, justifyContent: 'space-between' }} |
|||
onBarCodeRead={this.onBarCodeScanned} |
|||
ref={ref => (this.cameraRef = ref)} |
|||
barCodeTypes={[RNCamera.Constants.BarCodeType.qr]} |
|||
/> |
|||
<TouchableOpacity |
|||
style={{ |
|||
width: 40, |
|||
height: 40, |
|||
marginLeft: 24, |
|||
backgroundColor: '#FFFFFF', |
|||
justifyContent: 'center', |
|||
borderRadius: 20, |
|||
position: 'absolute', |
|||
top: 64, |
|||
}} |
|||
onPress={() => this.props.navigation.goBack(null)} |
|||
> |
|||
<Image style={{ alignSelf: 'center' }} source={require('../../img/close.png')} /> |
|||
</TouchableOpacity> |
|||
</SafeBlueArea> |
|||
); |
|||
} |
|||
})()} |
|||
</View> |
|||
); |
|||
} |
|||
} |
|||
|
|||
ScanQrWif.propTypes = { |
|||
navigation: PropTypes.shape({ |
|||
goBack: PropTypes.func, |
|||
popToTop: PropTypes.func, |
|||
navigate: PropTypes.func, |
|||
}), |
|||
}; |
File diff suppressed because one or more lines are too long
@ -0,0 +1,50 @@ |
|||
/* global describe, it */ |
|||
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; |
|||
const assert = require('assert'); |
|||
|
|||
describe('unit - DeepLinkSchemaMatch', function() { |
|||
it('hasSchema', () => { |
|||
const hasSchema = DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'); |
|||
assert.ok(hasSchema); |
|||
}); |
|||
|
|||
it('isBitcoin Address', () => { |
|||
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); |
|||
}); |
|||
|
|||
it('isLighting Invoice', () => { |
|||
assert.ok( |
|||
DeeplinkSchemaMatch.isLightningInvoice( |
|||
'lightning:lnbc10u1pwjqwkkpp5vlc3tttdzhpk9fwzkkue0sf2pumtza7qyw9vucxyyeh0yaqq66yqdq5f38z6mmwd3ujqar9wd6qcqzpgxq97zvuqrzjqvgptfurj3528snx6e3dtwepafxw5fpzdymw9pj20jj09sunnqmwqz9hx5qqtmgqqqqqqqlgqqqqqqgqjq5duu3fs9xq9vn89qk3ezwpygecu4p3n69wm3tnl28rpgn2gmk5hjaznemw0gy32wrslpn3g24khcgnpua9q04fttm2y8pnhmhhc2gncplz0zde', |
|||
), |
|||
); |
|||
}); |
|||
|
|||
it('isBoth Bitcoin & Invoice', () => { |
|||
assert.ok( |
|||
DeeplinkSchemaMatch.isBothBitcoinAndLightning( |
|||
'bitcoin:1DamianM2k8WfNEeJmyqSe2YW1upB7UATx?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4', |
|||
), |
|||
); |
|||
}); |
|||
|
|||
it('isLnurl', () => { |
|||
assert.ok( |
|||
DeeplinkSchemaMatch.isLnUrl( |
|||
'LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS', |
|||
), |
|||
); |
|||
}); |
|||
|
|||
it('navigationForRoute', () => { |
|||
const event = { uri: '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG' }; |
|||
DeeplinkSchemaMatch.navigationRouteFor(event, navValue => { |
|||
assert.strictEqual(navValue, { |
|||
routeName: 'SendDetails', |
|||
params: { |
|||
uri: event.url, |
|||
}, |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
Loading…
Reference in new issue