mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-23 01:22:03 -07:00
+12
-4
@@ -25,7 +25,7 @@ const AdminLib = require('../src/adapters/admin')
|
||||
// const adminLib = new AdminLib()
|
||||
|
||||
const WebHookLib = require('../src/adapters/webhook')
|
||||
const webHookLib = new WebHookLib()
|
||||
const webhookLib = new WebHookLib()
|
||||
|
||||
// const JSONRPC = require('../src/rpc')
|
||||
// const rpc = new JSONRPC()
|
||||
@@ -91,7 +91,15 @@ class Server {
|
||||
|
||||
// Create webhook
|
||||
try {
|
||||
await webHookLib.createWebHook('http://localhost:5002/entry')
|
||||
try {
|
||||
// Delete an old webhook if it exists.
|
||||
await webhookLib.deleteWebhook('http://localhost:5700/order')
|
||||
} catch (err) {
|
||||
/* exit quietly */
|
||||
// console.log('err deleting webhook: ', err)
|
||||
}
|
||||
|
||||
await webhookLib.createWebhook('http://localhost:5700/order')
|
||||
console.log('Webhook created')
|
||||
} catch (error) {
|
||||
console.log('Webhook cant be created')
|
||||
@@ -108,12 +116,12 @@ class Server {
|
||||
// Attach the other IPFS controllers
|
||||
await controllers.attachControllers(app)
|
||||
|
||||
// ipfs-coord has a memory leak. This app shuts down after 8 hours. It
|
||||
// ipfs-coord has a memory leak. This app shuts down after 4 hours. It
|
||||
// expects to be run by Docker or pm2, which can automatically restart
|
||||
// the app.
|
||||
setTimeout(function () {
|
||||
process.exit(0)
|
||||
}, 60000 * 60 * 8)
|
||||
}, 60000 * 60 * 4)
|
||||
|
||||
return app
|
||||
} catch (err) {
|
||||
|
||||
Vendored
+1
-1
@@ -7,6 +7,6 @@
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/torlist-service-dev',
|
||||
database: 'mongodb://localhost:27017/swap-service-dev',
|
||||
env: 'dev'
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -13,6 +13,6 @@ module.exports = {
|
||||
// database: 'mongodb://172.17.0.1:5555/ipfs-service-prod',
|
||||
database: process.env.DBURL
|
||||
? process.env.DBURL
|
||||
: 'mongodb://172.17.0.1:5555/torlist-service-prod',
|
||||
: 'mongodb://172.17.0.1:5555/swap-service-prod',
|
||||
env: 'prod'
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -7,6 +7,6 @@
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/torlist-service-test',
|
||||
database: 'mongodb://localhost:27017/swap-service-test',
|
||||
env: 'test'
|
||||
}
|
||||
|
||||
+20
-29
@@ -1,44 +1,42 @@
|
||||
# Developer Documentation
|
||||
|
||||
This is living documentation that will be updated, edited, and changed over time, using the same version control as the rest of the code. The purpose of this documentation is to capture and explain how this tor-list-api and [tor-list-frontend](https://github.com/Permissionless-Software-Foundation/tor-list-frontend) [SPA](https://en.wikipedia.org/wiki/Single-page_application) interact with the [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) to create a permissionless, censorship-resistant database for storing and sharing deep-web websites at [TorList.cash](https://torlist.cash).
|
||||
This is living documentation that will be updated, edited, and changed over time, using the same version control as the rest of the code. The purpose of this documentation is to capture and explain how this ipfs-swap-service interacts with the [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service), to create a permissionless, censorship-resistant database for storing trading orders. A web client will be built in the future that will interact with the REST API of this app.
|
||||
|
||||
- [Specification](./specification.md)
|
||||
|
||||
# Overview
|
||||
|
||||
There are three major pieces of software behind the tor-list concept. They work together to form a censorship-resistant application for curating information about websites.
|
||||
There are three major pieces of software behind the ipfs-swap-service concept. They work together to form a censorship-resistant application for exchanging transaction data for building trades.
|
||||
|
||||

|
||||

|
||||
|
||||
- [tor-list-frontend](https://github.com/Permissionless-Software-Foundation/tor-list-frontend) is the web based user interface (UI) which can be seen at [TorList.cash](https://torlist.cash/).
|
||||
- [tor-list-api](https://github.com/Permissionless-Software-Foundation/tor-list-api) is the back end REST API that maintains a local database of information that tor-list-frontend reads from.
|
||||
- _Client_ could be a web browser, or a command-line client like [psf-bch-wallet](https://github.com/Permissionless-Software-Foundation/psf-bch-wallet) or [psf-avax-wallet](https://github.com/Permissionless-Software-Foundation/psf-avax-wallet).
|
||||
- [ipfs-swap-service](https://github.com/christroutner/ipfs-swap-service) is the back end REST API that maintains a local database of information that the client reads from.
|
||||
- [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) is the pay-to-write global database with a REST API for interfacing with the other two pieces of software.
|
||||
|
||||
The arrows in the image represent the information flow between the three pieces of software:
|
||||
|
||||
- tor-list-frontend displays information about websites. It _reads_ this information from tor-list-api.
|
||||
- tor-list-frontend is also a web wallet. It can generate the needed transactions to _write_ information to the P2WDB.
|
||||
- tor-list-api imports data from the global database into its local database using a [webhook](https://en.wikipedia.org/wiki/Webhook).
|
||||
- The _Client_ displays information about orders. It _reads_ this information from ipfs-swap-service.
|
||||
- The _Client_ is also a wallet. It can generate the needed transactions to _write_ information to the P2WDB.
|
||||
- `ipfs-swap-service` 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 by creating an _Offer_ and submitting the data to the P2WDB to generate an _Order_ (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 curation sites similar to [TorList.cash](https://torlist.cash). Each site would cater to their niche by customizing the display of the data, but they would all tap into the same global database of information.
|
||||
|
||||
**Example:** [TorList.cash](https://torlist.cash) will prevent the display of sites that are associated with drugs, guns, pornography, or gambling. That data can still exist in the global database, it just won't be displayed on Torlist.cash. Other websites, using a fork of the same software, can display those categories. They could also impose stricter rules to filter the data even more aggressively. (Imagine a site that focuses just on cryptocurrency trading, and filters out all sites that are not strictly about cryptocurrency trading.) Each site competes with the others for users, but work together to maintain the global database.
|
||||
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 `ipfs-swap-service` on the network, and to empower individual traders to run their own, private copy.
|
||||
|
||||
# Back End
|
||||
|
||||
This section provides additional information on the tor-list-api and P2WDB back end software.
|
||||
This section provides additional information on `ipfs-swap-service` and P2WDB back end software.
|
||||
|
||||
## P2WDB
|
||||
|
||||
The heart of the censorship resistance is the pay-to-write database ([P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service)). This is an [OrbitDB](https://orbitdb.org/) peer-to-peer (p2p) database. The write-access rules have been customized to allow anyone to write to the database, so long they prove that a sufficient quantity [PSF tokens](https://psfoundation.cash) have been burned, to pay for the write.
|
||||
The heart of the censorship resistance is the pay-to-write database ([P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service)). This is an [OrbitDB](https://orbitdb.org/) peer-to-peer (p2p) database. The write-access rules have been customized to allow anyone to write to the database, so long as they prove that a sufficient quantity of [PSF tokens](https://psfoundation.cash) have been burned, to pay for the write.
|
||||
|
||||
Because OrbitDB is a p2p database, no one party holds the 'official' copy of the database. Instead, like a blockchain, the database is replicated among several peers, and they coordinate updates to the database using consensus rules. Peers are free to leave or enter the network. Each peer independently verifies the database entries have sufficient proof-of-burn.
|
||||
|
||||
## tor-list-api
|
||||
## `ipfs-swap-service`
|
||||
|
||||
The [tor-list-api](https://github.com/Permissionless-Software-Foundation/tor-list-api) replicates a copy of the global P2WDB, but has the ability to apply localized filters to the data before passing it on to tor-list-frontend to be displayed. The most important filter is the `blacklist` model, which allows administrators of the site to mark database entries as 'blacklisted', to prevent them from being displayed.
|
||||
The [ipfs-swap-service](https://github.com/christroutner/ipfs-swap-service) replicates a copy of the global P2WDB, but has the ability to apply localized filters to the data before passing it on to the _Client_, to be displayed.
|
||||
|
||||
tor-list-api is based on this [koa-api-boilerplate](https://github.com/christroutner/koa-api-boilerplate), which is a production ready REST API that includes many features for building a web app. This includes user management and authentication, REST API scaffolding, API documentation, Docker container generation, and extensive test coverage. It's intended to be customized for the needs of the website administrator.
|
||||
`ipfs-swap-service` is based on this [ipfs-service-provider boilerplate](https://github.com/Permissionless-Software-Foundation/ipfs-service-provider). It's a production-ready template for a web server, providing interfaces via REST API over HTTP, as well as JSON RPC over IPFS. It includes many features for building a web app. This includes user management and authentication, REST API and JSON RPC scaffolding, API documentation, Docker container generation, and extensive test coverage. It's intended to be customized for the needs of the website administrator.
|
||||
|
||||
- [Specification](./specification.md)
|
||||
|
||||
@@ -50,24 +48,17 @@ These are just a brief, high-level overview. Review the [Specifications](./speci
|
||||
|
||||
## Writing to the Global Database
|
||||
|
||||
Adding data to the global P2WDB is a result of the interaction between tor-list-frontend and the P2WDB. tor-list-api is not involved.
|
||||
Adding data to the global P2WDB is a result of the interaction between the _Client_ and the P2WDB. Ideally, `ipfs-swap-service` is not involved. The [p2wdb npm library](https://www.npmjs.com/package/p2wdb) can be leveraged for easy reading and writing to the P2WDB.
|
||||
|
||||
During development, `ipfs-swap-service` is being used to submit Offers to the P2WDB and custody funds. When the project reaches maturity, these functions may be removed, and they should be handled by the Client, so that legal issues around custody of funds are not a problem.
|
||||
|
||||
Writing data follows these steps:
|
||||
|
||||
- To add an entry to the P2WDB, tor-list-frontend collects the data the user wants to add to the database, but it also collects several pieces of required information 'behind the scenes':
|
||||
- The users BCH address.
|
||||
- A cleartext message (The website URL)
|
||||
- A signature generated from the cleartext message and the BCH address.
|
||||
- It burns the required amount of PSF tokens, and generates a transaction ID (TXID) as proof of this burn.
|
||||
- tor-list-frontend then communicates with P2WDB via its REST API to submit all the data.
|
||||
- A user submits data to the POST `/offer` REST API endpoint. This will move the funds a segregated UTXO and submit the data to the P2WDB to convert the Offer to an Order. Offers are tracked by the local instance of `ipfs-swap-service`, but Orders are tracked by all instances of `ipfs-swap-service`.
|
||||
- The P2WDB REST API will then evaluate the data and attempt to update the p2p database using the TXID.
|
||||
- Each peer on the network will independently validate the new database entry.
|
||||
- tor-list-api will receive a webhook call to its POST endpoint. This event will trigger the import of the new data into the apps local Mongo database.
|
||||
- `ipfs-swap-service` will receive a webhook call to its POST `/order` endpoint. This event will trigger the import of the new data into the apps local Mongo database, and generate a new Order model.
|
||||
|
||||
## Reading from the Local Database
|
||||
|
||||
tor-list-frontend reads data from the local database stored by tor-list-api, and does not read the global database directly. This gives tor-list-api the opportunity to filter and modify the data locally for a more controlled user experience.
|
||||
|
||||
The most important filter that tor-list-api runs against the data is the blacklist filter. Entries in the OrbitDB are identified by a [CID](https://docs.ipfs.io/concepts/content-addressing/) or 'hash'. The blacklist filter is a collection of CIDs that have been 'blacklisted' for removal before being displayed on the front end. This allows the administrators of the website to prevent spam and abuse.
|
||||
|
||||
Entries on tor-list-frontend are organized relative to their [merit](https://github.com/Permissionless-Software-Foundation/bch-message-lib/blob/master/lib/merit.js). This merit value will change with time. Every 24 hours, the entries displayed at TorList.cash have their merit recalculated, and the merit value in the local database is updated.
|
||||
The Client reads data from the local database stored by `ipfs-swap-service`, and does not read the global database directly. This gives `ipfs-swap-service` the opportunity to filter and modify the data locally for a more controlled user experience.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
+70
-35
@@ -1,68 +1,103 @@
|
||||
# Tor-List-API Specification
|
||||
# ipfs-swap-service Specification
|
||||
|
||||
This document contains a high-level, human-readable specification for the four major architectural areas of the tor-list-api:
|
||||
This document contains a high-level, human-readable specification for the four major architectural areas of the ipfs-swap-service:
|
||||
|
||||
- Entities
|
||||
- Use Cases
|
||||
- Controllers (inputs)
|
||||
- Adapters (outputs)
|
||||
|
||||
This reflects the [Clean Architecture](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service/blob/master/dev-docs/clean-architecture.md) design pattern.
|
||||
This reflects the [Clean Architecture](https://troutsblog.com/blog/clean-architecture) design pattern.
|
||||
|
||||
## Entities
|
||||
|
||||
Entities make up the core business concepts. If these entities change, they fundamentally change the entire app.
|
||||
|
||||
### Website
|
||||
### Order
|
||||
|
||||
The tor-list concept is all about curating deep-web websites that may have unusual or ephemeral address and may exist on different parts of the internet, like IPFS, Tor, or lightly supported TLDs like `.eth`. These Websites Entities have the following properties:
|
||||
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.
|
||||
|
||||
- **url** - not restricted to common TLDs. This could have an address to an IPFS gateway, a .onion, .eth, or other type of address.
|
||||
- **address** - A blockchain address associated with the website. This address is used to evaluate the merit value of the site. In the future, it can be used by the site owner to communicate additional information using the [PS001 specification](https://github.com/Permissionless-Software-Foundation/specifications/blob/master/ps001-media-sharing.md).
|
||||
- **blockchain** - The blockchain the address above belongs to. Default value is `BCH`
|
||||
- **merit** - A merit score for the website. This is used to rank sites within their category. The value is calculated and updated every 24 hours.
|
||||
- **merit timestamp** - A timestamp of when the merit of this entity was last calculated.
|
||||
- **mainCategory** - A string categorizing the website. (e.g. Tor websites)
|
||||
- **subCategory** - A string categorizing the website within the main category. (e.g. 'Email' services, within Tor websites)
|
||||
- **description** - A long-form string describing the website.
|
||||
- **isBlacklisted** - A Boolean. Set to `false` by default.
|
||||
Order entities have the following properties:
|
||||
|
||||
### Blacklist
|
||||
- _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.
|
||||
|
||||
The blacklist entity points at a Website entity. It's purpose is to identify P2WDB entries that should not be displayed on the front-end. It has the following properties:
|
||||
### Offer
|
||||
|
||||
- **hash** - The OrbitDB hash of the Website entity, used to identify it in the P2WDB.
|
||||
- **txid** - The TXID used as a key to identify the Website entity in the P2WDB.
|
||||
- **reason** - A string indicating the reason why an admin chose to blacklist this website.
|
||||
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.
|
||||
|
||||
### Category
|
||||
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 accidentally spent, which is why it needs to be segregated from other wallet UTXOs.
|
||||
|
||||
The Category Entity holds data about entries that fit within a single category. Having an entity reduces the computational load, because data can quickly be looked up in the Category Entity, and the values of that Category Entity can be recalculated asynchrously. e.g. The time information is retrieved and the time information is updated are different.
|
||||
Offer entities have the following properties:
|
||||
|
||||
- **websitesAll** - An array of Mongo IDs of Website Entities. Every Website Entity assigned to the category should appear in this list.
|
||||
- **websitesTop** - An array of Mongo IDs of Website Entities. This contains a sorted list of the top 100 websites, based on merit.
|
||||
- _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.
|
||||
|
||||
## Use Cases
|
||||
|
||||
Use cases are verbs or actions that is done _to_ an Entity or _between_ Entities.
|
||||
|
||||
- **Delete** - This is actually _not_ a use case. It's mentioned here to point out that deleting websites is not necessary. Users can instead move their PSF tokens to a different address, and the merit will go to zero. This will cause the website entry to drop to the bottom of the rankings for its category. Entries with zero merit will be ignored, or possibly garbage-collected.
|
||||
- **Add to Blacklist** - This is an action that can only be performed by a logged-in user with admin privledges. This allows the admin to prevent entries that violate the ToS from being displayed.
|
||||
- **Get Merit** - Read the merit value of the Website Entity.
|
||||
- **Update Merit** - Recalculate and update the merit value of the Website Entity.
|
||||
### 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
|
||||
|
||||
- **`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.
|
||||
|
||||
## Controllers
|
||||
|
||||
Controllers are inputs to the system. When a controller is activated, it causes the system to react in some way.
|
||||
|
||||
- **POST Webhook** - This POST REST endpoint will be triggered by a webhook generated by the P2WDB. This will notify the tor-list-api that a new entry has been added to the P2WDB that matches the `appId` of `torlist`. It's a new entry that should be evaluated for inclusion in the tor-list-api local database.
|
||||
- **GET by Category** - Return the 100 top-ranked Website Entities within a specific category, and with a merit above zero.
|
||||
- **POST Blacklist** - Creates a new Blacklist Entity. This endpoint is only accessible by an Admin user.
|
||||
- **24 hour Timer** - A timer which triggers every 24 hours, sets off an update of the database. When it triggers, the following actions should take place:
|
||||
- All the website entities should be added to a retry-queue. Highest-merit websites are processed first, lowest merit last.
|
||||
- The server will recalculate the merit for each Website Entity in the queue.
|
||||
- TODO: Figure out a good garbage collection scheme to remove websites that consistently have no merit.
|
||||
- The server will update the 'top 100' in each Category Entity.
|
||||
### Orders
|
||||
|
||||
- **POST /order** - This POST REST API endpoint will be triggered by a webhook generated by the P2WDB. This will notify the `ipfs-swap-service` 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`.
|
||||
|
||||
## Adapters
|
||||
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Ports
|
||||
export PORT=5001 # REST API port
|
||||
export IPFS_TCP_PORT=5268
|
||||
export IPFS_WS_PORT=5269
|
||||
|
||||
#export ENABLE_CIRCUIT_RELAY=1
|
||||
|
||||
# Production database connection string.
|
||||
#export DBURL=mongodb://localhost:27017/bch-service-dev
|
||||
|
||||
export COORD_NAME=ipfs-service-provider-generic
|
||||
export MNEMONIC="churn aisle shield silver ladder swear hunt slim pen demand spoil veteran"
|
||||
|
||||
export DEBUG_LEVEL=1
|
||||
|
||||
npm start
|
||||
Generated
+5549
-5617
File diff suppressed because it is too large
Load Diff
+44
-44
@@ -23,53 +23,53 @@
|
||||
},
|
||||
"repository": "christroutner/ipfs-swap-service",
|
||||
"dependencies": {
|
||||
"@chris.troutner/ipfs": "2.0.2",
|
||||
"@psf/bch-js": "^4.20.3",
|
||||
"axios": "^0.21.1",
|
||||
"bch-message-lib": "^1.13.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"glob": "^7.1.6",
|
||||
"ipfs-coord": "^6.6.4",
|
||||
"jsonrpc-lite": "^2.2.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwt-bch-lib": "^1.3.0",
|
||||
"kcors": "^2.2.2",
|
||||
"koa": "^2.13.1",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
"koa-convert": "^2.0.0",
|
||||
"koa-generic-session": "^2.1.1",
|
||||
"koa-logger": "^3.2.1",
|
||||
"koa-mount": "^4.0.0",
|
||||
"koa-passport": "^4.1.3",
|
||||
"koa-router": "^10.0.0",
|
||||
"koa-static": "^5.0.0",
|
||||
"koa2-ratelimit": "^0.9.0",
|
||||
"line-reader": "^0.4.0",
|
||||
"minimal-slp-wallet": "^3.4.7",
|
||||
"mongoose": "^5.11.15",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@^2.6.7",
|
||||
"nodemailer": "^6.4.17",
|
||||
"passport-local": "^1.0.0",
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.0"
|
||||
"@chris.troutner/ipfs": "^2.0.2",
|
||||
"@psf/bch-js": "4.20.26",
|
||||
"axios": "^0.21.4",
|
||||
"bch-message-lib": "1.13.9",
|
||||
"bcryptjs": "2.4.3",
|
||||
"glob": "7.1.6",
|
||||
"ipfs-coord": "^6.7.4",
|
||||
"jsonrpc-lite": "2.2.0",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jwt-bch-lib": "1.3.0",
|
||||
"kcors": "2.2.2",
|
||||
"koa": "2.13.1",
|
||||
"koa-bodyparser": "4.3.0",
|
||||
"koa-convert": "2.0.0",
|
||||
"koa-generic-session": "2.1.1",
|
||||
"koa-logger": "3.2.1",
|
||||
"koa-mount": "4.0.0",
|
||||
"koa-passport": "4.1.3",
|
||||
"koa-router": "10.0.0",
|
||||
"koa-static": "5.0.0",
|
||||
"koa2-ratelimit": "0.9.0",
|
||||
"line-reader": "0.4.0",
|
||||
"minimal-slp-wallet": "3.4.7",
|
||||
"mongoose": "^5.13.13",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
"nodemailer": "6.4.17",
|
||||
"passport-local": "1.0.0",
|
||||
"winston": "3.3.3",
|
||||
"winston-daily-rotate-file": "4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "^0.26.0",
|
||||
"chai": "^4.3.0",
|
||||
"coveralls": "^3.1.0",
|
||||
"apidoc": "0.26.0",
|
||||
"chai": "4.3.0",
|
||||
"coveralls": "^2.11.4",
|
||||
"eslint": "7.19.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"husky": "^4.3.8",
|
||||
"mocha": "^8.2.1",
|
||||
"nyc": "^15.1.0",
|
||||
"semantic-release": "^17.4.4",
|
||||
"sinon": "^9.2.4",
|
||||
"standard": "^16.0.3",
|
||||
"uuid": "^8.3.2"
|
||||
"eslint-config-prettier": "7.2.0",
|
||||
"eslint-config-standard": "16.0.2",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"eslint-plugin-standard": "4.0.0",
|
||||
"husky": "4.3.8",
|
||||
"mocha": "^8.4.0",
|
||||
"nyc": "15.1.0",
|
||||
"semantic-release": "17.4.4",
|
||||
"sinon": "9.2.4",
|
||||
"standard": "^16.0.4",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"release": {
|
||||
"publish": [
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
// Load Mongoose models.
|
||||
const Users = require('./models/users')
|
||||
const Entries = require('./models/entry')
|
||||
const Order = require('./models/order')
|
||||
|
||||
class LocalDB {
|
||||
constructor () {
|
||||
// Encapsulate dependencies
|
||||
this.Users = Users
|
||||
this.Entry = Entries
|
||||
this.Order = Order
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ const Offer = new mongoose.Schema({
|
||||
sigMsg: { type: String },
|
||||
utxoTxid: { type: String },
|
||||
utxoVout: { type: Number },
|
||||
numTokens: { type: Number },
|
||||
|
||||
//
|
||||
offerIpfsId: { type: String },
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const Order = new mongoose.Schema({
|
||||
// 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 }
|
||||
})
|
||||
|
||||
module.exports = mongoose.model('order', Order)
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Adapter for the Order database model. Provides pagination when retrieving
|
||||
orders from the datatabase.
|
||||
*/
|
||||
|
||||
const Order = require('./models/order')
|
||||
|
||||
// let _this
|
||||
|
||||
class OrderPagination {
|
||||
constructor (localConfig = {}) {
|
||||
// Encapsulate dependencies
|
||||
this.Order = Order
|
||||
// _this = this
|
||||
}
|
||||
|
||||
// Read all entries in the P2WDB.
|
||||
async readAll (page = 0) {
|
||||
try {
|
||||
const ENTRIES_PER_PAGE = 20
|
||||
|
||||
// Pull data from MongoDB.
|
||||
// Get all entries in the database.
|
||||
const data = await this.Order.find({})
|
||||
// Sort entries so newest entries show first.
|
||||
.sort('-timestamp')
|
||||
// Skip to the start of the selected page.
|
||||
.skip(page * ENTRIES_PER_PAGE)
|
||||
// Only return 20 results.
|
||||
.limit(ENTRIES_PER_PAGE)
|
||||
|
||||
// console.log('data: ', data)
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Error in order-pagination.js/readAll()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderPagination
|
||||
@@ -25,7 +25,7 @@ class P2wdbAdapter {
|
||||
|
||||
const dataObj = {
|
||||
appId,
|
||||
offer: data,
|
||||
data,
|
||||
timestamp: now.toISOString(),
|
||||
localTimeStamp: now.toLocaleString()
|
||||
}
|
||||
|
||||
@@ -88,6 +88,8 @@ class WalletAdapter {
|
||||
|
||||
// This method returns an object that contains a private key WIF, public address,
|
||||
// and the index of the HD wallet that the key pair was generated from.
|
||||
// TODO: Allow input integer. If input is used, use that as the index. If no
|
||||
// input is provided, then call incrementNextAddress().
|
||||
async getKeyPair () {
|
||||
try {
|
||||
const hdIndex = await this.incrementNextAddress()
|
||||
@@ -105,6 +107,8 @@ class WalletAdapter {
|
||||
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
|
||||
|
||||
const cashAddress = this.bchWallet.bchjs.HDNode.toCashAddress(childNode)
|
||||
console.log('cashAddress: ', cashAddress)
|
||||
|
||||
const wif = this.bchWallet.bchjs.HDNode.toWIF(childNode)
|
||||
|
||||
const outObj = {
|
||||
@@ -218,7 +222,7 @@ class WalletAdapter {
|
||||
// txid: 'fakeTxid',
|
||||
// }
|
||||
} catch (err) {
|
||||
console.error('Error in burnPsf()')
|
||||
console.error('Error in burnPsf(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
+27
-2
@@ -7,6 +7,8 @@ const axios = require('axios')
|
||||
|
||||
let _this
|
||||
|
||||
const APPID = 'swapTest555'
|
||||
|
||||
class WebHook {
|
||||
constructor () {
|
||||
_this = this
|
||||
@@ -15,7 +17,7 @@ class WebHook {
|
||||
}
|
||||
|
||||
// REST petition to create a webhook in p2wdb-service
|
||||
async createWebHook (url) {
|
||||
async createWebhook (url) {
|
||||
try {
|
||||
if (!url || typeof url !== 'string') {
|
||||
throw new Error('url must be a string')
|
||||
@@ -24,7 +26,7 @@ class WebHook {
|
||||
const endpoint = _this.config.webhookService
|
||||
|
||||
const obj = {
|
||||
appId: 'swapTest555',
|
||||
appId: APPID,
|
||||
url
|
||||
}
|
||||
|
||||
@@ -36,6 +38,29 @@ class WebHook {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// REST petition to delete a webhook in p2wdb-service
|
||||
async deleteWebhook (url) {
|
||||
try {
|
||||
if (!url || typeof url !== 'string') {
|
||||
throw new Error('url must be a string')
|
||||
}
|
||||
|
||||
const endpoint = _this.config.webhookService
|
||||
|
||||
const obj = {
|
||||
appId: APPID,
|
||||
url
|
||||
}
|
||||
|
||||
const result = await axios.delete(endpoint, { data: obj })
|
||||
|
||||
return result.data
|
||||
} catch (err) {
|
||||
console.log('Error in adapters/webhook/deleteWebHook()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebHook
|
||||
|
||||
@@ -13,6 +13,7 @@ const ContactRESTController = require('./contact')
|
||||
const LogsRESTController = require('./logs')
|
||||
const EntryRouter = require('./entry')
|
||||
const OfferRouter = require('./offer')
|
||||
const OrderRouter = require('./order')
|
||||
|
||||
class RESTControllers {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -61,6 +62,9 @@ class RESTControllers {
|
||||
|
||||
const offerRouter = new OfferRouter(dependencies)
|
||||
offerRouter.attach(app)
|
||||
|
||||
const orderRouter = new OrderRouter(dependencies)
|
||||
orderRouter.attach(app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
REST API Controller library for the /offer route
|
||||
*/
|
||||
|
||||
// const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class OrderRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.OrderModel = this.adapters.localdb.Order
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// No api-doc documentation because this wont be a public endpoint
|
||||
async createOrder (ctx) {
|
||||
try {
|
||||
console.log('body: ', ctx.request.body)
|
||||
|
||||
const orderObj = ctx.request.body
|
||||
|
||||
await _this.useCases.order.createOrder(orderObj)
|
||||
|
||||
ctx.body = {
|
||||
success: true
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log(`err.message: ${err.message}`)
|
||||
// console.log('err: ', err)
|
||||
// ctx.throw(422, err.message)
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
console.log('err', err.message)
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderRESTControllerLib
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
REST API library for /offer route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const OrderRESTControllerLib = require('./controller')
|
||||
|
||||
let _this
|
||||
|
||||
class OrderRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.orderRESTController = new OrderRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/order'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attached REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', _this.orderRESTController.createOrder)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(_this.router.routes())
|
||||
app.use(_this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderRouter
|
||||
@@ -2,7 +2,7 @@
|
||||
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 Order generated by this application.
|
||||
to track an Order generated by this application.
|
||||
|
||||
The Offer tracks the hdIndex address used to hold tokens or BCH for sale.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
*/
|
||||
+71
-19
@@ -2,23 +2,75 @@
|
||||
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
|
||||
}
|
||||
*/
|
||||
class OrderEntity {
|
||||
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
|
||||
} = orderData.data
|
||||
|
||||
// Input Validation
|
||||
if (!messageType || typeof messageType !== 'number') {
|
||||
throw new Error("Property 'messageType' must be an integer number.")
|
||||
}
|
||||
if (!messageClass || typeof messageClass !== 'number') {
|
||||
throw new Error("Property 'messageClass' must be an integer number.")
|
||||
}
|
||||
if (!tokenId || typeof tokenId !== 'string') {
|
||||
throw new Error("Property 'tokenId' must be a string.")
|
||||
}
|
||||
if (!buyOrSell || typeof buyOrSell !== 'string') {
|
||||
throw new Error("Property 'buyOrSell' must be a string.")
|
||||
}
|
||||
if (!rateInSats || typeof rateInSats !== 'number') {
|
||||
throw new Error("Property 'rateInSats' must be an integer number.")
|
||||
}
|
||||
if (!minSatsToExchange || typeof minSatsToExchange !== 'number') {
|
||||
throw new Error("Property 'minSatsToExchange' must be an integer number.")
|
||||
}
|
||||
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.")
|
||||
}
|
||||
|
||||
const validatedOrderData = {
|
||||
messageType,
|
||||
messageClass,
|
||||
tokenId,
|
||||
buyOrSell,
|
||||
rateInSats,
|
||||
minSatsToExchange,
|
||||
numTokens,
|
||||
utxoTxid,
|
||||
utxoVout,
|
||||
timestamp: orderData.timestamp,
|
||||
localTimestamp: orderData.localTimeStamp,
|
||||
txid: orderData.txid,
|
||||
p2wdbHash: orderData.hash
|
||||
}
|
||||
|
||||
return validatedOrderData
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderEntity
|
||||
|
||||
@@ -14,7 +14,7 @@ class EntryLib {
|
||||
// Encapsulate dependencies
|
||||
this.EntryEntity = new EntryEntiy()
|
||||
this.EntryModel = this.adapters.localdb.Entry
|
||||
this.bchjs = this.adapters.bchjs
|
||||
this.bch = this.adapters.bch
|
||||
}
|
||||
|
||||
// Create a new entry model and add it to the Mongo database.
|
||||
@@ -24,20 +24,22 @@ class EntryLib {
|
||||
const entryEntity = this.EntryEntity.validate(entryObj)
|
||||
|
||||
// Verify that the entry was signed by a specific BCH address.
|
||||
const isValidSignature = this.bchjs._verifySignature(entryEntity)
|
||||
const isValidSignature = this.bch._verifySignature(entryEntity)
|
||||
if (!isValidSignature) {
|
||||
throw new Error('Invalid signature')
|
||||
}
|
||||
|
||||
// Verify psf tokens balance
|
||||
|
||||
const psfBalance = await this.bchjs.getPSFTokenBalance(entryEntity.slpAddress)
|
||||
const psfBalance = await this.bch.getPSFTokenBalance(
|
||||
entryEntity.slpAddress
|
||||
)
|
||||
|
||||
if (psfBalance < 10) {
|
||||
throw new Error('Insufficient psf balance')
|
||||
}
|
||||
|
||||
const merit = await this.bchjs.getMerit(entryEntity.slpAddress)
|
||||
const merit = await this.bch.getMerit(entryEntity.slpAddress)
|
||||
|
||||
const updatedEntry = {
|
||||
entry: entryEntity.entry.trim(),
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
const UserUseCases = require('./user')
|
||||
const EntryUseCases = require('./entry')
|
||||
const OfferUseCases = require('./offer')
|
||||
const OrderUseCases = require('./order')
|
||||
|
||||
class UseCases {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -21,6 +22,7 @@ class UseCases {
|
||||
this.user = new UserUseCases(localConfig)
|
||||
this.entry = new EntryUseCases(localConfig)
|
||||
this.offer = new OfferUseCases(localConfig)
|
||||
this.order = new OrderUseCases(localConfig)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+36
-15
@@ -31,18 +31,22 @@ class OfferLib {
|
||||
await this.ensureFunds(offerEntity)
|
||||
|
||||
// Move the tokens to holding address.
|
||||
// const utxoInfo = await moveTokens(offerEntity)
|
||||
const utxoInfo = await this.moveTokens(offerEntity)
|
||||
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
|
||||
|
||||
// Burn PSF token to pay for P2WDB write.
|
||||
const txid = await this.adapters.wallet.burnPsf()
|
||||
console.log('burn txid: ', txid)
|
||||
console.log(`https://simpleledger.info/tx/${txid}`)
|
||||
|
||||
// TODO: Move tokens to holding address, which will generate the UTXO to
|
||||
// use in the Offer.
|
||||
|
||||
// generate signature.
|
||||
const now = new Date()
|
||||
const message = now.toISOString()
|
||||
@@ -69,6 +73,34 @@ class OfferLib {
|
||||
}
|
||||
}
|
||||
|
||||
// Move the tokens indicated in the offer 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) {
|
||||
try {
|
||||
const keyPair = await this.adapters.wallet.getKeyPair()
|
||||
console.log('keyPair: ', keyPair)
|
||||
|
||||
const receiver = {
|
||||
address: keyPair.cashAddress,
|
||||
tokenId: offerEntity.tokenId,
|
||||
qty: offerEntity.numTokens
|
||||
}
|
||||
|
||||
const txid = await this.adapters.wallet.bchWallet.sendTokens(receiver, 3)
|
||||
|
||||
const utxoInfo = {
|
||||
txid,
|
||||
vout: 0
|
||||
}
|
||||
|
||||
return utxoInfo
|
||||
} catch (err) {
|
||||
console.error('Error in moveTokens(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the wallet has enough BCH and tokens to complete the requested
|
||||
// trade.
|
||||
async ensureFunds (offerEntity) {
|
||||
@@ -111,17 +143,6 @@ class OfferLib {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Move the tokens indicated in the offer 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 () {
|
||||
try {
|
||||
//
|
||||
} catch (err) {
|
||||
console.error('Error in moveTokens()')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OfferLib
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Use Case library for Orders.
|
||||
Orders are created by a webhook trigger from the P2WDB. Orders 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.
|
||||
*/
|
||||
|
||||
const OrderEntity = require('../../entities/order')
|
||||
|
||||
class OrderUseCases {
|
||||
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.'
|
||||
)
|
||||
}
|
||||
|
||||
this.orderEntity = new OrderEntity()
|
||||
this.OrderModel = this.adapters.localdb.Order
|
||||
}
|
||||
|
||||
// This method is called by the POST /order REST API controller, which is
|
||||
// triggered by a P2WDB webhook.
|
||||
async createOrder (orderObj) {
|
||||
try {
|
||||
console.log('Use Case createOrder(orderObj): ', orderObj)
|
||||
|
||||
// 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
|
||||
const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
|
||||
txid,
|
||||
vout
|
||||
)
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
if (utxoStatus === null) return false
|
||||
|
||||
const orderEntity = this.orderEntity.validate(orderObj)
|
||||
console.log('orderEntity: ', orderEntity)
|
||||
|
||||
// Add order to the local database.
|
||||
const orderModel = new this.OrderModel(orderEntity)
|
||||
await orderModel.save()
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in createOrder()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderUseCases
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
This library leverages the p-retry and p-queue libraries, to create a
|
||||
validation queue with automatic retry.
|
||||
|
||||
New nodes syncing will attempt to rapidly validate a lot of entries.
|
||||
A promise-based queue allows this to happen while respecting rate-limits
|
||||
of the blockchain service provider.
|
||||
|
||||
pay-to-write-access-controller.js depends on this library.
|
||||
*/
|
||||
|
||||
const PQueue = require('p-queue').default
|
||||
const pRetry = require('p-retry')
|
||||
|
||||
let _this
|
||||
|
||||
class RetryQueue {
|
||||
constructor (localConfig = {}) {
|
||||
if (!localConfig.bchjs) {
|
||||
throw new Error(
|
||||
'Must pass instance of bch-js when instantiating RetryQueue Class.'
|
||||
)
|
||||
}
|
||||
this.bchjs = localConfig.bchjs
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.validationQueue = new PQueue({ concurrency: 1 })
|
||||
this.pRetry = pRetry
|
||||
|
||||
this.attempts = 5
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// Add an async function to the queue, and execute it with the input object.
|
||||
async addToQueue (funcHandle, inputObj) {
|
||||
try {
|
||||
console.log('addToQueue inputObj: ', inputObj)
|
||||
|
||||
if (!funcHandle) {
|
||||
throw new Error('function handler is required')
|
||||
}
|
||||
if (!inputObj) {
|
||||
throw new Error('input object is required')
|
||||
}
|
||||
|
||||
const returnVal = await _this.validationQueue.add(() =>
|
||||
_this.retryWrapper(funcHandle, inputObj)
|
||||
)
|
||||
return returnVal
|
||||
} catch (err) {
|
||||
console.error('Error in addToQueue(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the p-retry library.
|
||||
// This function returns a promise that will resolve to the output of the
|
||||
// function 'funcHandle'.
|
||||
async retryWrapper (funcHandle, inputObj) {
|
||||
try {
|
||||
console.log('retryWrapper inputObj: ', inputObj)
|
||||
|
||||
if (!funcHandle) {
|
||||
throw new Error('function handler is required')
|
||||
}
|
||||
if (!inputObj) {
|
||||
throw new Error('input object is required')
|
||||
}
|
||||
console.log('Entering retryWrapper()')
|
||||
|
||||
// Add artificial delay to prevent 429 errors.
|
||||
await this.bchjs.Util.sleep(2000)
|
||||
|
||||
return this.pRetry(
|
||||
async () => {
|
||||
return await funcHandle(inputObj)
|
||||
},
|
||||
{
|
||||
onFailedAttempt: _this.handleValidationError,
|
||||
retries: this.attempts // Retry 5 times
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('Error in retryWrapper: ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Notifies the user that an error occured and that a retry will be attempted.
|
||||
// It tracks the number of retries until it fails.
|
||||
async handleValidationError (error) {
|
||||
try {
|
||||
const errorMsg = `Attempt ${error.attemptNumber} to validate entry. There are ${error.retriesLeft} retries left. Waiting before trying again.`
|
||||
console.log(errorMsg)
|
||||
const SLEEP_TIME = 30000
|
||||
console.log(`Waiting ${SLEEP_TIME} milliseconds before trying again.\n`)
|
||||
await _this.bchjs.Util.sleep(SLEEP_TIME) // 30 sec
|
||||
} catch (err) {
|
||||
console.error('Error in handleValidationError(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RetryQueue
|
||||
@@ -19,7 +19,7 @@ async function start () {
|
||||
buyOrSell: 'sell',
|
||||
rateInSats: 1000,
|
||||
minSatsToExchange: 10,
|
||||
numTokens: 0.1
|
||||
numTokens: 0.02
|
||||
}
|
||||
|
||||
const options = {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Unit tests for the order-pagination.js library.
|
||||
|
||||
These unit tests will probably be rolled into a larger library in the
|
||||
the future.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
// Local libraries.
|
||||
const OrderPagination = require('../../../src/adapters/localdb/order-pagination')
|
||||
const Order = require('../../../src/adapters/localdb/models/order')
|
||||
const config = require('../../../config')
|
||||
|
||||
describe('#OrderPagination', () => {
|
||||
let uut, sandbox
|
||||
|
||||
before(async () => {
|
||||
// Connect to the Mongo Database.
|
||||
console.log(`Connecting to database: ${config.database}`)
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
|
||||
await mongoose.connect(config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
|
||||
const order = new Order({
|
||||
lokadId: 'lokadId',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'tokenId',
|
||||
buyOrSell: 'buy',
|
||||
rateInSats: 'rateInSats',
|
||||
minSatsToExchange: '100',
|
||||
signature: 'signature',
|
||||
sigMsg: 'sigMsg',
|
||||
utxoTxid: 'utxoTxid',
|
||||
utxoVout: 1,
|
||||
numTokens: 1,
|
||||
timestamp: 'timestamp',
|
||||
localTimestamp: 'localTimestamp',
|
||||
txid: 'txid',
|
||||
p2wdbHash: 'p2wdbHash'
|
||||
})
|
||||
await order.save()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new OrderPagination()
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
after(() => {
|
||||
mongoose.connection.close()
|
||||
})
|
||||
|
||||
describe('#readAll', () => {
|
||||
it('should get the first page of Order entries', async () => {
|
||||
const data = await uut.readAll()
|
||||
// console.log('data: ', data)
|
||||
|
||||
assert.isArray(data)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force and error.
|
||||
sandbox.stub(uut.Order, 'find').rejects(new Error('test error'))
|
||||
|
||||
await uut.readAll()
|
||||
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'is not a function')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -24,7 +24,7 @@ describe('#Webhook-Adapter', () => {
|
||||
describe('#createWebHook', () => {
|
||||
it('should throw error if input is not provided', async () => {
|
||||
try {
|
||||
await uut.createWebHook()
|
||||
await uut.createWebhook()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'url must be a string')
|
||||
@@ -33,7 +33,7 @@ describe('#Webhook-Adapter', () => {
|
||||
|
||||
it('should throw error if input is not string', async () => {
|
||||
try {
|
||||
await uut.createWebHook(1)
|
||||
await uut.createWebhook(1)
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'url must be a string')
|
||||
@@ -43,7 +43,7 @@ describe('#Webhook-Adapter', () => {
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.axios, 'post').throws(new Error('test error'))
|
||||
await uut.createWebHook('https://test.com/my-webhook')
|
||||
await uut.createWebhook('https://test.com/my-webhook')
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
@@ -56,7 +56,7 @@ describe('#Webhook-Adapter', () => {
|
||||
.resolves({ data: { success: true, id: '61018c8c9a71973a596cdccb' } })
|
||||
|
||||
const url = 'https://test.com/my-webhook'
|
||||
const result = await uut.createWebHook(url)
|
||||
const result = await uut.createWebhook(url)
|
||||
|
||||
assert.isObject(result)
|
||||
assert.property(result, 'success')
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /order endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const adapters = require('../../../mocks/adapters')
|
||||
const UseCasesMock = require('../../../mocks/use-cases')
|
||||
// const app = require('../../../mocks/app-mock')
|
||||
|
||||
const OrderRESTController = require('../../../../../src/controllers/rest-api/order/controller')
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Order-REST-Router', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new OrderRESTController({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new OrderRESTController()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new OrderRESTController({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
// sandbox.stub(uut.useCases.order, 'createOrder').resolves()
|
||||
|
||||
await uut.createOrder(ctx)
|
||||
|
||||
// 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')
|
||||
// }
|
||||
// })
|
||||
})
|
||||
|
||||
describe('#handleError', () => {
|
||||
it('should still throw error if there is no message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 404
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Not Found')
|
||||
}
|
||||
})
|
||||
it('should catch error if message is provided', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 422,
|
||||
message: 'Unprocessable Entity'
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Unprocessable Entity')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const adapters = require('../../../mocks/adapters')
|
||||
const UseCasesMock = require('../../../mocks/use-cases')
|
||||
// const app = require('../../../mocks/app-mock')
|
||||
|
||||
const OrderRouter = require('../../../../../src/controllers/rest-api/order')
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Order-REST-Router', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new OrderRouter({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
// ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new OrderRouter()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new OrderRouter({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating /order REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#attach', () => {
|
||||
it('should throw an error if app is not passed in.', () => {
|
||||
try {
|
||||
uut.attach()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass app object when attached REST API controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
Unit tests for the Order entity library.
|
||||
*/
|
||||
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const Order = require('../../../src/entities/order')
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
|
||||
describe('#Order-Entity', () => {
|
||||
before(async () => {})
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new Order()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#validate', () => {
|
||||
it('should throw an error if data is not provided', () => {
|
||||
try {
|
||||
uut.validate()
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
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 orderData = { data: {} }
|
||||
uut.validate(orderData)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'messageType' must be an integer number"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if messageClass is not included', () => {
|
||||
try {
|
||||
const orderData = { data: { messageType: 1 } }
|
||||
uut.validate(orderData)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'messageClass' must be an integer number"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if tokenId is not included', () => {
|
||||
try {
|
||||
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.")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if buyOrSell is not included', () => {
|
||||
try {
|
||||
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.")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if rateInSats is not included', () => {
|
||||
try {
|
||||
const orderData = {
|
||||
data: {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy'
|
||||
}
|
||||
}
|
||||
|
||||
uut.validate(orderData)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'rateInSats' must be an integer number."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if minSatsToExchange is not included', () => {
|
||||
try {
|
||||
const orderData = {
|
||||
data: {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy',
|
||||
rateInSats: 1000
|
||||
}
|
||||
}
|
||||
|
||||
uut.validate(orderData)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'minSatsToExchange' must be an integer number."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,10 @@
|
||||
/*
|
||||
Mocks for the Adapter library.
|
||||
*/
|
||||
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
const ipfs = {
|
||||
ipfsAdapter: {
|
||||
ipfs: {}
|
||||
@@ -60,13 +64,25 @@ const localdb = {
|
||||
static find () {}
|
||||
static findOne () {}
|
||||
|
||||
async save () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
|
||||
Order: class Order {
|
||||
constructor (obj) {}
|
||||
|
||||
static findById () {}
|
||||
static find () {}
|
||||
static findOne () {}
|
||||
|
||||
async save () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bchjs = {
|
||||
const bch = {
|
||||
getMerit: async () => {
|
||||
return 100
|
||||
},
|
||||
@@ -86,6 +102,9 @@ const { MockBchWallet } = require('./wallet')
|
||||
const wallet = {
|
||||
burnPsf: async () => {},
|
||||
generateSignature: async () => {},
|
||||
getKeyPair: async () => {
|
||||
return { cashAddress: 'fakeAddr', wif: 'fakeWif', hdIndex: 1 }
|
||||
},
|
||||
bchWallet: new MockBchWallet()
|
||||
}
|
||||
|
||||
@@ -93,4 +112,4 @@ const p2wdb = {
|
||||
write: async () => {}
|
||||
}
|
||||
|
||||
module.exports = { ipfs, localdb, bchjs, wallet, p2wdb }
|
||||
module.exports = { ipfs, localdb, bch, wallet, p2wdb, bchjs }
|
||||
|
||||
@@ -26,6 +26,10 @@ class MockBchWallet {
|
||||
this.burnTokens = async () => {
|
||||
return { success: true, txid: 'txid' }
|
||||
}
|
||||
this.sendTokens = async () => {
|
||||
return 'fakeTxid'
|
||||
}
|
||||
this.getUtxos = async () => {}
|
||||
|
||||
// Environment variable is used by wallet-balance.unit.js to force an error.
|
||||
if (process.env.NO_UTXO) {
|
||||
|
||||
@@ -43,6 +43,12 @@ class Offer {
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
async createOrder() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
class UseCasesMock {
|
||||
constuctor(localConfig = {}) {
|
||||
// this.user = new UserUseCaseMock(localConfig)
|
||||
@@ -51,6 +57,7 @@ class UseCasesMock {
|
||||
user = new UserUseCaseMock()
|
||||
entry = new EntryUseCaseMock()
|
||||
offer = new Offer()
|
||||
order = new Order()
|
||||
}
|
||||
|
||||
module.exports = UseCasesMock
|
||||
|
||||
@@ -89,13 +89,15 @@ describe('#users-use-case', () => {
|
||||
assert.include(err.message, "Property 'signature' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if category is not provided', async () => {
|
||||
try {
|
||||
const inputData = {
|
||||
entry: 'entry',
|
||||
description: 'test',
|
||||
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
|
||||
signature: 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw='
|
||||
signature:
|
||||
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw='
|
||||
}
|
||||
await uut.createEntry(inputData)
|
||||
assert.fail('Unexpected code path')
|
||||
@@ -103,6 +105,7 @@ describe('#users-use-case', () => {
|
||||
assert.include(err.message, "Property 'category' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should catch and throw DB errors', async () => {
|
||||
try {
|
||||
// Force an error with the database.
|
||||
@@ -112,7 +115,8 @@ describe('#users-use-case', () => {
|
||||
entry: 'entry',
|
||||
description: 'test',
|
||||
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
|
||||
signature: 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
|
||||
signature:
|
||||
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
|
||||
category: 'test'
|
||||
}
|
||||
|
||||
@@ -123,16 +127,20 @@ describe('#users-use-case', () => {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if signature is invalid', async () => {
|
||||
try {
|
||||
// Mocking bchjs functions
|
||||
sandbox.stub(uut.bchjs, '_verifySignature').callsFake(() => { return false })
|
||||
sandbox.stub(uut.bch, '_verifySignature').callsFake(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
const inputData = {
|
||||
entry: 'entry',
|
||||
description: 'test',
|
||||
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
|
||||
signature: 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
|
||||
signature:
|
||||
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
|
||||
category: 'test'
|
||||
}
|
||||
|
||||
@@ -146,13 +154,14 @@ describe('#users-use-case', () => {
|
||||
it('should throw error for insufficient psf balance', async () => {
|
||||
try {
|
||||
// Mocking bchjs functions
|
||||
sandbox.stub(uut.bchjs, 'getPSFTokenBalance').resolves(0)
|
||||
sandbox.stub(uut.bch, 'getPSFTokenBalance').resolves(0)
|
||||
|
||||
const inputData = {
|
||||
entry: 'entry',
|
||||
description: 'test',
|
||||
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
|
||||
signature: 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
|
||||
signature:
|
||||
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
|
||||
category: 'test'
|
||||
}
|
||||
|
||||
@@ -174,7 +183,8 @@ describe('#users-use-case', () => {
|
||||
entry: 'entry',
|
||||
description: 'test',
|
||||
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
|
||||
signature: 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
|
||||
signature:
|
||||
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
|
||||
category: 'test'
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ describe('#offer-use-case', () => {
|
||||
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 })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves()
|
||||
sandbox
|
||||
.stub(uut.adapters.wallet, 'generateSignature')
|
||||
.resolves('fakeSignature')
|
||||
@@ -109,4 +111,34 @@ describe('#offer-use-case', () => {
|
||||
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, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
Unit tests for the Order Use Case library.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
// const testUtils = require('../../utils/test-utils')
|
||||
|
||||
// Unit under test (uut)
|
||||
const OrderLib = require('../../../src/use-cases/order')
|
||||
const adapters = require('../mocks/adapters')
|
||||
|
||||
describe('#order-use-case', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
before(async () => {
|
||||
// Delete all previous users in the database.
|
||||
// await testUtils.deleteAllUsers()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
uut = new OrderLib({ adapters })
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new OrderLib()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
console.log(uut) // linter
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of adapters must be passed in when instantiating Order Use Cases library.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
// Force test environment
|
||||
// make sure environment variable is set before this file gets called.
|
||||
// see test script in package.json.
|
||||
// process.env.KOA_ENV = 'test'
|
||||
const config = require('../../config')
|
||||
|
||||
const User = require('../../src/models/users')
|
||||
|
||||
async function deleteUsers () {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
|
||||
// Get all the users in the DB.
|
||||
const users = await User.find({}, '-password')
|
||||
// console.log(`users: ${JSON.stringify(users, null, 2)}`)
|
||||
|
||||
// Delete each user.
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const thisUser = users[i]
|
||||
await thisUser.remove()
|
||||
}
|
||||
|
||||
mongoose.connection.close()
|
||||
}
|
||||
|
||||
deleteUsers()
|
||||
@@ -0,0 +1,21 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
const Order = require('../../src/adapters/localdb/models/order')
|
||||
|
||||
async function getOrder () {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(config.database, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
})
|
||||
|
||||
const orders = await Order.find({})
|
||||
console.log(`orders: ${JSON.stringify(orders, null, 2)}`)
|
||||
|
||||
mongoose.connection.close()
|
||||
}
|
||||
getOrder()
|
||||
Reference in New Issue
Block a user