fix(offer): Added entity biz logic and unit tests for offer status

This commit is contained in:
Chris Troutner
2022-03-21 11:39:23 -07:00
parent dcaa73a22f
commit 1563a740aa
10 changed files with 137 additions and 19 deletions
+4
View File
@@ -72,6 +72,10 @@ Offer entities have the following properties:
- _minUnitsToExchange_ - The minimum order size accepted. - _minUnitsToExchange_ - The minimum order size accepted.
- _p2wdbTxid_ - The TXID proof-of-burn used to add the order to the P2WDB. - _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. - _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: - Authentication Data:
- _signature_ - A message signed by the address which created the order. - _signature_ - A message signed by the address which created the order.
+1
View File
@@ -14,6 +14,7 @@ const Offer = new mongoose.Schema({
minUnitsToExchange: { type: String }, minUnitsToExchange: { type: String },
p2wdbTxid: { type: String }, p2wdbTxid: { type: String },
p2wdbHash: { type: String }, p2wdbHash: { type: String },
offerStatus: { type: String },
// Authentication data // Authentication data
signature: { type: String }, signature: { type: String },
+3 -7
View File
@@ -2,7 +2,7 @@
REST API Controller library for the /offer route REST API Controller library for the /offer route
*/ */
// const { wlogger } = require('../../../adapters/wlogger') const { wlogger } = require('../../../adapters/wlogger')
let _this let _this
@@ -65,19 +65,15 @@ class OfferRESTControllerLib {
// listed in the offer. // listed in the offer.
async takeOffer (ctx) { async takeOffer (ctx) {
try { try {
console.log('body: ', ctx.request.body) console.log('REST API controller, body: ', ctx.request.body)
const offerCid = ctx.request.body.offerCid 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) const hash = await _this.useCases.offer.takeOffer(offerCid)
ctx.body = { hash } ctx.body = { hash }
} catch (err) { } catch (err) {
console.log('Error in takeOffer REST API handler.') wlogger.error('Error in takeOffer() REST API handler.')
_this.handleError(ctx, err) _this.handleError(ctx, err)
} }
} }
+2 -2
View File
@@ -6,7 +6,7 @@
class OfferEntity { class OfferEntity {
constructor () { constructor () {
this.orderStatus = ['posted', 'taken', 'completed'] this.orderStatus = ['posted', 'taken', 'dead']
} }
validate (orderData = {}) { validate (orderData = {}) {
@@ -48,7 +48,7 @@ class OfferEntity {
throw new Error("Property 'utxoVout' must be an integer number.") throw new Error("Property 'utxoVout' must be an integer number.")
} }
if (orderStatus && !this.orderStatus.includes(orderStatus)) { 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 = { const validatedOfferData = {
+7 -2
View File
@@ -1,11 +1,16 @@
/* /*
Use Case library for Offers. Use Case library for Offers.
Offers are created by a webhook trigger from the P2WDB. Offers are a result of 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 new data in P2WDB. They differ from Offers, which are generated by a local
user. user.
An Offer is created to match a local Offer, but it's created indirectly, as 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 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. 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 // Local libraries
@@ -86,8 +91,8 @@ class OfferUseCases {
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`) console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
// Ensure the offer is in a 'posted' state and not already 'taken' // Ensure the offer is in a 'posted' state and not already 'taken'
if (offerInfo.offerStatus && offerInfo.offerStatus !== 'posted') { if (!offerInfo.offerStatus || offerInfo.offerStatus !== 'posted') {
throw new Error('offer already taken') 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. // Verify that UTXO for sale is unspent. Abort if it's been spent.
+4 -3
View File
@@ -27,7 +27,7 @@ class OrderLib {
// Create a new order model and add it to the Mongo database. // Create a new order model and add it to the Mongo database.
async createOrder (entryObj) { async createOrder (entryObj) {
try { try {
// console.log('createOrder(entryObj): ', entryObj) console.log('createOrder(entryObj): ', entryObj)
// Input Validation // Input Validation
const orderEntity = this.orderEntity.validate(entryObj) const orderEntity = this.orderEntity.validate(entryObj)
@@ -47,7 +47,6 @@ class OrderLib {
// Update the order with the new UTXO information. // Update the order with the new UTXO information.
orderEntity.utxoTxid = utxoInfo.txid orderEntity.utxoTxid = utxoInfo.txid
orderEntity.utxoVout = utxoInfo.vout orderEntity.utxoVout = utxoInfo.vout
orderEntity.hdIndex = utxoInfo.hdIndex
// Add order to P2WDB. // Add order to P2WDB.
const p2wdbObj = { const p2wdbObj = {
@@ -59,6 +58,7 @@ class OrderLib {
// console.log('hash: ', hash) // console.log('hash: ', hash)
// Create a MongoDB model to hold the Order // Create a MongoDB model to hold the Order
orderEntity.hdIndex = utxoInfo.hdIndex
orderEntity.p2wdbHash = hash orderEntity.p2wdbHash = hash
console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`) console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`)
const order = new this.OrderModel(orderEntity) const order = new this.OrderModel(orderEntity)
@@ -67,7 +67,8 @@ class OrderLib {
return hash return hash
} catch (err) { } catch (err) {
// console.log("Error in use-cases/entry.js/createEntry()", err.message) // 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 throw err
} }
} }
+3 -3
View File
@@ -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') const axios = require('axios')
@@ -10,9 +10,9 @@ async function start () {
try { try {
const options = { const options = {
method: 'post', method: 'post',
url: `${LOCALHOST}/order/take`, url: `${LOCALHOST}/offer/take`,
data: { data: {
orderCid: 'zdpuAkp98gTuivaNzGP31jTQi3ADXrFA6uANrceQcrQkTXy2j' offerCid: 'zdpuAppDJR57Hrn1mAhsFRssSBp5qNrDT6PcSeHL5ndQqiqJc'
} }
} }
+50 -1
View File
@@ -25,6 +25,8 @@ describe('#Offer-Entity', () => {
it('should throw an error if data is not provided', () => { it('should throw an error if data is not provided', () => {
try { try {
uut.validate() uut.validate()
assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -38,6 +40,8 @@ describe('#Offer-Entity', () => {
try { try {
const orderData = { data: {} } const orderData = { data: {} }
uut.validate(orderData) uut.validate(orderData)
assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -51,6 +55,8 @@ describe('#Offer-Entity', () => {
try { try {
const orderData = { data: { messageType: 1 } } const orderData = { data: { messageType: 1 } }
uut.validate(orderData) uut.validate(orderData)
assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -64,6 +70,8 @@ describe('#Offer-Entity', () => {
try { try {
const orderData = { data: { messageType: 1, messageClass: 1 } } const orderData = { data: { messageType: 1, messageClass: 1 } }
uut.validate(orderData) uut.validate(orderData)
assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include(err.message, "Property 'tokenId' must be a string.") assert.include(err.message, "Property 'tokenId' must be a string.")
@@ -77,6 +85,8 @@ describe('#Offer-Entity', () => {
} }
uut.validate(orderData) uut.validate(orderData)
assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include(err.message, "Property 'buyOrSell' must be a string.") assert.include(err.message, "Property 'buyOrSell' must be a string.")
@@ -95,6 +105,8 @@ describe('#Offer-Entity', () => {
} }
uut.validate(orderData) uut.validate(orderData)
assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -117,6 +129,8 @@ describe('#Offer-Entity', () => {
} }
uut.validate(orderData) uut.validate(orderData)
assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -139,6 +153,8 @@ describe('#Offer-Entity', () => {
} }
} }
uut.validate(orderData) uut.validate(orderData)
assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include(err.message, "Property 'numTokens' must be a number.") assert.include(err.message, "Property 'numTokens' must be a number.")
@@ -159,6 +175,8 @@ describe('#Offer-Entity', () => {
} }
} }
uut.validate(orderData) uut.validate(orderData)
assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include(err.message, "Property 'utxoTxid' must be a string.") assert.include(err.message, "Property 'utxoTxid' must be a string.")
@@ -180,6 +198,8 @@ describe('#Offer-Entity', () => {
} }
} }
uut.validate(orderData) uut.validate(orderData)
assert.fail('Unexpected code path')
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( 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', () => { it('should validate a new order', () => {
const orderObj = { const orderObj = {
appId: 'swapTest555', appId: 'swapTest555',
@@ -203,7 +251,8 @@ describe('#Offer-Entity', () => {
numTokens: 0.02, numTokens: 0.02,
utxoTxid: utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0 utxoVout: 0,
orderStatus: 'posted'
}, },
timestamp: '2021-09-20T17:54:26.395Z', timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM', localTimeStamp: '9/20/2021, 10:54:26 AM',
+37
View File
@@ -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()
+25
View File
@@ -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()