diff --git a/.on-save.json b/.on-save.json new file mode 100644 index 0000000..506a153 --- /dev/null +++ b/.on-save.json @@ -0,0 +1,8 @@ +[ + { + "srcDir": "", + "destDir": "", + "files": "**/*.js", + "command": "npm run lint" + } +] diff --git a/bin/server.js b/bin/server.js index c3a64db..c58ae53 100644 --- a/bin/server.js +++ b/bin/server.js @@ -94,13 +94,13 @@ class Server { try { try { // 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) { /* exit quietly */ // 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') } catch (error) { console.log('Webhook cant be created') diff --git a/src/adapters/localdb/models/order.js b/src/adapters/localdb/models/order.js index 792ca58..f2b9fd8 100644 --- a/src/adapters/localdb/models/order.js +++ b/src/adapters/localdb/models/order.js @@ -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 Order = new mongoose.Schema({ @@ -27,7 +33,6 @@ const Order = new mongoose.Schema({ lokadId: { type: String }, messageType: { type: Number }, messageClass: { type: Number } - }) module.exports = mongoose.model('order', Order) diff --git a/src/controllers/rest-api/offer/controller.js b/src/controllers/rest-api/offer/controller.js index 6ae0f92..f19dba7 100644 --- a/src/controllers/rest-api/offer/controller.js +++ b/src/controllers/rest-api/offer/controller.js @@ -32,13 +32,15 @@ class OfferRESTControllerLib { // No api-doc documentation because this wont be a public endpoint async createOffer (ctx) { 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) { // console.log(`err.message: ${err.message}`) // 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 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 (err.status) { if (err.message) { diff --git a/src/controllers/rest-api/offer/index.js b/src/controllers/rest-api/offer/index.js index 0193ae3..f057558 100644 --- a/src/controllers/rest-api/offer/index.js +++ b/src/controllers/rest-api/offer/index.js @@ -50,6 +50,8 @@ class OfferRouter { // Define the routes and attach the controller. 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. app.use(_this.router.routes()) diff --git a/src/controllers/rest-api/order/controller.js b/src/controllers/rest-api/order/controller.js index 86f50d7..61b0b02 100644 --- a/src/controllers/rest-api/order/controller.js +++ b/src/controllers/rest-api/order/controller.js @@ -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') @@ -32,15 +32,13 @@ class OrderRESTControllerLib { // No api-doc documentation because this wont be a public endpoint async createOrder (ctx) { 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 = { - success: true - } + ctx.body = { hash } } catch (err) { // console.log(`err.message: ${err.message}`) // 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 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 (err.status) { if (err.message) { diff --git a/src/controllers/rest-api/order/index.js b/src/controllers/rest-api/order/index.js index 8a80625..29af97f 100644 --- a/src/controllers/rest-api/order/index.js +++ b/src/controllers/rest-api/order/index.js @@ -1,5 +1,5 @@ /* - REST API library for /offer route. + REST API library for /order route. */ // Public npm libraries. @@ -50,8 +50,6 @@ class OrderRouter { // Define the routes and attach the controller. 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. app.use(_this.router.routes()) diff --git a/src/entities/offer.js b/src/entities/offer.js index 3169745..42b32e3 100644 --- a/src/entities/offer.js +++ b/src/entities/offer.js @@ -1,22 +1,23 @@ /* Offer Entity - An Offer Entity is nearly the same as an Order. But while an Order is generated - by a webhook from P2WDB, the Offer Entity is created internally. It is used - to track an Order generated by this application. - - The Offer tracks the hdIndex address used to hold tokens or BCH for sale. + An ffer 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. */ -class Offer { - validate (data) { - const { - messageType, - messageClass, - tokenId, - buyOrSell, - rateInSats, - minSatsToExchange, - numTokens - } = data + +class OfferEntity { + constructor () { + this.orderStatus = ['posted', 'taken', 'completed'] + } + + validate (orderData = {}) { + // Throw an error if input object does not have a data property + if (!orderData.data) { + throw new Error( + '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 if (!messageType || typeof messageType !== 'number') { @@ -40,19 +41,35 @@ class Offer { if (!numTokens || typeof numTokens !== '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, messageClass, tokenId, buyOrSell, rateInSats, 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 diff --git a/src/entities/order-old.js b/src/entities/order-old.js deleted file mode 100644 index 1e6b8d8..0000000 --- a/src/entities/order-old.js +++ /dev/null @@ -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 - } -*/ diff --git a/src/entities/order.js b/src/entities/order.js index 803bcd5..24d2aa6 100644 --- a/src/entities/order.js +++ b/src/entities/order.js @@ -1,22 +1,22 @@ /* 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. + An Order Entity is nearly the same as an Offer. But while an Offer is generated + 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 { - constructor () { - this.orderStatus = ['posted', 'taken', 'completed'] - } - - validate (orderData = {}) { - // Throw an error if input object does not have a data property - if (!orderData.data) { - throw new Error( - '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 +class Order { + validate (data) { + const { + messageType, + messageClass, + tokenId, + buyOrSell, + rateInSats, + minSatsToExchange, + numTokens + } = data // Input Validation if (!messageType || typeof messageType !== 'number') { @@ -40,35 +40,19 @@ class OrderEntity { if (!numTokens || typeof numTokens !== '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, messageClass, tokenId, buyOrSell, rateInSats, minSatsToExchange, - numTokens, - utxoTxid, - utxoVout, - timestamp: orderData.timestamp, - localTimestamp: orderData.localTimeStamp, - txid: orderData.txid, - p2wdbHash: orderData.hash, - orderStatus: orderStatus || this.orderStatus[0] + numTokens } - return validatedOrderData + return offerData } } -module.exports = OrderEntity +module.exports = Order diff --git a/src/use-cases/order/index.js b/src/use-cases/offer/index.js similarity index 58% rename from src/use-cases/order/index.js rename to src/use-cases/offer/index.js index f1b54a7..a0ba084 100644 --- a/src/use-cases/order/index.js +++ b/src/use-cases/offer/index.js @@ -1,46 +1,46 @@ /* - Use Case library for Orders. - Orders are created by a webhook trigger from the P2WDB. Orders are a result of + 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 Order 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 - local Offers are no different than Orders generated by other peers. + 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. */ // Local libraries -const OrderEntity = require('../../entities/order') +const OfferEntity = require('../../entities/offer') const config = require('../../../config') -class OrderUseCases { +class OfferUseCases { constructor (localConfig = {}) { // console.log('User localConfig: ', localConfig) this.adapters = localConfig.adapters if (!this.adapters) { 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 this.config = config - this.orderEntity = new OrderEntity() - this.OrderModel = this.adapters.localdb.Order + this.offerEntity = new OfferEntity() + 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. - async createOrder (orderObj) { + async createOffer (offerObj) { try { - console.log('Use Case createOrder(orderObj): ', orderObj) + console.log('Use Case createOffer(offerObj): ', offerObj) // console.log('this.adapters.bchjs: ', this.adapters.bchjs) - // Verify that UTXO in order is unspent. If it is spent, then ignore the - // order. - const txid = orderObj.data.utxoTxid - const vout = orderObj.data.utxoVout + // Verify that UTXO in offer is unspent. If it is spent, then ignore the + // offer. + const txid = offerObj.data.utxoTxid + const vout = offerObj.data.utxoVout const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut( txid, vout @@ -48,51 +48,51 @@ class OrderUseCases { console.log('utxoStatus: ', utxoStatus) if (utxoStatus === null) return false - // A new order gets a status of 'posted' - orderObj.data.orderStatus = 'posted' + // A new offer gets a status of 'posted' + offerObj.data.offerStatus = 'posted' - const orderEntity = this.orderEntity.validate(orderObj) - console.log('orderEntity: ', orderEntity) + const offerEntity = this.offerEntity.validate(offerObj) + console.log('offerEntity: ', offerEntity) - // Add order to the local database. - const orderModel = new this.OrderModel(orderEntity) - await orderModel.save() + // Add offer to the local database. + const offerModel = new this.OfferModel(offerEntity) + await offerModel.save() return true } catch (err) { - console.error('Error in createOrder()') + console.error('Error in createOffer()') throw err } } - async listOrders () { + async listOffers () { try { - return this.OrderModel.find({}) + return this.OfferModel.find({}) } catch (error) { - console.error('Error in use-cases/order/listOrders()') + console.error('Error in use-cases/offer/listOffers()') 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: // 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 { - console.log('orderCid: ', orderCid) + console.log('offerCid: ', offerCid) - // Get the Order information - const orderInfo = await this.findOrderByHash(orderCid) - console.log(`orderInfo: ${JSON.stringify(orderInfo, null, 2)}`) + // Get the Offer information + const offerInfo = await this.findOfferByHash(offerCid) + console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`) - // Ensure the order is in a 'posted' state and not already 'taken' - if (orderInfo.orderStatus && orderInfo.orderStatus !== 'posted') { - throw new Error('order already taken') + // Ensure the offer is in a 'posted' state and not already 'taken' + if (offerInfo.offerStatus && offerInfo.offerStatus !== 'posted') { + throw new Error('offer already taken') } // Verify that UTXO for sale is unspent. Abort if it's been spent. - const txid = orderInfo.utxoTxid - const vout = orderInfo.utxoVout + const txid = offerInfo.utxoTxid + const vout = offerInfo.utxoVout const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut( txid, vout @@ -104,32 +104,32 @@ class OrderUseCases { } // Ensure the app has enough funds to complete the trade. - await this.ensureFunds(orderInfo) + await this.ensureFunds(offerInfo) // Get UTXOs. const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore 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. // 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 - // Create valid Order object - const takenOrderInfo = Object.assign({}, orderInfo) - takenOrderInfo.patialTxHex = partialTxHex - delete takenOrderInfo.p2wdbHash - delete takenOrderInfo._id - takenOrderInfo.offerHash = orderInfo.p2wdbHash + // Create valid Offer object + const takenOfferInfo = Object.assign({}, offerInfo) + takenOfferInfo.patialTxHex = partialTxHex + delete takenOfferInfo.p2wdbHash + delete takenOfferInfo._id + 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 - // webhook on new orders, but not on counteroffers + // webhook on new offers, but not on counteroffers const p2wdbObj = { wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, - data: takenOrderInfo, + data: takenOfferInfo, appId: this.config.p2wdbAppId } const hash = await this.adapters.p2wdb.write(p2wdbObj) @@ -137,14 +137,14 @@ class OrderUseCases { // Return the P2WDB CID return hash } catch (err) { - console.error('Error in use-cases/order/takeOrder(): ', err) + console.error('Error in use-cases/offer/takeOffer(): ', err) throw err } } // 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. - async ensureFunds (orderEntity) { + async ensureFunds (offerEntity) { try { // console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet) // 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) 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 // 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() console.log(`wallet balance: ${balance}, sats needed: ${satsNeeded}`) const SATS_MARGIN = 5000 @@ -169,7 +169,7 @@ class OrderUseCases { // } else { // Buy Offer - throw new Error('Buy orders are not supported yet.') + throw new Error('Buy offers are not supported yet.') } return true @@ -179,27 +179,27 @@ class OrderUseCases { } } - async findOrderByHash (p2wdbHash) { + async findOfferByHash (p2wdbHash) { try { if (typeof p2wdbHash !== 'string' || !p2wdbHash) { throw new Error('p2wdbHash must be a string') } - const order = await this.OrderModel.findOne({ p2wdbHash }) + const offer = await this.OfferModel.findOne({ p2wdbHash }) - if (!order) { - throw new Error('order not found') + if (!offer) { + throw new Error('offer not found') } - const orderObject = order.toObject() - // return this.orderEntity.validateFromModel(orderObject) + const offerObject = offer.toObject() + // return this.offerEntity.validateFromModel(offerObject) - return orderObject + return offerObject } catch (err) { - console.error('Error in findOrder(): ', err) + console.error('Error in findOffer(): ', err) throw err } } } -module.exports = OrderUseCases +module.exports = OfferUseCases diff --git a/src/use-cases/order/retry-queue.js b/src/use-cases/offer/retry-queue.js similarity index 100% rename from src/use-cases/order/retry-queue.js rename to src/use-cases/offer/retry-queue.js diff --git a/src/use-cases/offer.js b/src/use-cases/order.js similarity index 65% rename from src/use-cases/offer.js rename to src/use-cases/order.js index 21a75a9..cb42f0c 100644 --- a/src/use-cases/offer.js +++ b/src/use-cases/order.js @@ -1,89 +1,89 @@ /* - Offer use-case library. + Order use-case library. */ // Local libraries const { wlogger } = require('../adapters/wlogger') -const OfferEntity = require('../entities/offer') +const OrderEntity = require('../entities/order') const config = require('../../config') -class OfferLib { +class OrderLib { constructor (localConfig = {}) { // console.log('User localConfig: ', localConfig) this.adapters = localConfig.adapters if (!this.adapters) { 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 - this.offerEntity = new OfferEntity() - this.OfferModel = this.adapters.localdb.Offer + this.orderEntity = new OrderEntity() + this.OrderModel = this.adapters.localdb.Order this.bch = this.adapters.bch this.config = config } - // Create a new offer model and add it to the Mongo database. - async createOffer (entryObj) { + // Create a new order model and add it to the Mongo database. + async createOrder (entryObj) { try { - // console.log('createOffer(entryObj): ', entryObj) + // console.log('createOrder(entryObj): ', entryObj) // Input Validation - const offerEntity = this.offerEntity.validate(entryObj) - console.log('offerEntity: ', offerEntity) + const orderEntity = this.orderEntity.validate(entryObj) + console.log('orderEntity: ', orderEntity) - // Ensure sufficient tokens exist to create the offer. - await this.ensureFunds(offerEntity) + // Ensure sufficient tokens exist to create the order. + await this.ensureFunds(orderEntity) // Move the tokens to holding address. - const utxoInfo = await this.moveTokens(offerEntity) + const utxoInfo = await this.moveTokens(orderEntity) console.log('utxoInfo: ', utxoInfo) // Update the UTXO store for the wallet. await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000) await this.adapters.wallet.bchWallet.getUtxos() - // Update the offer with the new UTXO information. - offerEntity.utxoTxid = utxoInfo.txid - offerEntity.utxoVout = utxoInfo.vout - offerEntity.hdIndex = utxoInfo.hdIndex + // Update the order with the new UTXO information. + orderEntity.utxoTxid = utxoInfo.txid + orderEntity.utxoVout = utxoInfo.vout + orderEntity.hdIndex = utxoInfo.hdIndex - // Add offer to P2WDB. + // Add order to P2WDB. const p2wdbObj = { wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, - data: offerEntity, + data: orderEntity, appId: this.config.p2wdbAppId } const hash = await this.adapters.p2wdb.write(p2wdbObj) // console.log('hash: ', hash) - // Create a MongoDB model to hold the Offer - offerEntity.p2wdbHash = hash - console.log(`creating new offer model: ${JSON.stringify(offerEntity, null, 2)}`) - const offer = new this.OfferModel(offerEntity) - await offer.save() + // Create a MongoDB model to hold the Order + orderEntity.p2wdbHash = hash + console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`) + const order = new this.OrderModel(orderEntity) + await order.save() return hash } catch (err) { // 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 } } - // 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 // moves the funds and returns the UTXO information. - async moveTokens (offerEntity) { + async moveTokens (orderEntity) { try { const keyPair = await this.adapters.wallet.getKeyPair() console.log('keyPair: ', keyPair) const receiver = { address: keyPair.cashAddress, - tokenId: offerEntity.tokenId, - qty: offerEntity.numTokens + tokenId: orderEntity.tokenId, + qty: orderEntity.numTokens } 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 // trade. - async ensureFunds (offerEntity) { + async ensureFunds (orderEntity) { try { // console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet) // 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 console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) - if (offerEntity.buyOrSell.includes('sell')) { - // Sell Offer + if (orderEntity.buyOrSell.includes('sell')) { + // 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( - x => x.tokenId === offerEntity.tokenId + x => x.tokenId === orderEntity.tokenId ) // console.log('tokenUtxos: ', tokenUtxos) // Get the total amount of tokens in the wallet that match the token - // in the offer. + // in the order. let totalTokenBalance = 0 tokenUtxos.map(x => (totalTokenBalance += parseFloat(x.qtyStr))) 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. - if (totalTokenBalance <= offerEntity.numTokens || isNaN(totalTokenBalance)) { + if (totalTokenBalance <= orderEntity.numTokens || isNaN(totalTokenBalance)) { 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 { - // Buy Offer + // Buy Order throw new Error('Buy orders are not supported yet.') } @@ -154,4 +154,4 @@ class OfferLib { } } -module.exports = OfferLib +module.exports = OrderLib diff --git a/test/e2e/manual/01-create-order.js b/test/e2e/manual/01-create-order.js index d6d057a..6eb3031 100644 --- a/test/e2e/manual/01-create-order.js +++ b/test/e2e/manual/01-create-order.js @@ -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. */ @@ -10,7 +11,7 @@ const LOCALHOST = 'http://localhost:5700' async function start () { try { - const mockOffer = { + const mockOrder = { lokadId: 'SWP', messageType: 1, messageClass: 1, @@ -24,8 +25,8 @@ async function start () { const options = { method: 'post', - url: `${LOCALHOST}/offer`, - data: { offer: mockOffer } + url: `${LOCALHOST}/order`, + data: { order: mockOrder } } const result = await axios(options) diff --git a/test/e2e/manual/02-take-order.js b/test/e2e/manual/02-take-offer.js similarity index 100% rename from test/e2e/manual/02-take-order.js rename to test/e2e/manual/02-take-offer.js diff --git a/test/unit/controllers/rest-api/offer/offer.rest.controller.unit.js b/test/unit/controllers/rest-api/offer/offer.rest.controller.unit.js index 48e8c8f..945b072 100644 --- a/test/unit/controllers/rest-api/offer/offer.rest.controller.unit.js +++ b/test/unit/controllers/rest-api/offer/offer.rest.controller.unit.js @@ -64,36 +64,53 @@ describe('#Offer-REST-Router', () => { describe('#createOffer', () => { it('should create a new offer', async () => { 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 - sandbox.stub(uut.useCases.offer, 'createOffer').resolves('testHash') + // sandbox.stub(uut.useCases.offer, 'createOffer').resolves() await uut.createOffer(ctx) - assert.equal(ctx.body.hash, 'testHash') + // assert.equal(ctx.body.hash, 'testHash') }) - it('should catch and throw an error', async () => { - try { - ctx.request.body = { - offer: {} - } - - // Force an error - sandbox - .stub(uut.useCases.offer, 'createOffer') - .rejects(new Error('test error')) - - await uut.createOffer(ctx) - - assert.fail('Unexpected code path') - } catch (err) { - // console.log('err: ', err) - assert.include(err.message, 'test error') - } - }) + // it('should catch and throw an error', async () => { + // try { + // ctx.request.body = { + // offer: {} + // } + // + // // Force an error + // sandbox + // .stub(uut.useCases.offer, 'createOffer') + // .rejects(new Error('test error')) + // + // await uut.createOffer(ctx) + // + // assert.fail('Unexpected code path') + // } catch (err) { + // // console.log('err: ', err) + // assert.include(err.message, 'test error') + // } + // }) }) describe('#handleError', () => { diff --git a/test/unit/controllers/rest-api/order/order.rest.controller.unit.js b/test/unit/controllers/rest-api/order/order.rest.controller.unit.js index 6185e4d..b772814 100644 --- a/test/unit/controllers/rest-api/order/order.rest.controller.unit.js +++ b/test/unit/controllers/rest-api/order/order.rest.controller.unit.js @@ -64,53 +64,36 @@ describe('#Order-REST-Router', () => { describe('#createOrder', () => { it('should create a new order', async () => { ctx.request.body = { - 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' + order: {} } // Mock dependencies - // sandbox.stub(uut.useCases.order, 'createOrder').resolves() + sandbox.stub(uut.useCases.order, 'createOrder').resolves('testHash') await uut.createOrder(ctx) - // assert.equal(ctx.body.hash, 'testHash') + assert.equal(ctx.body.hash, 'testHash') }) - // it('should catch and throw an error', async () => { - // try { - // ctx.request.body = { - // order: {} - // } - // - // // Force an error - // sandbox - // .stub(uut.useCases.order, 'createOrder') - // .rejects(new Error('test error')) - // - // await uut.createOrder(ctx) - // - // assert.fail('Unexpected code path') - // } catch (err) { - // // console.log('err: ', err) - // assert.include(err.message, 'test error') - // } - // }) + it('should catch and throw an error', async () => { + try { + ctx.request.body = { + order: {} + } + + // Force an error + sandbox + .stub(uut.useCases.order, 'createOrder') + .rejects(new Error('test error')) + + await uut.createOrder(ctx) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'test error') + } + }) }) describe('#handleError', () => { diff --git a/test/unit/entities/offer.entity.unit.js b/test/unit/entities/offer.entity.unit.js index 3498db0..e63703d 100644 --- a/test/unit/entities/offer.entity.unit.js +++ b/test/unit/entities/offer.entity.unit.js @@ -1,5 +1,5 @@ /* - Unit tests for the User entity library. + Unit tests for the Offer entity library. */ const assert = require('chai').assert @@ -27,14 +27,17 @@ describe('#Offer-Entity', () => { uut.validate() } catch (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', () => { try { - const data = {} - uut.validate(data) + const orderData = { data: {} } + uut.validate(orderData) } catch (err) { // console.log(err) assert.include( @@ -46,8 +49,8 @@ describe('#Offer-Entity', () => { it('should throw an error if messageClass is not included', () => { try { - const data = { messageType: 1 } - uut.validate(data) + const orderData = { data: { messageType: 1 } } + uut.validate(orderData) } catch (err) { // console.log(err) assert.include( @@ -59,8 +62,8 @@ describe('#Offer-Entity', () => { it('should throw an error if tokenId is not included', () => { try { - const data = { messageType: 1, messageClass: 1 } - uut.validate(data) + const orderData = { data: { messageType: 1, messageClass: 1 } } + uut.validate(orderData) } catch (err) { // console.log(err) 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', () => { try { - const data = { messageType: 1, messageClass: 1, tokenId: 'fakeId' } - uut.validate(data) + const orderData = { + data: { messageType: 1, messageClass: 1, tokenId: 'fakeId' } + } + + uut.validate(orderData) } catch (err) { // console.log(err) 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', () => { try { - const data = { - messageType: 1, - messageClass: 1, - tokenId: 'fakeId', - buyOrSell: 'buy' + const orderData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy' + } } - uut.validate(data) + + uut.validate(orderData) } catch (err) { // console.log(err) assert.include( @@ -97,14 +106,17 @@ describe('#Offer-Entity', () => { it('should throw an error if minSatsToExchange is not included', () => { try { - const data = { - messageType: 1, - messageClass: 1, - tokenId: 'fakeId', - buyOrSell: 'buy', - rateInSats: 1000 + const orderData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInSats: 1000 + } } - uut.validate(data) + + uut.validate(orderData) } catch (err) { // console.log(err) assert.include( @@ -116,19 +128,105 @@ describe('#Offer-Entity', () => { it('should throw an error if numTokens is not included', () => { try { - const data = { - messageType: 1, - messageClass: 1, - tokenId: 'fakeId', - buyOrSell: 'buy', - rateInSats: 1000, - minSatsToExchange: 350 + const orderData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInSats: 1000, + minSatsToExchange: 350 + } } - uut.validate(data) + uut.validate(orderData) } catch (err) { // console.log(err) 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') + }) }) }) diff --git a/test/unit/entities/order.entity.unit.js b/test/unit/entities/order.entity.unit.js index 163ffa9..ee655c3 100644 --- a/test/unit/entities/order.entity.unit.js +++ b/test/unit/entities/order.entity.unit.js @@ -1,5 +1,5 @@ /* - Unit tests for the Order entity library. + Unit tests for the User entity library. */ const assert = require('chai').assert @@ -27,17 +27,14 @@ describe('#Order-Entity', () => { uut.validate() } catch (err) { // console.log(err) - assert.include( - err.message, - 'Input to order.validate() must be an object with a data property.' - ) + assert.include(err.message, 'Cannot destructure property') } }) it('should throw an error if messageType is not included', () => { try { - const orderData = { data: {} } - uut.validate(orderData) + const data = {} + uut.validate(data) } catch (err) { // console.log(err) assert.include( @@ -49,8 +46,8 @@ describe('#Order-Entity', () => { it('should throw an error if messageClass is not included', () => { try { - const orderData = { data: { messageType: 1 } } - uut.validate(orderData) + const data = { messageType: 1 } + uut.validate(data) } catch (err) { // console.log(err) assert.include( @@ -62,8 +59,8 @@ describe('#Order-Entity', () => { it('should throw an error if tokenId is not included', () => { try { - const orderData = { data: { messageType: 1, messageClass: 1 } } - uut.validate(orderData) + const data = { messageType: 1, messageClass: 1 } + uut.validate(data) } catch (err) { // console.log(err) 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', () => { try { - const orderData = { - data: { messageType: 1, messageClass: 1, tokenId: 'fakeId' } - } - - uut.validate(orderData) + const data = { messageType: 1, messageClass: 1, tokenId: 'fakeId' } + uut.validate(data) } catch (err) { // console.log(err) 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', () => { try { - const orderData = { - data: { - messageType: 1, - messageClass: 1, - tokenId: 'fakeId', - buyOrSell: 'buy' - } + const data = { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy' } - - uut.validate(orderData) + uut.validate(data) } catch (err) { // console.log(err) assert.include( @@ -106,17 +97,14 @@ describe('#Order-Entity', () => { it('should throw an error if minSatsToExchange is not included', () => { try { - const orderData = { - data: { - messageType: 1, - messageClass: 1, - tokenId: 'fakeId', - buyOrSell: 'buy', - rateInSats: 1000 - } + const data = { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInSats: 1000 } - - uut.validate(orderData) + uut.validate(data) } catch (err) { // console.log(err) assert.include( @@ -128,105 +116,19 @@ describe('#Order-Entity', () => { it('should throw an error if numTokens is not included', () => { try { - const orderData = { - data: { - messageType: 1, - messageClass: 1, - tokenId: 'fakeId', - buyOrSell: 'buy', - rateInSats: 1000, - minSatsToExchange: 350 - } + const data = { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInSats: 1000, + minSatsToExchange: 350 } - uut.validate(orderData) + uut.validate(data) } catch (err) { // console.log(err) 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') - }) }) }) diff --git a/test/unit/use-cases/offer.use-case.unit.js b/test/unit/use-cases/offer.use-case.unit.js index 3cb2258..86b3945 100644 --- a/test/unit/use-cases/offer.use-case.unit.js +++ b/test/unit/use-cases/offer.use-case.unit.js @@ -46,97 +46,78 @@ describe('#offer-use-case', () => { }) }) - describe('#ensureFunds', () => { - it('should return true if wallet has enough funds for a sell order', async () => { - const offerEntity = { - lokadId: 'SWP', - messageType: 1, - messageClass: 1, - tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', - buyOrSell: 'sell', - rateInSats: 1000, - minSatsToExchange: 0, - numTokens: 1 + describe('#createOffer', () => { + it('should ignore an offer if utxo has been spent', 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' } - 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) }) }) - - 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') - } - }) - }) }) diff --git a/test/unit/use-cases/order.use-case.unit.js b/test/unit/use-cases/order.use-case.unit.js index 3dec408..914fb94 100644 --- a/test/unit/use-cases/order.use-case.unit.js +++ b/test/unit/use-cases/order.use-case.unit.js @@ -46,78 +46,97 @@ describe('#order-use-case', () => { }) }) - describe('#createOrder', () => { - it('should ignore an offer if utxo has been spent', 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' + describe('#ensureFunds', () => { + it('should return true if wallet has enough funds for a sell order', async () => { + const orderEntity = { + lokadId: 'SWP', + messageType: 1, + messageClass: 1, + tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2', + buyOrSell: 'sell', + rateInSats: 1000, + minSatsToExchange: 0, + numTokens: 1 } - // Mock dependencies - 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) + const result = await uut.ensureFunds(orderEntity) 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') + } + }) + }) })