REF: bip84 transaction

This commit is contained in:
Overtorment 2019-06-30 12:00:34 +01:00
parent 30c20b9e6c
commit f7bbf529a2
2 changed files with 96 additions and 66 deletions

View file

@ -10,11 +10,14 @@ const BigNumber = require('bignumber.js');
*/
export class HDSegwitBech32Transaction {
/**
* @param txhex string Object initialized with txhex
* @param wallet {HDSegwitBech32Wallet} If set - a wallet object to which transacton belongs
* @param txhex {string|null} Object is initialized with txhex
* @param txid {string|null} If txhex not present - txid whould be present
* @param wallet {HDSegwitBech32Wallet|null} If set - a wallet object to which transacton belongs
*/
constructor(txhex, wallet) {
constructor(txhex, txid, wallet) {
if (!txhex && !txid) throw new Error('Bad arguments');
this._txhex = txhex;
this._txid = txid;
if (wallet) {
if (wallet.type === HDSegwitBech32Wallet.type) {
@ -25,17 +28,32 @@ export class HDSegwitBech32Transaction {
}
}
this._txDecoded = bitcoin.Transaction.fromHex(this._txhex);
if (this._txhex) this._txDecoded = bitcoin.Transaction.fromHex(this._txhex);
this._remoteTx = null;
}
/**
* If only txid present - we fetch hex
*
* @returns {Promise<void>}
* @private
*/
async _fetchTxhexAndDecode() {
let hexes = await BlueElectrum.multiGetTransactionByTxid([this._txid], 10, false);
this._txhex = hexes[this._txid];
if (!this._txhex) throw new Error("Transaction can't be found in mempool");
this._txDecoded = bitcoin.Transaction.fromHex(this._txhex);
}
/**
* Returns max used sequence for this transaction. Next RBF transaction
* should have this sequence + 1
*
* @returns {number}
* @returns {Promise<number>}
*/
getMaxUsedSequence() {
async getMaxUsedSequence() {
if (!this._txDecoded) await this._fetchTxhexAndDecode();
let max = 0;
for (let inp of this._txDecoded.ins) {
max = Math.max(inp.sequence, max);
@ -47,21 +65,22 @@ export class HDSegwitBech32Transaction {
/**
* Basic check that Sequence num for this TX is replaceable
*
* @returns {boolean}
* @returns {Promise<boolean>}
*/
isSequenceReplaceable() {
return this.getMaxUsedSequence() < bitcoin.Transaction.DEFAULT_SEQUENCE;
async isSequenceReplaceable() {
return (await this.getMaxUsedSequence()) < bitcoin.Transaction.DEFAULT_SEQUENCE;
}
/**
* If internal extended tx data not set - this is a method
* to fetch and set this data from electrum
* to fetch and set this data from electrum. Its different data from
* decoded hex - it contains confirmations etc.
*
* @returns {Promise<void>}
* @private
*/
async _fetchRemoteTx() {
let result = await BlueElectrum.multiGetTransactionByTxid([this._txDecoded.getId()]);
let result = await BlueElectrum.multiGetTransactionByTxid([this._txid || this._txDecoded.getId()]);
this._remoteTx = Object.values(result)[0];
}
@ -72,13 +91,13 @@ export class HDSegwitBech32Transaction {
*/
async getRemoteConfirmationsNum() {
if (!this._remoteTx) await this._fetchRemoteTx();
return this._remoteTx.confirmations;
return this._remoteTx.confirmations || 0; // stupid undefined
}
/**
* Checks that tx belongs to a wallet and also
* tx value is < 0, which means its a spending transaction
* definately initiated by us.
* definately initiated by us, can be RBF'ed.
*
* @returns {Promise<boolean>}
*/
@ -86,7 +105,7 @@ export class HDSegwitBech32Transaction {
if (!this._wallet) throw new Error('Wallet required for this method');
let found = false;
for (let tx of this._wallet.getTransactions()) {
if (tx.txid === this._txDecoded.getId()) {
if (tx.txid === (this._txid || this._txDecoded.getId())) {
// its our transaction, and its spending transaction, which means we initiated it
if (tx.value < 0) found = true;
}
@ -94,9 +113,33 @@ export class HDSegwitBech32Transaction {
return found;
}
/**
* Checks that tx belongs to a wallet and also
* tx value is > 0, which means its a receiving transaction and thus
* can be CPFP'ed.
*
* @returns {Promise<boolean>}
*/
async isToUsTransaction() {
if (!this._wallet) throw new Error('Wallet required for this method');
let found = false;
for (let tx of this._wallet.getTransactions()) {
if (tx.txid === (this._txid || this._txDecoded.getId())) {
if (tx.value > 0) found = true;
}
}
return found;
}
/**
* Returns all the info about current transaction which is needed to do a replacement TX
*
* @returns {Promise<{fee: number, utxos: Array, changeAmount: number, feeRate: number, targets: Array}>}
*/
async getInfo() {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._remoteTx) await this._fetchRemoteTx();
if (!this._txDecoded) await this._fetchTxhexAndDecode();
let prevInputs = [];
for (let inp of this._txDecoded.ins) {
@ -147,20 +190,17 @@ export class HDSegwitBech32Transaction {
}
return { fee, feeRate, targets, changeAmount, utxos };
// this means...
// let maxPossibleFee = fee + changeAmount;
// let maxPossibleFeeRate = Math.floor(maxPossibleFee / (this._txhex.length / 2));
// console.warn({maxPossibleFeeRate});
}
/**
* Checks if tx has single output and that output belongs to us - that
* means we already canceled this tx and we can only bump fees. Or plain all outputs belong to us.
* @returns {boolean}
* Checks if all outputs belong to us, that
* means we already canceled this tx and we can only bump fees
*
* @returns {Promise<boolean>}
*/
canCancelTx() {
async canCancelTx() {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._txDecoded) await this._fetchTxhexAndDecode();
// if theres at least one output we dont own - we can cancel this transaction!
for (let outp of this._txDecoded.outs) {
@ -171,8 +211,11 @@ export class HDSegwitBech32Transaction {
}
/**
* @param newFeerate
* @returns {Promise<{outputs: Array, tx: HDSegwitBech32Transaction, inputs: Array, fee: Number}>}
* Creates an RBF transaction that can replace previous one and basically cancel it (rewrite
* output to the one our wallet controls)
*
* @param newFeerate {number} Sat/byte. Should be greater than previous tx feerate
* @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>}
*/
async createRBFcancelTx(newFeerate) {
if (!this._wallet) throw new Error('Wallet required for this method');
@ -188,23 +231,29 @@ export class HDSegwitBech32Transaction {
[{ address: myAddress }],
newFeerate,
/* meaningless in this context */ myAddress,
this.getMaxUsedSequence() + 1,
(await this.getMaxUsedSequence()) + 1,
);
}
/**
* @param newFeerate
* @returns {Promise<{outputs: Array, tx: HDSegwitBech32Transaction, inputs: Array, fee: Number}>}
* Creates an RBF transaction that can bumps fee of previous one
*
* @param newFeerate {number} Sat/byte
* @returns {Promise<{outputs: Array, tx: Transaction, inputs: Array, fee: Number}>}
*/
async createRBFbumpFee(newFeerate) {
if (!this._wallet) throw new Error('Wallet required for this method');
if (!this._remoteTx) await this._fetchRemoteTx();
let { feeRate, targets, utxos } = await this.getInfo();
let { feeRate, targets, changeAmount, utxos } = await this.getInfo();
if (newFeerate <= feeRate) throw new Error('New feerate should be bigger than the old one');
let myAddress = await this._wallet.getChangeAddressAsync();
return this._wallet.createTransaction(utxos, targets, newFeerate, myAddress, this.getMaxUsedSequence() + 1);
if (changeAmount === 0) delete targets[0].value;
// looks like this was sendMAX transaction (because there was no change), so we cant reuse amount in this
// target since fee wont change. removing the amount so `createTransaction` will sendMAX correctly with new feeRate
return this._wallet.createTransaction(utxos, targets, newFeerate, myAddress, (await this.getMaxUsedSequence()) + 1);
}
}

View file

@ -21,17 +21,16 @@ beforeAll(async () => {
describe('HDSegwitBech32Transaction', () => {
it('can decode & check sequence', async function() {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30 * 1000;
let T = new HDSegwitBech32Transaction(
'020000000001035b6bd5f35a4ae9fb97da83929d042ee1609aa37e763e1428f31578d2308b5afb0000000000ffffffff66595fc361ad1d5954f7c06e38482d0abf1b981feee7353d854cd25c7f6516e80000000000ffffffffc33a839294313ae2674ce4c574364ff3837d31c8e9c40b030409bff318ddcda10100000000ffffffff02888a01000000000017a914e286d58e53f9247a4710e51232cce0686f16873c87d2880000000000001600140c75ccd91c55c4f54788931d48e6f23909d3a21f024730440220078c59543244d8b642f9ef7a7ea2747ddf34c2937a89fe9641038832f5baebf00220146b116e9a915a6fd9ab855c63b7549d44b8abf1607639aaf67321d045596046012103e73f0f28b3007440783b188facc81e835df195687a6dc5e9fbd775022d27eebc02483045022100e2f3a3064517226dbde0d87733b0179e0bb0cd94968f27961ba66847f4553d3602200ab8a858b7967cdb656816861a8cab46cf86db69be8a285292157b1fe1546872012103d8a20ce5c997b78ebeb0611f12bcf6c6bd797e485ff99253fee0e9794ac73dfd0247304402207dd7833521682e01c399195d37965b84ca6085f5ff62a34e09446e3244760d2702207672b60ec9d307ed502dc023b30d00415c061fd68a7f503738c57ab81419c788012102b8faed0d077eb0523af21e14fe51bd6dcc5a14ba6e800071bac2a3583a9d3a6a00000000',
);
assert.strictEqual(T.getMaxUsedSequence(), 0xffffffff);
assert.strictEqual(T.isSequenceReplaceable(), false);
let T = new HDSegwitBech32Transaction(null, 'e9ef58baf4cff3ad55913a360c2fa1fd124309c59dcd720cdb172ce46582097b');
assert.strictEqual(await T.getMaxUsedSequence(), 0xffffffff);
assert.strictEqual(await T.isSequenceReplaceable(), false);
// 881c54edd95cbdd1583d6b9148eb35128a47b64a2e67a5368a649d6be960f08e
T = new HDSegwitBech32Transaction(
'02000000000102f1155666b534f7cb476a0523a45dc8731d38d56b5b08e877c968812423fbd7f3010000000000000000d8a2882a692ee759b43e6af48ac152dd3410cc4b7d25031e83b3396c16ffbc8900000000000000000002400d03000000000017a914e286d58e53f9247a4710e51232cce0686f16873c870695010000000000160014d3e2ecbf4d91321794e0297e0284c47527cf878b02483045022100d18dc865fb4d087004d021d480b983b8afb177a1934ce4cd11cf97b03e17944f02206d7310687a84aab5d4696d535bca69c2db4449b48feb55fff028aa004f2d1744012103af4b208608c75f38e78f6e5abfbcad9c360fb60d3e035193b2cd0cdc8fc0155c0247304402207556e859845df41d897fe442f59b6106c8fa39c74ba5b7b8e3268ab0aebf186f0220048a9f3742339c44a1e5c78b491822b96070bcfda3f64db9dc6434f8e8068475012102456e5223ed3884dc6b0e152067fd836e3eb1485422eda45558bf83f59c6ad09f00000000',
);
assert.strictEqual(T.getMaxUsedSequence(), 0);
assert.strictEqual(T.isSequenceReplaceable(), true);
assert.strictEqual(await T.getMaxUsedSequence(), 0);
assert.strictEqual(await T.isSequenceReplaceable(), true);
assert.ok((await T.getRemoteConfirmationsNum()) >= 292);
});
@ -48,17 +47,11 @@ describe('HDSegwitBech32Transaction', () => {
assert.ok(hd.validateMnemonic());
await hd.fetchTransactions();
let tt = new HDSegwitBech32Transaction(
'02000000000102f1155666b534f7cb476a0523a45dc8731d38d56b5b08e877c968812423fbd7f3010000000000000000d8a2882a692ee759b43e6af48ac152dd3410cc4b7d25031e83b3396c16ffbc8900000000000000000002400d03000000000017a914e286d58e53f9247a4710e51232cce0686f16873c870695010000000000160014d3e2ecbf4d91321794e0297e0284c47527cf878b02483045022100d18dc865fb4d087004d021d480b983b8afb177a1934ce4cd11cf97b03e17944f02206d7310687a84aab5d4696d535bca69c2db4449b48feb55fff028aa004f2d1744012103af4b208608c75f38e78f6e5abfbcad9c360fb60d3e035193b2cd0cdc8fc0155c0247304402207556e859845df41d897fe442f59b6106c8fa39c74ba5b7b8e3268ab0aebf186f0220048a9f3742339c44a1e5c78b491822b96070bcfda3f64db9dc6434f8e8068475012102456e5223ed3884dc6b0e152067fd836e3eb1485422eda45558bf83f59c6ad09f00000000',
hd,
);
let tt = new HDSegwitBech32Transaction(null, '881c54edd95cbdd1583d6b9148eb35128a47b64a2e67a5368a649d6be960f08e', hd);
assert.ok(await tt.isOurTransaction());
tt = new HDSegwitBech32Transaction(
'01000000000101e141f756746932f869c7323d941f26e6a1a6817143b97250a51f8c08510547a901000000171600148ba6d02e74c0a6e000e8b174eb2ed44e5ea211a60000000002400d03000000000016001465eb5c39aa6785f69f292fdb41c282ea7799721ff85c09000000000017a914e286d58e53f9247a4710e51232cce0686f16873c8702473044022009a072e3a920708a63bac6452f5ff74a0e918057bb79f9f0fce72494c7edd5c9022000e430179e9051fe37b6ea8ba538b4af94e120dd70fc30aed4e54d5054bc9f9d0121039a421d5eb7c9de6590ae2a471cb556b60de8c6b056beb907dbdc1f5e6092f58800000000',
hd,
);
tt = new HDSegwitBech32Transaction(null, '89bcff166c39b3831e03257d4bcc1034dd52c18af46a3eb459e72e692a88a2d8', hd);
assert.ok(!(await tt.isOurTransaction()));
});
@ -75,11 +68,7 @@ describe('HDSegwitBech32Transaction', () => {
await hd.fetchBalance();
await hd.fetchTransactions();
// 881c54edd95cbdd1583d6b9148eb35128a47b64a2e67a5368a649d6be960f08e
let tt = new HDSegwitBech32Transaction(
'02000000000102f1155666b534f7cb476a0523a45dc8731d38d56b5b08e877c968812423fbd7f3010000000000000000d8a2882a692ee759b43e6af48ac152dd3410cc4b7d25031e83b3396c16ffbc8900000000000000000002400d03000000000017a914e286d58e53f9247a4710e51232cce0686f16873c870695010000000000160014d3e2ecbf4d91321794e0297e0284c47527cf878b02483045022100d18dc865fb4d087004d021d480b983b8afb177a1934ce4cd11cf97b03e17944f02206d7310687a84aab5d4696d535bca69c2db4449b48feb55fff028aa004f2d1744012103af4b208608c75f38e78f6e5abfbcad9c360fb60d3e035193b2cd0cdc8fc0155c0247304402207556e859845df41d897fe442f59b6106c8fa39c74ba5b7b8e3268ab0aebf186f0220048a9f3742339c44a1e5c78b491822b96070bcfda3f64db9dc6434f8e8068475012102456e5223ed3884dc6b0e152067fd836e3eb1485422eda45558bf83f59c6ad09f00000000',
hd,
);
let tt = new HDSegwitBech32Transaction(null, '881c54edd95cbdd1583d6b9148eb35128a47b64a2e67a5368a649d6be960f08e', hd);
let { fee, feeRate, targets, changeAmount, utxos } = await tt.getInfo();
assert.strictEqual(fee, 4464);
@ -119,13 +108,9 @@ describe('HDSegwitBech32Transaction', () => {
await hd.fetchBalance();
await hd.fetchTransactions();
// 881c54edd95cbdd1583d6b9148eb35128a47b64a2e67a5368a649d6be960f08e
let tt = new HDSegwitBech32Transaction(
'02000000000102f1155666b534f7cb476a0523a45dc8731d38d56b5b08e877c968812423fbd7f3010000000000000000d8a2882a692ee759b43e6af48ac152dd3410cc4b7d25031e83b3396c16ffbc8900000000000000000002400d03000000000017a914e286d58e53f9247a4710e51232cce0686f16873c870695010000000000160014d3e2ecbf4d91321794e0297e0284c47527cf878b02483045022100d18dc865fb4d087004d021d480b983b8afb177a1934ce4cd11cf97b03e17944f02206d7310687a84aab5d4696d535bca69c2db4449b48feb55fff028aa004f2d1744012103af4b208608c75f38e78f6e5abfbcad9c360fb60d3e035193b2cd0cdc8fc0155c0247304402207556e859845df41d897fe442f59b6106c8fa39c74ba5b7b8e3268ab0aebf186f0220048a9f3742339c44a1e5c78b491822b96070bcfda3f64db9dc6434f8e8068475012102456e5223ed3884dc6b0e152067fd836e3eb1485422eda45558bf83f59c6ad09f00000000',
hd,
);
let tt = new HDSegwitBech32Transaction(null, '881c54edd95cbdd1583d6b9148eb35128a47b64a2e67a5368a649d6be960f08e', hd);
assert.strictEqual(tt.canCancelTx(), true);
assert.strictEqual(await tt.canCancelTx(), true);
let { tx } = await tt.createRBFcancelTx(15);
@ -138,8 +123,8 @@ describe('HDSegwitBech32Transaction', () => {
let actualFeerate = (108150 + 200000 - createdTx.outs[0].value) / (tx.toHex().length / 2);
assert.strictEqual(Math.round(actualFeerate), 15);
let tt2 = new HDSegwitBech32Transaction(tx.toHex(), hd);
assert.strictEqual(tt2.canCancelTx(), false); // newly created cancel tx is not cancellable anymore
let tt2 = new HDSegwitBech32Transaction(tx.toHex(), null, hd);
assert.strictEqual(await tt2.canCancelTx(), false); // newly created cancel tx is not cancellable anymore
});
it('can do RBF - bumpfees tx', async function() {
@ -154,13 +139,9 @@ describe('HDSegwitBech32Transaction', () => {
await hd.fetchBalance();
await hd.fetchTransactions();
// 881c54edd95cbdd1583d6b9148eb35128a47b64a2e67a5368a649d6be960f08e
let tt = new HDSegwitBech32Transaction(
'02000000000102f1155666b534f7cb476a0523a45dc8731d38d56b5b08e877c968812423fbd7f3010000000000000000d8a2882a692ee759b43e6af48ac152dd3410cc4b7d25031e83b3396c16ffbc8900000000000000000002400d03000000000017a914e286d58e53f9247a4710e51232cce0686f16873c870695010000000000160014d3e2ecbf4d91321794e0297e0284c47527cf878b02483045022100d18dc865fb4d087004d021d480b983b8afb177a1934ce4cd11cf97b03e17944f02206d7310687a84aab5d4696d535bca69c2db4449b48feb55fff028aa004f2d1744012103af4b208608c75f38e78f6e5abfbcad9c360fb60d3e035193b2cd0cdc8fc0155c0247304402207556e859845df41d897fe442f59b6106c8fa39c74ba5b7b8e3268ab0aebf186f0220048a9f3742339c44a1e5c78b491822b96070bcfda3f64db9dc6434f8e8068475012102456e5223ed3884dc6b0e152067fd836e3eb1485422eda45558bf83f59c6ad09f00000000',
hd,
);
let tt = new HDSegwitBech32Transaction(null, '881c54edd95cbdd1583d6b9148eb35128a47b64a2e67a5368a649d6be960f08e', hd);
assert.strictEqual(tt.canCancelTx(), true);
assert.strictEqual(await tt.canCancelTx(), true);
let { tx } = await tt.createRBFbumpFee(17);
@ -176,7 +157,7 @@ describe('HDSegwitBech32Transaction', () => {
let actualFeerate = (108150 + 200000 - (createdTx.outs[0].value + createdTx.outs[1].value)) / (tx.toHex().length / 2);
assert.strictEqual(Math.round(actualFeerate), 17);
let tt2 = new HDSegwitBech32Transaction(tx.toHex(), hd);
assert.strictEqual(tt2.canCancelTx(), true); // new tx is still cancellable since we only bumped fees
let tt2 = new HDSegwitBech32Transaction(tx.toHex(), null, hd);
assert.strictEqual(await tt2.canCancelTx(), true); // new tx is still cancellable since we only bumped fees
});
});