mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 01:40:12 +01:00
ADD: technical release to be able to send outputs with custom scripts (OP_RETURNs in particular)
This commit is contained in:
parent
f37818a956
commit
472cef126c
@ -6,7 +6,7 @@ import * as bip39 from 'bip39';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import { Transaction as BTransaction, Psbt } from 'bitcoinjs-lib';
|
||||
import b58 from 'bs58check';
|
||||
import { CoinSelectReturnInput, CoinSelectTarget } from 'coinselect';
|
||||
import { CoinSelectReturnInput } from 'coinselect';
|
||||
import { ECPairFactory } from 'ecpair';
|
||||
import { ECPairInterface } from 'ecpair/src/ecpair';
|
||||
|
||||
@ -15,7 +15,7 @@ import { ElectrumHistory } from '../../blue_modules/BlueElectrum';
|
||||
import ecc from '../../blue_modules/noble_ecc';
|
||||
import { randomBytes } from '../rng';
|
||||
import { AbstractHDWallet } from './abstract-hd-wallet';
|
||||
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
|
||||
import { CreateTransactionResult, CreateTransactionTarget, CreateTransactionUtxo, Transaction, Utxo } from './types';
|
||||
|
||||
const ECPair = ECPairFactory(ecc);
|
||||
const bip32 = BIP32Factory(ecc);
|
||||
@ -52,10 +52,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
// BIP47
|
||||
_enable_BIP47: boolean;
|
||||
_payment_code: string;
|
||||
_sender_payment_codes: string[];
|
||||
_sender_payment_codes: string[]; // who can pay us
|
||||
_payer_payment_codes: string[]; // whom can we pay
|
||||
_addresses_by_payment_code: Record<string, string[]>;
|
||||
_next_free_payment_code_address_index: Record<string, number>;
|
||||
_txs_by_payment_code_index: Record<string, Transaction[][]>;
|
||||
_txs_by_payment_code_index: Record<string, Transaction[][]>; // incoming tx e.g. someone paid us
|
||||
_balances_by_payment_code_index: Record<string, BalanceByIndex>;
|
||||
_bip47_instance?: BIP47Interface;
|
||||
|
||||
@ -73,6 +74,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
this._enable_BIP47 = false;
|
||||
this._payment_code = '';
|
||||
this._sender_payment_codes = [];
|
||||
this._payer_payment_codes = [];
|
||||
this._next_free_payment_code_address_index = {};
|
||||
this._txs_by_payment_code_index = {};
|
||||
this._balances_by_payment_code_index = {};
|
||||
@ -1108,7 +1110,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
*/
|
||||
createTransaction(
|
||||
utxos: CreateTransactionUtxo[],
|
||||
targets: CoinSelectTarget[],
|
||||
targets: CreateTransactionTarget[],
|
||||
feeRate: number,
|
||||
changeAddress: string,
|
||||
sequence: number,
|
||||
@ -1128,13 +1130,18 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
}
|
||||
|
||||
for (const t of targets) {
|
||||
if (t.address.startsWith('bc1')) {
|
||||
if (t.address && t.address.startsWith('bc1')) {
|
||||
// in case address is non-typical and takes more bytes than coinselect library anticipates by default
|
||||
t.script = { length: bitcoin.address.toOutputScript(t.address).length + 3 };
|
||||
}
|
||||
|
||||
if (t.script?.hex) {
|
||||
// setting length for coinselect lib manually as it is not aware of our field `hex`
|
||||
t.script.length = t.script.hex.length / 2 - 4;
|
||||
}
|
||||
}
|
||||
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate);
|
||||
|
||||
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
|
||||
let psbt = new bitcoin.Psbt();
|
||||
@ -1172,9 +1179,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
});
|
||||
|
||||
outputs.forEach(output => {
|
||||
// if output has no address - this is change output
|
||||
// if output has no address - this is change output or a custom script output
|
||||
let change = false;
|
||||
if (!output.address) {
|
||||
// @ts-ignore
|
||||
if (!output.address && !output.script?.hex) {
|
||||
change = true;
|
||||
output.address = changeAddress;
|
||||
}
|
||||
@ -1197,6 +1205,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
||||
|
||||
psbt.addOutput({
|
||||
address: output.address,
|
||||
// @ts-ignore types from bitcoinjs are not exported so we cant define outputData separately and add fields conditionally (either address or script should be present)
|
||||
script: output.script?.hex ? Buffer.from(output.script.hex, 'hex') : undefined,
|
||||
value: output.value,
|
||||
bip32Derivation:
|
||||
change && path && pubkey
|
||||
|
@ -5,9 +5,9 @@ import { AbstractWallet } from './abstract-wallet';
|
||||
import { HDSegwitBech32Wallet } from '..';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
|
||||
import coinSelect, { CoinSelectOutput, CoinSelectReturnInput, CoinSelectTarget, CoinSelectUtxo } from 'coinselect';
|
||||
import coinSelect, { CoinSelectOutput, CoinSelectReturnInput, CoinSelectTarget } from 'coinselect';
|
||||
import coinSelectSplit from 'coinselect/split';
|
||||
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
|
||||
import { CreateTransactionResult, CreateTransactionTarget, CreateTransactionUtxo, Transaction, Utxo } from './types';
|
||||
import { ECPairAPI, ECPairFactory, Signer } from 'ecpair';
|
||||
|
||||
import ecc from '../../blue_modules/noble_ecc';
|
||||
@ -369,24 +369,21 @@ export class LegacyWallet extends AbstractWallet {
|
||||
}
|
||||
|
||||
coinselect(
|
||||
utxos: CoinSelectUtxo[],
|
||||
targets: CoinSelectTarget[],
|
||||
utxos: CreateTransactionUtxo[],
|
||||
targets: CreateTransactionTarget[],
|
||||
feeRate: number,
|
||||
changeAddress: string,
|
||||
): {
|
||||
inputs: CoinSelectReturnInput[];
|
||||
outputs: CoinSelectOutput[];
|
||||
fee: number;
|
||||
} {
|
||||
if (!changeAddress) throw new Error('No change address provided');
|
||||
|
||||
let algo = coinSelect;
|
||||
// if targets has output without a value, we want send MAX to it
|
||||
if (targets.some(i => !('value' in i))) {
|
||||
algo = coinSelectSplit;
|
||||
}
|
||||
|
||||
const { inputs, outputs, fee } = algo(utxos, targets, feeRate);
|
||||
const { inputs, outputs, fee } = algo(utxos, targets as CoinSelectTarget[], feeRate);
|
||||
|
||||
// .inputs and .outputs will be undefined if no solution was found
|
||||
if (!inputs || !outputs) {
|
||||
@ -417,7 +414,7 @@ export class LegacyWallet extends AbstractWallet {
|
||||
masterFingerprint: number,
|
||||
): CreateTransactionResult {
|
||||
if (targets.length === 0) throw new Error('No destination provided');
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate);
|
||||
sequence = sequence || 0xffffffff; // disable RBF by default
|
||||
const psbt = new bitcoin.Psbt();
|
||||
let c = 0;
|
||||
|
@ -957,7 +957,7 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
|
||||
}
|
||||
}
|
||||
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate);
|
||||
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
|
||||
|
||||
let psbt = new bitcoin.Psbt();
|
||||
|
@ -84,7 +84,7 @@ export class SegwitBech32Wallet extends LegacyWallet {
|
||||
for (const u of utxos) {
|
||||
u.script = { length: 27 };
|
||||
}
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate);
|
||||
sequence = sequence || 0xffffffff; // disable RBF by default
|
||||
const psbt = new bitcoin.Psbt();
|
||||
let c = 0;
|
||||
|
@ -102,7 +102,7 @@ export class SegwitP2SHWallet extends LegacyWallet {
|
||||
for (const u of utxos) {
|
||||
u.script = { length: 50 };
|
||||
}
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate, changeAddress);
|
||||
const { inputs, outputs, fee } = this.coinselect(utxos, targets, feeRate);
|
||||
sequence = sequence || 0xffffffff; // disable RBF by default
|
||||
const psbt = new bitcoin.Psbt();
|
||||
let c = 0;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import bitcoin from 'bitcoinjs-lib';
|
||||
import { CoinSelectOutput, CoinSelectReturnInput } from 'coinselect';
|
||||
import { CoinSelectOutput, CoinSelectReturnInput, CoinSelectUtxo } from 'coinselect';
|
||||
import { HDAezeedWallet } from './hd-aezeed-wallet';
|
||||
import { HDLegacyBreadwalletWallet } from './hd-legacy-breadwallet-wallet';
|
||||
import { HDLegacyElectrumSeedP2PKHWallet } from './hd-legacy-electrum-seed-p2pkh-wallet';
|
||||
@ -31,16 +31,19 @@ export type Utxo = {
|
||||
};
|
||||
|
||||
/**
|
||||
* basically the same as coinselect.d.ts/CoinselectUtxo
|
||||
* and should be unified as soon as bullshit with txid/txId is sorted
|
||||
* same as coinselect.d.ts/CoinSelectUtxo
|
||||
*/
|
||||
export type CreateTransactionUtxo = {
|
||||
txid: string;
|
||||
txhex: string;
|
||||
vout: number;
|
||||
value: number;
|
||||
export interface CreateTransactionUtxo extends CoinSelectUtxo {}
|
||||
|
||||
/**
|
||||
* if address is missing and `script.hex` is set - this is a custom script (like OP_RETURN)
|
||||
*/
|
||||
export type CreateTransactionTarget = {
|
||||
address?: string;
|
||||
value?: number;
|
||||
script?: {
|
||||
length: number;
|
||||
length?: number; // either length or hex should be present
|
||||
hex?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,8 @@ import BIP47Factory from '@spsina/bip47';
|
||||
import assert from 'assert';
|
||||
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import ecc from 'tiny-secp256k1';
|
||||
import ecc from '../../blue_modules/noble_ecc';
|
||||
|
||||
const ECPair = ECPairFactory(ecc);
|
||||
|
||||
jest.setTimeout(90 * 1000);
|
||||
@ -135,5 +136,11 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
|
||||
?.scriptPubKey.hex,
|
||||
'6a4c50' + blindedPaymentCode,
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
w.getTransactions().find(tx => tx.txid === '06b4c14587182fd0474f265a77b156519b4778769a99c21623863a8194d0fa4f')?.outputs?.[1]
|
||||
.scriptPubKey.addresses[0],
|
||||
bobBip47.getNotificationAddress(),
|
||||
); // transaction is to Bob's notification address
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import BIP47Factory from '@spsina/bip47';
|
||||
import ecc from 'tiny-secp256k1';
|
||||
import ecc from '../../blue_modules/noble_ecc';
|
||||
import assert from 'assert';
|
||||
|
||||
import { HDSegwitBech32Wallet, WatchOnlyWallet } from '../../class';
|
||||
@ -23,6 +23,8 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
|
||||
|
||||
expect(bobNotificationAddress).toEqual('1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV'); // our notif address
|
||||
|
||||
assert.strictEqual(bobWallet.getBIP47NotificationAddress(), '1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV'); // our notif address
|
||||
|
||||
assert.ok(!bobWallet.weOwnAddress('1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW')); // alice notif address, we dont own it
|
||||
});
|
||||
|
||||
@ -68,6 +70,8 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
|
||||
|
||||
expect(ourNotificationAddress).toEqual('1EiP2kSqxNqRhn8MPMkrtSEqaWiCWLYyTS'); // our notif address
|
||||
|
||||
assert.strictEqual(w.getBIP47NotificationAddress(), '1EiP2kSqxNqRhn8MPMkrtSEqaWiCWLYyTS'); // our notif address
|
||||
|
||||
// since we dont do network calls in unit test we cant get counterparties payment codes from our notif address,
|
||||
// and thus, dont know collaborative addresses with our payers. lets hardcode our counterparty payment code to test
|
||||
// this functionality
|
||||
|
@ -1,5 +1,6 @@
|
||||
import assert from 'assert';
|
||||
import { HDSegwitBech32Wallet } from '../../class';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
|
||||
describe('Bech32 Segwit HD (BIP84)', () => {
|
||||
it('can create', async function () {
|
||||
@ -213,7 +214,7 @@ describe('Bech32 Segwit HD (BIP84)', () => {
|
||||
assert.notStrictEqual(id1, id3);
|
||||
});
|
||||
|
||||
it('cat createTransaction with a correct feerate (with lenghty segwit address)', () => {
|
||||
it('can createTransaction with a correct feerate (with lenghty segwit address)', () => {
|
||||
if (!process.env.HD_MNEMONIC_BIP84) {
|
||||
console.error('process.env.HD_MNEMONIC_BIP84 not set, skipped');
|
||||
return;
|
||||
@ -232,18 +233,71 @@ describe('Bech32 Segwit HD (BIP84)', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { tx, psbt } = hd.createTransaction(
|
||||
const { tx, psbt, outputs } = hd.createTransaction(
|
||||
utxo,
|
||||
[{ address: 'bc1qtmcfj7lvgjp866w8lytdpap82u7eege58jy52hp4ctk0hsncegyqel8prp' }], // sendMAX
|
||||
1,
|
||||
'bc1qtmcfj7lvgjp866w8lytdpap82u7eege58jy52hp4ctk0hsncegyqel8prp', // change wont actually be used
|
||||
[{ address: 'bc1qtmcfj7lvgjp866w8lytdpap82u7eege58jy52hp4ctk0hsncegyqel8prp', value: 546 }],
|
||||
10,
|
||||
'bc1qtmcfj7lvgjp866w8lytdpap82u7eege58jy52hp4ctk0hsncegyqel8prp',
|
||||
);
|
||||
|
||||
assert.strictEqual(outputs.length, 2);
|
||||
|
||||
const actualFeerate = psbt.getFee() / tx.virtualSize();
|
||||
assert.strictEqual(
|
||||
actualFeerate >= 1.0,
|
||||
Math.round(actualFeerate) >= 10 && actualFeerate <= 11,
|
||||
true,
|
||||
`bad feerate, got ${actualFeerate}, expected at least 1; fee: ${psbt.getFee()}; virsualSize: ${tx.virtualSize()} vbytes; ${tx.toHex()}`,
|
||||
`bad feerate, got ${actualFeerate}, expected at least 10; fee: ${psbt.getFee()}; virsualSize: ${tx.virtualSize()} vbytes; ${tx.toHex()}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('can createTransaction with OP_RETURN', () => {
|
||||
if (!process.env.HD_MNEMONIC_BIP84) {
|
||||
console.error('process.env.HD_MNEMONIC_BIP84 not set, skipped');
|
||||
return;
|
||||
}
|
||||
const hd = new HDSegwitBech32Wallet();
|
||||
hd.setSecret(process.env.HD_MNEMONIC_BIP84);
|
||||
assert.ok(hd.validateMnemonic());
|
||||
|
||||
const utxo = [
|
||||
{
|
||||
address: 'bc1q063ctu6jhe5k4v8ka99qac8rcm2tzjjnuktyrl',
|
||||
vout: 0,
|
||||
txid: '8b0ab2c7196312e021e0d3dc73f801693826428782970763df6134457bd2ec20',
|
||||
value: 69909,
|
||||
wif: '-',
|
||||
},
|
||||
];
|
||||
|
||||
const { tx, psbt, outputs } = hd.createTransaction(
|
||||
utxo,
|
||||
[
|
||||
{ address: hd._getExternalAddressByIndex(0), value: 546 },
|
||||
{ script: { hex: '00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff' }, value: 0 },
|
||||
],
|
||||
150,
|
||||
hd._getInternalAddressByIndex(0),
|
||||
);
|
||||
|
||||
assert.strictEqual(outputs.length, 3); // destination, op_return, change
|
||||
assert.ok(!outputs[1].address); // should not be there as it should be OP_RETURN
|
||||
|
||||
const decodedTx = bitcoin.Transaction.fromHex(tx.toHex());
|
||||
// console.log(decodedTx.outs);
|
||||
|
||||
assert.strictEqual(decodedTx.outs[0].value, 546); // first output - destination
|
||||
assert.strictEqual(decodedTx.outs[1].value, 0); // second output - op_return
|
||||
assert.ok(decodedTx.outs[2].value > 0); // third output - change
|
||||
|
||||
assert.strictEqual(decodedTx.outs[1].script.toString('hex'), '00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff'); // custom script that we are passing
|
||||
|
||||
// console.log(outputs);
|
||||
|
||||
const actualFeerate = psbt.getFee() / tx.virtualSize();
|
||||
assert.strictEqual(
|
||||
Math.round(actualFeerate) >= 150 && actualFeerate < 151,
|
||||
true,
|
||||
`bad feerate, got ${actualFeerate}, expected at least 11; fee: ${psbt.getFee()}; virsualSize: ${tx.virtualSize()} vbytes; ${tx.toHex()}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
4
typings/coinselect.d.ts
vendored
4
typings/coinselect.d.ts
vendored
@ -7,6 +7,10 @@ declare module 'coinselect' {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* not an accurate definition since coinselect lib can ignore certain fields, and just passes through unknown fields,
|
||||
* which we actually rely on
|
||||
*/
|
||||
export type CoinSelectUtxo = {
|
||||
vout: number;
|
||||
value: number;
|
||||
|
Loading…
Reference in New Issue
Block a user