Browse Source

Merge branch 'master' into localtrader-highlight

master
Nuno Coelho 5 years ago
committed by GitHub
parent
commit
dcbdc36112
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      App.js
  2. 31
      BlueComponents.js
  3. 6
      BlueElectrum.js
  4. 13
      MainBottomTabs.js
  5. 2
      android/app/build.gradle
  6. 80
      class/abstract-hd-electrum-wallet.js
  7. 243
      class/abstract-hd-wallet.js
  8. 26
      class/abstract-wallet.js
  9. 4
      class/app-storage.js
  10. 16
      class/deeplink-schema-match.js
  11. 10
      class/hd-legacy-breadwallet-wallet.js
  12. 33
      class/hd-legacy-electrum-seed-p2pkh-wallet.js
  13. 56
      class/hd-legacy-p2pkh-wallet.js
  14. 56
      class/hd-segwit-p2sh-wallet.js
  15. 2
      class/index.js
  16. 160
      class/legacy-wallet.js
  17. 59
      class/segwit-bech-wallet.js
  18. 149
      class/segwit-bech32-wallet.js
  19. 112
      class/segwit-p2sh-wallet.js
  20. 2
      class/walletGradient.js
  21. 14
      class/walletImport.js
  22. 4
      class/watch-only-wallet.js
  23. 2
      ios/BlueWallet/Info.plist
  24. 2
      ios/BlueWalletWatch Extension/Info.plist
  25. 2
      ios/BlueWalletWatch/Info.plist
  26. 2
      ios/TodayExtension/Info.plist
  27. 2
      loc/en.js
  28. 342
      models/signer.js
  29. 221
      package-lock.json
  30. 6
      package.json
  31. 16
      screen/lnd/lndViewInvoice.js
  32. 8
      screen/receive/details.js
  33. 146
      screen/selftest.js
  34. 162
      screen/send/broadcast.js
  35. 39
      screen/send/confirm.js
  36. 5
      screen/send/create.js
  37. 176
      screen/send/details.js
  38. 19
      screen/send/psbtWithHardwareWallet.js
  39. 19
      screen/transactions/CPFP.js
  40. 9
      screen/wallets/add.js
  41. 10
      screen/wallets/buyBitcoin.js
  42. 1
      screen/wallets/details.js
  43. 95
      screen/wallets/import.js
  44. 2
      screen/wallets/list.js
  45. 491
      screen/wallets/pleaseBackup.js
  46. 81
      tests/e2e/bluewallet.spec.js
  47. 184
      tests/integration/hd-segwit-p2sh-wallet.test.js
  48. 10
      tests/integration/legacy-wallet.test.js
  49. 45
      tests/unit/deeplink-schema-match.test.js
  50. 27
      tests/unit/hd-legacy-breadwallet.test.js
  51. 15
      tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js
  52. 131
      tests/unit/hd-legacy-wallet.test.js
  53. 9
      tests/unit/hd-segwit-bech32-wallet.test.js
  54. 70
      tests/unit/hd-segwit-p2sh-wallet.test.js
  55. 42
      tests/unit/legacy-wallet.test.js
  56. 39
      tests/unit/segwit-bech32-wallet.test.js
  57. 39
      tests/unit/segwit-p2sh-wallet.test.js
  58. 282
      tests/unit/signer.test.js

2
App.js

@ -10,7 +10,7 @@ import { Chain } from './models/bitcoinUnits';
import QuickActions from 'react-native-quick-actions';
import * as Sentry from '@sentry/react-native';
import OnAppLaunch from './class/onAppLaunch';
import DeeplinkSchemaMatch from './class/deeplinkSchemaMatch';
import DeeplinkSchemaMatch from './class/deeplink-schema-match';
import BitcoinBIP70TransactionDecode from './bip70/bip70';
const A = require('./analytics');

31
BlueComponents.js

