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 { AbstractHDElectrumWallet } from './abstract-hd-electrum-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; |
|
||||
|
|
||||
/** |
/** |
||||
* HD Wallet (BIP39). |
* HD Wallet (BIP39). |
||||
* In particular, BIP84 (Bech32 Native Segwit) |
* In particular, BIP84 (Bech32 Native Segwit) |
||||
* @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
|
* @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 type = 'HDsegwitBech32'; |
||||
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)'; |
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() { |
allowBatchSend() { |
||||
return true; |
return true; |
||||
} |
} |
||||
|
|
||||
allowSendMax(): boolean { |
allowSendMax() { |
||||
return true; |
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 */ |
/* global alert */ |
||||
import { |
import React, { useEffect, useState } from 'react'; |
||||
SegwitP2SHWallet, |
|
||||
LegacyWallet, |
|
||||
WatchOnlyWallet, |
|
||||
HDLegacyBreadwalletWallet, |
|
||||
HDSegwitP2SHWallet, |
|
||||
HDLegacyP2PKHWallet, |
|
||||
HDSegwitBech32Wallet, |
|
||||
} from '../../class'; |
|
||||
import React, { Component } from 'react'; |
|
||||
import { KeyboardAvoidingView, Platform, Dimensions, View, TouchableWithoutFeedback, Keyboard } from 'react-native'; |
import { KeyboardAvoidingView, Platform, Dimensions, View, TouchableWithoutFeedback, Keyboard } from 'react-native'; |
||||
import { |
import { |
||||
BlueFormMultiInput, |
BlueFormMultiInput, |
||||
BlueButtonLink, |
BlueButtonLink, |
||||
BlueFormLabel, |
BlueFormLabel, |
||||
BlueLoading, |
|
||||
BlueDoneAndDismissKeyboardInputAccessory, |
BlueDoneAndDismissKeyboardInputAccessory, |
||||
BlueButton, |
BlueButton, |
||||
SafeBlueArea, |
SafeBlueArea, |
||||
BlueSpacing20, |
BlueSpacing20, |
||||
BlueNavigationStyle, |
BlueNavigationStyle, |
||||
} from '../../BlueComponents'; |
} from '../../BlueComponents'; |
||||
import PropTypes from 'prop-types'; |
|
||||
import { LightningCustodianWallet } from '../../class/lightning-custodian-wallet'; |
|
||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; |
import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; |
||||
import Privacy from '../../Privacy'; |
import Privacy from '../../Privacy'; |
||||
let EV = require('../../events'); |
import { useNavigation, useNavigationParam } from 'react-navigation-hooks'; |
||||
let A = require('../../analytics'); |
import WalletImport from '../../class/walletImport'; |
||||
/** @type {AppStorage} */ |
|
||||
let BlueApp = require('../../BlueApp'); |
|
||||
let loc = require('../../loc'); |
let loc = require('../../loc'); |
||||
const { width } = Dimensions.get('window'); |
const { width } = Dimensions.get('window'); |
||||
|
|
||||
export default class WalletsImport extends Component { |
const WalletsImport = () => { |
||||
static navigationOptions = { |
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false); |
||||
...BlueNavigationStyle(), |
const [importText, setImportText] = useState(useNavigationParam('label') || ''); |
||||
title: loc.wallets.import.title, |
const { navigate, dismiss } = useNavigation(); |
||||
}; |
|
||||
|
|
||||
constructor(props) { |
|
||||
super(props); |
|
||||
this.state = { |
|
||||
isLoading: true, |
|
||||
isToolbarVisibleForAndroid: false, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
componentDidMount() { |
useEffect(() => { |
||||
this.setState({ |
|
||||
isLoading: false, |
|
||||
label: '', |
|
||||
}); |
|
||||
Privacy.enableBlur(); |
Privacy.enableBlur(); |
||||
} |
return () => Privacy.disableBlur(); |
||||
|
}); |
||||
|
|
||||
componentWillUnmount() { |
const importButtonPressed = () => { |
||||
Privacy.disableBlur(); |
if (importText.trim().length === 0) { |
||||
} |
return; |
||||
|
|
||||
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(); |
|
||||
} |
} |
||||
} |
importMnemonic(importText); |
||||
|
}; |
||||
|
|
||||
async importMnemonic(text) { |
const importMnemonic = importText => { |
||||
try { |
try { |
||||
// is it lightning custodian?
|
WalletImport.processImportText(importText); |
||||
if (text.indexOf('blitzhub://') !== -1 || text.indexOf('lndhub://') !== -1) { |
dismiss(); |
||||
let lnd = new LightningCustodianWallet(); |
} catch (error) { |
||||
if (text.includes('@')) { |
alert(loc.wallets.import.error); |
||||
const split = text.split('@'); |
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); |
||||
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> |
|
||||
); |
|
||||
} |
} |
||||
|
}; |
||||
|
|
||||
return ( |
const onBarScanned = value => { |
||||
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}> |
setImportText(value); |
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}> |
importMnemonic(value); |
||||
<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> |
|
||||
|
|
||||
<BlueSpacing20 /> |
return ( |
||||
<View |
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}> |
||||
style={{ |
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}> |
||||
alignItems: 'center', |
<KeyboardAvoidingView behavior="position" enabled> |
||||
}} |
<BlueFormLabel>{loc.wallets.import.explanation}</BlueFormLabel> |
||||
> |
<BlueSpacing20 /> |
||||
<BlueButton |
<BlueFormMultiInput |
||||
disabled={!this.state.label} |
value={importText} |
||||
title={loc.wallets.import.do_import} |
contextMenuHidden |
||||
buttonStyle={{ |
onChangeText={setImportText} |
||||
width: width / 1.5, |
inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID} |
||||
}} |
onFocus={() => setIsToolbarVisibleForAndroid(true)} |
||||
onPress={async () => { |
onBlur={() => setIsToolbarVisibleForAndroid(false)} |
||||
if (!this.state.label) { |
|
||||
return; |
|
||||
} |
|
||||
this.setState({ isLoading: true }, async () => { |
|
||||
await this.importMnemonic(this.state.label.trim()); |
|
||||
this.setState({ isLoading: false }); |
|
||||
}); |
|
||||
}} |
|
||||
/> |
/> |
||||
<BlueButtonLink |
{Platform.select({ |
||||
title={loc.wallets.import.scan_qr} |
ios: ( |
||||
onPress={() => { |
<BlueDoneAndDismissKeyboardInputAccessory |
||||
this.props.navigation.navigate('ScanQrWif'); |
onClearTapped={() => { |
||||
}} |
setImportText(''); |
||||
/> |
Keyboard.dismiss(); |
||||
</View> |
}} |
||||
</SafeBlueArea> |
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 = { |
WalletsImport.navigationOptions = { |
||||
navigation: PropTypes.shape({ |
...BlueNavigationStyle(), |
||||
navigate: PropTypes.func, |
title: loc.wallets.import.title, |
||||
goBack: PropTypes.func, |
|
||||
dismiss: PropTypes.func, |
|
||||
}), |
|
||||
}; |
}; |
||||
|
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