diff --git a/dev-docs/specification.md b/dev-docs/specification.md index 0528522..d5d3d4a 100644 --- a/dev-docs/specification.md +++ b/dev-docs/specification.md @@ -72,6 +72,10 @@ Offer entities have the following properties: - _minUnitsToExchange_ - The minimum order size accepted. - _p2wdbTxid_ - The TXID proof-of-burn used to add the order to the P2WDB. - _p2wdbHash_ - The CID used to identify the order entry in the P2WDB. + - _offerStatus_ - The state of the offer. When the data is added to the P2WDB, it gets a value of 'posted', but the database model internal to bch-dex can have the following properties: + - *posted* - The offer is posted and can be countered by a taker. + - *taken* - The offer was countered and accepted. + - *dead* - The UTXO was spent outside the trade, which automatically makes the offer dead. - Authentication Data: - _signature_ - A message signed by the address which created the order. diff --git a/src/adapters/localdb/models/offer.js b/src/adapters/localdb/models/offer.js index a3751ad..d75c526 100644 --- a/src/adapters/localdb/models/offer.js +++ b/src/adapters/localdb/models/offer.js @@ -14,6 +14,7 @@ const Offer = new mongoose.Schema({ minUnitsToExchange: { type: String }, p2wdbTxid: { type: String }, p2wdbHash: { type: String }, + offerStatus: { type: String }, // Authentication data signature: { type: String }, diff --git a/src/controllers/rest-api/offer/controller.js b/src/controllers/rest-api/offer/controller.js index f19dba7..fb9da5b 100644 --- a/src/controllers/rest-api/offer/controller.js +++ b/src/controllers/rest-api/offer/controller.js @@ -2,7 +2,7 @@ REST API Controller library for the /offer route */ -// const { wlogger } = require('../../../adapters/wlogger') +const { wlogger } = require('../../../adapters/wlogger') let _this @@ -65,19 +65,15 @@ class OfferRESTControllerLib { // listed in the offer. async takeOffer (ctx) { try { - console.log('body: ', ctx.request.body) + console.log('REST API controller, body: ', ctx.request.body) const offerCid = ctx.request.body.offerCid - // Find the Offer. - // const offerEntity = await _this.useCases.offer.findOffer(offerId) - - // 'Take' the Offer. const hash = await _this.useCases.offer.takeOffer(offerCid) ctx.body = { hash } } catch (err) { - console.log('Error in takeOffer REST API handler.') + wlogger.error('Error in takeOffer() REST API handler.') _this.handleError(ctx, err) } } diff --git a/src/entities/offer.js b/src/entities/offer.js index 42b32e3..5f00fdb 100644 --- a/src/entities/offer.js +++ b/src/entities/offer.js @@ -6,7 +6,7 @@ class OfferEntity { constructor () { - this.orderStatus = ['posted', 'taken', 'completed'] + this.orderStatus = ['posted', 'taken', 'dead'] } validate (orderData = {}) { @@ -48,7 +48,7 @@ class OfferEntity { throw new Error("Property 'utxoVout' must be an integer number.") } if (orderStatus && !this.orderStatus.includes(orderStatus)) { - throw new Error("Property 'orderStatus' must be a valid string") + throw new Error("Property 'orderStatus' must be posted, taken, or dead") } const validatedOfferData = { diff --git a/src/use-cases/offer/index.js b/src/use-cases/offer/index.js index a0ba084..5acda64 100644 --- a/src/use-cases/offer/index.js +++ b/src/use-cases/offer/index.js @@ -1,12 +1,17 @@ /* Use Case library for Offers. + Offers are created by a webhook trigger from the P2WDB. Offers are a result of new data in P2WDB. They differ from Offers, which are generated by a local user. + An Offer is created to match a local Offer, but it's created indirectly, as a response to the webhook from the P2WDB. In this way, Offers generated from local Offers are no different than Offers generated by other peers. -*/ + + A Counter Offer is created by calling the /take/:cid endpoint. It creates + a partially signed transaction. + */ // Local libraries const OfferEntity = require('../../entities/offer') @@ -86,8 +91,8 @@ class OfferUseCases { console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`) // Ensure the offer is in a 'posted' state and not already 'taken' - if (offerInfo.offerStatus && offerInfo.offerStatus !== 'posted') { - throw new Error('offer already taken') + if (!offerInfo.offerStatus || offerInfo.offerStatus !== 'posted') { + throw new Error('offer status is not "posted", so offer is dead and can not be countered.') } // Verify that UTXO for sale is unspent. Abort if it's been spent. diff --git a/src/use-cases/order.js b/src/use-cases/order.js index cb42f0c..c8b83be 100644 --- a/src/use-cases/order.js +++ b/src/use-cases/order.js @@ -27,7 +27,7 @@ class OrderLib { // Create a new order model and add it to the Mongo database. async createOrder (entryObj) { try { - // console.log('createOrder(entryObj): ', entryObj) + console.log('createOrder(entryObj): ', entryObj) // Input Validation const orderEntity = this.orderEntity.validate(entryObj) @@ -47,7 +47,6 @@ class OrderLib { // Update the order with the new UTXO information. orderEntity.utxoTxid = utxoInfo.txid orderEntity.utxoVout = utxoInfo.vout - orderEntity.hdIndex = utxoInfo.hdIndex // Add order to P2WDB. const p2wdbObj = { @@ -59,6 +58,7 @@ class OrderLib { // console.log('hash: ', hash) // Create a MongoDB model to hold the Order + orderEntity.hdIndex = utxoInfo.hdIndex orderEntity.p2wdbHash = hash console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`) const order = new this.OrderModel(orderEntity) @@ -67,7 +67,8 @@ class OrderLib { return hash } catch (err) { // console.log("Error in use-cases/entry.js/createEntry()", err.message) - wlogger.error('Error in use-cases/entry.js/createOrder())') + wlogger.error('Error in use-cases/createOrder())') + console.log('error entryObj: ', entryObj) throw err } } diff --git a/test/e2e/manual/02-take-offer.js b/test/e2e/manual/02-take-offer.js index 7b21377..2ab150d 100644 --- a/test/e2e/manual/02-take-offer.js +++ b/test/e2e/manual/02-take-offer.js @@ -1,5 +1,5 @@ /* - Part 2 of 3: Take an order + Part 2 of 3: Take an offer by submiting a counter-offer. */ const axios = require('axios') @@ -10,9 +10,9 @@ async function start () { try { const options = { method: 'post', - url: `${LOCALHOST}/order/take`, + url: `${LOCALHOST}/offer/take`, data: { - orderCid: 'zdpuAkp98gTuivaNzGP31jTQi3ADXrFA6uANrceQcrQkTXy2j' + offerCid: 'zdpuAppDJR57Hrn1mAhsFRssSBp5qNrDT6PcSeHL5ndQqiqJc' } } diff --git a/test/unit/entities/offer.entity.unit.js b/test/unit/entities/offer.entity.unit.js index e63703d..95ddaf9 100644 --- a/test/unit/entities/offer.entity.unit.js +++ b/test/unit/entities/offer.entity.unit.js @@ -25,6 +25,8 @@ describe('#Offer-Entity', () => { it('should throw an error if data is not provided', () => { try { uut.validate() + + assert.fail('Unexpected code path') } catch (err) { // console.log(err) assert.include( @@ -38,6 +40,8 @@ describe('#Offer-Entity', () => { try { const orderData = { data: {} } uut.validate(orderData) + + assert.fail('Unexpected code path') } catch (err) { // console.log(err) assert.include( @@ -51,6 +55,8 @@ describe('#Offer-Entity', () => { try { const orderData = { data: { messageType: 1 } } uut.validate(orderData) + + assert.fail('Unexpected code path') } catch (err) { // console.log(err) assert.include( @@ -64,6 +70,8 @@ describe('#Offer-Entity', () => { try { const orderData = { data: { messageType: 1, messageClass: 1 } } uut.validate(orderData) + + assert.fail('Unexpected code path') } catch (err) { // console.log(err) assert.include(err.message, "Property 'tokenId' must be a string.") @@ -77,6 +85,8 @@ describe('#Offer-Entity', () => { } uut.validate(orderData) + + assert.fail('Unexpected code path') } catch (err) { // console.log(err) assert.include(err.message, "Property 'buyOrSell' must be a string.") @@ -95,6 +105,8 @@ describe('#Offer-Entity', () => { } uut.validate(orderData) + + assert.fail('Unexpected code path') } catch (err) { // console.log(err) assert.include( @@ -117,6 +129,8 @@ describe('#Offer-Entity', () => { } uut.validate(orderData) + + assert.fail('Unexpected code path') } catch (err) { // console.log(err) assert.include( @@ -139,6 +153,8 @@ describe('#Offer-Entity', () => { } } uut.validate(orderData) + + assert.fail('Unexpected code path') } catch (err) { // console.log(err) assert.include(err.message, "Property 'numTokens' must be a number.") @@ -159,6 +175,8 @@ describe('#Offer-Entity', () => { } } uut.validate(orderData) + + assert.fail('Unexpected code path') } catch (err) { // console.log(err) assert.include(err.message, "Property 'utxoTxid' must be a string.") @@ -180,6 +198,8 @@ describe('#Offer-Entity', () => { } } uut.validate(orderData) + + assert.fail('Unexpected code path') } catch (err) { // console.log(err) assert.include( @@ -189,6 +209,34 @@ describe('#Offer-Entity', () => { } }) + it('should throw an error if proper status is not applied', () => { + try { + const orderData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInSats: 1000, + minSatsToExchange: 350, + numTokens: 1, + utxoTxid: 'fakeTxid', + utxoVout: 0, + orderStatus: 'badStatus' + } + } + uut.validate(orderData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include( + err.message, + "Property 'orderStatus' must be posted, taken, or dead" + ) + } + }) + it('should validate a new order', () => { const orderObj = { appId: 'swapTest555', @@ -203,7 +251,8 @@ describe('#Offer-Entity', () => { numTokens: 0.02, utxoTxid: '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', - utxoVout: 0 + utxoVout: 0, + orderStatus: 'posted' }, timestamp: '2021-09-20T17:54:26.395Z', localTimeStamp: '9/20/2021, 10:54:26 AM', diff --git a/util/offers/deleteOffers.js b/util/offers/deleteOffers.js new file mode 100644 index 0000000..a3087f4 --- /dev/null +++ b/util/offers/deleteOffers.js @@ -0,0 +1,37 @@ +/* + This script has not been customized for offers yet. +*/ + +const mongoose = require('mongoose') + +// Force test environment +// make sure environment variable is set before this file gets called. +// see test script in package.json. +// process.env.KOA_ENV = 'test' +const config = require('../../config') + +const User = require('../../src/models/users') + +async function deleteUsers () { + // Connect to the Mongo Database. + mongoose.Promise = global.Promise + mongoose.set('useCreateIndex', true) // Stop deprecation warning. + await mongoose.connect(config.database, { + useUnifiedTopology: true, + useNewUrlParser: true + }) + + // Get all the users in the DB. + const users = await User.find({}, '-password') + // console.log(`users: ${JSON.stringify(users, null, 2)}`) + + // Delete each user. + for (let i = 0; i < users.length; i++) { + const thisUser = users[i] + await thisUser.remove() + } + + mongoose.connection.close() +} + +deleteUsers() diff --git a/util/offers/getOffers.js b/util/offers/getOffers.js new file mode 100644 index 0000000..a7c7f84 --- /dev/null +++ b/util/offers/getOffers.js @@ -0,0 +1,25 @@ +/* + Get all Offers in the database. +*/ + +const mongoose = require('mongoose') + +const config = require('../../config') + +const Offer = require('../../src/adapters/localdb/models/offer') + +async function getOffers () { + // Connect to the Mongo Database. + mongoose.Promise = global.Promise + mongoose.set('useCreateIndex', true) // Stop deprecation warning. + await mongoose.connect(config.database, { + useNewUrlParser: true, + useUnifiedTopology: true + }) + + const offers = await Offer.find({}) + console.log(`offers: ${JSON.stringify(offers, null, 2)}`) + + mongoose.connection.close() +} +getOffers()