diff --git a/lib/node/http.js b/lib/node/http.js index 380c47745..2c6aa9f24 100644 --- a/lib/node/http.js +++ b/lib/node/http.js @@ -21,6 +21,8 @@ const Claim = require('../primitives/claim'); const Address = require('../primitives/address'); const Network = require('../protocol/network'); const pkg = require('../pkg'); +const rules = require('../covenants/rules'); +const Resource = require('../dns/resource'); /** * HTTP @@ -432,6 +434,133 @@ class HTTP extends Server { res.json(200, { success: true }); }); + + this.get('/name/:name', async (req, res) => { + const valid = Validator.fromRequest(req); + const name = valid.str('name'); + + if (!name || !rules.verifyName(name)) + throw new Error('Invalid parameter.'); + + const network = this.network; + const height = this.chain.height; + const nameHash = rules.hashName(name); + const reserved = rules.isReserved(nameHash, height + 1, network); + const [start, week] = rules.getRollout(nameHash, network); + const ns = await this.chain.db.getNameState(nameHash); + + let info = null; + + if (ns) { + if (!ns.isExpired(height, network)) + info = ns.getJSON(height, network); + } + + return res.json(200, { + start: { + reserved: reserved, + week: week, + start: start + }, + info + }); + }); + + this.get('/resource/hash/:hash', async (req, res) => { + const valid = Validator.fromRequest(req); + const hash = valid.bhash('hash'); + + if (!hash) + throw new Error('Invalid hash.'); + + const ns = await this.chain.db.getNameState(hash); + const height = this.chain.tip.height; + const network = this.network; + + if (!ns) + return res.json(404); + + return res.json(200, ns.getJSON(height,network)); + }); + + this.get('/resource/name/:name', async (req, res) => { + const valid = Validator.fromRequest(req); + const name = valid.str('name'); + + if (!name || !rules.verifyName(name)) + throw new Error('Invalid name.'); + + const nameHash = rules.hashName(name); + const ns = await this.chain.db.getNameState(nameHash); + + if (!ns || ns.data.length === 0) + return null; + + const resource = Resource.decode(ns.data); + + return res.json(200, resource.getJSON(name)); + }); + + this.get('/proof/name/:name', async (req, res) => { + const valid = Validator.fromRequest(req); + const name = valid.str('name'); + + if (!name || !rules.verifyName(name)) + throw new Error('Invalid name.'); + + const tip = this.chain.tip.hash; + const height = this.chain.tip.height; + const root = this.chain.tip.treeRoot; + const nameHash = rules.hashName(name); + const proof = await this.chain.db.prove(root, nameHash); + + return res.json(200, { + hash: tip.toString('hex'), + height: height, + root: root.toString('hex'), + name: name, + key: nameHash.toString('hex'), + ...proof.toJSON() + }); + }); + + this.get('/proof/hash/:nameHash', async (req, res) => { + const valid = Validator.fromRequest(req); + const nameHash = valid.bhash('nameHash'); + + if (!nameHash) + throw new Error('Invalid hash.'); + + const tip = this.chain.tip.hash; + const height = this.chain.tip.height; + const root = this.chain.tip.treeRoot; + const proof = await this.chain.db.prove(root, nameHash); + const ns = await this.chain.db.getNameState(nameHash); + + return res.json(200, { + hash: tip.toString('hex'), + height: height, + root: root.toString('hex'), + key: nameHash.toString('hex'), + name: ns ? ns.name.toString() : null, + ...proof.toJSON() + }); + }); + + this.get('/grind', async (req, res) => { + const valid = Validator.fromRequest(req); + const size = valid.u32('size'); + + if (size < 1 || size > 63) + throw new Error('Invalid length.'); + + const network = this.network; + const height = this.chain.height; + + const name = await rules.grindName(size, height + 1, network); + + res.json(200, { name }); + }); } /** diff --git a/test/node-http-test.js b/test/node-http-test.js new file mode 100644 index 000000000..7dc19d100 --- /dev/null +++ b/test/node-http-test.js @@ -0,0 +1,285 @@ +/* eslint-env mocha */ +/* eslint prefer-arrow-callback: 'off' */ + +'use strict'; + +const assert = require('bsert'); +const Network = require('../lib/protocol/network'); +const FullNode = require('../lib/node/fullnode'); +const rules = require('../lib/covenants/rules'); +const common = require('./util/common'); +const network = Network.get('regtest'); + +const { NodeClient, WalletClient } = require('hs-client'); + +const nclient = new NodeClient({ + port: network.rpcPort, + apiKey: 'foo' +}); + +const wclient = new WalletClient({ + port: network.walletPort, + apiKey: 'foo' +}); + +const wallet = wclient.wallet('primary'); + +describe('Node http', function() { + this.timeout(10000); + const witnessedBlocks = {}; + let NAME0, NAME1; + let node, mineBlocks, cbAddress; + + beforeEach(async () => { + assert.equal(network.names.auctionStart, 0); + + node = new FullNode({ + memory: true, + apiKey: 'foo', + network: 'regtest', + workers: true, + plugins: [require('../lib/wallet/plugin')] + }); + + node.on('connect', (entry, block) => { + const blockHash = block.hash().toString('hex'); + witnessedBlocks[blockHash] = blockHash; + }); + + mineBlocks = common.constructBlockMiner(node, nclient); + NAME0 = await rules.grindName(10, 0, network); + NAME1 = await rules.grindName(10, 20, network); + + await node.open(); + + cbAddress = (await wallet.createAddress('default')).address; + + await mineBlocks(network.names.auctionStart, cbAddress); + }); + + afterEach(async () => { + await node.close(); + }); + + describe('getNameInfo', () => { + describe('For names that are available at height 0', () => { + it('It should return null when there hasn\'t been an auction initiated', async () => { + const nameInfo = await nclient.get(`/name/${NAME0}`); + assert.deepEqual(nameInfo, { + info: null, + start: { + reserved: false, + start: 0, + week: 0 + } + }); + }); + it('It should start an auction on the first day', async () => { + const nameInfo = await nclient.get(`/name/${NAME0}`); + assert.deepEqual(nameInfo, { + info: null, + start: { + reserved: false, + start: 0, + week: 0 + } + }); + + await mineBlocks(10, cbAddress); + + const open = await wclient.execute('sendopen', [NAME0]); + assert(open); + }); + it('It should start an auction on the 2nd day', async () => { + await mineBlocks(40, cbAddress); + + const nameInfo = await nclient.get(`/name/${NAME0}`); + assert.deepEqual(nameInfo, { + info: null, + start: { + reserved: false, + start: 0, + week: 0 + } + }); + + const open = await wclient.execute('sendopen', [NAME0]); + assert(open); + + const nameInfoBefore = await nclient.get(`/name/${NAME0}`); + assert.equal(nameInfoBefore.info, null); + await mineBlocks(1, cbAddress); + + const nameInfoAfter = await nclient.get(`/name/${NAME0}`); + assert.equal(nameInfoAfter.info.name, NAME0); + assert.equal(nameInfoAfter.info.state, 'OPENING'); + }); + }); + + describe('For names that are available at height 20', () => { + it('It should getNameInfo for an opening name', async () => { + await mineBlocks(20, cbAddress); + + await wclient.execute('sendopen', [NAME1]); + await mineBlocks(1, cbAddress); + + const nameInfo = await nclient.get(`/name/${NAME1}`); + assert(nameInfo.start.start < 20); + assert.equal(nameInfo.start.reserved, false); + assert.equal(nameInfo.info.state, 'OPENING'); + }); + }); + }); + + describe('getNameByHash', () => { + it('It should return null when an auction has not been initiated', async () => { + const nameHash = rules.hashName(NAME0); + const name = await nclient.get(`/resource/hash/${nameHash.toString('hex')}`); + assert.equal(name, null); + }); + + describe('When an auction has been initiated', () => { + it('It should return the name', async () => { + await mineBlocks(10, cbAddress); + + await wclient.execute('sendopen', [NAME0]); + await mineBlocks(1, cbAddress); + + const nameHash = rules.hashName(NAME0); + const { name } = await nclient.get(`/resource/hash/${nameHash.toString('hex')}`); + assert.equal(name, NAME0); + }); + }); + }); + + describe('getNameResource', () => { + const records = { compat: false, version: 0, ttl: 172800, ns: ['ns1.example.com.@1.2.3.4'] }; + it('It should return null when an auction has not been initiated', async () => { + const resource = await nclient.get(`/resource/name/${NAME0}`); + assert.equal(resource, null); + }); + + describe('When an auction has been initiated', () => { + it('It should return the resource', async () => { + await mineBlocks(10, cbAddress); + + await wclient.execute('sendopen', [NAME0]); + await mineBlocks(1, cbAddress); + + const { stats: { blocksUntilBidding } } = await wclient.execute('getauctioninfo', [NAME0]); + + await mineBlocks(blocksUntilBidding, cbAddress); + + const sendBid = await wclient.execute('sendbid', [NAME0, 12, 12]); + assert(sendBid); + + const { stats: { blocksUntilReveal } } = await wclient.execute('getauctioninfo', [NAME0]); + + await mineBlocks(blocksUntilReveal, cbAddress); + + await wclient.execute('sendreveal', [NAME0]); + const { stats: { blocksUntilClose } } = await wclient.execute('getauctioninfo', [NAME0]); + + await mineBlocks(blocksUntilClose, cbAddress); + + await wclient.execute('sendupdate', [NAME0, records]); + await mineBlocks(1, cbAddress); + + const resource = await nclient.get(`/resource/name/${NAME0}`); + assert.deepEqual(resource, Object.assign(records, { name: NAME0, version: 0 })); + }); + }); + }); + + describe('getNameProof', () => { + it('It should return a proof type of TYPE_DEADEND when an auction has not been initiated', async () => { + const proof = await nclient.get(`/proof/name/${NAME0}`); + assert.equal(proof.type, 'TYPE_DEADEND'); + assert.equal(proof.name, NAME0); + }); + + describe('When an auction has been initiated', () => { + it('It should return the name\'s proof', async () => { + const record = { compat: false, version: 0, ttl: 172800, ns: ['ns1.example.com.@1.2.3.4'] }; + await mineBlocks(10, cbAddress); + + await wclient.execute('sendopen', [NAME0]); + await mineBlocks(1, cbAddress); + + const { stats: { blocksUntilBidding } } = await wclient.execute('getauctioninfo', [NAME0]); + + await mineBlocks(blocksUntilBidding, cbAddress); + + const sendBid = await wclient.execute('sendbid', [NAME0, 12, 12]); + assert(sendBid); + const { stats: { blocksUntilReveal } } = await wclient.execute('getauctioninfo', [NAME0]); + + await mineBlocks(blocksUntilReveal, cbAddress); + + await wclient.execute('sendreveal', [NAME0]); + const { stats: { blocksUntilClose } } = await wclient.execute('getauctioninfo', [NAME0]); + + await mineBlocks(blocksUntilClose, cbAddress); + + await wclient.execute('sendupdate', [NAME0, record]); + await mineBlocks(1, cbAddress); + + const proof = await nclient.get(`/proof/name/${NAME0}`); + assert.equal(proof.type, 'TYPE_EXISTS'); + assert.equal(proof.name, NAME0); + }); + }); + }); + + describe('getNameProofByHash', () => { + it('It should return a proof type of TYPE_DEADEND when an auction has not been initiated', async () => { + const nameHash = rules.hashName(NAME0).toString('hex'); + const proof = await nclient.get(`/proof/hash/${nameHash}`); + assert.equal(proof.type, 'TYPE_DEADEND'); + assert.equal(proof.name, null); + }); + + describe('When an auction has been initiated', () => { + it('It should return the name\'s proof', async () => { + const record = { compat: false, version: 0, ttl: 172800, ns: ['ns1.example.com.@1.2.3.4'] }; + await mineBlocks(10, cbAddress); + + await wclient.execute('sendopen', [NAME0]); + await mineBlocks(1, cbAddress); + + const { stats: { blocksUntilBidding } } = await wclient.execute('getauctioninfo', [NAME0]); + + await mineBlocks(blocksUntilBidding, cbAddress); + + const sendBid = await wclient.execute('sendbid', [NAME0, 12, 12]); + assert(sendBid); + const { stats: { blocksUntilReveal } } = await wclient.execute('getauctioninfo', [NAME0]); + + await mineBlocks(blocksUntilReveal, cbAddress); + + await wclient.execute('sendreveal', [NAME0]); + const { stats: { blocksUntilClose } } = await wclient.execute('getauctioninfo', [NAME0]); + + await mineBlocks(blocksUntilClose, cbAddress); + + await wclient.execute('sendupdate', [NAME0, record]); + await mineBlocks(1, cbAddress); + + const nameHash = rules.hashName(NAME0).toString('hex'); + const proof = await nclient.get(`/proof/hash/${nameHash}`); + assert.equal(proof.type, 'TYPE_EXISTS'); + assert.equal(proof.name, NAME0); + }); + }); + }); + + describe('grindName', () => { + it('It should grind a name', async () => { + const size = 10; + const { name } = await nclient.get('/grind', { size }); + assert(name); + assert.equal(name.length, size); + assert(rules.verifyName(name)); + }); + }); +}); diff --git a/test/util/common.js b/test/util/common.js index 050fbcf98..431b6479f 100644 --- a/test/util/common.js +++ b/test/util/common.js @@ -91,6 +91,10 @@ common.event = async function event(obj, name) { }); }; +common.sleep = function sleep(time) { + return new Promise(resolve => setTimeout(resolve, time)); +}; + common.forValue = async function(obj, key, val, timeout = 30000) { assert(typeof obj === 'object'); assert(typeof key === 'string'); @@ -113,6 +117,20 @@ common.forValue = async function(obj, key, val, timeout = 30000) { }); }; +common.constructBlockMiner = function (node, nclient) { + // take into account race conditions + return async function mineBlocks(count, address) { + for (let i = 0; i < count; i++) { + const obj = { complete: false }; + node.once('block', () => { + obj.complete = true; + }); + await nclient.execute('generatetoaddress', [1, address]); + await common.forValue(obj, 'complete', true); + } + }; +}; + function parseUndo(data) { const br = bio.read(data); const items = []; diff --git a/test/wallet-http-test.js b/test/wallet-http-test.js index 9ecfcb80c..956a7cbcf 100644 --- a/test/wallet-http-test.js +++ b/test/wallet-http-test.js @@ -47,7 +47,7 @@ const wclient = new WalletClient({ const wallet = wclient.wallet('primary'); const wallet2 = wclient.wallet('secondary'); -let name, cbAddress; +let name, cbAddress, mineBlocks; const accountTwo = 'foobar'; const { @@ -79,6 +79,7 @@ describe('Wallet HTTP', function() { }); beforeEach(async () => { + mineBlocks = common.constructBlockMiner(node, nclient); name = await nclient.execute('grindname', [5]); }); @@ -1098,18 +1099,6 @@ async function sleep(time) { return new Promise(resolve => setTimeout(resolve, time)); } -// take into account race conditions -async function mineBlocks(count, address) { - for (let i = 0; i < count; i++) { - const obj = { complete: false }; - node.once('block', () => { - obj.complete = true; - }); - await nclient.execute('generatetoaddress', [1, address]); - await common.forValue(obj, 'complete', true); - } -} - // create an OPEN output function openOutput(name, address) { const nameHash = rules.hashName(name);