diff --git a/App.js b/App.js index f80b99c4..4d441fa7 100644 --- a/App.js +++ b/App.js @@ -10,7 +10,7 @@ import { Chain } from './models/bitcoinUnits'; import QuickActions from 'react-native-quick-actions'; import * as Sentry from '@sentry/react-native'; import OnAppLaunch from './class/onAppLaunch'; -import DeeplinkSchemaMatch from './class/deeplinkSchemaMatch'; +import DeeplinkSchemaMatch from './class/deeplink-schema-match'; import BitcoinBIP70TransactionDecode from './bip70/bip70'; const A = require('./analytics'); diff --git a/BlueComponents.js b/BlueComponents.js index 5ae93d2d..9550b8c4 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -313,6 +313,7 @@ export class BlueWalletNavigationHeader extends Component { ) : ( @@ -954,7 +954,7 @@ export class BlueDoneAndDismissKeyboardInputAccessory extends Component { if (Platform.OS === 'ios') { return {inputView}; } else { - return {inputView}; + return {inputView}; } } } @@ -1257,7 +1257,7 @@ export class BlueReceiveButtonIcon extends Component { export class BlueSendButtonIcon extends Component { render() { return ( - + { this.props.onChangeText(text); }} @@ -2246,6 +2247,7 @@ export class BlueBitcoinAmount extends Component { { text = text.trim(); @@ -2315,3 +2317,22 @@ const styles = StyleSheet.create({ marginRight: 16, }, }); + +export function BlueBigCheckmark({ style }) { + const defaultStyles = { + backgroundColor: '#ccddf9', + width: 120, + height: 120, + borderRadius: 60, + alignSelf: 'center', + justifyContent: 'center', + marginTop: 0, + marginBottom: 0, + }; + const mergedStyles = { ...defaultStyles, ...style }; + return ( + + + + ); +} diff --git a/BlueElectrum.js b/BlueElectrum.js index 497c3668..fa3ae605 100644 --- a/BlueElectrum.js +++ b/BlueElectrum.js @@ -466,9 +466,9 @@ module.exports.broadcastV2 = async function(hex) { }; module.exports.estimateCurrentBlockheight = function() { - const baseTs = 1585837504347; // uS - const baseHeight = 624197; - return Math.floor(baseHeight + (+new Date() - baseTs) / 1000 / 60 / 10); + const baseTs = 1587570465609; // uS + const baseHeight = 627179; + return Math.floor(baseHeight + (+new Date() - baseTs) / 1000 / 60 / 9.5); }; /** diff --git a/MainBottomTabs.js b/MainBottomTabs.js index ec3969a4..57c8a00c 100644 --- a/MainBottomTabs.js +++ b/MainBottomTabs.js @@ -44,6 +44,7 @@ import sendCreate from './screen/send/create'; import Confirm from './screen/send/confirm'; import PsbtWithHardwareWallet from './screen/send/psbtWithHardwareWallet'; import Success from './screen/send/success'; +import Broadcast from './screen/send/broadcast'; import ScanLndInvoice from './screen/lnd/scanLndInvoice'; import LappBrowser from './screen/lnd/browser'; @@ -199,6 +200,9 @@ const CreateTransactionStackNavigator = createStackNavigator({ headerRight: null, }, }, + Broadcast: { + screen: Broadcast, + }, }); const LNDCreateInvoiceStackNavigator = createStackNavigator({ @@ -325,12 +329,17 @@ const MainBottomTabs = createStackNavigator( }, }, - // - ReceiveDetails: { screen: receiveDetails, }, + Broadcast: { + screen: Broadcast, + navigationOptions: () => ({ + title: 'Broadcast tx', + }), + }, + // // LND: diff --git a/android/app/build.gradle b/android/app/build.gradle index b99f90ea..4c1d7b6f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -140,7 +140,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "5.3.3" + versionName "5.3.4" multiDexEnabled true missingDimensionStrategy 'react-native-camera', 'general' testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type diff --git a/class/abstract-hd-electrum-wallet.js b/class/abstract-hd-electrum-wallet.js index b9717b8f..3742327d 100644 --- a/class/abstract-hd-electrum-wallet.js +++ b/class/abstract-hd-electrum-wallet.js @@ -56,6 +56,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { return false; } + /** + * + * @inheritDoc + */ getUnconfirmedBalance() { let ret = 0; for (let bal of Object.values(this._balances_by_external_index)) { @@ -751,8 +755,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { return ret; } - _getDerivationPathByAddress(address) { - const path = "m/84'/0'/0'"; + _getDerivationPathByAddress(address, BIP = 84) { + const path = `m/${BIP}'/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; } @@ -763,6 +767,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { return false; } + /** + * + * @param address {string} Address that belongs to this wallet + * @returns {Buffer|boolean} Either buffer with pubkey or 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); @@ -784,13 +793,6 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { 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 @@ -838,7 +840,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { // 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 masterFingerprintBuffer; if (masterFingerprint) { let masterFingerprintHex = Number(masterFingerprint).toString(16); @@ -850,24 +852,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { } // 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: masterFingerprintBuffer, - path, - pubkey, - }, - ], - witnessUtxo: { - script: p2wpkh.output, - value: input.value, - }, - }); + + psbt = this._addPsbtInput(psbt, input, sequence, masterFingerprintBuffer); }); outputs.forEach(output => { @@ -926,6 +912,31 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { return { tx, inputs, outputs, fee, psbt }; } + _addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) { + const pubkey = this._getPubkeyByAddress(input.address); + const path = this._getDerivationPathByAddress(input.address); + const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); + + psbt.addInput({ + hash: input.txId, + index: input.vout, + sequence, + bip32Derivation: [ + { + masterFingerprint: masterFingerprintBuffer, + path, + pubkey, + }, + ], + witnessUtxo: { + script: p2wpkh.output, + value: input.value, + }, + }); + + return psbt; + } + /** * Combines 2 PSBTs into final transaction from which you can * get HEX and broadcast @@ -977,19 +988,6 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet { return txs; } - /** - * Broadcast txhex. Can throw an exception if failed - * - * @param {String} txhex - * @returns {Promise} - */ - 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 - } - /** * Probes zero address in external hierarchy for transactions, if there are any returns TRUE. * Zero address is a pretty good indicator, since its a first one to fund the wallet. How can you use the wallet and diff --git a/class/abstract-hd-wallet.js b/class/abstract-hd-wallet.js index 0acf1437..bf812d5f 100644 --- a/class/abstract-hd-wallet.js +++ b/class/abstract-hd-wallet.js @@ -1,6 +1,5 @@ import { LegacyWallet } from './legacy-wallet'; import Frisbee from 'frisbee'; -const bitcoin = require('bitcoinjs-lib'); const bip39 = require('bip39'); const BlueElectrum = require('../BlueElectrum'); @@ -42,56 +41,7 @@ export class AbstractHDWallet extends LegacyWallet { } getTransactions() { - // need to reformat txs, as we are expected to return them in blockcypher format, - // but they are from blockchain.info actually (for all hd wallets) - - let uniq = {}; - let txs = []; - for (let tx of this.transactions) { - if (uniq[tx.hash]) continue; - uniq[tx.hash] = 1; - txs.push(AbstractHDWallet.convertTx(tx)); - } - - return txs; - } - - static convertTx(tx) { - // console.log('converting', tx); - var clone = Object.assign({}, tx); - clone.received = new Date(clone.time * 1000).toISOString(); - clone.outputs = clone.out; - if (clone.confirmations === undefined) { - clone.confirmations = 0; - } - for (let o of clone.outputs) { - o.addresses = [o.addr]; - } - for (let i of clone.inputs) { - if (i.prev_out && i.prev_out.addr) { - i.addresses = [i.prev_out.addr]; - } - } - - if (!clone.value) { - let value = 0; - for (let inp of clone.inputs) { - if (inp.prev_out && inp.prev_out.xpub) { - // our owned - value -= inp.prev_out.value; - } - } - - for (let out of clone.out) { - if (out.xpub) { - // to us - value += out.value; - } - } - clone.value = value; - } - - return clone; + throw new Error('Not implemented'); } setSecret(newSecret) { @@ -362,202 +312,15 @@ export class AbstractHDWallet extends LegacyWallet { throw new Error('Could not find WIF for ' + address); } - createTx() { - throw new Error('Not implemented'); - } - async fetchBalance() { - try { - let that = this; - - // refactor me - // eslint-disable-next-line - async function binarySearchIterationForInternalAddress(index, maxUsedIndex = 0, minUnusedIndex = 100500100, depth = 0) { - if (depth >= 20) return maxUsedIndex + 1; // fail - let txs = await BlueElectrum.getTransactionsByAddress(that._getInternalAddressByIndex(index)); - if (txs.length === 0) { - if (index === 0) return 0; - minUnusedIndex = Math.min(minUnusedIndex, index); // set - index = Math.floor((index - maxUsedIndex) / 2 + maxUsedIndex); - } else { - maxUsedIndex = Math.max(maxUsedIndex, index); // set - let txs2 = await BlueElectrum.getTransactionsByAddress(that._getInternalAddressByIndex(index + 1)); - if (txs2.length === 0) return index + 1; // thats our next free address - - index = Math.round((minUnusedIndex - index) / 2 + index); - } - - return binarySearchIterationForInternalAddress(index, maxUsedIndex, minUnusedIndex, depth + 1); - } - - // refactor me - // eslint-disable-next-line - async function binarySearchIterationForExternalAddress(index, maxUsedIndex = 0, minUnusedIndex = 100500100, depth = 0) { - if (depth >= 20) return maxUsedIndex + 1; // fail - let txs = await BlueElectrum.getTransactionsByAddress(that._getExternalAddressByIndex(index)); - if (txs.length === 0) { - if (index === 0) return 0; - minUnusedIndex = Math.min(minUnusedIndex, index); // set - index = Math.floor((index - maxUsedIndex) / 2 + maxUsedIndex); - } else { - maxUsedIndex = Math.max(maxUsedIndex, index); // set - let txs2 = await BlueElectrum.getTransactionsByAddress(that._getExternalAddressByIndex(index + 1)); - if (txs2.length === 0) return index + 1; // thats our next free address - - index = Math.round((minUnusedIndex - index) / 2 + index); - } - - return binarySearchIterationForExternalAddress(index, maxUsedIndex, minUnusedIndex, depth + 1); - } - - if (this.next_free_change_address_index === 0 && this.next_free_address_index === 0) { - // assuming that this is freshly imported/created wallet, with no internal variables set - // wild guess - its completely empty wallet: - let completelyEmptyWallet = false; - let txs = await BlueElectrum.getTransactionsByAddress(that._getInternalAddressByIndex(0)); - if (txs.length === 0) { - let txs2 = await BlueElectrum.getTransactionsByAddress(that._getExternalAddressByIndex(0)); - if (txs2.length === 0) { - // yep, completely empty wallet - completelyEmptyWallet = true; - } - } - - // wrong guess. will have to rescan - if (!completelyEmptyWallet) { - // so doing binary search for last used address: - this.next_free_change_address_index = await binarySearchIterationForInternalAddress(1000); - this.next_free_address_index = await 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 - - if (tryAgain) return this._fetchBalance(); - - // next, business as usuall. fetch balances - - this.usedAddresses = []; - // generating all involved addresses: - for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { - this.usedAddresses.push(this._getExternalAddressByIndex(c)); - } - for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { - this.usedAddresses.push(this._getInternalAddressByIndex(c)); - } - let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses); - this.balance = balance.balance; - this.unconfirmed_balance = balance.unconfirmed_balance; - this._lastBalanceFetch = +new Date(); - } - - async _fetchUtxoBatch(addresses) { - const api = new Frisbee({ - baseURI: 'https://blockchain.info', - }); - - addresses = addresses.join('|'); - let utxos = []; - - let response; - let uri; - try { - uri = 'https://blockchain.info' + '/unspent?active=' + addresses + '&limit=1000'; - response = await api.get('/unspent?active=' + addresses + '&limit=1000'); - // this endpoint does not support offset of some kind o_O - // so doing only one call - let json = response.body; - if (typeof json === 'undefined' || typeof json.unspent_outputs === 'undefined') { - throw new Error('Could not fetch UTXO from API ' + response.err); - } - - for (let unspent of json.unspent_outputs) { - // a lil transform for signer module - unspent.txid = unspent.tx_hash_big_endian; - unspent.vout = unspent.tx_output_n; - unspent.amount = unspent.value; - - unspent.address = bitcoin.address.fromOutputScript(Buffer.from(unspent.script, 'hex')); - utxos.push(unspent); - } - } catch (err) { - console.warn(err, { uri }); - } - - return utxos; + throw new Error('Not implemented'); } /** * @inheritDoc */ async fetchUtxo() { - if (this.usedAddresses.length === 0) { - // just for any case, refresh balance (it refreshes internal `this.usedAddresses`) - await this.fetchBalance(); - } - - this.utxo = []; - let addresses = this.usedAddresses; - addresses.push(this._getExternalAddressByIndex(this.next_free_address_index)); - addresses.push(this._getInternalAddressByIndex(this.next_free_change_address_index)); - - let duplicateUtxos = {}; - - let batch = []; - for (let addr of addresses) { - batch.push(addr); - if (batch.length >= 75) { - let utxos = await this._fetchUtxoBatch(batch); - for (let utxo of utxos) { - let key = utxo.txid + utxo.vout; - if (!duplicateUtxos[key]) { - this.utxo.push(utxo); - duplicateUtxos[key] = 1; - } - } - batch = []; - } - } - - // final batch - if (batch.length > 0) { - let utxos = await this._fetchUtxoBatch(batch); - for (let utxo of utxos) { - let key = utxo.txid + utxo.vout; - if (!duplicateUtxos[key]) { - this.utxo.push(utxo); - duplicateUtxos[key] = 1; - } - } - } + throw new Error('Not implemented'); } weOwnAddress(addr) { diff --git a/class/abstract-wallet.js b/class/abstract-wallet.js index cf5e53e2..d20a3448 100644 --- a/class/abstract-wallet.js +++ b/class/abstract-wallet.js @@ -72,7 +72,7 @@ export class AbstractWallet { * @returns {number} Available to spend amount, int, in sats */ getBalance() { - return this.balance; + return this.balance + (this.getUnconfirmedBalance() < 0 ? this.getUnconfirmedBalance() : 0); } getPreferredBalanceUnit() { @@ -116,7 +116,7 @@ export class AbstractWallet { * Returns delta of unconfirmed balance. For example, if theres no * unconfirmed balance its 0 * - * @return {number} + * @return {number} Satoshis */ getUnconfirmedBalance() { return this.unconfirmed_balance; @@ -158,7 +158,27 @@ export class AbstractWallet { return 0; } - // createTx () { throw Error('not implemented') } + /** + * @deprecated + */ + createTx() { + throw Error('not implemented'); + } + + /** + * + * @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 + * @param masterFingerprint {number} Decimal number of wallet's master fingerprint + * @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}} + */ + createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) { + throw Error('not implemented'); + } getAddress() { throw Error('not implemented'); diff --git a/class/app-storage.js b/class/app-storage.js index d168e9f6..e840c44c 100644 --- a/class/app-storage.js +++ b/class/app-storage.js @@ -11,6 +11,7 @@ import { HDSegwitBech32Wallet, PlaceholderWallet, LightningCustodianWallet, + HDLegacyElectrumSeedP2PKHWallet, } from './'; import WatchConnectivity from '../WatchConnectivity'; import DeviceQuickActions from './quickActions'; @@ -262,6 +263,9 @@ export class AppStorage { case HDLegacyBreadwalletWallet.type: unserializedWallet = HDLegacyBreadwalletWallet.fromJson(key); break; + case HDLegacyElectrumSeedP2PKHWallet.type: + unserializedWallet = HDLegacyElectrumSeedP2PKHWallet.fromJson(key); + break; case LightningCustodianWallet.type: /** @type {LightningCustodianWallet} */ unserializedWallet = LightningCustodianWallet.fromJson(key); diff --git a/class/deeplinkSchemaMatch.js b/class/deeplink-schema-match.js similarity index 94% rename from class/deeplinkSchemaMatch.js rename to class/deeplink-schema-match.js index 5cbbd405..597bfb3d 100644 --- a/class/deeplinkSchemaMatch.js +++ b/class/deeplink-schema-match.js @@ -5,6 +5,7 @@ import RNFS from 'react-native-fs'; import url from 'url'; import { Chain } from '../models/bitcoinUnits'; const bitcoin = require('bitcoinjs-lib'); +const bip21 = require('bip21'); const BlueApp: AppStorage = require('../BlueApp'); class DeeplinkSchemaMatch { @@ -194,6 +195,7 @@ class DeeplinkSchemaMatch { static isBitcoinAddress(address) { address = address .replace('bitcoin:', '') + .replace('BITCOIN:', '') .replace('bitcoin=', '') .split('?')[0]; let isValidBitcoinAddress = false; @@ -228,14 +230,14 @@ class DeeplinkSchemaMatch { } static isBothBitcoinAndLightning(url) { - if (url.includes('lightning') && url.includes('bitcoin')) { - const txInfo = url.split(/(bitcoin:|lightning:|lightning=|bitcoin=)+/); + if (url.includes('lightning') && (url.includes('bitcoin') || url.includes('BITCOIN'))) { + const txInfo = url.split(/(bitcoin:|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')) { + if (value.startsWith('bitcoin') || value.startsWith('BITCOIN')) { bitcoin = `bitcoin:${txInfo[index + 1]}`; if (!DeeplinkSchemaMatch.isBitcoinAddress(bitcoin)) { bitcoin = false; @@ -261,6 +263,14 @@ class DeeplinkSchemaMatch { } return undefined; } + + static bip21decode(uri) { + return bip21.decode(uri.replace('BITCOIN:', 'bitcoin:')); + } + + static bip21encode() { + return bip21.encode.apply(bip21, arguments); + } } export default DeeplinkSchemaMatch; diff --git a/class/hd-legacy-breadwallet-wallet.js b/class/hd-legacy-breadwallet-wallet.js index 779fbfbb..584847fd 100644 --- a/class/hd-legacy-breadwallet-wallet.js +++ b/class/hd-legacy-breadwallet-wallet.js @@ -1,5 +1,5 @@ -import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet'; import bip39 from 'bip39'; +import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet'; const bip32 = require('bip32'); const bitcoinjs = require('bitcoinjs-lib'); @@ -7,7 +7,7 @@ const bitcoinjs = require('bitcoinjs-lib'); * HD Wallet (BIP39). * In particular, Breadwallet-compatible (Legacy addresses) */ -export class HDLegacyBreadwalletWallet extends AbstractHDElectrumWallet { +export class HDLegacyBreadwalletWallet extends HDLegacyP2PKHWallet { static type = 'HDLegacyBreadwallet'; static typeReadable = 'HD Legacy Breadwallet (P2PKH)'; @@ -77,6 +77,10 @@ export class HDLegacyBreadwalletWallet extends AbstractHDElectrumWallet { const path = `m/0'/${internal ? 1 : 0}/${index}`; const child = root.derivePath(path); - return child.keyPair.toWIF(); + return child.toWIF(); + } + + allowSendMax() { + return true; } } diff --git a/class/hd-legacy-electrum-seed-p2pkh-wallet.js b/class/hd-legacy-electrum-seed-p2pkh-wallet.js index 789b15c7..5863c3a9 100644 --- a/class/hd-legacy-electrum-seed-p2pkh-wallet.js +++ b/class/hd-legacy-electrum-seed-p2pkh-wallet.js @@ -2,6 +2,7 @@ import { HDLegacyP2PKHWallet } from './'; const bitcoin = require('bitcoinjs-lib'); const mn = require('electrum-mnemonic'); +const HDNode = require('bip32'); /** * ElectrumSeed means that instead of BIP39 seed format it works with the format invented by Electrum wallet. Otherwise @@ -22,6 +23,10 @@ export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet { } } + async generate() { + throw new Error('Not implemented'); + } + getXpub() { if (this._xpub) { return this._xpub; // cache hit @@ -62,4 +67,32 @@ export class HDLegacyElectrumSeedP2PKHWallet extends HDLegacyP2PKHWallet { return child.toWIF(); } + + allowSendMax() { + return true; + } + + _getNodePubkeyByIndex(node, index) { + index = index * 1; // cast to int + + if (node === 0 && !this._node0) { + const xpub = this.getXpub(); + const hdNode = HDNode.fromBase58(xpub); + this._node0 = hdNode.derive(node); + } + + if (node === 1 && !this._node1) { + const xpub = 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; + } + } } diff --git a/class/hd-legacy-p2pkh-wallet.js b/class/hd-legacy-p2pkh-wallet.js index c5dd7bde..7e244e44 100644 --- a/class/hd-legacy-p2pkh-wallet.js +++ b/class/hd-legacy-p2pkh-wallet.js @@ -1,9 +1,8 @@ import bip39 from 'bip39'; -import BigNumber from 'bignumber.js'; -import signer from '../models/signer'; import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet'; const bitcoin = require('bitcoinjs-lib'); const HDNode = require('bip32'); +const BlueElectrum = require('../BlueElectrum'); /** * HD Wallet (BIP39). @@ -83,18 +82,49 @@ export class HDLegacyP2PKHWallet extends AbstractHDElectrumWallet { return (this.internal_addresses_cache[index] = address); } - createTx(utxos, amount, fee, address) { - for (let utxo of utxos) { - utxo.wif = this._getWifForAddress(utxo.address); + async fetchUtxo() { + await super.fetchUtxo(); + // now we need to fetch txhash for each input as required by PSBT + let txhexes = await BlueElectrum.multiGetTransactionByTxid( + this.getUtxo().map(x => x['txid']), + 50, + false, + ); + + let newUtxos = []; + for (let u of this.getUtxo()) { + if (txhexes[u.txid]) u.txhex = txhexes[u.txid]; + newUtxos.push(u); } - let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); - return signer.createHDTransaction( - utxos, - address, - amountPlusFee, - fee, - this._getInternalAddressByIndex(this.next_free_change_address_index), - ); + return newUtxos; + } + + _addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) { + const pubkey = this._getPubkeyByAddress(input.address); + const path = this._getDerivationPathByAddress(input.address, 44); + + 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, + bip32Derivation: [ + { + masterFingerprint: masterFingerprintBuffer, + path, + pubkey, + }, + ], + // non-segwit inputs now require passing the whole previous tx as Buffer + nonWitnessUtxo: Buffer.from(input.txhex, 'hex'), + }); + + return psbt; + } + + allowSendMax() { + return true; } } diff --git a/class/hd-segwit-p2sh-wallet.js b/class/hd-segwit-p2sh-wallet.js index 32b32e8e..52b04829 100644 --- a/class/hd-segwit-p2sh-wallet.js +++ b/class/hd-segwit-p2sh-wallet.js @@ -1,8 +1,5 @@ import bip39 from 'bip39'; -import BigNumber from 'bignumber.js'; import b58 from 'bs58check'; -import signer from '../models/signer'; -import { BitcoinUnit } from '../models/bitcoinUnits'; import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet'; const bitcoin = require('bitcoinjs-lib'); const HDNode = require('bip32'); @@ -97,36 +94,31 @@ export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet { return this._xpub; } - /** - * - * @param utxos - * @param amount Either float (BTC) or string 'MAX' (BitcoinUnit.MAX) to send all - * @param fee - * @param address - * @returns {string} - */ - createTx(utxos, amount, fee, address) { - for (let utxo of utxos) { - utxo.wif = this._getWifForAddress(utxo.address); - } - - let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); - - if (amount === BitcoinUnit.MAX) { - amountPlusFee = new BigNumber(0); - for (let utxo of utxos) { - amountPlusFee = amountPlusFee.plus(utxo.amount); - } - amountPlusFee = amountPlusFee.dividedBy(100000000).toString(10); - } + _addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) { + const pubkey = this._getPubkeyByAddress(input.address); + const path = this._getDerivationPathByAddress(input.address, 49); + const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); + let p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh }); + + psbt.addInput({ + hash: input.txid, + index: input.vout, + sequence, + bip32Derivation: [ + { + masterFingerprint: masterFingerprintBuffer, + path, + pubkey, + }, + ], + witnessUtxo: { + script: p2sh.output, + value: input.amount || input.value, + }, + redeemScript: p2wpkh.output, + }); - return signer.createHDSegwitTransaction( - utxos, - address, - amountPlusFee, - fee, - this._getInternalAddressByIndex(this.next_free_change_address_index), - ); + return psbt; } /** diff --git a/class/index.js b/class/index.js index 45793cd1..3a3a714e 100644 --- a/class/index.js +++ b/class/index.js @@ -2,7 +2,7 @@ export * from './abstract-wallet'; export * from './app-storage'; export * from './constants'; export * from './legacy-wallet'; -export * from './segwit-bech-wallet'; +export * from './segwit-bech32-wallet'; export * from './segwit-p2sh-wallet'; export * from './hd-segwit-p2sh-wallet'; export * from './hd-legacy-breadwallet-wallet'; diff --git a/class/legacy-wallet.js b/class/legacy-wallet.js index b2ba9108..0eb61b27 100644 --- a/class/legacy-wallet.js +++ b/class/legacy-wallet.js @@ -1,11 +1,12 @@ import { AbstractWallet } from './abstract-wallet'; import { HDSegwitBech32Wallet } from './'; import { NativeModules } from 'react-native'; + const bitcoin = require('bitcoinjs-lib'); const { RNRandomBytes } = NativeModules; -const BigNumber = require('bignumber.js'); -const signer = require('../models/signer'); const BlueElectrum = require('../BlueElectrum'); +const coinSelectAccumulative = require('coinselect/accumulative'); +const coinSelectSplit = require('coinselect/split'); /** * Has private key and single address like "1ABCD....." @@ -104,8 +105,7 @@ export class LegacyWallet extends AbstractWallet { try { let balance = await BlueElectrum.getBalanceByAddress(this.getAddress()); this.balance = Number(balance.confirmed); - this.unconfirmed_balance = new BigNumber(balance.unconfirmed); - this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1; // wtf + this.unconfirmed_balance = Number(balance.unconfirmed); this._lastBalanceFetch = +new Date(); } catch (Error) { console.warn(Error); @@ -124,20 +124,35 @@ export class LegacyWallet extends AbstractWallet { for (let arr of Object.values(utxos)) { this.utxo = this.utxo.concat(arr); } + + // 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; } catch (Error) { console.warn(Error); } - - // backward compatibility - for (let u of this.utxo) { - u.tx_output_n = u.vout; - u.tx_hash = u.txId; - u.confirmations = u.height ? 1 : 0; - } } getUtxo() { - return this.utxo; + 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; } /** @@ -231,39 +246,98 @@ export class LegacyWallet extends AbstractWallet { return hd.getTransactions.apply(this); } + /** + * Broadcast txhex. Can throw an exception if failed + * + * @param {String} txhex + * @returns {Promise} + */ async broadcastTx(txhex) { - try { - const broadcast = await BlueElectrum.broadcast(txhex); - return broadcast; - } catch (error) { - return error; - } + 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 } /** - * Takes UTXOs, transforms them into - * format expected by signer module, creates tx and returns signed string txhex. * - * @param utxos Unspent outputs, expects blockcypher format - * @param amount - * @param fee - * @param toAddress - * @param memo - * @return string Signed txhex ready for broadcast + * @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}} */ - createTx(utxos, amount, fee, toAddress, memo) { - // transforming UTXOs fields to how module expects it - for (let u of utxos) { - u.confirmations = 6; // hack to make module accept 0 confirmations - u.txid = u.tx_hash; - u.vout = u.tx_output_n; - u.amount = new BigNumber(u.value); - u.amount = u.amount.dividedBy(100000000); - u.amount = u.amount.toString(10); + 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; } - // console.log('creating legacy tx ', amount, ' with fee ', fee, 'secret=', this.getSecret(), 'from address', this.getAddress()); - let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); - return signer.createTransaction(utxos, toAddress, amountPlusFee, fee, this.getSecret(), this.getAddress()); + + 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 }; } getLatestTransactionTime() { @@ -316,4 +390,14 @@ export class LegacyWallet extends AbstractWallet { weOwnAddress(address) { return this.getAddress() === address || this._address === address; } + + allowSendMax() { + return true; + } + + async getChangeAddressAsync() { + return new Promise(resolve => { + resolve(this.getAddress()); + }); + } } diff --git a/class/segwit-bech-wallet.js b/class/segwit-bech-wallet.js deleted file mode 100644 index a9a733fa..00000000 --- a/class/segwit-bech-wallet.js +++ /dev/null @@ -1,59 +0,0 @@ -import { LegacyWallet } from './legacy-wallet'; -const bitcoin = require('bitcoinjs-lib'); - -export class SegwitBech32Wallet extends LegacyWallet { - static type = 'segwitBech32'; - static typeReadable = 'P2 WPKH'; - - getAddress() { - if (this._address) return this._address; - let address; - try { - let keyPair = bitcoin.ECPair.fromWIF(this.secret); - if (!keyPair.compressed) { - console.warn('only compressed public keys are good for segwit'); - return false; - } - address = bitcoin.payments.p2wpkh({ - pubkey: keyPair.publicKey, - }).address; - } catch (err) { - return false; - } - this._address = address; - - return this._address; - } - - static witnessToAddress(witness) { - const pubKey = Buffer.from(witness, 'hex'); - return bitcoin.payments.p2wpkh({ - pubkey: pubKey, - network: bitcoin.networks.bitcoin, - }).address; - } - - /** - * Converts script pub key to bech32 address if it can. Returns FALSE if it cant. - * - * @param scriptPubKey - * @returns {boolean|string} Either bech32 address or false - */ - static scriptPubKeyToAddress(scriptPubKey) { - const scriptPubKey2 = Buffer.from(scriptPubKey, 'hex'); - let ret; - try { - ret = bitcoin.payments.p2wpkh({ - output: scriptPubKey2, - network: bitcoin.networks.bitcoin, - }).address; - } catch (_) { - return false; - } - return ret; - } - - allowSend() { - return false; - } -} diff --git a/class/segwit-bech32-wallet.js b/class/segwit-bech32-wallet.js new file mode 100644 index 00000000..65a187dc --- /dev/null +++ b/class/segwit-bech32-wallet.js @@ -0,0 +1,149 @@ +import { LegacyWallet } from './legacy-wallet'; +const bitcoin = require('bitcoinjs-lib'); +const coinSelectAccumulative = require('coinselect/accumulative'); +const coinSelectSplit = require('coinselect/split'); + +export class SegwitBech32Wallet extends LegacyWallet { + static type = 'segwitBech32'; + static typeReadable = 'P2 WPKH'; + + getAddress() { + if (this._address) return this._address; + let address; + try { + let keyPair = bitcoin.ECPair.fromWIF(this.secret); + if (!keyPair.compressed) { + console.warn('only compressed public keys are good for segwit'); + return false; + } + address = bitcoin.payments.p2wpkh({ + pubkey: keyPair.publicKey, + }).address; + } catch (err) { + return false; + } + this._address = address; + + return this._address; + } + + static witnessToAddress(witness) { + const pubKey = Buffer.from(witness, 'hex'); + return bitcoin.payments.p2wpkh({ + pubkey: pubKey, + network: bitcoin.networks.bitcoin, + }).address; + } + + /** + * Converts script pub key to bech32 address if it can. Returns FALSE if it cant. + * + * @param scriptPubKey + * @returns {boolean|string} Either bech32 address or false + */ + static scriptPubKeyToAddress(scriptPubKey) { + const scriptPubKey2 = Buffer.from(scriptPubKey, 'hex'); + let ret; + try { + ret = bitcoin.payments.p2wpkh({ + output: scriptPubKey2, + network: bitcoin.networks.bitcoin, + }).address; + } catch (_) { + return false; + } + return ret; + } + + /** + * + * @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}} + */ + 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; + } + + 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++; + + const pubkey = keyPair.publicKey; + const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); + + psbt.addInput({ + hash: input.txid, + index: input.vout, + sequence, + witnessUtxo: { + script: p2wpkh.output, + value: input.value, + }, + }); + }); + + 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 }; + } + + allowSend() { + return true; + } + + allowSendMax() { + return true; + } +} diff --git a/class/segwit-p2sh-wallet.js b/class/segwit-p2sh-wallet.js index 03484d83..4cd67f9a 100644 --- a/class/segwit-p2sh-wallet.js +++ b/class/segwit-p2sh-wallet.js @@ -1,7 +1,7 @@ import { LegacyWallet } from './legacy-wallet'; const bitcoin = require('bitcoinjs-lib'); -const signer = require('../models/signer'); -const BigNumber = require('bignumber.js'); +const coinSelectAccumulative = require('coinselect/accumulative'); +const coinSelectSplit = require('coinselect/split'); /** * Creates Segwit P2SH Bitcoin address @@ -67,34 +67,92 @@ export class SegwitP2SHWallet extends LegacyWallet { } /** - * Takes UTXOs (as presented by blockcypher api), transforms them into - * format expected by signer module, creates tx and returns signed string txhex. * - * @param utxos Unspent outputs, expects blockcypher format - * @param amount - * @param fee - * @param address - * @param memo - * @param sequence By default zero. Increased with each transaction replace. - * @return string Signed txhex ready for broadcast + * @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}} */ - createTx(utxos, amount, fee, address, memo, sequence) { - // TODO: memo is not used here, get rid of it - if (sequence === undefined) { - sequence = 0; + 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; + } + + 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++; + + const pubkey = keyPair.publicKey; + const p2wpkh = bitcoin.payments.p2wpkh({ pubkey }); + let p2sh = bitcoin.payments.p2sh({ redeem: p2wpkh }); + + psbt.addInput({ + hash: input.txid, + index: input.vout, + sequence, + witnessUtxo: { + script: p2sh.output, + value: input.value, + }, + redeemScript: p2wpkh.output, + }); + }); + + 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); + } } - // transforming UTXOs fields to how module expects it - for (let u of utxos) { - u.confirmations = 6; // hack to make module accept 0 confirmations - u.txid = u.tx_hash; - u.vout = u.tx_output_n; - u.amount = new BigNumber(u.value); - u.amount = u.amount.dividedBy(100000000); - u.amount = u.amount.toString(10); + + let tx; + if (!skipSigning) { + tx = psbt.finalizeAllInputs().extractTransaction(); } - // console.log('creating tx ', amount, ' with fee ', fee, 'secret=', this.getSecret(), 'from address', this.getAddress()); - let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); - // to compensate that module substracts fee from amount - return signer.createSegwitTransaction(utxos, address, amountPlusFee, fee, this.getSecret(), this.getAddress(), sequence); + return { tx, inputs, outputs, fee, psbt }; + } + + allowSendMax() { + return true; } } diff --git a/class/walletGradient.js b/class/walletGradient.js index 8e076478..b30358c7 100644 --- a/class/walletGradient.js +++ b/class/walletGradient.js @@ -6,7 +6,7 @@ import { HDLegacyP2PKHWallet } from './hd-legacy-p2pkh-wallet'; import { WatchOnlyWallet } from './watch-only-wallet'; import { HDSegwitBech32Wallet } from './hd-segwit-bech32-wallet'; import { PlaceholderWallet } from './placeholder-wallet'; -import { SegwitBech32Wallet } from './segwit-bech-wallet'; +import { SegwitBech32Wallet } from './segwit-bech32-wallet'; export default class WalletGradient { static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1']; diff --git a/class/walletImport.js b/class/walletImport.js index d2ddbea4..5788adb0 100644 --- a/class/walletImport.js +++ b/class/walletImport.js @@ -204,12 +204,14 @@ export default class WalletImport { } } - let hdElectrumSeedLegacy = new HDLegacyElectrumSeedP2PKHWallet(); - hdElectrumSeedLegacy.setSecret(importText); - if (await hdElectrumSeedLegacy.wasEverUsed()) { - // not fetching txs or balances, fuck it, yolo, life is too short - return WalletImport._saveWallet(hdElectrumSeedLegacy); - } + try { + let hdElectrumSeedLegacy = new HDLegacyElectrumSeedP2PKHWallet(); + hdElectrumSeedLegacy.setSecret(importText); + if (await hdElectrumSeedLegacy.wasEverUsed()) { + // not fetching txs or balances, fuck it, yolo, life is too short + return WalletImport._saveWallet(hdElectrumSeedLegacy); + } + } catch (_) {} let hd2 = new HDSegwitP2SHWallet(); hd2.setSecret(importText); diff --git a/class/watch-only-wallet.js b/class/watch-only-wallet.js index 2dc4788b..e99895fe 100644 --- a/class/watch-only-wallet.js +++ b/class/watch-only-wallet.js @@ -40,10 +40,6 @@ export class WatchOnlyWallet extends LegacyWallet { throw new Error('Not initialized'); } - createTx(utxos, amount, fee, toAddress, memo) { - throw new Error('Not supported'); - } - valid() { if (this.secret.startsWith('xpub') || this.secret.startsWith('ypub') || this.secret.startsWith('zpub')) return true; diff --git a/ios/BlueWallet/Info.plist b/ios/BlueWallet/Info.plist index 01a30aba..69b3a627 100644 --- a/ios/BlueWallet/Info.plist +++ b/ios/BlueWallet/Info.plist @@ -48,7 +48,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 5.3.3 + 5.3.4 CFBundleSignature ???? CFBundleURLTypes diff --git a/ios/BlueWalletWatch Extension/Info.plist b/ios/BlueWalletWatch Extension/Info.plist index 9e22bffe..a0512f1b 100644 --- a/ios/BlueWalletWatch Extension/Info.plist +++ b/ios/BlueWalletWatch Extension/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 5.3.3 + 5.3.4 CFBundleVersion 239 CLKComplicationPrincipalClass diff --git a/ios/BlueWalletWatch/Info.plist b/ios/BlueWalletWatch/Info.plist index b1e07734..60c46c89 100644 --- a/ios/BlueWalletWatch/Info.plist +++ b/ios/BlueWalletWatch/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 5.3.3 + 5.3.4 CFBundleVersion 239 UISupportedInterfaceOrientations diff --git a/ios/TodayExtension/Info.plist b/ios/TodayExtension/Info.plist index 8f37a5f8..cfb3801a 100644 --- a/ios/TodayExtension/Info.plist +++ b/ios/TodayExtension/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 5.3.3 + 5.3.4 CFBundleVersion 1 NSExtension diff --git a/loc/en.js b/loc/en.js index 817852d2..c8aee6ef 100644 --- a/loc/en.js +++ b/loc/en.js @@ -82,7 +82,7 @@ module.exports = { error: 'Failed to import. Please, make sure that the provided data is valid.', success: 'Success', do_import: 'Import', - scan_qr: '...scan QR or import file instead?', + scan_qr: 'Scan or import a file', }, scanQrWif: { go_back: 'Go Back', diff --git a/models/signer.js b/models/signer.js deleted file mode 100644 index 979fa980..00000000 --- a/models/signer.js +++ /dev/null @@ -1,342 +0,0 @@ -/** - * Cashier-BTC - * ----------- - * Self-hosted bitcoin payment gateway - * - * https://github.com/Overtorment/Cashier-BTC - * - **/ -const bitcoinjs = require('bitcoinjs-lib'); -const _p2wpkh = bitcoinjs.payments.p2wpkh; -const _p2sh = bitcoinjs.payments.p2sh; -const toSatoshi = num => parseInt((num * 100000000).toFixed(0)); - -exports.createHDTransaction = function(utxos, toAddress, amount, fixedFee, changeAddress) { - let feeInSatoshis = parseInt((fixedFee * 100000000).toFixed(0)); - let amountToOutputSatoshi = parseInt(((amount - fixedFee) * 100000000).toFixed(0)); // how much payee should get - let txb = new bitcoinjs.TransactionBuilder(); - txb.setVersion(1); - let unspentAmountSatoshi = 0; - let ourOutputs = {}; - let outputNum = 0; - for (const unspent of utxos) { - if (unspent.confirmations < 1) { - // using only confirmed outputs - continue; - } - txb.addInput(unspent.txid, unspent.vout); - ourOutputs[outputNum] = ourOutputs[outputNum] || {}; - ourOutputs[outputNum].keyPair = bitcoinjs.ECPair.fromWIF(unspent.wif); - unspentAmountSatoshi += unspent.amount; - if (unspentAmountSatoshi >= amountToOutputSatoshi + feeInSatoshis) { - // found enough inputs to satisfy payee and pay fees - break; - } - outputNum++; - } - if (unspentAmountSatoshi < amountToOutputSatoshi + feeInSatoshis) { - throw new Error('Not enough balance. Please, try sending a smaller amount.'); - } - - // adding outputs - - txb.addOutput(toAddress, amountToOutputSatoshi); - if (amountToOutputSatoshi + feeInSatoshis < unspentAmountSatoshi) { - // sending less than we have, so the rest should go back - if (unspentAmountSatoshi - amountToOutputSatoshi - feeInSatoshis > 3 * feeInSatoshis) { - // to prevent @dust error change transferred amount should be at least 3xfee. - // if not - we just dont send change and it wil add to fee - txb.addOutput(changeAddress, unspentAmountSatoshi - amountToOutputSatoshi - feeInSatoshis); - } - } - - // now, signing every input with a corresponding key - - for (let c = 0; c <= outputNum; c++) { - txb.sign({ - prevOutScriptType: 'p2pkh', - vin: c, - keyPair: ourOutputs[c].keyPair, - }); - } - - let tx = txb.build(); - return tx.toHex(); -}; - -exports.createHDSegwitTransaction = function(utxos, toAddress, amount, fixedFee, changeAddress) { - let feeInSatoshis = parseInt((fixedFee * 100000000).toFixed(0)); - let amountToOutputSatoshi = parseInt(((amount - fixedFee) * 100000000).toFixed(0)); // how much payee should get - let psbt = new bitcoinjs.Psbt(); - psbt.setVersion(1); - let unspentAmountSatoshi = 0; - let ourOutputs = []; - let outputNum = 0; - for (const unspent of utxos) { - if (unspent.confirmations < 1) { - // using only confirmed outputs - continue; - } - let keyPair = bitcoinjs.ECPair.fromWIF(unspent.wif); - let p2wpkh = _p2wpkh({ - pubkey: keyPair.publicKey, - }); - let p2sh = _p2sh({ - redeem: p2wpkh, - }); - psbt.addInput({ - hash: unspent.txid, - index: unspent.vout, - witnessUtxo: { - script: p2sh.output, - value: unspent.amount, - }, - redeemScript: p2wpkh.output, - }); - ourOutputs[outputNum] = ourOutputs[outputNum] || {}; - ourOutputs[outputNum].keyPair = keyPair; - ourOutputs[outputNum].redeemScript = p2wpkh.output; - ourOutputs[outputNum].amount = unspent.amount; - unspentAmountSatoshi += unspent.amount; - if (unspentAmountSatoshi >= amountToOutputSatoshi + feeInSatoshis) { - // found enough inputs to satisfy payee and pay fees - break; - } - outputNum++; - } - - if (unspentAmountSatoshi < amountToOutputSatoshi + feeInSatoshis) { - throw new Error('Not enough balance. Please, try sending a smaller amount.'); - } - - // adding outputs - - psbt.addOutput({ - address: toAddress, - value: amountToOutputSatoshi, - }); - if (amountToOutputSatoshi + feeInSatoshis < unspentAmountSatoshi) { - // sending less than we have, so the rest should go back - if (unspentAmountSatoshi - amountToOutputSatoshi - feeInSatoshis > 3 * feeInSatoshis) { - // to prevent @dust error change transferred amount should be at least 3xfee. - // if not - we just dont send change and it wil add to fee - psbt.addOutput({ - address: changeAddress, - value: unspentAmountSatoshi - amountToOutputSatoshi - feeInSatoshis, - }); - } - } - - // now, signing every input with a corresponding key - - for (let c = 0; c <= outputNum; c++) { - psbt.signInput(c, ourOutputs[c].keyPair); - } - - let tx = psbt.finalizeAllInputs().extractTransaction(); - return tx.toHex(); -}; - -exports.createSegwitTransaction = function(utxos, toAddress, amount, fixedFee, WIF, changeAddress, sequence) { - changeAddress = changeAddress || exports.WIF2segwitAddress(WIF); - if (sequence === undefined) { - sequence = bitcoinjs.Transaction.DEFAULT_SEQUENCE; - } - - let feeInSatoshis = parseInt((fixedFee * 100000000).toFixed(0)); - let keyPair = bitcoinjs.ECPair.fromWIF(WIF); - let p2wpkh = _p2wpkh({ - pubkey: keyPair.publicKey, - }); - let p2sh = _p2sh({ - redeem: p2wpkh, - }); - - let psbt = new bitcoinjs.Psbt(); - psbt.setVersion(1); - let unspentAmount = 0; - for (const unspent of utxos) { - if (unspent.confirmations < 2) { - // using only confirmed outputs - continue; - } - const satoshis = parseInt((unspent.amount * 100000000).toFixed(0)); - psbt.addInput({ - hash: unspent.txid, - index: unspent.vout, - sequence, - witnessUtxo: { - script: p2sh.output, - value: satoshis, - }, - redeemScript: p2wpkh.output, - }); - unspentAmount += satoshis; - } - let amountToOutput = parseInt(((amount - fixedFee) * 100000000).toFixed(0)); - psbt.addOutput({ - address: toAddress, - value: amountToOutput, - }); - if (amountToOutput + feeInSatoshis < unspentAmount) { - // sending less than we have, so the rest should go back - - if (unspentAmount - amountToOutput - feeInSatoshis > 3 * feeInSatoshis) { - // to prevent @dust error change transferred amount should be at least 3xfee. - // if not - we just dont send change and it wil add to fee - psbt.addOutput({ - address: changeAddress, - value: unspentAmount - amountToOutput - feeInSatoshis, - }); - } - } - - for (let c = 0; c < utxos.length; c++) { - psbt.signInput(c, keyPair); - } - - let tx = psbt.finalizeAllInputs().extractTransaction(); - return tx.toHex(); -}; - -exports.createRBFSegwitTransaction = function(txhex, addressReplaceMap, feeDelta, WIF, utxodata) { - if (feeDelta < 0) { - throw Error('replace-by-fee requires increased fee, not decreased'); - } - - let tx = bitcoinjs.Transaction.fromHex(txhex); - - // looking for latest sequence number in inputs - let highestSequence = 0; - for (let i of tx.ins) { - if (i.sequence > highestSequence) { - highestSequence = i.sequence; - } - } - let keyPair = bitcoinjs.ECPair.fromWIF(WIF); - let p2wpkh = _p2wpkh({ - pubkey: keyPair.publicKey, - }); - let p2sh = _p2sh({ - redeem: p2wpkh, - }); - - // creating TX - let psbt = new bitcoinjs.Psbt(); - psbt.setVersion(1); - for (let unspent of tx.ins) { - let txid = Buffer.from(unspent.hash) - .reverse() - .toString('hex'); - let index = unspent.index; - let amount = utxodata[txid][index]; - psbt.addInput({ - hash: txid, - index, - sequence: highestSequence + 1, - witnessUtxo: { - script: p2sh.output, - value: amount, - }, - redeemScript: p2wpkh.output, - }); - } - - for (let o of tx.outs) { - let outAddress = bitcoinjs.address.fromOutputScript(o.script); - if (addressReplaceMap[outAddress]) { - // means this is DESTINATION address, not messing with it's amount - // but replacing the address itseld - psbt.addOutput({ - address: addressReplaceMap[outAddress], - value: o.value, - }); - } else { - // CHANGE address, so we deduct increased fee from here - let feeDeltaInSatoshi = parseInt((feeDelta * 100000000).toFixed(0)); - psbt.addOutput({ - address: outAddress, - value: o.value - feeDeltaInSatoshi, - }); - } - } - - // signing - for (let c = 0; c < tx.ins.length; c++) { - psbt.signInput(c, keyPair); - } - - let newTx = psbt.finalizeAllInputs().extractTransaction(); - return newTx.toHex(); -}; - -exports.generateNewSegwitAddress = function() { - let keyPair = bitcoinjs.ECPair.makeRandom(); - let address = bitcoinjs.payments.p2sh({ - redeem: bitcoinjs.payments.p2wpkh({ - pubkey: keyPair.publicKey, - }), - }).address; - - return { - address: address, - WIF: keyPair.toWIF(), - }; -}; - -exports.URI = function(paymentInfo) { - let uri = 'bitcoin:'; - uri += paymentInfo.address; - uri += '?amount='; - uri += parseFloat(paymentInfo.amount / 100000000); - uri += '&message='; - uri += encodeURIComponent(paymentInfo.message); - if (paymentInfo.label) { - uri += '&label='; - uri += encodeURIComponent(paymentInfo.label); - } - - return uri; -}; - -exports.WIF2segwitAddress = function(WIF) { - let keyPair = bitcoinjs.ECPair.fromWIF(WIF); - return bitcoinjs.payments.p2sh({ - redeem: bitcoinjs.payments.p2wpkh({ - pubkey: keyPair.publicKey, - }), - }).address; -}; - -exports.createTransaction = function(utxos, toAddress, _amount, _fixedFee, WIF, fromAddress) { - let fixedFee = toSatoshi(_fixedFee); - let amountToOutput = toSatoshi(_amount - _fixedFee); - let pk = bitcoinjs.ECPair.fromWIF(WIF); // eslint-disable-line new-cap - let txb = new bitcoinjs.TransactionBuilder(); - txb.setVersion(1); - let unspentAmount = 0; - for (const unspent of utxos) { - if (unspent.confirmations < 2) { - // using only confirmed outputs - continue; - } - txb.addInput(unspent.txid, unspent.vout); - unspentAmount += toSatoshi(unspent.amount); - } - txb.addOutput(toAddress, amountToOutput); - - if (amountToOutput + fixedFee < unspentAmount) { - // sending less than we have, so the rest should go back - txb.addOutput(fromAddress, unspentAmount - amountToOutput - fixedFee); - } - - for (let c = 0; c < utxos.length; c++) { - txb.sign({ - prevOutScriptType: 'p2pkh', - vin: c, - keyPair: pk, - }); - } - - return txb.build().toHex(); -}; diff --git a/package-lock.json b/package-lock.json index f4f003da..ab8d446c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "bluewallet", - "version": "5.3.3", + "version": "5.3.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3196,19 +3196,38 @@ } }, "buffer": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", - "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz", + "integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==", "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -3317,14 +3336,6 @@ } } }, - "can-promise": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/can-promise/-/can-promise-0.0.1.tgz", - "integrity": "sha512-gzVrHyyrvgt0YpDm7pn04MQt8gjh0ZAhN4ZDyCRtGl6YnuuK6b4aiUTD7G52r9l4YNmxfTtEscb92vxtAlL6XQ==", - "requires": { - "window-or-global": "^1.0.1" - } - }, "caniuse-lite": { "version": "1.0.30001040", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001040.tgz", @@ -10981,161 +10992,88 @@ } }, "qrcode": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.2.0.tgz", - "integrity": "sha512-wZK0Z0eYmOUDP2tOGzmLdeBn5Npa+4wms9GdvzH7HrywvGUq/Stz0BKUhW4DfmBf1PSrm9dNfdnVDq683Zxvag==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz", + "integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==", "requires": { - "can-promise": "^0.0.1", + "buffer": "^5.4.3", + "buffer-alloc": "^1.2.0", + "buffer-from": "^1.1.1", "dijkstrajs": "^1.0.1", "isarray": "^2.0.1", "pngjs": "^3.3.0", - "yargs": "^8.0.2" + "yargs": "^13.2.4" }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" - }, "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" } }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "^1.0.0" - } - }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "requires": { - "mimic-fn": "^1.0.0" - } + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "requires": { - "ansi-regex": "^2.0.0" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" } }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" - }, "yargs": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", - "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "requires": { - "camelcase": "^4.1.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "read-pkg-up": "^2.0.0", + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^3.0.0", "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^7.0.0" + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" } }, "yargs-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", - "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "requires": { - "camelcase": "^4.1.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } @@ -11719,12 +11657,12 @@ "from": "git+https://github.com/marcosrdz/react-native-prompt-android.git" }, "react-native-qrcode-svg": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-5.3.2.tgz", - "integrity": "sha512-qXujuIqog2PQ0jLa88emqDy8NcDBF41jRf5Rm/7DEY5wFnIiLrN3p7X+ucFDIIUqWuKB2gKdrb7HDs0eK2aryQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.0.6.tgz", + "integrity": "sha512-b+/teD+xj17VDujJzf956U2+9mX+gKwVJss2aqmhEIyjP7+TVOuE08D3UkzfOCWXE8gppcUTTz5gkY1NXgfwyQ==", "requires": { "prop-types": "^15.5.10", - "qrcode": "1.2.0" + "qrcode": "^1.3.2" } }, "react-native-quick-actions": { @@ -14127,11 +14065,6 @@ "bs58check": "<3.0.0" } }, - "window-or-global": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/window-or-global/-/window-or-global-1.0.1.tgz", - "integrity": "sha1-2+RboqKRqrxW1iz2bEW3+jIpRt4=" - }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index 6b044681..474f1da2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bluewallet", - "version": "5.3.3", + "version": "5.3.4", "license": "MIT", "devDependencies": { "@babel/core": "^7.6.2", @@ -73,7 +73,7 @@ "bip39": "2.5.0", "bitcoinjs-lib": "5.1.6", "bolt11": "1.2.7", - "buffer": "5.4.3", + "buffer": "5.5.0", "buffer-reverse": "1.0.1", "coinselect": "3.1.11", "crypto-js": "3.1.9-1", @@ -119,7 +119,7 @@ "react-native-popup-menu-android": "1.0.3", "react-native-privacy-snapshot": "git+https://github.com/BlueWallet/react-native-privacy-snapshot.git", "react-native-prompt-android": "git+https://github.com/marcosrdz/react-native-prompt-android.git", - "react-native-qrcode-svg": "5.3.2", + "react-native-qrcode-svg": "6.0.6", "react-native-quick-actions": "0.3.13", "react-native-randombytes": "3.5.3", "react-native-rate": "1.1.10", diff --git a/screen/lnd/lndViewInvoice.js b/screen/lnd/lndViewInvoice.js index 0e2424b5..776e3cca 100644 --- a/screen/lnd/lndViewInvoice.js +++ b/screen/lnd/lndViewInvoice.js @@ -9,6 +9,7 @@ import { BlueCopyTextToClipboard, BlueNavigationStyle, BlueSpacing20, + BlueBigCheckmark } from '../../BlueComponents'; import PropTypes from 'prop-types'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; @@ -187,20 +188,7 @@ export default class LNDViewInvoice extends Component { - - - + {loc.lndViewInvoice.has_been_paid} diff --git a/screen/receive/details.js b/screen/receive/details.js index 9be52613..409d8ead 100644 --- a/screen/receive/details.js +++ b/screen/receive/details.js @@ -2,7 +2,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { View, InteractionManager, Platform, TextInput, KeyboardAvoidingView, Keyboard, StyleSheet, ScrollView } from 'react-native'; import QRCode from 'react-native-qrcode-svg'; import { useNavigation, useNavigationParam } from 'react-navigation-hooks'; -import bip21 from 'bip21'; import { BlueLoading, SafeBlueArea, @@ -21,6 +20,7 @@ import Share from 'react-native-share'; import { Chain, BitcoinUnit } from '../../models/bitcoinUnits'; import Modal from 'react-native-modal'; import HandoffSettings from '../../class/handoff'; +import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; import Handoff from 'react-native-handoff'; /** @type {AppStorage} */ const BlueApp = require('../../BlueApp'); @@ -75,7 +75,7 @@ const ReceiveDetails = () => { setAddress(wallet.getAddress()); } InteractionManager.runAfterInteractions(async () => { - const bip21encoded = bip21.encode(address); + const bip21encoded = DeeplinkSchemaMatch.bip21encode(address); setBip21encoded(bip21encoded); }); }, [wallet]); @@ -116,7 +116,7 @@ const ReceiveDetails = () => { const createCustomAmountAddress = () => { setIsCustom(true); setIsCustomModalVisible(false); - setBip21encoded(bip21.encode(address, { amount: customAmount, label: customLabel })); + setBip21encoded(DeeplinkSchemaMatch.bip21encode(address, { amount: customAmount, label: customLabel })); }; const clearCustomAmount = () => { @@ -124,7 +124,7 @@ const ReceiveDetails = () => { setIsCustomModalVisible(false); setCustomAmount(''); setCustomLabel(''); - setBip21encoded(bip21.encode(address)); + setBip21encoded(DeeplinkSchemaMatch.bip21encode(address)); }; const renderCustomAmountModal = () => { diff --git a/screen/selftest.js b/screen/selftest.js index bf6b28a2..603d535d 100644 --- a/screen/selftest.js +++ b/screen/selftest.js @@ -5,7 +5,6 @@ import PropTypes from 'prop-types'; import { SegwitP2SHWallet, LegacyWallet, HDSegwitP2SHWallet, HDSegwitBech32Wallet } from '../class'; const bitcoin = require('bitcoinjs-lib'); const BlueCrypto = require('react-native-blue-crypto'); -let BigNumber = require('bignumber.js'); let encryption = require('../encryption'); let BlueElectrum = require('../BlueElectrum'); @@ -60,84 +59,28 @@ export default class Selftest extends Component { // let l = new LegacyWallet(); - l.setSecret('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct'); - if (l.getAddress() !== '19AAjaTUbRjQCMuVczepkoPswiZRhjtg31') { - throw new Error('failed to generate legacy address from WIF'); - } - - // - - // utxos as received from blockcypher + l.setSecret('L4ccWrPMmFDZw4kzAKFqJNxgHANjdy6b7YKNXMwB4xac4FLF3Tov'); + assertStrictEqual(l.getAddress(), '14YZ6iymQtBVQJk6gKnLCk49UScJK7SH4M'); let utxos = [ { - tx_hash: '2f445cf016fa2772db7d473bff97515355b4e6148e1c980ce351d47cf54c517f', - block_height: 523186, - tx_input_n: -1, - tx_output_n: 1, - value: 100000, - ref_balance: 100000, - spent: false, - confirmations: 215, - confirmed: '2018-05-18T03:16:34Z', - double_spend: false, - }, - ]; - let toAddr = '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB'; - let amount = 0.0009; - let fee = 0.0001; - let txHex = l.createTx(utxos, amount, fee, toAddr); - if ( - txHex !== - '01000000017f514cf57cd451e30c981c8e14e6b455535197ff3b477ddb7227fa16f05c442f010000006b483045022100b9a6545847bd30418c133437c7660a6676afafe6e7e837a37ef2ead931ebd586022056bc43cbf71855d0719f54151c8fcaaaa03367ecafdd7296dbe39f042e432f4f012103aea0dfd576151cb399347aa6732f8fdf027b9ea3ea2e65fb754803f776e0a509ffffffff01905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88ac00000000' - ) { - throw new Error('failed to create TX from legacy address'); - } - - // now, several utxos - // utxos as received from blockcypher - utxos = [ - { - amount: '0.002', - block_height: 523416, - confirmations: 6, - confirmed: '2018-05-19T15:46:43Z', - double_spend: false, - ref_balance: 300000, - spent: false, - tx_hash: 'dc3605040a03724bc584ed43bc22a559f5d32a1b0708ca05b20b9018fdd523ef', - tx_input_n: -1, - tx_output_n: 0, - txid: 'dc3605040a03724bc584ed43bc22a559f5d32a1b0708ca05b20b9018fdd523ef', - value: 200000, + txid: 'cc44e933a094296d9fe424ad7306f16916253a3d154d52e4f1a757c18242cec4', vout: 0, - }, - { - amount: '0.001', - block_height: 523186, - confirmations: 6, - confirmed: '2018-05-18T03:16:34Z', - double_spend: false, - ref_balance: 100000, - spent: false, - tx_hash: 'c473c104febfe6621804976d1082a1468c1198d0339e35f30a8ba1515d9eb017', - tx_input_n: -1, - tx_output_n: 0, - txid: 'c473c104febfe6621804976d1082a1468c1198d0339e35f30a8ba1515d9eb017', value: 100000, - vout: 0, + txhex: + '0200000000010161890cd52770c150da4d7d190920f43b9f88e7660c565a5a5ad141abb6de09de00000000000000008002a0860100000000001976a91426e01119d265aa980390c49eece923976c218f1588ac3e17000000000000160014c1af8c9dd85e0e55a532a952282604f820746fcd02473044022072b3f28808943c6aa588dd7a4e8f29fad7357a2814e05d6c5d767eb6b307b4e6022067bc6a8df2dbee43c87b8ce9ddd9fe678e00e0f7ae6690d5cb81eca6170c47e8012102e8fba5643e15ab70ec79528833a2c51338c1114c4eebc348a235b1a3e13ab07100000000', }, ]; - toAddr = '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB'; - amount = 0.0009; - fee = 0.0001; - txHex = l.createTx(utxos, amount, fee, toAddr); - if ( - txHex !== - '0100000002ef23d5fd18900bb205ca08071b2ad3f559a522bc43ed84c54b72030a040536dc000000006a47304402206b4f03e471d60dff19f4df1a8203ca97f6282658160034cea0f2b7d748c33d9802206058d23861dabdfb252c8df14249d1a2b00345dd90d32ab451cc3c6cfcb3b402012103aea0dfd576151cb399347aa6732f8fdf027b9ea3ea2e65fb754803f776e0a509ffffffff17b09e5d51a18b0af3359e33d098118c46a182106d97041862e6bffe04c173c4000000006b4830450221009785a61358a1ee7ab5885a98b111275226e0046a48b69980c4f53ecf99cdce0a02200503249e0b23d633ec1fbae5d41a0dcf9758dce3560066d1aee9ecfbfeefcfb7012103aea0dfd576151cb399347aa6732f8fdf027b9ea3ea2e65fb754803f776e0a509ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88ac400d0300000000001976a914597ce022baa887799951e0496c769d9cc0c759dc88ac00000000' - ) { - throw new Error('failed to create TX from legacy address'); - } + let txNew = l.createTransaction(utxos, [{ value: 90000, address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, l.getAddress()); + let txBitcoin = bitcoin.Transaction.fromHex(txNew.tx.toHex()); + assertStrictEqual( + txNew.tx.toHex(), + '0200000001c4ce4282c157a7f1e4524d153d3a251669f10673ad24e49f6d2994a033e944cc000000006a47304402200faed160757433bcd4d9fe5f55eb92420406e8f3099a7e12ef720c77313c8c7e022044bc9e1abca6a81a8ad5c749f5ec4694301589172b83b1803bc134eda0487dbc01210337c09b3cb889801638078fd4e6998218b28c92d338ea2602720a88847aedceb3ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88ac2f260000000000001976a91426e01119d265aa980390c49eece923976c218f1588ac00000000', + ); + assertStrictEqual(txBitcoin.ins.length, 1); + assertStrictEqual(txBitcoin.outs.length, 2); + assertStrictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(txBitcoin.outs[0].script)); // to address + assertStrictEqual(l.getAddress(), bitcoin.address.fromOutputScript(txBitcoin.outs[1].script)); // change address // @@ -149,44 +92,28 @@ export default class Selftest extends Component { // - // utxos as received from blockcypher - let utxo = [ + let wallet = new SegwitP2SHWallet(); + wallet.setSecret('Ky1vhqYGCiCbPd8nmbUeGfwLdXB1h5aGwxHwpXrzYRfY5cTZPDo4'); + assertStrictEqual(wallet.getAddress(), '3CKN8HTCews4rYJYsyub5hjAVm5g5VFdQJ'); + + utxos = [ { - tx_hash: '0f5eea78fb19e72b55bd119252ff29fc16c503d0e956a9c1b5b2ab0e95e0c323', - block_height: 514991, - tx_input_n: -1, - tx_output_n: 2, - value: 110000, - ref_balance: 546, - spent: false, - confirmations: 9, - confirmed: '2018-03-24T18:13:36Z', - double_spend: false, + txid: 'a56b44080cb606c0bd90e77fcd4fb34c863e68e5562e75b4386e611390eb860c', + vout: 0, + value: 300000, }, ]; - let tx = l.createTx(utxo, 0.001, 0.0001, '1QHf8Gp3wfmFiSdEX4FtrssCGR68diN1cj'); - let txDecoded = bitcoin.Transaction.fromHex(tx); - let txid = txDecoded.getId(); - - if (txid !== '110f51d28d585e922adbf701cba802e549b8fe3a53fa5d62426ab42549c9b6de') { - throw new Error('created txid doesnt match'); - } - if ( - tx !== - '0100000000010123c3e0950eabb2b5c1a956e9d003c516fc29ff529211bd552be719fb78ea5e0f0200000017160014597ce022baa887799951e0496c769d9cc0c759dc0000000001a0860100000000001976a914ff715fb722cb10646d80709aeac7f2f4ee00278f88ac02473044022075670317a0e5b5d4eef154b03db97396a64cbc6ef3b576d98367e1a83c1c488002206d6df1e8085fd711d6ea264de3803340f80fa2c6e30683879d9ad40f3228c56c012103aea0dfd576151cb399347aa6732f8fdf027b9ea3ea2e65fb754803f776e0a50900000000' - ) { - throw new Error('created tx hex doesnt match'); - } - - let feeSatoshi = new BigNumber(0.0001); - feeSatoshi = feeSatoshi.multipliedBy(100000000); - let satoshiPerByte = feeSatoshi.dividedBy(Math.round(tx.length / 2)); - satoshiPerByte = Math.round(satoshiPerByte.toString(10)); - - if (satoshiPerByte !== 46) { - throw new Error('created tx satoshiPerByte doesnt match'); - } + txNew = wallet.createTransaction(utxos, [{ value: 90000, address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, wallet.getAddress()); + let tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); + assertStrictEqual( + txNew.tx.toHex(), + '020000000001010c86eb9013616e38b4752e56e5683e864cb34fcd7fe790bdc006b60c08446ba50000000017160014139dc70d73097f9d775f8a3280ba3e3435515641ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88ac6f3303000000000017a914749118baa93fb4b88c28909c8bf0a8202a0484f487024730440220086b55a771f37daadbe64fe557a32fd68ee92995445af0b0a5b9343db67505e1022064c9a9778a19a0276761af69b8917d19ed4b791c785dd8cb4aae327f2a6b526f012103a5de146762f84055db3202c1316cd9008f16047f4f408c1482fdb108217eda0800000000', + ); + assertStrictEqual(tx.ins.length, 1); + assertStrictEqual(tx.outs.length, 2); + assertStrictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(tx.outs[0].script)); // to address + assertStrictEqual(bitcoin.address.fromOutputScript(tx.outs[1].script), wallet.getAddress()); // change address // @@ -312,6 +239,13 @@ export default class Selftest extends Component { } } +function assertStrictEqual(actual, expected, message) { + if (expected !== actual) { + if (message) throw new Error(message); + throw new Error('Assertion failed that ' + JSON.stringify(expected) + ' equals ' + JSON.stringify(actual)); + } +} + Selftest.propTypes = { navigation: PropTypes.shape({ navigate: PropTypes.func, diff --git a/screen/send/broadcast.js b/screen/send/broadcast.js new file mode 100644 index 00000000..ee046e63 --- /dev/null +++ b/screen/send/broadcast.js @@ -0,0 +1,162 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { ActivityIndicator, Linking, StyleSheet, View, KeyboardAvoidingView, Platform, Text, TextInput } from 'react-native'; +import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; +import { HDSegwitBech32Wallet } from '../../class'; +import { + SafeBlueArea, + BlueCard, + BlueButton, + BlueSpacing10, + BlueSpacing20, + BlueFormLabel, + BlueTextCentered, + BlueBigCheckmark, +} from '../../BlueComponents'; +import BlueElectrum from '../../BlueElectrum'; +const bitcoin = require('bitcoinjs-lib'); + +const BROADCAST_RESULT = Object.freeze({ + none: 'Input transaction hash', + pending: 'pending', + success: 'success', + error: 'error', +}); + +export default function Broadcast() { + const [tx, setTx] = useState(''); + const [txHex, setTxHex] = useState(''); + const [broadcastResult, setBroadcastResult] = useState(BROADCAST_RESULT.none); + const handleUpdateTxHex = nextValue => setTxHex(nextValue.trim()); + const handleBroadcast = async () => { + setBroadcastResult(BROADCAST_RESULT.pending); + try { + await BlueElectrum.ping(); + await BlueElectrum.waitTillConnected(); + const walletObj = new HDSegwitBech32Wallet(); + const result = await walletObj.broadcastTx(txHex); + if (result) { + let tx = bitcoin.Transaction.fromHex(txHex); + const txid = tx.getId(); + setTx(txid); + setBroadcastResult(BROADCAST_RESULT.success); + } else { + setBroadcastResult(BROADCAST_RESULT.error); + } + } catch (error) { + ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); + setBroadcastResult(BROADCAST_RESULT.error); + } + }; + + return ( + + + + {BROADCAST_RESULT.success !== broadcastResult && ( + + + {broadcastResult} + {BROADCAST_RESULT.pending === broadcastResult && } + + + + + + + )} + {BROADCAST_RESULT.success === broadcastResult && } + + + + ); +} + +const styles = StyleSheet.create({ + wrapper: { + marginTop: 16, + alignItems: 'center', + justifyContent: 'flex-start', + }, + blueArea: { + flex: 1, + paddingTop: 19, + }, + broadcastResultWrapper: { + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + }, + link: { + color: 'blue', + }, + mainCard: { + padding: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'flex-start', + }, + topFormRow: { + flex: 0.1, + flexBasis: 0.1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingBottom: 10, + paddingTop: 0, + paddingRight: 100, + height: 30, + maxHeight: 30, + }, +}); + +function SuccessScreen({ tx }) { + if (!tx) { + return null; + } + return ( + + + + + + Success! You transaction has been broadcasted! + + Linking.openURL(`https://blockstream.info/tx/${tx}`)}> + Open link in explorer + + + + + ); +} + +SuccessScreen.propTypes = { + tx: PropTypes.string.isRequired, +}; diff --git a/screen/send/confirm.js b/screen/send/confirm.js index f34b3049..c944b9f3 100644 --- a/screen/send/confirm.js +++ b/screen/send/confirm.js @@ -7,7 +7,16 @@ import { BitcoinUnit } from '../../models/bitcoinUnits'; import PropTypes from 'prop-types'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import Biometric from '../../class/biometrics'; -import { HDSegwitBech32Wallet } from '../../class'; +import { + HDLegacyElectrumSeedP2PKHWallet, + HDLegacyP2PKHWallet, + HDSegwitBech32Wallet, + HDSegwitP2SHWallet, + HDLegacyBreadwalletWallet, + LegacyWallet, + SegwitP2SHWallet, + SegwitBech32Wallet, +} from '../../class'; let loc = require('../../loc'); let EV = require('../../events'); let currency = require('../../currency'); @@ -58,17 +67,13 @@ export default class Confirm extends Component { } let result = await this.state.fromWallet.broadcastTx(this.state.tx); - if (result && result.code) { - if (result.code === 1) { - const message = result.message.split('\n'); - throw new Error(`${message[0]}: ${message[2]}`); - } + if (!result) { + throw new Error(`Broadcast failed`); } else { - console.log('broadcast result = ', result); EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs let amount = 0; const recipients = this.state.recipients; - if (recipients[0].amount === BitcoinUnit.MAX) { + if (recipients[0].amount === BitcoinUnit.MAX || !recipients[0].amount) { amount = this.state.fromWallet.getBalance() - this.state.feeSatoshi; } else { for (const recipient of recipients) { @@ -76,7 +81,19 @@ export default class Confirm extends Component { } } - if (this.state.fromWallet.type === HDSegwitBech32Wallet.type) { + // wallets that support new createTransaction() instead of deprecated createTx() + if ( + [ + HDSegwitBech32Wallet.type, + HDSegwitP2SHWallet.type, + HDLegacyP2PKHWallet.type, + HDLegacyBreadwalletWallet.type, + HDLegacyElectrumSeedP2PKHWallet.type, + LegacyWallet.type, + SegwitP2SHWallet.type, + SegwitBech32Wallet.type, + ].includes(this.state.fromWallet.type) + ) { amount = loc.formatBalanceWithoutSuffix(amount, BitcoinUnit.BTC, false); } @@ -100,13 +117,14 @@ export default class Confirm extends Component { <> - {item.amount === BitcoinUnit.MAX + {!item.value || item.value === BitcoinUnit.MAX ? currency.satoshiToBTC(this.state.fromWallet.getBalance() - this.state.feeSatoshi) : item.amount || currency.satoshiToBTC(item.value)} @@ -176,6 +194,7 @@ export default class Confirm extends Component { )} { if (this.isBiometricUseCapableAndEnabled) { diff --git a/screen/send/create.js b/screen/send/create.js index 6dbe444d..bb380e75 100644 --- a/screen/send/create.js +++ b/screen/send/create.js @@ -104,9 +104,9 @@ export default class SendCreate extends Component { {item.address} {loc.send.create.amount} - {item.amount === BitcoinUnit.MAX + {item.value === BitcoinUnit.MAX || !item.value ? currency.satoshiToBTC(this.state.wallet.getBalance()) - this.state.fee - : item.amount || currency.satoshiToBTC(item.value)}{' '} + : currency.satoshiToBTC(item.value)}{' '} {BitcoinUnit.BTC} {this.state.recipients.length > 1 && ( @@ -131,6 +131,7 @@ export default class SendCreate extends Component { {loc.send.create.this_is_hex} { - alert(Err.message); - ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); - }); - } - return; - } - - // legacy send below - - this.setState({ isLoading: true }, async () => { - let utxo; - let actualSatoshiPerByte; - let tx, txid; - let tries = 1; - let fee = 0.000001; // initial fee guess - const firstTransaction = this.state.addresses[0]; - try { - await this.state.fromWallet.fetchUtxo(); - if (this.state.fromWallet.getChangeAddressAsync) { - await this.state.fromWallet.getChangeAddressAsync(); // to refresh internal pointer to next free address - } - if (this.state.fromWallet.getAddressAsync) { - await this.state.fromWallet.getAddressAsync(); // to refresh internal pointer to next free address - } - - utxo = this.state.fromWallet.utxo; - - do { - console.log('try #', tries, 'fee=', fee); - if (this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), firstTransaction.amount, fee) < 0) { - // we could not add any fee. user is trying to send all he's got. that wont work - throw new Error(loc.send.details.total_exceeds_balance); - } - - let startTime = Date.now(); - tx = this.state.fromWallet.createTx(utxo, firstTransaction.amount, fee, firstTransaction.address, this.state.memo); - let endTime = Date.now(); - console.log('create tx ', (endTime - startTime) / 1000, 'sec'); - - let txDecoded = bitcoin.Transaction.fromHex(tx); - txid = txDecoded.getId(); - console.log('txid', txid); - console.log('txhex', tx); - - let feeSatoshi = new BigNumber(fee).multipliedBy(100000000); - actualSatoshiPerByte = feeSatoshi.dividedBy(Math.round(tx.length / 2)); - actualSatoshiPerByte = actualSatoshiPerByte.toNumber(); - console.log({ satoshiPerByte: actualSatoshiPerByte }); - - if (Math.round(actualSatoshiPerByte) !== requestedSatPerByte * 1 || Math.floor(actualSatoshiPerByte) < 1) { - console.log('fee is not correct, retrying'); - fee = feeSatoshi - .multipliedBy(requestedSatPerByte / actualSatoshiPerByte) - .plus(10) - .dividedBy(100000000) - .toNumber(); - } else { - break; - } - } while (tries++ < 5); - - BlueApp.tx_metadata = BlueApp.tx_metadata || {}; - BlueApp.tx_metadata[txid] = { - txhex: tx, - memo: this.state.memo, - }; - await BlueApp.saveToDisk(); - } catch (err) { - console.log(err); + try { + await this.createPsbtTransaction(); + } catch (Err) { + this.setState({ isLoading: false }, () => { + alert(Err.message); ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); - alert(err); - this.setState({ isLoading: false }); - return; - } - this.props.navigation.navigate('Confirm', { - recipients: [firstTransaction], - // HD wallet's utxo is in sats, classic segwit wallet utxos are in btc - fee: this.calculateFee( - utxo, - tx, - this.state.fromWallet.type === HDSegwitP2SHWallet.type || this.state.fromWallet.type === HDLegacyP2PKHWallet.type, - ), - memo: this.state.memo, - fromWallet: this.state.fromWallet, - tx: tx, - satoshiPerByte: actualSatoshiPerByte.toFixed(2), }); - this.setState({ isLoading: false }); - }); + } } - async createHDBech32Transaction() { + async createPsbtTransaction() { /** @type {HDSegwitBech32Wallet} */ const wallet = this.state.fromWallet; await wallet.fetchUtxo(); - const firstTransaction = this.state.addresses[0]; const changeAddress = await wallet.getChangeAddressAsync(); - let satoshis = new BigNumber(firstTransaction.amount).multipliedBy(100000000).toNumber(); const requestedSatPerByte = +this.state.fee.toString().replace(/\D/g, ''); - console.log({ satoshis, requestedSatPerByte, utxo: wallet.getUtxo() }); + console.log({ requestedSatPerByte, utxo: wallet.getUtxo() }); let targets = []; for (const transaction of this.state.addresses) { - const amount = - transaction.amount === BitcoinUnit.MAX ? BitcoinUnit.MAX : new BigNumber(transaction.amount).multipliedBy(100000000).toNumber(); - if (amount > 0.0 || amount === BitcoinUnit.MAX) { - targets.push({ address: transaction.address, value: amount }); + if (transaction.amount === BitcoinUnit.MAX) { + // single output with MAX + targets = [{ address: transaction.address }]; + break; + } + const value = new BigNumber(transaction.amount).multipliedBy(100000000).toNumber(); + if (value > 0) { + targets.push({ address: transaction.address, value }); } - } - - if (firstTransaction.amount === BitcoinUnit.MAX) { - targets = [{ address: firstTransaction.address, amount: BitcoinUnit.MAX }]; } let { tx, fee, psbt } = wallet.createTransaction( @@ -849,7 +731,11 @@ export default class SendDetails extends Component { renderCreateButton = () => { return ( - {this.state.isLoading ? : this.createTransaction()} title={'Next'} />} + {this.state.isLoading ? ( + + ) : ( + this.createTransaction()} title={'Next'} testID={'CreateTransactionButton'} /> + )} ); }; diff --git a/screen/send/psbtWithHardwareWallet.js b/screen/send/psbtWithHardwareWallet.js index b7b05a74..35247017 100644 --- a/screen/send/psbtWithHardwareWallet.js +++ b/screen/send/psbtWithHardwareWallet.js @@ -14,7 +14,7 @@ import { PermissionsAndroid, } from 'react-native'; import QRCode from 'react-native-qrcode-svg'; -import { Icon, Text } from 'react-native-elements'; +import { Text } from 'react-native-elements'; import { BlueButton, BlueText, @@ -23,6 +23,7 @@ import { BlueNavigationStyle, BlueSpacing20, BlueCopyToClipboardButton, + BlueBigCheckmark, } from '../../BlueComponents'; import PropTypes from 'prop-types'; import Share from 'react-native-share'; @@ -121,7 +122,6 @@ export default class PsbtWithHardwareWallet extends Component { await BlueElectrum.waitTillConnected(); let result = await this.state.fromWallet.broadcastTx(this.state.txhex); if (result) { - console.log('broadcast result = ', result); EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs this.setState({ success: true, isLoading: false }); if (this.state.memo) { @@ -180,20 +180,7 @@ export default class PsbtWithHardwareWallet extends Component { _renderSuccess() { return ( - - - + diff --git a/screen/transactions/CPFP.js b/screen/transactions/CPFP.js index f0f77ea3..c1ce81f5 100644 --- a/screen/transactions/CPFP.js +++ b/screen/transactions/CPFP.js @@ -10,10 +10,11 @@ import { BlueText, BlueSpacing, BlueNavigationStyle, + BlueBigCheckmark, } from '../../BlueComponents'; import PropTypes from 'prop-types'; import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class'; -import { Icon, Text } from 'react-native-elements'; +import { Text } from 'react-native-elements'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; /** @type {AppStorage} */ let EV = require('../../events'); @@ -50,7 +51,6 @@ export default class CPFP extends Component { await BlueElectrum.waitTillConnected(); let result = await this.state.wallet.broadcastTx(this.state.txhex); if (result) { - console.log('broadcast result = ', result); EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs this.setState({ stage: 3, isLoading: false }); this.onSuccessBroadcast(); @@ -192,20 +192,7 @@ export default class CPFP extends Component { - - - + { diff --git a/screen/wallets/add.js b/screen/wallets/add.js index e500b565..7de3e45d 100644 --- a/screen/wallets/add.js +++ b/screen/wallets/add.js @@ -93,8 +93,8 @@ export default class WalletsAdd extends Component { return ( - - + + {loc.wallets.add.wallet_name} { @@ -336,8 +337,8 @@ export default class WalletsAdd extends Component { }} /> - - + + ); } diff --git a/screen/wallets/buyBitcoin.js b/screen/wallets/buyBitcoin.js index de26dd9f..9aefca92 100644 --- a/screen/wallets/buyBitcoin.js +++ b/screen/wallets/buyBitcoin.js @@ -3,6 +3,7 @@ import { BlueNavigationStyle, BlueLoading, SafeBlueArea } from '../../BlueCompon import PropTypes from 'prop-types'; import { WebView } from 'react-native-webview'; import { AppStorage, LightningCustodianWallet, WatchOnlyWallet } from '../../class'; +const currency = require('../../currency'); let BlueApp: AppStorage = require('../../BlueApp'); let loc = require('../../loc'); @@ -27,6 +28,9 @@ export default class BuyBitcoin extends Component { async componentDidMount() { console.log('buyBitcoin - componentDidMount'); + let preferredCurrency = await currency.getPreferredCurrency(); + preferredCurrency = preferredCurrency.endPointKey; + /** @type {AbstractHDWallet|WatchOnlyWallet|LightningCustodianWallet} */ let wallet = this.state.wallet; @@ -38,6 +42,7 @@ export default class BuyBitcoin extends Component { this.setState({ isLoading: false, address, + preferredCurrency, }); return; } @@ -62,6 +67,7 @@ export default class BuyBitcoin extends Component { this.setState({ isLoading: false, address, + preferredCurrency, }); } @@ -78,6 +84,10 @@ export default class BuyBitcoin extends Component { uri += '&safelloStateToken=' + safelloStateToken; } + if (this.state.preferredCurrency) { + uri += '¤cy=' + this.state.preferredCurrency; + } + return ( )} + this.props.navigation.navigate('Broadcast')} title="Broadcast transaction" /> { useEffect(() => { Privacy.enableBlur(); - return () => Privacy.disableBlur(); - }); + Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', () => setIsToolbarVisibleForAndroid(true)); + Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => setIsToolbarVisibleForAndroid(false)); + return () => { + Keyboard.removeListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'); + Keyboard.removeListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'); + Privacy.disableBlur(); + }; + }, []); const importButtonPressed = () => { if (importText.trim().length === 0) { @@ -62,54 +68,20 @@ const WalletsImport = () => { return ( - - - {loc.wallets.import.explanation} - - setIsToolbarVisibleForAndroid(true)} - onBlur={() => setIsToolbarVisibleForAndroid(false)} - /> - {Platform.select({ - ios: ( - { - setImportText(''); - Keyboard.dismiss(); - }} - onPasteTapped={text => { - setImportText(text); - Keyboard.dismiss(); - }} - /> - ), - android: isToolbarVisibleForAndroid && ( - { - setImportText(''); - Keyboard.dismiss(); - }} - onPasteTapped={text => { - setImportText(text); - Keyboard.dismiss(); - }} - /> - ), - })} - - + {loc.wallets.import.explanation} + + - + { }} onPress={importButtonPressed} /> + { @@ -124,6 +97,32 @@ const WalletsImport = () => { }} /> + {Platform.select({ + ios: ( + { + setImportText(''); + Keyboard.dismiss(); + }} + onPasteTapped={text => { + setImportText(text); + Keyboard.dismiss(); + }} + /> + ), + android: isToolbarVisibleForAndroid && ( + { + setImportText(''); + Keyboard.dismiss(); + }} + onPasteTapped={text => { + setImportText(text); + Keyboard.dismiss(); + }} + /> + ), + })} ); }; diff --git a/screen/wallets/list.js b/screen/wallets/list.js index 510aab7f..1eec60bd 100644 --- a/screen/wallets/list.js +++ b/screen/wallets/list.js @@ -20,7 +20,7 @@ import { PlaceholderWallet } from '../../class'; import WalletImport from '../../class/walletImport'; import ViewPager from '@react-native-community/viewpager'; import ScanQRCode from '../send/ScanQRCode'; -import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; +import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; let EV = require('../../events'); let A = require('../../analytics'); /** @type {AppStorage} */ diff --git a/screen/wallets/pleaseBackup.js b/screen/wallets/pleaseBackup.js index 3138d552..d3486963 100644 --- a/screen/wallets/pleaseBackup.js +++ b/screen/wallets/pleaseBackup.js @@ -1,430 +1,95 @@ -import React, { Component } from 'react'; -import { ActivityIndicator, View, BackHandler, Text } from 'react-native'; +import React, { useEffect, useState, useCallback } from 'react'; +import { ActivityIndicator, View, BackHandler, Text, ScrollView } from 'react-native'; import { BlueSpacing20, SafeBlueArea, BlueNavigationStyle, BlueText, BlueButton } from '../../BlueComponents'; import { Badge } from 'react-native-elements'; -import PropTypes from 'prop-types'; import Privacy from '../../Privacy'; -import { ScrollView } from 'react-native-gesture-handler'; -let loc = require('../../loc'); +import { useNavigation, useNavigationParam } from 'react-navigation-hooks'; +const loc = require('../../loc'); -export default class PleaseBackup extends Component { - static navigationOptions = ({ navigation }) => ({ - ...BlueNavigationStyle(navigation, true), - title: loc.pleasebackup.title, - headerLeft: null, - headerRight: null, - }); +const PleaseBackup = () => { + const [isLoading, setIsLoading] = useState(true); + const words = useNavigationParam('secret').split(' '); + const { dismiss } = useNavigation(); - constructor(props) { - super(props); - - this.state = { - isLoading: true, - words: props.navigation.state.params.secret.split(' '), - }; - BackHandler.addEventListener('hardwareBackPress', this.handleBackButton.bind(this)); - } - - handleBackButton() { - this.props.navigation.dismiss(); + const handleBackButton = useCallback(() => { + dismiss(); return true; - } + }, [dismiss]); - componentDidMount() { + useEffect(() => { Privacy.enableBlur(); - this.setState({ - isLoading: false, - }); - } - - componentWillUnmount() { - Privacy.disableBlur(); - BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton.bind(this)); - } + setIsLoading(false); + return () => { + Privacy.disableBlur(); + BackHandler.removeEventListener('hardwareBackPress', handleBackButton); + }; + }, [handleBackButton, words]); - render() { - if (this.state.isLoading) { - return ( - - - + const renderSecret = () => { + let component = []; + for (const [index, secret] of words.entries()) { + component.push( + + + + {`${index}`}. {secret} + + + , ); } + return component; + }; - return ( - - - - {loc.pleasebackup.success} - {loc.pleasebackup.text} + return isLoading ? ( + + + + ) : ( + + + + {loc.pleasebackup.success} + {loc.pleasebackup.text} - - - - 1. {this.state.words[0]} - - - - - 2. {this.state.words[1]} - - - - - 3. {this.state.words[2]} - - - - - 4. {this.state.words[3]} - - - - - 5. {this.state.words[4]} - - - - - 6. {this.state.words[5]} - - - - - 7. {this.state.words[6]} - - - - - 8. {this.state.words[7]} - - - - - 9. {this.state.words[8]} - - - - - 10. {this.state.words[9]} - - - - - 11. {this.state.words[10]} - - - - - 12. {this.state.words[11]} - - - - - 13. {this.state.words[12]} - - - - - 14. {this.state.words[13]} - - - - - 15. {this.state.words[14]} - - - - - 16. {this.state.words[15]} - - - - - 17. {this.state.words[16]} - - - - - 18. {this.state.words[17]} - - - - - 19. {this.state.words[18]} - - - - - 20. {this.state.words[19]} - - - - - 21. {this.state.words[20]} - - - - - 22. {this.state.words[21]} - - - - - 23. {this.state.words[22]} - - - - - 24. {this.state.words[23]} - - - + + {renderSecret()} + - - - - this.props.navigation.dismiss()} title={loc.pleasebackup.ok} /> - + + + + - - - ); - } -} - -PleaseBackup.propTypes = { - navigation: PropTypes.shape({ - state: PropTypes.shape({ - params: PropTypes.shape({ - secret: PropTypes.string, - }), - }), - dismiss: PropTypes.func, - }), + + + + ); }; + +PleaseBackup.navigationOptions = ({ navigation }) => ({ + ...BlueNavigationStyle(navigation, true), + title: loc.pleasebackup.title, + headerLeft: null, + headerRight: null, +}); + +export default PleaseBackup; diff --git a/tests/e2e/bluewallet.spec.js b/tests/e2e/bluewallet.spec.js index 3d58558d..e23f9164 100644 --- a/tests/e2e/bluewallet.spec.js +++ b/tests/e2e/bluewallet.spec.js @@ -1,5 +1,8 @@ /* global it, describe, expect, element, by, waitFor, device */ +const bitcoin = require('bitcoinjs-lib'); +const assert = require('assert'); + describe('BlueWallet UI Tests', () => { it('selftest passes', async () => { await waitFor(element(by.id('WalletsList'))) @@ -300,6 +303,78 @@ describe('BlueWallet UI Tests', () => { await yo('WalletsList'); await expect(element(by.id('cr34t3d'))).toBeVisible(); }); + + it('can import BIP84 mnemonic, fetch balance & transactions, then create a transaction', async () => { + if (!process.env.HD_MNEMONIC_BIP84) { + console.error('process.env.HD_MNEMONIC_BIP84 not set, skipped'); + return; + } + await yo('WalletsList'); + + // going to Import Wallet screen and importing mnemonic for existing BIP84 wallet with real balance + await element(by.id('CreateAWallet')).tap(); + await element(by.id('ImportWallet')).tap(); + await element(by.id('MnemonicInput')).typeText(process.env.HD_MNEMONIC_BIP84); + try { + await element(by.id('DoImport')).tap(); + } catch (_) {} + await sleep(60000); + await sup('OK', 3 * 61000); // waiting for wallet import + await element(by.text('OK')).tap(); + // ok, wallet imported + + // lets go inside wallet + await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap(); + // label might change in the future; see HDSegwitBech32Wallet.typeReadable + expect(element(by.id('WalletBalance'))).toHaveText('0.00105526 BTC'); + + // lets create real transaction: + await element(by.id('SendButton')).tap(); + await element(by.id('AddressInput')).typeText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl'); + await element(by.id('BitcoinAmountInput')).typeText('0.0005\n'); + await sleep(5000); + try { + await element(by.id('CreateTransactionButton')).tap(); + } catch (_) {} + + // created. verifying: + await yo('TransactionValue'); + expect(element(by.id('TransactionValue'))).toHaveText('0.0005'); + await element(by.id('TransactionDetailsButton')).tap(); + + // now, a hack to extract element text. warning, this might break in future + // @see https://github.com/wix/detox/issues/445 + + let txhex = ''; + try { + await expect(element(by.id('TxhexInput'))).toHaveText('_unfoundable_text'); + } catch (error) { + if (device.getPlatform() === 'ios') { + const start = `accessibilityLabel was "`; + const end = '" on '; + const errorMessage = error.message.toString(); + const [, restMessage] = errorMessage.split(start); + const [label] = restMessage.split(end); + txhex = label; + } else { + const start = 'Got:'; + const end = '}"'; + const errorMessage = error.message.toString(); + const [, restMessage] = errorMessage.split(start); + const [label] = restMessage.split(end); + const value = label.split(','); + var combineText = value.find(i => i.includes('text=')).trim(); + const [, elementText] = combineText.split('='); + txhex = elementText; + } + } + + let transaction = bitcoin.Transaction.fromHex(txhex); + assert.strictEqual(transaction.ins.length, 2); + assert.strictEqual(transaction.outs.length, 2); + assert.strictEqual(bitcoin.address.fromOutputScript(transaction.outs[0].script), 'bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl'); // to address + assert.strictEqual(transaction.outs[0].value, 50000); + }); }); async function sleep(ms) { @@ -312,6 +387,12 @@ async function yo(id, timeout = 33000) { .withTimeout(timeout); } +async function sup(text, timeout = 33000) { + return waitFor(element(by.text(text))) + .toBeVisible() + .withTimeout(timeout); +} + async function helperCreateWallet(walletName) { await element(by.id('CreateAWallet')).tap(); await element(by.id('WalletNameInput')).typeText(walletName || 'cr34t3d'); diff --git a/tests/integration/hd-segwit-p2sh-wallet.test.js b/tests/integration/hd-segwit-p2sh-wallet.test.js index cc5fec0a..ad97ee9a 100644 --- a/tests/integration/hd-segwit-p2sh-wallet.test.js +++ b/tests/integration/hd-segwit-p2sh-wallet.test.js @@ -1,6 +1,5 @@ /* global it, jasmine, afterAll, beforeAll */ -import { HDSegwitP2SHWallet, HDLegacyBreadwalletWallet, HDLegacyP2PKHWallet } from '../../class'; -import { BitcoinUnit } from '../../models/bitcoinUnits'; +import { HDSegwitP2SHWallet } from '../../class'; const bitcoin = require('bitcoinjs-lib'); global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment let assert = require('assert'); @@ -68,18 +67,22 @@ it('HD (BIP49) can create TX', async () => { assert.ok(hd.utxo[0].amount); assert.ok(hd.utxo[0].address); assert.ok(hd.utxo[0].wif); - let txhex = hd.createTx(hd.utxo, 0.000014, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); + + let txNew = hd.createTransaction( + hd.getUtxo(), + [{ address: '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', value: 500 }], + 1, + hd._getInternalAddressByIndex(hd.next_free_change_address_index), + ); + let tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); assert.strictEqual( - txhex, - '0100000000010187c9acd9d5714845343b18abaa26cb83299be2487c22da9c0e270f241b4d9cfe0000000017160014a239b6a0cbc7aadc2e77643de36306a6167fad15ffffffff02780500000000000017a914a3a65daca3064280ae072b9d6773c027b30abace87b45f00000000000017a9140acff2c37ed45110baece4bb9d4dcc0c6309dbbd8702483045022100f489dfbd372b66348a25f6e9ba1b5eb88a3646efcd75ef1211c96cf46eed692c0220416ac99a94c5f4a076588291d9857fc5b854e02404d69635dc35e82fde3ecd9701210202ac3bd159e54dc31e65842ad5f9a10b4eb024e83864a319b27de65ee08b2a3900000000', + txNew.tx.toHex(), + '0200000000010187c9acd9d5714845343b18abaa26cb83299be2487c22da9c0e270f241b4d9cfe0000000017160014a239b6a0cbc7aadc2e77643de36306a6167fad150000008002f40100000000000017a914a3a65daca3064280ae072b9d6773c027b30abace87bb6200000000000017a9140acff2c37ed45110baece4bb9d4dcc0c6309dbbd87024830450221008506675a240c6a49fc5daf0332e44245991a1dfa4c8742d56e81687097e5b98b0220042e4bd3f69a842c7ac4013c2fd01151b098cc9bf889d53959475d6c8b47a32101210202ac3bd159e54dc31e65842ad5f9a10b4eb024e83864a319b27de65ee08b2a3900000000', ); - - txhex = hd.createTx(hd.utxo, 0.000005, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); - var tx = bitcoin.Transaction.fromHex(txhex); assert.strictEqual(tx.ins.length, 1); assert.strictEqual(tx.outs.length, 2); assert.strictEqual(tx.outs[0].value, 500); - assert.strictEqual(tx.outs[1].value, 25400); + assert.strictEqual(tx.outs[1].value, 25275); let toAddress = bitcoin.address.fromOutputScript(tx.outs[0].script); let changeAddress = bitcoin.address.fromOutputScript(tx.outs[1].script); assert.strictEqual('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress); @@ -87,48 +90,62 @@ it('HD (BIP49) can create TX', async () => { // - txhex = hd.createTx(hd.utxo, 0.000015, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); - tx = bitcoin.Transaction.fromHex(txhex); - assert.strictEqual(tx.ins.length, 1); - assert.strictEqual(tx.outs.length, 2); - - // - - txhex = hd.createTx(hd.utxo, 0.00025, 0.00001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); - tx = bitcoin.Transaction.fromHex(txhex); + txNew = hd.createTransaction( + hd.getUtxo(), + [{ address: '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', value: 25000 }], + 5, + hd._getInternalAddressByIndex(hd.next_free_change_address_index), + ); + tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); assert.strictEqual(tx.ins.length, 1); assert.strictEqual(tx.outs.length, 1); toAddress = bitcoin.address.fromOutputScript(tx.outs[0].script); assert.strictEqual('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress); // testing sendMAX - hd.utxo = [ + + const utxo = [ { - txid: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + height: 591862, + value: 26000, + address: '3C5iv2Hp6nfuhkfTZibb7GJPkXj367eurD', vout: 0, + txid: '2000000000000000000000000000000000000000000000000000000000000000', amount: 26000, - address: '39SpCj47M88ajRBTbkfaKRgpaX7FTLQJz5', wif: 'L3fg5Jb6tJDVMvoG2boP4u3CxjX1Er3e7Z4zDALQdGgVLLE8zVUr', + confirmations: 1, }, { - txid: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + height: 591862, + value: 26000, + address: '3C5iv2Hp6nfuhkfTZibb7GJPkXj367eurD', vout: 0, + txid: '1000000000000000000000000000000000000000000000000000000000000000', amount: 26000, - address: '39SpCj47M88ajRBTbkfaKRgpaX7FTLQJz5', wif: 'L3fg5Jb6tJDVMvoG2boP4u3CxjX1Er3e7Z4zDALQdGgVLLE8zVUr', + confirmations: 1, }, { - txid: 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + height: 591862, + value: 26000, + address: '3C5iv2Hp6nfuhkfTZibb7GJPkXj367eurD', vout: 0, + txid: '0000000000000000000000000000000000000000000000000000000000000000', amount: 26000, - address: '39SpCj47M88ajRBTbkfaKRgpaX7FTLQJz5', wif: 'L3fg5Jb6tJDVMvoG2boP4u3CxjX1Er3e7Z4zDALQdGgVLLE8zVUr', + confirmations: 1, }, ]; - txhex = hd.createTx(hd.utxo, BitcoinUnit.MAX, 0.00003, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); - tx = bitcoin.Transaction.fromHex(txhex); + + txNew = hd.createTransaction( + utxo, + [{ address: '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK' }], + 1, + hd._getInternalAddressByIndex(hd.next_free_change_address_index), + ); + tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); assert.strictEqual(tx.outs.length, 1); - assert.strictEqual(tx.outs[0].value, 75000); + assert.ok(tx.outs[0].value > 77000); }); it('Segwit HD (BIP49) can fetch balance with many used addresses in hierarchy', async function() { @@ -156,114 +173,3 @@ it('Segwit HD (BIP49) can fetch balance with many used addresses in hierarchy', await hd.fetchTransactions(); assert.strictEqual(hd.getTransactions().length, 107); }); - -it('can create a Legacy HD (BIP44)', async function() { - if (!process.env.HD_MNEMONIC_BREAD) { - console.error('process.env.HD_MNEMONIC_BREAD not set, skipped'); - return; - } - - let mnemonic = process.env.HD_MNEMONIC_BREAD; - let hd = new HDLegacyP2PKHWallet(); - hd.setSecret(mnemonic); - assert.strictEqual(hd.validateMnemonic(), true); - assert.strictEqual(hd._getExternalAddressByIndex(0), '12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'); - assert.strictEqual(hd._getExternalAddressByIndex(1), '1QDCFcpnrZ4yrAQxmbvSgeUC9iZZ8ehcR5'); - assert.strictEqual(hd._getInternalAddressByIndex(0), '1KZjqYHm7a1DjhjcdcjfQvYfF2h6PqatjX'); - assert.strictEqual(hd._getInternalAddressByIndex(1), '13CW9WWBsWpDUvLtbFqYziWBWTYUoQb4nU'); - assert.strictEqual( - hd.getXpub(), - 'xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps', - ); - - assert.strictEqual(hd._getExternalWIFByIndex(0), 'L1hqNoJ26YuCdujMBJfWBNfgf4Jo7AcKFvcNcKLoMtoJDdDtRq7Q'); - assert.strictEqual(hd._getExternalWIFByIndex(1), 'KyyH4h59iatJWwFfiYPnYkw39SP7cBwydC3xzszsBBXHpfwz9cKb'); - assert.strictEqual(hd._getInternalWIFByIndex(0), 'Kx3QkrfemEEV49Mj5oWfb4bsWymboPdstta7eN3kAzop9apxYEFP'); - assert.strictEqual(hd._getInternalWIFByIndex(1), 'Kwfg1EDjFapN9hgwafdNPEH22z3vkd4gtG785vXXjJ6uvVWAJGtr'); - await hd.fetchBalance(); - assert.strictEqual(hd.balance, 0); - assert.ok(hd._lastTxFetch === 0); - await hd.fetchTransactions(); - assert.ok(hd._lastTxFetch > 0); - assert.strictEqual(hd.getTransactions().length, 4); - assert.strictEqual(hd.next_free_address_index, 1); - assert.strictEqual(hd.getNextFreeAddressIndex(), 1); - assert.strictEqual(hd.next_free_change_address_index, 1); - - for (let tx of hd.getTransactions()) { - assert.ok(tx.value === 1000 || tx.value === 1377 || tx.value === -1377 || tx.value === -1000); - } - - // checking that internal pointer and async address getter return the same address - let freeAddress = await hd.getAddressAsync(); - assert.strictEqual(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress); - assert.strictEqual(hd._getExternalAddressByIndex(hd.getNextFreeAddressIndex()), freeAddress); -}); - -it('Legacy HD (BIP44) can create TX', async () => { - if (!process.env.HD_MNEMONIC) { - console.error('process.env.HD_MNEMONIC not set, skipped'); - return; - } - let hd = new HDLegacyP2PKHWallet(); - hd.setSecret(process.env.HD_MNEMONIC); - assert.ok(hd.validateMnemonic()); - - await hd.fetchBalance(); - await hd.fetchUtxo(); - assert.strictEqual(hd.utxo.length, 4); - let txhex = hd.createTx(hd.utxo, 0.0008, 0.000005, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); - - assert.strictEqual( - txhex, - '01000000045fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f000000006b48304502210080ffbde0d510c3fb9abcc5f7570448e9c0f7138d0b355d00bb97cada0679ac9502207ffd205373829c800ec08079a4280c3abe6f6f8c94ae7af0157a14ea5629d28701210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f010000006a473044022077788d7e118802fd7268aac7a1dde5a6724f01936e23edd46ac2750fd39265be0220776ac9e4c285580d06510a00b561cec6de1813293e7b04b6f870138af832bf9e012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f020000006b4830450221009e47b48dd1eee6d00a1817480605f446e11949b1e6f464f43f04bce2fc787ea9022022b3dcf80e7b2c995cf6defb3425b57d8a80918c7f543faaa0497d853820779101210316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667ffffffff5fbc74110c2d6fcf4d1161a59913fbcd2b6ab3c5a9eb4d0dc0859515cbc8654f030000006b48304502210089c20d6c0f6486c5979cf69a3c849f09e36416e5604499c05ae2dc22bea8553d022011241a206d550e55b4476ac5ba0fd744f0965d8f8bd69a740e428770689749a1012102ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002ffffffff02803801000000000017a914a3a65daca3064280ae072b9d6773c027b30abace872c4c0000000000001976a9146ee5e3e66dc73587a3a2d77a1a6c8554fae21b8a88ac00000000', - ); - - var tx = bitcoin.Transaction.fromHex(txhex); - assert.strictEqual(tx.ins.length, 4); - assert.strictEqual(tx.outs.length, 2); - assert.strictEqual(tx.outs[0].value, 80000); // payee - assert.strictEqual(tx.outs[1].value, 19500); // change - let toAddress = bitcoin.address.fromOutputScript(tx.outs[0].script); - let changeAddress = bitcoin.address.fromOutputScript(tx.outs[1].script); - assert.strictEqual('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress); - assert.strictEqual(hd._getInternalAddressByIndex(hd.next_free_change_address_index), changeAddress); - - // checking that change amount is at least 3x of fee, otherwise screw the change, just add it to fee. - // theres 0.001 on UTXOs, lets transfer (0.001 - 100sat), soo fee is equal to change (100 sat) - // which throws @dust error if broadcasted - txhex = hd.createTx(hd.utxo, 0.000998, 0.000001, '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'); - tx = bitcoin.Transaction.fromHex(txhex); - assert.strictEqual(tx.ins.length, 4); - assert.strictEqual(tx.outs.length, 1); // only 1 output, which means change is neglected - assert.strictEqual(tx.outs[0].value, 99800); -}); - -it('HD breadwallet works', async function() { - if (!process.env.HD_MNEMONIC_BREAD) { - console.error('process.env.HD_MNEMONIC_BREAD not set, skipped'); - return; - } - let hdBread = new HDLegacyBreadwalletWallet(); - hdBread.setSecret(process.env.HD_MNEMONIC_BREAD); - - assert.strictEqual(hdBread.validateMnemonic(), true); - assert.strictEqual(hdBread._getExternalAddressByIndex(0), '1ARGkNMdsBE36fJhddSwf8PqBXG3s4d2KU'); - assert.strictEqual(hdBread._getInternalAddressByIndex(0), '1JLvA5D7RpWgChb4A5sFcLNrfxYbyZdw3V'); - - assert.strictEqual( - hdBread.getXpub(), - 'xpub68nLLEi3KERQY7jyznC9PQSpSjmekrEmN8324YRCXayMXaavbdEJsK4gEcX2bNf9vGzT4xRks9utZ7ot1CTHLtdyCn9udvv1NWvtY7HXroh', - ); - await hdBread.fetchBalance(); - assert.strictEqual(hdBread.getBalance(), 123456); - - assert.strictEqual(hdBread.next_free_address_index, 11); - assert.strictEqual(hdBread.getNextFreeAddressIndex(), 11); - assert.strictEqual(hdBread.next_free_change_address_index, 118); - - // checking that internal pointer and async address getter return the same address - let freeAddress = await hdBread.getAddressAsync(); - assert.strictEqual(hdBread._getExternalAddressByIndex(hdBread.next_free_address_index), freeAddress); - assert.strictEqual(hdBread._getExternalAddressByIndex(hdBread.getNextFreeAddressIndex()), freeAddress); -}); diff --git a/tests/integration/legacy-wallet.test.js b/tests/integration/legacy-wallet.test.js index f6c7da14..05839f34 100644 --- a/tests/integration/legacy-wallet.test.js +++ b/tests/integration/legacy-wallet.test.js @@ -76,8 +76,8 @@ describe('LegacyWallet', function() { assert.ok(w.getUtxo().length > 0, 'unexpected empty UTXO'); assert.ok(w.getUtxo()[0]['value']); - assert.ok(w.getUtxo()[0]['tx_output_n'] === 0 || w.getUtxo()[0]['tx_output_n'] === 1, JSON.stringify(w.getUtxo()[0])); - assert.ok(w.getUtxo()[0]['tx_hash']); + assert.ok(w.getUtxo()[0]['vout'] === 1, JSON.stringify(w.getUtxo()[0])); + assert.ok(w.getUtxo()[0]['txid']); assert.ok(w.getUtxo()[0]['confirmations']); }); }); @@ -109,9 +109,9 @@ describe('SegwitBech32Wallet', function() { assert.ok(w.getUtxo().length > 0, 'unexpected empty UTXO'); assert.ok(w.getUtxo()[0]['value']); - assert.ok(w.getUtxo()[0]['tx_output_n'] === 0); - assert.ok(w.getUtxo()[0]['tx_hash']); - assert.ok(w.getUtxo()[0]['confirmations']); + assert.ok(w.getUtxo()[0]['vout'] === 0); + assert.ok(w.getUtxo()[0]['txid']); + assert.ok(w.getUtxo()[0]['confirmations'], JSON.stringify(w.getUtxo()[0], null, 2)); // double fetch shouldnt duplicate UTXOs: await w.fetchUtxo(); const l2 = w.getUtxo().length; diff --git a/tests/unit/deepLinkSchemaMatch.test.js b/tests/unit/deeplink-schema-match.test.js similarity index 51% rename from tests/unit/deepLinkSchemaMatch.test.js rename to tests/unit/deeplink-schema-match.test.js index 45de7c76..2b844276 100644 --- a/tests/unit/deepLinkSchemaMatch.test.js +++ b/tests/unit/deeplink-schema-match.test.js @@ -1,5 +1,5 @@ /* global describe, it */ -import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch'; +import DeeplinkSchemaMatch from '../../class/deeplink-schema-match'; const assert = require('assert'); describe('unit - DeepLinkSchemaMatch', function() { @@ -7,12 +7,18 @@ describe('unit - DeepLinkSchemaMatch', function() { assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:bc1qh6tf004ty7z7un2v5ntu4mkf630545gvhs45u7?amount=666&label=Yo')); assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:BC1QH6TF004TY7Z7UN2V5NTU4MKF630545GVHS45U7?amount=666&label=Yo')); + assert.ok(DeeplinkSchemaMatch.hasSchema('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE')); + assert.ok(DeeplinkSchemaMatch.hasSchema('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE?amount=666&label=Yo')); }); it('isBitcoin Address', () => { assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG')); + assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK')); assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('bc1qykcp2x3djgdtdwelxn9z4j2y956npte0a4sref')); assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BC1QYKCP2X3DJGDTDWELXN9Z4J2Y956NPTE0A4SREF')); + assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('bitcoin:BC1QH6TF004TY7Z7UN2V5NTU4MKF630545GVHS45U7')); + assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE')); + assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE?amount=666&label=Yo')); }); it('isLighting Invoice', () => { @@ -26,7 +32,12 @@ describe('unit - DeepLinkSchemaMatch', function() { it('isBoth Bitcoin & Invoice', () => { assert.ok( DeeplinkSchemaMatch.isBothBitcoinAndLightning( - 'bitcoin:1DamianM2k8WfNEeJmyqSe2YW1upB7UATx?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4', + 'bitcoin:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4', + ), + ); + assert.ok( + DeeplinkSchemaMatch.isBothBitcoinAndLightning( + 'BITCOIN:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4', ), ); }); @@ -55,4 +66,34 @@ describe('unit - DeepLinkSchemaMatch', function() { }); }); }); + + it('decodes bip21', () => { + let decoded = DeeplinkSchemaMatch.bip21decode('bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar'); + assert.deepStrictEqual(decoded, { + address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', + options: { + amount: 20.3, + label: 'Foobar', + }, + }); + + decoded = DeeplinkSchemaMatch.bip21decode('BITCOIN:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar'); + assert.deepStrictEqual(decoded, { + address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', + options: { + amount: 20.3, + label: 'Foobar', + }, + }); + }); + + it('encodes bip21', () => { + let encoded = DeeplinkSchemaMatch.bip21encode('1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH'); + assert.strictEqual(encoded, 'bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH'); + encoded = DeeplinkSchemaMatch.bip21encode('1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', { + amount: 20.3, + label: 'Foobar', + }); + assert.strictEqual(encoded, 'bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar'); + }); }); diff --git a/tests/unit/hd-legacy-breadwallet.test.js b/tests/unit/hd-legacy-breadwallet.test.js new file mode 100644 index 00000000..370f5c5c --- /dev/null +++ b/tests/unit/hd-legacy-breadwallet.test.js @@ -0,0 +1,27 @@ +/* global it */ +import { HDLegacyBreadwalletWallet } from '../../class'; +const assert = require('assert'); + +it('Legacy HD Breadwallet works', async () => { + let hdBread = new HDLegacyBreadwalletWallet(); + hdBread.setSecret(process.env.HD_MNEMONIC_BREAD); + + assert.strictEqual(hdBread.validateMnemonic(), true); + assert.strictEqual(hdBread._getExternalAddressByIndex(0), '1ARGkNMdsBE36fJhddSwf8PqBXG3s4d2KU'); + assert.strictEqual(hdBread._getInternalAddressByIndex(0), '1JLvA5D7RpWgChb4A5sFcLNrfxYbyZdw3V'); + assert.strictEqual(hdBread._getExternalWIFByIndex(0), 'L25CoHfqWKR5byQhgp4M8sW1roifBteD3Lj3zCGNcV4JXhbxZ93F'); + assert.strictEqual(hdBread._getInternalWIFByIndex(0), 'KyEQuB73eueeS7D6iBJrNSvkD1kkdkJoUsavuxGXv5fxWkPJxt96'); + assert.strictEqual( + hdBread._getPubkeyByAddress(hdBread._getExternalAddressByIndex(0)).toString('hex'), + '0354d804a7943eb61ec13deef44586510506889175dc2f3a375867e4796debf2a9', + ); + assert.strictEqual( + hdBread._getPubkeyByAddress(hdBread._getInternalAddressByIndex(0)).toString('hex'), + '02d241fadf3e48ff30a93360f6ef255cc3a797c588c907615d096510a918f46dce', + ); + + assert.strictEqual( + hdBread.getXpub(), + 'xpub68nLLEi3KERQY7jyznC9PQSpSjmekrEmN8324YRCXayMXaavbdEJsK4gEcX2bNf9vGzT4xRks9utZ7ot1CTHLtdyCn9udvv1NWvtY7HXroh', + ); +}); diff --git a/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js b/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js index a2eadda9..62b6710f 100644 --- a/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js +++ b/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js @@ -4,12 +4,8 @@ let assert = require('assert'); describe('HDLegacyElectrumSeedP2PKHWallet', () => { it('can import mnemonics and generate addresses and WIFs', async function() { - if (!process.env.HD_ELECTRUM_SEED_LEGACY) { - console.error('process.env.HD_ELECTRUM_SEED_LEGACY not set, skipped'); - return; - } let hd = new HDLegacyElectrumSeedP2PKHWallet(); - hd.setSecret(process.env.HD_ELECTRUM_SEED_LEGACY); + hd.setSecret('receive happy wash prosper update pet neck acid try profit proud hungry '); assert.ok(hd.validateMnemonic()); let address = hd._getExternalAddressByIndex(0); @@ -24,6 +20,15 @@ describe('HDLegacyElectrumSeedP2PKHWallet', () => { wif = hd._getInternalWIFByIndex(0); assert.strictEqual(wif, 'L52d26QmYGW8ctHo1omM5fZeJMgaonSkEWCGpnEekNvkVUoqTsNF'); + assert.strictEqual( + hd._getPubkeyByAddress(hd._getExternalAddressByIndex(0)).toString('hex'), + '02a6e6b674f82796cb4776673d824bf0673364fab24e62dcbfff4c1a5b69e3519b', + ); + assert.strictEqual( + hd._getPubkeyByAddress(hd._getInternalAddressByIndex(0)).toString('hex'), + '0344708260d2a832fd430285a0b915859d73e6ed4c6c6a9cb73e9069a9de56fb23', + ); + hd.setSecret('bs'); assert.ok(!hd.validateMnemonic()); }); diff --git a/tests/unit/hd-legacy-wallet.test.js b/tests/unit/hd-legacy-wallet.test.js new file mode 100644 index 00000000..b7f6f3f4 --- /dev/null +++ b/tests/unit/hd-legacy-wallet.test.js @@ -0,0 +1,131 @@ +/* global it */ +import { HDLegacyP2PKHWallet } from '../../class'; +const assert = require('assert'); +const bitcoin = require('bitcoinjs-lib'); + +it('Legacy HD (BIP44) works', async () => { + if (!process.env.HD_MNEMONIC) { + console.error('process.env.HD_MNEMONIC not set, skipped'); + return; + } + let hd = new HDLegacyP2PKHWallet(); + hd.setSecret(process.env.HD_MNEMONIC); + assert.ok(hd.validateMnemonic()); + + assert.strictEqual( + hd.getXpub(), + 'xpub6ByZUAv558PPheJgcPYHpxPLwz8M7TtueYMAik84NADeQcvbzS8W3WxxJ3C9NzfYkMoChiMAumWbeEvMWhTVpH75NqGv5c9wF3wKDbfQShb', + ); + + assert.strictEqual(hd._getExternalAddressByIndex(0), '186FBQmCV5W1xY7ywaWtTZPAQNciVN8Por'); + assert.strictEqual(hd._getInternalAddressByIndex(0), '1J9zoJz5LsAJ361SQHYnLTWg46Tc2AXUCj'); + + assert.strictEqual(hd._getInternalWIFByIndex(0), 'L4ojevRtK81A8Kof3qyLS2M7HvsVDbUDENNhJqU4vf79w9yGnQLb'); + assert.strictEqual(hd._getExternalWIFByIndex(0), 'Kz6kLhdyDfSbKuVH25XVqBRztjmFe8X22Xe1hnFzEv79gJNMkTAH'); + + assert.strictEqual( + hd._getPubkeyByAddress(hd._getExternalAddressByIndex(0)).toString('hex'), + '0316e84a2556f30a199541633f5dda6787710ccab26771b7084f4c9e1104f47667', + ); + assert.strictEqual( + hd._getPubkeyByAddress(hd._getInternalAddressByIndex(0)).toString('hex'), + '02ad7b2216f3a2b38d56db8a7ee5c540fd12c4bbb7013106eff78cc2ace65aa002', + ); + + assert.strictEqual(hd._getDerivationPathByAddress(hd._getExternalAddressByIndex(0)), "m/84'/0'/0'/0/0"); // wrong, FIXME + assert.strictEqual(hd._getDerivationPathByAddress(hd._getInternalAddressByIndex(0)), "m/84'/0'/0'/1/0"); // wrong, FIXME +}); + +it.only('Legacy HD (BIP44) can create TX', async () => { + if (!process.env.HD_MNEMONIC) { + console.error('process.env.HD_MNEMONIC not set, skipped'); + return; + } + let hd = new HDLegacyP2PKHWallet(); + hd.setSecret(process.env.HD_MNEMONIC); + assert.ok(hd.validateMnemonic()); + + const utxo = [ + { + height: 554830, + value: 10000, + address: '186FBQmCV5W1xY7ywaWtTZPAQNciVN8Por', + txId: '4f65c8cb159585c00d4deba9c5b36a2bcdfb1399a561114dcf6f2d0c1174bc5f', + vout: 0, + txid: '4f65c8cb159585c00d4deba9c5b36a2bcdfb1399a561114dcf6f2d0c1174bc5f', + amount: 10000, + wif: 'Kz6kLhdyDfSbKuVH25XVqBRztjmFe8X22Xe1hnFzEv79gJNMkTAH', + confirmations: 1, + txhex: + '01000000000101e8d98effbb4fba4f0a89bcf217eb5a7e2f8efcae44f32ecacbc5d8cc3ce683c301000000171600148ba6d02e74c0a6e000e8b174eb2ed44e5ea211a6ffffffff0510270000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac204e0000000000001976a914bc2db6b74c8db9b188711dcedd511e6a305603f588ac30750000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac409c0000000000001976a914bc2db6b74c8db9b188711dcedd511e6a305603f588ac204716000000000017a914e286d58e53f9247a4710e51232cce0686f16873c8702483045022100af3800cd8171f154785cf13f46c092f61c1668f97db432bb4e7ed7bc812a8c6d022051bddca1eaf1ad8b5f3bd0ccde7447e56fd3c8709e5906f02ec6326e9a5b2ff30121039a421d5eb7c9de6590ae2a471cb556b60de8c6b056beb907dbdc1f5e6092f58800000000', + }, + { + height: 554830, + value: 20000, + address: '1J9zoJz5LsAJ361SQHYnLTWg46Tc2AXUCj', + txId: '4f65c8cb159585c00d4deba9c5b36a2bcdfb1399a561114dcf6f2d0c1174bc5f', + vout: 1, + txid: '4f65c8cb159585c00d4deba9c5b36a2bcdfb1399a561114dcf6f2d0c1174bc5f', + amount: 20000, + wif: 'L4ojevRtK81A8Kof3qyLS2M7HvsVDbUDENNhJqU4vf79w9yGnQLb', + confirmations: 1, + txhex: + '01000000000101e8d98effbb4fba4f0a89bcf217eb5a7e2f8efcae44f32ecacbc5d8cc3ce683c301000000171600148ba6d02e74c0a6e000e8b174eb2ed44e5ea211a6ffffffff0510270000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac204e0000000000001976a914bc2db6b74c8db9b188711dcedd511e6a305603f588ac30750000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac409c0000000000001976a914bc2db6b74c8db9b188711dcedd511e6a305603f588ac204716000000000017a914e286d58e53f9247a4710e51232cce0686f16873c8702483045022100af3800cd8171f154785cf13f46c092f61c1668f97db432bb4e7ed7bc812a8c6d022051bddca1eaf1ad8b5f3bd0ccde7447e56fd3c8709e5906f02ec6326e9a5b2ff30121039a421d5eb7c9de6590ae2a471cb556b60de8c6b056beb907dbdc1f5e6092f58800000000', + }, + { + height: 554830, + value: 30000, + address: '186FBQmCV5W1xY7ywaWtTZPAQNciVN8Por', + txId: '4f65c8cb159585c00d4deba9c5b36a2bcdfb1399a561114dcf6f2d0c1174bc5f', + vout: 2, + txid: '4f65c8cb159585c00d4deba9c5b36a2bcdfb1399a561114dcf6f2d0c1174bc5f', + amount: 30000, + wif: 'Kz6kLhdyDfSbKuVH25XVqBRztjmFe8X22Xe1hnFzEv79gJNMkTAH', + confirmations: 1, + txhex: + '01000000000101e8d98effbb4fba4f0a89bcf217eb5a7e2f8efcae44f32ecacbc5d8cc3ce683c301000000171600148ba6d02e74c0a6e000e8b174eb2ed44e5ea211a6ffffffff0510270000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac204e0000000000001976a914bc2db6b74c8db9b188711dcedd511e6a305603f588ac30750000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac409c0000000000001976a914bc2db6b74c8db9b188711dcedd511e6a305603f588ac204716000000000017a914e286d58e53f9247a4710e51232cce0686f16873c8702483045022100af3800cd8171f154785cf13f46c092f61c1668f97db432bb4e7ed7bc812a8c6d022051bddca1eaf1ad8b5f3bd0ccde7447e56fd3c8709e5906f02ec6326e9a5b2ff30121039a421d5eb7c9de6590ae2a471cb556b60de8c6b056beb907dbdc1f5e6092f58800000000', + }, + { + height: 554830, + value: 40000, + address: '1J9zoJz5LsAJ361SQHYnLTWg46Tc2AXUCj', + txId: '4f65c8cb159585c00d4deba9c5b36a2bcdfb1399a561114dcf6f2d0c1174bc5f', + vout: 3, + txid: '4f65c8cb159585c00d4deba9c5b36a2bcdfb1399a561114dcf6f2d0c1174bc5f', + amount: 40000, + wif: 'L4ojevRtK81A8Kof3qyLS2M7HvsVDbUDENNhJqU4vf79w9yGnQLb', + confirmations: 1, + txhex: + '01000000000101e8d98effbb4fba4f0a89bcf217eb5a7e2f8efcae44f32ecacbc5d8cc3ce683c301000000171600148ba6d02e74c0a6e000e8b174eb2ed44e5ea211a6ffffffff0510270000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac204e0000000000001976a914bc2db6b74c8db9b188711dcedd511e6a305603f588ac30750000000000001976a9144dc6cbf64df9ab106cee812c7501960b93e9217788ac409c0000000000001976a914bc2db6b74c8db9b188711dcedd511e6a305603f588ac204716000000000017a914e286d58e53f9247a4710e51232cce0686f16873c8702483045022100af3800cd8171f154785cf13f46c092f61c1668f97db432bb4e7ed7bc812a8c6d022051bddca1eaf1ad8b5f3bd0ccde7447e56fd3c8709e5906f02ec6326e9a5b2ff30121039a421d5eb7c9de6590ae2a471cb556b60de8c6b056beb907dbdc1f5e6092f58800000000', + }, + ]; + + let txNew = hd.createTransaction( + utxo, + [{ address: '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', value: 80000 }], + 1, + hd._getInternalAddressByIndex(hd.next_free_change_address_index), + ); + let tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); + assert.strictEqual(tx.ins.length, 4); + assert.strictEqual(tx.outs.length, 2); + assert.strictEqual(tx.outs[0].value, 80000); // payee + assert.strictEqual(tx.outs[1].value, 19334); // change + let toAddress = bitcoin.address.fromOutputScript(tx.outs[0].script); + let changeAddress = bitcoin.address.fromOutputScript(tx.outs[1].script); + assert.strictEqual('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress); + assert.strictEqual(hd._getInternalAddressByIndex(hd.next_free_change_address_index), changeAddress); + + // testing sendMax + txNew = hd.createTransaction( + utxo, + [{ address: '3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK' }], + 1, + hd._getInternalAddressByIndex(hd.next_free_change_address_index), + ); + tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); + assert.strictEqual(tx.ins.length, 4); + assert.strictEqual(tx.outs.length, 1); + toAddress = bitcoin.address.fromOutputScript(tx.outs[0].script); + assert.strictEqual('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK', toAddress); +}); diff --git a/tests/unit/hd-segwit-bech32-wallet.test.js b/tests/unit/hd-segwit-bech32-wallet.test.js index f81dcdeb..c143f507 100644 --- a/tests/unit/hd-segwit-bech32-wallet.test.js +++ b/tests/unit/hd-segwit-bech32-wallet.test.js @@ -25,6 +25,15 @@ describe('Bech32 Segwit HD (BIP84)', () => { assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el'); assert.ok(hd._getInternalAddressByIndex(0) !== hd._getInternalAddressByIndex(1)); + assert.strictEqual( + hd._getPubkeyByAddress(hd._getExternalAddressByIndex(0)).toString('hex'), + '0330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c', + ); + assert.strictEqual( + hd._getPubkeyByAddress(hd._getInternalAddressByIndex(0)).toString('hex'), + '03025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a6', + ); + assert.strictEqual(hd._getDerivationPathByAddress(hd._getExternalAddressByIndex(0)), "m/84'/0'/0'/0/0"); assert.strictEqual(hd._getDerivationPathByAddress(hd._getExternalAddressByIndex(1)), "m/84'/0'/0'/0/1"); assert.strictEqual(hd._getDerivationPathByAddress(hd._getInternalAddressByIndex(0)), "m/84'/0'/0'/1/0"); diff --git a/tests/unit/hd-segwit-p2sh-wallet.test.js b/tests/unit/hd-segwit-p2sh-wallet.test.js index 05cd1c2a..a75e6710 100644 --- a/tests/unit/hd-segwit-p2sh-wallet.test.js +++ b/tests/unit/hd-segwit-p2sh-wallet.test.js @@ -13,6 +13,18 @@ it('can create a Segwit HD (BIP49)', async function() { assert.strictEqual('32yn5CdevZQLk3ckuZuA8fEKBco8mEkLei', hd._getInternalAddressByIndex(0)); assert.strictEqual(true, hd.validateMnemonic()); + assert.strictEqual( + hd._getPubkeyByAddress(hd._getExternalAddressByIndex(0)).toString('hex'), + '0348192db90b753484601aaf1e6220644ffe37d83a9a5feff32b4da43739f736be', + ); + assert.strictEqual( + hd._getPubkeyByAddress(hd._getInternalAddressByIndex(0)).toString('hex'), + '03c107e6976d59e17490513fbed3fb321736b7231d24f3d09306c72714acf1859d', + ); + + assert.strictEqual(hd._getDerivationPathByAddress(hd._getExternalAddressByIndex(0)), "m/84'/0'/0'/0/0"); // wrong, FIXME + assert.strictEqual(hd._getDerivationPathByAddress(hd._getInternalAddressByIndex(0)), "m/84'/0'/0'/1/0"); // wrong, FIXME + assert.strictEqual('L4MqtwJm6hkbACLG4ho5DF8GhcXdLEbbvpJnbzA9abfD6RDpbr2m', hd._getExternalWIFByIndex(0)); assert.strictEqual( 'ypub6WhHmKBmHNjcrUVNCa3sXduH9yxutMipDcwiKW31vWjcMbfhQHjXdyx4rqXbEtVgzdbhFJ5mZJWmfWwnP4Vjzx97admTUYKQt6b9D7jjSCp', @@ -97,61 +109,3 @@ it('Legacy HD (BIP44) can generate addressess based on xpub', async function() { assert.strictEqual(hd._getExternalAddressByIndex(1), '1QDCFcpnrZ4yrAQxmbvSgeUC9iZZ8ehcR5'); assert.strictEqual(hd._getInternalAddressByIndex(1), '13CW9WWBsWpDUvLtbFqYziWBWTYUoQb4nU'); }); - -it('can convert blockchain.info TX to blockcypher TX format', () => { - const blockchaininfotx = { - hash: '25aa409a9ecbea6a987b35cef18ffa9c53f5ba985bdaadffaac85cdf9fdbb9e1', - ver: 1, - vin_sz: 1, - vout_sz: 1, - size: 189, - weight: 756, - fee: 1184, - relayed_by: '0.0.0.0', - lock_time: 0, - tx_index: 357712243, - double_spend: false, - result: -91300, - balance: 0, - time: 1530469581, - block_height: 530072, - inputs: [ - { - prev_out: { - value: 91300, - tx_index: 357704878, - n: 1, - spent: true, - script: '76a9147580ebb44301a1165e73e25bcccd7372e1bbfe9c88ac', - type: 0, - addr: '1BiJW1jyUaxcJp2JWwbPLPzB1toPNWTFJV', - xpub: { - m: 'xpub68nLLEi3KERQY7jyznC9PQSpSjmekrEmN8324YRCXayMXaavbdEJsK4gEcX2bNf9vGzT4xRks9utZ7ot1CTHLtdyCn9udvv1NWvtY7HXroh', - path: 'M/1/117', - }, - }, - sequence: 4294967295, - script: - '47304402206f676bd8c87dcf6f9e5016a8d222b06cd542d824e3b22c9ae937c05e59590f7602206cfb75a516e70a79e5f33031a189ebca55f1339be8fcd94b1e1fc9149b55354201210339b7fc52be2c33a64f8f4020c9e80fb23f5ee89992a8c5dd070309b001f16a21', - witness: '', - }, - ], - out: [ - { - value: 90116, - tx_index: 357712243, - n: 0, - spent: true, - script: 'a914e286d58e53f9247a4710e51232cce0686f16873c87', - type: 0, - addr: '3NLnALo49CFEF4tCRhCvz45ySSfz3UktZC', - }, - ], - }; - let blockcyphertx = HDSegwitP2SHWallet.convertTx(blockchaininfotx); - assert.ok(blockcyphertx.received); // time - assert.ok(blockcyphertx.hash); - assert.ok(blockcyphertx.value); - assert.ok(typeof blockcyphertx.confirmations === 'number'); - assert.ok(blockcyphertx.outputs); -}); diff --git a/tests/unit/legacy-wallet.test.js b/tests/unit/legacy-wallet.test.js new file mode 100644 index 00000000..86c6d785 --- /dev/null +++ b/tests/unit/legacy-wallet.test.js @@ -0,0 +1,42 @@ +/* global it, describe */ +import { LegacyWallet } from '../../class'; +const bitcoin = require('bitcoinjs-lib'); +const assert = require('assert'); + +describe('Legacy wallet', () => { + it('can create transaction', async () => { + let l = new LegacyWallet(); + l.setSecret('L4ccWrPMmFDZw4kzAKFqJNxgHANjdy6b7YKNXMwB4xac4FLF3Tov'); + assert.strictEqual(l.getAddress(), '14YZ6iymQtBVQJk6gKnLCk49UScJK7SH4M'); + assert.strictEqual(await l.getChangeAddressAsync(), l.getAddress()); + + let utxos = [ + { + txid: 'cc44e933a094296d9fe424ad7306f16916253a3d154d52e4f1a757c18242cec4', + vout: 0, + value: 100000, + txhex: + '0200000000010161890cd52770c150da4d7d190920f43b9f88e7660c565a5a5ad141abb6de09de00000000000000008002a0860100000000001976a91426e01119d265aa980390c49eece923976c218f1588ac3e17000000000000160014c1af8c9dd85e0e55a532a952282604f820746fcd02473044022072b3f28808943c6aa588dd7a4e8f29fad7357a2814e05d6c5d767eb6b307b4e6022067bc6a8df2dbee43c87b8ce9ddd9fe678e00e0f7ae6690d5cb81eca6170c47e8012102e8fba5643e15ab70ec79528833a2c51338c1114c4eebc348a235b1a3e13ab07100000000', + }, + ]; + // ^^ only non-segwit inputs need full transaction txhex + + let txNew = l.createTransaction(utxos, [{ value: 90000, address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, l.getAddress()); + let tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); + assert.strictEqual( + txNew.tx.toHex(), + '0200000001c4ce4282c157a7f1e4524d153d3a251669f10673ad24e49f6d2994a033e944cc000000006a47304402200faed160757433bcd4d9fe5f55eb92420406e8f3099a7e12ef720c77313c8c7e022044bc9e1abca6a81a8ad5c749f5ec4694301589172b83b1803bc134eda0487dbc01210337c09b3cb889801638078fd4e6998218b28c92d338ea2602720a88847aedceb3ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88ac2f260000000000001976a91426e01119d265aa980390c49eece923976c218f1588ac00000000', + ); + assert.strictEqual(tx.ins.length, 1); + assert.strictEqual(tx.outs.length, 2); + assert.strictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(tx.outs[0].script)); // to address + assert.strictEqual(l.getAddress(), bitcoin.address.fromOutputScript(tx.outs[1].script)); // change address + + // sendMax + txNew = l.createTransaction(utxos, [{ address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, l.getAddress()); + tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); + assert.strictEqual(tx.ins.length, 1); + assert.strictEqual(tx.outs.length, 1); + assert.strictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(tx.outs[0].script)); // to address + }); +}); diff --git a/tests/unit/segwit-bech32-wallet.test.js b/tests/unit/segwit-bech32-wallet.test.js new file mode 100644 index 00000000..cf519c60 --- /dev/null +++ b/tests/unit/segwit-bech32-wallet.test.js @@ -0,0 +1,39 @@ +/* global it, describe */ +import { SegwitBech32Wallet } from '../../class'; +const bitcoin = require('bitcoinjs-lib'); +const assert = require('assert'); + +describe('Segwit P2SH wallet', () => { + it('can create transaction', async () => { + let wallet = new SegwitBech32Wallet(); + wallet.setSecret('L4vn2KxgMLrEVpxjfLwxfjnPPQMnx42DCjZJ2H7nN4mdHDyEUWXd'); + assert.strictEqual(wallet.getAddress(), 'bc1q3rl0mkyk0zrtxfmqn9wpcd3gnaz00yv9yp0hxe'); + assert.strictEqual(await wallet.getChangeAddressAsync(), wallet.getAddress()); + + let utxos = [ + { + txid: '57d18bc076b919583ff074cfba6201edd577f7fe35f69147ea512e970f95ffeb', + vout: 0, + value: 100000, + }, + ]; + + let txNew = wallet.createTransaction(utxos, [{ value: 90000, address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, wallet.getAddress()); + let tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); + assert.strictEqual( + txNew.tx.toHex(), + '02000000000101ebff950f972e51ea4791f635fef777d5ed0162bacf74f03f5819b976c08bd1570000000000ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88ac2f2600000000000016001488fefdd8967886b32760995c1c36289f44f791850248304502210094d8b9d291b3c131594dbacceebf9277ba598f454acbc2c9fa4a7b20895bb74302201a592c4c121f154be1212e6e6b8cd82bb72b97b0f9c098ce8dbe011fbefc8ac101210314cf2bf53f221e58c5adc1dd95adba9239b248f39b09eb2c550aadc1926fe7aa00000000', + ); + assert.strictEqual(tx.ins.length, 1); + assert.strictEqual(tx.outs.length, 2); + assert.strictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(tx.outs[0].script)); // to address + assert.strictEqual(bitcoin.address.fromOutputScript(tx.outs[1].script), wallet.getAddress()); // change address + + // sendMax + txNew = wallet.createTransaction(utxos, [{ address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, wallet.getAddress()); + tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); + assert.strictEqual(tx.ins.length, 1); + assert.strictEqual(tx.outs.length, 1); + assert.strictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(tx.outs[0].script)); // to address + }); +}); diff --git a/tests/unit/segwit-p2sh-wallet.test.js b/tests/unit/segwit-p2sh-wallet.test.js new file mode 100644 index 00000000..56939d91 --- /dev/null +++ b/tests/unit/segwit-p2sh-wallet.test.js @@ -0,0 +1,39 @@ +/* global it, describe */ +import { SegwitP2SHWallet } from '../../class'; +const bitcoin = require('bitcoinjs-lib'); +const assert = require('assert'); + +describe('Segwit P2SH wallet', () => { + it('can create transaction', async () => { + let wallet = new SegwitP2SHWallet(); + wallet.setSecret('Ky1vhqYGCiCbPd8nmbUeGfwLdXB1h5aGwxHwpXrzYRfY5cTZPDo4'); + assert.strictEqual(wallet.getAddress(), '3CKN8HTCews4rYJYsyub5hjAVm5g5VFdQJ'); + assert.strictEqual(await wallet.getChangeAddressAsync(), wallet.getAddress()); + + let utxos = [ + { + txid: 'a56b44080cb606c0bd90e77fcd4fb34c863e68e5562e75b4386e611390eb860c', + vout: 0, + value: 300000, + }, + ]; + + let txNew = wallet.createTransaction(utxos, [{ value: 90000, address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, wallet.getAddress()); + let tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); + assert.strictEqual( + txNew.tx.toHex(), + '020000000001010c86eb9013616e38b4752e56e5683e864cb34fcd7fe790bdc006b60c08446ba50000000017160014139dc70d73097f9d775f8a3280ba3e3435515641ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88ac6f3303000000000017a914749118baa93fb4b88c28909c8bf0a8202a0484f487024730440220086b55a771f37daadbe64fe557a32fd68ee92995445af0b0a5b9343db67505e1022064c9a9778a19a0276761af69b8917d19ed4b791c785dd8cb4aae327f2a6b526f012103a5de146762f84055db3202c1316cd9008f16047f4f408c1482fdb108217eda0800000000', + ); + assert.strictEqual(tx.ins.length, 1); + assert.strictEqual(tx.outs.length, 2); + assert.strictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(tx.outs[0].script)); // to address + assert.strictEqual(bitcoin.address.fromOutputScript(tx.outs[1].script), wallet.getAddress()); // change address + + // sendMax + txNew = wallet.createTransaction(utxos, [{ address: '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB' }], 1, wallet.getAddress()); + tx = bitcoin.Transaction.fromHex(txNew.tx.toHex()); + assert.strictEqual(tx.ins.length, 1); + assert.strictEqual(tx.outs.length, 1); + assert.strictEqual('1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB', bitcoin.address.fromOutputScript(tx.outs[0].script)); // to address + }); +}); diff --git a/tests/unit/signer.test.js b/tests/unit/signer.test.js deleted file mode 100644 index 3dceeee5..00000000 --- a/tests/unit/signer.test.js +++ /dev/null @@ -1,282 +0,0 @@ -/* global describe, it */ -const bitcoinjs = require('bitcoinjs-lib'); -let assert = require('assert'); - -describe('unit - signer', function() { - describe('createSegwitTransaction()', function() { - it('should return valid tx hex for segwit transactions', function(done) { - let signer = require('../../models/signer'); - let utxos = [ - { - txid: '1e1a8cced5580eecd0ac15845fc3adfafbb0f5944a54950e4a16b8f6d1e9b715', - vout: 1, - address: '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi', - account: '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi', - scriptPubKey: 'a9146fbf1cee74734503297e46a0db3e3fbb06f2e9d387', - amount: 0.001, - confirmations: 108, - spendable: false, - solvable: false, - safe: true, - }, - ]; - let tx = signer.createSegwitTransaction( - utxos, - '1Pb81K1xJnMjUfFgKUbva6gr1HCHXxHVnr', - 0.001, - 0.0001, - 'KyWpryAKPiXXbipxWhtprZjSLVjp22sxbVnJssq2TCNQxs1SuMeD', - ); - assert.strictEqual( - tx, - '0100000000010115b7e9d1f6b8164a0e95544a94f5b0fbfaadc35f8415acd0ec0e58d5ce8c1a1e0100000017160014f90e5bca5635b84bd828064586bd7eb117fee9a9ffffffff01905f0100000000001976a914f7c6c1f9f6142107ed293c8fbf85fbc49eb5f1b988ac02473044022023eef496f43936550e08898d10b254ee910dfd19268341edb2f61b873ccba25502204b722787fabc37c2c9e9575832331b0ba0c3f7cd0c18a6fb90027f4327bd8d850121039425479ea581ebc7f55959da8c2e1a1063491768860386335dd4630b5eeacfc500000000', - ); - done(); - }); - - it('should return valid tx hex for RBF-able segwit transactions', function(done) { - let signer = require('../../models/signer'); - let utxos = [ - { - txid: '1e1a8cced5580eecd0ac15845fc3adfafbb0f5944a54950e4a16b8f6d1e9b715', - vout: 1, - address: '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi', - account: '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi', - scriptPubKey: 'a9146fbf1cee74734503297e46a0db3e3fbb06f2e9d387', - amount: 0.1, - confirmations: 108, - spendable: false, - solvable: false, - safe: true, - }, - ]; - let txhex = signer.createSegwitTransaction( - utxos, - '1Pb81K1xJnMjUfFgKUbva6gr1HCHXxHVnr', - 0.001, - 0.0001, - 'KyWpryAKPiXXbipxWhtprZjSLVjp22sxbVnJssq2TCNQxs1SuMeD', - '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi', - 0, - ); - assert.strictEqual( - txhex, - '0100000000010115b7e9d1f6b8164a0e95544a94f5b0fbfaadc35f8415acd0ec0e58d5ce8c1a1e0100000017160014f90e5bca5635b84bd828064586bd7eb117fee9a90000000002905f0100000000001976a914f7c6c1f9f6142107ed293c8fbf85fbc49eb5f1b988ace00f97000000000017a9146fbf1cee74734503297e46a0db3e3fbb06f2e9d38702483045022100bd687693e57161282a80affb82f18386cbf319bca72ca2c16320b0f3b087bee802205e22a9a16b86628ea08eab83aebec1348c476e9d0c90cd41aa73c47f50d86aab0121039425479ea581ebc7f55959da8c2e1a1063491768860386335dd4630b5eeacfc500000000', - ); - // now, testing change addess, destination address, amounts & fees... - let tx = bitcoinjs.Transaction.fromHex(txhex); - assert.strictEqual(bitcoinjs.address.fromOutputScript(tx.outs[0].script), '1Pb81K1xJnMjUfFgKUbva6gr1HCHXxHVnr'); - assert.strictEqual(bitcoinjs.address.fromOutputScript(tx.outs[1].script), '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi'); - assert.strictEqual(tx.outs[0].value, 90000); // 0.0009 because we deducted fee 0.0001 - assert.strictEqual(tx.outs[1].value, 9900000); // 0.099 because 0.1 - 0.001 - done(); - }); - - it('should create Replace-By-Fee tx, given txhex', () => { - let txhex = - '0100000000010115b7e9d1f6b8164a0e95544a94f5b0fbfaadc35f8415acd0ec0e58d5ce8c1a1e0100000017160014f90e5bca5635b84bd828064586bd7eb117fee9a90000000002905f0100000000001976a914f7c6c1f9f6142107ed293c8fbf85fbc49eb5f1b988ace00f97000000000017a9146fbf1cee74734503297e46a0db3e3fbb06f2e9d38702483045022100bd687693e57161282a80affb82f18386cbf319bca72ca2c16320b0f3b087bee802205e22a9a16b86628ea08eab83aebec1348c476e9d0c90cd41aa73c47f50d86aab0121039425479ea581ebc7f55959da8c2e1a1063491768860386335dd4630b5eeacfc500000000'; - let signer = require('../../models/signer'); - let dummyUtxodata = { - '1e1a8cced5580eecd0ac15845fc3adfafbb0f5944a54950e4a16b8f6d1e9b715': { - // txid we use output from - 1: 10000000, // output index and it's value in satoshi - }, - }; - let newhex = signer.createRBFSegwitTransaction( - txhex, - { '1Pb81K1xJnMjUfFgKUbva6gr1HCHXxHVnr': '3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2' }, - 0.0001, - 'KyWpryAKPiXXbipxWhtprZjSLVjp22sxbVnJssq2TCNQxs1SuMeD', - dummyUtxodata, - ); - let oldTx = bitcoinjs.Transaction.fromHex(txhex); - let newTx = bitcoinjs.Transaction.fromHex(newhex); - // just checking old tx... - assert.strictEqual(bitcoinjs.address.fromOutputScript(oldTx.outs[0].script), '1Pb81K1xJnMjUfFgKUbva6gr1HCHXxHVnr'); // old DESTINATION address - assert.strictEqual(bitcoinjs.address.fromOutputScript(oldTx.outs[1].script), '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi'); // old CHANGE address - assert.strictEqual(oldTx.outs[0].value, 90000); // 0.0009 because we deducted fee 0.0001 - assert.strictEqual(oldTx.outs[1].value, 9900000); // 0.099 because 0.1 - 0.001 - // finaly, new tx checks... - assert.strictEqual(oldTx.outs[0].value, newTx.outs[0].value); // DESTINATION output amount remains unchanged - assert.strictEqual(oldTx.outs[1].value - newTx.outs[1].value, 0.0001 * 100000000); // CHANGE output decreased on the amount of fee delta - assert.strictEqual(bitcoinjs.address.fromOutputScript(newTx.outs[0].script), '3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'); // new DESTINATION address - assert.strictEqual(bitcoinjs.address.fromOutputScript(newTx.outs[1].script), '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi'); // CHANGE address remains - assert.strictEqual(oldTx.ins[0].sequence + 1, newTx.ins[0].sequence); - }); - - it('should return valid tx hex for segwit transactions with multiple inputs', function(done) { - let signer = require('../../models/signer'); - let utxos = [ - { - txid: '4e2a536aaf6b0b8a4f439d0343436cd321b8bac9840a24d13b8eed484a257b0b', - vout: 0, - address: '3NBtBset4qPD8DZeLw4QbFi6SNjNL8hg7x', - account: '3NBtBset4qPD8DZeLw4QbFi6SNjNL8hg7x', - scriptPubKey: 'a914e0d81f03546ab8f29392b488ec62ab355ee7c57387', - amount: 0.0009, - confirmations: 67, - spendable: false, - solvable: false, - safe: true, - }, - { - txid: '09e1b78d4ecd95dd4c7dbc840a2619da6d02caa345a63b2733f3972666462fbd', - vout: 0, - address: '3NBtBset4qPD8DZeLw4QbFi6SNjNL8hg7x', - account: '3NBtBset4qPD8DZeLw4QbFi6SNjNL8hg7x', - scriptPubKey: 'a914e0d81f03546ab8f29392b488ec62ab355ee7c57387', - amount: 0.0019, - confirmations: 142, - spendable: false, - solvable: false, - safe: true, - }, - ]; - let tx = signer.createSegwitTransaction( - utxos, - '1Pb81K1xJnMjUfFgKUbva6gr1HCHXxHVnr', - 0.0028, - 0.0002, - 'L4iRvejJG9gRhKVc3rZm5haoyd4EuCi77G91DnXRrvNDqiXktkXh', - ); - assert.strictEqual( - tx, - '010000000001020b7b254a48ed8e3bd1240a84c9bab821d36c4343039d434f8a0b6baf6a532a4e00000000171600141e16a923b1a9e8d0c2a044030608a6aa13f97e9affffffffbd2f46662697f333273ba645a3ca026dda19260a84bc7d4cdd95cd4e8db7e10900000000171600141e16a923b1a9e8d0c2a044030608a6aa13f97e9affffffff01a0f70300000000001976a914f7c6c1f9f6142107ed293c8fbf85fbc49eb5f1b988ac02483045022100b3e001b880a7a18294640165cc40c777669534803cee7206c8d3f03531bb315502204642a4569576a2e9e77342c7a9aaa508a21248b7720fe0f9e6d76713951c133001210314389c888e9669ae05739819fc7c43d7a50fdeabd2a8951f9607c8cad394fd4b02473044022078bd4f47178ce13c4fbf77c5ce78c80ac10251aa053c68c8febb21ce228f844e02207b02bdd754fbc2df9f62ea98e7dbd6c43e760b8f78c7c00b43512a06b498adb501210314389c888e9669ae05739819fc7c43d7a50fdeabd2a8951f9607c8cad394fd4b00000000', - ); - done(); - }); - - it('should return valid tx hex for segwit transactions with change address', function(done) { - let signer = require('../../models/signer'); - let utxos = [ - { - txid: '160559030484800a77f9b38717bb0217e87bfeb47b92e2e5bad6316ad9d8d360', - vout: 1, - address: '3NBtBset4qPD8DZeLw4QbFi6SNjNL8hg7x', - account: '3NBtBset4qPD8DZeLw4QbFi6SNjNL8hg7x', - scriptPubKey: 'a914e0d81f03546ab8f29392b488ec62ab355ee7c57387', - amount: 0.004, - confirmations: 271, - spendable: false, - solvable: false, - safe: true, - }, - ]; - let tx = signer.createSegwitTransaction( - utxos, - '1Pb81K1xJnMjUfFgKUbva6gr1HCHXxHVnr', - 0.002, - 0.0001, - 'L4iRvejJG9gRhKVc3rZm5haoyd4EuCi77G91DnXRrvNDqiXktkXh', - ); - assert.strictEqual( - tx, - '0100000000010160d3d8d96a31d6bae5e2927bb4fe7be81702bb1787b3f9770a8084040359051601000000171600141e16a923b1a9e8d0c2a044030608a6aa13f97e9affffffff0230e60200000000001976a914f7c6c1f9f6142107ed293c8fbf85fbc49eb5f1b988ac400d03000000000017a914e0d81f03546ab8f29392b488ec62ab355ee7c573870247304402202c962e14ae6abd45dc9613d2f088ad487e805670548e244deb25d762b310a60002204f12c7f9b8da3567b39906ff6c46b27ce087e7ae91bbe34fb1cdee1b994b9d3001210314389c888e9669ae05739819fc7c43d7a50fdeabd2a8951f9607c8cad394fd4b00000000', - ); - done(); - }); - - it('should return valid tx hex for segwit transactions if change is too small so it causes @dust error', function(done) { - // checking that change amount is at least 3x of fee, otherwise screw the change, just add it to fee - let signer = require('../../models/signer'); - let utxos = [ - { - txid: '160559030484800a77f9b38717bb0217e87bfeb47b92e2e5bad6316ad9d8d360', - vout: 1, - address: '3NBtBset4qPD8DZeLw4QbFi6SNjNL8hg7x', - account: '3NBtBset4qPD8DZeLw4QbFi6SNjNL8hg7x', - scriptPubKey: 'a914e0d81f03546ab8f29392b488ec62ab355ee7c57387', - amount: 0.004, - confirmations: 271, - spendable: false, - solvable: false, - safe: true, - }, - ]; - let txhex = signer.createSegwitTransaction( - utxos, - '1Pb81K1xJnMjUfFgKUbva6gr1HCHXxHVnr', - 0.003998, - 0.000001, - 'L4iRvejJG9gRhKVc3rZm5haoyd4EuCi77G91DnXRrvNDqiXktkXh', - ); - let bitcoin = bitcoinjs; - let tx = bitcoin.Transaction.fromHex(txhex); - assert.strictEqual(tx.ins.length, 1); - assert.strictEqual(tx.outs.length, 1); // only 1 output, which means change is neglected - assert.strictEqual(tx.outs[0].value, 399700); - done(); - }); - }); - - describe('WIF2address()', function() { - it('should convert WIF to segwit P2SH address', function(done) { - let signer = require('../../models/signer'); - let address = signer.WIF2segwitAddress('L55uHs7pyz7rP18K38kB7kqDVNJaeYFzJtZyC3ZjD2c684dzXQWs'); - assert.strictEqual('3FSL9x8P8cQ74iW2HLP6JPGPRgc4K2FnsU', address); - done(); - }); - }); - - describe('generateNewAddress()', function() { - it('should generate new address', function(done) { - let signer = require('../../models/signer'); - let address = signer.generateNewSegwitAddress(); - assert.ok(address.WIF); - assert.ok(address.address); - assert.strictEqual(address.address, signer.WIF2segwitAddress(address.WIF)); - done(); - }); - }); - - describe('URI()', function() { - it('should form correct payment url', function(done) { - let signer = require('../../models/signer'); - let url = signer.URI({ - address: '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi', - message: 'For goods & services', - label: 'nolabel', - amount: 1000000, - }); - assert.strictEqual(url, 'bitcoin:3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi?amount=0.01&message=For%20goods%20%26%20services&label=nolabel'); - - url = signer.URI({ - address: '1DzJepHCRD2C9vpFjk11eXJi97juEZ3ftv', - message: 'wheres the money lebowski', - amount: 400000, - }); - assert.strictEqual(url, 'bitcoin:1DzJepHCRD2C9vpFjk11eXJi97juEZ3ftv?amount=0.004&message=wheres%20the%20money%20lebowski'); - done(); - }); - }); - - describe('createTransaction()', () => { - const signer = require('../../models/signer'); - it('should return valid TX hex for legacy transactions', () => { - let utxos = [ - { - txid: '2f445cf016fa2772db7d473bff97515355b4e6148e1c980ce351d47cf54c517f', - vout: 1, - address: '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi', - account: '3Bsssbs4ANCGNETvGLJ3Fvri6SiVnH1fbi', - scriptPubKey: 'a9146fbf1cee74734503297e46a0db3e3fbb06f2e9d387', - amount: 0.01, - confirmations: 108, - spendable: false, - solvable: false, - safe: true, - }, - ]; - let toAddr = '1GX36PGBUrF8XahZEGQqHqnJGW2vCZteoB'; - let amount = 0.001; - let fee = 0.0001; - let WIF = 'KzbTHhzzZyVhkTYpuReMBkE7zUvvDEZtavq1DJV85MtBZyHK1TTF'; - let fromAddr = '179JSjDc9Dh9pWWq9qv35sZsXQAV6VdE1E'; - let txHex = signer.createTransaction(utxos, toAddr, amount, fee, WIF, fromAddr); - assert.strictEqual( - txHex, - '01000000017f514cf57cd451e30c981c8e14e6b455535197ff3b477ddb7227fa16f05c442f010000006b483045022100c5d6b024db144aa1f0cb6d6212c326c9753f4144fd69947c1f38657944b92022022039214118b745afe6e031f96f3e98e705979f2b9f9cbbc6a91e11c89c811a3292012103f5438d524ad1cc288963466d6ef1a27d83183f7e9b7fe30879ecdae887692a31ffffffff02905f0100000000001976a914aa381cd428a4e91327fd4434aa0a08ff131f1a5a88aca0bb0d00000000001976a9144362a4c0dbf5102238164d1ec97f3b518bb651cd88ac00000000', - ); - }); - }); -});