Compare commits

..
11 Commits
Author SHA1 Message Date
Chris Troutner dcaa73a22f Merge pull request #7 from Permissionless-Software-Foundation/ct-unstable
feat(order/offer): Refactored code to switch Order and Offer terminology
2022-03-21 09:56:50 -07:00
Chris Troutner 83c66b363e feat(order/offer): Refactored code to switch Order and Offer terminology 2022-03-21 09:53:58 -07:00
Chris Troutner b857eb9a6d Fixing typo in dev docs 2022-03-15 10:03:36 -07:00
Chris Troutner 7301df1880 Merge pull request #6 from Permissionless-Software-Foundation/ct-unstable
Updating dev docs and specification
2022-03-12 06:10:34 -08:00
Chris Troutner e77dbb16e3 fix(mongo): Aligning models and specification 2022-03-11 18:23:36 -08:00
Chris Troutner 8f7d997b3e Editing dev docs 2022-03-11 16:37:29 -08:00
Chris Troutner 5a4cd8a125 Editing dev docs 2022-03-11 16:30:42 -08:00
Chris Troutner fdaae3f52e Editing dev docs 2022-03-11 16:25:22 -08:00
Chris Troutner afebdc5448 Editing dev docs 2022-03-11 12:52:08 -08:00
Chris Troutner ad744e807f Editing dev docs 2022-03-11 12:48:58 -08:00
Chris Troutner 4fe89a1190 Editing dev docs 2022-03-11 12:47:57 -08:00
24 changed files with 784 additions and 715 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 {
// 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')
+43 -8
View File
@@ -17,10 +17,26 @@ There are three major pieces of software behind the bch-dex concept. They work t
The arrows in the image represent the information flow between the three pieces of software:
- The _Client_ is essentially a 'dummy terminal' with a bidirectional interface to bch-dex. bch-dex does the heavy lifting, and the _Client_ is a 'thin' UI wrapper.
- `bch-dex` imports data from the global P2WDB database into its local database, using a [webhook](https://en.wikipedia.org/wiki/Webhook) (dashed line). It can also custody funds, pay transaction fees, and create an _Order_ by submitting the data to the P2WDB to generate an _Offer_ (solid line).
- `bch-dex` imports data from the global P2WDB database into its local database, using a [webhook](https://en.wikipedia.org/wiki/Webhook) (dashed line). It can also custody funds, pay transaction fees, and create an _Offer_ by creating an _Order_ and submitting the data to the P2WDB (solid line).
This architecture keeps the global database highly censorship resistant, while allowing local installations to maintain tight control over the user experience. The goal is to have many redundant copies of `bch-dex` on the network, and to empower individual traders to run their own, private copy, while maintaining a single source of truth via the P2WDB.
# Definitions
The workflow of a token trade has three parts:
1. **Make** - An *Offer* to buy or sell tokens is generated by a user, known as the *Maker*.
2. **Take** - A second user, known as a *Taker*, will *take* the *Offer* by issuing a *Counter Offer*
3. **Accept** - The original *Maker* checks the *Counter Offer* and *Accepts* it by signing and then broadcasting the transaction.
Trades done in this way are both *trustless* and *atomic*:
- **Trustless** - This means that neither party needs to trust the other. The *Maker* gets to review the *Counter Offer* before broadcasting it. The *Maker* can not alter the *Counter Offer* after the *Taker* has signed it.
- **Atomic** - The trade happens in a single transaction. There is no middle-state where the trade can get stuck. It either happens or doesn't, it's state is binary and atomic.
Specific *Entities* are defined in the [specification](./specification.md), but here is a brief summary:
- **Order** represents the *Maker* side of the trade. This entity is internal to `bch-dex`. It is used to track tokens set aside for sale and managed by the wallet controlled by `bch-dex`.
- **Offer** contains most of the same information as an **Order**, but is external to `bch-dex`. This is data submitted to the P2WDB and visible to all users on the network.
- **Counter Offer** is generated by a *Taker*, in order to take the other side of the trade. It contains a partially-signed transaction, ready for review by the *Maker*.
# Back End
This section provides additional information on `bch-dex` and P2WDB back end software.
@@ -45,19 +61,38 @@ This section describes the protocols for the database interactions between the t
These are just a brief, high-level overview. Review the [Specifications](./specification.md) for more details.
## Writing to the Global Database
## Reading from the Local Database
The *Client* reads data from the local database stored by `bch-dex`, and does not read the P2WDB global database directly. This gives `bch-dex` the opportunity to filter and modify the data locally, for a more controlled user experience.
## Making an Offer
Adding data to the global P2WDB is triggered by the _Client_ calling a REST API endpoint on `bch-dex`. The [p2wdb npm library](https://www.npmjs.com/package/p2wdb) can be leveraged for easy reading and writing to the P2WDB.
Writing data follows these steps:
- Tokens and BCH are held by a wallet which is under the controlled of `bch-dex`.
- The _Client_ submits data to the POST `/offer` REST API endpoint to create a new Offer.
- `bch-dex` will move the funds into a segregated UTXO, and will use that UTXO to create an Offer. The Offer data is written to the P2WDB. The Offer data is also saved to the local MongoDB.
- After submitting the data to the P2WDB, `bch-dex` will receive a webhook call to its POST `/order` endpoint by the P2WDB. This event will trigger the import of the new data into the apps local Mongo database, and generate a new Order model.
- This webhook event is mirrored by every instance of `bch-dex` on the network. Each P2WDB peer on the network will independently validate the new database entry and create a new Order model.
- The _Client_ submits data to the POST `/order` REST API endpoint to create a new Order.
- `bch-dex` will move the funds into a segregated UTXO, and will use that UTXO to create an Order. The Order data is written to the P2WDB. The Order data is also saved to the local MongoDB.
- After submitting the data to the P2WDB, `bch-dex` will receive a webhook call to its POST `/offer` endpoint by the P2WDB. This event will trigger the import of the new data into the apps local Mongo database, and generate a new Offer model.
- This webhook event is mirrored by every instance of `bch-dex` on the network. Each P2WDB peer on the network will independently validate the new database entry and create a new Offer model.
## Taking an Offer
## Reading from the Local Database
Users can browse the Offers tracked by a local `bch-dex` by using a *Client*. When they find an Offer they want to to take, they'll use some UI element that will send data to the POST `/offer/take` REST API endpoint. These series of steps happen:
The *Client* reads data from the local database stored by `bch-dex`, and does not read the P2WDB global database directly. This gives `bch-dex` the opportunity to filter and modify the data locally, for a more controlled user experience.
- The `bch-dex` checks to see if the wallet it controls has enough BCH to take the other side of the Offer. If it does, the funds for the offer are moved to a segregated UTXO.
- The new UTXO is used to generate a *Counter Offer*, which contain a partially signed transaction saved as a hex string.
- The *Counter Offer* is submitted to the P2WDB. This triggers a webhook event in every running instance of `bch-dex` on the network.
- When the webhook is triggered, `bch-dex` will check to see if the *Counter Offer* matches an *Order* under its control. If a match is found, it will trigger an *Accept* event.
## Accepting a Counter Offer
This part of the process is automated and does not require input from the user.
- When a *Counter Offer* is received that matches an *Order* tracked by the local copy of `bch-dex`, it will trigger the *Acceptance* phase.
- In the *Acceptance* phase, the transaction will be checked to see if it matches the conditions in the original *Order*. If all checks pass, `bch-dex` will sign the transaction and broadcast it, completing the trade.
## Maintenance
Occasional maintenance functions will be called by an interval timer. The primary purpose of these functions is to check the UTXOs in the Order, Offer, and Counter Offer entities. If any of these UTXOs are spent, the entity is deleted from the local Mongo database.
+85 -59
View File
@@ -15,89 +15,115 @@ Entities make up the core business concepts. If these entities change, they fund
### Order
An order is created from data passed to the app by the P2WDB webhook.
It is destroyed when the UTXO described in the Signal has been detected as spent.
An Order Entity is nearly the same as an Offer. The Order is generated first, but is always internal to the bch-dex system. Most of the data in an Order is submitted to the P2WDB, which generates an Offer (external) Entity.
The Order tracks the [HD index address](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05.asciidoc#hd-wallets-bip-32bip-44) used to hold tokens or BCH for sale. This is the part of the app concerned with the custody of the funds. It creates a segregated [UTXO](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch06.asciidoc#transaction-outputs-and-inputs) to hold the offered asset. The Order is automatically destroyed if the UTXO is spent.
Order entities have the following properties:
- _tokenId_ - The unique ID that identifies the class of token being offered for sale.
- _buyOrSell_ - A string with a value `buy` or `sell` indicating which type of offer this is.
- _rateInSats_ - The rate in terms of tokens-per-currency-unit.
- For Bitcoin, the min currency is sats.
- For AVAX, the min currency is nano-Avax.
- for eCash, the min currency is bits.
- _minSatsToExchange_ - The minimum order size accepted.
- _signature_ - A message signed by the address which created the order.
- _sigMsg_ - The clear-text message used to generate the signature.
- _utxoTxid_ - The TXID of the UTXO used in the order.
- _utxoVout_ - The vout of the UTXO used in the order.
- _numTokens_ - The maximum number of tokens offered for sale.
- _timestamp_ - The ISO time when the order was created.
- _localTimestamp_ - The localized time when the order was created.
- _p2wdbTxid_ - The TXID proof-of-burn used to add the order to the P2WDB.
- _p2wdbHash_ - The hash used to identify the order entry in the P2WDB.
- _lokadId_ - Not used. Provided for future functionality.
- _messageType_ - Not used. Provided for future functionality.
- _messageClass_ - Not used. Provided for future functionality.
- Token Data:
- _tokenId_ - The unique ID that identifies the class of token being offered for sale.
- _utxoTxid_ - The TXID of the UTXO representing the token or BCH being offered for sale.
- _utxoVout_ - The vout of the UTXO representing the token or BCH being offered for sale.
- Trade Data:
- _buyOrSell_ - A string with a value `buy` or `sell` indicating which type of offer this is.
- _numTokens_ - The maximum number of tokens offered for sale.
- _rateInBaseUnit_ - The rate in terms of currency-unit-per-token. Ex: 1000 = 1000 sats per token
- For Bitcoin, the min currency is sats.
- For AVAX, the min currency is nano-Avax.
- for eCash, the min currency is bits.
- _minUnitsToExchange_ - The minimum order size accepted.
- Authentication Data:
- _signature_ - A message signed by the address which created the order.
- _sigMsg_ - The clear-text message used to generate the signature.
- _offerBchAddr_ - The BCH address controlling the offer.
- _offerPubKey_ - The public key used to generate the BCH address, used for encryption.
- Wallet Data:
- _hdIndex_ - The HD index of the wallet used to generate the keypair to store the UTXO being offered for sale.
- SWaP Protocol properties:
- _lokadId_ - Not used. Provided for future functionality.
- _messageType_ - Not used. Provided for future functionality.
- _messageClass_ - Not used. Provided for future functionality.
### Offer
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 and managed by this application.
The Offer tracks the [HD index address](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05.asciidoc#hd-wallets-bip-32bip-44) used to hold tokens or BCH for sale. This is the part of the app concerned with the custody of the funds. It creates a segregated [UTXO](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch06.asciidoc#transaction-outputs-and-inputs) to hold the offered asset. The Offer is automatically destroyed if the UTXO is spent.
An offer is created from data passed to the app by the P2WDB webhook.
It is destroyed when the UTXO described in the Signal has been detected as spent.
Offer entities have the following properties:
- _offerIpfsId_ - The IPFS ID of the instance of `ipfs-swap-service` that is managing the offer.
- _offerBchAddr_ - The BCH address controlling the offer.
- _offerPubKey_ - The public key used to generate the BCH address, used for encryption.
- _tokenId_ - The unique ID that identifies the class of token being offered for sale.
- _buyOrSell_ - A string with a value `buy` or `sell` indicating which type of offer this is.
- _rateInSats_ - The rate in terms of tokens-per-currency-unit.
- For Bitcoin, the min currency is sats.
- For AVAX, the min currency is nano-Avax.
- for eCash, the min currency is bits.
- _minSatsToExchange_ - The minimum order size accepted.
- _signature_ - A message signed by the address which created the order.
- _sigMsg_ - The clear-text message used to generate the signature.
- _utxoTxid_ - The TXID of the UTXO used in the order.
- _utxoVout_ - The vout of the UTXO used in the order.
- _numTokens_ - The maximum number of tokens offered for sale.
- _timestamp_ - The ISO time when the order was created.
- _localTimestamp_ - The localized time when the order was created.
- _p2wdbTxid_ - The TXID proof-of-burn used to add the order to the P2WDB.
- _p2wdbHash_ - The hash used to identify the order entry in the P2WDB.
- _lokadId_ - Not used. Provided for future functionality.
- _messageType_ - Not used. Provided for future functionality.
- _messageClass_ - Not used. Provided for future functionality.
- Token Data:
- _tokenId_ - The unique ID that identifies the class of token being offered for sale.
- _utxoTxid_ - The TXID of the UTXO representing the token or BCH being offered for sale.
- _utxoVout_ - The vout of the UTXO representing the token or BCH being offered for sale.
- Trade Data:
- _buyOrSell_ - A string with a value `buy` or `sell` indicating which type of offer this is.
- _numTokens_ - The maximum number of tokens offered for sale.
- _rateInBaseUnit_ - The rate in terms of currency-unit-per-token. Ex: 1000 = 1000 sats per token
- For Bitcoin, the min currency is sats.
- For AVAX, the min currency is nano-Avax.
- for eCash, the min currency is bits.
- _minUnitsToExchange_ - The minimum order size accepted.
- _p2wdbTxid_ - The TXID proof-of-burn used to add the order to the P2WDB.
- _p2wdbHash_ - The CID used to identify the order entry in the P2WDB.
- Authentication Data:
- _signature_ - A message signed by the address which created the order.
- _sigMsg_ - The clear-text message used to generate the signature.
- _offerBchAddr_ - The BCH address controlling the offer.
- _offerPubKey_ - The public key used to generate the BCH address, used for encryption.
- Utility Data:
- _timestamp_ - The ISO time when the order was created.
- _localTimestamp_ - The localized time when the order was created.
- SWaP Protocol properties:
- _lokadId_ - Not used. Provided for future functionality.
- _messageType_ - Not used. Provided for future functionality.
- _messageClass_ - Not used. Provided for future functionality.
### Counter Offer
A Counter Offer is the other side of an Offer. It contains a partially signed transaction, created by the Taker. The Maker will review the Counter Offer before accepting and finalizing the trade.
Details TBD.
## Use Cases
Use cases are verbs or actions that is done _to_ an Entity or _between_ Entities.
### Order
- **`createOrder()`** - This method is triggered by a webhook from the P2WDB. It will take the data provided by the P2WDB and create a new Order entity in the local database.
### Offer
- **`createOffer()`** - This method is triggered by a webhook from the P2WDB. It will take the data provided by the P2WDB and create a new Order entity in the local database.
### Order
- **`ensureFunds()`** - Ensure that the wallet has enough BCH and tokens to complete the requested trade.
- **`moveTokens()`** - Move the tokens indicated in the offer to a temporary holding address. This will generate the UTXO used in the webhook message. This function moves the funds and returns the UTXO information.
- **`createOffer()`** - A macro command that leverages `ensureFunds()` and `moveTokens()`, to create a new Offer and submit it to the P2WDB.
- **`moveTokens()`** - Move the tokens indicated in the order to a temporary holding address. This will generate the UTXO used in the webhook message. This function moves the funds and returns the UTXO information.
- **`createOrder()`** - A macro command that leverages `ensureFunds()` and `moveTokens()`, to create a new Order and submit it to the P2WDB.
### Counter Offer
TBD
## Controllers
Controllers are inputs to the system. When a controller is activated, it causes the system to react in some way.
### Orders
- **POST /order** - This POST REST API endpoint will be triggered by a webhook generated from P2WDB. This will notify the `bch-dex` that a new entry has been added to the P2WDB that matches the `appId` of `swap-<chain>`, where `<chain>` has a value of `avax`, `bch`, or `ecash`. It's a new entry that should be evaluated for inclusion in the `ipfs-swap-service` local database.
### Offers
- **POST /offer** - This POST REST API endpoint can be triggered by the Client or a simple curl call. It passes in the data needed for `ipfs-swap-service` to generate and track a new Offer, then submit the data to the P2WDB to generate an Order that is tracked by all other instances of `ipfs-swap-service`.
- **POST /offer** - This POST REST API endpoint will be triggered by a webhook generated from P2WDB. This will notify the `bch-dex` that a new entry has been added to the P2WDB that matches the `appId` of `swap-<chain>`, where `<chain>` has a value of `avax`, `bch`, or `ecash`. It's a new entry that should be evaluated for inclusion in the `ipfs-swap-service` local database.
### Orders
- **POST /order** - This POST REST API endpoint can be triggered by the Client or a simple curl call. It passes in the data needed for `bch-dex` to generate and track a new Order, then submit the data to the P2WDB to generate an Offer that is tracked by all other instances of `bch-dex`.
## Adapters
+23 -15
View File
@@ -1,25 +1,33 @@
const mongoose = require('mongoose')
const Offer = new mongoose.Schema({
// Token data
tokenId: { type: String },
utxoTxid: { type: String },
utxoVout: { type: Number },
// Trade data
buyOrSell: { type: String },
numTokens: { type: Number },
rateInBaseUnit: { type: String },
minUnitsToExchange: { type: String },
p2wdbTxid: { type: String },
p2wdbHash: { type: String },
// Authentication data
signature: { type: String },
sigMsg: { type: String },
// Utility data
timestamp: { type: String },
localTimestamp: { type: String },
// SWaP Protocol Properties
lokadId: { type: String },
messageType: { type: Number },
messageClass: { type: Number },
tokenId: { type: String },
buyOrSell: { type: String },
rateInSats: { type: String },
minSatsToExchange: { type: String },
signature: { type: String },
sigMsg: { type: String },
utxoTxid: { type: String },
utxoVout: { type: Number },
numTokens: { type: Number },
hdIndex: { type: Number }, // HD index address holding the UTXO for this offer.
messageClass: { type: Number }
//
offerIpfsId: { type: String },
offerBchAddr: { type: String },
offerPubKey: { type: String }
})
module.exports = mongoose.model('offer', Offer)
+29 -15
View File
@@ -1,24 +1,38 @@
/*
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({
// Token data
tokenId: { type: String },
utxoTxid: { type: String },
utxoVout: { type: Number },
// Trade data
buyOrSell: { type: String },
numTokens: { type: Number },
rateInBaseUnit: { type: String },
minUnitsToExchange: { type: String },
p2wdbTxid: { type: String },
p2wdbHash: { type: String },
// Authentication data
signature: { type: String },
sigMsg: { type: String },
offerBchAddr: { type: String },
offerPubKey: { type: String },
// Wallet Data
hdIndex: { type: Number }, // HD index address holding the UTXO for this offer.
// SWaP Protocol Properties
lokadId: { type: String },
messageType: { type: Number },
messageClass: { type: Number },
tokenId: { type: String },
buyOrSell: { type: String },
rateInSats: { type: String },
minSatsToExchange: { type: String },
signature: { type: String },
sigMsg: { type: String },
utxoTxid: { type: String },
utxoVout: { type: Number },
numTokens: { type: Number },
timestamp: { type: String },
localTimestamp: { type: String },
p2wdbTxid: { type: String },
p2wdbHash: { type: String },
orderStatus: { type: String }
messageClass: { type: Number }
})
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
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) {
+2
View File
@@ -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())
+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')
@@ -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) {
+1 -3
View File
@@ -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())
+37 -20
View File
@@ -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
-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
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
@@ -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
@@ -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
+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.
*/
@@ -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)
@@ -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', () => {
@@ -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', () => {
+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
@@ -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')
})
})
})
+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
@@ -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')
})
})
})
+68 -87
View File
@@ -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')
}
})
})
})
+87 -68
View File
@@ -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')
}
})
})
})