Compare commits

...
76 Commits
Author SHA1 Message Date
Chris Troutner 2d9a1d7cbf Merge pull request #10 from Permissionless-Software-Foundation/ct-unstable
Update to work better with UI
2022-03-25 17:53:58 -07:00
Chris Troutner e6c942d532 Fixing tests 2022-03-25 12:36:28 -07:00
Chris Troutner 1206b6576e Got first successful take workflow complete 2022-03-25 12:17:13 -07:00
Chris Troutner dbd5845e3a feat(offers): Adding garbage collection 2022-03-24 06:57:39 -07:00
Chris Troutner d14dcae6f5 feat(orders): Added garbage collection 2022-03-24 06:43:51 -07:00
Chris Troutner 6b44aab429 Merge pull request #9 from Permissionless-Software-Foundation/ct-unstable
Updating specifications
2022-03-23 16:20:27 -07:00
Chris Troutner 8cd8086440 Fixing test 2022-03-23 16:17:26 -07:00
Chris Troutner 37e2377113 Editing 2022-03-23 16:05:36 -07:00
Chris Troutner a6949f0e8d fix(docs): Updating dev docs and spec 2022-03-23 15:52:34 -07:00
Chris Troutner 9e14f0a230 Merge pull request #8 from Permissionless-Software-Foundation/ct-unstable
First successful complete trade
2022-03-23 11:26:38 -07:00
Chris Troutner 7e6b76703d Fixing unit tests 2022-03-23 11:23:51 -07:00
Chris Troutner 5910620048 First successful collaborative transaction and token trade 2022-03-23 11:04:48 -07:00
Chris Troutner 4201850389 Added makerAddr to Order and Offer 2022-03-22 11:05:31 -07:00
Chris Troutner 449efd2a98 Fixing unit tests 2022-03-22 10:05:50 -07:00
Chris Troutner 5af8a31242 Successfully deserializing partial tx during acceptance phase 2022-03-22 10:02:00 -07:00
Chris Troutner 65bffe1ef6 Successfully detecting and routing offers and counter-offers 2022-03-22 08:00:08 -07:00
Chris Troutner ceafb2820d Adding 'dataType' to P2WDB data, to distinguish between 'offer' and 'counter-offer' data 2022-03-22 06:46:03 -07:00
Chris Troutner 90d1e68dae Refactored partial transaction creation 2022-03-22 06:13:01 -07:00
Chris Troutner 57dfad9215 Got moveBCH() working in takeOffer() 2022-03-21 17:07:12 -07:00
Chris Troutner 212bc65c7e feat(moveBch): Function to create segregated BCH UTXOs 2022-03-21 16:28:09 -07:00
Chris Troutner 810d386dbc fix(moveTokens): Moved from order use-case to wallet adapter 2022-03-21 14:03:40 -07:00
Chris Troutner 8ed1dbcffe Renamed database model properties 2022-03-21 13:08:14 -07:00
Chris Troutner 1563a740aa fix(offer): Added entity biz logic and unit tests for offer status 2022-03-21 11:39:23 -07:00
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
Chris Troutner a904cbb473 Merge pull request #5 from Permissionless-Software-Foundation/ct-unstable
Got /order/take to generate a partial TX
2022-03-11 08:29:12 -08:00
Chris Troutner 84dfd56918 Fixing dependencies 2022-03-11 08:25:35 -08:00
Chris Troutner d62fc3b575 Got /order/take mocked up for further development 2022-03-11 08:15:31 -08:00
Chris Troutner a7f8a520a2 Got /order/take to generate a partial TX 2022-03-11 06:35:56 -08:00
Chris Troutner 87b9046af8 fix(offer): Fixing bug. vout = 1 2022-03-10 15:50:13 -08:00
Chris Troutner a6b4677571 Working on take-offer logic 2022-03-10 13:21:25 -08:00
Chris Troutner bf71810974 Adding wallet sweep script for cleaning up test wallets 2022-03-10 10:33:34 -08:00
Chris Troutner e68514f542 linting 2022-03-09 14:25:51 -08:00
Chris Troutner 5fc26dcc7c fix(order): Adding 'orderStatus' to Order model 2022-03-09 14:25:37 -08:00
Chris Troutner fb00c1bc86 Merge pull request #4 from Permissionless-Software-Foundation/ct-unstable
Getting Offer and Order creation working with updated deps.
2022-03-09 13:55:56 -08:00
Chris Troutner 7964bfbae7 fix(tests): Fixing broken unit tests 2022-03-09 13:52:45 -08:00
Chris Troutner a13a1c91e8 Got Offer creation working 2022-03-09 10:08:51 -08:00
Chris Troutner b19d4da345 Added integration tests for offer use case 2022-03-09 09:37:17 -08:00
Chris Troutner 443f405f56 fix(package.json): Updating dependencies 2022-03-09 08:11:20 -08:00
Chris Troutner 61bb5169b3 fix(p2wdb): Refactored p2wdb adapter and unit tests 2022-03-09 07:53:26 -08:00
Chris Troutner f812440555 fix(tests): Updated unit tests for node v16 2022-03-08 15:23:56 -08:00
Chris Troutner 693c98fb15 Merge branch 'master' of https://github.com/Permissionless-Software-Foundation/ipfs-service-provider into ct-unstable 2022-03-08 15:04:53 -08:00
Chris Troutner fdf73610eb Merge pull request #79 from Permissionless-Software-Foundation/ct-unstable
fix(tests): Updating tests for node.js v16
2022-03-08 09:42:42 -08:00
Chris Troutner b09e77c079 fix(tests): Updating tests for node.js v16 2022-03-08 09:39:54 -08:00
Chris Troutner 1a4c602234 Syncing with upstream ipfs-service-provider 2022-03-07 17:58:03 -08:00
Chris Troutner dedc6e2d30 syncing with upstream ipfs-service-provider 2022-03-07 17:56:52 -08:00
Chris Troutner 68ad2561b1 Merge pull request #78 from Permissionless-Software-Foundation/ct-unstable
fix(ipfs-coord): Updating to v7.1.4
2022-03-05 14:22:25 -08:00
Chris Troutner 791d11e597 Merge branch 'master' into ct-unstable 2022-03-05 14:20:19 -08:00
Chris Troutner 5489e76f55 fix(ipfs-coord): Updating to v7.1.4 2022-03-05 14:20:13 -08:00
Chris Troutner 496a1dd07c Merge pull request #77 from Permissionless-Software-Foundation/ct-unstable
fix(ipfs-coord): Updating to v7.1.3
2022-03-05 08:26:32 -08:00
Chris Troutner a7b8ca9ad5 linting 2022-03-05 08:24:50 -08:00
Chris Troutner 6eb16eb11a fix(ipfs-coord): Updating to v7.1.3 2022-03-05 08:24:36 -08:00
Chris Troutner d1fb676a8f Customizing for bch 2022-03-01 18:36:15 -08:00
Chris Troutner 649655c7bb Merge pull request #3 from Permissionless-Software-Foundation/ct-unstable
fix(e2e): Got e2e create-offer.js working
2022-02-23 15:25:47 -08:00
Chris Troutner e7e6f31c96 Got webhooks Order creation working 2022-02-23 15:23:37 -08:00
Chris Troutner f630b61343 Fixed bug in unit test 2022-02-23 14:58:32 -08:00
Chris Troutner f4a1f77077 fix(e2e): Got e2e create-offer.js working 2022-02-23 14:03:57 -08:00
Chris Troutner 956c67a288 Updating dev docs 2022-02-23 11:54:22 -08:00
Chris Troutner 827f094c94 Updating dev docs 2022-02-23 10:40:47 -08:00
Chris Troutner 41306cc943 Updating dev docs 2022-02-23 10:37:56 -08:00
Chris Troutner b5b86aa525 Updating dev docs 2022-02-23 10:07:00 -08:00
Chris Troutner 075676aec1 Merge pull request #2 from Permissionless-Software-Foundation/ct-unstable
fix(instanceWallet): Refactored to use web 3 infra
2022-02-23 09:23:14 -08:00
Chris Troutner f6646f8658 fix(instanceWallet): Refactored to use web 3 infra 2022-02-23 09:21:23 -08:00
Chris Troutner aa0a8a862c Got tests running 2022-02-23 08:38:16 -08:00
Chris Troutner 44c02beb3e Changing name of appID in p2wdb 2022-02-23 07:42:57 -08:00
Chris Troutner 10fb87d465 Merge pull request #1 from Permissionless-Software-Foundation/ct-unstable
fix(LICENSE): Updating for 2022
2022-02-23 07:07:15 -08:00
Chris Troutner 03942d5232 fix(LICENSE): Updating for 2022 2022-02-23 07:01:24 -08:00
72 changed files with 26751 additions and 7535 deletions
+8
View File
@@ -0,0 +1,8 @@
[
{
"srcDir": "",
"destDir": "",
"files": "**/*.js",
"command": "npm run lint"
}
]
+1 -1
View File
@@ -1,5 +1,5 @@
The MIT License (MIT)
Copyright (c) 2021 Permissionless Software Foundation
Copyright (c) 2022 Permissionless Software Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+2 -2
View File
@@ -94,13 +94,13 @@ class Server {
try {
try {
// Delete an old webhook if it exists.
await webhookLib.deleteWebhook('http://localhost:5700/order')
await webhookLib.deleteWebhook(`http://localhost:${config.port}/p2wdb`)
} catch (err) {
/* exit quietly */
// console.log('err deleting webhook: ', err)
}
await webhookLib.createWebhook('http://localhost:5700/order')
await webhookLib.createWebhook(`http://localhost:${config.port}/p2wdb`)
console.log('Webhook created')
} catch (error) {
console.log('Webhook cant be created')
+15 -4
View File
@@ -15,7 +15,7 @@ const ipfsCoordName = process.env.COORD_NAME
module.exports = {
// Configure TCP port.
port: process.env.PORT || 5002,
port: process.env.PORT || 5700,
// Password for HTML UI that displays logs.
logPass: 'test',
@@ -31,6 +31,16 @@ module.exports = {
? process.env.EMAILPASS
: 'emailpassword',
// PSF Web 3 community infrastructure
useFullStackCash: process.env.USE_FULLSTACKCASH ? true : false,
consumerUrl: process.env.CONSUMER_URL
? process.env.CONSUMER_URL
// : 'https://free-bch.fullstack.cash',
: 'https://wa-usa-bch-consumer.fullstackcash.nl',
// P2WDB URL that will accept API calls from the p2wdb npm library.
p2wdbUrl: process.env.P2WDB_URL ? process.env.P2WDB_URL : 'https://p2wdb.fullstack.cash',
// FullStack.cash account information, used for automatic JWT handling.
getJwtAtStartup: process.env.GET_JWT_AT_STARTUP ? true : false,
authServer: process.env.AUTHSERVER
@@ -61,8 +71,7 @@ module.exports = {
name: ipfsCoordName,
version,
protocol: 'generic-service',
description:
'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider',
description: 'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider',
documentation: 'https://ipfs-service-provider.fullstack.cash/',
provider: {
'@type': 'Organization',
@@ -72,9 +81,11 @@ module.exports = {
},
// P2WDB webhook endpoint
p2wdbPort: process.env.P2WDB_PORT ? process.env.P2WDB_PORT : 5010,
webhookService: process.env.WEBHOOKSERVICE
? process.env.WEBHOOKSERVICE
: 'http://localhost:5001/webhook', // P2WDB.
: 'http://localhost:5010/webhook', // P2WDB.
p2wdbAppId: process.env.APP_ID ? process.env.APP_ID : 'bch-dex-test556',
// IPFS Ports
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 4001,
+1 -1
View File
@@ -7,6 +7,6 @@
module.exports = {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://localhost:27017/swap-service-dev',
database: 'mongodb://localhost:27017/bch-swap-service-dev',
env: 'dev'
}
+1 -1
View File
@@ -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/swap-service-prod',
: 'mongodb://172.17.0.1:5555/bch-swap-service-prod',
env: 'prod'
}
+1 -1
View File
@@ -7,6 +7,6 @@
module.exports = {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://localhost:27017/swap-service-test',
database: 'mongodb://localhost:27017/bch-swap-service-test',
env: 'test'
}
+57 -22
View File
@@ -1,30 +1,45 @@
# 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 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.
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 the `bch-dex` 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 ipfs-swap-service concept. They work together to form a censorship-resistant application for exchanging transaction data for building trades.
There are three major pieces of software behind the bch-dex concept. They work together to form a censorship-resistant application for exchanging transaction data for building trades.
![ipfs-swap-service major subcomponents](./diagrams/software-interaction.png)
![bch-dex major subcomponents](./diagrams/software-interaction.png)
- _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.
- _Client_ could be a web browser like [wallet.fullstack.cash](https://bchn-wallet.fullstack.cash), 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).
- [bch-dex](https://github.com/Permissionless-Software-Foundation/bch-dex) 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:
- 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).
- 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 _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 `ipfs-swap-service` on the network, and to empower individual traders to run their own, private copy.
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 `ipfs-swap-service` and P2WDB back end software.
This section provides additional information on `bch-dex` and P2WDB back end software.
## P2WDB
@@ -32,11 +47,11 @@ The heart of the censorship resistance is the pay-to-write database ([P2WDB](htt
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.
## `ipfs-swap-service`
## `bch-dex`
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.
The `bch-dex` 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.
`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.
`bch-dex` 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)
@@ -46,19 +61,39 @@ 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
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.
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.
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.
## 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:
- 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.
- `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.
- Tokens and BCH are held by a wallet which is under the controlled of `bch-dex`.
- 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 `/p2wdb` 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.
## Reading from the Local Database
## Taking an Offer
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.
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 `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 contains a partially signed transaction saved as a hex string.
- The *Counter Offer* is uploaded 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.
- A 'garbage collection' function that runs periodically will delete Orders and Offers in bch-dex that have had their UTXO spent, automatically cleaning up stale trade data that is no longer valid.
## 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.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

+122 -58
View File
@@ -1,13 +1,13 @@
# ipfs-swap-service Specification
# bch-dex Specification
This document contains a high-level, human-readable specification for the four major architectural areas of the ipfs-swap-service:
This document contains a high-level, human-readable specification for the four major architectural areas of the bch-dex:
- Entities
- Use Cases
- Controllers (inputs)
- Adapters (outputs)
This reflects the [Clean Architecture](https://troutsblog.com/blog/clean-architecture) design pattern.
This reflects the [Clean Architecture](https://bafybeiajggd4zju7oen627bcy5l32hrxqomoqzvwqfir6phzgducozksv4.ipfs.dweb.link/blog/clean-architecture) design pattern.
## Entities
@@ -15,63 +15,105 @@ 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.
- _makerAddr_ - The address for the taker to send money to.
- _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.
- 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 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 accidentally spent, which is why it needs to be segregated from other wallet UTXOs.
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.
- _makerAddr_ - The address for the taker to send money to.
- _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.
- _offerStatus_ - The state of the offer. When the data is added to the P2WDB, it gets a value of 'posted', but the database model internal to bch-dex can have the following properties:
- *posted* - The offer is posted and can be countered by a taker.
- *taken* - The offer was countered and accepted.
- *dead* - The UTXO was spent outside the trade, which automatically makes the offer dead.
- Authentication Data:
- _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.
When an Offer is uploaded to the P2WDB, the following properties are added to the data:
- Added properties to P2WDB data:
- *dataType* - Has a value of 'offer'. This is used to route the data correctly when the P2WDB webhook passes the data to `bch-dex`.
### Counter Offer
A Counter Offer is the other side of the trade, a mirror image to 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.
Counter Offers are not tracked via database models like Offers and Orders are. They are processed by `bch-dex` as soon as they are received. A garbage collection function will be called periodically, to sweep the segregated UTXOs used to generate a Counter Offer back into the root address of the wallet, if it is not accepted after a period of time.
To generate a Counter Offer, a segregated UTXO is created that matches the requirements in the Offer. A partially signed transaction is created that includes the Offer UTXO and the Counter Offer UTXO. The Taker signs the input consuming the Counter Offer UTXO. The partially signed transaction and other trade data is uploaded to P2WDB.
When the Counter Offer data is uploaded to the P2WDB, the data must have the following properties:
- *partialTxHex* - A hexidecimal representation of the partially-signed transaction, which includes the UTXO in the Offer and a second UTXO for that matches the conditions in the Offer.
- *dataType* - Must have a value of `counter-offer`
## Use Cases
@@ -79,13 +121,18 @@ 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.
- **`createOrder()`** - A macro command that leverages the other functions in this library to create a new Order and submit it to the P2WDB.
- **`ensureFunds()`** - Ensure that the wallet has enough BCH and tokens to complete the requested trade.
- **`findOrderByHash()`** - Given a P2WDB CID 'hash', this function will return the corresponding Order model associated with that CID.
### 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.
- **`createOffer()`** - This method is triggered by a webhook from the P2WDB. It will take the data provided by the P2WDB and create a new Offer entity in the local database.
- **`listOffers()`** - Returns a list of all the active Offers tracked by `bch-dex`.
- **`takeOffer()`** - Generate a segregated UTXO and partially signed transaction, then upload the Counter Offer data to the P2WDB.
- **`ensureFunds()`** - Ensure the wallet has enough BCH and tokens to make a Counter Offer.
- **`findOfferByHash()`** - Given a P2WDB CID 'hash', this function will return the corresponding Offer model associated with that CID.
- **`acceptCounterOffer()`** - This function is triggered by the P2WDB webhook REST API handler. When a Counter Offer is passed to `bch-dex` by the P2WDB, the data is then passed to this function. It does due diligence on the Counter Offer, then signs and broadcasts the transaction to accept the Counter Offer.
## Controllers
@@ -93,15 +140,32 @@ Controllers are inputs to the system. When a controller is activated, it causes
### 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.
- **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 submits the data to the P2WDB to generate an Offer that is tracked by all other instances of `bch-dex` on the network.
### 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`.
- **GET /offer/list** - Returns an array of objects, where each object represents an active Offer.
- **POST /offer/take** - Given the P2WDB CID of an Offer, calling this endpoint will attempt to generate a Counter Offer and upload it to the P2WDB.
### P2WDB
- **POST /p2wdb** - This POST REST API endpoint will be called by a webhook generated from the 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 `bch-dex` local database. Based on the `dataType` property, the data is routed to either create a new Offer or to process a new Counter Offer.
## Adapters
Adapters are output libraries so that the business logic doesn't need to know any specific information about the I/O. They are essentially the output of the application.
Adapters are outputs. These libraries exist so that the business logic doesn't need to know any specific information about the I/O. They handle the low-level mechanics of the various subsystems.
- **localdb** - An adapter for the local database (MongoDB).
- **ipfs** - An adapter for IPFS and the ipfs-coord library. It allows the app to use the JSON-RPC over IPFS.
- **wallet** - Handles low-level wallet tasks.
- **`moveTokens()`** - Move the tokens indicated in the order to a temporary holding address. This will generate the segregated UTXOs used in the trade. This function moves the funds and returns the UTXO information.
- **`moveBCH()`** - Same idea as `moveTokens()`, but creates a non-SLP segregated UTXO, used when generating a Counter Offer.
- **`openWallet()`** - Opens the JSON file containing the wallet data for the app.
- **`instanceWallet()`** - Given the wallet data returned by `openWallet()`, it generates an instance of the [minimal-slp-wallet](https://www.npmjs.com/package/minimal-slp-wallet) for generating UTXOs and interacting with the blockchain.
- **`incrementNextAddress()`** - Increments the index of the [HD wallet](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05.asciidoc#hd_wallets) used to generate keypairs for storing segregated UTXOs.
- **`getKeyPair()`** - Generates a keypair from the HD wallet.
- **`generateSignature()`** - Generate a cryptographic signature.
- **`generatePartialTx()`** - Create a partial transaction as part of a Counter Offer.
- **`deseralizeTx()`** - Used only for debugging purposes. Expands a hex transaction into a JSON object.
- **`completeTx()`** - Complete the partially signed transaction by signing the first input, then broadcasting the transaction to the network.
+23697 -6164
View File
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -6,9 +6,10 @@
"scripts": {
"start": "node index.js",
"test": "npm run test:all",
"test:all": "export BCH_DEX=test && nyc --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
"test:all": "export BCH_DEX=test && nyc --reporter=text mocha --exit --timeout 30000 --recursive test/unit test/e2e/automated/",
"test:unit": "export BCH_DEX=test && mocha --exit --timeout 15000 --recursive test/unit/",
"test:e2e:auto": "export BCH_DEX=test && mocha --exit --timeout 15000 test/e2e/automated/",
"test:e2e:auto": "export BCH_DEX=test && mocha --exit --timeout 30000 test/e2e/automated/",
"test:integration": "export BCH_DEX=test && mocha --exit --timeout 45000 --recursive test/integration",
"test:temp": "export BCH_DEX=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
"lint": "standard --env mocha --fix",
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
@@ -26,13 +27,14 @@
},
"repository": "Permissionless-Software-Foundation/bch-dex",
"dependencies": {
"@psf/bch-js": "5.3.2",
"@psf/bch-js": "6.2.1",
"axios": "0.21.1",
"bch-message-lib": "2.1.4",
"bcryptjs": "2.4.3",
"bitcoincashjs-lib": "3.3.3",
"glob": "7.1.6",
"ipfs": "0.60.0",
"ipfs-coord": "6.8.12",
"ipfs-coord": "7.1.6",
"ipfs-http-client": "55.0.0",
"jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1",
@@ -50,10 +52,11 @@
"koa2-ratelimit": "0.9.1",
"libp2p": "^0.36.2",
"line-reader": "0.4.0",
"minimal-slp-wallet": "4.4.1",
"minimal-slp-wallet": "4.4.5",
"mongoose": "5.13.13",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.4.17",
"p2wdb": "1.3.1",
"passport-local": "1.0.0",
"public-ip": "^4.0.4",
"winston": "3.3.3",
@@ -61,6 +64,7 @@
},
"devDependencies": {
"apidoc": "0.26.0",
"bch-token-sweep": "1.5.16",
"chai": "4.3.0",
"coveralls": "2.11.4",
"eslint": "7.19.0",
@@ -68,6 +72,7 @@
"eslint-config-standard": "16.0.2",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "3.3.1",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "4.0.0",
"husky": "4.3.8",
"mocha": "8.4.0",
+24 -24
View File
@@ -40,30 +40,30 @@ class Bch {
}
// Gets the total psf token balance
async getPSFTokenBalance (slpAddress) {
try {
if (!slpAddress || typeof slpAddress !== 'string') {
throw new Error('slpAddress must be a string')
}
let psfBalance = 0
const balances = await this.bchjs.SLP.Utils.balancesForAddress(
slpAddress
)
// Sums all the balances of all tokens
// that match the psf token ID
for (let i = 0; i < balances.length; i++) {
if (balances[i].tokenId === this.PSF_TOKEN_ID) {
psfBalance += balances[i].balance
}
}
return psfBalance
} catch (err) {
console.error('Error in bch.js/getPSFTokenBalance()')
throw err
}
}
// async getPSFTokenBalance (slpAddress) {
// try {
// if (!slpAddress || typeof slpAddress !== 'string') {
// throw new Error('slpAddress must be a string')
// }
// let psfBalance = 0
// const balances = await this.bchjs.SLP.Utils.balancesForAddress(
// slpAddress
// )
//
// // Sums all the balances of all tokens
// // that match the psf token ID
// for (let i = 0; i < balances.length; i++) {
// if (balances[i].tokenId === this.PSF_TOKEN_ID) {
// psfBalance += balances[i].balance
// }
// }
//
// return psfBalance
// } catch (err) {
// console.error('Error in bch.js/getPSFTokenBalance()')
// throw err
// }
// }
async getMerit (slpAddr) {
try {
+4 -3
View File
@@ -18,7 +18,7 @@ const JSONFiles = require('./json-files')
const FullStackJWT = require('./fullstack-jwt')
const BCHAdapter = require('./bch')
const WalletAdapter = require('./wallet')
const P2wdbAdapter = require('./p2wdb')
const P2wdbAdapter = require('./p2wdb-adapter')
//
// // Instantiate adapter libraries.
@@ -52,10 +52,11 @@ class Adapters {
this.nodemailer = new Nodemailer()
this.jsonFiles = new JSONFiles()
this.bchjs = new BCHJS()
localConfig.bchjs = this.bchjs
this.bch = new BCHAdapter()
this.config = config
this.wallet = new WalletAdapter()
this.p2wdb = new P2wdbAdapter()
this.p2wdb = new P2wdbAdapter(localConfig)
// Get a valid JWT API key and instance bch-js.
this.fullStackJwt = new FullStackJWT(config)
@@ -72,7 +73,7 @@ class Adapters {
}
// Start the IPFS node.
await this.ipfs.start()
// await this.ipfs.start()
// Open the wallet file
const walletData = await this.wallet.openWallet()
+1 -1
View File
@@ -91,7 +91,7 @@ class IpfsCoordAdapter {
attachRPCRouter (router) {
try {
_this.ipfsCoord.privateLog = router
_this.ipfsCoord.adapters.orbit.privateLog = router
_this.ipfsCoord.adapters.pubsub.privateLog = router
} catch (err) {
console.error('Error in attachRPCRouter()')
throw err
+2
View File
@@ -6,6 +6,7 @@
const Users = require('./models/users')
const Entries = require('./models/entry')
const Order = require('./models/order')
const Offer = require('./models/offer')
class LocalDB {
constructor () {
@@ -13,6 +14,7 @@ class LocalDB {
this.Users = Users
this.Entry = Entries
this.Order = Order
this.Offer = Offer
}
}
+26 -14
View File
@@ -1,24 +1,36 @@
const mongoose = require('mongoose')
const Offer = new mongoose.Schema({
// Token data
tokenId: { type: String },
utxoTxid: { type: String },
utxoVout: { type: Number },
ticker: { type: String },
// Trade data
buyOrSell: { type: String },
numTokens: { type: Number },
rateInBaseUnit: { type: String },
minUnitsToExchange: { type: String },
p2wdbTxid: { type: String },
p2wdbHash: { type: String },
offerStatus: { type: String },
makerAddr: { 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 },
messageClass: { type: Number }
//
offerIpfsId: { type: String },
offerBchAddr: { type: String },
offerPubKey: { type: String }
})
module.exports = mongoose.model('offer', Offer)
+33 -14
View File
@@ -1,23 +1,42 @@
/*
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.
See the dev-docs/specification.md for details on each property.
*/
const mongoose = require('mongoose')
const Order = new mongoose.Schema({
// Token data
tokenId: { type: String },
utxoTxid: { type: String },
utxoVout: { type: Number },
ticker: { type: String },
// Trade data
buyOrSell: { type: String },
numTokens: { type: Number },
rateInBaseUnit: { type: String },
minUnitsToExchange: { type: String },
p2wdbTxid: { type: String },
p2wdbHash: { type: String },
makerAddr: { 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 }
messageClass: { type: Number }
})
module.exports = mongoose.model('order', Order)
+107
View File
@@ -0,0 +1,107 @@
/*
Adapter library for interacting with the P2WDB
*/
// Public npm libraries.
const axios = require('axios')
const { Write, Read } = require('p2wdb/index')
// Local libraries
const config = require('../../config')
class P2wdbAdapter {
constructor (localConfig = {}) {
// Dependency injection
this.bchjs = localConfig.bchjs
if (!this.bchjs) {
throw new Error('Must pass an instance of bch-js when instantiating p2wdb.js adapter.')
}
// Encapsulate dependencies
this.axios = axios
this.config = config
this.Write = Write
this.Read = Read
// Allow the localConfig to overwrite the config file values.
this.p2wdbURL = localConfig.p2wdbURL || config.p2wdbUrl
}
// Write some data to the P2WDB
async write (inputObj = {}) {
try {
const { appId = 'test', data = {}, wif } = inputObj
if (typeof wif !== 'string' || !wif) {
throw new Error('wif input must be a private key starting with the letter L or K')
}
const p2write = this.instantiateWriteLib(wif)
const result = await p2write.postEntry(data, appId)
return result.hash
} catch (err) {
console.error('Error in p2wdb.js/write()')
throw err
}
}
// Instantiate the Write library with a WIF, handling the various config
// settings.
instantiateWriteLib (wif) {
try {
// Object used to configure the Write library.
let configObj = {}
if (this.config.useFullStackCash) {
// Use web 2 infrastructure.
// bch-js and FullStack.cash
configObj = {
wif,
serverURL: this.p2wdbURL,
restURL: this.bchjs.restURL,
apiToken: this.bchjs.apiToken
}
} else {
// Use web 3 infrastructure.
// ipfs-bch-wallet-consumer and bch-consumer library
configObj = {
wif,
serverURL: this.p2wdbURL,
interface: 'consumer-api',
restURL: this.config.consumerUrl
}
}
const write = new this.Write(configObj)
return write
} catch (err) {
console.error('Error in p2wdb-adapter.js/instantitateWriteLib()')
throw err
}
}
// Returns a promise that resolves to true or false, to indicate if the
// WIF controls the required funds to write to the P2WDB.
async checkForSufficientFunds (wif) {
try {
if (typeof wif !== 'string' || !wif) {
throw new Error('invalid wif to check for funds')
}
// Instatiate the Write library
const p2write = this.instantiateWriteLib(wif)
return await p2write.checkForSufficientFunds()
} catch (err) {
console.error('Error in p2wdb.js/checkForSufficientFunds()')
throw err
}
}
}
module.exports = P2wdbAdapter
-51
View File
@@ -1,51 +0,0 @@
/*
Adapter library for interacting with the P2WDB
*/
// Public npm libraries.
const axios = require('axios')
// Global constants
const P2WDB_SERVER = 'http://localhost:5001/entry/write'
// const P2WDB_SERVER = 'https://p2wdb.fullstack.cash/entry/write'
class P2wdbAdapter {
constructor (localConfig = {}) {
// Encapsulate dependencies
this.axios = axios
}
async write (inputObj) {
try {
const { txid, signature, message, appId, data } = inputObj
// TODO: Input validation
const now = new Date()
const dataObj = {
appId,
data,
timestamp: now.toISOString(),
localTimeStamp: now.toLocaleString()
}
const bodyData = {
txid,
message,
signature,
data: JSON.stringify(dataObj)
}
const result = await this.axios.post(P2WDB_SERVER, bodyData)
// console.log(`Response from API: ${JSON.stringify(result.data, null, 2)}`)
return result.data.hash
} catch (err) {
console.error('Error in p2wdb.js/write()')
throw err
}
}
}
module.exports = P2wdbAdapter
+316 -74
View File
@@ -4,14 +4,16 @@
// Public npm libraries
const BchWallet = require('minimal-slp-wallet/index')
const bitcoinJs = require('bitcoincashjs-lib')
// Local libraries
const JsonFiles = require('./json-files')
const config = require('../../config')
const WALLET_FILE = `${__dirname.toString()}/../../wallet.json`
const PROOF_OF_BURN_QTY = 0.01
const P2WDB_TOKEN_ID =
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
// const PROOF_OF_BURN_QTY = 0.01
// const P2WDB_TOKEN_ID =
// '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
class WalletAdapter {
constructor (localConfig = {}) {
@@ -19,9 +21,13 @@ class WalletAdapter {
this.jsonFiles = new JsonFiles()
this.WALLET_FILE = WALLET_FILE
this.BchWallet = BchWallet
this.config = config
this.bitcoinJs = bitcoinJs
}
// Open the wallet file, or create one if the file doesn't exist.
// Does not instance the wallet. The output of this function is expected to
// be passed to instanceWallet().
async openWallet () {
try {
let walletData
@@ -32,7 +38,6 @@ class WalletAdapter {
walletData = await this.jsonFiles.readJSON(this.WALLET_FILE)
} catch (err) {
// Create a new wallet file if one does not already exist.
// console.log('caught: ', err)
console.warn('Wallet file not found. Creating new wallet.json file.')
// Create a new wallet.
@@ -60,6 +65,36 @@ class WalletAdapter {
}
}
// Create an instance of minimal-slp-wallet. Use data in the wallet.json file,
// and pass the bch-js information to the minimal-slp-wallet library.
async instanceWallet (walletData) {
try {
// TODO: throw error if wallet data is not passed in.
const advancedConfig = {}
if (this.config.useFullStackCash) {
advancedConfig.interface = 'rest-api'
advancedConfig.restURL = this.config.apiServer
advancedConfig.apiToken = this.config.apiToken
} else {
advancedConfig.interface = 'consumer-api'
advancedConfig.restURL = this.config.consumerUrl
}
// Instantiate minimal-slp-wallet.
this.bchWallet = new this.BchWallet(walletData.mnemonic, advancedConfig)
// Wait for wallet to initialize.
await this.bchWallet.walletInfoPromise
console.log('BCH wallet initialized.')
return this.bchWallet
} catch (err) {
console.error('Error in instanceWallet()')
throw err
}
}
// Increments the 'nextAddress' property in the wallet file. This property
// indicates the HD index that should be used to generate a key pair for
// storing funds for Offers.
@@ -90,9 +125,12 @@ class WalletAdapter {
// 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 () {
async getKeyPair (hdIndex = 0) {
try {
const hdIndex = await this.incrementNextAddress()
if (!hdIndex) {
// Increment the HD index and generate a new key pair.
hdIndex = await this.incrementNextAddress()
}
const mnemonic = this.bchWallet.walletInfo.mnemonic
@@ -107,7 +145,7 @@ class WalletAdapter {
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
const cashAddress = this.bchWallet.bchjs.HDNode.toCashAddress(childNode)
console.log('cashAddress: ', cashAddress)
console.log('Generating a new key pair for cashAddress: ', cashAddress)
const wif = this.bchWallet.bchjs.HDNode.toWIF(childNode)
@@ -124,31 +162,6 @@ class WalletAdapter {
}
}
// Create an instance of minimal-slp-wallet. Use data in the wallet.json file,
// and pass the bch-js information to the minimal-slp-wallet library.
async instanceWallet (walletData, bchjs) {
try {
// TODO: Throw error if bch-js is not passed in.
// TODO: throw error if wallet data is not passed in.
const advancedConfig = {
restURL: bchjs.restURL,
apiToken: bchjs.apiToken
}
// Instantiate minimal-slp-wallet.
this.bchWallet = new this.BchWallet(walletData.mnemonic, advancedConfig)
// Wait for wallet to initialize.
await this.bchWallet.walletInfoPromise
return true
} catch (err) {
console.error('Error in instanceWallet()')
throw err
}
}
// Generate a cryptographic signature, required to write to the P2WDB.
async generateSignature (message) {
try {
@@ -171,58 +184,287 @@ class WalletAdapter {
}
}
// Burn enough PSF to generate a valide proof-of-burn for writing to the P2WDB.
async burnPsf () {
// Generate a partial transcation to *take* a 'sell' offer.
async generatePartialTx (offerInfo, utxoInfo) {
try {
// TODO: Throw error if this.bchWallet has not been instantiated.
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
// console.log('walletData: ', walletData)
// console.log(
// `walletData.utxos.utxoStore.slpUtxos: ${JSON.stringify(
// walletData.utxos.utxoStore.slpUtxos,
// null,
// 2,
// )}`,
const bchjs = this.bchWallet.bchjs
// instance of transaction builder
const transactionBuilder = new bchjs.TransactionBuilder()
// Get a payment UTXO.
// TODO: Create a segregated UTXO.
// const bchUtxos = this.bchWallet.utxos.utxoStore.bchUtxos
// const paymentUtxo = await bchjs.Utxo.findBiggestUtxo(bchUtxos)
// console.log('paymentUtxo: ', paymentUtxo)
// Get token info on the offered UTXO
const txData = await this.bchWallet.getTxData([offerInfo.utxoTxid])
// console.log(`txData: ${JSON.stringify(txData, null, 2)}`)
// Construct the UTXO being offered for sale.
const offeredUtxo = {
txid: offerInfo.utxoTxid,
vout: offerInfo.utxoVout,
tokenId: offerInfo.tokenId,
decimals: txData[0].tokenDecimals,
tokenQty: offerInfo.numTokens.toString()
}
console.log(`offeredUtxo: ${JSON.stringify(offeredUtxo, null, 2)}`)
// Build First part of the collaborative Tx a.k.a. Alice
// Generate the OP_RETURN code.
const slpSendObj = bchjs.SLP.TokenType1.generateSendOpReturn(
[offeredUtxo],
offerInfo.numTokens.toString()
)
const slpData = slpSendObj.script
console.log(`slpOutputs: ${slpSendObj.outputs}`)
// Currently this app only supports a single SLP token UTXO for exact
// token quantities (no token change). e.g. 1 UTXO representing the
// exact number of 'numTokens'
if (slpSendObj.outputs > 1) {
console.log('WARNING: choose one UTXO with all tokens to exchange')
return
}
// Calculate sats needed to pay the offer.
const satsNeeded = offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)
if (isNaN(satsNeeded)) throw new Error('Can not calculate needed sats')
// Calculate miner fees.
// Get byte count (minimum 2 inputs, 3 outputs)
// const opReturnBufLength = slpData.byteLength + 32 // add padding
// const byteCount =
// bchjs.BitcoinCash.getByteCount({ P2PKH: 2 }, { P2PKH: 4 }) +
// opReturnBufLength
// const totalSatsNeeded = byteCount + satsNeeded
// console.log(`satoshis needed: ${satsNeeded}`)
// One last check to ensure the app wallet has enough BCH to complete
// the trade.
// if (totalSatsNeeded > paymentUtxo.value) {
// console.log(`Selected payment UTXO is not big enough. Sats needed: ${totalSatsNeeded}, UTXO value: ${paymentUtxo.value}`)
// }
// add UTXO for sale (STILL CANNOT SPEND - not signed yet)
transactionBuilder.addInput(offerInfo.utxoTxid, offerInfo.utxoVout)
// add payment UTXO
// transactionBuilder.addInput(paymentUtxo.tx_hash, paymentUtxo.tx_pos)
transactionBuilder.addInput(utxoInfo.txid, utxoInfo.vout)
// const originalAmount = paymentUtxo.value
const dust = 546
// const remainder = originalAmount - satsNeeded - dust // exchange fee + token UTXO dust
// Add the SLP OP_RETURN data as the first output.
transactionBuilder.addOutput(slpData, 0)
const buyerAddr = this.bchWallet.walletInfo.legacyAddress
// console.log(`buyAddr: ${JSON.stringify(buyAddr, null, 2)}`)
// Send dust transaction representing tokens being sent.
transactionBuilder.addOutput(
buyerAddr,
dust
)
// Get seller address
const sellerAddr = offerInfo.makerAddr
// Send payment to the offer side
transactionBuilder.addOutput(sellerAddr, satsNeeded)
// Send the BCH change back to the buyer
// if (remainder > 550) {
// transactionBuilder.addOutput(buyerAddr, remainder)
// }
// const buyerECPair = bchjs.ECPair.fromWIF(this.bchWallet.walletInfo.privateKey)
const buyerECPair = bchjs.ECPair.fromWIF(utxoInfo.wif)
// Sign the buyers input UTXO for spending.
transactionBuilder.sign(
1,
buyerECPair,
null,
transactionBuilder.hashTypes.SIGHASH_ALL,
utxoInfo.sats
)
const tx = transactionBuilder.transaction.buildIncomplete()
const hex = tx.toHex()
// console.log('hex: ', hex)
return hex
} catch (err) {
console.error('Error in wallet.js/generatePartialTx(): ', err)
throw err
}
// return true
}
// Move tokens to an address controlled by the HD wallet, to generate a
// segregated UTXO.
async moveTokens (inObj = {}) {
try {
const { tokenId, qty } = inObj
const keyPair = await this.getKeyPair()
console.log('keyPair: ', keyPair)
const receiver = {
address: keyPair.cashAddress,
tokenId,
qty
}
// Update the UTXO store of the wallet.
await this.bchWallet.getUtxos()
const txid = await this.bchWallet.sendTokens(receiver, 3)
const utxoInfo = {
txid,
vout: 1,
hdIndex: keyPair.hdIndex
}
return utxoInfo
} catch (err) {
console.error('Error in wallet.js/moveTokens()')
throw err
}
}
// Move a quanity of BCH to an address controlled by the HD wallet, to
// generate a segregated UTXO.
async moveBch (amountSat) {
try {
const keyPair = await this.getKeyPair()
console.log('keyPair: ', keyPair)
const receivers = [{
address: keyPair.cashAddress,
amountSat
}]
console.log(`receivers: ${JSON.stringify(receivers, null, 2)}`)
// Update the UTXO store of the wallet.
await this.bchWallet.getUtxos()
const txid = await this.bchWallet.send(receivers)
const utxoInfo = {
txid,
vout: 0,
hdIndex: keyPair.hdIndex,
wif: keyPair.wif
}
return utxoInfo
} catch (err) {
console.error('Error in wallet.js/moveBch()')
throw err
}
}
// Deserialize a hex string representing a partial TX. Returns an object
// representing the transaction.
async deseralizeTx (txHex) {
try {
// Ensure the URL points at FullStack.cash, since the web 3 infra does not
// yet support this call.
const oldUrl = this.bchWallet.bchjs.RawTransactions.restURL
this.bchWallet.bchjs.RawTransactions.restURL = 'https://api.fullstack.cash/v5/'
// Use a full node to deserialize the transaction.
const txObj2 = await this.bchWallet.bchjs.RawTransactions.decodeRawTransaction(txHex)
// console.log(`txObj2: ${JSON.stringify(txObj2, null, 2)}`)
// Restore the old URL.
this.bchWallet.bchjs.RawTransactions.restURL = oldUrl
return txObj2
} catch (err) {
console.error('Error in wallet.js/deserializePartialTx()')
throw err
}
}
// Complete the partially signed transaction by signing the first input,
// then broadcasting the transaction to the network.
async completeTx (hex, hdIndex) {
try {
// console.log('hex: ', hex)
const bchjs = this.bchWallet.bchjs
// instance of transaction builder
// const transactionBuilder = new bchjs.TransactionBuilder()
// Convert the hex string version of the transaction into a Buffer.
// const paymentBuffer = Buffer.from(hex, 'hex')
// Generate a Transaction object from the transaction binary data.
// const csTransaction = this.bitcoinJs.Transaction.fromBuffer(paymentBuffer)
const csTransaction = this.bitcoinJs.Transaction.fromHex(hex)
// console.log(`payment tx: ${JSON.stringify(csTransaction, null, 2)}`)
// Instantiate the Transaction Builder.
const csTransactionBuilder = this.bitcoinJs.TransactionBuilder.fromTransaction(
csTransaction,
'mainnet'
)
// const csTransactionBuilder = bchjs.TransactionBuilder.fromTransaction(
// csTransaction,
// 'mainnet'
// )
// Get token UTXOs held by the wallet.
const tokenUtxos = this.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens
// Get the keypair for the address used in the Order
const keyPair = await this.getKeyPair(hdIndex)
console.log(`maker keyPair: ${JSON.stringify(keyPair, null, 2)}`)
const makerECPair = bchjs.ECPair.fromWIF(keyPair.wif)
// Find a token UTXO that contains PSF with a quantity higher than needed
// to generate a proof-of-burn.
let tokenUtxo = {}
for (let i = 0; i < tokenUtxos.length; i++) {
const thisUtxo = tokenUtxos[i]
// Assumption: segregated UTXO has a value of 546 sats. It might be better
// to explicitly look this data up via the blockchain.
const dust = 546
// If token ID matches.
if (thisUtxo.tokenId === P2WDB_TOKEN_ID) {
if (parseFloat(thisUtxo.tokenQty) >= PROOF_OF_BURN_QTY) {
tokenUtxo = thisUtxo
break
}
}
}
if (tokenUtxo.tokenId !== P2WDB_TOKEN_ID) {
throw new Error(
`Token UTXO of with ID of ${P2WDB_TOKEN_ID} and quantity greater than ${PROOF_OF_BURN_QTY} could not be found in wallet.`
)
}
const result = await this.bchWallet.burnTokens(
PROOF_OF_BURN_QTY,
P2WDB_TOKEN_ID
// Coutnersign the Maker's input, representing the tokens for sale.
csTransactionBuilder.sign(
0,
makerECPair,
null,
this.bitcoinJs.Transaction.SIGHASH_ALL,
// csTransactionBuilder.hashTypes.SIGHASH_ALL,
dust
)
// console.log('walletData.burnTokens() result: ', result)
return result
// build tx
const csTx = csTransactionBuilder.build()
// return {
// success: true,
// txid: 'fakeTxid',
// }
// output rawhex
const csTxHex = csTx.toHex()
console.log(`Fully signed Tx hex: ${csTxHex}`)
// Debug: Display the fully signed TX
const txObj = await this.deseralizeTx(csTxHex)
console.log(`Final tx: ${JSON.stringify(txObj, null, 2)}`)
// return csTxHex
// Broadcast transaction to the network
const txid = await this.bchWallet.ar.sendTx(csTxHex)
return txid
} catch (err) {
console.error('Error in burnPsf(): ', err)
console.error('Error in wallet.js/completeTx()')
throw err
}
}
+2 -4
View File
@@ -7,8 +7,6 @@ const axios = require('axios')
let _this
const APPID = 'swapTest555'
class WebHook {
constructor () {
_this = this
@@ -26,7 +24,7 @@ class WebHook {
const endpoint = _this.config.webhookService
const obj = {
appId: APPID,
appId: this.config.p2wdbAppId,
url
}
@@ -49,7 +47,7 @@ class WebHook {
const endpoint = _this.config.webhookService
const obj = {
appId: APPID,
appId: this.config.p2wdbAppId,
url
}
+4 -9
View File
@@ -6,18 +6,12 @@
// Public npm libraries.
// Load the Clean Architecture Adapters library
// Local libraries
const Adapters = require('../adapters')
// Load the JSON RPC Controller.
const JSONRPC = require('./json-rpc')
// Load the Clean Architecture Use Case libraries.
const UseCases = require('../use-cases')
// const useCases = new UseCases({ adapters })
// Load the REST API Controllers.
const RESTControllers = require('./rest-api')
const TimerControllers = require('./timer-controllers')
class Controllers {
constructor (localConfig = {}) {
@@ -32,7 +26,8 @@ class Controllers {
// Attach the REST controllers to the Koa app.
// this.attachRESTControllers(app)
this.attachRPCControllers()
// this.attachRPCControllers()
this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases })
}
// Top-level function for this library.
+5
View File
@@ -1,5 +1,8 @@
/*
This is the JSON RPC router for the users API
CT 3/5/22: This library can probably be deleted. Handling of the /about endpoint
is now directly controlled by the ipfs-coord library.
*/
// Public npm libraries
@@ -33,6 +36,8 @@ class AboutRPC {
// This is a bit different than other router libraries, because there is
// only one response, which is a string about this node.
async aboutRouter (rpcData) {
console.log('debugging: aboutRouter from ipfs-service-provider triggered')
return {
success: true,
status: 200,
+4
View File
@@ -14,6 +14,7 @@ const LogsRESTController = require('./logs')
const EntryRouter = require('./entry')
const OfferRouter = require('./offer')
const OrderRouter = require('./order')
const P2WDBRouter = require('./p2wdb')
class RESTControllers {
constructor (localConfig = {}) {
@@ -65,6 +66,9 @@ class RESTControllers {
const orderRouter = new OrderRouter(dependencies)
orderRouter.attach(app)
const p2wdbRouter = new P2WDBRouter(dependencies)
p2wdbRouter.attach(app)
}
}
+36 -5
View File
@@ -2,7 +2,7 @@
REST API Controller library for the /offer route
*/
// const { wlogger } = require('../../../adapters/wlogger')
const { wlogger } = require('../../../adapters/wlogger')
let _this
@@ -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,6 +49,35 @@ 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('REST API controller, body: ', ctx.request.body)
const offerCid = ctx.request.body.offerCid
const hash = await _this.useCases.offer.takeOffer(offerCid)
ctx.body = { hash }
} catch (err) {
wlogger.error('Error in takeOffer() REST API handler.')
_this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', 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 -8
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)
@@ -51,7 +49,8 @@ class OrderRESTControllerLib {
// 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 -1
View File
@@ -1,5 +1,5 @@
/*
REST API library for /offer route.
REST API library for /order route.
*/
// Public npm libraries.
@@ -0,0 +1,93 @@
/*
REST API Controller library for the /p2wdb route
This route handles incoming data from the P2WDB webhook, and routes the data
to the proper handler.
*/
// const { wlogger } = require('../../../adapters/wlogger')
let _this
class P2WDBRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /p2wdb REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /p2wdb 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 routeWebhook (ctx) {
try {
console.log('p2wdb REST API handler: body: ', ctx.request.body)
const dataType = ctx.request.body.data.dataType
if (dataType.includes('counter')) {
// Detect and handle Counter Offers
console.log('counter-offer data detected.')
const counterOffer = ctx.request.body
await _this.useCases.offer.acceptCounterOffer(counterOffer)
} else if (dataType.includes('offer')) {
// Detect and handle new Offers
console.log('offer data detected')
const offerObj = ctx.request.body
await _this.useCases.offer.createOffer(offerObj)
} else {
console.log('Could not route P2WDB webhook data.')
}
// const orderObj = ctx.request.body.order
//
// const hash = await _this.useCases.order.createOrder(orderObj)
//
// ctx.body = { hash }
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)
// 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 = P2WDBRESTControllerLib
+62
View File
@@ -0,0 +1,62 @@
/*
REST API library for /p2wdb route.
This route handles incoming data from the P2WDB webhook, and routes the data
to the proper handler.
*/
// Public npm libraries.
const Router = require('koa-router')
// Local libraries.
const P2WDBRESTControllerLib = require('./controller')
let _this
class P2WDBRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /p2wdb REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /p2wdb REST Controller.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.p2wdbRESTController = new P2WDBRESTControllerLib(dependencies)
// Instantiate the router and set the base route.
const baseUrl = '/p2wdb'
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.p2wdbRESTController.routeWebhook)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
app.use(_this.router.allowedMethods())
}
}
module.exports = P2WDBRouter
+59
View File
@@ -0,0 +1,59 @@
/*
This Controller library is concerned with timer-based functions that are
kicked off periodicially.
*/
// Used to retain scope of 'this', when the scope is lost.
let _this
class TimerControllers {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating Timer Controller libraries.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating Timer Controller libraries.'
)
}
this.debugLevel = localConfig.debugLevel
_this = this
this.startTimers()
}
// Start all the time-based controllers.
startTimers () {
setInterval(this.gcOrders, 60000 * 5)
setInterval(this.gcOffers, 60000 * 5)
}
// Garbage Collect the Orders.
gcOrders () {
try {
_this.useCases.order.removeStaleOrders()
} catch (err) {
// Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/gcOrders(): ', err)
}
}
// Garbage Collect the Offers.
gcOffers () {
try {
_this.useCases.offer.removeStaleOffers()
} catch (err) {
// Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/gcOffers(): ', err)
}
}
}
module.exports = TimerControllers
+59 -21
View File
@@ -1,22 +1,36 @@
/*
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) {
class OfferEntity {
constructor () {
this.offerStatus = ['posted', 'taken', 'dead']
}
validate (offerData = {}) {
// Throw an error if input object does not have a data property
if (!offerData.data) {
throw new Error(
'Input to offer.validate() must be an object with a data property.'
)
}
const {
messageType,
messageClass,
tokenId,
buyOrSell,
rateInSats,
minSatsToExchange,
numTokens
} = data
rateInBaseUnit,
minUnitsToExchange,
numTokens,
utxoTxid,
utxoVout,
offerStatus,
makerAddr,
ticker
} = offerData.data
// Input Validation
if (!messageType || typeof messageType !== 'number') {
@@ -31,28 +45,52 @@ class Offer {
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 (!rateInBaseUnit || typeof rateInBaseUnit !== 'number') {
throw new Error("Property 'rateInBaseUnit' must be an integer number.")
}
if (!minSatsToExchange || typeof minSatsToExchange !== 'number') {
throw new Error("Property 'minSatsToExchange' must be an integer number.")
if (!minUnitsToExchange || typeof minUnitsToExchange !== 'number') {
throw new Error("Property 'minUnitsToExchange' 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.")
}
if (offerStatus && !this.offerStatus.includes(offerStatus)) {
throw new Error("Property 'offerStatus' must be posted, taken, or dead")
}
if (!makerAddr || typeof makerAddr !== 'string') {
throw new Error("Property 'makerAddr' must be a string.")
}
if (!ticker || typeof ticker !== 'string') {
throw new Error("Property 'ticker' must be a string.")
}
const offerData = {
const validatedOfferData = {
messageType,
messageClass,
tokenId,
buyOrSell,
rateInSats,
minSatsToExchange,
numTokens
rateInBaseUnit,
minUnitsToExchange,
numTokens,
utxoTxid,
utxoVout,
timestamp: offerData.timestamp,
localTimestamp: offerData.localTimeStamp,
txid: offerData.txid,
p2wdbHash: offerData.hash,
offerStatus: offerStatus || this.offerStatus[0],
makerAddr,
ticker
}
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
}
*/
+27 -35
View File
@@ -1,28 +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.
*/
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.'
)
}
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 Order {
validate (data) {
const {
messageType,
messageClass,
tokenId,
buyOrSell,
rateInSats,
minSatsToExchange,
rateInBaseUnit,
minUnitsToExchange,
numTokens,
utxoTxid,
utxoVout
} = orderData.data
makerAddr,
ticker
} = data
// Input Validation
if (!messageType || typeof messageType !== 'number') {
@@ -37,40 +33,36 @@ class OrderEntity {
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 (!rateInBaseUnit || typeof rateInBaseUnit !== 'number') {
throw new Error("Property 'rateInBaseUnit' must be an integer number.")
}
if (!minSatsToExchange || typeof minSatsToExchange !== 'number') {
throw new Error("Property 'minSatsToExchange' must be an integer number.")
if (!minUnitsToExchange || typeof minUnitsToExchange !== 'number') {
throw new Error("Property 'minUnitsToExchange' 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 (!makerAddr || typeof makerAddr !== 'string') {
throw new Error("Property 'makerAddr' must be a string.")
}
if (typeof utxoVout !== 'number') {
throw new Error("Property 'utxoVout' must be an integer number.")
if (!ticker || typeof ticker !== 'string') {
throw new Error("Property 'ticker' must be a string.")
}
const validatedOrderData = {
const offerData = {
messageType,
messageClass,
tokenId,
buyOrSell,
rateInSats,
minSatsToExchange,
rateInBaseUnit,
minUnitsToExchange,
numTokens,
utxoTxid,
utxoVout,
timestamp: orderData.timestamp,
localTimestamp: orderData.localTimeStamp,
txid: orderData.txid,
p2wdbHash: orderData.hash
makerAddr,
ticker
}
return validatedOrderData
return offerData
}
}
module.exports = OrderEntity
module.exports = Order
+4 -4
View File
@@ -30,10 +30,10 @@ class EntryLib {
}
// Verify psf tokens balance
const psfBalance = await this.bch.getPSFTokenBalance(
entryEntity.slpAddress
)
// const psfBalance = await this.bch.getPSFTokenBalance(
// entryEntity.slpAddress
// )
const psfBalance = 1
if (psfBalance < 10) {
throw new Error('Insufficient psf balance')
+2 -1
View File
@@ -21,8 +21,9 @@ class UseCases {
// console.log('use-cases/index.js localConfig: ', localConfig)
this.user = new UserUseCases(localConfig)
this.entry = new EntryUseCases(localConfig)
this.offer = new OfferUseCases(localConfig)
this.order = new OrderUseCases(localConfig)
localConfig.order = this.order
this.offer = new OfferUseCases(localConfig)
}
}
-148
View File
@@ -1,148 +0,0 @@
const { wlogger } = require('../adapters/wlogger')
const OfferEntity = require('../entities/offer')
class OfferLib {
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.'
)
}
// Encapsulate dependencies
this.offerEntity = new OfferEntity()
this.OfferModel = this.adapters.localdb.Offer
this.bch = this.adapters.bch
}
// Create a new offer model and add it to the Mongo database.
async createOffer (entryObj) {
try {
// console.log('createOffer(entryObj): ', entryObj)
// Input Validation
const offerEntity = this.offerEntity.validate(entryObj)
// console.log('offerEntity: ', offerEntity)
// Ensure sufficient tokens exist to create the offer.
await this.ensureFunds(offerEntity)
// Move the tokens to holding address.
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}`)
// generate signature.
const now = new Date()
const message = now.toISOString()
const signature = await this.adapters.wallet.generateSignature(message)
// console.log('signature: ', signature)
const p2wdbObj = {
txid,
signature,
message,
appId: 'swapTest555',
data: offerEntity
}
// Add offer to P2WDB.
const hash = await this.adapters.p2wdb.write(p2wdbObj)
// console.log('hash: ', hash)
return hash
} catch (err) {
// console.log("Error in use-cases/entry.js/createEntry()", err.message)
wlogger.error('Error in use-cases/entry.js/createOffer())')
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 (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) {
try {
// console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet)
// Get UTXOs.
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
if (offerEntity.buyOrSell.includes('sell')) {
// Sell Offer
// Get token UTXOs that match the token in the offer.
const tokenUtxos = utxos.slpUtxos.type1.tokens.filter(
(x) => x.tokenId === offerEntity.tokenId
)
// console.log('tokenUtxos: ', tokenUtxos)
// Get the total amount of tokens in the wallet that match the token
// in the offer.
let totalTokenBalance = 0
tokenUtxos.map((x) => (totalTokenBalance += parseFloat(x.tokenQty)))
// console.log('totalTokenBalance: ', totalTokenBalance)
// If there are fewer tokens in the wallet than what's in the offer,
// throw an error.
if (totalTokenBalance <= offerEntity.numTokens) {
throw new Error(
'App wallet does not have enough tokens to satisfy the SELL offer.'
)
}
} else {
// Buy Offer
}
return true
} catch (err) {
console.error('Error in ensureFunds()')
throw err
}
}
}
module.exports = OfferLib
+351
View File
@@ -0,0 +1,351 @@
/*
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 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.
A Counter Offer is created by calling the /take/:cid endpoint. It creates
a partially signed transaction.
*/
// Local libraries
const OfferEntity = require('../../entities/offer')
const config = require('../../../config')
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 Offer Use Cases library.'
)
}
this.orderUseCase = localConfig.order
if (!this.orderUseCase) {
throw new Error(
'Instance of Offer Use Cases must be passed in when instantiating Offer Use Cases library.'
)
}
// Encapsulate dependencies
this.config = config
this.offerEntity = new OfferEntity()
this.OfferModel = this.adapters.localdb.Offer
}
// This method is called by the POST /offer REST API controller, which is
// triggered by a P2WDB webhook.
async createOffer (offerObj) {
try {
console.log('Use Case createOffer(offerObj): ', offerObj)
// console.log('this.adapters.bchjs: ', this.adapters.bchjs)
// 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
)
console.log('utxoStatus: ', utxoStatus)
if (utxoStatus === null) return false
// A new offer gets a status of 'posted'
offerObj.data.offerStatus = 'posted'
const offerEntity = this.offerEntity.validate(offerObj)
console.log('offerEntity: ', offerEntity)
// Add offer to the local database.
const offerModel = new this.OfferModel(offerEntity)
await offerModel.save()
return true
} catch (err) {
console.error('Error in createOffer()')
throw err
}
}
async listOffers () {
try {
return this.OfferModel.find({})
} catch (error) {
console.error('Error in use-cases/offer/listOffers()')
throw error
}
}
// Generate phase 2 of 3 - take the other side of an Offer.
// Based on this example:
// https://github.com/Permission=less-Software-Foundation/bch-js-examples/blob/master/bch/applications/collaborate/sell-slp/e2e-exchange/step2-purchase-tx.js
//
// Dev Note: Right now this use cases takes all the tokens offered. It does not
// provide functionality to take less than the total amount of tokens offered
// (offerInfo.numTokens). Taking less than the offered amount will be added
// in the future.
async takeOffer (offerCid) {
try {
console.log('offerCid: ', offerCid)
// Get the Offer information
const offerInfo = await this.findOfferByHash(offerCid)
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
// Ensure the offer is in a 'posted' state and not already 'taken'
if (!offerInfo.offerStatus || offerInfo.offerStatus !== 'posted') {
throw new Error('offer status is not "posted", so offer is dead and can not be countered.')
}
// Verify that UTXO for sale is unspent. Abort if it's been spent.
const txid = offerInfo.utxoTxid
const vout = offerInfo.utxoVout
const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
txid,
vout
)
console.log('utxoStatus: ', utxoStatus)
if (utxoStatus === null) {
console.log(`utxo txid: ${txid}, vout: ${vout}`)
// TODO: Mark this Offer as 'dead'
throw new Error('UTXO does not exist. Aborting.')
}
// Ensure the app has enough funds to complete the trade.
await this.ensureFunds(offerInfo)
// Get UTXOs.
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
// Calculate amount of sats to generate a counter offer.
let satsToMove = offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)
if (isNaN(satsToMove)) {
throw new Error('Could not calculate the amount of BCH to generate counter offer')
}
// Add sats to cover mining fees and dust for token UTXO
satsToMove += 1000
// Move funds to create a segrated UTXO for taking the offer
const utxoInfo = await this.adapters.wallet.moveBch(satsToMove)
utxoInfo.sats = satsToMove
console.log('utxoInfo: ', utxoInfo)
// 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(offerInfo, utxoInfo)
console.log('partialTxHex: ', partialTxHex)
// return partialTxHex
// Debug: Decode the transaction for manual QA
const txObj = await this.adapters.wallet.deseralizeTx(partialTxHex)
console.log(`partially-signed transaction: ${JSON.stringify(txObj, null, 2)}`)
// Create valid Offer object
const takenOfferInfo = Object.assign({}, offerInfo)
takenOfferInfo.partialTxHex = partialTxHex
delete takenOfferInfo.p2wdbHash
delete takenOfferInfo._id
takenOfferInfo.offerHash = offerInfo.p2wdbHash
// Add P2WDB specific flag for signaling that this is a new offer.
takenOfferInfo.dataType = 'counter-offer'
// Write offer info to the P2WDB
// TODO: This will trigger the webhook. Find some way of triggering the
// webhook on new offers, but not on counteroffers
const p2wdbObj = {
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
data: takenOfferInfo,
appId: this.config.p2wdbAppId
}
const hash = await this.adapters.p2wdb.write(p2wdbObj)
// Return the P2WDB CID
return hash
// return 'fake-hash'
} catch (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 (offerEntity) {
try {
// console.log('this.adapters.wallet: ', this.adapters.wallet.bchWallet)
// console.log(`walletInfo: ${JSON.stringify(this.adapters.wallet.bchWallet.walletInfo, null, 2)}`)
await this.adapters.wallet.bchWallet.walletInfoPromise
// console.log(`utxos: ${JSON.stringify(this.adapters.wallet.bchWallet.utxos.utxoStore, null, 2)}`)
// Ensure the app wallet has enough funds to write to the P2WDB.
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
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 (offerEntity.buyOrSell.includes('sell')) {
// Sell Offer
// Calculate the sats needed
const satsNeeded = offerEntity.numTokens * parseInt(offerEntity.rateInBaseUnit)
if (isNaN(satsNeeded)) {
throw new Error('Could not calculate sats needed!')
}
// Ensure the app wallet controlls enough BCH to pay for the tokens.
const balance = await this.adapters.wallet.bchWallet.getBalance()
console.log(`wallet balance: ${balance}, sats needed: ${satsNeeded}`)
const SATS_MARGIN = 5000
if (satsNeeded + SATS_MARGIN > balance) {
throw new Error('App wallet does not control enough BCH to purchase the tokens.')
}
//
} else {
// Buy Offer
throw new Error('Buy offers are not supported yet.')
}
return true
} catch (err) {
console.error('Error in ensureFunds()')
throw err
}
}
async findOfferByHash (p2wdbHash) {
try {
if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
throw new Error('p2wdbHash must be a string')
}
const offer = await this.OfferModel.findOne({ p2wdbHash })
if (!offer) {
throw new Error('offer not found')
}
const offerObject = offer.toObject()
// return this.offerEntity.validateFromModel(offerObject)
return offerObject
} catch (err) {
console.error('Error in findOffer(): ', err)
throw err
}
}
// This function is called by the P2WDB webhook REST API handler. When a
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
// to this function. It does due dilligence on the Counter Offer, then signs
// and broadcasts the transaction to accept the Counter Offer.
async acceptCounterOffer (p2wdbData) {
try {
console.log(`acceptCounterOffer() p2wdbData: ${JSON.stringify(p2wdbData, null, 2)}`)
// See if this instance of bch-dex is managing the Order associated with
// the incoming Counter Offer.
const orderHash = p2wdbData.data.offerHash
const orderData = await this.orderUseCase.findOrderByHash(orderHash)
console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`)
// Deserialize the partially signed transaction.
const txHex = p2wdbData.data.partialTxHex
const txObj = await this.adapters.wallet.deseralizeTx(txHex)
console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`)
// Ensure the 3rd output (vout=2) contains the required amount of BCH.
const satsToReceive = orderData.numTokens * parseInt(orderData.rateInBaseUnit)
if (isNaN(satsToReceive)) {
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
}
const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value)
const hasRequiredAmount = satsOut === satsToReceive
if (!hasRequiredAmount) {
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
}
// Ensure the 3rd output (vout=2) is going to the maker address specified
// in the Offer.
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
const makerAddr = orderData.makerAddr
const hasCorrectAddr = makerAddr === addrInCounterOffer
if (!hasCorrectAddr) {
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
}
// Sign and broadcast the transaction.
const txid = await this.adapters.wallet.completeTx(txHex, orderData.hdIndex)
console.log('txid: ', txid)
return txid
} catch (err) {
console.error('Error in acceptCounterOffer()')
throw err
}
}
// This function is called by the garbage collection timer controller. It
// checks the UTXO associated with each Offer in the database. If the UTXO
// has been spent, the Offer is deleted from the database.
async removeStaleOffers () {
try {
const now = new Date()
console.log(`Starting garbage collection for Offers at ${now.toLocaleString()}`)
// Get all Offers in the database.
const offers = await this.OfferModel.find({})
// console.log('offers: ', offers)
// Loop through each Offer and ensure the UTXO is still valid.
for (let i = 0; i < offers.length; i++) {
const thisOffer = offers[i]
let utxoStatus = null
try {
// Get the status of the UTXO associate with this Offer.
utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
thisOffer.utxoTxid,
thisOffer.utxoVout
)
// console.log('utxoStatus: ', utxoStatus)
} catch (err) {
// Handle corner case of bad-data in the Offer model.
if (err.message.includes('txid needs to be a proper transaction ID')) {
console.log(`Deleting Offer with bad data: ${JSON.stringify(thisOffer, null, 2)}`)
await thisOffer.remove()
continue
} else {
throw err
}
}
// If the Offer UTXO is spent, delete the Offer model.
if (utxoStatus === null) {
console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`)
await thisOffer.remove()
}
}
} catch (err) {
console.error('Error in removeStaleOffers()')
throw err
}
}
}
module.exports = OfferUseCases
+215
View File
@@ -0,0 +1,215 @@
/*
Order use-case library.
*/
// Local libraries
const { wlogger } = require('../adapters/wlogger')
const OrderEntity = require('../entities/order')
const config = require('../../config')
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 Order Use Cases library.'
)
}
// Encapsulate dependencies
this.orderEntity = new OrderEntity()
this.OrderModel = this.adapters.localdb.Order
this.bch = this.adapters.bch
this.config = config
}
// Create a new order model and add it to the Mongo database.
async createOrder (entryObj) {
try {
console.log('createOrder(entryObj): ', entryObj)
// Specify the address to send payment.
entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress
console.log('entryObj.makerAddr: ', entryObj.makerAddr)
// Get Ticker for token ID.
const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId])
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
entryObj.ticker = tokenData[0].tokenTicker
// Input Validation
const orderEntity = this.orderEntity.validate(entryObj)
console.log('orderEntity: ', orderEntity)
// Ensure sufficient tokens exist to create the order.
await this.ensureFunds(orderEntity)
// Move the tokens to holding address.
const moveObj = {
tokenId: orderEntity.tokenId,
qty: orderEntity.numTokens
}
const utxoInfo = await this.adapters.wallet.moveTokens(moveObj)
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 order with the new UTXO information.
orderEntity.utxoTxid = utxoInfo.txid
orderEntity.utxoVout = utxoInfo.vout
// Add P2WDB specific flag for signaling that this is a new offer.
orderEntity.dataType = 'offer'
// Add order to P2WDB.
const p2wdbObj = {
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
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 Order
orderEntity.hdIndex = utxoInfo.hdIndex
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/createOrder())')
console.log('error entryObj: ', entryObj)
throw err
}
}
// Ensure that the wallet has enough BCH and tokens to complete the requested
// trade.
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)}`)
// Ensure the app wallet has enough funds to write to the P2WDB.
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
// Get UTXOs.
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
if (orderEntity.buyOrSell.includes('sell')) {
// Sell Order
// Get token UTXOs that match the token in the order.
const tokenUtxos = utxos.slpUtxos.type1.tokens.filter(
x => x.tokenId === orderEntity.tokenId
)
// console.log('tokenUtxos: ', tokenUtxos)
// Get the total amount of tokens in the wallet that match the token
// 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 order,
// throw an error.
if (totalTokenBalance <= orderEntity.numTokens || isNaN(totalTokenBalance)) {
throw new Error(
'App wallet does not have enough tokens to satisfy the SELL order.'
)
}
//
} else {
// Buy Order
throw new Error('Buy orders are not supported yet.')
}
return true
} catch (err) {
console.error('Error in ensureFunds()')
throw err
}
}
// Retrieve an Order model from the database. Find it by its P2WDB CID.
async findOrderByHash (p2wdbHash) {
try {
if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
throw new Error('p2wdbHash must be a string')
}
const order = await this.OrderModel.findOne({ p2wdbHash })
if (!order) {
throw new Error('order not found')
}
const orderObject = order.toObject()
// return this.offerEntity.validateFromModel(offerObject)
return orderObject
} catch (err) {
console.error('Error in findOrder(): ', err)
throw err
}
}
// This function is called by the garbage collection timer controller. It
// checks the UTXO associated with each Order in the database. If the UTXO
// has been spent, the Order is deleted from the database.
async removeStaleOrders () {
try {
const now = new Date()
console.log(`Starting garbage collection for Orders at ${now.toLocaleString()}`)
// Get all Orders in the database.
const orders = await this.OrderModel.find({})
// console.log('orders: ', orders)
// Loop through each Order and ensure the UTXO is still valid.
for (let i = 0; i < orders.length; i++) {
const thisOrder = orders[i]
let utxoStatus = null
try {
// Get the status of the UTXO associate with this Order.
utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
thisOrder.utxoTxid,
thisOrder.utxoVout
)
// console.log('utxoStatus: ', utxoStatus)
} catch (err) {
// Handle corner case of bad-data in the Order model.
if (err.message.includes('txid needs to be a proper transaction ID')) {
console.log(`Deleting Order with bad data: ${JSON.stringify(thisOrder, null, 2)}`)
await thisOrder.remove()
continue
} else {
throw err
}
}
// If the Order UTXO is spent, delete the Order model.
if (utxoStatus === null) {
console.log(`Spent UTXO detected. Deleting this Order: ${JSON.stringify(thisOrder, null, 2)}`)
await thisOrder.remove()
}
}
} catch (err) {
console.error('Error in removeStaleOrders()')
throw err
}
}
}
module.exports = OrderLib
-61
View File
@@ -1,61 +0,0 @@
/*
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
@@ -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,22 +11,22 @@ const LOCALHOST = 'http://localhost:5700'
async function start () {
try {
const mockOffer = {
const mockOrder = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 10,
numTokens: 0.02
rateInBaseUnit: 5000,
minUnitsToExchange: 5000,
numTokens: 1
}
const options = {
method: 'post',
url: `${LOCALHOST}/offer`,
data: { offer: mockOffer }
url: `${LOCALHOST}/order`,
data: { order: mockOrder }
}
const result = await axios(options)
+25
View File
@@ -0,0 +1,25 @@
/*
Part 2 of 3: Take an offer by submiting a counter-offer.
*/
const axios = require('axios')
const LOCALHOST = 'http://localhost:5700'
async function start () {
try {
const options = {
method: 'post',
url: `${LOCALHOST}/offer/take`,
data: {
offerCid: 'zdpuAkv76xbgqFEdFvcz28yNvtVjoZS7wsMo5wvuxWstycLRg'
}
}
const result = await axios(options)
console.log('result.data: ', result.data)
} catch (err) {
console.log(err)
}
}
start()
@@ -0,0 +1,21 @@
/*
Integration tests for the bch.js adapter.
*/
// Local libraries.
// const BchAdapter = require('../../../src/adapters/bch')
//
// describe('#wallet', () => {
// let uut
//
// beforeEach(() => {
// uut = new BchAdapter()
// })
//
// // describe('#getPSFTokenBalance', () => {
// // it('should get the PSF balance for the root address of the wallet', async () => {
// // await uut.wallet.walletInfoPromise
// // console.log('bch.wallet.walletInfo: ', uut.wallet.walletInfo)
// // })
// // })
// })
@@ -0,0 +1,54 @@
/*
Integration tests for hte p2wdb-adapter.js library
These integration tests may not necessarily pass. They require a WIF set
as an environment variable, and loaded with enough BCH and PSF to make
a write to the P2WDB.
If these tests fail, it does not necessarily mean there is a problem with the
code. Check that the WIF envrionment variable meets the requirements.
*/
// Public npm libraries
const BCHJS = require('@psf/bch-js')
// Local libraries
const P2wdbAdapter = require('../../../src/adapters/p2wdb-adapter')
const wif = process.env.WIF
if (!wif) {
throw new Error('You must provide a BCH WIF private key as an environment variable to run this test.')
}
describe('#p2wdb-adapter.js', () => {
let uut
beforeEach(() => {
const bchjs = new BCHJS()
uut = new P2wdbAdapter({ bchjs })
})
describe('#checkForSufficientFunds', () => {
it('should check for sufficient funds', async () => {
const result = await uut.checkForSufficientFunds(wif)
console.log('Provided WIF has funds for making a write: ', result)
})
})
describe('#write', () => {
it('should write data to the P2WDB', async () => {
const inObj = {
appId: 'test234',
data: {
key: 'value'
},
wif
}
const result = await uut.write(inObj)
console.log('result: ', result)
})
})
})
@@ -0,0 +1,96 @@
/*
Integration tests for the wallet.js adapter library.
*/
// Public npm libraries.
// const assert = require('chai').assert
// Local libraries.
const WalletAdapter = require('../../../src/adapters/wallet')
describe('#wallet', () => {
let uut
beforeEach(() => {
uut = new WalletAdapter()
})
// describe('#instanceWallet', () => {
// it('should instance the wallet using the web 3 infra by default', async () => {
// const walletData = await uut.openWallet()
// // console.log('walletData: ', walletData)
//
// const walletInstance = await uut.instanceWallet(walletData)
// // console.log('walletInstance: ', walletInstance)
//
// assert.equal(walletInstance.ar.interface, 'consumer-api')
// })
//
// it('should instance using web 2 FullStack.cash infra', async () => {
// const walletData = await uut.openWallet()
//
// // Force usage of FullStack.cash
// uut.config.useFullStackCash = true
//
// const walletInstance = await uut.instanceWallet(walletData)
// // console.log('walletInstance: ', walletInstance)
//
// assert.equal(walletInstance.ar.interface, 'rest-api')
// })
// })
//
// describe('#moveTokens', () => {
// it('should move tokens to a new address in the HD wallet', async () => {
// const walletData = await uut.openWallet()
//
// // Force usage of FullStack.cash
// uut.config.useFullStackCash = true
//
// await uut.instanceWallet(walletData)
//
// const inObj = {
// tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
// qty: 1
// }
//
// const result = await uut.moveTokens(inObj)
// // console.log('result: ', result)
//
// assert.property(result, 'txid')
// assert.property(result, 'vout')
// assert.property(result, 'hdIndex')
// })
// })
//
// describe('#moveBch', () => {
// it('should move BCH to a new address in the HD wallet', async () => {
// const walletData = await uut.openWallet()
//
// // Force usage of FullStack.cash
// uut.config.useFullStackCash = true
//
// await uut.instanceWallet(walletData)
//
// const amountSat = 1000
//
// const result = await uut.moveBch(amountSat)
// console.log('result: ', result)
// })
// })
describe('#completeTx', () => {
it('should complete the transaction with bchjs.TransactionBuilder', async () => {
const walletData = await uut.openWallet()
// Force usage of FullStack.cash
uut.config.useFullStackCash = true
await uut.instanceWallet(walletData)
const hex = '0200000002cf24c6f6e55fc84e7699223f7dac568aae991f1a49747758a797d06a516f718c0100000000ffffffff0c7a53c39a7f215c9fa0f409710cf5cb8926e40c104efcedb43d69d5e113fd9d000000006a47304402204d5d1ce837594a151ef8ccecf95b5c7ebb2e71560c6d2597fa559b92f9f032bc02206ef67cd2ef4b4df7207225c26c9982df184959ac389c3f49f1ff5dce641a72ee412103fdc2366a32184220e77fd50e7fff30e7c76b2b7246dea6d56b8177f9fca6fec7ffffffff030000000000000000376a04534c500001010453454e4420a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b208000000000000006422020000000000001976a914d46461bf6a5f3a8e0e0e92b85465ad79696ccb7688ac88130000000000001976a914d46461bf6a5f3a8e0e0e92b85465ad79696ccb7688ac00000000'
const result = await uut.completeTx(hex, 10)
console.log('result: ', result)
})
})
})
@@ -0,0 +1,44 @@
/*
Integration tests for the offer-use-case.js library
For these tests to pass, the app wallet must have Trout test tokens with
a token ID of a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2
*/
// Public npm libraries
const assert = require('chai').assert
// Local npm libraries
const Adapters = require('../../../src/adapters')
const Offer = require('../../../src/use-cases/offer')
describe('#offer-use-case.js', () => {
let uut
before(async () => {
const adapters = new Adapters()
adapters.config.getJwtAtStartup = false
await adapters.start()
uut = new Offer({ adapters })
})
describe('#ensureFunds', () => {
it('should return true if wallet has enough funds', async () => {
const offerEntity = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
buyOrSell: 'sell',
rateInBaseUnit: 1000,
minUnitsToExchange: 1250,
numTokens: 1
}
const result = await uut.ensureFunds(offerEntity)
assert.equal(result, true)
})
})
})
+52 -51
View File
@@ -7,7 +7,7 @@ const sinon = require('sinon')
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
const mockData = require('../mocks/bchjs-mock')
// const mockData = require('../mocks/bchjs-mock')
let sandbox
let uut
@@ -69,57 +69,58 @@ describe('bch', () => {
})
})
describe('#getPSFTokenBalance', () => {
it('should throw error if slpAddress is not provided', async () => {
try {
await uut.getPSFTokenBalance()
assert.fail('Unexpected result')
} catch (err) {
assert.include(err.message, 'slpAddress must be a string')
}
})
// describe('#getPSFTokenBalance', () => {
// it('should throw error if slpAddress is not provided', async () => {
// try {
// await uut.getPSFTokenBalance()
// assert.fail('Unexpected result')
// } catch (err) {
// assert.include(err.message, 'slpAddress must be a string')
// }
// })
//
// it('should return psf tokens balance', async () => {
// // Mock live network calls.
// sandbox
// .stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
// .resolves(mockData.psfBalances)
//
// const slpAddress =
// 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
// const result = await uut.getPSFTokenBalance(slpAddress)
//
// assert.isNumber(result)
// assert.equal(result, 2)
// })
//
// it('should return 0 if the slp address does not has Psf tokens', async () => {
// // Mock live network calls.
// sandbox
// .stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
// .resolves(mockData.noPsfBalances)
//
// const slpAddress =
// 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
// const result = await uut.getPSFTokenBalance(slpAddress)
//
// assert.isNumber(result)
// assert.equal(result, 0)
// })
//
// it('should return 0 for empty balances', async () => {
// // Mock live network calls.
// sandbox.stub(uut.bchjs.SLP.Utils, 'balancesForAddress').resolves([])
//
// const slpAddress =
// 'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
// const result = await uut.getPSFTokenBalance(slpAddress)
//
// assert.isNumber(result)
// assert.equal(result, 0)
// })
// })
it('should return psf tokens balance', async () => {
// Mock live network calls.
sandbox
.stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
.resolves(mockData.psfBalances)
const slpAddress =
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const result = await uut.getPSFTokenBalance(slpAddress)
assert.isNumber(result)
assert.equal(result, 2)
})
it('should return 0 if the slp address does not has Psf tokens', async () => {
// Mock live network calls.
sandbox
.stub(uut.bchjs.SLP.Utils, 'balancesForAddress')
.resolves(mockData.noPsfBalances)
const slpAddress =
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const result = await uut.getPSFTokenBalance(slpAddress)
assert.isNumber(result)
assert.equal(result, 0)
})
it('should return 0 for empty balances', async () => {
// Mock live network calls.
sandbox.stub(uut.bchjs.SLP.Utils, 'balancesForAddress').resolves([])
const slpAddress =
'simpleledger:qp49th03gvjn58d6fxzaga6u09w4z56smyuk43lzkd'
const result = await uut.getPSFTokenBalance(slpAddress)
assert.isNumber(result)
assert.equal(result, 0)
})
})
describe('#getPSFTokenBalance', () => {
describe('#getMerit', () => {
it('should throw error if slpAddr is not provided', async () => {
try {
await uut.getMerit()
@@ -74,8 +74,9 @@ describe('#IPFS', () => {
}
},
adapters: {
orbit: {
privateLog: () => {}
pubsub: {
privateLog: () => {
}
}
}
}
@@ -96,7 +97,7 @@ describe('#IPFS', () => {
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Cannot read property')
assert.include(err.message, 'Cannot read')
}
})
})
+22 -21
View File
@@ -52,7 +52,8 @@ describe('#IPFS-adapter', () => {
it('should stop the IPFS node', async () => {
// Mock dependencies
uut.ipfs = {
stop: () => {}
stop: () => {
}
}
const result = await uut.stop()
@@ -61,24 +62,24 @@ describe('#IPFS-adapter', () => {
})
})
describe('#rmBlocksDir', () => {
it('should delete the /blocks directory', () => {
const result = uut.rmBlocksDir()
assert.equal(result, true)
})
it('should catch and throw an error', () => {
try {
// Force an error
sandbox.stub(uut.fs, 'rmdirSync').throws(new Error('test error'))
uut.rmBlocksDir()
assert.fail('Unexpected code path')
} catch (err) {
assert.equal(err.message, 'test error')
}
})
})
// describe('#rmBlocksDir', () => {
// it('should delete the /blocks directory', () => {
// const result = uut.rmBlocksDir()
//
// assert.equal(result, true)
// })
//
// it('should catch and throw an error', () => {
// try {
// // Force an error
// sandbox.stub(uut.fs, 'rmdirSync').throws(new Error('test error'))
//
// uut.rmBlocksDir()
//
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.equal(err.message, 'test error')
// }
// })
// })
})
+108 -15
View File
@@ -5,48 +5,141 @@
// Public npm libraries.
const assert = require('chai').assert
const sinon = require('sinon')
const BCHJS = require('@psf/bch-js')
// Local libraries.
const P2wdbAdapter = require('../../../src/adapters/p2wdb')
const P2wdbAdapter = require('../../../src/adapters/p2wdb-adapter')
describe('#P2wdbAdapter', () => {
let uut, sandbox
beforeEach(() => {
uut = new P2wdbAdapter()
const bchjs = new BCHJS()
uut = new P2wdbAdapter({ bchjs })
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('#write', () => {
it('should write an offer to the P2WDB', async () => {
const inputObj = {
txid: 'testTxid',
signature: 'testSignature',
message: 'testMessage',
appId: 'testAppId',
data: { key: 'value' }
describe('#constructor', () => {
it('should throw error if bch-js is not passed in', () => {
try {
uut = new P2wdbAdapter()
assert.fail('Unexpected coded path')
} catch (err) {
assert.include(err.message, 'Must pass an instance of bch-js when instantiating p2wdb.js adapter.')
}
})
})
// Mock axios to prevent live network call
sandbox.stub(uut.axios, 'post').resolves({ data: { hash: 'testhash' } })
describe('#instantiateWriteLib', () => {
it('should instantiate Write library using web 3 interface', async () => {
// Force code path
uut.config.useFullStackCash = false
const result = await uut.write(inputObj)
// Mock dependencies
uut.Write =
class Write {
constructor () {
this.interface = 'web3'
}
}
const wif = 'L1tcvcqa5PztqqDH4ZEcUmHA9aSHhTau5E2Zwp1xEK5CrKBrjP3m'
const result = uut.instantiateWriteLib(wif)
// console.log('result: ', result)
assert.equal(result, 'testhash')
assert.equal(result.interface, 'web3')
})
it('should instantiate Write library using web 2 interface', async () => {
// Force code path
uut.config.useFullStackCash = true
// Mock dependencies
uut.Write =
class Write {
constructor () {
this.interface = 'fullstack'
}
}
const wif = 'L1tcvcqa5PztqqDH4ZEcUmHA9aSHhTau5E2Zwp1xEK5CrKBrjP3m'
const result = uut.instantiateWriteLib(wif)
// console.log('result: ', result)
assert.equal(result.interface, 'fullstack')
})
it('should catch and throw errors', async () => {
try {
await uut.instantiateWriteLib()
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'WIF private key required')
}
})
})
describe('#checkForSufficientFunds', () => {
it('should throw error if WIF is not provided', async () => {
try {
await uut.checkForSufficientFunds()
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'invalid wif to check for funds')
}
})
it('should return status of funds', async () => {
// Mock dependencies
sandbox.stub(uut, 'instantiateWriteLib').returns({
checkForSufficientFunds: () => true
})
const wif = 'L1tcvcqa5PztqqDH4ZEcUmHA9aSHhTau5E2Zwp1xEK5CrKBrjP3m'
const result = await uut.checkForSufficientFunds(wif)
assert.equal(result, true)
})
})
describe('#write', async () => {
it('should throw error if WIF is not provided', async () => {
try {
await uut.write()
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'Cannot destructure property')
assert.include(err.message, 'wif input must be a private key starting with the letter L or K')
}
})
it('should write data to the P2WDB', async () => {
// Mock dependencies
sandbox.stub(uut, 'instantiateWriteLib').returns({
postEntry: async () => {
return { hash: 'fake-hash' }
}
})
const wif = 'L1tcvcqa5PztqqDH4ZEcUmHA9aSHhTau5E2Zwp1xEK5CrKBrjP3m'
const inObj = { wif }
const result = await uut.write(inObj)
// console.log('result: ', result)
assert.equal(result, 'fake-hash')
})
})
})
+95 -47
View File
@@ -84,7 +84,8 @@ describe('#wallet', () => {
try {
// Force an error
uut.WALLET_FILE = ''
uut.BchWallet = () => {}
uut.BchWallet = () => {
}
await uut.openWallet()
// console.log('result: ', result)
@@ -102,19 +103,15 @@ describe('#wallet', () => {
// Mock dependencies
uut.BchWallet = MockBchWallet
const bchjs = {
restURL: 'dummyUrl',
apiToken: 'dummyToken'
}
// Ensure we open the test file, not the production wallet file.
uut.WALLET_FILE = testWalletFile
const walletData = await uut.openWallet()
const result = await uut.instanceWallet(walletData.mnemonic, bchjs)
const result = await uut.instanceWallet(walletData.mnemonic)
console.log('result: ', result)
assert.equal(result, true)
assert.property(result, 'walletInfoPromise')
})
it('should catch and throw an error', async () => {
@@ -124,7 +121,7 @@ describe('#wallet', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'Cannot read property')
assert.include(err.message, 'Cannot read')
}
})
})
@@ -159,44 +156,44 @@ describe('#wallet', () => {
})
})
describe('#burnPsf', () => {
it('should burn PSF tokens and return the txid', async () => {
// mock instance of minimal-slp-wallet
uut.bchWallet = new MockBchWallet()
const result = await uut.burnPsf()
// console.log('result: ', result)
assert.equal(result.success, true)
assert.equal(result.txid, 'txid')
})
it('should throw error if no PSF tokens are found', async () => {
try {
// mock instance of minimal-slp-wallet
uut.bchWallet = new MockBchWallet()
// Remove the PSF token from the mock data.
uut.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens.pop()
await uut.burnPsf()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Token UTXO of with ID of')
}
})
it('should catch and throw an error', async () => {
try {
await uut.burnPsf()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Cannot read property')
}
})
})
// describe('#burnPsf', () => {
// it('should burn PSF tokens and return the txid', async () => {
// // mock instance of minimal-slp-wallet
// uut.bchWallet = new MockBchWallet()
//
// const result = await uut.burnPsf()
// // console.log('result: ', result)
//
// assert.equal(result.success, true)
// assert.equal(result.txid, 'txid')
// })
//
// it('should throw error if no PSF tokens are found', async () => {
// try {
// // mock instance of minimal-slp-wallet
// uut.bchWallet = new MockBchWallet()
//
// // Remove the PSF token from the mock data.
// uut.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens.pop()
//
// await uut.burnPsf()
//
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, 'Token UTXO of with ID of')
// }
// })
//
// it('should catch and throw an error', async () => {
// try {
// await uut.burnPsf()
//
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, 'Cannot read')
// }
// })
// })
describe('#incrementNextAddress', () => {
it('should increment the nextAddress property', async () => {
@@ -256,6 +253,57 @@ describe('#wallet', () => {
}
})
})
describe('#moveTokens', () => {
it('should move tokens to a new address in the HD wallet', async () => {
// Mock dependencies
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'bitcoincash:qqsj63493jk4p05zzdgqzc29k5unqtet9vv8l4x0yt',
wif: 'L4qKTMCwjH9jHnYtNh9Vsrxj7Hg6zmoN8E2v7N47UKvNVEjw7FU8',
hdIndex: 6
})
uut.bchWallet = {
sendTokens: async () => 'fake-txid',
getUtxos: async () => {}
}
const inObj = {
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
qty: 1
}
const result = await uut.moveTokens(inObj)
// console.log('result: ', result)
assert.property(result, 'txid')
assert.property(result, 'vout')
assert.property(result, 'hdIndex')
})
})
describe('#moveBch', () => {
it('should move BCH to a new address in the HD wallet', async () => {
// Mock dependencies
sandbox.stub(uut, 'getKeyPair').resolves({
cashAddress: 'bitcoincash:qqsj63493jk4p05zzdgqzc29k5unqtet9vv8l4x0yt',
wif: 'L4qKTMCwjH9jHnYtNh9Vsrxj7Hg6zmoN8E2v7N47UKvNVEjw7FU8',
hdIndex: 6
})
uut.bchWallet = {
send: async () => 'fake-txid',
getUtxos: async () => {}
}
const amountSat = 1000
const result = await uut.moveBch(amountSat)
// console.log('result: ', result)
assert.property(result, 'txid')
assert.property(result, 'vout')
assert.property(result, 'hdIndex')
})
})
})
const deleteFile = (filepath) => {
@@ -333,7 +333,7 @@ describe('#UserRPC', () => {
assert.equal(result.endpoint, 'updateUser')
assert.equal(result.success, false)
assert.equal(result.status, 422)
assert.include(result.message, 'Cannot read property')
assert.include(result.message, 'Cannot read')
})
})
@@ -375,7 +375,7 @@ describe('#UserRPC', () => {
assert.equal(result.endpoint, 'getUser')
assert.equal(result.success, false)
assert.equal(result.status, 422)
assert.include(result.message, 'Cannot read property')
assert.include(result.message, 'Cannot read')
})
})
@@ -14,7 +14,8 @@ let ctx
const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('Contact', () => {
before(async () => {})
before(async () => {
})
beforeEach(() => {
uut = new ContactController()
@@ -36,7 +37,7 @@ describe('Contact', () => {
} catch (err) {
// console.log(err)
assert.equal(err.status, 422)
assert.include(err.message, 'Cannot read property')
assert.include(err.message, 'Cannot read')
}
})
@@ -69,7 +69,7 @@ describe('#Entry-REST-Controller', () => {
} catch (err) {
// console.log(err)
assert.equal(err.status, 422)
assert.include(err.message, 'Cannot read property')
assert.include(err.message, 'Cannot read')
}
})
@@ -79,8 +79,7 @@ describe('#Entry-REST-Controller', () => {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
signature:
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
signature: 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
category: 'test'
}
}
@@ -14,7 +14,8 @@ let ctx
const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('Logapi', () => {
before(async () => {})
before(async () => {
})
beforeEach(() => {
uut = new LogsApiController()
@@ -35,7 +36,7 @@ describe('Logapi', () => {
assert.fail('Unexpected result')
} catch (err) {
assert.equal(err.status, 422)
assert.include(err.message, 'Cannot read property')
assert.include(err.message, 'Cannot read')
}
})
@@ -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', () => {
@@ -69,7 +69,7 @@ describe('#Users-REST-Controller', () => {
} catch (err) {
// console.log(err)
assert.equal(err.status, 422)
assert.include(err.message, 'Cannot read property')
assert.include(err.message, 'Cannot read')
}
})
@@ -91,9 +91,9 @@ describe('#Users-REST-Controller', () => {
assert.property(ctx.response.body, 'user')
assert.property(ctx.response.body, 'token')
// Used by downstream tests.
// testUser = ctx.response.body.user
// console.log('testUser: ', testUser)
// Used by downstream tests.
// testUser = ctx.response.body.user
// console.log('testUser: ', testUser)
})
})
@@ -181,7 +181,7 @@ describe('#Users-REST-Controller', () => {
} catch (err) {
// console.log(err)
assert.equal(err.status, 422)
assert.include(err.message, 'Cannot read property')
assert.include(err.message, 'Cannot read')
}
})
@@ -223,7 +223,7 @@ describe('#Users-REST-Controller', () => {
} catch (err) {
// console.log(err)
assert.equal(err.status, 422)
assert.include(err.message, 'Cannot read property')
assert.include(err.message, 'Cannot read')
}
})
+185 -35
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
@@ -25,16 +25,23 @@ describe('#Offer-Entity', () => {
it('should throw an error if data is not provided', () => {
try {
uut.validate()
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'Cannot destructure property')
assert.include(
err.message,
'Input to offer.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 offerData = { data: {} }
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(
@@ -46,8 +53,10 @@ describe('#Offer-Entity', () => {
it('should throw an error if messageClass is not included', () => {
try {
const data = { messageType: 1 }
uut.validate(data)
const offerData = { data: { messageType: 1 } }
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(
@@ -59,8 +68,10 @@ describe('#Offer-Entity', () => {
it('should throw an error if tokenId is not included', () => {
try {
const data = { messageType: 1, messageClass: 1 }
uut.validate(data)
const offerData = { data: { messageType: 1, messageClass: 1 } }
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'tokenId' must be a string.")
@@ -69,66 +80,205 @@ 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 offerData = {
data: { messageType: 1, messageClass: 1, tokenId: 'fakeId' }
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} 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', () => {
it('should throw an error if rateInBaseUnit is not included', () => {
try {
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy'
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy'
}
}
uut.validate(data)
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(
err.message,
"Property 'rateInSats' must be an integer number."
"Property 'rateInBaseUnit' must be an integer number."
)
}
})
it('should throw an error if minSatsToExchange is not included', () => {
it('should throw an error if minUnitsToExchange is not included', () => {
try {
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInSats: 1000
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000
}
}
uut.validate(data)
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(
err.message,
"Property 'minSatsToExchange' must be an integer number."
"Property 'minUnitsToExchange' must be an integer number."
)
}
})
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 offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350
}
}
uut.validate(data)
uut.validate(offerData)
assert.fail('Unexpected code path')
} 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 offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} 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 offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid'
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(
err.message,
"Property 'utxoVout' must be an integer number."
)
}
})
it('should throw an error if proper status is not applied', () => {
try {
const offerData = {
data: {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 350,
numTokens: 1,
utxoTxid: 'fakeTxid',
utxoVout: 0,
offerStatus: 'badStatus'
}
}
uut.validate(offerData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(
err.message,
"Property 'offerStatus' must be posted, taken, or dead"
)
}
})
it('should validate a new offer', () => {
const offerObj = {
appId: 'swapTest555',
data: {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInBaseUnit: 1000,
minUnitsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0,
offerStatus: 'posted',
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
ticker: 'TROUT'
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
}
const result = uut.validate(offerObj)
// console.log('result: ', result)
assert.property(result, 'messageType')
assert.property(result, 'messageClass')
assert.property(result, 'tokenId')
assert.property(result, 'buyOrSell')
assert.property(result, 'rateInBaseUnit')
assert.property(result, 'minUnitsToExchange')
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')
})
})
})
+35 -133
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,161 +69,66 @@ 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.")
}
})
it('should throw an error if rateInSats is not included', () => {
it('should throw an error if rateInBaseUnit 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(
err.message,
"Property 'rateInSats' must be an integer number."
"Property 'rateInBaseUnit' must be an integer number."
)
}
})
it('should throw an error if minSatsToExchange is not included', () => {
it('should throw an error if minUnitsToExchange 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',
rateInBaseUnit: 1000
}
uut.validate(orderData)
uut.validate(data)
} catch (err) {
// console.log(err)
assert.include(
err.message,
"Property 'minSatsToExchange' must be an integer number."
"Property 'minUnitsToExchange' 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
}
const data = {
messageType: 1,
messageClass: 1,
tokenId: 'fakeId',
buyOrSell: 'buy',
rateInBaseUnit: 1000,
minUnitsToExchange: 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')
})
})
})
+24 -6
View File
@@ -13,7 +13,8 @@ const ipfs = {
ipfsCoord: {
useCases: {
peer: {
sendPrivateMessage: () => {}
sendPrivateMessage: () => {
}
}
}
}
@@ -82,6 +83,18 @@ const localdb = {
static find () {}
static findOne () {}
async save () {
return {}
}
},
Offer: class Offer {
constructor (obj) {}
static findById () {}
static find () {}
static findOne () {}
async save () {
return {}
}
@@ -106,16 +119,21 @@ const bch = {
// }
const { MockBchWallet } = require('./wallet')
const wallet = {
burnPsf: async () => {},
generateSignature: async () => {},
burnPsf: async () => {
},
generateSignature: async () => {
},
getKeyPair: async () => {
return { cashAddress: 'fakeAddr', wif: 'fakeWif', hdIndex: 1 }
},
bchWallet: new MockBchWallet()
bchWallet: new MockBchWallet(),
moveTokens: async () => {}
}
const p2wdb = {
write: async () => {}
write: async () => {
},
checkForSufficientFunds: async () => true
}
module.exports = { ipfs, localdb, bch, wallet, p2wdb, bchjs }
module.exports = { ipfs, localdb, bch, wallet, p2wdb, bchjs}
+65 -48
View File
@@ -5,11 +5,9 @@
const BCHJS = require('@psf/bch-js')
const mockWallet = {
mnemonic:
'course abstract aerobic deer try switch turtle diet fence affair butter top',
mnemonic: 'course abstract aerobic deer try switch turtle diet fence affair butter top',
privateKey: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
publicKey:
'0379433ffc401483ade310469953c1cba77c71af904f07c15bde330d7198b4d6dc',
publicKey: '0379433ffc401483ade310469953c1cba77c71af904f07c15bde330d7198b4d6dc',
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
address: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
slpAddress: 'simpleledger:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acq9zm7d33',
@@ -29,7 +27,13 @@ class MockBchWallet {
this.sendTokens = async () => {
return 'fakeTxid'
}
this.getUtxos = async () => {}
this.getUtxos = async () => {
}
this.getTxData = async () => {
return [{
tokenTicker: 'TROUT'
}]
}
// Environment variable is used by wallet-balance.unit.js to force an error.
if (process.env.NO_UTXO) {
@@ -41,8 +45,7 @@ class MockBchWallet {
bchUtxos: [
{
height: 700685,
tx_hash:
'1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
tx_pos: 0,
value: 1000,
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
@@ -51,8 +54,7 @@ class MockBchWallet {
},
{
height: 700685,
tx_hash:
'1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
tx_pos: 2,
value: 19406,
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
@@ -66,46 +68,61 @@ class MockBchWallet {
mintBatons: [],
tokens: [
{
height: 700522,
tx_hash:
'bb5691b50930816be78dad76d203a1c97ac94c03f6051b2fa0159c71c43aa3d0',
tx_pos: 1,
value: 546,
txid: 'bb5691b50930816be78dad76d203a1c97ac94c03f6051b2fa0159c71c43aa3d0',
vout: 1,
utxoType: 'token',
transactionType: 'send',
tokenId:
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
tokenTicker: 'TROUT',
tokenName: "Trout's test token",
tokenDocumentUrl: 'troutsblog.com',
tokenDocumentHash: '',
decimals: 2,
tokenType: 1,
isValid: true,
tokenQty: '4.25'
'height': 717331,
'tx_hash': '74889580bb1a5f8c026aa2f55118ac9917df3332f7abae72a70343daa1c29621',
'tx_pos': 1,
'value': 546,
'txid': '74889580bb1a5f8c026aa2f55118ac9917df3332f7abae72a70343daa1c29621',
'vout': 1,
'isSlp': true,
'type': 'token',
'qty': '10',
'tokenId': '600ee24d0f208aebc2bdd2c4ee1b9acb6d57343561442e8676b5bbea311d5a0f',
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
'ticker': 'FLIPS',
'name': 'FLIPS',
'documentUri': '',
'documentHash': '',
'decimals': 1,
'qtyStr': '1'
},
{
height: 0,
tx_hash:
'c0ac066ce6efa1fa4763bf85a91c738e57c12b8765731bd07f0d8f5a55ce582f',
tx_pos: 1,
value: 546,
txid: 'c0ac066ce6efa1fa4763bf85a91c738e57c12b8765731bd07f0d8f5a55ce582f',
vout: 1,
utxoType: 'token',
transactionType: 'send',
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
tokenTicker: 'PSF',
tokenName: 'Permissionless Software Foundation',
tokenDocumentUrl: 'psfoundation.cash',
tokenDocumentHash: '',
decimals: 8,
tokenType: 1,
isValid: true,
tokenQty: '1'
'height': 730597,
'tx_hash': '52520faddfafc46b8f8c9548b097f3a3b82a5bf363b5095047b9c5f83247fe36',
'tx_pos': 1,
'value': 546,
'txid': '52520faddfafc46b8f8c9548b097f3a3b82a5bf363b5095047b9c5f83247fe36',
'vout': 1,
'isSlp': true,
'type': 'token',
'qty': '34999991',
'tokenId': '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
'ticker': 'PSF',
'name': 'Permissionless Software Foundation',
'documentUri': 'psfoundation.cash',
'documentHash': '',
'decimals': 8,
'qtyStr': '0.34999991'
},
{
'height': 730597,
'tx_hash': '5dc7e7c91382aed1666a51212dfb74050261e12c3c4f62b6b1e57f42d6c51ee1',
'tx_pos': 2,
'value': 546,
'txid': '5dc7e7c91382aed1666a51212dfb74050261e12c3c4f62b6b1e57f42d6c51ee1',
'vout': 2,
'isSlp': true,
'type': 'token',
'qty': '18898',
'tokenId': 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
'ticker': 'TROUT',
'name': "Trout's test token",
'documentUri': 'troutsblog.com',
'documentHash': '',
'decimals': 2,
'qtyStr': '188.98'
}
]
},
@@ -121,4 +138,4 @@ class MockBchWallet {
}
}
module.exports = { MockBchWallet, mockWallet }
module.exports = { MockBchWallet, mockWallet}
+173 -172
View File
@@ -9,7 +9,7 @@ const sinon = require('sinon')
const EntryLib = require('../../../src/use-cases/entry')
const adapters = require('../mocks/adapters')
describe('#users-use-case', () => {
describe('#entry-use-case', () => {
let uut
let sandbox
@@ -32,6 +32,7 @@ describe('#users-use-case', () => {
uut = new EntryLib()
assert.fail('Unexpected code path')
console.log(uut)
} catch (err) {
assert.include(
err.message,
@@ -41,175 +42,175 @@ describe('#users-use-case', () => {
})
})
describe('#createEntry', () => {
it('should throw an error if entry is not provided', async () => {
try {
await uut.createEntry({})
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'entry' must be a string!")
}
})
it('should throw an error if description is not provided', async () => {
try {
const inputData = {
entry: 'entry'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'description' must be a string!")
}
})
it('should throw an error if slpAddress is not provided', async () => {
try {
const inputData = {
entry: 'entry',
description: 'test'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, "Property 'slpAddress' must be a string!")
}
})
it('should throw an error if signature is not provided', async () => {
try {
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
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='
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
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.
sandbox.stub(uut, 'EntryModel').throws(new Error('test error'))
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
signature:
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
category: 'test'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should throw error if signature is invalid', async () => {
try {
// Mocking bchjs functions
sandbox.stub(uut.bch, '_verifySignature').callsFake(() => {
return false
})
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
signature:
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
category: 'test'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Invalid signature')
}
})
it('should throw error for insufficient psf balance', async () => {
try {
// Mocking bchjs functions
sandbox.stub(uut.bch, 'getPSFTokenBalance').resolves(0)
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
signature:
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
category: 'test'
}
await uut.createEntry(inputData)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Insufficient psf balance')
}
})
it('should create a new entry in the DB', async () => {
// Mocking bchjs functions
// sandbox.stub(uut.bchjs, '_verifySignature').resolves(true)
// sandbox.stub(uut.bchjs, 'getPSFTokenBalance').resolves(100)
// sandbox.stub(uut.bchjs, 'getMerit').resolves(100)
const inputData = {
entry: 'entry',
description: 'test',
slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
signature:
'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
category: 'test'
}
const result = await uut.createEntry(inputData)
assert.property(result, 'entry')
assert.isString(result.entry)
assert.property(result, 'slpAddress')
assert.isString(result.slpAddress)
assert.property(result, 'description')
assert.isString(result.description)
assert.property(result, 'signature')
assert.isString(result.signature)
assert.property(result, 'category')
assert.isString(result.category)
assert.property(result, 'balance')
assert.isNumber(result.balance)
assert.property(result, 'merit')
assert.isNumber(result.merit)
})
})
// describe('#createEntry', () => {
// it('should throw an error if entry is not provided', async () => {
// try {
// await uut.createEntry({})
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, "Property 'entry' must be a string!")
// }
// })
//
// it('should throw an error if description is not provided', async () => {
// try {
// const inputData = {
// entry: 'entry'
// }
// await uut.createEntry(inputData)
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, "Property 'description' must be a string!")
// }
// })
//
// it('should throw an error if slpAddress is not provided', async () => {
// try {
// const inputData = {
// entry: 'entry',
// description: 'test'
// }
// await uut.createEntry(inputData)
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, "Property 'slpAddress' must be a string!")
// }
// })
//
// it('should throw an error if signature is not provided', async () => {
// try {
// const inputData = {
// entry: 'entry',
// description: 'test',
// slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg'
// }
// await uut.createEntry(inputData)
// assert.fail('Unexpected code path')
// } catch (err) {
// 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='
// }
// await uut.createEntry(inputData)
// assert.fail('Unexpected code path')
// } catch (err) {
// 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.
// sandbox.stub(uut, 'EntryModel').throws(new Error('test error'))
//
// const inputData = {
// entry: 'entry',
// description: 'test',
// slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
// signature:
// 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
// category: 'test'
// }
//
// await uut.createEntry(inputData)
//
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, 'test error')
// }
// })
//
// it('should throw error if signature is invalid', async () => {
// try {
// // Mocking bchjs functions
// sandbox.stub(uut.bch, '_verifySignature').callsFake(() => {
// return false
// })
//
// const inputData = {
// entry: 'entry',
// description: 'test',
// slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
// signature:
// 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
// category: 'test'
// }
//
// await uut.createEntry(inputData)
//
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, 'Invalid signature')
// }
// })
// it('should throw error for insufficient psf balance', async () => {
// try {
// // Mocking bchjs functions
// sandbox.stub(uut.bch, 'getPSFTokenBalance').resolves(0)
//
// const inputData = {
// entry: 'entry',
// description: 'test',
// slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
// signature:
// 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
// category: 'test'
// }
//
// await uut.createEntry(inputData)
//
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, 'Insufficient psf balance')
// }
// })
//
// it('should create a new entry in the DB', async () => {
// // Mocking bchjs functions
// // sandbox.stub(uut.bchjs, '_verifySignature').resolves(true)
// // sandbox.stub(uut.bchjs, 'getPSFTokenBalance').resolves(100)
// // sandbox.stub(uut.bchjs, 'getMerit').resolves(100)
//
// const inputData = {
// entry: 'entry',
// description: 'test',
// slpAddress: 'simpleledger:qpnty9t0w93fez04h7yzevujpv8pun204qqp0jfafg',
// signature:
// 'IFytRg6KpvTHCzcW0ZwVhPqdKtRGpoRDcuEb958yIgJFUJlb1F5qPzt/JnlYE7r012BSFj+UT67DZVTU8oNB5vw=',
// category: 'test'
// }
//
// const result = await uut.createEntry(inputData)
//
// assert.property(result, 'entry')
// assert.isString(result.entry)
//
// assert.property(result, 'slpAddress')
// assert.isString(result.slpAddress)
//
// assert.property(result, 'description')
// assert.isString(result.description)
//
// assert.property(result, 'signature')
// assert.isString(result.signature)
//
// assert.property(result, 'category')
// assert.isString(result.category)
//
// assert.property(result, 'balance')
// assert.isNumber(result.balance)
//
// assert.property(result, 'merit')
// assert.isNumber(result.merit)
// })
// })
})
+65 -82
View File
@@ -11,6 +11,7 @@ const sinon = require('sinon')
// Unit under test (uut)
const OfferLib = require('../../../src/use-cases/offer')
const OrderUseCase = require('../../../src/use-cases/order')
const adapters = require('../mocks/adapters')
describe('#offer-use-case', () => {
@@ -25,7 +26,8 @@ describe('#offer-use-case', () => {
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new OfferLib({ adapters })
const order = new OrderUseCase({ adapters })
uut = new OfferLib({ adapters, order })
})
afterEach(() => sandbox.restore())
@@ -47,98 +49,79 @@ describe('#offer-use-case', () => {
})
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
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'
}
// 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 })
sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves()
sandbox
.stub(uut.adapters.wallet, 'generateSignature')
.resolves('fakeSignature')
sandbox.stub(uut.adapters.p2wdb, 'write').resolves('fakeHash')
sandbox.stub(uut.adapters.bchjs.Blockchain, 'getTxOut').resolves(null)
const result = await uut.createOffer(entryObj)
console.log('result: ', result)
const result = await uut.createOffer(offerObj)
// console.log('result: ', result)
assert.isString(result)
assert.equal(result, false)
})
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')
}
})
})
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
it('should create an offer and return the hash', async () => {
const offerObj = {
appId: 'swapTest555',
data: {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInBaseUnit: 1000,
minUnitsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0,
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
ticker: 'TROUT'
},
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({
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, 0)
})
})
})
+81 -68
View File
@@ -46,78 +46,91 @@ 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',
// rateInBaseUnit: 1000,
// minUnitsToExchange: 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',
rateInBaseUnit: 1000,
minUnitsToExchange: 1250,
numTokens: 1
}
// Mock dependencies
// sandbox.stub(uut.adapters.wallet, 'burnPsf').resolves('fakeTxid')
// sandbox.stub(uut.adapters.wallet.bchWallet, 'getTxData').resolves({ tokenTicker: 'TROUT' })
sandbox.stub(uut.orderEntity, 'validate').returns(entryObj)
sandbox.stub(uut, 'ensureFunds').resolves()
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.Util, 'sleep').resolves()
sandbox.stub(uut.adapters.wallet, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves()
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 {
await uut.createOrder()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Cannot set')
}
})
})
})
+9 -9
View File
@@ -153,7 +153,7 @@ describe('#users-use-case', () => {
await uut.getAllUsers()
// console.log(`users: ${JSON.stringify(users, null, 2)}`)
// assert.isArray(users)
// assert.isArray(users)
})
it('should catch and throw an error', async () => {
@@ -228,8 +228,8 @@ describe('#users-use-case', () => {
// Assert that the expected properties for the user model exist.
// assert.property(result, 'type')
assert.property(result, '_id')
// assert.property(result, 'email')
// assert.property(result, 'name')
// assert.property(result, 'email')
// assert.property(result, 'name')
})
})
@@ -241,7 +241,7 @@ describe('#users-use-case', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'Cannot read property')
assert.include(err.message, 'Cannot read')
}
})
@@ -346,7 +346,7 @@ describe('#users-use-case', () => {
// assert.equal(result.name, 'testy tester')
// })
// TODO: verify that an admin can change the type of a user
// TODO: verify that an admin can change the type of a user
})
describe('#authUser', () => {
@@ -356,9 +356,9 @@ describe('#users-use-case', () => {
await uut.authUser('test@test.com', 'password')
// console.log('user: ', user)
// assert.property(user, '_id')
// assert.property(user, 'email')
// assert.property(user, 'name')
// assert.property(user, '_id')
// assert.property(user, 'email')
// assert.property(user, 'name')
})
it('should throw an error if no user matches the login', async () => {
@@ -399,7 +399,7 @@ describe('#users-use-case', () => {
assert.fail('Unexpected code path.')
} catch (err) {
// console.log(err)
assert.include(err.message, 'Cannot read property')
assert.include(err.message, 'Cannot read')
}
})
+37
View File
@@ -0,0 +1,37 @@
/*
This script has not been customized for offers yet.
*/
const mongoose = require('mongoose')
// Force test environment
// make sure environment variable is set before this file gets called.
// see test script in package.json.
// process.env.KOA_ENV = 'test'
const config = require('../../config')
const User = require('../../src/models/users')
async function deleteUsers () {
// Connect to the Mongo Database.
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(config.database, {
useUnifiedTopology: true,
useNewUrlParser: true
})
// Get all the users in the DB.
const users = await User.find({}, '-password')
// console.log(`users: ${JSON.stringify(users, null, 2)}`)
// Delete each user.
for (let i = 0; i < users.length; i++) {
const thisUser = users[i]
await thisUser.remove()
}
mongoose.connection.close()
}
deleteUsers()
+25
View File
@@ -0,0 +1,25 @@
/*
Get all Offers in the database.
*/
const mongoose = require('mongoose')
const config = require('../../config')
const Offer = require('../../src/adapters/localdb/models/offer')
async function getOffers () {
// Connect to the Mongo Database.
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(config.database, {
useNewUrlParser: true,
useUnifiedTopology: true
})
const offers = await Offer.find({})
console.log(`offers: ${JSON.stringify(offers, null, 2)}`)
mongoose.connection.close()
}
getOffers()
+84
View File
@@ -0,0 +1,84 @@
/*
This script will travers the HD wallet and sweep funds and tokens back
into the root address (index 0). That root address needs to have funds
to pay for the transactions.
The root address will make a final transaction to consolidate all tokens.
*/
// Public npm libraries
const BCHJS = require('@psf/bch-js')
const BchTokenSweep = require('bch-token-sweep/index')
// Local libraries
const WalletAdapter = require('../../src/adapters/wallet')
// Constants
const EMTPY_ADDR_CUTOFF = 5
async function sweepFunds () {
try {
// Open the wallet files.
const wallet = new WalletAdapter()
const walletInfo = await wallet.openWallet()
console.log('walletInfo: ', walletInfo)
const rootAddr = walletInfo.cashAddress
const rootWif = walletInfo.privateKey
console.log(`Sweeping all funds into root address ${rootAddr}...`)
// Generate an HD tree
const bchjs = new BCHJS()
const rootSeed = await bchjs.Mnemonic.toSeed(walletInfo.mnemonic)
const masterHDNode = bchjs.HDNode.fromSeed(rootSeed)
let emptyAddrCnt = 0
let hdIndex = 1
do {
// Generate a keypair from the HD wallet.
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
const cashAddress = bchjs.HDNode.toCashAddress(childNode)
const wifToSweep = bchjs.HDNode.toWIF(childNode)
console.log(`\nSweeping ${cashAddress}`)
try {
// Sweep tokens from address
const sweeper = new BchTokenSweep(
wifToSweep,
rootWif,
bchjs,
550,
rootAddr
)
await sweeper.populateObjectFromNetwork()
const hex = await sweeper.sweepTo(rootAddr)
// console.log(`hex: ${hex}`)
const txid = await sweeper.blockchain.broadcast(hex)
// console.log('Transaction ID', txid)
console.log(`Swept HD index ${hdIndex}. TXID: ${txid}`)
emptyAddrCnt = 0
// Wait between loop iterations.
await bchjs.Util.sleep(3000)
} catch (err) {
console.log(`error message with index ${hdIndex}: ${err.message}`)
emptyAddrCnt++
}
hdIndex++
} while (emptyAddrCnt < EMTPY_ADDR_CUTOFF)
console.log(`${EMTPY_ADDR_CUTOFF} empty addresses detected. Exiting.`)
console.log('\n\nDo not forget to reset the nextAddress property in the wallet.json file!\n\n')
} catch (err) {
console.error('Error in sweepFunds(): ', err)
}
}
sweepFunds()