2018-03-24 22:24:20 +01:00
import { AbstractWallet } from './abstract-wallet' ;
2020-03-09 19:51:34 +01:00
import { HDSegwitBech32Wallet } from './' ;
2018-12-11 23:52:46 +01:00
import { NativeModules } from 'react-native' ;
2019-09-14 00:15:59 +02:00
const bitcoin = require ( 'bitcoinjs-lib' ) ;
2018-12-11 23:52:46 +01:00
const { RNRandomBytes } = NativeModules ;
2019-02-28 03:50:31 +01:00
const BlueElectrum = require ( '../BlueElectrum' ) ;
2020-04-22 17:13:18 +02:00
const coinSelectAccumulative = require ( 'coinselect/accumulative' ) ;
const coinSelectSplit = require ( 'coinselect/split' ) ;
2018-03-20 21:41:07 +01:00
/ * *
2018-12-22 17:51:07 +01:00
* Has private key and single address like "1ABCD....."
2018-03-20 21:41:07 +01:00
* ( legacy P2PKH compressed )
* /
export class LegacyWallet extends AbstractWallet {
2018-12-28 16:52:06 +01:00
static type = 'legacy' ;
static typeReadable = 'Legacy (P2PKH)' ;
2018-03-20 21:41:07 +01:00
2018-07-14 22:32:36 +02:00
/ * *
* Simple function which says that we havent tried to fetch balance
* for a long time
*
* @ return { boolean }
* /
timeToRefreshBalance ( ) {
2018-07-14 21:52:27 +02:00
if ( + new Date ( ) - this . _lastBalanceFetch >= 5 * 60 * 1000 ) {
2018-07-02 15:51:24 +02:00
return true ;
}
2018-07-14 22:32:36 +02:00
return false ;
}
2018-07-12 01:38:24 +02:00
2018-07-14 22:32:36 +02:00
/ * *
* Simple function which says if we hve some low - confirmed transactions
* and we better fetch them
*
* @ return { boolean }
* /
timeToRefreshTransaction ( ) {
2020-03-09 19:51:34 +01:00
for ( let tx of this . getTransactions ( ) ) {
2018-07-12 01:38:24 +02:00
if ( tx . confirmations < 7 ) {
return true ;
}
}
2018-07-14 22:32:36 +02:00
return false ;
2018-07-02 15:51:24 +02:00
}
2018-12-11 23:52:46 +01:00
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 = bitcoin . ECPair . makeRandom ( {
rng : function ( length ) {
return buf ;
} ,
} ) . toWIF ( ) ;
resolve ( ) ;
} ) ;
2018-03-20 21:41:07 +01:00
}
2018-12-11 23:52:46 +01:00
// RN environment
RNRandomBytes . randomBytes ( 32 , ( err , bytes ) => {
if ( err ) throw new Error ( err ) ;
that . secret = bitcoin . ECPair . makeRandom ( {
rng : function ( length ) {
let b = Buffer . from ( bytes , 'base64' ) ;
return b ;
} ,
} ) . toWIF ( ) ;
resolve ( ) ;
} ) ;
} ) ;
2018-03-20 21:41:07 +01:00
}
/ * *
*
* @ returns { string }
* /
getAddress ( ) {
if ( this . _address ) return this . _address ;
let address ;
try {
let keyPair = bitcoin . ECPair . fromWIF ( this . secret ) ;
2019-09-14 00:15:59 +02:00
address = bitcoin . payments . p2pkh ( {
pubkey : keyPair . publicKey ,
} ) . address ;
2018-03-20 21:41:07 +01:00
} catch ( err ) {
return false ;
}
this . _address = address ;
return this . _address ;
}
2018-05-20 11:38:50 +02:00
/ * *
2018-06-17 12:46:19 +02:00
* Fetches balance of the Wallet via API .
* Returns VOID . Get the actual balance via getter .
2018-05-20 11:38:50 +02:00
*
* @ returns { Promise . < void > }
* /
2018-03-20 21:41:07 +01:00
async fetchBalance ( ) {
try {
2020-03-09 19:51:34 +01:00
let balance = await BlueElectrum . getBalanceByAddress ( this . getAddress ( ) ) ;
this . balance = Number ( balance . confirmed ) ;
2020-04-22 17:13:18 +02:00
this . unconfirmed _balance = Number ( balance . unconfirmed ) ;
2018-07-02 11:48:40 +02:00
this . _lastBalanceFetch = + new Date ( ) ;
2020-03-09 19:51:34 +01:00
} catch ( Error ) {
console . warn ( Error ) ;
2018-03-20 21:41:07 +01:00
}
}
2018-06-17 12:46:19 +02:00
/ * *
* Fetches UTXO from API . Returns VOID .
*
* @ return { Promise . < void > }
* /
2018-03-20 21:41:07 +01:00
async fetchUtxo ( ) {
try {
2020-03-09 19:51:34 +01:00
let utxos = await BlueElectrum . multiGetUtxoByAddress ( [ this . getAddress ( ) ] ) ;
2020-03-31 18:46:31 +02:00
this . utxo = [ ] ;
2020-03-09 19:51:34 +01:00
for ( let arr of Object . values ( utxos ) ) {
this . utxo = this . utxo . concat ( arr ) ;
}
2020-04-22 17:13:18 +02:00
// now we need to fetch txhash for each input as required by PSBT
if ( LegacyWallet . type !== this . type ) return ; // but only for LEGACY single-address wallets
let txhexes = await BlueElectrum . multiGetTransactionByTxid (
this . utxo . map ( u => u [ 'txId' ] ) ,
50 ,
false ,
) ;
let newUtxos = [ ] ;
for ( let u of this . utxo ) {
if ( txhexes [ u . txId ] ) u . txhex = txhexes [ u . txId ] ;
newUtxos . push ( u ) ;
}
this . utxo = newUtxos ;
2020-03-09 19:51:34 +01:00
} catch ( Error ) {
console . warn ( Error ) ;
}
2018-03-20 21:41:07 +01:00
}
2020-03-09 19:51:34 +01:00
getUtxo ( ) {
2020-04-22 17:13:18 +02:00
let ret = [ ] ;
for ( let u of this . utxo ) {
if ( u . txId ) u . txid = u . txId ;
if ( ! u . confirmations && u . height ) u . confirmations = BlueElectrum . estimateCurrentBlockheight ( ) - u . height ;
ret . push ( u ) ;
}
return ret ;
2020-03-09 19:51:34 +01:00
}
2018-06-17 12:46:19 +02:00
/ * *
2020-03-09 19:51:34 +01:00
* Fetches transactions via Electrum . Returns VOID .
* Use getter to get the actual list . *
* @ see AbstractHDElectrumWallet . fetchTransactions ( )
2018-06-17 12:46:19 +02:00
*
* @ return { Promise . < void > }
* /
2018-03-20 21:41:07 +01:00
async fetchTransactions ( ) {
2020-03-09 19:51:34 +01:00
// Below is a simplified copypaste from HD electrum wallet
this . _txs _by _external _index = [ ] ;
let addresses2fetch = [ this . getAddress ( ) ] ;
// 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 ;
2018-03-20 21:41:07 +01:00
}
2020-03-09 19:51:34 +01:00
}
2018-03-20 21:41:07 +01:00
2020-03-09 19:51:34 +01:00
// next, batch fetching each txid we got
let txdatas = await BlueElectrum . multiGetTransactionByTxid ( Object . keys ( txs ) ) ;
2018-07-25 01:27:21 +02:00
2020-03-09 19:51:34 +01:00
// 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 ;
2018-03-20 21:41:07 +01:00
}
}
}
2020-03-09 19:51:34 +01:00
// now, we need to put transactions in all relevant `cells` of internal hashmaps: this.transactions_by_internal_index && this.transactions_by_external_index
2018-10-11 19:25:39 +02:00
2020-03-09 19:51:34 +01:00
for ( let tx of Object . values ( txdatas ) ) {
for ( let vin of tx . vin ) {
if ( vin . addresses && vin . addresses . indexOf ( this . getAddress ( ) ) !== - 1 ) {
// this TX is related to our address
let clonedTx = Object . assign ( { } , tx ) ;
clonedTx . inputs = tx . vin . slice ( 0 ) ;
clonedTx . outputs = tx . vout . slice ( 0 ) ;
delete clonedTx . vin ;
delete clonedTx . vout ;
2018-10-11 19:25:39 +02:00
2020-03-09 19:51:34 +01:00
this . _txs _by _external _index . push ( clonedTx ) ;
}
2018-10-11 19:25:39 +02:00
}
2020-03-09 19:51:34 +01:00
for ( let vout of tx . vout ) {
if ( vout . scriptPubKey . addresses . indexOf ( this . getAddress ( ) ) !== - 1 ) {
// this TX is related to our address
let clonedTx = Object . assign ( { } , tx ) ;
clonedTx . inputs = tx . vin . slice ( 0 ) ;
clonedTx . outputs = tx . vout . slice ( 0 ) ;
delete clonedTx . vin ;
delete clonedTx . vout ;
this . _txs _by _external _index . push ( clonedTx ) ;
}
2018-10-11 19:25:39 +02:00
}
2020-03-09 19:51:34 +01:00
}
2018-10-11 19:25:39 +02:00
2020-03-09 19:51:34 +01:00
this . _lastTxFetch = + new Date ( ) ;
2018-10-11 19:25:39 +02:00
}
2020-03-09 19:51:34 +01:00
getTransactions ( ) {
// a hacky code reuse from electrum HD wallet:
this . _txs _by _external _index = this . _txs _by _external _index || [ ] ;
this . _txs _by _internal _index = [ ] ;
2018-10-11 19:25:39 +02:00
2020-03-09 19:51:34 +01:00
let hd = new HDSegwitBech32Wallet ( ) ;
return hd . getTransactions . apply ( this ) ;
2018-10-11 19:25:39 +02:00
}
2018-03-20 21:41:07 +01:00
async broadcastTx ( txhex ) {
2019-02-28 03:50:31 +01:00
try {
const broadcast = await BlueElectrum . broadcast ( txhex ) ;
return broadcast ;
} catch ( error ) {
return error ;
2018-06-11 21:17:16 +02:00
}
}
2018-05-20 11:38:50 +02:00
/ * *
*
2020-04-22 17:13:18 +02:00
* @ param utxos { Array . < { vout : Number , value : Number , txId : String , address : String , txhex : 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
* @ param masterFingerprint { number } Decimal number of wallet ' s master fingerprint
* @ returns { { outputs : Array , tx : Transaction , inputs : Array , fee : Number , psbt : Psbt } }
2018-05-20 11:38:50 +02:00
* /
2020-04-22 17:13:18 +02:00
createTransaction ( utxos , targets , feeRate , changeAddress , sequence , skipSigning = false , masterFingerprint ) {
if ( ! changeAddress ) throw new Error ( 'No change address provided' ) ;
sequence = sequence || 0xffffffff ; // disable RBF by default
let algo = coinSelectAccumulative ;
if ( targets . length === 1 && targets [ 0 ] && ! targets [ 0 ] . value ) {
// we want to send MAX
algo = coinSelectSplit ;
2018-05-20 11:38:50 +02:00
}
2020-04-22 17:13:18 +02:00
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 values = { } ;
let keyPair ;
inputs . forEach ( input => {
if ( ! skipSigning ) {
// skiping signing related stuff
keyPair = bitcoin . ECPair . fromWIF ( this . secret ) ; // secret is WIF
}
values [ c ] = input . value ;
c ++ ;
if ( ! input . txhex ) throw new Error ( 'UTXO is missing txhex of the input, which is required by PSBT for non-segwit input' ) ;
psbt . addInput ( {
hash : input . txid ,
index : input . vout ,
sequence ,
// non-segwit inputs now require passing the whole previous tx as Buffer
nonWitnessUtxo : Buffer . from ( input . txhex , 'hex' ) ,
} ) ;
} ) ;
outputs . forEach ( output => {
// if output has no address - this is change output
if ( ! output . address ) {
output . address = changeAddress ;
}
let outputData = {
address : output . address ,
value : output . value ,
} ;
psbt . addOutput ( outputData ) ;
} ) ;
if ( ! skipSigning ) {
// skiping signing related stuff
for ( let cc = 0 ; cc < c ; cc ++ ) {
psbt . signInput ( cc , keyPair ) ;
}
}
let tx ;
if ( ! skipSigning ) {
tx = psbt . finalizeAllInputs ( ) . extractTransaction ( ) ;
}
return { tx , inputs , outputs , fee , psbt } ;
2018-05-20 11:38:50 +02:00
}
2018-06-17 12:46:44 +02:00
2018-06-25 00:22:46 +02:00
getLatestTransactionTime ( ) {
2018-09-01 01:28:19 +02:00
if ( this . getTransactions ( ) . length === 0 ) {
return 0 ;
}
let max = 0 ;
2018-07-22 16:49:59 +02:00
for ( let tx of this . getTransactions ( ) ) {
2018-09-01 01:28:19 +02:00
max = Math . max ( new Date ( tx . received ) * 1 , max ) ;
2018-06-25 00:22:46 +02:00
}
2018-09-01 01:28:19 +02:00
return new Date ( max ) . toString ( ) ;
2018-06-25 00:22:46 +02:00
}
2020-03-09 19:51:34 +01:00
/ * *
* Validates any address , including legacy , p2sh and bech32
*
* @ param address
* @ returns { boolean }
* /
2018-07-12 01:38:24 +02:00
isAddressValid ( address ) {
try {
bitcoin . address . toOutputScript ( address ) ;
return true ;
} catch ( e ) {
return false ;
}
}
2020-03-09 19:51:34 +01:00
2020-03-23 17:32:51 +01:00
/ * *
* Converts script pub key to legacy address if it can . Returns FALSE if it cant .
*
* @ param scriptPubKey
* @ returns { boolean | string } Either p2pkh address or false
* /
static scriptPubKeyToAddress ( scriptPubKey ) {
const scriptPubKey2 = Buffer . from ( scriptPubKey , 'hex' ) ;
let ret ;
try {
ret = bitcoin . payments . p2pkh ( {
output : scriptPubKey2 ,
network : bitcoin . networks . bitcoin ,
} ) . address ;
} catch ( _ ) {
return false ;
}
return ret ;
}
2020-03-09 19:51:34 +01:00
weOwnAddress ( address ) {
return this . getAddress ( ) === address || this . _address === address ;
}
2020-04-22 17:13:18 +02:00
allowSendMax ( ) {
return true ;
}
async getChangeAddressAsync ( ) {
return new Promise ( resolve => {
resolve ( this . getAddress ( ) ) ;
} ) ;
}
2018-03-20 21:41:07 +01:00
}