diff --git a/class/hd-segwit-bech32-transaction.js b/class/hd-segwit-bech32-transaction.js index 54fa405c1..05afbf9e5 100644 --- a/class/hd-segwit-bech32-transaction.js +++ b/class/hd-segwit-bech32-transaction.js @@ -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} + * @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} */ - 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} */ - 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} * @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} */ @@ -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} + */ + 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} */ - 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); } } diff --git a/tests/integration/hd-segwit-bech32-transaction.test.js b/tests/integration/hd-segwit-bech32-transaction.test.js index f63107e33..be37a4031 100644 --- a/tests/integration/hd-segwit-bech32-transaction.test.js +++ b/tests/integration/hd-segwit-bech32-transaction.test.js @@ -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 }); });