Compare commits

...
31 Commits
Author SHA1 Message Date
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
43 changed files with 23842 additions and 6147 deletions
+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}/order`)
} catch (err) {
/* exit quietly */
// console.log('err deleting webhook: ', err)
}
await webhookLib.createWebhook('http://localhost:5700/order')
await webhookLib.createWebhook(`http://localhost:${config.port}/order`)
console.log('Webhook created')
} catch (error) {
console.log('Webhook cant be created')
+14 -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,15 @@ 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',
// 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 +70,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 +80,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'
}
+19 -20
View File
@@ -1,30 +1,29 @@
# 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.
- [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 submitting the data to the P2WDB to generate an _Order_ (solid line).
This architecture keeps the global database highly censorship resistant, while allowing local installations to maintain tight control over the user experience. The goal is to have many 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.
# 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 +31,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)
@@ -48,17 +47,17 @@ These are just a brief, high-level overview. Review the [Specifications](./speci
## Writing to the Global Database
Adding data to the global P2WDB is a result of the interaction between the _Client_ and the P2WDB. Ideally, `ipfs-swap-service` is not involved. The [p2wdb npm library](https://www.npmjs.com/package/p2wdb) can be leveraged for easy reading and writing to the P2WDB.
During development, `ipfs-swap-service` is being used to submit Offers to the P2WDB and custody funds. When the project reaches maturity, these functions may be removed, and they should be handled by the Client, so that legal issues around custody of funds are not a problem.
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 `/offer` REST API endpoint to create a new Offer.
- `bch-dex` will move the funds into a segregated UTXO, and will use that UTXO to create an Offer. The Offer data is written to the P2WDB. The Offer data is also saved to the local MongoDB.
- After submitting the data to the P2WDB, `bch-dex` will receive a webhook call to its POST `/order` endpoint by the P2WDB. This event will trigger the import of the new data into the apps local Mongo database, and generate a new Order model.
- This webhook event is mirrored by every instance of `bch-dex` on the network. Each P2WDB peer on the network will independently validate the new database entry and create a new Order model.
## Reading from the Local Database
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.
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.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

+6 -6
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
@@ -44,9 +44,9 @@ Order entities have the following properties:
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.
to track an 'Order' generated and managed by this application.
The Offer tracks the [HD index address](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05.asciidoc#hd-wallets-bip-32bip-44) used to hold tokens or BCH for sale. This is the part of the app concerned with the custody of the funds. It creates a segregated [UTXO](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch06.asciidoc#transaction-outputs-and-inputs) to hold the offered asset. The Offer is automatically destroyed if the UTXO is accidentally spent, which is why it needs to be segregated from other wallet UTXOs.
The Offer tracks the [HD index address](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05.asciidoc#hd-wallets-bip-32bip-44) used to hold tokens or BCH for sale. This is the part of the app concerned with the custody of the funds. It creates a segregated [UTXO](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch06.asciidoc#transaction-outputs-and-inputs) to hold the offered asset. The Offer is automatically destroyed if the UTXO is spent.
Offer entities have the following properties:
@@ -93,7 +93,7 @@ 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 will be triggered by a webhook generated from P2WDB. This will notify the `bch-dex` that a new entry has been added to the P2WDB that matches the `appId` of `swap-<chain>`, where `<chain>` has a value of `avax`, `bch`, or `ecash`. It's a new entry that should be evaluated for inclusion in the `ipfs-swap-service` local database.
### Offers
+23020 -5721
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -9,6 +9,7 @@
"test:all": "export BCH_DEX=test && nyc --reporter=text mocha --exit --timeout 15000 --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: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",
@@ -32,7 +33,7 @@
"bcryptjs": "2.4.3",
"glob": "7.1.6",
"ipfs": "0.60.0",
"ipfs-coord": "6.8.12",
"ipfs-coord": "7.1.4",
"ipfs-http-client": "55.0.0",
"jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1",
@@ -50,10 +51,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.2",
"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",
@@ -68,6 +70,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",
+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
}
}
+1
View File
@@ -14,6 +14,7 @@ const Offer = new mongoose.Schema({
utxoTxid: { type: String },
utxoVout: { type: Number },
numTokens: { type: Number },
hdIndex: { type: Number }, // HD index address holding the UTXO for this offer.
//
offerIpfsId: { type: String },
+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
+94 -85
View File
@@ -7,11 +7,12 @@ const BchWallet = require('minimal-slp-wallet/index')
// 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 +20,12 @@ class WalletAdapter {
this.jsonFiles = new JsonFiles()
this.WALLET_FILE = WALLET_FILE
this.BchWallet = BchWallet
this.config = config
}
// 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 +36,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 +63,35 @@ 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
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.
@@ -107,7 +139,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 +156,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,61 +178,63 @@ class WalletAdapter {
}
}
// Burn enough PSF to generate a valide proof-of-burn for writing to the P2WDB.
async burnPsf () {
try {
// TODO: Throw error if this.bchWallet has not been instantiated.
// console.log('walletData: ', walletData)
// console.log(
// `walletData.utxos.utxoStore.slpUtxos: ${JSON.stringify(
// walletData.utxos.utxoStore.slpUtxos,
// null,
// 2,
// )}`,
// )
// Get token UTXOs held by the wallet.
const tokenUtxos = this.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens
// 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]
// 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
)
// console.log('walletData.burnTokens() result: ', result)
return result
// return {
// success: true,
// txid: 'fakeTxid',
// }
} catch (err) {
console.error('Error in burnPsf(): ', err)
throw err
}
}
// Burn enough PSF to generate a valide proof-of-burn for writing to the P2WDB.
// async burnPsf () {
// try {
// // TODO: Throw error if this.bchWallet has not been instantiated.
//
// // console.log('walletData: ', walletData)
// // console.log(
// // `walletData.utxos.utxoStore.slpUtxos: ${JSON.stringify(
// // walletData.utxos.utxoStore.slpUtxos,
// // null,
// // 2,
// // )}`,
// // )
//
// // Get token UTXOs held by the wallet.
// const tokenUtxos = this.bchWallet.utxos.utxoStore.slpUtxos.type1.tokens
// // console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
//
// // 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]
//
// // If token ID matches.
// if (thisUtxo.tokenId === P2WDB_TOKEN_ID) {
// if (parseFloat(thisUtxo.qtyStr) >= 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.`
// )
// }
// // console.log(`tokenUtxo: ${JSON.stringify(tokenUtxo, null, 2)}`)
//
// const result = await this.bchWallet.burnTokens(
// PROOF_OF_BURN_QTY,
// P2WDB_TOKEN_ID
// )
// // console.log('walletData.burnTokens() result: ', result)
//
// return result
//
// // return {
// // success: true,
// // txid: 'fakeTxid',
// // }
// } catch (err) {
// console.error('Error in burnPsf(): ', err)
// throw err
// }
// }
}
module.exports = WalletAdapter
+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
}
+1 -1
View File
@@ -32,7 +32,7 @@ class Controllers {
// Attach the REST controllers to the Koa app.
// this.attachRESTControllers(app)
this.attachRPCControllers()
// this.attachRPCControllers()
}
// 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,
+2 -1
View File
@@ -49,7 +49,8 @@ class OfferRESTControllerLib {
// 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) {
@@ -49,6 +49,18 @@ class OrderRESTControllerLib {
}
}
// curl -X GET http://localhost:5700/order/list
async listOrders (ctx) {
try {
const orders = await _this.useCases.order.listOrders()
ctx.body = orders
} catch (err) {
console.log('Error in listOrders REST API handler.')
_this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', err.message)
+1
View File
@@ -50,6 +50,7 @@ class OrderRouter {
// Define the routes and attach the controller.
this.router.post('/', _this.orderRESTController.createOrder)
this.router.get('/list', _this.orderRESTController.listOrders)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
+36 -27
View File
@@ -1,6 +1,11 @@
const { wlogger } = require('../adapters/wlogger')
/*
Offer use-case library.
*/
// Local libraries
const { wlogger } = require('../adapters/wlogger')
const OfferEntity = require('../entities/offer')
const config = require('../../config')
class OfferLib {
constructor (localConfig = {}) {
@@ -16,6 +21,7 @@ class OfferLib {
this.offerEntity = new OfferEntity()
this.OfferModel = this.adapters.localdb.Offer
this.bch = this.adapters.bch
this.config = config
}
// Create a new offer model and add it to the Mongo database.
@@ -25,7 +31,7 @@ class OfferLib {
// Input Validation
const offerEntity = this.offerEntity.validate(entryObj)
// console.log('offerEntity: ', offerEntity)
console.log('offerEntity: ', offerEntity)
// Ensure sufficient tokens exist to create the offer.
await this.ensureFunds(offerEntity)
@@ -41,30 +47,23 @@ class OfferLib {
// 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
}
offerEntity.hdIndex = utxoInfo.hdIndex
// Add offer to P2WDB.
const p2wdbObj = {
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
data: offerEntity,
appId: this.config.p2wdbAppId
}
const hash = await this.adapters.p2wdb.write(p2wdbObj)
// console.log('hash: ', hash)
// Create a MongoDB model to hold the Offer
offerEntity.p2wdbHash = hash
console.log(`creating new offer model: ${JSON.stringify(offerEntity, null, 2)}`)
const offer = new this.OfferModel(offerEntity)
await offer.save()
return hash
} catch (err) {
// console.log("Error in use-cases/entry.js/createEntry()", err.message)
@@ -91,7 +90,8 @@ class OfferLib {
const utxoInfo = {
txid,
vout: 0
vout: 0,
hdIndex: keyPair.hdIndex
}
return utxoInfo
@@ -106,35 +106,44 @@ class OfferLib {
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)}`)
// 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)}`)
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
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)
tokenUtxos.map(x => (totalTokenBalance += parseFloat(x.qtyStr)))
console.log('totalTokenBalance: ', totalTokenBalance)
// If there are fewer tokens in the wallet than what's in the offer,
// throw an error.
if (totalTokenBalance <= offerEntity.numTokens) {
if (totalTokenBalance <= offerEntity.numTokens || isNaN(totalTokenBalance)) {
throw new Error(
'App wallet does not have enough tokens to satisfy the SELL offer.'
)
}
//
} else {
// Buy Offer
throw new Error('Buy orders are not supported yet.')
}
return true
+9
View File
@@ -56,6 +56,15 @@ class OrderUseCases {
throw err
}
}
async listOrders () {
try {
return this.OrderModel.find({})
} catch (error) {
console.error('Error in use-cases/order/listOrders()')
throw error
}
}
}
module.exports = OrderUseCases
+3 -3
View File
@@ -15,11 +15,11 @@ async function start () {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 10,
numTokens: 0.02
minSatsToExchange: 1000,
numTokens: 1
}
const options = {
@@ -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,41 @@
/*
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')
})
})
})
@@ -0,0 +1,65 @@
/*
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 throw error if wallet does not have enough funds', async () => {
try {
const offerEntity = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'token-id',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 1250,
numTokens: 1
}
await uut.ensureFunds(offerEntity)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'App wallet does not have enough tokens to satisfy the SELL offer.')
}
})
it('should return true if wallet has enough funds', async () => {
const offerEntity = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 1250,
numTokens: 1
}
const result = await uut.ensureFunds(offerEntity)
assert.equal(result, true)
})
})
})
@@ -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')
})
})
})
+44 -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 () => {
@@ -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')
}
})
@@ -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')
}
})
+22 -5
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,8 +119,10 @@ 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 }
},
@@ -115,7 +130,9 @@ const wallet = {
}
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}
+60 -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,8 @@ class MockBchWallet {
this.sendTokens = async () => {
return 'fakeTxid'
}
this.getUtxos = async () => {}
this.getUtxos = async () => {
}
// Environment variable is used by wallet-balance.unit.js to force an error.
if (process.env.NO_UTXO) {
@@ -41,8 +40,7 @@ class MockBchWallet {
bchUtxos: [
{
height: 700685,
tx_hash:
'1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
tx_pos: 0,
value: 1000,
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
@@ -51,8 +49,7 @@ class MockBchWallet {
},
{
height: 700685,
tx_hash:
'1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
tx_pos: 2,
value: 19406,
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
@@ -66,46 +63,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 +133,4 @@ class MockBchWallet {
}
}
module.exports = { MockBchWallet, mockWallet }
module.exports = { MockBchWallet, mockWallet}
+48 -50
View File
@@ -46,60 +46,13 @@ 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
}
// 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')
const result = await uut.createOffer(entryObj)
console.log('result: ', result)
assert.isString(result)
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox
.stub(uut.offerEntity, 'validate')
.throws(new Error('test error'))
await uut.createOffer()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
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',
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 0,
@@ -123,8 +76,7 @@ describe('#offer-use-case', () => {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId:
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 0,
@@ -141,4 +93,50 @@ describe('#offer-use-case', () => {
assert.equal(result.vout, 0)
})
})
describe('#createOffer', () => {
it('should create an offer and return the hash', async () => {
const entryObj = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'token-id',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 1250,
numTokens: 1
}
// Mock dependencies
// sandbox.stub(uut.adapters.wallet, 'burnPsf').resolves('fakeTxid')
sandbox.stub(uut.offerEntity, 'validate').returns(entryObj)
sandbox.stub(uut, 'ensureFunds').resolves()
sandbox.stub(uut, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves()
// sandbox
// .stub(uut.adapters.wallet, 'generateSignature')
// .resolves('fakeSignature')
sandbox.stub(uut.adapters.p2wdb, 'write').resolves('fakeHash')
const result = await uut.createOffer(entryObj)
console.log('result: ', result)
assert.isString(result)
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox
.stub(uut.offerEntity, 'validate')
.throws(new Error('test error'))
await uut.createOffer()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
})
+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')
}
})