2021-03-09 12:26:56 +01:00
import BigNumber from 'bignumber.js' ;
import bitcoinMessage from 'bitcoinjs-message' ;
2020-05-24 12:27:08 +02:00
import { randomBytes } from '../rng' ;
2018-03-24 22:24:20 +01:00
import { AbstractWallet } from './abstract-wallet' ;
2020-05-24 12:27:08 +02:00
import { HDSegwitBech32Wallet } from '..' ;
2021-07-26 15:40:37 +02:00
import * as bitcoin from 'bitcoinjs-lib' ;
import * as BlueElectrum from '../../blue_modules/BlueElectrum' ;
2022-09-11 12:59:00 +02:00
import coinSelect , { CoinSelectOutput , CoinSelectReturnInput , CoinSelectTarget , CoinSelectUtxo } from 'coinselect' ;
2021-07-26 15:40:37 +02:00
import coinSelectSplit from 'coinselect/split' ;
import { CreateTransactionResult , CreateTransactionUtxo , Transaction , Utxo } from './types' ;
2022-09-03 01:48:07 +02:00
import { ECPairAPI , ECPairFactory , Signer } from 'ecpair' ;
2022-10-02 19:33:30 +02:00
import ecc from '../../blue_modules/noble_ecc' ;
2021-12-20 18:11:07 +01:00
const ECPair : ECPairAPI = ECPairFactory ( ecc ) ;
2023-05-26 20:56:24 +02:00
bitcoin . initEccLib ( ecc ) ;
2021-07-26 15:40:37 +02:00
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
2023-07-25 15:50:04 +02:00
_txs_by_external_index : Transaction [ ] = [ ] ;
_txs_by_internal_index : Transaction [ ] = [ ] ;
2021-07-26 15:40:37 +02: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 }
* /
2021-07-26 15:40:37 +02:00
timeToRefreshBalance ( ) : boolean {
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 }
* /
2021-07-26 15:40:37 +02:00
timeToRefreshTransaction ( ) : boolean {
2020-06-01 14:54:23 +02:00
for ( const tx of this . getTransactions ( ) ) {
2021-08-27 17:14:29 +02:00
if ( ( tx . confirmations ? ? 0 ) < 7 && this . _lastTxFetch < + new Date ( ) - 5 * 60 * 1000 ) {
2018-07-12 01:38:24 +02:00
return true ;
}
}
2018-07-14 22:32:36 +02:00
return false ;
2018-07-02 15:51:24 +02:00
}
2021-07-26 15:40:37 +02:00
async generate ( ) : Promise < void > {
2020-05-09 09:21:45 +02:00
const buf = await randomBytes ( 32 ) ;
2021-11-25 16:50:40 +01:00
this . secret = ECPair . makeRandom ( { rng : ( ) = > buf } ) . toWIF ( ) ;
2018-03-20 21:41:07 +01:00
}
2021-07-26 15:40:37 +02:00
async generateFromEntropy ( user : Buffer ) : Promise < void > {
2020-06-29 14:58:43 +02:00
let i = 0 ;
do {
i += 1 ;
const random = await randomBytes ( user . length < 32 ? 32 - user.length : 0 ) ;
const buf = Buffer . concat ( [ user , random ] , 32 ) ;
try {
2021-11-25 16:50:40 +01:00
this . secret = ECPair . fromPrivateKey ( buf ) . toWIF ( ) ;
2020-06-29 14:58:43 +02:00
return ;
} catch ( e ) {
if ( i === 5 ) throw e ;
}
} while ( true ) ;
}
2018-03-20 21:41:07 +01:00
/ * *
*
* @returns { string }
* /
2021-07-26 15:40:37 +02:00
getAddress ( ) : string | false {
2018-03-20 21:41:07 +01:00
if ( this . _address ) return this . _address ;
let address ;
try {
2021-11-25 16:50:40 +01:00
const keyPair = 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 ;
}
2021-07-26 15:40:37 +02:00
this . _address = address ? ? false ;
2018-03-20 21:41:07 +01:00
return this . _address ;
}
2020-07-29 22:00:00 +02:00
/ * *
* @inheritDoc
* /
2021-07-26 15:40:37 +02:00
getAllExternalAddresses ( ) : string [ ] {
const address = this . getAddress ( ) ;
return address ? [ address ] : [ ] ;
2020-07-29 22:00:00 +02:00
}
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 > }
* /
2021-07-26 15:40:37 +02:00
async fetchBalance ( ) : Promise < void > {
2018-03-20 21:41:07 +01:00
try {
2021-07-26 15:40:37 +02:00
const address = this . getAddress ( ) ;
if ( ! address ) throw new Error ( 'LegacyWallet: Invalid address' ) ;
const balance = await BlueElectrum . getBalanceByAddress ( address ) ;
2020-03-09 19:51:34 +01:00
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 ( ) ;
2023-07-25 15:50:04 +02: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 > }
* /
2021-07-26 15:40:37 +02:00
async fetchUtxo ( ) : Promise < void > {
2018-03-20 21:41:07 +01:00
try {
2021-07-26 15:40:37 +02:00
const address = this . getAddress ( ) ;
if ( ! address ) throw new Error ( 'LegacyWallet: Invalid address' ) ;
const utxos = await BlueElectrum . multiGetUtxoByAddress ( [ address ] ) ;
2020-03-31 18:46:31 +02:00
this . utxo = [ ] ;
2020-06-01 14:54:23 +02:00
for ( const arr of Object . values ( utxos ) ) {
2020-03-09 19:51:34 +01:00
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
2020-06-01 14:54:23 +02:00
const txhexes = await BlueElectrum . multiGetTransactionByTxid (
this . utxo . map ( u = > u . txId ) ,
2020-04-22 17:13:18 +02:00
50 ,
false ,
) ;
2020-06-01 14:54:23 +02:00
const newUtxos = [ ] ;
for ( const u of this . utxo ) {
2020-04-22 17:13:18 +02:00
if ( txhexes [ u . txId ] ) u . txhex = txhexes [ u . txId ] ;
newUtxos . push ( u ) ;
}
this . utxo = newUtxos ;
2023-07-25 15:50:04 +02:00
} catch ( error ) {
console . warn ( error ) ;
2020-03-09 19:51:34 +01:00
}
2018-03-20 21:41:07 +01:00
}
2020-11-22 08:43:11 +01:00
/ * *
* Getter for previously fetched UTXO . For example :
* [ { height : 0 ,
* value : 666 ,
* address : 'string' ,
* txId : 'string' ,
* vout : 1 ,
* txid : 'string' ,
* amount : 666 ,
* wif : 'string' ,
* confirmations : 0 } ]
*
* @param respectFrozen { boolean } Add Frozen outputs
* @returns { [ ] }
* /
2021-07-26 15:40:37 +02:00
getUtxo ( respectFrozen = false ) : Utxo [ ] {
let ret : Utxo [ ] = [ ] ;
2020-06-01 14:54:23 +02:00
for ( const u of this . utxo ) {
2020-04-22 17:13:18 +02:00
if ( u . txId ) u . txid = u . txId ;
if ( ! u . confirmations && u . height ) u . confirmations = BlueElectrum . estimateCurrentBlockheight ( ) - u . height ;
ret . push ( u ) ;
}
2021-01-25 16:29:12 +01:00
if ( ret . length === 0 ) {
ret = this . getDerivedUtxoFromOurTransaction ( ) ; // oy vey, no stored utxo. lets attempt to derive it from stored transactions
}
2020-11-22 08:43:11 +01:00
if ( ! respectFrozen ) {
2021-07-26 15:40:37 +02:00
ret = ret . filter ( ( { txid , vout } ) = > ! txid || ! this . getUTXOMetadata ( txid , vout ) . frozen ) ;
2020-10-23 12:27:03 +02:00
}
2020-04-22 17:13:18 +02:00
return ret ;
2020-03-09 19:51:34 +01:00
}
2021-07-26 15:40:37 +02:00
getDerivedUtxoFromOurTransaction ( returnSpentUtxoAsWell = false ) : Utxo [ ] {
const utxos : Utxo [ ] = [ ] ;
2021-01-25 16:29:12 +01:00
2021-07-26 15:40:37 +02:00
const ownedAddressesHashmap : Record < string , boolean > = { } ;
2023-07-25 15:50:04 +02:00
const addrs = this . getAddress ( ) ;
if ( addrs ) ownedAddressesHashmap [ addrs ] = true ;
2021-01-25 16:29:12 +01:00
/ * *
* below copypasted from
* @see AbstractHDElectrumWallet . getDerivedUtxoFromOurTransaction
* /
for ( const tx of this . getTransactions ( ) ) {
for ( const output of tx . outputs ) {
2021-07-26 15:40:37 +02:00
let address : string | false = false ;
2021-01-25 16:29:12 +01:00
if ( output . scriptPubKey && output . scriptPubKey . addresses && output . scriptPubKey . addresses [ 0 ] ) {
address = output . scriptPubKey . addresses [ 0 ] ;
}
2021-07-26 15:40:37 +02:00
if ( address && ownedAddressesHashmap [ address ] ) {
2021-01-25 16:29:12 +01:00
const value = new BigNumber ( output . value ) . multipliedBy ( 100000000 ) . toNumber ( ) ;
utxos . push ( {
txid : tx.txid ,
txId : tx.txid ,
vout : output.n ,
address ,
value ,
amount : value ,
confirmations : tx.confirmations ,
wif : false ,
2021-08-27 17:14:29 +02:00
height : BlueElectrum.estimateCurrentBlockheight ( ) - ( tx . confirmations ? ? 0 ) ,
2021-01-25 16:29:12 +01:00
} ) ;
}
}
}
if ( returnSpentUtxoAsWell ) return utxos ;
// got all utxos we ever had. lets filter out the ones that are spent:
const ret = [ ] ;
for ( const utxo of utxos ) {
let spent = false ;
for ( const tx of this . getTransactions ( ) ) {
for ( const input of tx . inputs ) {
if ( input . txid === utxo . txid && input . vout === utxo . vout ) spent = true ;
// utxo we got previously was actually spent right here ^^
}
}
if ( ! spent ) {
ret . push ( utxo ) ;
}
}
return ret ;
}
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 > }
* /
2021-07-26 15:40:37 +02:00
async fetchTransactions ( ) : Promise < void > {
2020-03-09 19:51:34 +01:00
// Below is a simplified copypaste from HD electrum wallet
2021-07-26 15:40:37 +02:00
const _txsByExternalIndex : Transaction [ ] = [ ] ;
const address = this . getAddress ( ) ;
const addresses2fetch = address ? [ address ] : [ ] ;
2020-03-09 19:51:34 +01:00
// first: batch fetch for all addresses histories
2020-06-01 14:54:23 +02:00
const histories = await BlueElectrum . multiGetHistoryByAddress ( addresses2fetch ) ;
2021-07-26 15:40:37 +02:00
const txs : Record <
string ,
{
2023-07-25 15:50:04 +02:00
tx_hash : string ;
2021-07-26 15:40:37 +02:00
height : number ;
address : string ;
}
> = { } ;
2020-06-01 14:54:23 +02:00
for ( const history of Object . values ( histories ) ) {
for ( const tx of history ) {
2020-03-09 19:51:34 +01:00
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
2021-08-12 11:50:22 +02:00
if ( this . getTransactions ( ) . length === 0 && Object . values ( txs ) . length > 1000 )
throw new Error ( 'Addresses with history of > 1000 transactions are not supported' ) ;
// we check existing transactions, so if there are any then user is just using his wallet and gradually reaching the theshold, which
// is safe because in that case our cache is filled
2020-03-09 19:51:34 +01:00
// next, batch fetching each txid we got
2020-06-01 14:54:23 +02:00
const txdatas = await BlueElectrum . multiGetTransactionByTxid ( Object . keys ( txs ) ) ;
2021-07-26 15:40:37 +02:00
const transactions = Object . values ( txdatas ) ;
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)
2020-06-01 14:54:23 +02:00
const vinTxids = [ ] ;
2021-07-26 15:40:37 +02:00
for ( const txdata of transactions ) {
2020-06-01 14:54:23 +02:00
for ( const vin of txdata . vin ) {
2023-12-24 23:27:50 +01:00
vin . txid && vinTxids . push ( vin . txid ) ;
// ^^^^ not all inputs have txid, some of them are Coinbase (newly-created coins)
2020-03-09 19:51:34 +01:00
}
}
2020-06-01 14:54:23 +02:00
const vintxdatas = await BlueElectrum . multiGetTransactionByTxid ( vinTxids ) ;
2020-03-09 19:51:34 +01:00
// fetched all transactions from our inputs. now we need to combine it.
// iterating all _our_ transactions:
2021-07-26 15:40:37 +02:00
const transactionsWithInputValue = transactions . map ( tx = > {
return {
. . . tx ,
vin : tx.vin.map ( vin = > {
const inpTxid = vin . txid ;
const inpVout = vin . vout ;
// got txid and output number of _previous_ transaction we shoud look into
if ( vintxdatas [ inpTxid ] && vintxdatas [ inpTxid ] . vout [ inpVout ] ) {
return {
. . . vin ,
addresses : vintxdatas [ inpTxid ] . vout [ inpVout ] . scriptPubKey . addresses ,
value : vintxdatas [ inpTxid ] . vout [ inpVout ] . value ,
} ;
} else {
return vin ;
}
} ) ,
} ;
} ) ;
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
2021-07-26 15:40:37 +02:00
for ( const tx of transactionsWithInputValue ) {
2020-06-01 14:54:23 +02:00
for ( const vin of tx . vin ) {
2021-07-26 15:40:37 +02:00
if ( 'addresses' in vin && vin . addresses && vin . addresses . indexOf ( address || '' ) !== - 1 ) {
2020-03-09 19:51:34 +01:00
// this TX is related to our address
2023-07-25 15:50:04 +02:00
const { vin : vin2 , vout , . . . txRest } = tx ;
2021-07-26 15:40:37 +02:00
const clonedTx : Transaction = {
. . . txRest ,
2023-07-25 15:50:04 +02:00
inputs : [ . . . vin2 ] ,
2021-07-26 15:40:37 +02:00
outputs : [ . . . vout ] ,
} ;
2018-10-11 19:25:39 +02:00
2020-12-08 14:00:09 +01:00
_txsByExternalIndex . push ( clonedTx ) ;
2020-03-09 19:51:34 +01:00
}
2018-10-11 19:25:39 +02:00
}
2020-06-01 14:54:23 +02:00
for ( const vout of tx . vout ) {
2021-07-26 15:40:37 +02:00
if ( vout . scriptPubKey . addresses && vout . scriptPubKey . addresses . indexOf ( address || '' ) !== - 1 ) {
2020-03-09 19:51:34 +01:00
// this TX is related to our address
2023-07-25 15:50:04 +02:00
const { vin , vout : vout2 , . . . txRest } = tx ;
2021-07-26 15:40:37 +02:00
const clonedTx : Transaction = {
. . . txRest ,
inputs : [ . . . vin ] ,
2023-07-25 15:50:04 +02:00
outputs : [ . . . vout2 ] ,
2021-07-26 15:40:37 +02:00
} ;
2020-03-09 19:51:34 +01:00
2020-12-08 14:00:09 +01:00
_txsByExternalIndex . push ( clonedTx ) ;
2020-03-09 19:51:34 +01:00
}
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-12-08 14:00:09 +01:00
this . _txs_by_external_index = _txsByExternalIndex ;
2020-03-09 19:51:34 +01:00
this . _lastTxFetch = + new Date ( ) ;
2018-10-11 19:25:39 +02:00
}
2021-07-26 15:40:37 +02:00
getTransactions ( ) : Transaction [ ] {
2020-03-09 19:51:34 +01:00
// 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-06-01 14:54:23 +02:00
const hd = new HDSegwitBech32Wallet ( ) ;
2020-03-09 19:51:34 +01:00
return hd . getTransactions . apply ( this ) ;
2018-10-11 19:25:39 +02:00
}
2020-04-28 18:27:35 +02:00
/ * *
* Broadcast txhex . Can throw an exception if failed
*
* @param { String } txhex
* @returns { Promise < boolean > }
* /
2021-07-26 15:40:37 +02:00
async broadcastTx ( txhex : string ) : Promise < boolean > {
2020-06-01 14:54:23 +02:00
const broadcast = await BlueElectrum . broadcastV2 ( txhex ) ;
2020-04-28 18:27:35 +02:00
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
2018-06-11 21:17:16 +02:00
}
2022-09-03 01:48:07 +02:00
coinselect (
2022-09-10 13:11:28 +02:00
utxos : CoinSelectUtxo [ ] ,
2022-09-03 01:48:07 +02:00
targets : CoinSelectTarget [ ] ,
2021-07-26 15:40:37 +02:00
feeRate : number ,
changeAddress : string ,
) : {
2022-09-11 12:59:00 +02:00
inputs : CoinSelectReturnInput [ ] ;
2022-09-03 01:48:07 +02:00
outputs : CoinSelectOutput [ ] ;
2021-07-26 15:40:37 +02:00
fee : number ;
} {
2020-04-22 17:13:18 +02:00
if ( ! changeAddress ) throw new Error ( 'No change address provided' ) ;
2021-05-10 17:01:28 +02:00
let algo = coinSelect ;
2020-12-24 14:36:28 +01:00
// if targets has output without a value, we want send MAX to it
if ( targets . some ( i = > ! ( 'value' in i ) ) ) {
2020-04-22 17:13:18 +02:00
algo = coinSelectSplit ;
2018-05-20 11:38:50 +02:00
}
2020-04-22 17:13:18 +02:00
2020-06-01 14:54:23 +02:00
const { inputs , outputs , fee } = algo ( utxos , targets , feeRate ) ;
2020-04-22 17:13:18 +02:00
// .inputs and .outputs will be undefined if no solution was found
if ( ! inputs || ! outputs ) {
2020-12-24 14:36:28 +01:00
throw new Error ( 'Not enough balance. Try sending smaller amount or decrease the fee.' ) ;
2020-04-22 17:13:18 +02:00
}
2020-09-14 12:49:08 +02:00
return { inputs , outputs , fee } ;
}
2020-04-22 17:13:18 +02:00
2020-09-14 12:49:08 +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 } }
* /
2022-09-03 01:48:07 +02:00
createTransaction (
utxos : CreateTransactionUtxo [ ] ,
targets : CoinSelectTarget [ ] ,
2021-07-26 15:40:37 +02:00
feeRate : number ,
changeAddress : string ,
sequence : number ,
skipSigning = false ,
masterFingerprint : number ,
2022-09-03 01:48:07 +02:00
) : CreateTransactionResult {
2020-09-14 19:24:27 +02:00
if ( targets . length === 0 ) throw new Error ( 'No destination provided' ) ;
2020-09-14 12:49:08 +02:00
const { inputs , outputs , fee } = this . coinselect ( utxos , targets , feeRate , changeAddress ) ;
sequence = sequence || 0xffffffff ; // disable RBF by default
const psbt = new bitcoin . Psbt ( ) ;
2020-04-22 17:13:18 +02:00
let c = 0 ;
2021-07-26 15:40:37 +02:00
const values : Record < number , number > = { } ;
2021-11-25 16:50:40 +01:00
let keyPair : Signer | null = null ;
2020-04-22 17:13:18 +02:00
inputs . forEach ( input = > {
if ( ! skipSigning ) {
// skiping signing related stuff
2021-11-25 16:50:40 +01:00
keyPair = ECPair . fromWIF ( this . secret ) ; // secret is WIF
2020-04-22 17:13:18 +02:00
}
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' ) ,
} ) ;
} ) ;
2021-07-26 15:40:37 +02:00
const sanitizedOutputs = outputs . map ( output = > ( {
. . . output ,
2020-04-22 17:13:18 +02:00
// if output has no address - this is change output
2021-07-26 15:40:37 +02:00
address : output.address ? ? changeAddress ,
} ) ) ;
2020-04-22 17:13:18 +02:00
2021-07-26 15:40:37 +02:00
sanitizedOutputs . forEach ( output = > {
2020-06-01 14:54:23 +02:00
const outputData = {
2020-04-22 17:13:18 +02:00
address : output.address ,
value : output.value ,
} ;
psbt . addOutput ( outputData ) ;
} ) ;
2021-07-26 15:40:37 +02:00
if ( ! skipSigning && keyPair ) {
2020-04-22 17:13:18 +02:00
// skiping signing related stuff
for ( let cc = 0 ; cc < c ; cc ++ ) {
psbt . signInput ( cc , keyPair ) ;
}
}
let tx ;
if ( ! skipSigning ) {
tx = psbt . finalizeAllInputs ( ) . extractTransaction ( ) ;
}
2021-07-26 15:40:37 +02:00
return { tx , inputs , outputs : sanitizedOutputs , fee , psbt } ;
2018-05-20 11:38:50 +02:00
}
2018-06-17 12:46:44 +02:00
2021-07-26 15:40:37 +02:00
getLatestTransactionTime ( ) : string | 0 {
2018-09-01 01:28:19 +02:00
if ( this . getTransactions ( ) . length === 0 ) {
return 0 ;
}
let max = 0 ;
2020-06-01 14:54:23 +02:00
for ( const tx of this . getTransactions ( ) ) {
2021-07-26 15:40:37 +02:00
max = Math . max ( new Date ( tx . received ? ? 0 ) . getTime ( ) , 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
*
2021-12-05 14:24:48 +01:00
* p2tr addresses have extra logic , rejecting all versions > 1
* @see https : //github.com/BlueWallet/BlueWallet/issues/3394
* @see https : //github.com/bitcoinjs/bitcoinjs-lib/issues/1750
* @see https : //github.com/bitcoin/bips/blob/edffe529056f6dfd33d8f716fb871467c3c09263/bip-0350.mediawiki#Addresses_for_segregated_witness_outputs
*
2020-03-09 19:51:34 +01:00
* @param address
* @returns { boolean }
* /
2021-07-26 15:40:37 +02:00
isAddressValid ( address : string ) : boolean {
2018-07-12 01:38:24 +02:00
try {
2021-12-05 14:24:48 +01:00
bitcoin . address . toOutputScript ( address ) ; // throws, no?
if ( ! address . toLowerCase ( ) . startsWith ( 'bc1' ) ) return true ;
const decoded = bitcoin . address . fromBech32 ( address ) ;
2021-12-05 19:46:42 +01:00
if ( decoded . version === 0 ) return true ;
if ( decoded . version === 1 && decoded . data . length !== 32 ) return false ;
if ( decoded . version === 1 && ! ecc . isPoint ( Buffer . concat ( [ Buffer . from ( [ 2 ] ) , decoded . data ] ) ) ) return false ;
if ( decoded . version > 1 ) return false ;
2021-12-05 14:24:48 +01:00
// ^^^ some day, when versions above 1 will be actually utilized, we would need to unhardcode this
2021-12-05 19:46:42 +01:00
return true ;
2018-07-12 01:38:24 +02:00
} 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
* /
2021-07-26 15:40:37 +02:00
static scriptPubKeyToAddress ( scriptPubKey : string ) : string | false {
2020-03-23 17:32:51 +01:00
try {
2021-02-24 19:21:37 +01:00
const scriptPubKey2 = Buffer . from ( scriptPubKey , 'hex' ) ;
2021-07-26 15:40:37 +02:00
return (
bitcoin . payments . p2pkh ( {
output : scriptPubKey2 ,
network : bitcoin.networks.bitcoin ,
} ) . address ? ? false
) ;
2020-03-23 17:32:51 +01:00
} catch ( _ ) {
return false ;
}
}
2021-07-26 15:40:37 +02:00
weOwnAddress ( address : string ) : boolean {
2021-05-20 10:49:40 +02:00
if ( ! address ) return false ;
2021-05-19 17:52:56 +02:00
let cleanAddress = address ;
if ( this . segwitType === 'p2wpkh' ) {
cleanAddress = address . toLowerCase ( ) ;
}
return this . getAddress ( ) === cleanAddress || this . _address === cleanAddress ;
2020-03-09 19:51:34 +01:00
}
2020-04-22 17:13:18 +02:00
2021-07-26 15:40:37 +02:00
weOwnTransaction ( txid : string ) : boolean {
2020-08-10 16:17:50 +02:00
for ( const tx of this . getTransactions ( ) ) {
if ( tx && tx . txid && tx . txid === txid ) return true ;
}
return false ;
}
2021-07-26 15:40:37 +02:00
allowSignVerifyMessage ( ) : boolean {
2021-03-09 12:26:56 +01:00
return true ;
}
2020-11-22 09:19:52 +01:00
/ * *
* Check if address is a Change address . Needed for Coin control .
* Useless for Legacy wallets , so it is always false
*
* @param address
* @returns { Boolean } Either address is a change or not
* /
2021-07-26 15:40:37 +02:00
addressIsChange ( address : string ) : boolean {
2020-11-09 11:32:51 +01:00
return false ;
2020-10-29 19:42:40 +01:00
}
2021-03-09 12:26:56 +01:00
2021-03-11 11:45:49 +01:00
/ * *
* Finds WIF corresponding to address and returns it
*
* @param address { string } Address that belongs to this wallet
2022-09-03 01:48:07 +02:00
* @returns { string | false } WIF or false
2021-03-11 11:45:49 +01:00
* /
2022-09-03 01:48:07 +02:00
_getWIFbyAddress ( address : string ) : string | false {
return this . getAddress ( ) === address ? this . secret : false ;
2021-03-11 11:45:49 +01:00
}
2021-03-09 12:26:56 +01:00
2021-03-11 11:45:49 +01:00
/ * *
* Signes text message using address private key and returs signature
*
* @param message { string }
* @param address { string }
* @returns { string } base64 encoded signature
* /
2021-07-26 15:40:37 +02:00
signMessage ( message : string , address : string , useSegwit = true ) : string {
2021-03-11 11:45:49 +01:00
const wif = this . _getWIFbyAddress ( address ) ;
2022-09-03 01:48:07 +02:00
if ( ! wif ) throw new Error ( 'Invalid address' ) ;
2021-11-25 16:50:40 +01:00
const keyPair = ECPair . fromWIF ( wif ) ;
2021-03-09 12:26:56 +01:00
const privateKey = keyPair . privateKey ;
2021-07-26 15:40:37 +02:00
if ( ! privateKey ) throw new Error ( 'Invalid private key' ) ;
2021-04-15 19:46:10 +02:00
const options = this . segwitType && useSegwit ? { segwitType : this.segwitType } : undefined ;
2021-03-09 12:26:56 +01:00
const signature = bitcoinMessage . sign ( message , privateKey , keyPair . compressed , options ) ;
return signature . toString ( 'base64' ) ;
}
2021-03-11 11:45:49 +01:00
/ * *
* Verifies text message signature by address
*
* @param message { string }
* @param address { string }
* @param signature { string }
* @returns { boolean } base64 encoded signature
* /
2021-07-26 15:40:37 +02:00
verifyMessage ( message : string , address : string , signature : string ) : boolean {
2021-10-13 15:47:39 +02:00
// undefined, true so it can verify Electrum signatures without errors
try {
return bitcoinMessage . verify ( message , address , signature , undefined , true ) ;
2022-01-27 17:20:25 +01:00
} catch ( e : any ) {
2021-10-13 15:47:39 +02:00
if ( e . message === 'checkSegwitAlways can only be used with a compressed pubkey signature flagbyte' ) {
// If message created with uncompressed private key, it will throw this error
// in this case we should re-try with checkSegwitAlways flag off
// node_modules/bitcoinjs-message/index.js:187
return bitcoinMessage . verify ( message , address , signature ) ;
}
throw e ;
}
2021-03-09 12:26:56 +01:00
}
2021-03-25 16:28:25 +01:00
/ * *
* Probes address for transactions , if there are any returns TRUE
*
* @returns { Promise < boolean > }
* /
2021-07-26 15:40:37 +02:00
async wasEverUsed ( ) : Promise < boolean > {
const address = this . getAddress ( ) ;
if ( ! address ) return Promise . resolve ( false ) ;
const txs = await BlueElectrum . getTransactionsByAddress ( address ) ;
2021-03-25 16:28:25 +01:00
return txs . length > 0 ;
}
2018-03-20 21:41:07 +01:00
}