diff --git a/class/abstract-hd-electrum-wallet.js b/class/abstract-hd-electrum-wallet.js
index b9717b8f..f3d3f4e1 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
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/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..31b8cdaf 100644
--- a/class/legacy-wallet.js
+++ b/class/legacy-wallet.js
@@ -3,9 +3,9 @@ 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 +104,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 +123,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;
}
/**
@@ -241,29 +255,84 @@ export class LegacyWallet extends AbstractWallet {
}
/**
- * 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 +385,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/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/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/confirm.js b/screen/send/confirm.js
index f34b3049..db38cad1 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');
@@ -68,7 +77,7 @@ export default class Confirm extends Component {
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 +85,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);
}
@@ -106,7 +127,7 @@ export default class Confirm extends Component {
fontWeight: '600',
}}
>
- {item.amount === BitcoinUnit.MAX
+ {!item.amount || item.amount === BitcoinUnit.MAX
? currency.satoshiToBTC(this.state.fromWallet.getBalance() - this.state.feeSatoshi)
: item.amount || currency.satoshiToBTC(item.value)}
diff --git a/screen/send/create.js b/screen/send/create.js
index 6dbe444d..5fd1c964 100644
--- a/screen/send/create.js
+++ b/screen/send/create.js
@@ -104,7 +104,7 @@ export default class SendCreate extends Component {
{item.address}
{loc.send.create.amount}
- {item.amount === BitcoinUnit.MAX
+ {item.amount === BitcoinUnit.MAX || !item.amount
? currency.satoshiToBTC(this.state.wallet.getBalance()) - this.state.fee
: item.amount || currency.satoshiToBTC(item.value)}{' '}
{BitcoinUnit.BTC}
diff --git a/screen/send/details.js b/screen/send/details.js
index 50b365f2..6f6b62c9 100644
--- a/screen/send/details.js
+++ b/screen/send/details.js
@@ -35,7 +35,7 @@ import Modal from 'react-native-modal';
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
import BitcoinBIP70TransactionDecode from '../../bip70/bip70';
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
-import { HDLegacyP2PKHWallet, HDSegwitBech32Wallet, HDSegwitP2SHWallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
+import { HDSegwitBech32Wallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
import DocumentPicker from 'react-native-document-picker';
@@ -424,104 +424,17 @@ export default class SendDetails extends Component {
return;
}
- if (this.state.fromWallet.type === HDSegwitBech32Wallet.type || this.state.fromWallet.type === WatchOnlyWallet.type) {
- // new send is supported by BIP84 or watchonly with HW wallet support (it uses BIP84 under the hood anyway)
- try {
- await this.createHDBech32Transaction();
- } catch (Err) {
- this.setState({ isLoading: false }, () => {
- 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();
@@ -541,7 +454,7 @@ export default class SendDetails extends Component {
}
if (firstTransaction.amount === BitcoinUnit.MAX) {
- targets = [{ address: firstTransaction.address, amount: BitcoinUnit.MAX }];
+ targets = [{ address: firstTransaction.address }];
}
let { tx, fee, psbt } = wallet.createTransaction(
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/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',
- );
- });
- });
-});