From c2f49cf00e02d8168140b4aa5c9c723653367d1d Mon Sep 17 00:00:00 2001 From: Nodari Chkuaselidze Date: Mon, 14 Jul 2025 16:29:15 +0400 Subject: [PATCH 1/2] wallet: accept array of objects instead of arrays in the createBatch. --- lib/wallet/rpc.js | 30 ++--- lib/wallet/wallet.js | 92 +++++++------- test/wallet-auction-test.js | 210 +++++++++++++++++++------------- test/wallet-balance-test.js | 92 +++++++------- test/wallet-importnonce-test.js | 4 +- 5 files changed, 237 insertions(+), 191 deletions(-) diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index a73830b2..e916cc5a 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -2762,7 +2762,7 @@ class RPC extends RPCBase { 'NONE action requires 2 arguments: address, value' ); const {addr, value} = this._validateSendToAddress(action); - actions.push([type, addr, value]); + actions.push({ type: type, args: [addr, value] }); break; } case 'OPEN': { @@ -2771,7 +2771,7 @@ class RPC extends RPCBase { 'OPEN action requires 1 argument: name' ); const {name} = this._validateOpen(action); - actions.push([type, name]); + actions.push({ type: type, args: [name] }); break; } case 'BID': { @@ -2780,7 +2780,7 @@ class RPC extends RPCBase { 'BID action requires 3 arguments: name, bid, value' ); const {name, bid, value} = this._validateBid(action); - actions.push([type, name, bid, value]); + actions.push({ type: type, args: [name, bid, value] }); break; } case 'REVEAL': { @@ -2790,9 +2790,9 @@ class RPC extends RPCBase { ); const {name} = this._validateReveal(action); if (name) - actions.push([type, name]); + actions.push({ type: type, args: [name] }); else - actions.push([type]); + actions.push({ type: type }); break; } case 'REDEEM': { @@ -2802,9 +2802,9 @@ class RPC extends RPCBase { ); const {name} = this._validateRedeem(action); if (name) - actions.push([type, name]); + actions.push({ type: type, args: [name] }); else - actions.push([type]); + actions.push({ type: type }); break; } case 'UPDATE': { @@ -2813,7 +2813,7 @@ class RPC extends RPCBase { 'UPDATE action requires 2 arguments: name, data' ); const {name, resource} = this._validateUpdate(action); - actions.push([type, name, resource]); + actions.push({ type: type, args: [name, resource] }); break; } case 'RENEW': { @@ -2823,9 +2823,9 @@ class RPC extends RPCBase { ); if (action.length === 1) { const {name} = this._validateRenewal(action); - actions.push([type, name]); + actions.push({ type: type, args: [name] }); } else { - actions.push([type]); + actions.push({ type: type }); } break; } @@ -2835,7 +2835,7 @@ class RPC extends RPCBase { 'TRANSFER action requires 2 arguments: name, address' ); const {name, address} = this._validateTransfer(action); - actions.push([type, name, address]); + actions.push({ type: type, args: [name, address] }); break; } case 'FINALIZE': { @@ -2845,9 +2845,9 @@ class RPC extends RPCBase { ); if (action.length === 1) { const {name} = this._validateFinalize(action); - actions.push([type, name]); + actions.push({ type: type, args: [name] }); } else { - actions.push([type]); + actions.push({ type: type }); } break; } @@ -2857,7 +2857,7 @@ class RPC extends RPCBase { 'CANCEL action requires 1 argument: name' ); const {name} = this._validateCancel(action); - actions.push([type, name]); + actions.push({ type: type, args: [name] }); break; } case 'REVOKE': { @@ -2866,7 +2866,7 @@ class RPC extends RPCBase { 'REVOKE action requires 1 argument: name' ); const {name} = this._validateRevoke(action); - actions.push([type, name]); + actions.push({ type: type, args: [name] }); break; } default: diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index f7674ec7..f1a521df 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -3861,9 +3861,15 @@ class Wallet extends EventEmitter { } } + /** + * @typedef {Object} BatchAction + * @property {String} type - Action type + * @property {Array} [args] - Arguments for the action + */ + /** * Make a batch transaction with multiple actions. - * @param {Array} actions + * @param {BatchAction[]} actions * @param {Object} options * @returns {Promise} */ @@ -3885,15 +3891,13 @@ class Wallet extends EventEmitter { // covenant like NONE, OPEN, or BID could shift the // output array out of sync with their corresponding inputs. actions.sort((a, b) => { - assert(Array.isArray(a)); - assert(Array.isArray(b)); - assert(a.length); - assert(b.length); + assert(a.type); + assert(b.type); - if (a[0] === b[0]) + if (a.type === b.type) return 0; - switch (b[0]) { + switch (b.type) { case 'REVEAL': case 'REDEEM': case 'UPDATE': @@ -3916,106 +3920,110 @@ class Wallet extends EventEmitter { // "actions" are arrays that start with a covenant type (or meta-type) // followed by the arguments expected by the corresponding "make" function. for (const action of actions) { - const type = action.shift(); - assert(typeof type === 'string'); + assert(action); + assert(typeof action.type === 'string'); - switch (type) { + const args = action.args || []; + + assert(Array.isArray(args), 'Action args must be an array.'); + + switch (action.type) { case 'NONE': { - assert(action.length === 2); + assert(args.length === 2); this.makeTX([{ - address: action[0], - value: action[1] + address: args[0], + value: args[1] }], mtx); break; } case 'OPEN': { - assert(action.length === 1, 'Bad arguments for OPEN.'); - const name = action[0]; + assert(args.length === 1, 'Bad arguments for OPEN.'); + const name = args[0]; await this.makeOpen(name, acct, mtx); break; } case 'BID': { - assert(action.length === 3, 'Bad arguments for BID.'); + assert(args.length === 3, 'Bad arguments for BID.'); const address = account.deriveReceive(receiveIndex++).getAddress(); - const name = action[0]; - const value = action[1]; - const lockup = action[2]; + const name = args[0]; + const value = args[1]; + const lockup = args[2]; await this.makeBid(name, value, lockup, acct, mtx, address); break; } case 'REVEAL': { - if (action.length === 1) { - const name = action[0]; + if (args.length === 1) { + const name = args[0]; await this.makeReveal(name, acct, mtx); break; } - assert(action.length === 0, 'Bad arguments for REVEAL.'); + assert(args.length === 0, 'Bad arguments for REVEAL.'); await this.makeRevealAll(mtx, witnessSize); break; } case 'REDEEM': { - if (action.length === 1) { - const name = action[0]; + if (args.length === 1) { + const name = args[0]; await this.makeRedeem(name, acct, mtx); break; } - assert(action.length === 0, 'Bad arguments for REDEEM.'); + assert(args.length === 0, 'Bad arguments for REDEEM.'); await this.makeRedeemAll(mtx, witnessSize); break; } case 'UPDATE': { - assert(action.length === 2, 'Bad arguments for UPDATE.'); - const name = action[0]; - const resource = action[1]; + assert(args.length === 2, 'Bad arguments for UPDATE.'); + const name = args[0]; + const resource = args[1]; await this.makeUpdate(name, resource, acct, mtx); break; } case 'RENEW': { - if (action.length === 1) { - const name = action[0]; + if (args.length === 1) { + const name = args[0]; await this.makeRenewal(name, acct, mtx); break; } - assert(action.length === 0, 'Bad arguments for RENEW.'); + assert(args.length === 0, 'Bad arguments for RENEW.'); await this.makeRenewalAll(mtx, witnessSize); break; } case 'TRANSFER': { - assert(action.length === 2, 'Bad arguments for TRANSFER.'); - const name = action[0]; - const address = action[1]; + assert(args.length === 2, 'Bad arguments for TRANSFER.'); + const name = args[0]; + const address = args[1]; await this.makeTransfer(name, address, acct, mtx); break; } case 'FINALIZE': { - if (action.length === 1) { - const name = action[0]; + if (args.length === 1) { + const name = args[0]; await this.makeFinalize(name, acct, mtx); break; } - assert(action.length === 0, 'Bad arguments for FINALIZE.'); + assert(args.length === 0, 'Bad arguments for FINALIZE.'); await this.makeFinalizeAll(mtx, witnessSize); break; } case 'CANCEL': { - assert(action.length === 1, 'Bad arguments for CANCEL.'); - const name = action[0]; + assert(args.length === 1, 'Bad arguments for CANCEL.'); + const name = args[0]; await this.makeCancel(name, acct, mtx); break; } case 'REVOKE': { - assert(action.length === 1, 'Bad arguments for REVOKE.'); - const name = action[0]; + assert(args.length === 1, 'Bad arguments for REVOKE.'); + const name = args[0]; await this.makeRevoke(name, acct, mtx); break; } default: - throw new Error(`Unknown action type: ${type}`); + throw new Error(`Unknown action type: ${action.type}`); } if (rules.countOpens(mtx) > consensus.MAX_BLOCK_OPENS) diff --git a/test/wallet-auction-test.js b/test/wallet-auction-test.js index e5281fe4..b7a343d2 100644 --- a/test/wallet-auction-test.js +++ b/test/wallet-auction-test.js @@ -698,9 +698,9 @@ describe('Wallet Auction', function() { it('should create multiple OPENs with options', async () => { const mtx = await wallet.createBatch( [ - ['OPEN', name1], - ['OPEN', name2], - ['OPEN', name3] + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } ], { hardFee @@ -730,9 +730,9 @@ describe('Wallet Auction', function() { await assert.rejects( wallet.sendBatch( [ - ['OPEN', 'google'], - ['OPEN', name2], - ['OPEN', name3] + { type: 'OPEN', args: ['google'] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } ] ), {message: 'Name is reserved: google.'} @@ -743,9 +743,9 @@ describe('Wallet Auction', function() { await assert.rejects( wallet.sendBatch( [ - ['OPEN', name1], - ['OPEN', name1], - ['OPEN', name3] + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name3] } ] ), {message: 'Duplicate name with exclusive action.'} @@ -756,9 +756,9 @@ describe('Wallet Auction', function() { await assert.rejects( wallet.sendBatch( [ - ['OPEN', name1], - ['OPEN', name1], - ['OPEN', name3] + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name3] } ] ), {message: 'Duplicate name with exclusive action.'} @@ -769,9 +769,9 @@ describe('Wallet Auction', function() { await assert.rejects( wallet.sendBatch( [ - ['BID', name1, 1, 1], - ['OPEN', name2], - ['OPEN', name3] + { type: 'BID', args: [name1, 1, 1] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } ] ), {message: `Name has not reached the bidding phase yet: ${name1}.`} @@ -782,9 +782,9 @@ describe('Wallet Auction', function() { await assert.rejects( wallet.sendBatch( [ - ['BID', name1, 21000000], - ['OPEN', name2], - ['OPEN', name3] + { type: 'BID', args: [name1, 21000000] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } ] ), {message: 'Bad arguments for BID.'} @@ -795,7 +795,7 @@ describe('Wallet Auction', function() { await assert.rejects( wallet.sendBatch( [ - ['REVEAL'] + { type: 'REVEAL' } ] ), {message: 'Nothing to do.'} @@ -806,7 +806,7 @@ describe('Wallet Auction', function() { await assert.rejects( wallet.sendBatch( [ - ['REDEEM'] + { type: 'REDEEM' } ] ), {message: 'Nothing to do.'} @@ -818,10 +818,10 @@ describe('Wallet Auction', function() { await assert.rejects( wallet.sendBatch( [ - ['OPEN', name1], - ['OPEN', name1], - ['OPEN', name3], - ['NONE', addr, 1] + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name3] }, + { type: 'NONE', args: [addr, 1] } ] ), {message: 'Output is dust.'} @@ -832,10 +832,10 @@ describe('Wallet Auction', function() { await assert.rejects( wallet.sendBatch( [ - ['OPEN', name1], - ['OPEN', name1], - ['OPEN', name3], - ['open', name4] + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name3] }, + { type: 'open', args: [name4] } ] ), {message: 'Unknown action type: open'} @@ -847,10 +847,10 @@ describe('Wallet Auction', function() { it('3 OPENs and 1 NONE', async () => { const tx = await wallet.sendBatch( [ - ['OPEN', name1], - ['OPEN', name2], - ['OPEN', name3], - ['NONE', addr, 10000] + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] }, + { type: 'NONE', args: [addr, 10000] } ] ); @@ -861,10 +861,10 @@ describe('Wallet Auction', function() { it('4 BIDs', async () => { const tx = await wallet.sendBatch( [ - ['BID', name1, 10000, 20000], - ['BID', name1, 10001, 20000], // self-snipe! - ['BID', name2, 30000, 40000], - ['BID', name3, 50000, 60000] + { type: 'BID', args: [name1, 10000, 20000] }, + { type: 'BID', args: [name1, 10001, 20000] }, // self-snipe! + { type: 'BID', args: [name2, 30000, 40000] }, + { type: 'BID', args: [name3, 50000, 60000] } ] ); @@ -876,7 +876,7 @@ describe('Wallet Auction', function() { // Don't send this one const revealAll = await wallet.createBatch( [ - ['REVEAL'] + { type: 'REVEAL' } ] ); @@ -892,8 +892,8 @@ describe('Wallet Auction', function() { it('2 REVEALs then 1 REVEAL', async () => { const tx = await wallet.sendBatch( [ - ['REVEAL', name1], - ['REVEAL', name2] + { type: 'REVEAL', args: [name1] }, + { type: 'REVEAL', args: [name2] } ] ); @@ -903,7 +903,7 @@ describe('Wallet Auction', function() { // because names are being revealed individually. await wallet.sendBatch( [ - ['REVEAL', name3] + { type: 'REVEAL', args: [name3] } ] ); await mineBlocks(revealPeriod); @@ -913,7 +913,7 @@ describe('Wallet Auction', function() { // Don't send this one const redeemAll = await wallet.createBatch( [ - ['REDEEM'] + { type: 'REDEEM' } ] ); @@ -930,11 +930,11 @@ describe('Wallet Auction', function() { // Complete all 4 bids win and/or lose in one TX const batch1 = await wallet.sendBatch( [ - ['OPEN', name4], - ['REDEEM', name1], - ['UPDATE', name1, res1], - ['UPDATE', name2, res2], - ['UPDATE', name3, res3] + { type: 'OPEN', args: [name4] }, + { type: 'REDEEM', args: [name1] }, + { type: 'UPDATE', args: [name1, res1] }, + { type: 'UPDATE', args: [name2, res2] }, + { type: 'UPDATE', args: [name3, res3] } ] ); @@ -955,10 +955,10 @@ describe('Wallet Auction', function() { }); const tx = await wallet.sendBatch( [ - ['TRANSFER', name1, nullAddr], - ['TRANSFER', name2, nullAddr], - ['TRANSFER', name3, nullAddr], - ['BID', name4, 70000, 80000] + { type: 'TRANSFER', args: [name1, nullAddr] }, + { type: 'TRANSFER', args: [name2, nullAddr] }, + { type: 'TRANSFER', args: [name3, nullAddr] }, + { type: 'BID', args: [name4, 70000, 80000] } ] ); @@ -974,10 +974,10 @@ describe('Wallet Auction', function() { it('1 FINALIZE, 1 CANCEL, 1 REVOKE and 1 REVEAL', async () => { const tx = await wallet.sendBatch( [ - ['FINALIZE', name1], - ['CANCEL', name2], - ['REVOKE', name3], - ['REVEAL', name4] + { type: 'FINALIZE', args: [name1] }, + { type: 'CANCEL', args: [name2] }, + { type: 'REVOKE', args: [name3] }, + { type: 'REVEAL', args: [name4] } ] ); @@ -992,8 +992,8 @@ describe('Wallet Auction', function() { it('1 revoked name re-OPEN and 1 REGISTER', async () => { const batch2 = await wallet.sendBatch( [ - ['OPEN', name3], // and the cycle begins again... - ['UPDATE', name4, res4] + { type: 'OPEN', args: [name3] }, // and the cycle begins again... + { type: 'UPDATE', args: [name4, res4] } ] ); @@ -1010,7 +1010,7 @@ describe('Wallet Auction', function() { const actions = []; for (let i = 0; i < 10; i++) { const addr = Address.fromProgram(0, Buffer.alloc(20, i + 1)); - actions.push(['NONE', addr, 10000]); + actions.push({ type: 'NONE', args: [addr, 10000] }); } const batch = await wallet.sendBatch(actions, {hardFee: 1000}); @@ -1058,8 +1058,8 @@ describe('Wallet Auction', function() { it('2 RENEW', async () => { await wallet.sendBatch( [ - ['RENEW', name2], - ['RENEW', name4] + { type: 'RENEW', args: [name2] }, + { type: 'RENEW', args: [name4] } ] ); @@ -1130,14 +1130,16 @@ describe('Wallet Auction', function() { it('should OPEN', async () => { name = rules.grindName(4, chain.height, network); - await wallet.sendBatch([['OPEN', name]]); + await wallet.sendBatch([ + { type: 'OPEN', args: [name] } + ]); await mineBlocks(treeInterval + 1); }); it('should not batch too many BIDs', async () => { const batch = []; for (let i = 201; i > 0; i--) - batch.push(['BID', name, i * 1000, i * 1000]); + batch.push({ type: 'BID', args: [name, i * 1000, i * 1000] }); await assert.rejects( wallet.sendBatch(batch), @@ -1148,19 +1150,19 @@ describe('Wallet Auction', function() { it('should batch BIDs', async () => { let batch = []; for (let i = 200; i > 0; i--) - batch.push(['BID', name, i * 1000, i * 1000]); + batch.push({ type: 'BID', args: [name, i * 1000, i * 1000] }); await wallet.sendBatch(batch); batch = []; for (let i = 200; i > 0; i--) - batch.push(['BID', name, i * 1001, i * 1001]); + batch.push({ type: 'BID', args: [name, i * 1001, i * 1001] }); await wallet.sendBatch(batch); batch = []; for (let i = 200; i > 0; i--) - batch.push(['BID', name, i * 1002, i * 1002]); + batch.push({ type: 'BID', args: [name, i * 1002, i * 1002] }); await wallet.sendBatch(batch); batch = []; for (let i = 150; i > 0; i--) - batch.push(['BID', name, i * 1003, i * 1003]); + batch.push({ type: 'BID', args: [name, i * 1003, i * 1003] }); await wallet.sendBatch(batch); await mineBlocks(biddingPeriod); @@ -1175,7 +1177,7 @@ describe('Wallet Auction', function() { it('should create batch just under weight limit', async () => { // Start with the batch we would normally make - const mtx = await wallet.createBatch([['REVEAL']]); + const mtx = await wallet.createBatch([{ type: 'REVEAL' }]); // Find a spendable coin const coins = await wallet.getCoins(); @@ -1213,12 +1215,12 @@ describe('Wallet Auction', function() { it('should REVEAL all in several batches', async () => { let reveals = 0; - const mtx1 = await wallet.createBatch([['REVEAL']]); + const mtx1 = await wallet.createBatch([{ type: 'REVEAL' }]); assert(mtx1.changeIndex >= 0); reveals += mtx1.outputs.length - 1; await wdb.addTX(mtx1.toTX()); - const mtx2 = await wallet.createBatch([['REVEAL']]); + const mtx2 = await wallet.createBatch([{ type: 'REVEAL' }]); assert(mtx2.changeIndex >= 0); reveals += mtx2.outputs.length - 1; await wdb.addTX(mtx2.toTX()); @@ -1239,12 +1241,12 @@ describe('Wallet Auction', function() { it('should REDEEM all in several batches', async () => { let reveals = 0; - const mtx1 = await wallet.createBatch([['REDEEM']]); + const mtx1 = await wallet.createBatch([{ type: 'REDEEM' }]); assert(mtx1.changeIndex >= 0); reveals += mtx1.outputs.length - 1; await wdb.addTX(mtx1.toTX()); - const mtx2 = await wallet.createBatch([['REDEEM']]); + const mtx2 = await wallet.createBatch([{ type: 'REDEEM' }]); assert(mtx2.changeIndex >= 0); reveals += mtx2.outputs.length - 1; await wdb.addTX(mtx2.toTX()); @@ -1292,7 +1294,7 @@ describe('Wallet Auction', function() { it('should not batch too many OPENs', async () => { const batch = []; for (let i = 0; i < consensus.MAX_BLOCK_OPENS + 1; i++) - batch.push(['OPEN', names[i]]); + batch.push({ type: 'OPEN', args: [names[i]] }); await assert.rejects( wallet.createBatch(batch), @@ -1305,7 +1307,7 @@ describe('Wallet Auction', function() { for (let i = 1; i <= 8; i++) { const batch = []; for (let j = 1; j <= 100; j++) { - batch.push(['OPEN', names[count++]]); + batch.push({ type: 'OPEN', args: [names[count++]] }); } await wallet.sendBatch(batch); await mineBlocks(1); @@ -1319,8 +1321,8 @@ describe('Wallet Auction', function() { const batch = []; for (let j = 1; j <= 100; j++) { batch.push( - ['BID', names[count], 10000, 10000], - ['BID', names[count++], 10000, 10000] + { type: 'BID', args: [names[count], 10000, 10000]}, + { type: 'BID', args: [names[count++], 10000, 10000]} ); } await wallet.sendBatch(batch); @@ -1334,7 +1336,12 @@ describe('Wallet Auction', function() { let reveals = 0; for (;;) { try { - const tx = await wallet.sendBatch([['REVEAL'], ['REDEEM'], ['RENEW'], ['FINALIZE']]); + const tx = await wallet.sendBatch([ + { type: 'REVEAL' }, + { type: 'REDEEM' }, + { type: 'RENEW' }, + { type: 'FINALIZE' } + ]); reveals += tx.outputs.length - 1; // Don't count change output } catch (e) { assert.strictEqual(e.message, 'Nothing to do.'); @@ -1351,7 +1358,13 @@ describe('Wallet Auction', function() { let redeems = 0; for (;;) { try { - const tx = await wallet.sendBatch([['REVEAL'], ['REDEEM'], ['RENEW'], ['FINALIZE']]); + const tx = await wallet.sendBatch([ + { type: 'REVEAL' }, + { type: 'REDEEM' }, + { type: 'RENEW' }, + { type: 'FINALIZE' } + ]); + redeems += tx.outputs.length - 1; // Don't count change output } catch (e) { assert.strictEqual(e.message, 'Nothing to do.'); @@ -1367,7 +1380,7 @@ describe('Wallet Auction', function() { for (let i = 1; i <= 8; i++) { const batch = []; for (let j = 1; j <= 100; j++) { - batch.push(['UPDATE', names[count++], new Resource()]); + batch.push({ type: 'UPDATE', args: [names[count++], new Resource()]}); } await wallet.sendBatch(batch); await mineBlocks(1); @@ -1390,7 +1403,7 @@ describe('Wallet Auction', function() { it('should not batch too many UPDATEs', async () => { const batch = []; for (let i = 0; i < consensus.MAX_BLOCK_UPDATES + 1; i++) - batch.push(['UPDATE', names[i], new Resource()]); + batch.push({ type: 'UPDATE', args: [names[i], new Resource()]}); await assert.rejects( wallet.createBatch(batch), @@ -1406,7 +1419,12 @@ describe('Wallet Auction', function() { ); await assert.rejects( - wallet.sendBatch([['REVEAL'], ['REDEEM'], ['RENEW'], ['FINALIZE']]), + wallet.sendBatch([ + { type: 'REVEAL' }, + { type: 'REDEEM' }, + { type: 'RENEW' }, + { type: 'FINALIZE' } + ]), {message: 'Nothing to do.'} ); }); @@ -1414,7 +1432,7 @@ describe('Wallet Auction', function() { it('should not batch too many RENEWs', async () => { const batch = []; for (let i = 0; i < consensus.MAX_BLOCK_RENEWALS + 1; i++) - batch.push(['RENEW', names[i]]); + batch.push({ type: 'RENEW', args: [names[i]]}); await assert.rejects( wallet.createBatch(batch), @@ -1427,7 +1445,12 @@ describe('Wallet Auction', function() { let renewals = 0; for (;;) { - const tx = await wallet.sendBatch([['REVEAL'], ['REDEEM'], ['RENEW'], ['FINALIZE']]); + const tx = await wallet.sendBatch([ + { type: 'REVEAL' }, + { type: 'REDEEM' }, + { type: 'RENEW' }, + { type: 'FINALIZE' } + ]); await mineBlocks(1); if (!renewals) { @@ -1446,7 +1469,7 @@ describe('Wallet Auction', function() { it('should not batch too many TRANSFERs', async () => { const batch = []; for (const name of names) - batch.push(['TRANSFER', name, new Address()]); + batch.push({ type: 'TRANSFER', args: [name, new Address()]}); await assert.rejects( wallet.createBatch(batch), @@ -1460,7 +1483,7 @@ describe('Wallet Auction', function() { for (let i = 1; i <= 8; i++) { const batch = []; for (let j = 1; j <= 100; j++) { - batch.push(['TRANSFER', names[count++], addr]); + batch.push({ type: 'TRANSFER', args: [names[count++], addr]}); } await wallet.sendBatch(batch); await mineBlocks(1); @@ -1471,7 +1494,12 @@ describe('Wallet Auction', function() { await mineBlocks(network.names.lockupPeriod - 9); await assert.rejects( - wallet.sendBatch([['REVEAL'], ['REDEEM'], ['RENEW'], ['FINALIZE']]), + wallet.sendBatch([ + { type: 'REVEAL' }, + { type: 'REDEEM' }, + { type: 'RENEW' }, + { type: 'FINALIZE' } + ]), {message: 'Nothing to do.'} ); }); @@ -1481,7 +1509,12 @@ describe('Wallet Auction', function() { let finalizes = 0; for (;;) { - const tx = await wallet.sendBatch([['REVEAL'], ['REDEEM'], ['RENEW'], ['FINALIZE']]); + const tx = await wallet.sendBatch([ + { type: 'REVEAL' }, + { type: 'REDEEM' }, + { type: 'RENEW' }, + { type: 'FINALIZE' } + ]); await mineBlocks(1); finalizes += tx.outputs.length - 1; // Don't count change output @@ -1493,7 +1526,12 @@ describe('Wallet Auction', function() { it('should have nothing to do', async () => { await assert.rejects( - wallet.sendBatch([['REVEAL'], ['REDEEM'], ['RENEW'], ['FINALIZE']]), + wallet.sendBatch([ + { type: 'REVEAL' }, + { type: 'REDEEM' }, + { type: 'RENEW' }, + { type: 'FINALIZE' } + ]), {message: 'Nothing to do.'} ); }); diff --git a/test/wallet-balance-test.js b/test/wallet-balance-test.js index 065f8c99..6a0ae42b 100644 --- a/test/wallet-balance-test.js +++ b/test/wallet-balance-test.js @@ -565,8 +565,8 @@ describe('Wallet Balance', function() { const {nextAddr} = getAheadAddr(account, ahead); await primary.sendBatch([ - ['OPEN', name1], - ['OPEN', name2] + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name2] } ]); await mineBlocks(openingPeriod); @@ -574,8 +574,8 @@ describe('Wallet Balance', function() { // all three bids are there. const bidMTX = await wallet.createBatch([ - ['BID', name1, BID_AMOUNT_1, BLIND_AMOUNT_1], - ['BID', name2, BID_AMOUNT_2, BLIND_AMOUNT_2] + { type: 'BID', args: [name1, BID_AMOUNT_1, BLIND_AMOUNT_1] }, + { type: 'BID', args: [name2, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); @@ -604,16 +604,16 @@ describe('Wallet Balance', function() { await primary.sendReveal(name2); await wallet.sendBatch([ - ['REVEAL', name1], - ['REVEAL', name2] + { type: 'REVEAL', args: [name1] }, + { type: 'REVEAL', args: [name2] } ], txOpts); await mineBlocks(revealPeriod); if (register !== false) { await wallet.sendBatch([ - ['UPDATE', name1, EMPTY_RS], - ['UPDATE', name2, EMPTY_RS] + { type: 'UPDATE', args: [name1, EMPTY_RS] }, + { type: 'UPDATE', args: [name2, EMPTY_RS] } ], { hardFee: HARD_FEE }); @@ -1019,8 +1019,8 @@ describe('Wallet Balance', function() { const txOpts = { hardFee: HARD_FEE }; const bidMTX = await wallet.createBatch([ - ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], - ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + { type: 'BID', args: [name, BID_AMOUNT_1, BLIND_AMOUNT_1] }, + { type: 'BID', args: [name, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); @@ -1112,8 +1112,8 @@ describe('Wallet Balance', function() { const txOpts = { hardFee: HARD_FEE }; const bidMTX = await primary.createBatch([ - ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], - ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + { type: 'BID', args: [name, BID_AMOUNT_1, BLIND_AMOUNT_1] }, + { type: 'BID', args: [name, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); @@ -1209,8 +1209,8 @@ describe('Wallet Balance', function() { const addr2 = getAheadAddr(altAccount, ahead); const bidMTX = await wallet.createBatch([ - ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], - ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + { type: 'BID', args: [name, BID_AMOUNT_1, BLIND_AMOUNT_1] }, + { type: 'BID', args: [name, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); @@ -1394,8 +1394,8 @@ describe('Wallet Balance', function() { await mineBlocks(openingPeriod); const bidMTX = await clone.createBatch([ - ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], - ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + { type: 'BID', args: [name, BID_AMOUNT_1, BLIND_AMOUNT_1] }, + { type: 'BID', args: [name, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); @@ -1511,8 +1511,8 @@ describe('Wallet Balance', function() { await mineBlocks(openingPeriod); await wallet.sendBatch([ - ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], - ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + { type: 'BID', args: [name, BID_AMOUNT_1, BLIND_AMOUNT_1] }, + { type: 'BID', args: [name, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); await mineBlocks(biddingPeriod); }; @@ -1738,8 +1738,8 @@ describe('Wallet Balance', function() { await primary.sendOpen(name, false); await mineBlocks(openingPeriod); await primary.sendBatch([ - ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], - ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + { type: 'BID', args: [name, BID_AMOUNT_1, BLIND_AMOUNT_1] }, + { type: 'BID', args: [name, BID_AMOUNT_2, BLIND_AMOUNT_2] } ]); await mineBlocks(biddingPeriod); }; @@ -1840,8 +1840,8 @@ describe('Wallet Balance', function() { const addr1 = getAheadAddr(cloneAccount, ahead); await primary.sendBatch([ - ['OPEN', name1], - ['OPEN', name2] + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name2] } ]); await mineBlocks(openingPeriod); @@ -1853,8 +1853,8 @@ describe('Wallet Balance', function() { // all three bids are there. const bidMTX = await clone.createBatch([ - ['BID', name1, BID_AMOUNT_1, BLIND_AMOUNT_1], - ['BID', name2, BID_AMOUNT_2, BLIND_AMOUNT_2] + { type: 'BID', args: [name1, BID_AMOUNT_1, BLIND_AMOUNT_1] }, + { type: 'BID', args: [name2, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); @@ -1873,8 +1873,8 @@ describe('Wallet Balance', function() { await primary.sendReveal(name2); await clone.sendBatch([ - ['REVEAL', name1], - ['REVEAL', name2] + { type: 'REVEAL', args: [name1] }, + { type: 'REVEAL', args: [name2] } ], txOpts); await mineBlocks(revealPeriod + 1); @@ -1882,8 +1882,8 @@ describe('Wallet Balance', function() { const sendRedeems = async (wallet, clone, ahead) => { await clone.sendBatch([ - ['REDEEM', name1], - ['REDEEM', name2] + { type: 'REDEEM', args: [name1] }, + { type: 'REDEEM', args: [name2] } ], { hardFee: HARD_FEE }); @@ -1984,8 +1984,8 @@ describe('Wallet Balance', function() { const sendRedeems = async (wallet, clone, ahead) => { await clone.sendBatch([ - ['UPDATE', name1, EMPTY_RS], - ['UPDATE', name2, EMPTY_RS] + { type: 'UPDATE', args: [name1, EMPTY_RS] }, + { type: 'UPDATE', args: [name2, EMPTY_RS] } ], { hardFee: HARD_FEE }); @@ -2124,8 +2124,8 @@ describe('Wallet Balance', function() { const sendUpdates = async (wallet, clone) => { await clone.sendBatch([ - ['UPDATE', name1, EMPTY_RS], - ['UPDATE', name2, EMPTY_RS] + { type: 'UPDATE', args: [name1, EMPTY_RS] }, + { type: 'UPDATE', args: [name2, EMPTY_RS] } ], { hardFee: HARD_FEE }); @@ -2165,8 +2165,8 @@ describe('Wallet Balance', function() { const sendRevokes = async (wallet, clone) => { await clone.sendBatch([ - ['REVOKE', name1], - ['REVOKE', name2] + { type: 'REVOKE', args: [name1] }, + { type: 'REVOKE', args: [name2] } ], { hardFee: HARD_FEE }); @@ -2205,8 +2205,8 @@ describe('Wallet Balance', function() { const sendRenews = async (wallet, clone) => { await mineBlocks(treeInterval); await clone.sendBatch([ - ['RENEW', name1], - ['RENEW', name2] + { type: 'RENEW', args: [name1] }, + { type: 'RENEW', args: [name2] } ], { hardFee: HARD_FEE }); @@ -2244,8 +2244,8 @@ describe('Wallet Balance', function() { const sendTransfers = async (wallet, clone) => { await clone.sendBatch([ - ['TRANSFER', name1, await primary.receiveAddress()], - ['TRANSFER', name2, await primary.receiveAddress()] + { type: 'TRANSFER', args: [name1, await primary.receiveAddress()] }, + { type: 'TRANSFER', args: [name2, await primary.receiveAddress()] } ], { hardFee: HARD_FEE }); @@ -2281,8 +2281,8 @@ describe('Wallet Balance', function() { name2 = names[1]; await clone.sendBatch([ - ['TRANSFER', name1, await primary.receiveAddress()], - ['TRANSFER', name2, await primary.receiveAddress()] + { type: 'TRANSFER', args: [name1, await primary.receiveAddress()] }, + { type: 'TRANSFER', args: [name2, await primary.receiveAddress()] } ], { hardFee: HARD_FEE }); @@ -2292,8 +2292,8 @@ describe('Wallet Balance', function() { const sendFinalizes = async (wallet, clone) => { await clone.sendBatch([ - ['FINALIZE', name1], - ['FINALIZE', name2] + { type: 'FINALIZE', args: [name1] }, + { type: 'FINALIZE', args: [name2] } ], { hardFee: HARD_FEE }); @@ -2383,8 +2383,8 @@ describe('Wallet Balance', function() { name2 = names[1]; await clone.sendBatch([ - ['TRANSFER', name1, recv], - ['TRANSFER', name2, nextAddr] + { type: 'TRANSFER', args: [name1, recv] }, + { type: 'TRANSFER', args: [name2, nextAddr] } ], { hardFee: HARD_FEE }); @@ -2394,8 +2394,8 @@ describe('Wallet Balance', function() { const sendFinalizes = async (wallet, clone) => { await clone.sendBatch([ - ['FINALIZE', name1], - ['FINALIZE', name2] + { type: 'FINALIZE', args: [name1] }, + { type: 'FINALIZE', args: [name2] } ], { hardFee: HARD_FEE }); diff --git a/test/wallet-importnonce-test.js b/test/wallet-importnonce-test.js index 85c6d2b0..8ce9c326 100644 --- a/test/wallet-importnonce-test.js +++ b/test/wallet-importnonce-test.js @@ -75,8 +75,8 @@ describe('Wallet Import Nonce', function () { it('should bid with sendbatch', async () => { const batch = [ - ['BID', NAME, BIDS[1].value, BIDS[1].lockup], - ['BID', NAME, BIDS[2].value, BIDS[2].lockup] + { type: 'BID', args: [NAME, BIDS[1].value, BIDS[1].lockup]}, + { type: 'BID', args: [NAME, BIDS[2].value, BIDS[2].lockup]} ]; const bidTx = await walletA.sendBatch(batch); From e11f3bc8c89dc9903207ccbcb44e5c127a5b93d9 Mon Sep 17 00:00:00 2001 From: Nodari Chkuaselidze Date: Mon, 21 Jul 2025 16:11:29 +0400 Subject: [PATCH 2/2] wallet: return errors array with from makeBatch and related methods. --- lib/wallet/rpc.js | 4 +- lib/wallet/wallet.js | 108 +++++++++++---- test/wallet-auction-test.js | 231 ++++++++++++++++++++++++++++---- test/wallet-balance-test.js | 12 +- test/wallet-importnonce-test.js | 2 +- 5 files changed, 298 insertions(+), 59 deletions(-) diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index e916cc5a..61ae0004 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -2717,7 +2717,7 @@ class RPC extends RPCBase { async sendBatch(args, help) { const [actions, options] = this._validateBatch(args, help, 'sendbatch'); const wallet = this.wallet; - const tx = await wallet.sendBatch(actions, options); + const {tx} = await wallet.sendBatch(actions, options); return tx.getJSON(this.network); } @@ -2727,7 +2727,7 @@ class RPC extends RPCBase { options.paths = true; const wallet = this.wallet; - const mtx = await wallet.createBatch(actions, options); + const {mtx} = await wallet.createBatch(actions, options); return mtx.getJSON(this.network); } diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index f1a521df..f5920c22 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -3863,18 +3863,44 @@ class Wallet extends EventEmitter { /** * @typedef {Object} BatchAction - * @property {String} type - Action type - * @property {Array} [args] - Arguments for the action + * @property {String} type - Action type. + * @property {String} [id] - Action ID. + * @property {Array} [args] - Arguments for the action. + */ + + /** + * @typedef {Object} BatchError + * @property {String} message - Error message. + * @property {String} type - Type of the action that caused the error. + * @property {Error} error - Original error object. + * @property {String} [id] - Optional ID for the action. + */ + + /** + * @typedef {Object} BatchResult + * @property {MTX} mtx - The resulting transaction + * @property {BatchError[]} errors - List of errors encountered during + * processing. + */ + + /** + * @typedef {Object} BatchTXResult + * @property {TX} tx - The resulting transaction + * @property {BatchError[]} errors - List of errors encountered during + * processing. */ /** * Make a batch transaction with multiple actions. * @param {BatchAction[]} actions * @param {Object} options - * @returns {Promise} + * @param {Number|String} [options.account=0] - Account index or name. + * @param {Boolean} [options.partialFailure=false] - Allow partial failure. + * @throws {Error} - general validations. + * @returns {Promise} */ - async makeBatch(actions, options) { + async makeBatch(actions, options = {}) { assert(Array.isArray(actions)); assert(actions.length, 'Batches require at least one action.'); @@ -3916,17 +3942,15 @@ class Wallet extends EventEmitter { // We track that by bumping receiveIndex. const account = await this.getAccount(acct); let receiveIndex = account.receiveDepth - 1; + /** @type {BatchError[]} */ + const errors = []; - // "actions" are arrays that start with a covenant type (or meta-type) - // followed by the arguments expected by the corresponding "make" function. - for (const action of actions) { - assert(action); - assert(typeof action.type === 'string'); - - const args = action.args || []; - - assert(Array.isArray(args), 'Action args must be an array.'); + /** + * @param {BatchAction} action + * @param {Array} args + */ + const handleAction = async (action, args) => { switch (action.type) { case 'NONE': { assert(args.length === 2); @@ -3944,8 +3968,10 @@ class Wallet extends EventEmitter { break; } case 'BID': { + const bidIndex = receiveIndex++; + assert(args.length === 3, 'Bad arguments for BID.'); - const address = account.deriveReceive(receiveIndex++).getAddress(); + const address = account.deriveReceive(bidIndex).getAddress(); const name = args[0]; const value = args[1]; const lockup = args[2]; @@ -4025,6 +4051,34 @@ class Wallet extends EventEmitter { default: throw new Error(`Unknown action type: ${action.type}`); } + }; + + // "actions" are arrays that start with a covenant type (or meta-type) + // followed by the arguments expected by the corresponding "make" function. + for (const action of actions) { + assert(action); + assert(typeof action.type === 'string'); + + const args = action.args || []; + + assert(Array.isArray(args), 'Action args must be an array.'); + + try { + await handleAction(action, args); + } catch (err) { + if (!options.partialFailure) + throw err; + + if (action.type === 'BID') + receiveIndex--; + + errors.push({ + message: err.message, + type: action.type, + error: err, + id: action.id || null + }); + } if (rules.countOpens(mtx) > consensus.MAX_BLOCK_OPENS) throw new Error('Too many OPENs.'); @@ -4076,27 +4130,30 @@ class Wallet extends EventEmitter { throw new Error('Batch output addresses would exceed lookahead.'); } - return mtx; + return {mtx, errors}; } /** * Make a batch transaction with multiple actions. * @param {Array} actions * @param {Object} options - * @returns {Promise} + * @returns {Promise} */ async _createBatch(actions, options) { - const mtx = await this.makeBatch(actions, options); + const {mtx, errors} = await this.makeBatch(actions, options); await this.fill(mtx, options); - return this.finalize(mtx, options); + return { + mtx: await this.finalize(mtx, options), + errors + }; } /** * Make a batch transaction with multiple actions. * @param {Array} actions * @param {Object} options - * @returns {Promise} + * @returns {Promise} */ async createBatch(actions, options) { @@ -4112,20 +4169,23 @@ class Wallet extends EventEmitter { * Create and send a batch transaction with multiple actions. * @param {Array} actions * @param {Object} options - * @returns {Promise} + * @returns {Promise} */ async _sendBatch(actions, options) { const passphrase = options ? options.passphrase : null; - const mtx = await this._createBatch(actions, options); - return this.sendMTX(mtx, passphrase); + const {mtx, errors} = await this._createBatch(actions, options); + return { + tx: await this.sendMTX(mtx, passphrase), + errors + }; } /** * Create and send a batch transaction with multiple actions. * @param {Array} actions * @param {Object} options - * @returns {Promise} + * @returns {Promise} */ async sendBatch(actions, options) { @@ -5470,7 +5530,7 @@ class Wallet extends EventEmitter { /** * Get current change address. - * @param {Number} [acct=0] + * @param {Number|String} [acct=0] * @returns {Promise
} */ diff --git a/test/wallet-auction-test.js b/test/wallet-auction-test.js index b7a343d2..33ae3de3 100644 --- a/test/wallet-auction-test.js +++ b/test/wallet-auction-test.js @@ -17,6 +17,8 @@ const Covenant = require('../lib/primitives/covenant'); const {Resource} = require('../lib/dns/resource'); const {forEvent} = require('./util/common'); +/** @typedef {import('../lib/wallet/wallet')} Wallet */ + const network = Network.get('regtest'); const { treeInterval, @@ -637,13 +639,16 @@ describe('Wallet Auction', function() { }); describe('Batch TXs', function() { - let wallet, receive; + /** @type {Wallet} */ + let wallet; + let receive; const hardFee = 12345; const name1 = rules.grindName(3, 0, network); const name2 = rules.grindName(4, 0, network); const name3 = rules.grindName(5, 0, network); const name4 = rules.grindName(6, 0, network); + const name5 = rules.grindName(7, 0, network); const res1 = Resource.fromJSON({records: [{type: 'TXT', txt: ['one']}]}); const res2 = Resource.fromJSON({records: [{type: 'TXT', txt: ['two']}]}); @@ -696,7 +701,7 @@ describe('Wallet Auction', function() { }); it('should create multiple OPENs with options', async () => { - const mtx = await wallet.createBatch( + const {mtx, errors} = await wallet.createBatch( [ { type: 'OPEN', args: [name1] }, { type: 'OPEN', args: [name2] }, @@ -708,8 +713,9 @@ describe('Wallet Auction', function() { ); assert(uniqueAddrs(mtx)); - + assert.strictEqual(errors.length, 0); assert.strictEqual(mtx.outputs.length, 4); + let opens = 0; for (const output of mtx.outputs) { if (output.covenant.type === rules.types.OPEN) @@ -739,6 +745,26 @@ describe('Wallet Auction', function() { ); }); + it('should fail if one action is invalid: OPEN reserved (partial)', async () => { + const {mtx, errors} = await wallet.createBatch([ + { type: 'OPEN', args: ['google'], id: 'google-id' }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } + ], { + partialFailure: true + }); + + assert.strictEqual(errors.length, 1); + assert.deepStrictEqual(errors[0], { + id: 'google-id', + type: 'OPEN', + message: 'Name is reserved: google.', + error: new Error('Name is reserved: google.') + }); + + assert.strictEqual(mtx.outputs.length, 3); + }); + it('should fail if one action is invalid: OPEN duplicated', async () => { await assert.rejects( wallet.sendBatch( @@ -750,6 +776,34 @@ describe('Wallet Auction', function() { ), {message: 'Duplicate name with exclusive action.'} ); + + await assert.rejects( + wallet.sendBatch( + [ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name3] } + ], { + partialFailure: true + } + ), + {message: 'Duplicate name with exclusive action.'} + ); + }); + + it('should fail if one action is invalid: OPEN duplicated (partial)', async () => { + await assert.rejects( + wallet.sendBatch( + [ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name3] } + ], { + partialFailure: true + } + ), + {message: 'Duplicate name with exclusive action.'} + ); }); it('should fail if one action is invalid: REVEAL before bid', async () => { @@ -765,6 +819,21 @@ describe('Wallet Auction', function() { ); }); + it('should fail if one action is invalid: REVEAL before bid (partial)', async () => { + await assert.rejects( + wallet.sendBatch( + [ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name3] } + ], { + partialFailure: true + } + ), + {message: 'Duplicate name with exclusive action.'} + ); + }); + it('should fail if one action is invalid: BID early', async () => { await assert.rejects( wallet.sendBatch( @@ -778,6 +847,26 @@ describe('Wallet Auction', function() { ); }); + it('should fail if one action is invalid: BID early (partial)', async () => { + const {mtx, errors} = await wallet.createBatch( [ + { type: 'BID', args: [name1, 1, 1], id: name1 }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } + ], { + partialFailure: true + }); + + assert.strictEqual(errors.length, 1); + assert.deepStrictEqual(errors[0], { + id: name1, + type: 'BID', + message: `Name has not reached the bidding phase yet: ${name1}.`, + error: new Error(`Name has not reached the bidding phase yet: ${name1}.`) + }); + + assert.strictEqual(mtx.outputs.length, 3); + }); + it('should fail if one action is invalid: wrong arguments', async () => { await assert.rejects( wallet.sendBatch( @@ -791,6 +880,27 @@ describe('Wallet Auction', function() { ); }); + it('should fail if one action is invalid: wrong arguments (partial)', async () => { + const {mtx, errors} = await wallet.createBatch([ + { type: 'BID', args: [name1, 21000000] }, + { type: 'OPEN', args: [name2] }, + { type: 'OPEN', args: [name3] } + ], { + partialFailure: true + }); + + assert.strictEqual(errors.length, 1); + assert.strictEqual(errors[0].error.message, 'Bad arguments for BID.'); + delete errors[0].error; + + assert.deepStrictEqual(errors[0], { + id: null, + type: 'BID', + message: 'Bad arguments for BID.' + }); + assert.strictEqual(mtx.outputs.length, 3); + }); + it('should fail if one action is invalid: REVEAL all before bid', async () => { await assert.rejects( wallet.sendBatch( @@ -828,6 +938,30 @@ describe('Wallet Auction', function() { ); }); + it('should fail if one action is invalid: NONE below dust (partial)', async () => { + const addr = Address.fromProgram(0, Buffer.alloc(20, 0x01)).toString('regtest'); + const {mtx, errors} = await wallet.createBatch( + [ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name3] }, + { type: 'NONE', args: [addr, 1] } + ], + { + partialFailure: true + } + ); + + assert.strictEqual(errors.length, 1); + assert.deepStrictEqual(errors[0], { + id: null, + type: 'NONE', + message: 'Output is dust.', + error: new Error('Output is dust.') + }); + + assert.strictEqual(mtx.outputs.length, 3); + }); + it('should fail if one action is invalid: unknown action', async () => { await assert.rejects( wallet.sendBatch( @@ -842,10 +976,31 @@ describe('Wallet Auction', function() { ); }); + it('should fail if one action is invalid: unknown action (partial)', async () => { + const {mtx, errors} = await wallet.createBatch([ + { type: 'OPEN', args: [name1] }, + { type: 'OPEN', args: [name3] }, + { type: 'open', args: [name4] } + ], { + partialFailure: true + }); + + assert.strictEqual(errors.length, 1); + assert.deepStrictEqual(errors[0], { + id: null, + type: 'open', + message: 'Unknown action type: open', + error: new Error('Unknown action type: open') + }); + + assert.strictEqual(mtx.outputs.length, 3); + }); + describe('Complete auction and diverse-action batches', function() { const addr = Address.fromProgram(0, Buffer.alloc(20, 0x01)).toString('regtest'); + it('3 OPENs and 1 NONE', async () => { - const tx = await wallet.sendBatch( + const {tx, errors} = await wallet.sendBatch( [ { type: 'OPEN', args: [name1] }, { type: 'OPEN', args: [name2] }, @@ -855,32 +1010,43 @@ describe('Wallet Auction', function() { ); assert(uniqueAddrs(tx)); + assert.strictEqual(errors.length, 0); await mineBlocks(treeInterval + 1); }); it('4 BIDs', async () => { - const tx = await wallet.sendBatch( + const {tx, errors} = await wallet.sendBatch( [ { type: 'BID', args: [name1, 10000, 20000] }, { type: 'BID', args: [name1, 10001, 20000] }, // self-snipe! { type: 'BID', args: [name2, 30000, 40000] }, - { type: 'BID', args: [name3, 50000, 60000] } - ] + { type: 'BID', args: [name3, 50000, 60000] }, + // Handle failed index increments. + { type: 'BID', args: ['random', -100, -1]}, + { type: 'BID', args: ['random', -100, -1]}, + { type: 'BID', args: ['random', -100]}, + { type: 'OPEN', args: [name5]} + ], { + partialFailure: true + } ); assert(uniqueAddrs(tx)); + assert.strictEqual(errors.length, 3); await mineBlocks(biddingPeriod); }); it('REVEAL all', async () => { // Don't send this one - const revealAll = await wallet.createBatch( + const {mtx: revealAll, errors} = await wallet.createBatch( [ { type: 'REVEAL' } ] ); assert.strictEqual(revealAll.outputs.length, 5); + assert.strictEqual(errors.length, 0); + let reveals = 0; for (const output of revealAll.outputs) { if (output.covenant.type === rules.types.REVEAL) @@ -890,7 +1056,7 @@ describe('Wallet Auction', function() { }); it('2 REVEALs then 1 REVEAL', async () => { - const tx = await wallet.sendBatch( + const {tx, errors} = await wallet.sendBatch( [ { type: 'REVEAL', args: [name1] }, { type: 'REVEAL', args: [name2] } @@ -898,6 +1064,7 @@ describe('Wallet Auction', function() { ); assert(uniqueAddrs(tx)); + assert.strictEqual(errors.length, 0); // No "could not resolve preferred inputs" error // because names are being revealed individually. @@ -911,13 +1078,15 @@ describe('Wallet Auction', function() { it('REDEEM all', async () => { // Don't send this one - const redeemAll = await wallet.createBatch( + const {mtx: redeemAll, errors} = await wallet.createBatch( [ { type: 'REDEEM' } ] ); assert.strictEqual(redeemAll.outputs.length, 2); + assert.strictEqual(errors.length, 0); + let redeems = 0; for (const output of redeemAll.outputs) { if (output.covenant.type === rules.types.REDEEM) @@ -928,7 +1097,7 @@ describe('Wallet Auction', function() { it('3 REGISTERs, 1 REDEEM and 1 OPEN', async () => { // Complete all 4 bids win and/or lose in one TX - const batch1 = await wallet.sendBatch( + const {tx: batch1, errors} = await wallet.sendBatch( [ { type: 'OPEN', args: [name4] }, { type: 'REDEEM', args: [name1] }, @@ -939,6 +1108,7 @@ describe('Wallet Auction', function() { ); assert(uniqueAddrs(batch1)); + assert.strictEqual(errors.length, 0); // Unlinked covenant (OPEN) was listed first but // should be sorted last with the change output (NONE). @@ -953,7 +1123,8 @@ describe('Wallet Auction', function() { version: 31, hash: Buffer.from([1, 2, 3]) }); - const tx = await wallet.sendBatch( + + const {tx, errors} = await wallet.sendBatch( [ { type: 'TRANSFER', args: [name1, nullAddr] }, { type: 'TRANSFER', args: [name2, nullAddr] }, @@ -963,6 +1134,7 @@ describe('Wallet Auction', function() { ); assert(uniqueAddrs(tx)); + assert.strictEqual(errors.length, 0); // True for regtest but not mainnet, // should allow both REVEAL and FINALIZE @@ -972,7 +1144,7 @@ describe('Wallet Auction', function() { }); it('1 FINALIZE, 1 CANCEL, 1 REVOKE and 1 REVEAL', async () => { - const tx = await wallet.sendBatch( + const {tx, errors} = await wallet.sendBatch( [ { type: 'FINALIZE', args: [name1] }, { type: 'CANCEL', args: [name2] }, @@ -982,6 +1154,7 @@ describe('Wallet Auction', function() { ); assert(uniqueAddrs(tx)); + assert.strictEqual(errors.length, 0); // Should allow for both REGISTER and re-open revoked name assert(auctionMaturity > revealPeriod); @@ -990,7 +1163,7 @@ describe('Wallet Auction', function() { }); it('1 revoked name re-OPEN and 1 REGISTER', async () => { - const batch2 = await wallet.sendBatch( + const {tx: batch2, errors} = await wallet.sendBatch( [ { type: 'OPEN', args: [name3] }, // and the cycle begins again... { type: 'UPDATE', args: [name4, res4] } @@ -998,6 +1171,7 @@ describe('Wallet Auction', function() { ); assert(uniqueAddrs(batch2)); + assert.strictEqual(errors.length, 0); // Linked covenant (UPDATE) was listed last but should be sorted first. assert.strictEqual(batch2.outputs[0].covenant.type, rules.types.REGISTER); @@ -1013,9 +1187,10 @@ describe('Wallet Auction', function() { actions.push({ type: 'NONE', args: [addr, 10000] }); } - const batch = await wallet.sendBatch(actions, {hardFee: 1000}); + const {tx: batch, errors} = await wallet.sendBatch(actions, {hardFee: 1000}); assert.strictEqual(batch.outputs.length, 11); + assert.strictEqual(errors.length, 0); // Mine to some other wallet so reward doesn't affect our balance receive = new Address(); @@ -1056,13 +1231,15 @@ describe('Wallet Auction', function() { }); it('2 RENEW', async () => { - await wallet.sendBatch( + const {errors} = await wallet.sendBatch( [ { type: 'RENEW', args: [name2] }, { type: 'RENEW', args: [name4] } ] ); + assert.strictEqual(errors.length, 0); + await mineBlocks(1); const ns1 = await chain.db.getNameStateByName(name1); const ns2 = await chain.db.getNameStateByName(name2); @@ -1094,14 +1271,16 @@ describe('Wallet Auction', function() { const wtxs = await wallet.toDetails(txs); for (const wtx of wtxs) { - for (const output of wtx.outputs) + for (const output of wtx.outputs) { if ( output.path && output.path.account === 0 && output.path.branch === 0 // receive ) { addrIndexes[output.path.index]++; } + } } + // Ensure every receive address was used at least once assert(addrIndexes.indexOf(0) === -1); }); @@ -1177,7 +1356,7 @@ describe('Wallet Auction', function() { it('should create batch just under weight limit', async () => { // Start with the batch we would normally make - const mtx = await wallet.createBatch([{ type: 'REVEAL' }]); + const {mtx} = await wallet.createBatch([{ type: 'REVEAL' }]); // Find a spendable coin const coins = await wallet.getCoins(); @@ -1215,12 +1394,12 @@ describe('Wallet Auction', function() { it('should REVEAL all in several batches', async () => { let reveals = 0; - const mtx1 = await wallet.createBatch([{ type: 'REVEAL' }]); + const {mtx: mtx1} = await wallet.createBatch([{ type: 'REVEAL' }]); assert(mtx1.changeIndex >= 0); reveals += mtx1.outputs.length - 1; await wdb.addTX(mtx1.toTX()); - const mtx2 = await wallet.createBatch([{ type: 'REVEAL' }]); + const {mtx: mtx2} = await wallet.createBatch([{ type: 'REVEAL' }]); assert(mtx2.changeIndex >= 0); reveals += mtx2.outputs.length - 1; await wdb.addTX(mtx2.toTX()); @@ -1241,12 +1420,12 @@ describe('Wallet Auction', function() { it('should REDEEM all in several batches', async () => { let reveals = 0; - const mtx1 = await wallet.createBatch([{ type: 'REDEEM' }]); + const {mtx: mtx1} = await wallet.createBatch([{ type: 'REDEEM' }]); assert(mtx1.changeIndex >= 0); reveals += mtx1.outputs.length - 1; await wdb.addTX(mtx1.toTX()); - const mtx2 = await wallet.createBatch([{ type: 'REDEEM' }]); + const {mtx: mtx2} = await wallet.createBatch([{ type: 'REDEEM' }]); assert(mtx2.changeIndex >= 0); reveals += mtx2.outputs.length - 1; await wdb.addTX(mtx2.toTX()); @@ -1336,7 +1515,7 @@ describe('Wallet Auction', function() { let reveals = 0; for (;;) { try { - const tx = await wallet.sendBatch([ + const {tx} = await wallet.sendBatch([ { type: 'REVEAL' }, { type: 'REDEEM' }, { type: 'RENEW' }, @@ -1358,7 +1537,7 @@ describe('Wallet Auction', function() { let redeems = 0; for (;;) { try { - const tx = await wallet.sendBatch([ + const {tx} = await wallet.sendBatch([ { type: 'REVEAL' }, { type: 'REDEEM' }, { type: 'RENEW' }, @@ -1445,7 +1624,7 @@ describe('Wallet Auction', function() { let renewals = 0; for (;;) { - const tx = await wallet.sendBatch([ + const {tx} = await wallet.sendBatch([ { type: 'REVEAL' }, { type: 'REDEEM' }, { type: 'RENEW' }, @@ -1509,7 +1688,7 @@ describe('Wallet Auction', function() { let finalizes = 0; for (;;) { - const tx = await wallet.sendBatch([ + const {tx} = await wallet.sendBatch([ { type: 'REVEAL' }, { type: 'REDEEM' }, { type: 'RENEW' }, diff --git a/test/wallet-balance-test.js b/test/wallet-balance-test.js index 6a0ae42b..664a2c60 100644 --- a/test/wallet-balance-test.js +++ b/test/wallet-balance-test.js @@ -573,7 +573,7 @@ describe('Wallet Balance', function() { const txOpts = { hardFee: HARD_FEE }; // all three bids are there. - const bidMTX = await wallet.createBatch([ + const {mtx: bidMTX} = await wallet.createBatch([ { type: 'BID', args: [name1, BID_AMOUNT_1, BLIND_AMOUNT_1] }, { type: 'BID', args: [name2, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); @@ -1018,7 +1018,7 @@ describe('Wallet Balance', function() { const {nextAddr} = getAheadAddr(account, ahead); const txOpts = { hardFee: HARD_FEE }; - const bidMTX = await wallet.createBatch([ + const {mtx: bidMTX} = await wallet.createBatch([ { type: 'BID', args: [name, BID_AMOUNT_1, BLIND_AMOUNT_1] }, { type: 'BID', args: [name, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); @@ -1111,7 +1111,7 @@ describe('Wallet Balance', function() { const {nextAddr} = getAheadAddr(account, ahead); const txOpts = { hardFee: HARD_FEE }; - const bidMTX = await primary.createBatch([ + const {mtx: bidMTX} = await primary.createBatch([ { type: 'BID', args: [name, BID_AMOUNT_1, BLIND_AMOUNT_1] }, { type: 'BID', args: [name, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); @@ -1208,7 +1208,7 @@ describe('Wallet Balance', function() { const addr1 = getAheadAddr(altAccount, -altAccount.lookahead); const addr2 = getAheadAddr(altAccount, ahead); - const bidMTX = await wallet.createBatch([ + const {mtx: bidMTX} = await wallet.createBatch([ { type: 'BID', args: [name, BID_AMOUNT_1, BLIND_AMOUNT_1] }, { type: 'BID', args: [name, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); @@ -1393,7 +1393,7 @@ describe('Wallet Balance', function() { await primary.sendOpen(name, false); await mineBlocks(openingPeriod); - const bidMTX = await clone.createBatch([ + const {mtx: bidMTX} = await clone.createBatch([ { type: 'BID', args: [name, BID_AMOUNT_1, BLIND_AMOUNT_1] }, { type: 'BID', args: [name, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); @@ -1852,7 +1852,7 @@ describe('Wallet Balance', function() { const txOpts = { hardFee: HARD_FEE }; // all three bids are there. - const bidMTX = await clone.createBatch([ + const {mtx: bidMTX} = await clone.createBatch([ { type: 'BID', args: [name1, BID_AMOUNT_1, BLIND_AMOUNT_1] }, { type: 'BID', args: [name2, BID_AMOUNT_2, BLIND_AMOUNT_2] } ], txOpts); diff --git a/test/wallet-importnonce-test.js b/test/wallet-importnonce-test.js index 8ce9c326..183f77e5 100644 --- a/test/wallet-importnonce-test.js +++ b/test/wallet-importnonce-test.js @@ -79,7 +79,7 @@ describe('Wallet Import Nonce', function () { { type: 'BID', args: [NAME, BIDS[2].value, BIDS[2].lockup]} ]; - const bidTx = await walletA.sendBatch(batch); + const {tx: bidTx} = await walletA.sendBatch(batch); // Save address for importnonce later for (const output of bidTx.outputs) {