Browse Source

ADD: low level code to support CPFP for BIP84 wallets

fixqramount
Overtorment 5 years ago
parent
commit
65a61db099
  1. 73
      class/hd-segwit-bech32-transaction.js
  2. 33
      tests/integration/hd-segwit-bech32-transaction.test.js

73
class/hd-segwit-bech32-transaction.js

@ -133,8 +133,14 @@ export class HDSegwitBech32Transaction {
/**
* Returns all the info about current transaction which is needed to do a replacement TX
* * fee - current tx fee
* * utxos - UTXOs current tx consumes
* * changeAmount - amount of satoshis that sent to change address (or addresses) we control
* * feeRate - sat/byte for current tx
* * targets - destination(s) of funds (outputs we do not control)
* * unconfirmedUtxos - UTXOs created by this transaction (only the ones we control)
*
* @returns {Promise<{fee: number, utxos: Array, changeAmount: number, feeRate: number, targets: Array}>}
* @returns {Promise<{fee: number, utxos: Array, unconfirmedUtxos: Array, changeAmount: number, feeRate: number, targets: Array}>}
*/
async getInfo() {
if (!this._wallet) throw new Error('Wallet required for this method');
@ -189,7 +195,22 @@ export class HDSegwitBech32Transaction {
}
}
return { fee, feeRate, targets, changeAmount, utxos };
// lets find outputs we own that current transaction creates. can be used in CPFP
let unconfirmedUtxos = [];
for (let outp of this._remoteTx.vout) {
let address = outp.scriptPubKey.addresses[0];
let value = new BigNumber(outp.value).multipliedBy(100000000).toNumber();
if (this._wallet.weOwnAddress(address)) {
unconfirmedUtxos.push({
vout: outp.n,
value: value,
txId: this._txid || this._txDecoded.getId(),
address: address,
});
}
}
return { fee, feeRate, targets, changeAmount, utxos, unconfirmedUtxos };
}
/**
@ -212,7 +233,8 @@ export class HDSegwitBech32Transaction {
/**
* Creates an RBF transaction that can replace previous one and basically cancel it (rewrite
* output to the one our wallet controls)
* output to the one our wallet controls). Note, this cannot add more utxo in RBF transaction if
* newFeerate is too high
*
* @param newFeerate {number} Sat/byte. Should be greater than previous tx feerate
* @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>}
@ -236,7 +258,8 @@ export class HDSegwitBech32Transaction {
}
/**
* Creates an RBF transaction that can bumps fee of previous one
* Creates an RBF transaction that can bumps fee of previous one. Note, this cannot add more utxo in RBF
* transaction if newFeerate is too high
*
* @param newFeerate {number} Sat/byte
* @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>}
@ -256,4 +279,46 @@ export class HDSegwitBech32Transaction {
return this._wallet.createTransaction(utxos, targets, newFeerate, myAddress, (await this.getMaxUsedSequence()) + 1);
}
/**
* Creates a CPFP transaction that can bumps fee of previous one (spends created but not confirmed outputs
* that belong to us). Note, this cannot add more utxo in CPFP transaction if newFeerate is too high
*
* @param newFeerate {number} sat/byte
* @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>}
*/
async createCPFPbumpFee(newFeerate) {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._remoteTx) await this._fetchRemoteTx();
let { feeRate, fee: oldFee, unconfirmedUtxos } = await this.getInfo();
if (newFeerate <= feeRate) throw new Error('New feerate should be bigger than the old one');
let myAddress = await this._wallet.getChangeAddressAsync();
// calculating feerate for CPFP tx so that average between current and CPFP tx will equal newFeerate.
// this works well if both txs are +/- equal size in bytes
const targetFeeRate = 2 * newFeerate - feeRate;
let add = 0;
while (add <= 128) {
var { tx, inputs, outputs, fee } = this._wallet.createTransaction(
unconfirmedUtxos,
[{ address: myAddress }],
targetFeeRate + add,
myAddress,
0,
);
let combinedFeeRate = (oldFee + fee) / (this._txhex.length / 2 + tx.toHex().length / 2); // avg
if (Math.round(combinedFeeRate) < newFeerate) {
add *= 2;
if (!add) add = 2;
} else {
// reached target feerate
break;
}
}
return { tx, inputs, outputs, fee };
}
}

33
tests/integration/hd-segwit-bech32-transaction.test.js

@ -160,4 +160,37 @@ describe('HDSegwitBech32Transaction', () => {
let tt2 = new HDSegwitBech32Transaction(tx.toHex(), null, hd);
assert.strictEqual(await tt2.canCancelTx(), true); // new tx is still cancellable since we only bumped fees
});
it('can do CPFP - bump fees', async function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30 * 1000;
if (!process.env.HD_MNEMONIC_BIP84) {
console.error('process.env.HD_MNEMONIC_BIP84 not set, skipped');
return;
}
let hd = new HDSegwitBech32Wallet();
hd.setSecret(process.env.HD_MNEMONIC_BIP84);
await hd.fetchBalance();
await hd.fetchTransactions();
let tt = new HDSegwitBech32Transaction(null, '2ec8a1d0686dcccffc102ba5453a28d99c8a1e5061c27b41f5c0a23b0b27e75f', hd);
assert.ok(await tt.isToUsTransaction());
let { unconfirmedUtxos, fee: oldFee } = await tt.getInfo();
assert.strictEqual(
JSON.stringify(unconfirmedUtxos),
JSON.stringify([
{
vout: 0,
value: 200000,
txId: '2ec8a1d0686dcccffc102ba5453a28d99c8a1e5061c27b41f5c0a23b0b27e75f',
address: 'bc1qvlmgrq0gtatanmas0tswrsknllvupq2g844ss2',
},
]),
);
let { tx, fee } = await tt.createCPFPbumpFee(20);
let avgFeeRate = (oldFee + fee) / (tt._txhex.length / 2 + tx.toHex().length / 2);
assert.ok(Math.round(avgFeeRate) >= 20);
});
});

Loading…
Cancel
Save