@ -313,6 +313,7 @@ export class BlueWalletNavigationHeader extends Component {
<BluePrivateBalance />
) : (
<Text
testID={'WalletBalance'}
numberOfLines={1}
adjustsFontSizeToFit
style={{
@ -640,6 +641,7 @@ export class BlueFormMultiInput extends Component {
underlineColorAndroid="transparent"
numberOfLines={4}
style={{
flex: 1,
marginTop: 5,
marginHorizontal: 20,
borderColor: BlueApp.settings.inputBorderColor,
@ -647,7 +649,6 @@ export class BlueFormMultiInput extends Component {
borderWidth: 0.5,
borderBottomWidth: 0.5,
backgroundColor: BlueApp.settings.inputBackgroundColor,
height: 200,
color: BlueApp.settings.foregroundColor,
}}
autoCorrect={false}
@ -938,11 +939,10 @@ export class BlueDoneAndDismissKeyboardInputAccessory extends Component {
<View
style={{
backgroundColor: '#eef0f4',
height: 44,
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
maxHeight: 44,
}}
>
<BlueButtonLink title="Clear" onPress={this.props.onClearTapped} />
@ -954,7 +954,7 @@ export class BlueDoneAndDismissKeyboardInputAccessory extends Component {
if (Platform.OS === 'ios') {
return <InputAccessoryView nativeID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}>{inputView}</InputAccessoryView>;
} else {
return <KeyboardAvoidingView style={{ height: 44 }}>{inputView}</KeyboardAvoidingView>;
return <KeyboardAvoidingView>{inputView}</KeyboardAvoidingView>;
}
}
}
@ -1257,7 +1257,7 @@ export class BlueReceiveButtonIcon extends Component {
export class BlueSendButtonIcon extends Component {
render() {
return (
<TouchableOpacity {...this.props}>
<TouchableOpacity {...this.props} testID={'SendButton'}>
<View
style={{
flex: 1,
@ -2053,6 +2053,7 @@ export class BlueAddressInput extends Component {
}}
>
<TextInput
testID={'AddressInput'}
onChangeText={text => {
this.props.onChangeText(text);
}}
@ -2246,6 +2247,7 @@ export class BlueBitcoinAmount extends Component {
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 2 }}>
<TextInput
{...this.props}
testID={'BitcoinAmountInput'}
keyboardType="numeric"
onChangeText={text => {
text = text.trim();
@ -2315,3 +2317,22 @@ const styles = StyleSheet.create({
marginRight: 16,
},
});
export function BlueBigCheckmark({ style }) {
const defaultStyles = {
backgroundColor: '#ccddf9',
width: 120,
height: 120,
borderRadius: 60,
alignSelf: 'center',
justifyContent: 'center',
marginTop: 0,
marginBottom: 0,
};
const mergedStyles = { ...defaultStyles, ...style };
return (
<View style={mergedStyles}>
<Icon name="check" size={50} type="font-awesome" color="#0f5cc0" />
</View>
);
}

6
BlueElectrum.js

@ -466,9 +466,9 @@ module.exports.broadcastV2 = async function(hex) {
};
module.exports.estimateCurrentBlockheight = function() {
const baseTs = 1585837504347; // uS
const baseHeight = 624197;
return Math.floor(baseHeight + (+new Date() - baseTs) / 1000 / 60 / 10);
const baseTs = 1587570465609; // uS
const baseHeight = 627179;
return Math.floor(baseHeight + (+new Date() - baseTs) / 1000 / 60 / 9.5);
};
/**

13
MainBottomTabs.js

@ -44,6 +44,7 @@ import sendCreate from './screen/send/create';
import Confirm from './screen/send/confirm';
import PsbtWithHardwareWallet from './screen/send/psbtWithHardwareWallet';
import Success from './screen/send/success';
import Broadcast from './screen/send/broadcast';
import ScanLndInvoice from './screen/lnd/scanLndInvoice';
import LappBrowser from './screen/lnd/browser';
@ -199,6 +200,9 @@ const CreateTransactionStackNavigator = createStackNavigator({
headerRight: null,
},
},
Broadcast: {
screen: Broadcast,
},
});
const LNDCreateInvoiceStackNavigator = createStackNavigator({
@ -325,12 +329,17 @@ const MainBottomTabs = createStackNavigator(
},
},
//
ReceiveDetails: {
screen: receiveDetails,
},
Broadcast: {
screen: Broadcast,
navigationOptions: () => ({
title: 'Broadcast tx',
}),
},
//
// LND:

2
android/app/build.gradle

@ -140,7 +140,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "5.3.3"
versionName "5.3.4"
multiDexEnabled true
missingDimensionStrategy 'react-native-camera', 'general'
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type

80
class/abstract-hd-electrum-wallet.js

@ -56,6 +56,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return false;
}
/**
*
* @inheritDoc
*/
getUnconfirmedBalance() {
let ret = 0;
for (let bal of Object.values(this._balances_by_external_index)) {
@ -751,8 +755,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return ret;
}
_getDerivationPathByAddress(address) {
const path = "m/84'/0'/0'";
_getDerivationPathByAddress(address, BIP = 84) {
const path = `m/${BIP}'/0'/0'`;
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c;
}
@ -763,6 +767,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return false;
}
/**
*
* @param address {string} Address that belongs to this wallet
* @returns {Buffer|boolean} Either buffer with pubkey or false
*/
_getPubkeyByAddress(address) {
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._getExternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(0, c);
@ -784,13 +793,6 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return false;
}
/**
* @deprecated
*/
createTx(utxos, amount, fee, address) {
throw new Error('Deprecated');
}
/**
*
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos
@ -838,7 +840,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// skiping signing related stuff
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input');
}
let pubkey = this._getPubkeyByAddress(input.address);
let masterFingerprintBuffer;
if (masterFingerprint) {
let masterFingerprintHex = Number(masterFingerprint).toString(16);
@ -850,24 +852,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
// this is not correct fingerprint, as we dont know real fingerprint - we got zpub with 84/0, but fingerpting
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
let path = this._getDerivationPathByAddress(input.address);
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey });
psbt.addInput({
hash: input.txId,
index: input.vout,
sequence,
bip32Derivation: [
{
masterFingerprint: masterFingerprintBuffer,
path,
pubkey,
},
],
witnessUtxo: {
script: p2wpkh.output,
value: input.value,
},
});
psbt = this._addPsbtInput(psbt, input, sequence, masterFingerprintBuffer);
});
outputs.forEach(output => {
@ -926,6 +912,31 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return { tx, inputs, outputs, fee, psbt };
}
_addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) {
const pubkey = this._getPubkeyByAddress(input.address);
const path = this._getDerivationPathByAddress(input.address);
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey });
psbt.addInput({
hash: input.txId,
index: input.vout,
sequence,
bip32Derivation: [
{
masterFingerprint: masterFingerprintBuffer,
path,
pubkey,
},
],
witnessUtxo: {
script: p2wpkh.output,
value: input.value,
},
});
return psbt;
}
/**
* Combines 2 PSBTs into final transaction from which you can
* get HEX and broadcast
@ -977,19 +988,6 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return txs;
}
/**
* Broadcast txhex. Can throw an exception if failed
*
* @param {String} txhex
* @returns {Promise<boolean>}
*/
async broadcastTx(txhex) {
let broadcast = await BlueElectrum.broadcastV2(txhex);
console.log({ broadcast });
if (broadcast.indexOf('successfully') !== -1) return true;
return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok
}
/**
* Probes zero address in external hierarchy for transactions, if there are any returns TRUE.
* Zero address is a pretty good indicator, since its a first one to fund the wallet. How can you use the wallet and

243
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) {

26
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');

4
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);

16
class/deeplinkSchemaMatch.js → class/deeplink-schema-match.js

@ -5,6 +5,7 @@ import RNFS from 'react-native-fs';
import url from 'url';
import { Chain } from '../models/bitcoinUnits';
const bitcoin = require('bitcoinjs-lib');
const bip21 = require('bip21');
const BlueApp: AppStorage = require('../BlueApp');
class DeeplinkSchemaMatch {
@ -194,6 +195,7 @@ class DeeplinkSchemaMatch {
static isBitcoinAddress(address) {
address = address
.replace('bitcoin:', '')
.replace('BITCOIN:', '')
.replace('bitcoin=', '')
.split('?')[0];
let isValidBitcoinAddress = false;
@ -228,14 +230,14 @@ class DeeplinkSchemaMatch {
}
static isBothBitcoinAndLightning(url) {
if (url.includes('lightning') && url.includes('bitcoin')) {
const txInfo = url.split(/(bitcoin:|lightning:|lightning=|bitcoin=)+/);
if (url.includes('lightning') && (url.includes('bitcoin') || url.includes('BITCOIN'))) {
const txInfo = url.split(/(bitcoin:|BITCOIN:|lightning:|lightning=|bitcoin=)+/);
let bitcoin;
let lndInvoice;
for (const [index, value] of txInfo.entries()) {
try {
// Inside try-catch. We dont wan't to crash in case of an out-of-bounds error.
if (value.startsWith('bitcoin')) {
if (value.startsWith('bitcoin') || value.startsWith('BITCOIN')) {
bitcoin = `bitcoin:${txInfo[index + 1]}`;
if (!DeeplinkSchemaMatch.isBitcoinAddress(bitcoin)) {
bitcoin = false;
@ -261,6 +263,14 @@ class DeeplinkSchemaMatch {
}
return undefined;
}
static bip21decode(uri) {
return bip21.decode(uri.replace('BITCOIN:', 'bitcoin:'));
}
static bip21encode() {
return bip21.encode.apply(bip21, arguments);
}
}
export default DeeplinkSchemaMatch;

10
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;
}
}

33
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;
}
}
}

56
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;
}
}

56
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;
}
/**

2
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';

160
class/legacy-wallet.js

@ -1,11 +1,12 @@
import { AbstractWallet } from './abstract-wallet';
import { HDSegwitBech32Wallet } from './';
import { NativeModules } from 'react-native';
const bitcoin = require('bitcoinjs-lib');
const { RNRandomBytes } = NativeModules;
const BigNumber = require('bignumber.js');
const signer = require('../models/signer');
const BlueElectrum = require('../BlueElectrum');
const coinSelectAccumulative = require('coinselect/accumulative');
const coinSelectSplit = require('coinselect/split');
/**
* Has private key and single address like "1ABCD....."
@ -104,8 +105,7 @@ export class LegacyWallet extends AbstractWallet {
try {
let balance = await BlueElectrum.getBalanceByAddress(this.getAddress());
this.balance = Number(balance.confirmed);
this.unconfirmed_balance = new BigNumber(balance.unconfirmed);
this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1; // wtf
this.unconfirmed_balance = Number(balance.unconfirmed);
this._lastBalanceFetch = +new Date();
} catch (Error) {
console.warn(Error);
@ -124,20 +124,35 @@ export class LegacyWallet extends AbstractWallet {
for (let arr of Object.values(utxos)) {
this.utxo = this.utxo.concat(arr);
}
// now we need to fetch txhash for each input as required by PSBT
if (LegacyWallet.type !== this.type) return; // but only for LEGACY single-address wallets
let txhexes = await BlueElectrum.multiGetTransactionByTxid(
this.utxo.map(u => u['txId']),
50,
false,
);
let newUtxos = [];
for (let u of this.utxo) {
if (txhexes[u.txId]) u.txhex = txhexes[u.txId];
newUtxos.push(u);
}
this.utxo = newUtxos;
} catch (Error) {
console.warn(Error);
}
// backward compatibility
for (let u of this.utxo) {
u.tx_output_n = u.vout;
u.tx_hash = u.txId;
u.confirmations = u.height ? 1 : 0;
}
}
getUtxo() {
return this.utxo;
let ret = [];
for (let u of this.utxo) {
if (u.txId) u.txid = u.txId;
if (!u.confirmations && u.height) u.confirmations = BlueElectrum.estimateCurrentBlockheight() - u.height;
ret.push(u);
}
return ret;
}
/**
@ -231,39 +246,98 @@ export class LegacyWallet extends AbstractWallet {
return hd.getTransactions.apply(this);
}
/**
* Broadcast txhex. Can throw an exception if failed
*
* @param {String} txhex
* @returns {Promise<boolean>}
*/
async broadcastTx(txhex) {
try {
const broadcast = await BlueElectrum.broadcast(txhex);
return broadcast;
} catch (error) {
return error;
}
let broadcast = await BlueElectrum.broadcastV2(txhex);
console.log({ broadcast });
if (broadcast.indexOf('successfully') !== -1) return true;
return broadcast.length === 64; // this means return string is txid (precise length), so it was broadcasted ok
}
/**
* Takes UTXOs, transforms them into
* format expected by signer module, creates tx and returns signed string txhex.
*
* @param utxos Unspent outputs, expects blockcypher format
* @param amount
* @param fee
* @param toAddress
* @param memo
* @return string Signed txhex ready for broadcast
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String, txhex: String, }>} List of spendable utxos
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate)
* @param feeRate {Number} satoshi per byte
* @param changeAddress {String} Excessive coins will go back to that address
* @param sequence {Number} Used in RBF
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
* @param masterFingerprint {number} Decimal number of wallet's master fingerprint
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
*/
createTx(utxos, amount, fee, toAddress, memo) {
// transforming UTXOs fields to how module expects it
for (let u of utxos) {
u.confirmations = 6; // hack to make module accept 0 confirmations
u.txid = u.tx_hash;
u.vout = u.tx_output_n;
u.amount = new BigNumber(u.value);
u.amount = u.amount.dividedBy(100000000);
u.amount = u.amount.toString(10);
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
if (!changeAddress) throw new Error('No change address provided');
sequence = sequence || 0xffffffff; // disable RBF by default
let algo = coinSelectAccumulative;
if (targets.length === 1 && targets[0] && !targets[0].value) {
// we want to send MAX
algo = coinSelectSplit;
}
// console.log('creating legacy tx ', amount, ' with fee ', fee, 'secret=', this.getSecret(), 'from address', this.getAddress());
let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10));
return signer.createTransaction(utxos, toAddress, amountPlusFee, fee, this.getSecret(), this.getAddress());
let { inputs, outputs, fee } = algo(utxos, targets, feeRate);
// .inputs and .outputs will be undefined if no solution was found
if (!inputs || !outputs) {
throw new Error('Not enough balance. Try sending smaller amount');
}
let psbt = new bitcoin.Psbt();
let c = 0;
let values = {};
let keyPair;
inputs.forEach(input => {
if (!skipSigning) {
// skiping signing related stuff
keyPair = bitcoin.ECPair.fromWIF(this.secret); // secret is WIF
}
values[c] = input.value;
c++;
if (!input.txhex) throw new Error('UTXO is missing txhex of the input, which is required by PSBT for non-segwit input');
psbt.addInput({
hash: input.txid,
index: input.vout,
sequence,
// non-segwit inputs now require passing the whole previous tx as Buffer
nonWitnessUtxo: Buffer.from(input.txhex, 'hex'),
});
});
outputs.forEach(output => {
// if output has no address - this is change output
if (!output.address) {
output.address = changeAddress;
}
let outputData = {
address: output.address,
value: output.value,
};
psbt.addOutput(outputData);
});
if (!skipSigning) {
// skiping signing related stuff
for (let cc = 0; cc < c; cc++) {
psbt.signInput(cc, keyPair);
}
}
let tx;
if (!skipSigning) {
tx = psbt.finalizeAllInputs().extractTransaction();
}
return { tx, inputs, outputs, fee, psbt };
}
getLatestTransactionTime() {
@ -316,4 +390,14 @@ export class LegacyWallet extends AbstractWallet {
weOwnAddress(address) {
return this.getAddress() === address || this._address === address;
}
allowSendMax() {
return true;
}
async getChangeAddressAsync() {
return new Promise(resolve => {
resolve(this.getAddress());
});
}
}

59
class/segwit-bech-wallet.js

@ -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;
}
}

149
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;
}
}

112
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;
}
}

2
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'];

14
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);

4
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;

2
ios/BlueWallet/Info.plist

@ -48,7 +48,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>5.3.3</string>
<string>5.3.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

2
ios/BlueWalletWatch Extension/Info.plist

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>5.3.3</string>
<string>5.3.4</string>
<key>CFBundleVersion</key>
<string>239</string>
<key>CLKComplicationPrincipalClass</key>

2
ios/BlueWalletWatch/Info.plist

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>5.3.3</string>
<string>5.3.4</string>
<key>CFBundleVersion</key>
<string>239</string>
<key>UISupportedInterfaceOrientations</key>

2
ios/TodayExtension/Info.plist

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>5.3.3</string>
<string>5.3.4</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>

2
loc/en.js

@ -82,7 +82,7 @@ module.exports = {
error: 'Failed to import. Please, make sure that the provided data is valid.',
success: 'Success',
do_import: 'Import',
scan_qr: '...scan QR or import file instead?',
scan_qr: 'Scan or import a file',
},
scanQrWif: {
go_back: 'Go Back',

342
models/signer.js

@ -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();
};

221
package-lock.json

@ -1,6 +1,6 @@
{
"name": "bluewallet",
"version": "5.3.3",
"version": "5.3.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -3196,19 +3196,38 @@
}
},
"buffer": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz",
"integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz",
"integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
},
"buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"requires": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
}
},
"buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
},
"buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
},
"buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
"integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
@ -3317,14 +3336,6 @@
}
}
},
"can-promise": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/can-promise/-/can-promise-0.0.1.tgz",
"integrity": "sha512-gzVrHyyrvgt0YpDm7pn04MQt8gjh0ZAhN4ZDyCRtGl6YnuuK6b4aiUTD7G52r9l4YNmxfTtEscb92vxtAlL6XQ==",
"requires": {
"window-or-global": "^1.0.1"
}
},
"caniuse-lite": {
"version": "1.0.30001040",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001040.tgz",
@ -10981,161 +10992,88 @@
}
},
"qrcode": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.2.0.tgz",
"integrity": "sha512-wZK0Z0eYmOUDP2tOGzmLdeBn5Npa+4wms9GdvzH7HrywvGUq/Stz0BKUhW4DfmBf1PSrm9dNfdnVDq683Zxvag==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
"integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==",
"requires": {
"can-promise": "^0.0.1",
"buffer": "^5.4.3",
"buffer-alloc": "^1.2.0",
"buffer-from": "^1.1.1",
"dijkstrajs": "^1.0.1",
"isarray": "^2.0.1",
"pngjs": "^3.3.0",
"yargs": "^8.0.2"
"yargs": "^13.2.4"
},
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"camelcase": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
"integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
},
"cliui": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
"requires": {
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wrap-ansi": "^2.0.0"
},
"dependencies": {
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
}
}
}
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"requires": {
"lru-cache": "^4.0.1",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
},
"execa": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
"integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"requires": {
"cross-spawn": "^5.0.1",
"get-stream": "^3.0.0",
"is-stream": "^1.1.0",
"npm-run-path": "^2.0.0",
"p-finally": "^1.0.0",
"signal-exit": "^3.0.0",
"strip-eof": "^1.0.0"
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"invert-kv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"requires": {
"number-is-nan": "^1.0.0"
}
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
},
"lcid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
"integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
"requires": {
"invert-kv": "^1.0.0"
}
},
"mem": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
"integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
"requires": {
"mimic-fn": "^1.0.0"
}
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"os-locale": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
"integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"requires": {
"execa": "^0.7.0",
"lcid": "^1.0.0",
"mem": "^1.1.0"
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"requires": {
"ansi-regex": "^2.0.0"
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"y18n": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
},
"yargs": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz",
"integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=",
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"requires": {
"camelcase": "^4.1.0",
"cliui": "^3.2.0",
"decamelize": "^1.1.1",
"get-caller-file": "^1.0.1",
"os-locale": "^2.0.0",
"read-pkg-up": "^2.0.0",
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^1.0.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^3.2.1",
"yargs-parser": "^7.0.0"
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz",
"integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"requires": {
"camelcase": "^4.1.0"
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
@ -11719,12 +11657,12 @@
"from": "git+https://github.com/marcosrdz/react-native-prompt-android.git"
},
"react-native-qrcode-svg": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-5.3.2.tgz",
"integrity": "sha512-qXujuIqog2PQ0jLa88emqDy8NcDBF41jRf5Rm/7DEY5wFnIiLrN3p7X+ucFDIIUqWuKB2gKdrb7HDs0eK2aryQ==",
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.0.6.tgz",
"integrity": "sha512-b+/teD+xj17VDujJzf956U2+9mX+gKwVJss2aqmhEIyjP7+TVOuE08D3UkzfOCWXE8gppcUTTz5gkY1NXgfwyQ==",
"requires": {
"prop-types": "^15.5.10",
"qrcode": "1.2.0"
"qrcode": "^1.3.2"
}
},
"react-native-quick-actions": {
@ -14127,11 +14065,6 @@
"bs58check": "<3.0.0"
}
},
"window-or-global": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/window-or-global/-/window-or-global-1.0.1.tgz",
"integrity": "sha1-2+RboqKRqrxW1iz2bEW3+jIpRt4="
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",

