diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 47474d701..085be1a35 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -1083,6 +1083,7 @@ class TXDB { const details = new Details(wtx, block); const state = new BalanceDelta(); const view = new CoinView(); + let own = false; wtx.setBlock(block); @@ -1126,6 +1127,7 @@ class TXDB { const path = await this.getPath(coin); assert(path); + own = true; details.setInput(i, path, coin); @@ -1157,8 +1159,22 @@ class TXDB { details.setOutput(i, path); - const credit = await this.getCredit(hash, i); - assert(credit); + let credit = await this.getCredit(hash, i); + + if (!credit) { + // This credit didn't belong to us the first time we + // saw the transaction (before confirmation or rescan). + // Create new credit for database. + credit = Credit.fromTX(tx, i, height); + + // If this tx spent any of our own coins, we "own" this output, + // meaning if it becomes unconfirmed, we can still confidently spend it. + credit.own = own; + + // Add coin to "unconfirmed" balance (which includes confirmed coins) + state.coin(path, 1); + state.unconfirmed(path, credit.coin.value); + } // Credits spent in the mempool add an // undo coin for ease. If this credit is diff --git a/test/wallet-test.js b/test/wallet-test.js index 20bbc67c6..165454590 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -18,8 +18,8 @@ const KeyRing = require('../lib/primitives/keyring'); const Input = require('../lib/primitives/input'); const Outpoint = require('../lib/primitives/outpoint'); const Script = require('../lib/script/script'); -const PrivateKey = require('../lib/hd/private.js'); const policy = require('../lib/protocol/policy'); +const HDPrivateKey = require('../lib/hd/private'); const KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' + 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ'; @@ -1220,7 +1220,7 @@ describe('Wallet', function() { ); } - const privateKey = PrivateKey.generate(); + const privateKey = HDPrivateKey.generate(); const xpub = privateKey.xpubkey('main'); watchWallet = await wdb.create({ watchOnly: true, @@ -1572,6 +1572,165 @@ describe('Wallet', function() { }); }); + it('should create credit if not found during confirmation', async () => { + // Create wallet and get one address + const wallet = await wdb.create(); + const addr1 = await wallet.receiveAddress(); + + // Outside the wallet, generate a second private key and address. + const key2 = HDPrivateKey.generate(); + const ring2 = KeyRing.fromPrivate(key2.privateKey); + const addr2 = ring2.getAddress(); + + // Build TX to both addresses, known and unknown + const mtx = new MTX(); + mtx.addOutpoint(new Outpoint(Buffer.alloc(32), 0)); + mtx.addOutput(addr1, 1020304); + mtx.addOutput(addr2, 4030201); + const tx = mtx.toTX(); + const hash = tx.hash(); + + // Add unconfirmed TX to txdb (no block provided) + await wallet.txdb.add(tx, null); + + // Check + const bal1 = await wallet.getBalance(); + assert.strictEqual(bal1.tx, 1); + assert.strictEqual(bal1.coin, 1); + assert.strictEqual(bal1.confirmed, 0); + assert.strictEqual(bal1.unconfirmed, 1020304); + + // Import private key into wallet + assert(!await wallet.hasAddress(addr2)); + await wallet.importKey('default', ring2); + assert(await wallet.hasAddress(addr2)); + + // Confirm TX with newly-added output address + // Create dummy block + const block = { + height: 100, + hash: Buffer.alloc(32), + time: Date.now() + }; + + // Get TX from txdb + const wtx = await wallet.txdb.getTX(hash); + + // Confirm TX with dummy block in txdb + const details = await wallet.txdb.confirm(wtx, block); + assert.bufferEqual(details.tx.hash(), hash); + + // Check balance + const bal2 = await wallet.getBalance(); + assert.strictEqual(bal2.confirmed, bal2.unconfirmed); + assert.strictEqual(bal2.confirmed, 5050505); + assert.strictEqual(bal2.coin, 2); + assert.strictEqual(bal2.tx, 1); + + // Check for unconfirmed transactions + const pending = await wallet.getPending(); + assert.strictEqual(pending.length, 0); + + // Check history for TX + const history = await wallet.getHistory(); + const wtxs = await wallet.toDetails(history); + assert.strictEqual(wtxs.length, 1); + assert.bufferEqual(wtxs[0].hash, hash); + + // Both old and new credits are not "owned" + // (created by the wallet spending its own coins) + for (let i = 0; i < tx.outputs.length; i++) { + const credit = await wallet.txdb.getCredit(tx.hash(), i); + assert(!credit.own); + } + }); + + it('should create owned credit if not found during confirm', async () => { + // Create wallet and get one address + const wallet = await wdb.create(); + const addr1 = await wallet.receiveAddress(); + + // Outside the wallet, generate a second private key and address. + const key2 = HDPrivateKey.generate(); + const ring2 = KeyRing.fromPrivate(key2.privateKey); + const addr2 = ring2.getAddress(); + + // Create a confirmed, unspent, wallet-owned credit in txdb + const mtx1 = new MTX(); + mtx1.addOutpoint(new Outpoint(Buffer.alloc(32), 0)); + mtx1.addOutput(addr1, 1 * 1e8); + const tx1 = mtx1.toTX(); + await wallet.txdb.add(tx1, null); + + // Create dummy block + const block1 = { + height: 99, + hash: Buffer.alloc(32), + time: Date.now() + }; + // Get TX from txdb + const wtx1 = await wallet.txdb.getTX(tx1.hash()); + + // Confirm TX with dummy block in txdb + await wallet.txdb.confirm(wtx1, block1); + + // Build TX to both addresses, known and unknown + const mtx2 = new MTX(); + mtx2.addTX(tx1, 0, 99); + mtx2.addOutput(addr1, 1020304); + mtx2.addOutput(addr2, 4030201); + const tx2 = mtx2.toTX(); + const hash = tx2.hash(); + + // Add unconfirmed TX to txdb (no block provided) + await wallet.txdb.add(tx2, null); + + // Check + const bal1 = await wallet.getBalance(); + assert.strictEqual(bal1.tx, 2); + assert.strictEqual(bal1.coin, 1); + assert.strictEqual(bal1.confirmed, 1 * 1e8); + assert.strictEqual(bal1.unconfirmed, 1020304); + + // Import private key into wallet + assert(!await wallet.hasAddress(addr2)); + await wallet.importKey('default', ring2); + assert(await wallet.hasAddress(addr2)); + + // Confirm TX with newly-added output address + // Create dummy block + const block2 = { + height: 100, + hash: Buffer.alloc(32), + time: Date.now() + }; + + // Get TX from txdb + const wtx2 = await wallet.txdb.getTX(hash); + + // Confirm TX with dummy block in txdb + const details = await wallet.txdb.confirm(wtx2, block2); + assert.bufferEqual(details.tx.hash(), hash); + + // Check balance + const bal2 = await wallet.getBalance(); + assert.strictEqual(bal2.confirmed, bal2.unconfirmed); + assert.strictEqual(bal2.confirmed, 5050505); + assert.strictEqual(bal2.coin, 2); + assert.strictEqual(bal2.tx, 2); + + // Check for unconfirmed transactions + const pending = await wallet.getPending(); + assert.strictEqual(pending.length, 0); + + // Both old and new credits are "owned" + // (created by the wallet spending its own coins) + for (let i = 0; i < tx2.outputs.length; i++) { + const credit = await wallet.txdb.getCredit(tx2.hash(), i); + assert(credit.own); + } + }); + it('should cleanup', async () => { network.coinbaseMaturity = 2; await wdb.close();