mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-26 08:55:56 +01:00
ADD: BIP84 send, technical release
This commit is contained in:
parent
8e9015e2fb
commit
38c7ecf79d
5 changed files with 161 additions and 20 deletions
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
5
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue