ADD: BIP84 send, technical release

This commit is contained in:
Overtorment 2019-05-28 21:30:03 +01:00
parent 8e9015e2fb
commit 38c7ecf79d
5 changed files with 161 additions and 20 deletions

View file

@ -104,7 +104,6 @@ describe('Bech32 Segwit HD (BIP84)', () => {
await hd.fetchBalance();
await hd.fetchTransactions();
assert.strictEqual(hd.getTransactions().length, 4);
// console.warn(JSON.stringify(hd.getTransactions(), null, 2));
for (let tx of hd.getTransactions()) {
assert.ok(tx.hash);
@ -161,4 +160,65 @@ describe('Bech32 Segwit HD (BIP84)', () => {
hd2.setSecret(hd.getSecret());
assert.ok(hd2.validateMnemonic());
});
it.only('can create transactions', async () => {
if (!process.env.HD_MNEMONIC_BIP84) {
console.error('process.env.HD_MNEMONIC_BIP84 not set, skipped');
return;
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
let hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC_BIP84);
assert.ok(hd.validateMnemonic());
let start = +new Date();
await hd.fetchBalance();
let end = +new Date();
console.log('balance', hd.getBalance());
end - start > 5000 && console.warn('fetchBalance took', (end - start) / 1000, 'sec');
start = +new Date();
await hd.fetchTransactions();
end = +new Date();
end - start > 15000 && console.warn('fetchTransactions took', (end - start) / 1000, 'sec');
let txFound = 0;
for (let tx of hd.getTransactions()) {
if (tx.hash === 'e9ef58baf4cff3ad55913a360c2fa1fd124309c59dcd720cdb172ce46582097b') {
assert.strictEqual(tx.value, -129545);
txFound++;
}
if (tx.hash === 'e112771fd43962abfe4e4623bf788d6d95ff1bd0f9b56a6a41fb9ed4dacc75f1') {
assert.strictEqual(tx.value, 1000000);
txFound++;
}
}
assert.ok(txFound === 2);
await hd.fetchUtxo();
let changeAddress = await hd.getChangeAddressAsync();
assert.ok(changeAddress && changeAddress.startsWith('bc1'));
let { tx, inputs, outputs, fee } = hd.createTransaction(
hd.getUtxo(),
[{ address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu', value: 101000 }],
13,
changeAddress,
);
assert.strictEqual(Math.round(fee / tx.byteLength()), 13);
let totalInput = 0;
for (let inp of inputs) {
totalInput += inp.value;
}
let totalOutput = 0;
for (let outp of outputs) {
totalOutput += outp.value;
}
assert.strictEqual(totalInput - totalOutput, fee);
assert.strictEqual(outputs[outputs.length - 1].address, changeAddress);
});
});

View file

@ -338,14 +338,14 @@ export class AbstractHDWallet extends LegacyWallet {
}
// no luck - lets iterate over all addresses we have up to first unused address index
for (let c = 0; c <= this.next_free_change_address_index + 3; c++) {
for (let c = 0; c <= this.next_free_change_address_index + this.gap_limit; c++) {
let possibleAddress = this._getInternalAddressByIndex(c);
if (possibleAddress === address) {
return (this._address_to_wif_cache[address] = this._getInternalWIFByIndex(c));
}
}
for (let c = 0; c <= this.next_free_address_index + 3; c++) {
for (let c = 0; c <= this.next_free_address_index + this.gap_limit; c++) {
let possibleAddress = this._getExternalAddressByIndex(c);
if (possibleAddress === address) {
return (this._address_to_wif_cache[address] = this._getExternalWIFByIndex(c));

View file

@ -3,10 +3,11 @@ import { NativeModules } from 'react-native';
import bip39 from 'bip39';
import BigNumber from 'bignumber.js';
import b58 from 'bs58check';
import signer from '../models/signer';
const BlueElectrum = require('../BlueElectrum');
const bitcoin5 = require('bitcoinjs5');
const HDNode = require('bip32');
const coinSelectAccumulative = require('coinselect/accumulative');
const coinSelectSplit = require('coinselect/split');
const { RNRandomBytes } = NativeModules;
@ -216,13 +217,26 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
// if txs are absent for some internal address in hierarchy - this is a sign
// we should fetch txs for that address
// OR if some address has unconfirmed balance - should fetch it's txs
// OR some tx for address is unconfirmed
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (!this._txs_by_external_index[c] || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) {
// external addresses first
let hasUnconfirmed = false;
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || (!tx.confirmations || tx.confirmations === 0);
if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) {
this._txs_by_external_index[c] = await BlueElectrum.getTransactionsFullByAddress(this._getExternalAddressByIndex(c));
}
}
for (let c = 0; c < this.next_free_change_address_index + 1 /* this.gap_limit */; c++) {
if (!this._txs_by_internal_index[c] || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
// next, internal addresses
let hasUnconfirmed = false;
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || (!tx.confirmations || tx.confirmations === 0);
if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) {
this._txs_by_internal_index[c] = await BlueElectrum.getTransactionsFullByAddress(this._getInternalAddressByIndex(c));
}
}
@ -241,6 +255,8 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
let ret = [];
for (let tx of txs) {
tx.received = tx.blocktime * 1000;
if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed
tx.confirmations = tx.confirmations || 0; // unconfirmed
tx.hash = tx.txid;
tx.value = 0;
@ -260,7 +276,17 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
ret.push(tx);
}
return ret;
// now, deduplication:
let usedTxIds = {};
let ret2 = [];
for (let tx of ret) {
if (!usedTxIds[tx.txid]) ret2.push(tx);
usedTxIds[tx.txid] = 1;
}
return ret2.sort(function(a, b) {
return b.received - a.received;
});
}
async _fetchBalance() {
@ -376,24 +402,73 @@ export class HDSegwitBech32Wallet extends AbstractHDWallet {
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._getExternalAddressByIndex(c) === address) return true;
}
for (let c = 0; c < this.next_free_change_address_index + 1 /* this.gap_limit */; c++) {
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === address) return true;
}
return false;
}
createTx(utxos, amount, fee, address) {
for (let utxo of utxos) {
utxo.wif = this._getWifForAddress(utxo.address);
throw new Error('Deprecated');
}
/**
*
* @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
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number}}
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
if (!changeAddress) throw new Error('No change address provided');
sequence = sequence || 0;
let algo = coinSelectAccumulative;
if (targets.length === 1 && targets[0] && !targets[0].value) {
// we want to send MAX
algo = coinSelectSplit;
}
let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10));
return signer.createHDSegwitTransaction(
utxos,
address,
amountPlusFee,
fee,
this._getInternalAddressByIndex(this.next_free_change_address_index),
);
let { inputs, outputs, fee } = algo(utxos, targets, feeRate);
console.log({ inputs, outputs, fee });
// .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 txb = new bitcoin5.TransactionBuilder();
let c = 0;
let keypairs = {};
let values = {};
inputs.forEach(input => {
const keyPair = bitcoin5.ECPair.fromWIF(this._getWifForAddress(input.address));
keypairs[c] = keyPair;
values[c] = input.value;
c++;
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input');
const p2wpkh = bitcoin5.payments.p2wpkh({ pubkey: keyPair.publicKey });
txb.addInput(input.txId, input.vout, sequence, p2wpkh.output); // NOTE: provide the prevOutScript!
});
outputs.forEach(output => {
// if output has no address - this is change output
if (!output.address) {
output.address = changeAddress;
}
txb.addOutput(output.address, output.value);
});
for (let cc = 0; cc < c; cc++) {
txb.sign(cc, keypairs[cc], null, null, values[cc]); // NOTE: no redeem script
}
const tx = txb.build();
return { tx, inputs, outputs, fee };
}
}

5
package-lock.json generated
View file

@ -5345,6 +5345,11 @@
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"coinselect": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/coinselect/-/coinselect-3.1.11.tgz",
"integrity": "sha1-4fBjvRpYgvZzXuBRm52LWsSpMJk="
},
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",

View file

@ -49,12 +49,13 @@
"bip32": "2.0.2",
"bip39": "2.5.0",
"bitcoinjs-lib": "3.3.2",
"bitcoinjs5": "git+https://github.com/Overtorment/bitcoinjs5.git",
"buffer": "5.2.1",
"buffer-reverse": "1.0.1",
"coinselect": "3.1.11",
"crypto-js": "3.1.9-1",
"dayjs": "1.8.13",
"electrum-client": "git+https://github.com/Overtorment/rn-electrum-client.git",
"bitcoinjs5": "git+https://github.com/Overtorment/bitcoinjs5.git",
"eslint-config-prettier": "4.2.0",
"eslint-config-standard": "12.0.0",
"eslint-config-standard-react": "7.0.2",