6
package.json

@ -1,6 +1,6 @@
{
"name": "bluewallet",
"version": "5.3.3",
"version": "5.3.4",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.6.2",
@ -73,7 +73,7 @@
"bip39": "2.5.0",
"bitcoinjs-lib": "5.1.6",
"bolt11": "1.2.7",
"buffer": "5.4.3",
"buffer": "5.5.0",
"buffer-reverse": "1.0.1",
"coinselect": "3.1.11",
"crypto-js": "3.1.9-1",
@ -119,7 +119,7 @@
"react-native-popup-menu-android": "1.0.3",
"react-native-privacy-snapshot": "git+https://github.com/BlueWallet/react-native-privacy-snapshot.git",
"react-native-prompt-android": "git+https://github.com/marcosrdz/react-native-prompt-android.git",
"react-native-qrcode-svg": "5.3.2",
"react-native-qrcode-svg": "6.0.6",
"react-native-quick-actions": "0.3.13",
"react-native-randombytes": "3.5.3",
"react-native-rate": "1.1.10",

16
screen/lnd/lndViewInvoice.js

@ -9,6 +9,7 @@ import {
BlueCopyTextToClipboard,
BlueNavigationStyle,
BlueSpacing20,
BlueBigCheckmark
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
@ -187,20 +188,7 @@ export default class LNDViewInvoice extends Component {
</View>
<View style={{ flex: 3, alignItems: 'center', justifyContent: 'center' }}>
<View
style={{
backgroundColor: '#ccddf9',
width: 120,
height: 120,
borderRadius: 60,
alignSelf: 'center',
justifyContent: 'center',
marginTop: -100,
marginBottom: 16,
}}
>
<Icon name="check" size={50} type="font-awesome" color="#0f5cc0" />
</View>
<BlueBigCheckmark style={{ marginTop: -100, marginBottom: 16 }} />
<BlueText>{loc.lndViewInvoice.has_been_paid}</BlueText>
</View>
<View style={{ flex: 1, justifyContent: 'flex-end', marginBottom: 24, alignItems: 'center' }}>

8
screen/receive/details.js

@ -2,7 +2,6 @@ import React, { useEffect, useState, useCallback } from 'react';
import { View, InteractionManager, Platform, TextInput, KeyboardAvoidingView, Keyboard, StyleSheet, ScrollView } from 'react-native';
import QRCode from 'react-native-qrcode-svg';
import { useNavigation, useNavigationParam } from 'react-navigation-hooks';
import bip21 from 'bip21';
import {
BlueLoading,
SafeBlueArea,
@ -21,6 +20,7 @@ import Share from 'react-native-share';
import { Chain, BitcoinUnit } from '../../models/bitcoinUnits';
import Modal from 'react-native-modal';
import HandoffSettings from '../../class/handoff';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import Handoff from 'react-native-handoff';
/** @type {AppStorage} */
const BlueApp = require('../../BlueApp');
@ -75,7 +75,7 @@ const ReceiveDetails = () => {
setAddress(wallet.getAddress());
}
InteractionManager.runAfterInteractions(async () => {
const bip21encoded = bip21.encode(address);
const bip21encoded = DeeplinkSchemaMatch.bip21encode(address);
setBip21encoded(bip21encoded);
});
}, [wallet]);
@ -116,7 +116,7 @@ const ReceiveDetails = () => {
const createCustomAmountAddress = () => {
setIsCustom(true);
setIsCustomModalVisible(false);
setBip21encoded(bip21.encode(address, { amount: customAmount, label: customLabel }));
setBip21encoded(DeeplinkSchemaMatch.bip21encode(address, { amount: customAmount, label: customLabel }));
};
const clearCustomAmount = () => {
@ -124,7 +124,7 @@ const ReceiveDetails = () => {
setIsCustomModalVisible(false);
setCustomAmount('');
setCustomLabel('');
setBip21encoded(bip21.encode(address));
setBip21encoded(DeeplinkSchemaMatch.bip21encode(address));
};
const renderCustomAmountModal = () => {

146
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,

162
screen/send/broadcast.js

@ -0,0 +1,162 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { ActivityIndicator, Linking, StyleSheet, View, KeyboardAvoidingView, Platform, Text, TextInput } from 'react-native';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { HDSegwitBech32Wallet } from '../../class';
import {
SafeBlueArea,
BlueCard,
BlueButton,
BlueSpacing10,
BlueSpacing20,
BlueFormLabel,
BlueTextCentered,
BlueBigCheckmark,
} from '../../BlueComponents';
import BlueElectrum from '../../BlueElectrum';
const bitcoin = require('bitcoinjs-lib');
const BROADCAST_RESULT = Object.freeze({
none: 'Input transaction hash',
pending: 'pending',
success: 'success',
error: 'error',
});
export default function Broadcast() {
const [tx, setTx] = useState('');
const [txHex, setTxHex] = useState('');
const [broadcastResult, setBroadcastResult] = useState(BROADCAST_RESULT.none);
const handleUpdateTxHex = nextValue => setTxHex(nextValue.trim());
const handleBroadcast = async () => {
setBroadcastResult(BROADCAST_RESULT.pending);
try {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
const walletObj = new HDSegwitBech32Wallet();
const result = await walletObj.broadcastTx(txHex);
if (result) {
let tx = bitcoin.Transaction.fromHex(txHex);
const txid = tx.getId();
setTx(txid);
setBroadcastResult(BROADCAST_RESULT.success);
} else {
setBroadcastResult(BROADCAST_RESULT.error);
}
} catch (error) {
ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false });
setBroadcastResult(BROADCAST_RESULT.error);
}
};
return (
<SafeBlueArea style={styles.blueArea}>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null} keyboardShouldPersistTaps="handled">
<View style={styles.wrapper}>
{BROADCAST_RESULT.success !== broadcastResult && (
<BlueCard style={styles.mainCard}>
<View style={styles.topFormRow}>
<BlueFormLabel>{broadcastResult}</BlueFormLabel>
{BROADCAST_RESULT.pending === broadcastResult && <ActivityIndicator size="small" />}
</View>
<TextInput
style={{
flex: 1,
borderColor: '#ebebeb',
backgroundColor: '#d2f8d6',
borderRadius: 4,
marginTop: 20,
color: '#37c0a1',
fontWeight: '500',
fontSize: 14,
paddingHorizontal: 16,
paddingBottom: 16,
paddingTop: 16,
}}
maxHeight={100}
minHeight={100}
maxWidth={'100%'}
minWidth={'100%'}
multiline
editable
value={txHex}
onChangeText={handleUpdateTxHex}
/>
<BlueSpacing10 />
<BlueButton title="BROADCAST" onPress={handleBroadcast} disabled={broadcastResult === BROADCAST_RESULT.pending} />
</BlueCard>
)}
{BROADCAST_RESULT.success === broadcastResult && <SuccessScreen tx={tx} />}
</View>
</KeyboardAvoidingView>
</SafeBlueArea>
);
}
const styles = StyleSheet.create({
wrapper: {
marginTop: 16,
alignItems: 'center',
justifyContent: 'flex-start',
},
blueArea: {
flex: 1,
paddingTop: 19,
},
broadcastResultWrapper: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
},
link: {
color: 'blue',
},
mainCard: {
padding: 0,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-start',
},
topFormRow: {
flex: 0.1,
flexBasis: 0.1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingBottom: 10,
paddingTop: 0,
paddingRight: 100,
height: 30,
maxHeight: 30,
},
});
function SuccessScreen({ tx }) {
if (!tx) {
return null;
}
return (
<View style={styles.wrapper}>
<BlueCard>
<View style={styles.broadcastResultWrapper}>
<BlueBigCheckmark />
<BlueSpacing20 />
<BlueTextCentered>Success! You transaction has been broadcasted!</BlueTextCentered>
<BlueSpacing10 />
<Text style={styles.link} onPress={() => Linking.openURL(`https://blockstream.info/tx/${tx}`)}>
Open link in explorer
</Text>
</View>
</BlueCard>
</View>
);
}
SuccessScreen.propTypes = {
tx: PropTypes.string.isRequired,
};

39
screen/send/confirm.js

@ -7,7 +7,16 @@ import { BitcoinUnit } from '../../models/bitcoinUnits';
import PropTypes from 'prop-types';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import Biometric from '../../class/biometrics';
import { HDSegwitBech32Wallet } from '../../class';
import {
HDLegacyElectrumSeedP2PKHWallet,
HDLegacyP2PKHWallet,
HDSegwitBech32Wallet,
HDSegwitP2SHWallet,
HDLegacyBreadwalletWallet,
LegacyWallet,
SegwitP2SHWallet,
SegwitBech32Wallet,
} from '../../class';
let loc = require('../../loc');
let EV = require('../../events');
let currency = require('../../currency');
@ -58,17 +67,13 @@ export default class Confirm extends Component {
}
let result = await this.state.fromWallet.broadcastTx(this.state.tx);
if (result && result.code) {
if (result.code === 1) {
const message = result.message.split('\n');
throw new Error(`${message[0]}: ${message[2]}`);
}
if (!result) {
throw new Error(`Broadcast failed`);
} else {
console.log('broadcast result = ', result);
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
let amount = 0;
const recipients = this.state.recipients;
if (recipients[0].amount === BitcoinUnit.MAX) {
if (recipients[0].amount === BitcoinUnit.MAX || !recipients[0].amount) {
amount = this.state.fromWallet.getBalance() - this.state.feeSatoshi;
} else {
for (const recipient of recipients) {
@ -76,7 +81,19 @@ export default class Confirm extends Component {
}
}
if (this.state.fromWallet.type === HDSegwitBech32Wallet.type) {
// wallets that support new createTransaction() instead of deprecated createTx()
if (
[
HDSegwitBech32Wallet.type,
HDSegwitP2SHWallet.type,
HDLegacyP2PKHWallet.type,
HDLegacyBreadwalletWallet.type,
HDLegacyElectrumSeedP2PKHWallet.type,
LegacyWallet.type,
SegwitP2SHWallet.type,
SegwitBech32Wallet.type,
].includes(this.state.fromWallet.type)
) {
amount = loc.formatBalanceWithoutSuffix(amount, BitcoinUnit.BTC, false);
}
@ -100,13 +117,14 @@ export default class Confirm extends Component {
<>
<View style={{ flexDirection: 'row', justifyContent: 'center' }}>
<Text
testID={'TransactionValue'}
style={{
color: '#0f5cc0',
fontSize: 36,
fontWeight: '600',
}}
>
{item.amount === BitcoinUnit.MAX
{!item.value || item.value === BitcoinUnit.MAX
? currency.satoshiToBTC(this.state.fromWallet.getBalance() - this.state.feeSatoshi)
: item.amount || currency.satoshiToBTC(item.value)}
</Text>
@ -176,6 +194,7 @@ export default class Confirm extends Component {
)}
<TouchableOpacity
testID={'TransactionDetailsButton'}
style={{ marginVertical: 24 }}
onPress={async () => {
if (this.isBiometricUseCapableAndEnabled) {

5
screen/send/create.js

@ -104,9 +104,9 @@ export default class SendCreate extends Component {
<Text style={styles.transactionDetailsSubtitle}>{item.address}</Text>
<Text style={styles.transactionDetailsTitle}>{loc.send.create.amount}</Text>
<Text style={styles.transactionDetailsSubtitle}>
{item.amount === BitcoinUnit.MAX
{item.value === BitcoinUnit.MAX || !item.value
? currency.satoshiToBTC(this.state.wallet.getBalance()) - this.state.fee
: item.amount || currency.satoshiToBTC(item.value)}{' '}
: currency.satoshiToBTC(item.value)}{' '}
{BitcoinUnit.BTC}
</Text>
{this.state.recipients.length > 1 && (
@ -131,6 +131,7 @@ export default class SendCreate extends Component {
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
<BlueText style={{ color: '#0c2550', fontWeight: '500' }}>{loc.send.create.this_is_hex}</BlueText>
<TextInput
testID={'TxhexInput'}
style={{
borderColor: '#ebebeb',
backgroundColor: '#d2f8d6',

176
screen/send/details.js

@ -35,18 +35,16 @@ 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 { AppStorage, HDSegwitBech32Wallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
import DocumentPicker from 'react-native-document-picker';
import RNFS from 'react-native-fs';
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
const bitcoin = require('bitcoinjs-lib');
const bip21 = require('bip21');
let BigNumber = require('bignumber.js');
const { width } = Dimensions.get('window');
/** @type {AppStorage} */
let BlueApp = require('../../BlueApp');
let BlueApp: AppStorage = require('../../BlueApp');
let loc = require('../../loc');
const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/;
@ -73,6 +71,7 @@ export default class SendDetails extends Component {
if (props.navigation.state.params) fromAddress = props.navigation.state.params.fromAddress;
let fromSecret;
if (props.navigation.state.params) fromSecret = props.navigation.state.params.fromSecret;
/** @type {LegacyWallet} */
let fromWallet = null;
if (props.navigation.state.params) fromWallet = props.navigation.state.params.fromWallet;
@ -136,12 +135,10 @@ export default class SendDetails extends Component {
bip70TransactionExpiration: bip70.bip70TransactionExpiration,
});
} else {
console.warn('2');
let recipients = this.state.addresses;
const dataWithoutSchema = data.replace('bitcoin:', '');
if (
btcAddressRx.test(dataWithoutSchema) ||
((dataWithoutSchema.indexOf('bc1') === 0 || dataWithoutSchema.indexOf('BC1') === 0) && dataWithoutSchema.indexOf('?') === -1)
) {
const dataWithoutSchema = data.replace('bitcoin:', '').replace('BITCOIN:', '');
if (this.state.fromWallet.isAddressValid(dataWithoutSchema)) {
recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema;
this.setState({
address: recipients,
@ -155,12 +152,12 @@ export default class SendDetails extends Component {
if (!data.toLowerCase().startsWith('bitcoin:')) {
data = `bitcoin:${data}`;
}
const decoded = bip21.decode(data);
const decoded = DeeplinkSchemaMatch.bip21decode(data);
address = decoded.address;
options = decoded.options;
} catch (error) {
data = data.replace(/(amount)=([^&]+)/g, '').replace(/(amount)=([^&]+)&/g, '');
const decoded = bip21.decode(data);
const decoded = DeeplinkSchemaMatch.bip21decode(data);
decoded.options.amount = 0;
address = decoded.address;
options = decoded.options;
@ -277,7 +274,7 @@ export default class SendDetails extends Component {
let address = uri || '';
let memo = '';
try {
parsedBitcoinUri = bip21.decode(uri);
parsedBitcoinUri = DeeplinkSchemaMatch.bip21decode(uri);
address = parsedBitcoinUri.hasOwnProperty('address') ? parsedBitcoinUri.address : address;
if (parsedBitcoinUri.hasOwnProperty('options')) {
if (parsedBitcoinUri.options.hasOwnProperty('amount')) {
@ -309,32 +306,6 @@ export default class SendDetails extends Component {
return (availableBalance === 'NaN' && balance) || availableBalance;
}
calculateFee(utxos, txhex, utxoIsInSatoshis) {
let index = {};
let c = 1;
index[0] = 0;
for (let utxo of utxos) {
if (!utxoIsInSatoshis) {
utxo.amount = new BigNumber(utxo.amount).multipliedBy(100000000).toNumber();
}
index[c] = utxo.amount + index[c - 1];
c++;
}
let tx = bitcoin.Transaction.fromHex(txhex);
let totalInput = index[tx.ins.length];
// ^^^ dumb way to calculate total input. we assume that signer uses utxos sequentially
// so total input == sum of yongest used inputs (and num of used inputs is `tx.ins.length`)
// TODO: good candidate to refactor and move to appropriate class. some day
let totalOutput = 0;
for (let o of tx.outs) {
totalOutput += o.value * 1;
}
return new BigNumber(totalInput - totalOutput).dividedBy(100000000).toNumber();
}
async processBIP70Invoice(text) {
try {
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(text)) {
@ -424,124 +395,35 @@ 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();
const firstTransaction = this.state.addresses[0];
const changeAddress = await wallet.getChangeAddressAsync();
let satoshis = new BigNumber(firstTransaction.amount).multipliedBy(100000000).toNumber();
const requestedSatPerByte = +this.state.fee.toString().replace(/\D/g, '');
console.log({ satoshis, requestedSatPerByte, utxo: wallet.getUtxo() });
console.log({ requestedSatPerByte, utxo: wallet.getUtxo() });
let targets = [];
for (const transaction of this.state.addresses) {
const amount =
transaction.amount === BitcoinUnit.MAX ? BitcoinUnit.MAX : new BigNumber(transaction.amount).multipliedBy(100000000).toNumber();
if (amount > 0.0 || amount === BitcoinUnit.MAX) {
targets.push({ address: transaction.address, value: amount });
if (transaction.amount === BitcoinUnit.MAX) {
// single output with MAX
targets = [{ address: transaction.address }];
break;
}
const value = new BigNumber(transaction.amount).multipliedBy(100000000).toNumber();
if (value > 0) {
targets.push({ address: transaction.address, value });
}
}
if (firstTransaction.amount === BitcoinUnit.MAX) {
targets = [{ address: firstTransaction.address, amount: BitcoinUnit.MAX }];
}
let { tx, fee, psbt } = wallet.createTransaction(
@ -849,7 +731,11 @@ export default class SendDetails extends Component {
renderCreateButton = () => {
return (
<View style={{ marginHorizontal: 56, marginVertical: 16, alignContent: 'center', backgroundColor: '#FFFFFF', minHeight: 44 }}>
{this.state.isLoading ? <ActivityIndicator /> : <BlueButton onPress={() => this.createTransaction()} title={'Next'} />}
{this.state.isLoading ? (
<ActivityIndicator />
) : (
<BlueButton onPress={() => this.createTransaction()} title={'Next'} testID={'CreateTransactionButton'} />
)}
</View>
);
};

19
screen/send/psbtWithHardwareWallet.js

@ -14,7 +14,7 @@ import {
PermissionsAndroid,
} from 'react-native';
import QRCode from 'react-native-qrcode-svg';
import { Icon, Text } from 'react-native-elements';
import { Text } from 'react-native-elements';
import {
BlueButton,
BlueText,
@ -23,6 +23,7 @@ import {
BlueNavigationStyle,
BlueSpacing20,
BlueCopyToClipboardButton,
BlueBigCheckmark,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import Share from 'react-native-share';
@ -121,7 +122,6 @@ export default class PsbtWithHardwareWallet extends Component {
await BlueElectrum.waitTillConnected();
let result = await this.state.fromWallet.broadcastTx(this.state.txhex);
if (result) {
console.log('broadcast result = ', result);
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
this.setState({ success: true, isLoading: false });
if (this.state.memo) {
@ -180,20 +180,7 @@ export default class PsbtWithHardwareWallet extends Component {
_renderSuccess() {
return (
<SafeBlueArea style={{ flex: 1 }}>
<View
style={{
backgroundColor: '#ccddf9',
width: 120,
height: 120,
borderRadius: 60,
alignSelf: 'center',
justifyContent: 'center',
marginTop: 143,
marginBottom: 53,
}}
>
<Icon name="check" size={50} type="font-awesome" color="#0f5cc0" />
</View>
<BlueBigCheckmark style={{ marginTop: 143, marginBottom: 53 }} />
<BlueCard>
<BlueButton onPress={this.props.navigation.dismiss} title={loc.send.success.done} />
</BlueCard>

19
screen/transactions/CPFP.js

@ -10,10 +10,11 @@ import {
BlueText,
BlueSpacing,
BlueNavigationStyle,
BlueBigCheckmark,
} from '../../BlueComponents';
import PropTypes from 'prop-types';
import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class';
import { Icon, Text } from 'react-native-elements';
import { Text } from 'react-native-elements';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
/** @type {AppStorage} */
let EV = require('../../events');
@ -50,7 +51,6 @@ export default class CPFP extends Component {
await BlueElectrum.waitTillConnected();
let result = await this.state.wallet.broadcastTx(this.state.txhex);
if (result) {
console.log('broadcast result = ', result);
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
this.setState({ stage: 3, isLoading: false });
this.onSuccessBroadcast();
@ -192,20 +192,7 @@ export default class CPFP extends Component {
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 76, paddingBottom: 16 }} />
</BlueCard>
<View
style={{
backgroundColor: '#ccddf9',
width: 120,
height: 120,
borderRadius: 60,
alignSelf: 'center',
justifyContent: 'center',
marginTop: 43,
marginBottom: 53,
}}
>
<Icon name="check" size={50} type="font-awesome" color="#0f5cc0" />
</View>
<BlueBigCheckmark style={{ marginTop: 43, marginBottom: 53 }} />
<BlueCard>
<BlueButton
onPress={() => {

9
screen/wallets/add.js

@ -93,8 +93,8 @@ export default class WalletsAdd extends Component {
return (
<SafeBlueArea>
<KeyboardAvoidingView enabled behavior={Platform.OS === 'ios' ? 'padding' : null} keyboardVerticalOffset={62}>
<ScrollView>
<ScrollView>
<KeyboardAvoidingView enabled behavior={Platform.OS === 'ios' ? 'padding' : null} keyboardVerticalOffset={62}>
<BlueFormLabel>{loc.wallets.add.wallet_name}</BlueFormLabel>
<View
style={{
@ -329,6 +329,7 @@ export default class WalletsAdd extends Component {
)}
</View>
<BlueButtonLink
testID="ImportWallet"
style={{ marginBottom: 0, marginTop: 24 }}
title={loc.wallets.add.import_wallet}
onPress={() => {
@ -336,8 +337,8 @@ export default class WalletsAdd extends Component {
}}
/>
</View>
</ScrollView>
</KeyboardAvoidingView>
</KeyboardAvoidingView>
</ScrollView>
</SafeBlueArea>
);
}

10
screen/wallets/buyBitcoin.js

@ -3,6 +3,7 @@ import { BlueNavigationStyle, BlueLoading, SafeBlueArea } from '../../BlueCompon
import PropTypes from 'prop-types';
import { WebView } from 'react-native-webview';
import { AppStorage, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
const currency = require('../../currency');
let BlueApp: AppStorage = require('../../BlueApp');
let loc = require('../../loc');
@ -27,6 +28,9 @@ export default class BuyBitcoin extends Component {
async componentDidMount() {
console.log('buyBitcoin - componentDidMount');
let preferredCurrency = await currency.getPreferredCurrency();
preferredCurrency = preferredCurrency.endPointKey;
/** @type {AbstractHDWallet|WatchOnlyWallet|LightningCustodianWallet} */
let wallet = this.state.wallet;
@ -38,6 +42,7 @@ export default class BuyBitcoin extends Component {
this.setState({
isLoading: false,
address,
preferredCurrency,
});
return;
}
@ -62,6 +67,7 @@ export default class BuyBitcoin extends Component {
this.setState({
isLoading: false,
address,
preferredCurrency,
});
}
@ -78,6 +84,10 @@ export default class BuyBitcoin extends Component {
uri += '&safelloStateToken=' + safelloStateToken;
}
if (this.state.preferredCurrency) {
uri += '&currency=' + this.state.preferredCurrency;
}
return (
<SafeBlueArea style={{ flex: 1 }}>
<WebView

1
screen/wallets/details.js

@ -270,6 +270,7 @@ export default class WalletDetails extends Component {
{this.renderMarketplaceButton()}
</React.Fragment>
)}
<BlueButton onPress={() => this.props.navigation.navigate('Broadcast')} title="Broadcast transaction" />
<BlueSpacing20 />
<TouchableOpacity
style={{ alignItems: 'center' }}

95
screen/wallets/import.js

@ -1,6 +1,6 @@
/* global alert */
import React, { useEffect, useState } from 'react';
import { KeyboardAvoidingView, Platform, Dimensions, View, TouchableWithoutFeedback, Keyboard } from 'react-native';
import { Platform, Dimensions, View, Keyboard } from 'react-native';
import {
BlueFormMultiInput,
BlueButtonLink,
@ -25,8 +25,14 @@ const WalletsImport = () => {
useEffect(() => {
Privacy.enableBlur();
return () => Privacy.disableBlur();
});
Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', () => setIsToolbarVisibleForAndroid(true));
Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => setIsToolbarVisibleForAndroid(false));
return () => {
Keyboard.removeListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide');
Keyboard.removeListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow');
Privacy.disableBlur();
};
}, []);
const importButtonPressed = () => {
if (importText.trim().length === 0) {
@ -62,54 +68,20 @@ const WalletsImport = () => {
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<KeyboardAvoidingView behavior="position" enabled>
<BlueFormLabel>{loc.wallets.import.explanation}</BlueFormLabel>
<BlueSpacing20 />
<BlueFormMultiInput
value={importText}
contextMenuHidden
onChangeText={setImportText}
inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}
onFocus={() => setIsToolbarVisibleForAndroid(true)}
onBlur={() => setIsToolbarVisibleForAndroid(false)}
/>
{Platform.select({
ios: (
<BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => {
setImportText('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setImportText(text);
Keyboard.dismiss();
}}
/>
),
android: isToolbarVisibleForAndroid && (
<BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => {
setImportText('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setImportText(text);
Keyboard.dismiss();
}}
/>
),
})}
</KeyboardAvoidingView>
</TouchableWithoutFeedback>
<BlueFormLabel>{loc.wallets.import.explanation}</BlueFormLabel>
<BlueSpacing20 />
<BlueFormMultiInput
testID="MnemonicInput"
value={importText}
contextMenuHidden
onChangeText={setImportText}
inputAccessoryViewID={BlueDoneAndDismissKeyboardInputAccessory.InputAccessoryViewID}
/>
<BlueSpacing20 />
<View
style={{
alignItems: 'center',
}}
>
<View style={{ flex: 1, alignItems: 'center' }}>
<BlueButton
testID="DoImport"
disabled={importText.trim().length === 0}
title={loc.wallets.import.do_import}
buttonStyle={{
@ -117,6 +89,7 @@ const WalletsImport = () => {
}}
onPress={importButtonPressed}
/>
<BlueSpacing20 />
<BlueButtonLink
title={loc.wallets.import.scan_qr}
onPress={() => {
@ -124,6 +97,32 @@ const WalletsImport = () => {
}}
/>
</View>
{Platform.select({
ios: (
<BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => {
setImportText('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setImportText(text);
Keyboard.dismiss();
}}
/>
),
android: isToolbarVisibleForAndroid && (
<BlueDoneAndDismissKeyboardInputAccessory
onClearTapped={() => {
setImportText('');
Keyboard.dismiss();
}}
onPasteTapped={text => {
setImportText(text);
Keyboard.dismiss();
}}
/>
),
})}
</SafeBlueArea>
);
};

2
screen/wallets/list.js

@ -20,7 +20,7 @@ import { PlaceholderWallet } from '../../class';
import WalletImport from '../../class/walletImport';
import ViewPager from '@react-native-community/viewpager';
import ScanQRCode from '../send/ScanQRCode';
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
let EV = require('../../events');
let A = require('../../analytics');
/** @type {AppStorage} */

491
screen/wallets/pleaseBackup.js

@ -1,430 +1,95 @@
import React, { Component } from 'react';
import { ActivityIndicator, View, BackHandler, Text } from 'react-native';
import React, { useEffect, useState, useCallback } from 'react';
import { ActivityIndicator, View, BackHandler, Text, ScrollView } from 'react-native';
import { BlueSpacing20, SafeBlueArea, BlueNavigationStyle, BlueText, BlueButton } from '../../BlueComponents';
import { Badge } from 'react-native-elements';
import PropTypes from 'prop-types';
import Privacy from '../../Privacy';
import { ScrollView } from 'react-native-gesture-handler';
let loc = require('../../loc');
import { useNavigation, useNavigationParam } from 'react-navigation-hooks';
const loc = require('../../loc');
export default class PleaseBackup extends Component {
static navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: loc.pleasebackup.title,
headerLeft: null,
headerRight: null,
});
const PleaseBackup = () => {
const [isLoading, setIsLoading] = useState(true);
const words = useNavigationParam('secret').split(' ');
const { dismiss } = useNavigation();
constructor(props) {
super(props);
this.state = {
isLoading: true,
words: props.navigation.state.params.secret.split(' '),
};
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton.bind(this));
}
handleBackButton() {
this.props.navigation.dismiss();
const handleBackButton = useCallback(() => {
dismiss();
return true;
}
}, [dismiss]);
componentDidMount() {
useEffect(() => {
Privacy.enableBlur();
this.setState({
isLoading: false,
});
}
componentWillUnmount() {
Privacy.disableBlur();
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton.bind(this));
}
setIsLoading(false);
return () => {
Privacy.disableBlur();
BackHandler.removeEventListener('hardwareBackPress', handleBackButton);
};
}, [handleBackButton, words]);
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
<ActivityIndicator />
</View>
const renderSecret = () => {
let component = [];
for (const [index, secret] of words.entries()) {
component.push(
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }} key={`${secret}${index}`}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>
{`${index}`}. {secret}
</Text>
</Badge>
</View>,
);
}
return component;
};
return (
<SafeBlueArea style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ justifyContent: 'space-between' }} testID="PleaseBackupScrollView">
<View style={{ alignItems: 'center', paddingHorizontal: 16 }}>
<BlueText style={{ textAlign: 'center', fontWeight: 'bold', color: '#0C2550' }}>{loc.pleasebackup.success}</BlueText>
<BlueText style={{ paddingBottom: 10, paddingRight: 0, paddingLeft: 0, color: '#0C2550' }}>{loc.pleasebackup.text}</BlueText>
return isLoading ? (
<View style={{ flex: 1, paddingTop: 20 }}>
<ActivityIndicator />
</View>
) : (
<SafeBlueArea style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ justifyContent: 'space-between' }} testID="PleaseBackupScrollView">
<View style={{ alignItems: 'center', paddingHorizontal: 16 }}>
<BlueText style={{ textAlign: 'center', fontWeight: 'bold', color: '#0C2550' }}>{loc.pleasebackup.success}</BlueText>
<BlueText style={{ paddingBottom: 10, paddingRight: 0, paddingLeft: 0, color: '#0C2550' }}>{loc.pleasebackup.text}</BlueText>
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap',
marginTop: 14,
}}
>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>1. {this.state.words[0]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>2. {this.state.words[1]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>3. {this.state.words[2]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>4. {this.state.words[3]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>5. {this.state.words[4]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>6. {this.state.words[5]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>7. {this.state.words[6]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>8. {this.state.words[7]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>9. {this.state.words[8]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>10. {this.state.words[9]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>11. {this.state.words[10]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>12. {this.state.words[11]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>13. {this.state.words[12]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>14. {this.state.words[13]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>15. {this.state.words[14]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>16. {this.state.words[15]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>17. {this.state.words[16]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>18. {this.state.words[17]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>19. {this.state.words[18]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>20. {this.state.words[19]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>21. {this.state.words[20]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>22. {this.state.words[21]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>23. {this.state.words[22]}</Text>
</Badge>
</View>
<View style={{ width: 'auto', marginRight: 8, marginBottom: 8 }}>
<Badge
containerStyle={{
backgroundColor: '#f5f5f5',
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 8,
paddingRight: 8,
borderRadius: 4,
}}
>
<Text style={{ color: '#81868E', fontWeight: 'bold' }}>24. {this.state.words[23]}</Text>
</Badge>
</View>
</View>
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap',
marginTop: 14,
}}
>
{renderSecret()}
</View>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', flexWrap: 'wrap' }}>
<View style={{ flex: 1 }}>
<BlueSpacing20 />
<BlueButton testID="PleasebackupOk" onPress={() => this.props.navigation.dismiss()} title={loc.pleasebackup.ok} />
</View>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', flexWrap: 'wrap' }}>
<View style={{ flex: 1 }}>
<BlueSpacing20 />
<BlueButton testID="PleasebackupOk" onPress={dismiss} title={loc.pleasebackup.ok} />
</View>
</View>
</ScrollView>
</SafeBlueArea>
);
}
}
PleaseBackup.propTypes = {
navigation: PropTypes.shape({
state: PropTypes.shape({
params: PropTypes.shape({
secret: PropTypes.string,
}),
}),
dismiss: PropTypes.func,
}),
</View>
</ScrollView>
</SafeBlueArea>
);
};
PleaseBackup.navigationOptions = ({ navigation }) => ({
...BlueNavigationStyle(navigation, true),
title: loc.pleasebackup.title,
headerLeft: null,
headerRight: null,
});
export default PleaseBackup;

81
tests/e2e/bluewallet.spec.js

@ -1,5 +1,8 @@
/* global it, describe, expect, element, by, waitFor, device */
const bitcoin = require('bitcoinjs-lib');
const assert = require('assert');
describe('BlueWallet UI Tests', () => {
it('selftest passes', async () => {
await waitFor(element(by.id('WalletsList')))
@ -300,6 +303,78 @@ describe('BlueWallet UI Tests', () => {
await yo('WalletsList');
await expect(element(by.id('cr34t3d'))).toBeVisible();
});
it('can import BIP84 mnemonic, fetch balance & transactions, then create a transaction', async () => {
if (!process.env.HD_MNEMONIC_BIP84) {
console.error('process.env.HD_MNEMONIC_BIP84 not set, skipped');
return;
}
await yo('WalletsList');
// going to Import Wallet screen and importing mnemonic for existing BIP84 wallet with real balance
await element(by.id('CreateAWallet')).tap();
await element(by.id('ImportWallet')).tap();
await element(by.id('MnemonicInput')).typeText(process.env.HD_MNEMONIC_BIP84);
try {
await element(by.id('DoImport')).tap();
} catch (_) {}
await sleep(60000);
await sup('OK', 3 * 61000); // waiting for wallet import
await element(by.text('OK')).tap();
// ok, wallet imported
// lets go inside wallet
await element(by.text('Imported HD SegWit (BIP84 Bech32 Native)')).tap();
// label might change in the future; see HDSegwitBech32Wallet.typeReadable
expect(element(by.id('WalletBalance'))).toHaveText('0.00105526 BTC');
// lets create real transaction:
await element(by.id('SendButton')).tap();
await element(by.id('AddressInput')).typeText('bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl');
await element(by.id('BitcoinAmountInput')).typeText('0.0005\n');
await sleep(5000);
try {
await element(by.id('CreateTransactionButton')).tap();
} catch (_) {}
// created. verifying:
await yo('TransactionValue');
expect(element(by.id('TransactionValue'))).toHaveText('0.0005');
await element(by.id('TransactionDetailsButton')).tap();
// now, a hack to extract element text. warning, this might break in future
// @see https://github.com/wix/detox/issues/445
let txhex = '';
try {
await expect(element(by.id('TxhexInput'))).toHaveText('_unfoundable_text');
} catch (error) {
if (device.getPlatform() === 'ios') {
const start = `accessibilityLabel was "`;
const end = '" on ';
const errorMessage = error.message.toString();
const [, restMessage] = errorMessage.split(start);
const [label] = restMessage.split(end);
txhex = label;
} else {
const start = 'Got:';
const end = '}"';
const errorMessage = error.message.toString();
const [, restMessage] = errorMessage.split(start);
const [label] = restMessage.split(end);
const value = label.split(',');
var combineText = value.find(i => i.includes('text=')).trim();
const [, elementText] = combineText.split('=');
txhex = elementText;
}
}
let transaction = bitcoin.Transaction.fromHex(txhex);
assert.strictEqual(transaction.ins.length, 2);
assert.strictEqual(transaction.outs.length, 2);
assert.strictEqual(bitcoin.address.fromOutputScript(transaction.outs[0].script), 'bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl'); // to address
assert.strictEqual(transaction.outs[0].value, 50000);
});
});
async function sleep(ms) {
@ -312,6 +387,12 @@ async function yo(id, timeout = 33000) {
.withTimeout(timeout);
}
async function sup(text, timeout = 33000) {
return waitFor(element(by.text(text)))
.toBeVisible()
.withTimeout(timeout);
}
async function helperCreateWallet(walletName) {
await element(by.id('CreateAWallet')).tap();
await element(by.id('WalletNameInput')).typeText(walletName || 'cr34t3d');

184
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);
});

10
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;

45
tests/unit/deepLinkSchemaMatch.test.js → tests/unit/deeplink-schema-match.test.js

@ -1,5 +1,5 @@
/* global describe, it */
import DeeplinkSchemaMatch from '../../class/deeplinkSchemaMatch';
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
const assert = require('assert');
describe('unit - DeepLinkSchemaMatch', function() {
@ -7,12 +7,18 @@ describe('unit - DeepLinkSchemaMatch', function() {
assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:bc1qh6tf004ty7z7un2v5ntu4mkf630545gvhs45u7?amount=666&label=Yo'));
assert.ok(DeeplinkSchemaMatch.hasSchema('bitcoin:BC1QH6TF004TY7Z7UN2V5NTU4MKF630545GVHS45U7?amount=666&label=Yo'));
assert.ok(DeeplinkSchemaMatch.hasSchema('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE'));
assert.ok(DeeplinkSchemaMatch.hasSchema('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE?amount=666&label=Yo'));
});
it('isBitcoin Address', () => {
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('3GcKN7q7gZuZ8eHygAhHrvPa5zZbG5Q1rK'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('bc1qykcp2x3djgdtdwelxn9z4j2y956npte0a4sref'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BC1QYKCP2X3DJGDTDWELXN9Z4J2Y956NPTE0A4SREF'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('bitcoin:BC1QH6TF004TY7Z7UN2V5NTU4MKF630545GVHS45U7'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE'));
assert.ok(DeeplinkSchemaMatch.isBitcoinAddress('BITCOIN:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE?amount=666&label=Yo'));
});
it('isLighting Invoice', () => {
@ -26,7 +32,12 @@ describe('unit - DeepLinkSchemaMatch', function() {
it('isBoth Bitcoin & Invoice', () => {
assert.ok(
DeeplinkSchemaMatch.isBothBitcoinAndLightning(
'bitcoin:1DamianM2k8WfNEeJmyqSe2YW1upB7UATx?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4',
'bitcoin:BC1Q3RL0MKYK0ZRTXFMQN9WPCD3GNAZ00YV9YP0HXE?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4',
),
);
assert.ok(
DeeplinkSchemaMatch.isBothBitcoinAndLightning(
'BITCOIN:12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG?amount=0.000001&lightning=lnbc1u1pwry044pp53xlmkghmzjzm3cljl6729cwwqz5hhnhevwfajpkln850n7clft4sdqlgfy4qv33ypmj7sj0f32rzvfqw3jhxaqcqzysxq97zvuq5zy8ge6q70prnvgwtade0g2k5h2r76ws7j2926xdjj2pjaq6q3r4awsxtm6k5prqcul73p3atveljkn6wxdkrcy69t6k5edhtc6q7lgpe4m5k4',
),
);
});
@ -55,4 +66,34 @@ describe('unit - DeepLinkSchemaMatch', function() {
});
});
});
it('decodes bip21', () => {
let decoded = DeeplinkSchemaMatch.bip21decode('bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar');
assert.deepStrictEqual(decoded, {
address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH',
options: {
amount: 20.3,
label: 'Foobar',
},
});
decoded = DeeplinkSchemaMatch.bip21decode('BITCOIN:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar');
assert.deepStrictEqual(decoded, {
address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH',
options: {
amount: 20.3,
label: 'Foobar',
},
});
});
it('encodes bip21', () => {
let encoded = DeeplinkSchemaMatch.bip21encode('1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH');
assert.strictEqual(encoded, 'bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH');
encoded = DeeplinkSchemaMatch.bip21encode('1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', {
amount: 20.3,
label: 'Foobar',
});
assert.strictEqual(encoded, 'bitcoin:1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH?amount=20.3&label=Foobar');
});
});

27
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',
);
});

15
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());
});

131
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);
});

9
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");

70
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);
});

42
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
});
});

39
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
});
});

39
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
});
});

282
tests/unit/signer.test.js

@ -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',
);
});
});
});
Loading…
Cancel
Save