Skip to content

Commit a4755e6

Browse files
committed
wallet: check account ownership of name before making update TX
also: make renew and make redeem
1 parent 7d8edf6 commit a4755e6

File tree

2 files changed

+296
-7
lines changed

2 files changed

+296
-7
lines changed

lib/wallet/wallet.js

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2010,15 +2010,21 @@ class Wallet extends EventEmitter {
20102010
/**
20112011
* Make a redeem MTX.
20122012
* @param {String} name
2013+
* @param {String|Number} acct
20132014
* @returns {MTX}
20142015
*/
20152016

2016-
async makeRedeem(name) {
2017+
async makeRedeem(name, acct) {
20172018
assert(typeof name === 'string');
20182019

20192020
if (!rules.verifyName(name))
20202021
throw new Error('Invalid name.');
20212022

2023+
if (acct != null) {
2024+
assert((acct >>> 0) === acct || typeof acct === 'string');
2025+
acct = await this.getAccountIndex(acct);
2026+
}
2027+
20222028
const rawName = Buffer.from(name, 'ascii');
20232029
const nameHash = rules.hashName(rawName);
20242030
const ns = await this.getNameState(nameHash);
@@ -2046,6 +2052,7 @@ class Wallet extends EventEmitter {
20462052
if (!own)
20472053
continue;
20482054

2055+
// Winner can not redeem
20492056
if (prevout.equals(ns.owner))
20502057
continue;
20512058

@@ -2054,6 +2061,9 @@ class Wallet extends EventEmitter {
20542061
if (!coin)
20552062
continue;
20562063

2064+
if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index))
2065+
continue;
2066+
20572067
// Is local?
20582068
if (coin.height < ns.height)
20592069
continue;
@@ -2085,7 +2095,8 @@ class Wallet extends EventEmitter {
20852095
*/
20862096

20872097
async _createRedeem(name, options) {
2088-
const mtx = await this.makeRedeem(name);
2098+
const acct = options ? options.account : null;
2099+
const mtx = await this.makeRedeem(name, acct);
20892100
await this.fill(mtx, options);
20902101
return this.finalize(mtx, options);
20912102
}
@@ -2335,16 +2346,22 @@ class Wallet extends EventEmitter {
23352346
* Make an update MTX.
23362347
* @param {String} name
23372348
* @param {Resource} resource
2349+
* @param {String|Number} acct
23382350
* @returns {MTX}
23392351
*/
23402352

2341-
async makeUpdate(name, resource) {
2353+
async makeUpdate(name, resource, acct) {
23422354
assert(typeof name === 'string');
23432355
assert(resource instanceof Resource);
23442356

23452357
if (!rules.verifyName(name))
23462358
throw new Error('Invalid name.');
23472359

2360+
if (acct != null) {
2361+
assert((acct >>> 0) === acct || typeof acct === 'string');
2362+
acct = await this.getAccountIndex(acct);
2363+
}
2364+
23482365
const rawName = Buffer.from(name, 'ascii');
23492366
const nameHash = rules.hashName(rawName);
23502367
const ns = await this.getNameState(nameHash);
@@ -2360,6 +2377,9 @@ class Wallet extends EventEmitter {
23602377
if (!coin)
23612378
throw new Error(`Wallet does not own: "${name}".`);
23622379

2380+
if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index))
2381+
throw new Error(`Account does not own: "${name}".`);
2382+
23632383
if (coin.covenant.isReveal() || coin.covenant.isClaim())
23642384
return this._makeRegister(name, resource);
23652385

@@ -2412,7 +2432,8 @@ class Wallet extends EventEmitter {
24122432
*/
24132433

24142434
async _createUpdate(name, resource, options) {
2415-
const mtx = await this.makeUpdate(name, resource);
2435+
const acct = options ? options.account : null;
2436+
const mtx = await this.makeUpdate(name, resource, acct);
24162437
await this.fill(mtx, options);
24172438
return this.finalize(mtx, options);
24182439
}
@@ -2470,16 +2491,21 @@ class Wallet extends EventEmitter {
24702491
* Make a renewal MTX.
24712492
* @private
24722493
* @param {String} name
2473-
* @param {Resource?} resource
2494+
* @param {String|Number} acct
24742495
* @returns {MTX}
24752496
*/
24762497

2477-
async makeRenewal(name) {
2498+
async makeRenewal(name, acct) {
24782499
assert(typeof name === 'string');
24792500

24802501
if (!rules.verifyName(name))
24812502
throw new Error('Invalid name.');
24822503

2504+
if (acct != null) {
2505+
assert((acct >>> 0) === acct || typeof acct === 'string');
2506+
acct = await this.getAccountIndex(acct);
2507+
}
2508+
24832509
const rawName = Buffer.from(name, 'ascii');
24842510
const nameHash = rules.hashName(rawName);
24852511
const ns = await this.getNameState(nameHash);
@@ -2502,6 +2528,9 @@ class Wallet extends EventEmitter {
25022528
if (coin.height < ns.height)
25032529
throw new Error(`Wallet does not own: "${name}".`);
25042530

2531+
if (acct != null && !await this.txdb.hasCoinByAccount(acct, hash, index))
2532+
throw new Error(`Account does not own: "${name}".`);
2533+
25052534
const state = ns.state(height, network);
25062535

25072536
if (state !== states.CLOSED)
@@ -2541,7 +2570,8 @@ class Wallet extends EventEmitter {
25412570
*/
25422571

25432572
async _createRenewal(name, options) {
2544-
const mtx = await this.makeRenewal(name);
2573+
const acct = options ? options.account : null;
2574+
const mtx = await this.makeRenewal(name, acct);
25452575
await this.fill(mtx, options);
25462576
return this.finalize(mtx, options);
25472577
}

test/wallet-accounts-auction-test.js

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/* eslint-env mocha */
2+
/* eslint prefer-arrow-callback: "off" */
3+
4+
'use strict';
5+
6+
const assert = require('bsert');
7+
const Network = require('../lib/protocol/network');
8+
const FullNode = require('../lib/node/fullnode');
9+
const Address = require('../lib/primitives/address');
10+
const rules = require('../lib/covenants/rules');
11+
const Resource = require('../lib/dns/resource');
12+
const {WalletClient} = require('hs-client');
13+
14+
const network = Network.get('regtest');
15+
16+
const node = new FullNode({
17+
memory: true,
18+
network: 'regtest',
19+
plugins: [require('../lib/wallet/plugin')]
20+
});
21+
22+
// Prevent mempool from sending duplicate TXs back to the walletDB and txdb.
23+
// This will prevent a race condition when we need to remove spent (but
24+
// unconfirmed) outputs from the wallet so they can be reused in other tests.
25+
node.mempool.emit = () => {};
26+
27+
const wclient = new WalletClient({
28+
port: network.walletPort
29+
});
30+
31+
const {wdb} = node.require('walletdb');
32+
33+
const name = rules.grindName(5, 1, network);
34+
let wallet, alice, bob, aliceReceive, bobReceive;
35+
36+
async function mineBlocks(n, addr) {
37+
addr = addr ? addr : new Address().toString('regtest');
38+
for (let i = 0; i < n; i++) {
39+
const block = await node.miner.mineBlock(null, addr);
40+
await node.chain.add(block);
41+
}
42+
}
43+
44+
describe('Multiple accounts participating in same auction', function() {
45+
before(async () => {
46+
await node.open();
47+
await wclient.open();
48+
49+
wallet = await wdb.create();
50+
51+
// We'll use an account number for alice and a string for bob
52+
// to ensure that both types work as options.
53+
alice = await wallet.getAccount(0);
54+
bob = await wallet.createAccount({name: 'bob'});
55+
56+
aliceReceive = await alice.receiveAddress();
57+
bobReceive = await bob.receiveAddress();
58+
});
59+
60+
after(async () => {
61+
await wclient.close();
62+
await node.close();
63+
});
64+
65+
it('should fund both accounts', async () => {
66+
await mineBlocks(2, aliceReceive);
67+
await mineBlocks(2, bobReceive);
68+
69+
// Wallet rescan is an effective way to ensure that
70+
// wallet and chain are synced before proceeding.
71+
await wdb.rescan(0);
72+
73+
const aliceBal = await wallet.getBalance(0);
74+
const bobBal = await wallet.getBalance('bob');
75+
assert(aliceBal.confirmed === 2000 * 2 * 1e6);
76+
assert(bobBal.confirmed === 2000 * 2 * 1e6);
77+
});
78+
79+
it('should open an auction and proceed to REVEAL phase', async () => {
80+
await wallet.sendOpen(name, false, {account: 0});
81+
await mineBlocks(network.names.treeInterval + 2);
82+
let ns = await node.chain.db.getNameStateByName(name);
83+
assert(ns.isBidding(node.chain.height, network));
84+
85+
await wdb.rescan(0);
86+
87+
await wallet.sendBid(name, 100000, 200000, {account: 0});
88+
await wallet.sendBid(name, 50000, 200000, {account: 'bob'});
89+
await mineBlocks(network.names.biddingPeriod);
90+
ns = await node.chain.db.getNameStateByName(name);
91+
assert(ns.isReveal(node.chain.height, network));
92+
93+
await wdb.rescan(0);
94+
95+
const walletBids = await wallet.getBidsByName(name);
96+
assert.strictEqual(walletBids.length, 2);
97+
98+
for (const bid of walletBids)
99+
assert(bid.own);
100+
101+
assert.strictEqual(node.mempool.map.size, 0);
102+
});
103+
104+
describe('REVEAL', function() {
105+
it('should send one REVEAL per account', async () => {
106+
const tx1 = await wallet.sendReveal(name, {account: 0});
107+
assert(tx1);
108+
109+
const tx2 = await wallet.sendReveal(name, {account: 'bob'});
110+
assert(tx2);
111+
112+
// Reset for next test
113+
await wallet.abandon(tx1.hash());
114+
await wallet.abandon(tx2.hash());
115+
116+
assert.strictEqual(node.mempool.map.size, 2);
117+
await node.mempool.reset();
118+
assert.strictEqual(node.mempool.map.size, 0);
119+
});
120+
121+
it('should send one REVEAL for all accounts in one tx', async () => {
122+
const tx = await wallet.sendRevealAll();
123+
assert(tx);
124+
125+
// Reset for next test
126+
await wallet.abandon(tx.hash());
127+
128+
assert.strictEqual(node.mempool.map.size, 1);
129+
await mineBlocks(1);
130+
assert.strictEqual(node.mempool.map.size, 0);
131+
});
132+
});
133+
134+
describe('UPDATE', function() {
135+
const aliceResource = Resource.Resource.fromJSON({
136+
records: [
137+
{
138+
type: 'TXT',
139+
txt: ['ALICE']
140+
}
141+
]});
142+
const bobResource = Resource.Resource.fromJSON({
143+
records: [
144+
{
145+
type: 'TXT',
146+
txt: ['BOB']
147+
}
148+
]});
149+
150+
it('should advance auction to REGISTER phase', async () => {
151+
await mineBlocks(network.names.revealPeriod);
152+
const ns = await node.chain.db.getNameStateByName(name);
153+
assert(ns.isClosed(node.chain.height, network));
154+
155+
await wdb.rescan(0);
156+
157+
// Alice is the winner
158+
const {hash, index} = ns.owner;
159+
assert(await wallet.txdb.hasCoinByAccount(0, hash, index));
160+
161+
// ...not Bob (sanity check)
162+
assert(!await wallet.txdb.hasCoinByAccount(1, hash, index));
163+
});
164+
165+
it('should reject REGISTER given wrong account', async () => {
166+
await assert.rejects(async () => {
167+
await wallet.sendUpdate(name, bobResource, {account: 'bob'});
168+
}, {
169+
name: 'Error',
170+
message: `Account does not own: "${name}".`
171+
});
172+
});
173+
174+
it('should send REGISTER given correct account', async () => {
175+
const tx = await wallet.sendUpdate(name, aliceResource, {account: 0});
176+
assert(tx);
177+
178+
await wallet.abandon(tx.hash());
179+
180+
assert.strictEqual(node.mempool.map.size, 1);
181+
await node.mempool.reset();
182+
assert.strictEqual(node.mempool.map.size, 0);
183+
});
184+
185+
it('should send REGISTER from correct account automatically', async () => {
186+
const tx = await wallet.sendUpdate(name, aliceResource);
187+
assert(tx);
188+
189+
await mineBlocks(1);
190+
});
191+
});
192+
193+
describe('REDEEM', function() {
194+
it('should reject REDEEM given wrong account', async () => {
195+
await assert.rejects(async () => {
196+
await wallet.sendRedeem(name, {account: 0});
197+
}, {
198+
name: 'Error',
199+
message: 'No reveals to redeem.'
200+
});
201+
});
202+
203+
it('should send REDEEM from correct account', async () => {
204+
const tx = await wallet.sendRedeem(name, {account: 'bob'});
205+
assert(tx);
206+
207+
await wallet.abandon(tx.hash());
208+
209+
assert.strictEqual(node.mempool.map.size, 1);
210+
await node.mempool.reset();
211+
assert.strictEqual(node.mempool.map.size, 0);
212+
});
213+
214+
it('should send REDEEM from correct account automatically', async () => {
215+
const tx = await wallet.sendRedeem(name);
216+
assert(tx);
217+
218+
await mineBlocks(1);
219+
});
220+
});
221+
222+
describe('RENEW', function() {
223+
it('should advance chain to allow renewal', async () => {
224+
await mineBlocks(network.names.treeInterval);
225+
await wdb.rescan(0);
226+
});
227+
228+
it('should reject RENEW from wrong account', async () => {
229+
await assert.rejects(async () => {
230+
await wallet.sendRenewal(name, {account: 'bob'});
231+
}, {
232+
name: 'Error',
233+
message: `Account does not own: "${name}".`
234+
});
235+
});
236+
237+
it('should send RENEW from correct account', async () => {
238+
const tx = await wallet.sendRenewal(name, {account: 0});
239+
assert(tx);
240+
241+
await wallet.abandon(tx.hash());
242+
243+
assert.strictEqual(node.mempool.map.size, 1);
244+
await node.mempool.reset();
245+
assert.strictEqual(node.mempool.map.size, 0);
246+
});
247+
248+
it('should send RENEW from correct account automatically', async () => {
249+
const tx = await wallet.sendRenewal(name);
250+
assert(tx);
251+
252+
await wallet.abandon(tx.hash());
253+
254+
assert.strictEqual(node.mempool.map.size, 1);
255+
await node.mempool.reset();
256+
assert.strictEqual(node.mempool.map.size, 0);
257+
});
258+
});
259+
});

0 commit comments

Comments
 (0)