Merge pull request #7 from Permissionless-Software-Foundation/ct-unstable

feat(order/offer): Refactored code to switch Order and Offer terminology
This commit is contained in:
Chris Troutner
2022-03-21 09:56:50 -07:00
committed by GitHub
21 changed files with 610 additions and 619 deletions
+8
View File
@@ -0,0 +1,8 @@
[
{
"srcDir": "",
"destDir": "",
"files": "**/*.js",
"command": "npm run lint"
}
]
+2 -2
View File
@@ -94,13 +94,13 @@ class Server {
try { try {
try { try {
// Delete an old webhook if it exists. // Delete an old webhook if it exists.
await webhookLib.deleteWebhook(`http://localhost:${config.port}/order`) await webhookLib.deleteWebhook(`http://localhost:${config.port}/offer`)
} catch (err) { } catch (err) {
/* exit quietly */ /* exit quietly */
// console.log('err deleting webhook: ', err) // console.log('err deleting webhook: ', err)
} }
await webhookLib.createWebhook(`http://localhost:${config.port}/order`) await webhookLib.createWebhook(`http://localhost:${config.port}/offer`)
console.log('Webhook created') console.log('Webhook created')
} catch (error) { } catch (error) {
console.log('Webhook cant be created') console.log('Webhook cant be created')
+6 -1
View File
@@ -1,3 +1,9 @@
/*
Order Model. Orders are 'internal' to the system and track the HD wallet index
that contains funds for that Order. This is in contrast to Offers, which
are 'external' to the system and mirrored by every instance of bch-dex.
*/
const mongoose = require('mongoose') const mongoose = require('mongoose')
const Order = new mongoose.Schema({ const Order = new mongoose.Schema({
@@ -27,7 +33,6 @@ const Order = new mongoose.Schema({
lokadId: { type: String }, lokadId: { type: String },
messageType: { type: Number }, messageType: { type: Number },
messageClass: { type: Number } messageClass: { type: Number }
}) })
module.exports = mongoose.model('order', Order) module.exports = mongoose.model('order', Order)
+40 -6
View File
@@ -32,13 +32,15 @@ class OfferRESTControllerLib {
// No api-doc documentation because this wont be a public endpoint // No api-doc documentation because this wont be a public endpoint
async createOffer (ctx) { async createOffer (ctx) {
try { try {
// console.log('body: ', ctx.request.body) console.log('body: ', ctx.request.body)
const offerObj = ctx.request.body.offer const offerObj = ctx.request.body
const hash = await _this.useCases.offer.createOffer(offerObj) await _this.useCases.offer.createOffer(offerObj)
ctx.body = { hash } ctx.body = {
success: true
}
} catch (err) { } catch (err) {
// console.log(`err.message: ${err.message}`) // console.log(`err.message: ${err.message}`)
// console.log('err: ', err) // console.log('err: ', err)
@@ -47,10 +49,42 @@ class OfferRESTControllerLib {
} }
} }
// curl -X GET http://localhost:5700/offer/list
async listOffers (ctx) {
try {
const offers = await _this.useCases.offer.listOffers()
ctx.body = offers
} catch (err) {
console.log('Error in listOffers REST API handler.')
_this.handleError(ctx, err)
}
}
// Currently only supports 'sell' offers, and will only buy the 'numTokens'
// listed in the offer.
async takeOffer (ctx) {
try {
console.log('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.')
_this.handleError(ctx, err)
}
}
// DRY error handler // DRY error handler
handleError (ctx, err) { handleError (ctx, err) {
console.log('err', err) console.log('err', err.message)
// If an HTTP status is specified by the buisiness logic, use that. // If an HTTP status is specified by the buisiness logic, use that.
if (err.status) { if (err.status) {
if (err.message) { if (err.message) {
+2
View File
@@ -50,6 +50,8 @@ class OfferRouter {
// Define the routes and attach the controller. // Define the routes and attach the controller.
this.router.post('/', _this.offerRESTController.createOffer) this.router.post('/', _this.offerRESTController.createOffer)
this.router.get('/list', _this.offerRESTController.listOffers)
this.router.post('/take', _this.offerRESTController.takeOffer)
// Attach the Controller routes to the Koa app. // Attach the Controller routes to the Koa app.
app.use(_this.router.routes()) app.use(_this.router.routes())
+7 -41
View File
@@ -1,5 +1,5 @@
/* /*
REST API Controller library for the /offer route REST API Controller library for the /order route
*/ */
// const { wlogger } = require('../../../adapters/wlogger') // const { wlogger } = require('../../../adapters/wlogger')
@@ -32,15 +32,13 @@ class OrderRESTControllerLib {
// No api-doc documentation because this wont be a public endpoint // No api-doc documentation because this wont be a public endpoint
async createOrder (ctx) { async createOrder (ctx) {
try { try {
console.log('body: ', ctx.request.body) // console.log('body: ', ctx.request.body)
const orderObj = ctx.request.body const orderObj = ctx.request.body.order
await _this.useCases.order.createOrder(orderObj) const hash = await _this.useCases.order.createOrder(orderObj)
ctx.body = { ctx.body = { hash }
success: true
}
} catch (err) { } catch (err) {
// console.log(`err.message: ${err.message}`) // console.log(`err.message: ${err.message}`)
// console.log('err: ', err) // console.log('err: ', err)
@@ -49,42 +47,10 @@ class OrderRESTControllerLib {
} }
} }
// curl -X GET http://localhost:5700/order/list
async listOrders (ctx) {
try {
const orders = await _this.useCases.order.listOrders()
ctx.body = orders
} catch (err) {
console.log('Error in listOrders REST API handler.')
_this.handleError(ctx, err)
}
}
// Currently only supports 'sell' orders, and will only buy the 'numTokens'
// listed in the order.
async takeOrder (ctx) {
try {
console.log('body: ', ctx.request.body)
const orderCid = ctx.request.body.orderCid
// Find the Order.
// const orderEntity = await _this.useCases.order.findOrder(orderId)
// 'Take' the Order.
const hash = await _this.useCases.order.takeOrder(orderCid)
ctx.body = { hash }
} catch (err) {
console.log('Error in takeOrder REST API handler.')
_this.handleError(ctx, err)
}
}
// DRY error handler // DRY error handler
handleError (ctx, err) { handleError (ctx, err) {
console.log('err', err.message) console.log('err', err)
// If an HTTP status is specified by the buisiness logic, use that. // If an HTTP status is specified by the buisiness logic, use that.
if (err.status) { if (err.status) {
if (err.message) { if (err.message) {
+1 -3
View File
@@ -1,5 +1,5 @@
/* /*
REST API library for /offer route. REST API library for /order route.
*/ */
// Public npm libraries. // Public npm libraries.
@@ -50,8 +50,6 @@ class OrderRouter {
// Define the routes and attach the controller. // Define the routes and attach the controller.
this.router.post('/', _this.orderRESTController.createOrder) this.router.post('/', _this.orderRESTController.createOrder)
this.router.get('/list', _this.orderRESTController.listOrders)
this.router.post('/take', _this.orderRESTController.takeOrder)
// Attach the Controller routes to the Koa app. // Attach the Controller routes to the Koa app.
app.use(_this.router.routes()) app.use(_this.router.routes())
+37 -20
View File
@@ -1,22 +1,23 @@
/* /*
Offer Entity Offer Entity
An Offer Entity is nearly the same as an Order. But while an Order is generated An ffer is created when a new Signal is detected via the P2WDB webhook.
by a webhook from P2WDB, the Offer Entity is created internally. It is used It's destroyed when the UTXO described in the Signal has been detected as spent.
to track an Order generated by this application.
The Offer tracks the hdIndex address used to hold tokens or BCH for sale.
*/ */
class Offer {
validate (data) { class OfferEntity {
const { constructor () {
messageType, this.orderStatus = ['posted', 'taken', 'completed']
messageClass, }
tokenId,
buyOrSell, validate (orderData = {}) {
rateInSats, // Throw an error if input object does not have a data property
minSatsToExchange, if (!orderData.data) {
numTokens throw new Error(
} = data 'Input to order.validate() must be an object with a data property.'
)
}
const { messageType, messageClass, tokenId, buyOrSell, rateInSats, minSatsToExchange, numTokens, utxoTxid, utxoVout, orderStatus } = orderData.data
// Input Validation // Input Validation
if (!messageType || typeof messageType !== 'number') { if (!messageType || typeof messageType !== 'number') {
@@ -40,19 +41,35 @@ class Offer {
if (!numTokens || typeof numTokens !== 'number') { if (!numTokens || typeof numTokens !== 'number') {
throw new Error("Property 'numTokens' must be a number.") throw new Error("Property 'numTokens' must be a number.")
} }
if (!utxoTxid || typeof utxoTxid !== 'string') {
throw new Error("Property 'utxoTxid' must be a string.")
}
if (typeof utxoVout !== 'number') {
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")
}
const offerData = { const validatedOfferData = {
messageType, messageType,
messageClass, messageClass,
tokenId, tokenId,
buyOrSell, buyOrSell,
rateInSats, rateInSats,
minSatsToExchange, minSatsToExchange,
numTokens numTokens,
utxoTxid,
utxoVout,
timestamp: orderData.timestamp,
localTimestamp: orderData.localTimeStamp,
txid: orderData.txid,
p2wdbHash: orderData.hash,
orderStatus: orderStatus || this.orderStatus[0]
} }
return offerData return validatedOfferData
} }
} }
module.exports = Offer module.exports = OfferEntity
-24
View File
@@ -1,24 +0,0 @@
/*
Order Entity
An order is created when a new Signal is detected via the P2WDB webhook.
It's destroyed when the UTXO described in the Signal has been detected as spent.
{
lokadId: 'SWP', // Placeholder for now, use for backwards compatibility
messageType: 1, // SLP Atomic Swap
messageClass: 1,
tokenId: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInSats: 7972, // Price per token in sats
minSatsToExchange: 8100, // Minum size of UTXO to use, e.g. 1 token + miner fees
// Signature from the address holding the tokens, provides 'proof of reserves'
signature: 'H2Sq0UPh0jgs1Zt3JERHtbzfPGXJk9DgJ0FVxVa6iUqiIh6XcvEUFBbvYIuODQs3hYSCkkjcuzbvzNEiv69kFKg=',
sigMsg: 'test',
address: 'bitcoincash:qphjncqpnv444jq8acqk4dkm3296c50xhqggeatvn8',
// UTXO for sale
utxoTxid: 'b9457808be70c39a9cc6c5857cbef856b35fdc91a59debfe06acfc45b11955e3',
utxoVout: 2
}
*/
+20 -36
View File
@@ -1,22 +1,22 @@
/* /*
Order Entity Order Entity
An order is created when a new Signal is detected via the P2WDB webhook. An Order Entity is nearly the same as an Offer. But while an Offer is generated
It's destroyed when the UTXO described in the Signal has been detected as spent. by a webhook from P2WDB, the Order Entity is created internally. It is used
to track an Order generated by this application.
The Order tracks the hdIndex address used to hold tokens or BCH for sale.
*/ */
class OrderEntity { class Order {
constructor () { validate (data) {
this.orderStatus = ['posted', 'taken', 'completed'] const {
} messageType,
messageClass,
validate (orderData = {}) { tokenId,
// Throw an error if input object does not have a data property buyOrSell,
if (!orderData.data) { rateInSats,
throw new Error( minSatsToExchange,
'Input to order.validate() must be an object with a data property.' numTokens
) } = data
}
const { messageType, messageClass, tokenId, buyOrSell, rateInSats, minSatsToExchange, numTokens, utxoTxid, utxoVout, orderStatus } = orderData.data
// Input Validation // Input Validation
if (!messageType || typeof messageType !== 'number') { if (!messageType || typeof messageType !== 'number') {
@@ -40,35 +40,19 @@ class OrderEntity {
if (!numTokens || typeof numTokens !== 'number') { if (!numTokens || typeof numTokens !== 'number') {
throw new Error("Property 'numTokens' must be a number.") throw new Error("Property 'numTokens' must be a number.")
} }
if (!utxoTxid || typeof utxoTxid !== 'string') {
throw new Error("Property 'utxoTxid' must be a string.")
}
if (typeof utxoVout !== 'number') {
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")
}
const validatedOrderData = { const offerData = {
messageType, messageType,
messageClass, messageClass,
tokenId, tokenId,
buyOrSell, buyOrSell,
rateInSats, rateInSats,
minSatsToExchange, minSatsToExchange,
numTokens, numTokens
utxoTxid,
utxoVout,
timestamp: orderData.timestamp,
localTimestamp: orderData.localTimeStamp,
txid: orderData.txid,
p2wdbHash: orderData.hash,
orderStatus: orderStatus || this.orderStatus[0]
} }
return validatedOrderData return offerData
} }
} }
module.exports = OrderEntity module.exports = Order
@@ -1,46 +1,46 @@
/* /*
Use Case library for Orders. Use Case library for Offers.
Orders are created by a webhook trigger from the P2WDB. Orders 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 Order 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, Orders generated from a response to the webhook from the P2WDB. In this way, Offers generated from
local Offers are no different than Orders generated by other peers. local Offers are no different than Offers generated by other peers.
*/ */
// Local libraries // Local libraries
const OrderEntity = require('../../entities/order') const OfferEntity = require('../../entities/offer')
const config = require('../../../config') const config = require('../../../config')
class OrderUseCases { class OfferUseCases {
constructor (localConfig = {}) { constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig) // console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters this.adapters = localConfig.adapters
if (!this.adapters) { if (!this.adapters) {
throw new Error( throw new Error(
'Instance of adapters must be passed in when instantiating Order Use Cases library.' 'Instance of adapters must be passed in when instantiating Offer Use Cases library.'
) )
} }
// Encapsulate dependencies // Encapsulate dependencies
this.config = config this.config = config
this.orderEntity = new OrderEntity() this.offerEntity = new OfferEntity()
this.OrderModel = this.adapters.localdb.Order this.OfferModel = this.adapters.localdb.Offer
} }
// This method is called by the POST /order REST API controller, which is // This method is called by the POST /offer REST API controller, which is
// triggered by a P2WDB webhook. // triggered by a P2WDB webhook.
async createOrder (orderObj) { async createOffer (offerObj) {
try { try {
console.log('Use Case createOrder(orderObj): ', orderObj) console.log('Use Case createOffer(offerObj): ', offerObj)
// console.log('this.adapters.bchjs: ', this.adapters.bchjs) // console.log('this.adapters.bchjs: ', this.adapters.bchjs)
// Verify that UTXO in order is unspent. If it is spent, then ignore the // Verify that UTXO in offer is unspent. If it is spent, then ignore the
// order. // offer.
const txid = orderObj.data.utxoTxid const txid = offerObj.data.utxoTxid
const vout = orderObj.data.utxoVout const vout = offerObj.data.utxoVout
const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut( const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
txid, txid,
vout vout
@@ -48,51 +48,51 @@ class OrderUseCases {
console.log('utxoStatus: ', utxoStatus) console.log('utxoStatus: ', utxoStatus)
if (utxoStatus === null) return false if (utxoStatus === null) return false
// A new order gets a status of 'posted' // A new offer gets a status of 'posted'
orderObj.data.orderStatus = 'posted' offerObj.data.offerStatus = 'posted'
const orderEntity = this.orderEntity.validate(orderObj) const offerEntity = this.offerEntity.validate(offerObj)
console.log('orderEntity: ', orderEntity) console.log('offerEntity: ', offerEntity)
// Add order to the local database. // Add offer to the local database.
const orderModel = new this.OrderModel(orderEntity) const offerModel = new this.OfferModel(offerEntity)
await orderModel.save() await offerModel.save()
return true return true
} catch (err) { } catch (err) {
console.error('Error in createOrder()') console.error('Error in createOffer()')
throw err throw err
} }
} }
async listOrders () { async listOffers () {
try { try {
return this.OrderModel.find({}) return this.OfferModel.find({})
} catch (error) { } catch (error) {
console.error('Error in use-cases/order/listOrders()') console.error('Error in use-cases/offer/listOffers()')
throw error throw error
} }
} }
// Generate phase 2 of 3 - take the other side of an Order. // Generate phase 2 of 3 - take the other side of an Offer.
// Based on this example: // Based on this example:
// https://github.com/Permissionless-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js // https://github.com/Permissionless-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js
async takeOrder (orderCid) { async takeOffer (offerCid) {
try { try {
console.log('orderCid: ', orderCid) console.log('offerCid: ', offerCid)
// Get the Order information // Get the Offer information
const orderInfo = await this.findOrderByHash(orderCid) const offerInfo = await this.findOfferByHash(offerCid)
console.log(`orderInfo: ${JSON.stringify(orderInfo, null, 2)}`) console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
// Ensure the order is in a 'posted' state and not already 'taken' // Ensure the offer is in a 'posted' state and not already 'taken'
if (orderInfo.orderStatus && orderInfo.orderStatus !== 'posted') { if (offerInfo.offerStatus && offerInfo.offerStatus !== 'posted') {
throw new Error('order already taken') throw new Error('offer already taken')
} }
// 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.
const txid = orderInfo.utxoTxid const txid = offerInfo.utxoTxid
const vout = orderInfo.utxoVout const vout = offerInfo.utxoVout
const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut( const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
txid, txid,
vout vout
@@ -104,32 +104,32 @@ class OrderUseCases {
} }
// Ensure the app has enough funds to complete the trade. // Ensure the app has enough funds to complete the trade.
await this.ensureFunds(orderInfo) await this.ensureFunds(offerInfo)
// Get UTXOs. // Get UTXOs.
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
// TODO: Move funds to create a segrated UTXO for taking the order // TODO: Move funds to create a segrated UTXO for taking the offer
// Create a partially signed transaction. // Create a partially signed transaction.
// https://github.com/Permissionless-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js#L59 // https://github.com/Permissionless-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js#L59
const partialTxHex = await this.adapters.wallet.generatePartialTx(orderInfo) const partialTxHex = await this.adapters.wallet.generatePartialTx(offerInfo)
// return partialTxHex // return partialTxHex
// Create valid Order object // Create valid Offer object
const takenOrderInfo = Object.assign({}, orderInfo) const takenOfferInfo = Object.assign({}, offerInfo)
takenOrderInfo.patialTxHex = partialTxHex takenOfferInfo.patialTxHex = partialTxHex
delete takenOrderInfo.p2wdbHash delete takenOfferInfo.p2wdbHash
delete takenOrderInfo._id delete takenOfferInfo._id
takenOrderInfo.offerHash = orderInfo.p2wdbHash takenOfferInfo.offerHash = offerInfo.p2wdbHash
// Write order info to the P2WDB // Write offer info to the P2WDB
// TODO: This will trigger the webhook. Find some way of triggering the // TODO: This will trigger the webhook. Find some way of triggering the
// webhook on new orders, but not on counteroffers // webhook on new offers, but not on counteroffers
const p2wdbObj = { const p2wdbObj = {
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
data: takenOrderInfo, data: takenOfferInfo,
appId: this.config.p2wdbAppId appId: this.config.p2wdbAppId
} }
const hash = await this.adapters.p2wdb.write(p2wdbObj) const hash = await this.adapters.p2wdb.write(p2wdbObj)
@@ -137,14 +137,14 @@ class OrderUseCases {
// Return the P2WDB CID // Return the P2WDB CID
return hash return hash
} catch (err) { } catch (err) {
console.error('Error in use-cases/order/takeOrder(): ', err) console.error('Error in use-cases/offer/takeOffer(): ', err)
throw err throw err
} }
} }
// Ensure that the wallet has enough BCH and tokens to complete the requested // Ensure that the wallet has enough BCH and tokens to complete the requested
// trade. Will return true if it does. Will throw an error if it doesn't. // trade. Will return true if it does. Will throw an error if it doesn't.
async ensureFunds (orderEntity) { async ensureFunds (offerEntity) {
try { try {
// console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet) // console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet)
// console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`) // console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`)
@@ -156,11 +156,11 @@ class OrderUseCases {
const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif) const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.') if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
if (orderEntity.buyOrSell.includes('sell')) { if (offerEntity.buyOrSell.includes('sell')) {
// Sell Offer // Sell Offer
// Ensure the app wallet controlls enough BCH to pay for the tokens. // Ensure the app wallet controlls enough BCH to pay for the tokens.
const satsNeeded = orderEntity.numTokens * parseInt(orderEntity.rateInSats) const satsNeeded = offerEntity.numTokens * parseInt(offerEntity.rateInSats)
const balance = await this.adapters.wallet.bchWallet.getBalance() const balance = await this.adapters.wallet.bchWallet.getBalance()
console.log(`wallet balance: ${balance}, sats needed: ${satsNeeded}`) console.log(`wallet balance: ${balance}, sats needed: ${satsNeeded}`)
const SATS_MARGIN = 5000 const SATS_MARGIN = 5000
@@ -169,7 +169,7 @@ class OrderUseCases {
// //
} else { } else {
// Buy Offer // Buy Offer
throw new Error('Buy orders are not supported yet.') throw new Error('Buy offers are not supported yet.')
} }
return true return true
@@ -179,27 +179,27 @@ class OrderUseCases {
} }
} }
async findOrderByHash (p2wdbHash) { async findOfferByHash (p2wdbHash) {
try { try {
if (typeof p2wdbHash !== 'string' || !p2wdbHash) { if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
throw new Error('p2wdbHash must be a string') throw new Error('p2wdbHash must be a string')
} }
const order = await this.OrderModel.findOne({ p2wdbHash }) const offer = await this.OfferModel.findOne({ p2wdbHash })
if (!order) { if (!offer) {
throw new Error('order not found') throw new Error('offer not found')
} }
const orderObject = order.toObject() const offerObject = offer.toObject()
// return this.orderEntity.validateFromModel(orderObject) // return this.offerEntity.validateFromModel(offerObject)
return orderObject return offerObject
} catch (err) { } catch (err) {
console.error('Error in findOrder(): ', err) console.error('Error in findOffer(): ', err)
throw err throw err
} }
} }
} }
module.exports = OrderUseCases module.exports = OfferUseCases
@@ -1,89 +1,89 @@
/* /*
Offer use-case library. Order use-case library.
*/ */
// Local libraries // Local libraries
const { wlogger } = require('../adapters/wlogger') const { wlogger } = require('../adapters/wlogger')
const OfferEntity = require('../entities/offer') const OrderEntity = require('../entities/order')
const config = require('../../config') const config = require('../../config')
class OfferLib { class OrderLib {
constructor (localConfig = {}) { constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig) // console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters this.adapters = localConfig.adapters
if (!this.adapters) { if (!this.adapters) {
throw new Error( throw new Error(
'Instance of adapters must be passed in when instantiating Offer Use Cases library.' 'Instance of adapters must be passed in when instantiating Order Use Cases library.'
) )
} }
// Encapsulate dependencies // Encapsulate dependencies
this.offerEntity = new OfferEntity() this.orderEntity = new OrderEntity()
this.OfferModel = this.adapters.localdb.Offer this.OrderModel = this.adapters.localdb.Order
this.bch = this.adapters.bch this.bch = this.adapters.bch
this.config = config this.config = config
} }
// Create a new offer model and add it to the Mongo database. // Create a new order model and add it to the Mongo database.
async createOffer (entryObj) { async createOrder (entryObj) {
try { try {
// console.log('createOffer(entryObj): ', entryObj) // console.log('createOrder(entryObj): ', entryObj)
// Input Validation // Input Validation
const offerEntity = this.offerEntity.validate(entryObj) const orderEntity = this.orderEntity.validate(entryObj)
console.log('offerEntity: ', offerEntity) console.log('orderEntity: ', orderEntity)
// Ensure sufficient tokens exist to create the offer. // Ensure sufficient tokens exist to create the order.
await this.ensureFunds(offerEntity) await this.ensureFunds(orderEntity)
// Move the tokens to holding address. // Move the tokens to holding address.
const utxoInfo = await this.moveTokens(offerEntity) const utxoInfo = await this.moveTokens(orderEntity)
console.log('utxoInfo: ', utxoInfo) console.log('utxoInfo: ', utxoInfo)
// Update the UTXO store for the wallet. // Update the UTXO store for the wallet.
await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000) await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000)
await this.adapters.wallet.bchWallet.getUtxos() await this.adapters.wallet.bchWallet.getUtxos()
// Update the offer with the new UTXO information. // Update the order with the new UTXO information.
offerEntity.utxoTxid = utxoInfo.txid orderEntity.utxoTxid = utxoInfo.txid
offerEntity.utxoVout = utxoInfo.vout orderEntity.utxoVout = utxoInfo.vout
offerEntity.hdIndex = utxoInfo.hdIndex orderEntity.hdIndex = utxoInfo.hdIndex
// Add offer to P2WDB. // Add order to P2WDB.
const p2wdbObj = { const p2wdbObj = {
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
data: offerEntity, data: orderEntity,
appId: this.config.p2wdbAppId appId: this.config.p2wdbAppId
} }
const hash = await this.adapters.p2wdb.write(p2wdbObj) const hash = await this.adapters.p2wdb.write(p2wdbObj)
// console.log('hash: ', hash) // console.log('hash: ', hash)
// Create a MongoDB model to hold the Offer // Create a MongoDB model to hold the Order
offerEntity.p2wdbHash = hash orderEntity.p2wdbHash = hash
console.log(`creating new offer model: ${JSON.stringify(offerEntity, null, 2)}`) console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`)
const offer = new this.OfferModel(offerEntity) const order = new this.OrderModel(orderEntity)
await offer.save() await order.save()
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/createOffer())') wlogger.error('Error in use-cases/entry.js/createOrder())')
throw err throw err
} }
} }
// Move the tokens indicated in the offer to a temporary holding address. // Move the tokens indicated in the order to a temporary holding address.
// This will generate the UTXO used in the Signal message. This function // This will generate the UTXO used in the Signal message. This function
// moves the funds and returns the UTXO information. // moves the funds and returns the UTXO information.
async moveTokens (offerEntity) { async moveTokens (orderEntity) {
try { try {
const keyPair = await this.adapters.wallet.getKeyPair() const keyPair = await this.adapters.wallet.getKeyPair()
console.log('keyPair: ', keyPair) console.log('keyPair: ', keyPair)
const receiver = { const receiver = {
address: keyPair.cashAddress, address: keyPair.cashAddress,
tokenId: offerEntity.tokenId, tokenId: orderEntity.tokenId,
qty: offerEntity.numTokens qty: orderEntity.numTokens
} }
const txid = await this.adapters.wallet.bchWallet.sendTokens(receiver, 3) const txid = await this.adapters.wallet.bchWallet.sendTokens(receiver, 3)
@@ -103,7 +103,7 @@ class OfferLib {
// Ensure that the wallet has enough BCH and tokens to complete the requested // Ensure that the wallet has enough BCH and tokens to complete the requested
// trade. // trade.
async ensureFunds (offerEntity) { async ensureFunds (orderEntity) {
try { try {
// console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet) // console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet)
// console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`) // console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`)
@@ -117,32 +117,32 @@ class OfferLib {
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
if (offerEntity.buyOrSell.includes('sell')) { if (orderEntity.buyOrSell.includes('sell')) {
// Sell Offer // Sell Order
// Get token UTXOs that match the token in the offer. // Get token UTXOs that match the token in the order.
const tokenUtxos = utxos.slpUtxos.type1.tokens.filter( const tokenUtxos = utxos.slpUtxos.type1.tokens.filter(
x => x.tokenId === offerEntity.tokenId x => x.tokenId === orderEntity.tokenId
) )
// console.log('tokenUtxos: ', tokenUtxos) // console.log('tokenUtxos: ', tokenUtxos)
// Get the total amount of tokens in the wallet that match the token // Get the total amount of tokens in the wallet that match the token
// in the offer. // in the order.
let totalTokenBalance = 0 let totalTokenBalance = 0
tokenUtxos.map(x => (totalTokenBalance += parseFloat(x.qtyStr))) tokenUtxos.map(x => (totalTokenBalance += parseFloat(x.qtyStr)))
console.log('totalTokenBalance: ', totalTokenBalance) console.log('totalTokenBalance: ', totalTokenBalance)
// If there are fewer tokens in the wallet than what's in the offer, // If there are fewer tokens in the wallet than what's in the order,
// throw an error. // throw an error.
if (totalTokenBalance <= offerEntity.numTokens || isNaN(totalTokenBalance)) { if (totalTokenBalance <= orderEntity.numTokens || isNaN(totalTokenBalance)) {
throw new Error( throw new Error(
'App wallet does not have enough tokens to satisfy the SELL offer.' 'App wallet does not have enough tokens to satisfy the SELL order.'
) )
} }
// //
} else { } else {
// Buy Offer // Buy Order
throw new Error('Buy orders are not supported yet.') throw new Error('Buy orders are not supported yet.')
} }
@@ -154,4 +154,4 @@ class OfferLib {
} }
} }
module.exports = OfferLib module.exports = OrderLib
+5 -4
View File
@@ -1,5 +1,6 @@
/* /*
A manual e2e test for creating an swap offer. A manual e2e test for creating an Order, which then generates an Offer through
the P2WDB webhook.
Ensure the REST API is up an running before running this test. Ensure the REST API is up an running before running this test.
*/ */
@@ -10,7 +11,7 @@ const LOCALHOST = 'http://localhost:5700'
async function start () { async function start () {
try { try {
const mockOffer = { const mockOrder = {
lokadId: 'SWP', lokadId: 'SWP',
messageType: 1, messageType: 1,
messageClass: 1, messageClass: 1,
@@ -24,8 +25,8 @@ async function start () {
const options = { const options = {
method: 'post', method: 'post',
url: `${LOCALHOST}/offer`, url: `${LOCALHOST}/order`,
data: { offer: mockOffer } data: { order: mockOrder }
} }
const result = await axios(options) const result = await axios(options)
@@ -64,36 +64,53 @@ describe('#Offer-REST-Router', () => {
describe('#createOffer', () => { describe('#createOffer', () => {
it('should create a new offer', async () => { it('should create a new offer', async () => {
ctx.request.body = { ctx.request.body = {
offer: {} appId: 'swapTest555',
data: {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
} }
// Mock dependencies // Mock dependencies
sandbox.stub(uut.useCases.offer, 'createOffer').resolves('testHash') // sandbox.stub(uut.useCases.offer, 'createOffer').resolves()
await uut.createOffer(ctx) await uut.createOffer(ctx)
assert.equal(ctx.body.hash, 'testHash') // assert.equal(ctx.body.hash, 'testHash')
}) })
it('should catch and throw an error', async () => { // it('should catch and throw an error', async () => {
try { // try {
ctx.request.body = { // ctx.request.body = {
offer: {} // offer: {}
} // }
//
// Force an error // // Force an error
sandbox // sandbox
.stub(uut.useCases.offer, 'createOffer') // .stub(uut.useCases.offer, 'createOffer')
.rejects(new Error('test error')) // .rejects(new Error('test error'))
//
await uut.createOffer(ctx) // await uut.createOffer(ctx)
//
assert.fail('Unexpected code path') // assert.fail('Unexpected code path')
} catch (err) { // } catch (err) {
// console.log('err: ', err) // // console.log('err: ', err)
assert.include(err.message, 'test error') // assert.include(err.message, 'test error')
} // }
}) // })
}) })
describe('#handleError', () => { describe('#handleError', () => {
@@ -64,53 +64,36 @@ describe('#Order-REST-Router', () => {
describe('#createOrder', () => { describe('#createOrder', () => {
it('should create a new order', async () => { it('should create a new order', async () => {
ctx.request.body = { ctx.request.body = {
appId: 'swapTest555', order: {}
data: {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
} }
// Mock dependencies // Mock dependencies
// sandbox.stub(uut.useCases.order, 'createOrder').resolves() sandbox.stub(uut.useCases.order, 'createOrder').resolves('testHash')
await uut.createOrder(ctx) await uut.createOrder(ctx)
// assert.equal(ctx.body.hash, 'testHash') assert.equal(ctx.body.hash, 'testHash')
}) })
// it('should catch and throw an error', async () => { it('should catch and throw an error', async () => {
// try { try {
// ctx.request.body = { ctx.request.body = {
// order: {} order: {}
// } }
//
// // Force an error // Force an error
// sandbox sandbox
// .stub(uut.useCases.order, 'createOrder') .stub(uut.useCases.order, 'createOrder')
// .rejects(new Error('test error')) .rejects(new Error('test error'))
//
// await uut.createOrder(ctx) await uut.createOrder(ctx)
//
// assert.fail('Unexpected code path') assert.fail('Unexpected code path')
// } catch (err) { } catch (err) {
// // console.log('err: ', err) // console.log('err: ', err)
// assert.include(err.message, 'test error') assert.include(err.message, 'test error')
// } }
// }) })
}) })
describe('#handleError', () => { describe('#handleError', () => {
+129 -31
View File
@@ -1,5 +1,5 @@
/* /*
Unit tests for the User entity library. Unit tests for the Offer entity library.
*/ */
const assert = require('chai').assert const assert = require('chai').assert
@@ -27,14 +27,17 @@ describe('#Offer-Entity', () => {
uut.validate() uut.validate()
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include(err.message, 'Cannot destructure property') assert.include(
err.message,
'Input to order.validate() must be an object with a data property.'
)
} }
}) })
it('should throw an error if messageType is not included', () => { it('should throw an error if messageType is not included', () => {
try { try {
const data = {} const orderData = { data: {} }
uut.validate(data) uut.validate(orderData)
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -46,8 +49,8 @@ describe('#Offer-Entity', () => {
it('should throw an error if messageClass is not included', () => { it('should throw an error if messageClass is not included', () => {
try { try {
const data = { messageType: 1 } const orderData = { data: { messageType: 1 } }
uut.validate(data) uut.validate(orderData)
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -59,8 +62,8 @@ describe('#Offer-Entity', () => {
it('should throw an error if tokenId is not included', () => { it('should throw an error if tokenId is not included', () => {
try { try {
const data = { messageType: 1, messageClass: 1 } const orderData = { data: { messageType: 1, messageClass: 1 } }
uut.validate(data) uut.validate(orderData)
} 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.")
@@ -69,8 +72,11 @@ describe('#Offer-Entity', () => {
it('should throw an error if buyOrSell is not included', () => { it('should throw an error if buyOrSell is not included', () => {
try { try {
const data = { messageType: 1, messageClass: 1, tokenId: 'fakeId' } const orderData = {
uut.validate(data) data: { messageType: 1, messageClass: 1, tokenId: 'fakeId' }
}
uut.validate(orderData)
} 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.")
@@ -79,13 +85,16 @@ describe('#Offer-Entity', () => {
it('should throw an error if rateInSats is not included', () => { it('should throw an error if rateInSats is not included', () => {
try { try {
const data = { const orderData = {
messageType: 1, data: {
messageClass: 1, messageType: 1,
tokenId: 'fakeId', messageClass: 1,
buyOrSell: 'buy' tokenId: 'fakeId',
buyOrSell: 'buy'
}
} }
uut.validate(data)
uut.validate(orderData)
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -97,14 +106,17 @@ describe('#Offer-Entity', () => {
it('should throw an error if minSatsToExchange is not included', () => { it('should throw an error if minSatsToExchange is not included', () => {
try { try {
const data = { const orderData = {
messageType: 1, data: {
messageClass: 1, messageType: 1,
tokenId: 'fakeId', messageClass: 1,
buyOrSell: 'buy', tokenId: 'fakeId',
rateInSats: 1000 buyOrSell: 'buy',
rateInSats: 1000
}
} }
uut.validate(data)
uut.validate(orderData)
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -116,19 +128,105 @@ describe('#Offer-Entity', () => {
it('should throw an error if numTokens is not included', () => { it('should throw an error if numTokens is not included', () => {
try { try {
const data = { const orderData = {
messageType: 1, data: {
messageClass: 1, messageType: 1,
tokenId: 'fakeId', messageClass: 1,
buyOrSell: 'buy', tokenId: 'fakeId',
rateInSats: 1000, buyOrSell: 'buy',
minSatsToExchange: 350 rateInSats: 1000,
minSatsToExchange: 350
}
} }
uut.validate(data) uut.validate(orderData)
} 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.")
} }
}) })
it('should throw an error if utxoTxid is not included', () => {
try {
const orderData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInSats: 1000,
minSatsToExchange: 350,
numTokens: 1
}
}
uut.validate(orderData)
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'utxoTxid' must be a string.")
}
})
it('should throw an error if utxoVout is not included', () => {
try {
const orderData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInSats: 1000,
minSatsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid'
}
}
uut.validate(orderData)
} catch (err) {
// console.log(err)
assert.include(
err.message,
"Property 'utxoVout' must be an integer number."
)
}
})
it('should validate a new order', () => {
const orderObj = {
appId: 'swapTest555',
data: {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
}
const result = uut.validate(orderObj)
// console.log('result: ', result)
assert.property(result, 'messageType')
assert.property(result, 'messageClass')
assert.property(result, 'tokenId')
assert.property(result, 'buyOrSell')
assert.property(result, 'rateInSats')
assert.property(result, 'minSatsToExchange')
assert.property(result, 'numTokens')
assert.property(result, 'utxoTxid')
assert.property(result, 'utxoVout')
assert.property(result, 'timestamp')
assert.property(result, 'localTimestamp')
assert.property(result, 'txid')
assert.property(result, 'p2wdbHash')
})
}) })
}) })
+31 -129
View File
@@ -1,5 +1,5 @@
/* /*
Unit tests for the Order entity library. Unit tests for the User entity library.
*/ */
const assert = require('chai').assert const assert = require('chai').assert
@@ -27,17 +27,14 @@ describe('#Order-Entity', () => {
uut.validate() uut.validate()
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(err.message, 'Cannot destructure property')
err.message,
'Input to order.validate() must be an object with a data property.'
)
} }
}) })
it('should throw an error if messageType is not included', () => { it('should throw an error if messageType is not included', () => {
try { try {
const orderData = { data: {} } const data = {}
uut.validate(orderData) uut.validate(data)
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -49,8 +46,8 @@ describe('#Order-Entity', () => {
it('should throw an error if messageClass is not included', () => { it('should throw an error if messageClass is not included', () => {
try { try {
const orderData = { data: { messageType: 1 } } const data = { messageType: 1 }
uut.validate(orderData) uut.validate(data)
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -62,8 +59,8 @@ describe('#Order-Entity', () => {
it('should throw an error if tokenId is not included', () => { it('should throw an error if tokenId is not included', () => {
try { try {
const orderData = { data: { messageType: 1, messageClass: 1 } } const data = { messageType: 1, messageClass: 1 }
uut.validate(orderData) uut.validate(data)
} 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.")
@@ -72,11 +69,8 @@ describe('#Order-Entity', () => {
it('should throw an error if buyOrSell is not included', () => { it('should throw an error if buyOrSell is not included', () => {
try { try {
const orderData = { const data = { messageType: 1, messageClass: 1, tokenId: 'fakeId' }
data: { messageType: 1, messageClass: 1, tokenId: 'fakeId' } uut.validate(data)
}
uut.validate(orderData)
} 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.")
@@ -85,16 +79,13 @@ describe('#Order-Entity', () => {
it('should throw an error if rateInSats is not included', () => { it('should throw an error if rateInSats is not included', () => {
try { try {
const orderData = { const data = {
data: { messageType: 1,
messageType: 1, messageClass: 1,
messageClass: 1, tokenId: 'fakeId',
tokenId: 'fakeId', buyOrSell: 'buy'
buyOrSell: 'buy'
}
} }
uut.validate(data)
uut.validate(orderData)
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -106,17 +97,14 @@ describe('#Order-Entity', () => {
it('should throw an error if minSatsToExchange is not included', () => { it('should throw an error if minSatsToExchange is not included', () => {
try { try {
const orderData = { const data = {
data: { messageType: 1,
messageType: 1, messageClass: 1,
messageClass: 1, tokenId: 'fakeId',
tokenId: 'fakeId', buyOrSell: 'buy',
buyOrSell: 'buy', rateInSats: 1000
rateInSats: 1000
}
} }
uut.validate(data)
uut.validate(orderData)
} catch (err) { } catch (err) {
// console.log(err) // console.log(err)
assert.include( assert.include(
@@ -128,105 +116,19 @@ describe('#Order-Entity', () => {
it('should throw an error if numTokens is not included', () => { it('should throw an error if numTokens is not included', () => {
try { try {
const orderData = { const data = {
data: { messageType: 1,
messageType: 1, messageClass: 1,
messageClass: 1, tokenId: 'fakeId',
tokenId: 'fakeId', buyOrSell: 'buy',
buyOrSell: 'buy', rateInSats: 1000,
rateInSats: 1000, minSatsToExchange: 350
minSatsToExchange: 350
}
} }
uut.validate(orderData) uut.validate(data)
} 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.")
} }
}) })
it('should throw an error if utxoTxid is not included', () => {
try {
const orderData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInSats: 1000,
minSatsToExchange: 350,
numTokens: 1
}
}
uut.validate(orderData)
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'utxoTxid' must be a string.")
}
})
it('should throw an error if utxoVout is not included', () => {
try {
const orderData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInSats: 1000,
minSatsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid'
}
}
uut.validate(orderData)
} catch (err) {
// console.log(err)
assert.include(
err.message,
"Property 'utxoVout' must be an integer number."
)
}
})
it('should validate a new order', () => {
const orderObj = {
appId: 'swapTest555',
data: {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
}
const result = uut.validate(orderObj)
// console.log('result: ', result)
assert.property(result, 'messageType')
assert.property(result, 'messageClass')
assert.property(result, 'tokenId')
assert.property(result, 'buyOrSell')
assert.property(result, 'rateInSats')
assert.property(result, 'minSatsToExchange')
assert.property(result, 'numTokens')
assert.property(result, 'utxoTxid')
assert.property(result, 'utxoVout')
assert.property(result, 'timestamp')
assert.property(result, 'localTimestamp')
assert.property(result, 'txid')
assert.property(result, 'p2wdbHash')
})
}) })
}) })
+68 -87
View File
@@ -46,97 +46,78 @@ describe('#offer-use-case', () => {
}) })
}) })
describe('#ensureFunds', () => { describe('#createOffer', () => {
it('should return true if wallet has enough funds for a sell order', async () => { it('should ignore an offer if utxo has been spent', async () => {
const offerEntity = { const offerObj = {
lokadId: 'SWP', appId: 'swapTest555',
messageType: 1, data: {
messageClass: 1, messageType: 1,
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', messageClass: 1,
buyOrSell: 'sell', tokenId:
rateInSats: 1000, '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
minSatsToExchange: 0, buyOrSell: 'sell',
numTokens: 1 rateInSats: 1000,
minSatsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
} }
const result = await uut.ensureFunds(offerEntity) // Mock dependencies
sandbox.stub(uut.adapters.bchjs.Blockchain, 'getTxOut').resolves(null)
const result = await uut.createOffer(offerObj)
// console.log('result: ', result)
assert.equal(result, false)
})
it('should create an offer and return the hash', async () => {
const offerObj = {
appId: 'swapTest555',
data: {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
}
// Mock dependencies
sandbox.stub(uut.adapters.bchjs.Blockchain, 'getTxOut').resolves({
bestblock:
'000000000000000000d2060b83f90f8187b92fcccb4a42aaa19ce5a305fe0ae3',
confirmations: 0,
value: 0,
scriptPubKey: {
asm: 'OP_RETURN 5262419 1 1145980243 38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0 00000000001e8480 0000000009a7ec80',
hex: '6a04534c500001010453454e442038e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b00800000000001e8480080000000009a7ec80',
type: 'nulldata'
},
coinbase: false
})
const result = await uut.createOffer(offerObj)
// console.log('result: ', result)
assert.equal(result, true) assert.equal(result, true)
}) })
}) })
describe('#moveTokens', () => {
it('should move tokens to the holding address', async () => {
// Mock dependencies
// sandbox
// .stub(uut.adapters.wallet.bchWallet, 'sendTokens')
// .resolves('fakeTxid')
const offerEntity = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 0,
numTokens: 1
}
const result = await uut.moveTokens(offerEntity)
// console.log('result: ', result)
assert.property(result, 'txid')
assert.property(result, 'vout')
assert.equal(result.txid, 'fakeTxid')
assert.equal(result.vout, 1)
})
})
describe('#createOffer', () => {
it('should create an offer and return the hash', async () => {
const entryObj = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'token-id',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 1250,
numTokens: 1
}
// Mock dependencies
// sandbox.stub(uut.adapters.wallet, 'burnPsf').resolves('fakeTxid')
sandbox.stub(uut.offerEntity, 'validate').returns(entryObj)
sandbox.stub(uut, 'ensureFunds').resolves()
sandbox.stub(uut, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves()
// sandbox
// .stub(uut.adapters.wallet, 'generateSignature')
// .resolves('fakeSignature')
sandbox.stub(uut.adapters.p2wdb, 'write').resolves('fakeHash')
const result = await uut.createOffer(entryObj)
console.log('result: ', result)
assert.isString(result)
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox
.stub(uut.offerEntity, 'validate')
.throws(new Error('test error'))
await uut.createOffer()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
}) })
+87 -68
View File
@@ -46,78 +46,97 @@ describe('#order-use-case', () => {
}) })
}) })
describe('#createOrder', () => { describe('#ensureFunds', () => {
it('should ignore an offer if utxo has been spent', async () => { it('should return true if wallet has enough funds for a sell order', async () => {
const orderObj = { const orderEntity = {
appId: 'swapTest555', lokadId: 'SWP',
data: { messageType: 1,
messageType: 1, messageClass: 1,
messageClass: 1, tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
tokenId: buyOrSell: 'sell',
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0', rateInSats: 1000,
buyOrSell: 'sell', minSatsToExchange: 0,
rateInSats: 1000, numTokens: 1
minSatsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
} }
// Mock dependencies const result = await uut.ensureFunds(orderEntity)
sandbox.stub(uut.adapters.bchjs.Blockchain, 'getTxOut').resolves(null)
const result = await uut.createOrder(orderObj)
// console.log('result: ', result)
assert.equal(result, false)
})
it('should create an offer and return the hash', async () => {
const orderObj = {
appId: 'swapTest555',
data: {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
}
// Mock dependencies
sandbox.stub(uut.adapters.bchjs.Blockchain, 'getTxOut').resolves({
bestblock:
'000000000000000000d2060b83f90f8187b92fcccb4a42aaa19ce5a305fe0ae3',
confirmations: 0,
value: 0,
scriptPubKey: {
asm: 'OP_RETURN 5262419 1 1145980243 38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0 00000000001e8480 0000000009a7ec80',
hex: '6a04534c500001010453454e442038e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b00800000000001e8480080000000009a7ec80',
type: 'nulldata'
},
coinbase: false
})
const result = await uut.createOrder(orderObj)
// console.log('result: ', result)
assert.equal(result, true) assert.equal(result, true)
}) })
}) })
describe('#moveTokens', () => {
it('should move tokens to the holding address', async () => {
// Mock dependencies
// sandbox
// .stub(uut.adapters.wallet.bchWallet, 'sendTokens')
// .resolves('fakeTxid')
const orderEntity = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 0,
numTokens: 1
}
const result = await uut.moveTokens(orderEntity)
// console.log('result: ', result)
assert.property(result, 'txid')
assert.property(result, 'vout')
assert.equal(result.txid, 'fakeTxid')
assert.equal(result.vout, 1)
})
})
describe('#createOrder', () => {
it('should create an order and return the hash', async () => {
const entryObj = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'token-id',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 1250,
numTokens: 1
}
// Mock dependencies
// sandbox.stub(uut.adapters.wallet, 'burnPsf').resolves('fakeTxid')
sandbox.stub(uut.orderEntity, 'validate').returns(entryObj)
sandbox.stub(uut, 'ensureFunds').resolves()
sandbox.stub(uut, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves()
// sandbox
// .stub(uut.adapters.wallet, 'generateSignature')
// .resolves('fakeSignature')
sandbox.stub(uut.adapters.p2wdb, 'write').resolves('fakeHash')
const result = await uut.createOrder(entryObj)
console.log('result: ', result)
assert.isString(result)
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox
.stub(uut.orderEntity, 'validate')
.throws(new Error('test error'))
await uut.createOrder()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
}) })