mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-22 01:02:00 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94eb342435 | ||
|
|
ac326b8e34 | ||
|
|
164a38ad20 | ||
|
|
3300e0fc80 | ||
|
|
3a4569c33f | ||
|
|
05a0620663 | ||
|
|
af83c06b43 | ||
|
|
e6317c3282 | ||
|
|
ddce40bb52 | ||
|
|
746f9a3488 | ||
|
|
a611580f48 | ||
|
|
07c671ede4 | ||
|
|
7c8d73810d | ||
|
|
9829e8581c | ||
|
|
a12aa5609d | ||
|
|
abf834c5a8 | ||
|
|
7ae557d143 | ||
|
|
5f4b0c8da3 | ||
|
|
6ef197dae3 | ||
|
|
fafa7eecd9 | ||
|
|
5ede934be5 | ||
|
|
8d036ea9ba | ||
|
|
bc6acc846a | ||
|
|
afa050261f | ||
|
|
1715d064fb | ||
|
|
cf2f2af90f | ||
|
|
354321c956 | ||
|
|
af2572a493 | ||
|
|
1a5dadbf35 | ||
|
|
ced7a60f14 | ||
|
|
043c4a8b2b | ||
|
|
f9b4bf06ab | ||
|
|
8048182f46 | ||
|
|
cb4d1d3eba | ||
|
|
3209f96e7f | ||
|
|
c65d662630 | ||
|
|
1b99f8afa1 | ||
|
|
c6f0a1317c | ||
|
|
fad5f8e23d | ||
|
|
ec6e0f5e46 | ||
|
|
f1caef2c0d | ||
|
|
55bf9424f9 | ||
|
|
aa100c97ed | ||
|
|
9fa5eafe95 | ||
|
|
4d9e948e30 | ||
|
|
e5cdf28e34 | ||
|
|
1d82b36105 | ||
|
|
b910cd1e6d | ||
|
|
536861ba99 | ||
|
|
ef030e3dc6 | ||
|
|
3da46c3d6f | ||
|
|
82f5552836 | ||
|
|
4d25681c51 | ||
|
|
98a1631498 | ||
|
|
8dce8cbf8d | ||
|
|
0b3049138b | ||
|
|
d7136583b8 | ||
|
|
19b13e5d6e | ||
|
|
c92ffac645 | ||
|
|
216f0b6910 | ||
|
|
c31bf96986 | ||
|
|
7f8ef994a7 | ||
|
|
72ba1c48c2 | ||
|
|
c721da1562 | ||
|
|
3606a2d31b | ||
|
|
764905b041 | ||
|
|
556c6ec522 | ||
|
|
2a78145170 | ||
|
|
04395b70a6 | ||
|
|
d15fee7c7a | ||
|
|
9437b8fb80 | ||
|
|
b611663379 | ||
|
|
03b7ed980a | ||
|
|
bf74d50bb0 | ||
|
|
003480bcb5 | ||
|
|
9dbd18d0d6 | ||
|
|
b3ba7f03e4 | ||
|
|
0a8bb52241 | ||
|
|
064ac36dc7 | ||
|
|
5e2cf86f4b | ||
|
|
425d2ada65 |
@@ -1,14 +1,14 @@
|
||||
# bch-dex
|
||||
|
||||
[](http://standardjs.com) [](https://github.com/semantic-release/semantic-release) [](https://gitter.im/Permissionless-Software-Foundation/psf-dex-dev?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://standardjs.com) [](https://github.com/semantic-release/semantic-release)
|
||||
|
||||
`bch-dex` is a decentralized exchange for permissionless trading of [SLP tokens](https://github.com/simpleledger/slp-specifications/blob/master/slp-token-type-1.md) and [NFTs](https://github.com/simpleledger/slp-specifications/blob/master/slp-nft-1.md). Trading fees are incredibly small, compared to other DEX platforms. The software is based entirely on open protocols and open source software. It's not a '.com' website that you visit, but software you run on your own computer, in the confort of your own home. So long as you have an internet connection and the technical knowledge to run this software, no one can stop you from trading tokens. `bch-dex` software is maintained by the [Permissionless Software Foundation](https://psfoundation.cash/).
|
||||
`bch-dex` is a decentralized exchange *protocol* for permissionless trading of [SLP tokens](https://github.com/simpleledger/slp-specifications/blob/master/slp-token-type-1.md) and [NFTs](https://github.com/simpleledger/slp-specifications/blob/master/slp-nft-1.md). Trading fees are incredibly small, compared to other DEX platforms. The software is based entirely on open protocols and open source software. `bch-dex` software is maintained by the [Permissionless Software Foundation](https://psfoundation.info).
|
||||
|
||||
- [High-level Overview of bch-dex](https://youtu.be/LVX8CLi4sHw) (Video)
|
||||
|
||||
This is a prototype web service that monitors the [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) for trading signals, to trade BCH and SLP tokens. It's inspired by the [SWaP Protocol](https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md).
|
||||
This is a prototype web service that monitors one or more [Nostr relays](https://nostr.com) for trading signals, to trade BCH and SLP tokens. It's inspired by the [SWaP Protocol](https://github.com/vinarmani/swap-protocol/blob/master/swap-protocol-spec.md).
|
||||
|
||||
This repository contains the back end code. The user interface is contained in the [bch-dex-ui](https://github.com/Permissionless-Software-Foundation/bch-dex-ui) repository.
|
||||
This repository contains the back end code. The user interface is contained in the [bch-dex-ui-v3](https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v3) repository.
|
||||
|
||||
**Warning**: This repository is under active development. Things will be constantly changing and breaking.
|
||||
|
||||
@@ -19,24 +19,17 @@ This repository contains the back end code. The user interface is contained in t
|
||||
|
||||
## Participate
|
||||
This is an open source project, and we encourage other JavaScript developers to participate in its creation and maintenance. We have two chat rooms for the community:
|
||||
- [Telegram Channel](https://t.me/psf_dex_dev)
|
||||
- [Gitter Channel](https://gitter.im/Permissionless-Software-Foundation/psf-dex-dev)
|
||||
- [Telegram Channel](https://t.me/bch_js_toolkit)
|
||||
|
||||
## Installation
|
||||
Running the DEX requires composition of these different software packages:
|
||||
- bch-dex - This repository is the back end software that tracks trade data on the network, generates [Offers and Counter Offers](https://github.com/Permissionless-Software-Foundation/bch-dex/tree/ct-unstable/dev-docs#definitions), and finalizes trades by accepting Counter Offers.
|
||||
- [bch-dex-ui](https://github.com/Permissionless-Software-Foundation/bch-dex-ui) is a [Gatsby](https://www.gatsbyjs.com/) web app and user interface (UI) for bch-dex.
|
||||
- [P2WDB](https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service) is a censorship-resistant database used to communicate trade data between peers running bch-dex.
|
||||
- [IPFS](https://ipfs.io/) is a censorship-resistant network for communicating data over the internet.
|
||||
- [MongoDB](https://www.mongodb.com/) is a database used by both P2WDB and bch-dex to store and manage local data.
|
||||
- [bch-dex-ui-v3](https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v3) web app and user interface (UI) for bch-dex focused on Sellers of tokens.
|
||||
- [MongoDB](https://www.mongodb.com/) is a database used by bch-dex to store and manage local data, user accounts, etc.
|
||||
|
||||
The above software is orchestrated using [Docker](https://www.docker.com/) and Docker Compose. The target operating system is Ubuntu 20+, and the target hardware is amd64 (normal desktop PCs) and the arm64 (Raspberry Pi 4). Trying to operate this software on other operating systems or hardware is possible, but not supported.
|
||||
The above software is orchestrated using [Docker](https://www.docker.com/) and Docker Compose. The target operating system is Ubuntu 20+, and the target hardware is amd64 (normal desktop PCs). Trying to operate this software on other operating systems or hardware is possible, but not supported.
|
||||
|
||||
The Raspberry Pi 4 is the primary hardware target, because this software functions best when it is always running. The Pi 4 makes a great appliance for monitoring trade signals and finalizing trades.
|
||||
|
||||
Instructions for setting up Node.js, Docker, and Docker Compose can be found in [this Gist](https://gist.github.com/christroutner/a39f656850dc022b60f25c9663dd1cdd). Walk-through videos can also be found on the [PSF Videos page](https://psfoundation.cash/video/).
|
||||
|
||||
Here are the steps involved in setting up bch-dex:
|
||||
Setup instructions:
|
||||
|
||||
1. Follow the direction in [this Gist](https://gist.github.com/christroutner/a39f656850dc022b60f25c9663dd1cdd) to install Node.js, Docker, and Docker Compose.
|
||||
1. Clone the repository with `git clone https://github.com/Permissionless-Software-Foundation/bch-dex` and enter it with `cd bch-dex`.
|
||||
@@ -44,32 +37,17 @@ Here are the steps involved in setting up bch-dex:
|
||||
1. Create a wallet:
|
||||
- `cd production/scripts`
|
||||
- `node create-wallet.js`
|
||||
1. Change directory to the `production/docker` or `production/rpi-docker` folder depending on your hardware target.
|
||||
1. Change directory to the `production/docker` folder.
|
||||
1. Pull the Docker images down from Docker Hub: `docker-compose pull`
|
||||
1. Build the core software: `docker-compose build --no-cache`
|
||||
1. Start the Docker containers with `docker-compose up -d`
|
||||
1. Wait for the P2WDB to sync and populate bch-dex with trade data. You can monitor it with `docker logs --tail 20 -f p2wdb`.
|
||||
1. Open a web browser and navigate the `http://localhost:4500`. You'll be able to see new Offers as they come in and are detected by bch-dex.
|
||||
1. To take the other side of the trade, click the `Take` button in the UI.
|
||||
1. You can add the 12-word mnemonic from the `wallet.json` file to the the web wallet, which will mirror your wallet in the UI, and allow you to perform basic wallet functions (send and receive BCH and tokens).
|
||||
|
||||
## Blockchain Infrastructure
|
||||
|
||||
The P2WDB and bch-dex containers requires a 'Global Back End' (as described in [CashStack.info](https://cashstack.info)) in order to connect to the blockchain. By default, the Docker containers connect to [free-bch.fullstack.cash](https://free-bch.fullstack.cash/). Several community-provided Global Back End servers are provided and can be [viewed here](https://gist.github.com/christroutner/63c5513782181f8b8ea3eb89f7cadeb6). The back end can be changed by setting the `CONSUMER_URL` environment variable in the `docker-compose.yml` file.
|
||||
|
||||
### Applying Software Updates
|
||||
As this is an active project, software updates will happen frequently. To apply a software update, perform these steps.
|
||||
|
||||
|
||||
1. Enter the `docker` or `rpi-docker` folder, depending on your hardware target.
|
||||
1. Bring down the Docker containers with `docker-compose down`.
|
||||
1. Pull in the software updates with `git pull`
|
||||
1. Update dependencies with `npm install`
|
||||
1. Pull new Docker images with `docker-compose pull`
|
||||
1. Clean up disk space by deleting old Docker images with `./cleanup-images.sh`
|
||||
1. Start the Docker containers with `docker-compose up -d`
|
||||
|
||||
|
||||
Sometimes it may be necessary to delete the databases before applying a software update. This can be done by stopping the Docker containers and deleting the `production/data` directory. When the Docker containers are restarted, they will recreate that directory. The P2WDB will re-sync and bch-dex will be populated with fresh trade data.
|
||||
bch-dex requires a [Cash Stack](https://cashstack.info) back end in order to connect to the blockchain. By default, the Docker containers connect to [free-bch.fullstack.cash](https://free-bch.fullstack.cash/). Several community-provided servers are provided and can be [viewed here](https://consumers.psfoundation.info/consumers.json). The back end can be changed by setting the `CONSUMER_URL` environment variable in the `docker-compose.yml` file.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+5
-1
@@ -25,6 +25,7 @@ import config from '../config/index.js' // this first.
|
||||
|
||||
import AdminLib from '../src/adapters/admin.js'
|
||||
import errorMiddleware from '../src/controllers/rest-api/middleware/error.js'
|
||||
import { usageMiddleware } from '../src/use-cases/usage-use-cases.js'
|
||||
// import wlogger from '../src/adapters/wlogger.js'
|
||||
import Controllers from '../src/controllers/index.js'
|
||||
import { applyPassportMods } from '../config/passport.js'
|
||||
@@ -70,6 +71,7 @@ class Server {
|
||||
app.use(bodyParser())
|
||||
app.use(session())
|
||||
app.use(errorMiddleware())
|
||||
app.use(usageMiddleware())
|
||||
|
||||
// Used to generate the docs.
|
||||
app.use(mount('/', serve(`${process.cwd()}/docs`)))
|
||||
@@ -142,7 +144,9 @@ class Server {
|
||||
console.log('\nConfiguration:')
|
||||
console.log(`Circuit Relay: ${this.config.isCircuitRelay}`)
|
||||
console.log(`IPFS TCP port: ${this.config.ipfsTcpPort}`)
|
||||
console.log(`IPFS WS port: ${this.config.ipfsWsPort}\n`)
|
||||
console.log(`IPFS WS port: ${this.config.ipfsWsPort}`)
|
||||
console.log(`IPFS WebRTC port: ${this.config.ipfsWebRtcPort}`)
|
||||
console.log(`Connection preference: ${this.config.connectPref}\n`)
|
||||
|
||||
return app
|
||||
} catch (err) {
|
||||
|
||||
Vendored
+10
-2
@@ -155,6 +155,8 @@ export default {
|
||||
// IPFS Ports
|
||||
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 4001,
|
||||
ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 4003,
|
||||
ipfsWebRtcPort: process.env.IPFS_WEB_RTC_PORT ? process.env.IPFS_WEB_RTC_PORT : 4005,
|
||||
connectPref: process.env.CONNECT_PREF, // Used in helia-coord to select connection preference.
|
||||
|
||||
// Settings for production, using external go-ipfs node.
|
||||
isProduction: process.env.SVC_ENV === 'prod' ? true : false,
|
||||
@@ -166,7 +168,7 @@ export default {
|
||||
chatPubSubChan: 'psf-ipfs-chat-001',
|
||||
|
||||
// IPFS gateway
|
||||
ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://p2wdb-gateway-678.fullstack.cash/ipfs/',
|
||||
ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://pin.fullstack.cash/ipfs/download/',
|
||||
|
||||
// This can add specific Circuit Relay v2 servers to connect to.
|
||||
bootstrapRelays: [
|
||||
@@ -181,5 +183,11 @@ export default {
|
||||
|
||||
// Nostr
|
||||
nostrRelay: process.env.NOSTR_RELAY ? process.env.NOSTR_RELAY : 'wss://nostr-relay.psfoundation.info',
|
||||
nostrTopic: process.env.NOSTR_TOPIC ? process.env.NOSTR_TOPIC : 'bch-dex-test-topic-01'
|
||||
nostrTopic: process.env.NOSTR_TOPIC ? process.env.NOSTR_TOPIC : 'bch-dex-test-topic-01',
|
||||
|
||||
// Account Configuration
|
||||
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
|
||||
|
||||
// Admin password
|
||||
adminPassword: process.env.ADMIN_PASSWORD
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import Server from './bin/server.js'
|
||||
const server = new Server()
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.log(`Handling ${reason.code} error. stack: `, reason)
|
||||
})
|
||||
|
||||
server.startServer()
|
||||
|
||||
Generated
+1838
-4126
File diff suppressed because it is too large
Load Diff
+24
-22
@@ -28,32 +28,33 @@
|
||||
},
|
||||
"repository": "Permissionless-Software-Foundation/bch-dex",
|
||||
"dependencies": {
|
||||
"@chainsafe/libp2p-gossipsub": "13.1.1",
|
||||
"@chainsafe/libp2p-noise": "15.1.1",
|
||||
"@chainsafe/libp2p-yamux": "6.0.2",
|
||||
"@chainsafe/libp2p-gossipsub": "14.1.0",
|
||||
"@chainsafe/libp2p-noise": "16.0.1",
|
||||
"@chainsafe/libp2p-yamux": "7.0.1",
|
||||
"@chris.troutner/retry-queue": "1.0.8",
|
||||
"@helia/unixfs": "3.0.7",
|
||||
"@libp2p/bootstrap": "10.1.5",
|
||||
"@libp2p/circuit-relay-v2": "1.1.5",
|
||||
"@libp2p/identify": "2.1.5",
|
||||
"@libp2p/keychain": "4.1.5",
|
||||
"@libp2p/logger": "4.0.20",
|
||||
"@libp2p/tcp": "9.1.5",
|
||||
"@libp2p/webrtc": "4.1.7",
|
||||
"@libp2p/websockets": "8.2.0",
|
||||
"@multiformats/multiaddr": "12.3.0",
|
||||
"@psf/bch-js": "6.7.3",
|
||||
"@helia/unixfs": "4.0.2",
|
||||
"@libp2p/bootstrap": "11.0.26",
|
||||
"@libp2p/circuit-relay-v2": "3.2.2",
|
||||
"@libp2p/config": "1.1.0",
|
||||
"@libp2p/identify": "3.0.22",
|
||||
"@libp2p/keychain": "5.0.14",
|
||||
"@libp2p/logger": "5.1.8",
|
||||
"@libp2p/ping": "2.0.22",
|
||||
"@libp2p/tcp": "10.1.2",
|
||||
"@libp2p/webrtc": "5.2.2",
|
||||
"@libp2p/websockets": "9.2.2",
|
||||
"@multiformats/multiaddr": "12.3.5",
|
||||
"axios": "0.27.2",
|
||||
"bch-message-lib": "2.2.1",
|
||||
"bch-nostr": "1.3.1",
|
||||
"bch-nostr": "1.3.3",
|
||||
"bch-token-sweep": "2.2.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bitcoincashjs-lib": "3.3.3",
|
||||
"blockstore-fs": "2.0.0",
|
||||
"datastore-fs": "9.1.7",
|
||||
"bitcoincashjs-lib": "3.3.3",
|
||||
"blockstore-fs": "2.0.2",
|
||||
"datastore-fs": "10.0.2",
|
||||
"glob": "7.1.6",
|
||||
"helia": "4.2.5",
|
||||
"helia-coord": "1.5.15",
|
||||
"helia": "5.2.1",
|
||||
"helia-coord": "1.7.2",
|
||||
"ipfs-coord-esm": "9.1.7",
|
||||
"ipfs-http-client": "58.0.0",
|
||||
"jsonrpc-lite": "2.2.0",
|
||||
@@ -70,12 +71,13 @@
|
||||
"koa-router": "10.0.0",
|
||||
"koa-static": "5.0.0",
|
||||
"koa2-ratelimit": "0.9.1",
|
||||
"libp2p": "1.9.1",
|
||||
"libp2p": "2.7.2",
|
||||
"line-reader": "0.4.0",
|
||||
"minimal-slp-wallet": "5.12.0",
|
||||
"minimal-slp-wallet": "5.13.2",
|
||||
"mongoose": "5.13.14",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
"nodemailer": "6.7.5",
|
||||
"nostr-tools": "2.10.4",
|
||||
"p2wdb-esm": "2.2.9",
|
||||
"passport-local": "1.0.0",
|
||||
"public-ip": "6.0.1",
|
||||
|
||||
@@ -1 +1 @@
|
||||
COMPOSE_PROJECT_NAME=ipfs-service-provider
|
||||
COMPOSE_PROJECT_NAME=bch-dex
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN apt-get update
|
||||
RUN apt-get install -y sudo git curl nano gnupg wget
|
||||
|
||||
#Install Node and NPM
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
|
||||
RUN bash nodesource_setup.sh
|
||||
RUN apt-get install -y nodejs build-essential
|
||||
|
||||
@@ -34,7 +34,7 @@ WORKDIR /home/safeuser
|
||||
|
||||
# Update to the latest version of npm.
|
||||
#RUN npm install -g npm@7.23.0
|
||||
RUN npm install -g npm
|
||||
#RUN npm install -g npm
|
||||
|
||||
# npm mirror to prevent direct dependency on npm.
|
||||
#RUN npm set registry http://94.130.170.209:4873/
|
||||
@@ -46,11 +46,11 @@ RUN npm install -g npm
|
||||
|
||||
# Clone the rest.bitcoin.com repository
|
||||
WORKDIR /home/safeuser
|
||||
RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v2
|
||||
RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex-ui-v3
|
||||
|
||||
# Switch to the desired branch. `master` is usually stable,
|
||||
# and `stage` has the most up-to-date changes.
|
||||
WORKDIR /home/safeuser/bch-dex-ui-v2
|
||||
WORKDIR /home/safeuser/bch-dex-ui-v3
|
||||
|
||||
# For development: switch to unstable branch
|
||||
RUN git checkout ct-unstable
|
||||
@@ -61,13 +61,13 @@ RUN git checkout ct-unstable
|
||||
RUN npm install
|
||||
|
||||
# Build the site
|
||||
RUN CI=true npm run build
|
||||
RUN npm run build
|
||||
|
||||
# Load the NGINX image.
|
||||
FROM nginx
|
||||
EXPOSE 80
|
||||
|
||||
# Copy the files built in the first container to the new NGINX container.
|
||||
COPY --from=builder /home/safeuser/bch-dex-ui-v2/build /usr/share/nginx/html
|
||||
COPY --from=builder /home/safeuser/bch-dex-ui-v3/build /usr/share/nginx/html
|
||||
|
||||
#USER safeuser
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN apt-get update
|
||||
RUN apt-get install -y sudo git curl nano gnupg wget
|
||||
|
||||
#Install Node and NPM
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
|
||||
RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
|
||||
RUN bash nodesource_setup.sh
|
||||
RUN apt-get install -y nodejs build-essential
|
||||
|
||||
@@ -34,7 +34,7 @@ WORKDIR /home/safeuser
|
||||
|
||||
# Update to the latest version of npm.
|
||||
#RUN npm install -g npm@7.23.0
|
||||
RUN npm install -g npm
|
||||
#RUN npm install -g npm
|
||||
|
||||
# npm mirror to prevent direct dependency on npm.
|
||||
RUN npm set registry http://94.130.170.209:4873/
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
mongo-p2wdb-service:
|
||||
mongo-dex:
|
||||
image: mongo:4.2.0
|
||||
container_name: mongo-p2wdb-service
|
||||
container_name: mongo-dex
|
||||
ports:
|
||||
- '5666:27017' # <host port>:<container port>
|
||||
volumes:
|
||||
@@ -13,69 +13,11 @@ services:
|
||||
command: mongod --logpath=/dev/null # -- quiet
|
||||
restart: always
|
||||
|
||||
ipfs:
|
||||
# https://github.com/christroutner/trickle-ipfs
|
||||
image: christroutner/trickle-ipfs:v1.0.1
|
||||
#build:
|
||||
# context: ./
|
||||
# dockerfile: Dockerfile
|
||||
container_name: ipfs
|
||||
environment:
|
||||
IPFS_DAEMON_ARGUMENTS: '--enable-pubsub-experiment --migrate=true --agent-version-suffix=docker --routing=dhtclient'
|
||||
UPLOAD_KBPS: '1000'
|
||||
DOWNLOAD_KBPS: '1000'
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '10'
|
||||
mem_limit: 2000mb
|
||||
ports:
|
||||
- 4001:4001
|
||||
- 4003:4003
|
||||
- 172.17.0.1:5001:5001
|
||||
- 172.17.0.1:8080:8080
|
||||
command: [
|
||||
'./start-ipfs.sh'
|
||||
]
|
||||
volumes:
|
||||
- ../data/go-ipfs/data:/root/.ipfs
|
||||
restart: always
|
||||
|
||||
p2wdb:
|
||||
#build:
|
||||
# context: ./p2wdb/
|
||||
# dockerfile: Dockerfile
|
||||
image: christroutner/p2wdb:v4.1.3
|
||||
container_name: p2wdb
|
||||
environment:
|
||||
CONSUMER_URL: 'https://free-bch.fullstack.cash'
|
||||
#ENABLE_BCH_PAYMENT: 1
|
||||
DEBUG_LEVEL: 1
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '10'
|
||||
mem_limit: 1gb
|
||||
links:
|
||||
- mongo-p2wdb-service
|
||||
ports:
|
||||
# <host port>:<container port>
|
||||
- 5667:5667 # REST API
|
||||
#- 5668:5668 # IPFS TCP port
|
||||
#- 5669:5669 # IPFS WS Port
|
||||
volumes:
|
||||
- ../data/ipfsdata/p2wdb:/home/safeuser/ipfs-p2wdb-service/.ipfsdata/p2wdb
|
||||
- ./p2wdb/start-production.sh:/home/safeuser/ipfs-p2wdb-service/start-production.sh
|
||||
#- ../scripts/wallet.json:/home/safeuser/ipfs-p2wdb-service/wallet.json
|
||||
restart: always
|
||||
|
||||
bch-dex:
|
||||
#build:
|
||||
# context: ./bch-dex/
|
||||
# dockerfile: Dockerfile
|
||||
image: christroutner/bch-dex:v2.2.4
|
||||
build:
|
||||
context: ./bch-dex/
|
||||
dockerfile: Dockerfile
|
||||
#image: christroutner/bch-dex:v2.2.4
|
||||
container_name: bch-dex
|
||||
environment:
|
||||
CONSUMER_URL: 'https://free-bch.fullstack.cash'
|
||||
@@ -86,23 +28,28 @@ services:
|
||||
max-file: '10'
|
||||
mem_limit: 1000mb
|
||||
links:
|
||||
- mongo-p2wdb-service
|
||||
- p2wdb
|
||||
- mongo-dex
|
||||
#- p2wdb
|
||||
ports:
|
||||
- '5700:5700' # <host port>:<container port>
|
||||
# <host port>:<container port>
|
||||
- '5700:5700' # REST API
|
||||
- '4001:4001' # TCP
|
||||
- '4003:4003' # Websockets
|
||||
- '4005:4005' # WebRTC
|
||||
|
||||
volumes:
|
||||
- ../scripts/wallet.json:/home/safeuser/bch-dex/wallet.json
|
||||
- ./bch-dex/start-production.sh:/home/safeuser/bch-dex/start-production.sh
|
||||
restart: always
|
||||
|
||||
dex-ui:
|
||||
#build:
|
||||
# context: ./bch-dex-ui/
|
||||
# dockerfile: Dockerfile
|
||||
image: christroutner/bch-dex-ui:v1.2.4
|
||||
build:
|
||||
context: ./bch-dex-ui/
|
||||
dockerfile: Dockerfile
|
||||
#image: christroutner/bch-dex-ui:v1.2.4
|
||||
container_name: dex-ui
|
||||
environment:
|
||||
SERVER: 'http://192.168.2.3'
|
||||
SERVER: 'http://172.17.0.1'
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# Create a Docker container for the P2WDB
|
||||
#
|
||||
|
||||
#IMAGE BUILD COMMANDS
|
||||
FROM ubuntu:20.04
|
||||
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
||||
|
||||
#Update the OS and install any OS packages needed.
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y sudo git curl nano gnupg wget
|
||||
|
||||
#Install Node and NPM
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
|
||||
RUN bash nodesource_setup.sh
|
||||
RUN apt-get install -y nodejs build-essential
|
||||
|
||||
#Create the user 'safeuser' and add them to the sudo group.
|
||||
RUN useradd -ms /bin/bash safeuser
|
||||
RUN adduser safeuser sudo
|
||||
|
||||
#Set password to 'abcd8765' change value below if you want a different password
|
||||
RUN echo safeuser:abcd8765 | chpasswd
|
||||
|
||||
#Set the working directory to be the users home directory
|
||||
WORKDIR /home/safeuser
|
||||
|
||||
#Setup NPM for non-root global install (like on a mac)
|
||||
RUN mkdir /home/safeuser/.npm-global
|
||||
RUN chown -R safeuser .npm-global
|
||||
RUN echo "export PATH=~/.npm-global/bin:$PATH" >> /home/safeuser/.profile
|
||||
RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
|
||||
|
||||
# Update to the latest version of npm.
|
||||
#RUN npm install -g npm@7.23.0
|
||||
RUN npm install -g npm
|
||||
|
||||
# npm mirror to prevent direct dependency on npm.
|
||||
RUN npm set registry http://94.130.170.209:4873/
|
||||
|
||||
# Switch to user account.
|
||||
#USER safeuser
|
||||
# Prep 'sudo' commands.
|
||||
#RUN echo 'abcd8765' | sudo -S pwd
|
||||
|
||||
# Clone the rest.bitcoin.com repository
|
||||
RUN git clone https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service
|
||||
WORKDIR /home/safeuser/ipfs-p2wdb-service
|
||||
RUN mkdir .ipfsdata
|
||||
RUN git checkout ct-unstable
|
||||
|
||||
#RUN npm install fsevents
|
||||
|
||||
# Install dependencies
|
||||
#RUN mkdir .ipfsdata
|
||||
RUN npm install
|
||||
|
||||
# Generate the API docs
|
||||
RUN npm run docs
|
||||
|
||||
# Expose the port the API will be served on.
|
||||
#EXPOSE 5010
|
||||
|
||||
# Start the application.
|
||||
#COPY start-production.sh start-production.sh
|
||||
CMD ["./start-production.sh"]
|
||||
|
||||
#CMD ["npm", "start"]
|
||||
|
||||
# Used for debugging
|
||||
#COPY dummyapp.js dummyapp.js
|
||||
#CMD ["node", "dummyapp.js"]
|
||||
@@ -1,4 +0,0 @@
|
||||
setInterval(function () {
|
||||
const now = new Date()
|
||||
console.log(`ping ${now.toLocaleString()}`)
|
||||
}, 10000)
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# BEGIN: Optional configuration settings
|
||||
|
||||
# This mnemonic is used to set up persistent public key for e2ee
|
||||
# Replace this with your own 12-word mnemonic.
|
||||
export MNEMONIC="mass response fiscal world message exact series swallow forward confirm canoe festival"
|
||||
|
||||
# The human readable name this IPFS node identifies as.
|
||||
export COORD_NAME=generic-p2wdb-production
|
||||
|
||||
# Allow this node to function as a circuit relay. It must not be behind a firewall.
|
||||
#export ENABLE_CIRCUIT_RELAY=true
|
||||
# For browsers to use your circuit realy, you must set up a domain, SSL certificate,
|
||||
# and you must forward that subdomain to the IPFS_WS_PORT.
|
||||
#export CR_DOMAIN=subdomain.yourdomain.com
|
||||
|
||||
# Debug level. 0 = minimal info. 2 = max info.
|
||||
# This value set by docker-compose.yml
|
||||
#export DEBUG_LEVEL=1
|
||||
|
||||
# Log-in information for retrieving a JWT token from FullStack.cash.
|
||||
export FULLSTACKLOGIN=demo@demo.com
|
||||
export FULLSTACKPASS=demo
|
||||
|
||||
# END: Optional configuration settings
|
||||
|
||||
|
||||
# Production database connection string.
|
||||
export DBURL=mongodb://172.17.0.1:5666/p2wdb-service-dev
|
||||
|
||||
# Configure IPFS ports
|
||||
#export IPFS_TCP_PORT=5668
|
||||
#export IPFS_WS_PORT=5669
|
||||
|
||||
# Configure REST API port
|
||||
export PORT=5667
|
||||
|
||||
export P2W_ENV=production
|
||||
export IPFS_HOST=172.17.0.1
|
||||
export IPFS_API_PORT=5001
|
||||
export IPFS_TCP_PORT=4001
|
||||
|
||||
# P2WDB specific env vars
|
||||
#export ORBITDB_NAME=/orbitdb/zdpuAm4fPafpjs5mgbBUJV8Az7kd8Xj1ttD6M1JaAxF8RMRrw/psf-bch-p2wdb-keyvalue-v1.0.0-0004
|
||||
|
||||
npm start
|
||||
@@ -56,7 +56,7 @@ RUN git clone https://github.com/Permissionless-Software-Foundation/bch-dex
|
||||
WORKDIR /home/safeuser/bch-dex
|
||||
|
||||
# For development: switch to unstable branch
|
||||
RUN git checkout ct-unstable
|
||||
RUN git checkout master
|
||||
|
||||
# Install dependencies
|
||||
#RUN mkdir .ipfsdata
|
||||
@@ -66,14 +66,6 @@ RUN npm install
|
||||
# Generate the API docs
|
||||
RUN npm run docs
|
||||
|
||||
# Copy the wallet files
|
||||
#COPY wallet.json wallet.json
|
||||
|
||||
#VOLUME /home/safeuser/keys
|
||||
|
||||
# Expose the port the API will be served on.
|
||||
#EXPOSE 5010
|
||||
|
||||
# Start the application.
|
||||
#COPY start-production.sh start-production.sh
|
||||
CMD ["./start-production.sh"]
|
||||
|
||||
@@ -41,8 +41,14 @@ export IPFS_HOST=172.17.0.1
|
||||
export IPFS_API_PORT=5001
|
||||
export IPFS_TCP_PORT=4001
|
||||
export IPFS_WS_PORT=4003
|
||||
export IPFS_WEB_RTC_PORT=4005
|
||||
# Set the debug level for helia-coord. 0-3.
|
||||
# 0 = no debug logs. 3 = maximum debug logs.
|
||||
export DEBUG_LEVEL=0
|
||||
|
||||
# P2WDB specific env vars
|
||||
#export ORBITDB_NAME=/orbitdb/zdpuAqNiwLiJBfbRK7uihV2hAbNSXj78ufzv5VyQb8GuvRwDh/psf-bch-p2wdb-keyvalue-v3.0.0-0001
|
||||
# Use this if the IPFS node has a publically accessible IP address.
|
||||
#export CONNECT_PREF=direct
|
||||
# Use this if the IPFS node is behind a NAT or Firewall and cannot be accessed directly.
|
||||
export CONNECT_PREF=cr
|
||||
|
||||
npm start
|
||||
|
||||
+27
-58
@@ -51,70 +51,38 @@ class Admin {
|
||||
// used by the Listing Manager and test scripts, in order access private API
|
||||
// functions.
|
||||
async createSystemUser () {
|
||||
// Create the system user.
|
||||
try {
|
||||
context.password = _this._randomString(20)
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'system@system.com',
|
||||
password: context.password,
|
||||
name: 'admin'
|
||||
}
|
||||
}
|
||||
const context = {
|
||||
email: 'system@system.com',
|
||||
name: 'admin',
|
||||
password: _this.config.adminPassword || _this._randomString(20),
|
||||
type: 'admin',
|
||||
mnemonic: 'spread ivory oval pioneer flash gap merit nerve simple image entire trouble'
|
||||
}
|
||||
const result = await _this.axios.request(options)
|
||||
// console.log('admin.data: ', result.data)
|
||||
// Check if the user already exists
|
||||
let adminUser = await _this.User.findOne({ email: context.email })
|
||||
|
||||
context.email = result.data.user.email
|
||||
context.id = result.data.user._id
|
||||
context.token = result.data.token
|
||||
if (adminUser) {
|
||||
// Update the password
|
||||
adminUser.password = context.password
|
||||
} else {
|
||||
// Create a new admin user
|
||||
adminUser = new _this.User(context)
|
||||
}
|
||||
// Update context with the new user id and token
|
||||
context.id = adminUser._id
|
||||
context.token = await adminUser.generateToken()
|
||||
|
||||
// Get the mongoDB entry
|
||||
const user = await _this.User.findById(context.id)
|
||||
// Save the user
|
||||
await adminUser.save()
|
||||
|
||||
// Change the user type to admin
|
||||
user.type = 'admin'
|
||||
// console.log(`user created: ${JSON.stringify(user, null, 2)}`)
|
||||
|
||||
// Save the user model.
|
||||
await user.save()
|
||||
|
||||
// console.log(`admin user created: ${JSON.stringify(result.body, null, 2)}`)
|
||||
// console.log(`with password: ${context.password}`)
|
||||
|
||||
// Write out the system user information to a JSON file that external
|
||||
// applications like the Task Manager and the test scripts can access.
|
||||
|
||||
await jsonFiles.writeJSON(context, JSON_PATH)
|
||||
// console.log('context: ', context)
|
||||
// console.log('JSON_PATH: ', JSON_PATH)
|
||||
// Write the user data to the JSON file
|
||||
await _this.jsonFiles.writeJSON(context, JSON_PATH)
|
||||
|
||||
return context
|
||||
} catch (err) {
|
||||
// Handle existing system user.
|
||||
if (err.response.status === 422) {
|
||||
try {
|
||||
// Delete the existing user
|
||||
await _this.deleteExistingSystemUser()
|
||||
|
||||
// Call this function again.
|
||||
return _this.createSystemUser()
|
||||
} catch (err2) {
|
||||
console.error(
|
||||
'Error in admin.js/createSystemUser() while trying generate new system user.'
|
||||
)
|
||||
// process.end(1)
|
||||
throw err2
|
||||
}
|
||||
} else {
|
||||
console.log('Error in admin.js/createSystemUser: ')
|
||||
// process.end(1)
|
||||
throw err
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error in admin.js/createSystemUser()')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +97,7 @@ class Admin {
|
||||
})
|
||||
|
||||
await _this.User.deleteOne({ email: 'system@system.com' })
|
||||
return true
|
||||
} catch (err) {
|
||||
console.log('Error in admin.js/deleteExistingSystemUser()')
|
||||
throw err
|
||||
@@ -152,7 +121,7 @@ class Admin {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
data: {
|
||||
email: 'system@system.com',
|
||||
email: existingUser.email,
|
||||
password: existingUser.password
|
||||
}
|
||||
}
|
||||
|
||||
+81
-50
@@ -19,17 +19,20 @@ import { noise } from '@chainsafe/libp2p-noise'
|
||||
import { yamux } from '@chainsafe/libp2p-yamux'
|
||||
// import { bootstrap } from '@libp2p/bootstrap'
|
||||
// import { identifyService } from 'libp2p/identify'
|
||||
import { identify } from '@libp2p/identify'
|
||||
// import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
|
||||
import { circuitRelayServer } from '@libp2p/circuit-relay-v2'
|
||||
import { identify, identifyPush } from '@libp2p/identify'
|
||||
import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
|
||||
// import { circuitRelayServer } from '@libp2p/circuit-relay-v2'
|
||||
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
|
||||
import { webSockets } from '@libp2p/websockets'
|
||||
import { publicIpv4 } from 'public-ip'
|
||||
import { multiaddr } from '@multiformats/multiaddr'
|
||||
// import { webRTC } from '@libp2p/webrtc'
|
||||
import { webRTC, webRTCDirect } from '@libp2p/webrtc'
|
||||
import { keychain } from '@libp2p/keychain'
|
||||
import { defaultLogger } from '@libp2p/logger'
|
||||
import { unixfs } from '@helia/unixfs'
|
||||
// import { generateKeyPairFromSeed } from '@libp2p/crypto/keys'
|
||||
// import crypto from 'crypto'
|
||||
import { ping } from '@libp2p/ping'
|
||||
import { loadOrCreateSelfKey } from '@libp2p/config'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../../config/index.js'
|
||||
@@ -64,8 +67,8 @@ class IpfsAdapter {
|
||||
this.createNode = this.createNode.bind(this)
|
||||
this.stop = this.stop.bind(this)
|
||||
this.ensureBlocksDir = this.ensureBlocksDir.bind(this)
|
||||
// this.getPrivateKey = this.getPrivateKey.bind(this)
|
||||
this.getSeed = this.getSeed.bind(this)
|
||||
this.getKeychain = this.getKeychain.bind(this)
|
||||
}
|
||||
|
||||
// Start an IPFS node.
|
||||
@@ -111,19 +114,6 @@ class IpfsAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async getKeychain (datastore) {
|
||||
const keychainInit = {
|
||||
pass: await this.getSeed()
|
||||
}
|
||||
|
||||
const chain = this.keychain(keychainInit)({
|
||||
datastore,
|
||||
logger: defaultLogger()
|
||||
})
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
// This function creates an IPFS node using Helia.
|
||||
// It returns the node as an object.
|
||||
async createNode () {
|
||||
@@ -132,44 +122,38 @@ class IpfsAdapter {
|
||||
const blockstore = new FsBlockstore(`${IPFS_DIR}/blockstore`)
|
||||
const datastore = new FsDatastore(`${IPFS_DIR}/datastore`)
|
||||
|
||||
// const keychainInit = {
|
||||
// pass: await this.getSeed()
|
||||
// }
|
||||
// Get the private key.
|
||||
// const privateKey = await this.getPrivateKey()
|
||||
// console.log('privateKey: ', privateKey)
|
||||
|
||||
// Create an identity
|
||||
let peerId
|
||||
// console.log('this.keychain: ', this.keychain)
|
||||
// const chain = this.keychain(keychainInit)({
|
||||
// datastore,
|
||||
// logger: defaultLogger()
|
||||
// })
|
||||
const chain = await this.getKeychain(datastore)
|
||||
try {
|
||||
peerId = await chain.exportPeerId('myKey')
|
||||
} catch (err) {
|
||||
await chain.createKey('myKey', 'Ed25519', 4096)
|
||||
peerId = await chain.exportPeerId('myKey')
|
||||
const keychainInit = {
|
||||
selfKey: 'myKey',
|
||||
pass: await this.getSeed()
|
||||
}
|
||||
const privateKey = await loadOrCreateSelfKey(datastore, keychainInit)
|
||||
|
||||
// Configure services
|
||||
const services = {
|
||||
identify: identify(),
|
||||
pubsub: gossipsub({ allowPublishToZeroTopicPeers: true })
|
||||
identifyPush: identifyPush(),
|
||||
pubsub: gossipsub({ allowPublishToZeroTopicPeers: true }),
|
||||
ping: ping(),
|
||||
keychain: keychain(keychainInit)
|
||||
}
|
||||
if (this.config.isCircuitRelay) {
|
||||
console.log('Helia (IPFS) node IS configured as Circuit Relay')
|
||||
services.relay = circuitRelayServer({ // makes the node function as a relay server
|
||||
hopTimeout: 30 * 1000, // incoming relay requests must be resolved within this time limit
|
||||
advertise: true,
|
||||
reservations: {
|
||||
maxReservations: 15, // how many peers are allowed to reserve relay slots on this server
|
||||
reservationClearInterval: 300 * 1000, // how often to reclaim stale reservations
|
||||
applyDefaultLimit: true, // whether to apply default data/duration limits to each relayed connection
|
||||
defaultDurationLimit: 2 * 60 * 1000, // the default maximum amount of time a relayed connection can be open for
|
||||
defaultDataLimit: BigInt(2 << 7), // the default maximum number of bytes that can be transferred over a relayed connection
|
||||
maxInboundHopStreams: 32, // how many inbound HOP streams are allow simultaneously
|
||||
maxOutboundHopStreams: 64 // how many outbound HOP streams are allow simultaneously
|
||||
}
|
||||
defaultDataLimit: BigInt(2 << 7) // the default maximum number of bytes that can be transferred over a relayed connection
|
||||
},
|
||||
maxInboundHopStreams: 32, // how many inbound HOP streams are allow simultaneously
|
||||
maxOutboundHopStreams: 64 // how many outbound HOP streams are allow simultaneously
|
||||
})
|
||||
} else {
|
||||
console.log('Helia (IPFS) node IS NOT configured as Circuit Relay')
|
||||
@@ -177,28 +161,31 @@ class IpfsAdapter {
|
||||
|
||||
const transports = [
|
||||
tcp(),
|
||||
webSockets()
|
||||
// circuitRelayTransport({
|
||||
// discoverRelays: 3,
|
||||
// reservationConcurrency: 3
|
||||
// }),
|
||||
// webRTC()
|
||||
webSockets(),
|
||||
circuitRelayTransport({
|
||||
discoverRelays: 3,
|
||||
reservationConcurrency: 3
|
||||
}),
|
||||
webRTC(),
|
||||
webRTCDirect()
|
||||
]
|
||||
|
||||
// libp2p is the networking layer that underpins Helia
|
||||
const libp2p = await this.createLibp2p({
|
||||
peerId,
|
||||
// peerId,
|
||||
privateKey,
|
||||
datastore,
|
||||
addresses: {
|
||||
listen: [
|
||||
'/ip4/127.0.0.1/tcp/0',
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`
|
||||
// '/webrtc'
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`,
|
||||
'/webrtc',
|
||||
'/p2p-circuit'
|
||||
]
|
||||
},
|
||||
transports,
|
||||
connectionEncryption: [
|
||||
connectionEncrypters: [
|
||||
noise()
|
||||
],
|
||||
streamMuxers: [
|
||||
@@ -244,6 +231,8 @@ class IpfsAdapter {
|
||||
|
||||
!this.fs.existsSync(`${IPFS_DIR}/datastore`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore`)
|
||||
|
||||
!this.fs.existsSync(`${IPFS_DIR}/datastore/pkcs8`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore/pkcs8`)
|
||||
|
||||
// !fs.existsSync(`${IPFS_DIR}/datastore/peers`) && fs.mkdirSync(`${IPFS_DIR}/datastore/peers`)
|
||||
|
||||
return true
|
||||
@@ -280,6 +269,48 @@ class IpfsAdapter {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the private key from disk, or generate a new one and save it,
|
||||
// if it doesn't exist.
|
||||
// async getPrivateKey () {
|
||||
// try {
|
||||
// const filename = `${IPFS_DIR}/privkey.json`
|
||||
|
||||
// let privKeyHex
|
||||
// try {
|
||||
// // Try to read the JSON file containing the seed.
|
||||
// privKeyHex = await this.jsonFiles.readJSON(filename)
|
||||
// // console.log('saved privKeyHex: ', privKeyHex)
|
||||
// } catch (err) {
|
||||
// // Generate a new private key and save it to disk.
|
||||
|
||||
// // Generate a new private key and save it to disk.
|
||||
// const randomBuffer = crypto.randomBytes(32)
|
||||
// // console.log('randomBuffer: ', randomBuffer)
|
||||
|
||||
// privKeyHex = randomBuffer.toString('hex')
|
||||
// // console.log('new privKeyHex: ', privKeyHex)
|
||||
|
||||
// // Save the newly generated seed
|
||||
// await this.jsonFiles.writeJSON(privKeyHex, filename)
|
||||
// }
|
||||
|
||||
// // Convert the saved hex string to a buffer.
|
||||
// const privKeyBuf = Buffer.from(privKeyHex, 'hex')
|
||||
|
||||
// // Convert the buffer to a Uint8Array 'seed'
|
||||
// const seed = new Uint8Array(privKeyBuf.buffer, privKeyBuf.byteOffset, privKeyBuf.byteLength)
|
||||
// // console.log('seed: ', seed)
|
||||
|
||||
// // Generate a ED25519 key pair.
|
||||
// const privKey = await generateKeyPairFromSeed('Ed25519', seed)
|
||||
|
||||
// return privKey
|
||||
// } catch (err) {
|
||||
// console.error('Error in adapters/ipfs/ipfs.js/getPrivateKey(): ', err)
|
||||
// throw err
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
export default IpfsAdapter
|
||||
|
||||
@@ -34,7 +34,8 @@ const Offer = new mongoose.Schema({
|
||||
// SWaP Protocol Properties
|
||||
lokadId: { type: String },
|
||||
messageType: { type: Number },
|
||||
messageClass: { type: Number }
|
||||
messageClass: { type: Number },
|
||||
nostrEventId: { type: String } // Nostr Event Id.
|
||||
|
||||
})
|
||||
|
||||
|
||||
@@ -10,20 +10,20 @@ import mongoose from 'mongoose'
|
||||
|
||||
const Order = new mongoose.Schema({
|
||||
// Token data
|
||||
tokenId: { type: String },
|
||||
utxoTxid: { type: String },
|
||||
utxoVout: { type: Number },
|
||||
ticker: { type: String },
|
||||
tokenType: { type: Number },
|
||||
tokenId: { type: String, required: true },
|
||||
utxoTxid: { type: String, required: true },
|
||||
utxoVout: { type: Number, required: true },
|
||||
ticker: { type: String, required: true },
|
||||
tokenType: { type: Number, required: true },
|
||||
|
||||
// Trade data
|
||||
buyOrSell: { type: String },
|
||||
numTokens: { type: Number },
|
||||
rateInBaseUnit: { type: String },
|
||||
buyOrSell: { type: String, required: true },
|
||||
numTokens: { type: Number, required: true },
|
||||
rateInBaseUnit: { type: String, required: true },
|
||||
minUnitsToExchange: { type: String },
|
||||
p2wdbTxid: { type: String },
|
||||
p2wdbHash: { type: String },
|
||||
makerAddr: { type: String },
|
||||
makerAddr: { type: String, required: true },
|
||||
|
||||
// Authentication data
|
||||
signature: { type: String },
|
||||
@@ -32,13 +32,16 @@ const Order = new mongoose.Schema({
|
||||
offerPubKey: { type: String },
|
||||
|
||||
// Wallet Data
|
||||
hdIndex: { type: Number }, // HD index address holding the UTXO for this offer.
|
||||
hdIndex: { type: Number, required: true }, // HD index address holding the UTXO for this offer.
|
||||
|
||||
// SWaP Protocol Properties
|
||||
lokadId: { type: String },
|
||||
messageType: { type: Number },
|
||||
messageClass: { type: Number },
|
||||
nostrEventId: { type: String } // Nostr Event Id.
|
||||
nostrEventId: { type: String, required: true }, // Nostr Event Id.
|
||||
|
||||
// Additional properties found in createOrder
|
||||
dataType: { type: String, required: true }
|
||||
})
|
||||
|
||||
export default mongoose.model('order', Order)
|
||||
|
||||
@@ -11,6 +11,7 @@ const User = new mongoose.Schema({
|
||||
name: { type: String },
|
||||
username: { type: String },
|
||||
password: { type: String, required: true },
|
||||
mnemonic: { type: String, required: true },
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
+18
-2
@@ -2,8 +2,11 @@
|
||||
Adapter library for working with Nostr.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import BchNostr from 'bch-nostr'
|
||||
import { RelayPool } from 'nostr'
|
||||
import RetryQueue from '@chris.troutner/retry-queue'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
|
||||
class NostrAdapter {
|
||||
constructor (localConfig = { nostrRelay: '', nostrTopic: '' }) {
|
||||
@@ -25,11 +28,17 @@ class NostrAdapter {
|
||||
// Encapsulate dependencies
|
||||
this.bchNostr = new BchNostr()
|
||||
this.RelayPool = RelayPool
|
||||
this.retryQueue = new RetryQueue({
|
||||
concurrency: 1,
|
||||
attempts: 5,
|
||||
retryPeriod: 1000
|
||||
})
|
||||
|
||||
// Bind the 'this' object
|
||||
this.start = this.start.bind(this)
|
||||
this.post = this.post.bind(this)
|
||||
this.read = this.read.bind(this)
|
||||
this.eventId2note = this.eventId2note.bind(this)
|
||||
}
|
||||
|
||||
// Create nostr keys.
|
||||
@@ -71,7 +80,7 @@ class NostrAdapter {
|
||||
msg,
|
||||
tags: [['t', this.topic]]
|
||||
}
|
||||
const eventId = await this.bchNostr.post.uploadToNostr(inObj)
|
||||
const eventId = await this.retryQueue.addToQueue(this.bchNostr.post.uploadToNostr, inObj)
|
||||
return eventId
|
||||
} catch (error) {
|
||||
console.log(`Error in nostr.js/post() ${error.message} `)
|
||||
@@ -103,7 +112,8 @@ class NostrAdapter {
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
messages.push(ev.content)
|
||||
// console.log('ev: ', ev)
|
||||
messages.push({ content: ev.content, eventId: ev.id })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -115,6 +125,12 @@ class NostrAdapter {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Convert an Event ID into a `noteabc..` syntax that Astral expects.
|
||||
// This can be used to generate a link to Astral to display the post.
|
||||
eventId2note (eventId) {
|
||||
return nip19.noteEncode(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
export default NostrAdapter
|
||||
|
||||
@@ -20,6 +20,13 @@ class Wallet {
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.instanceWalletWithoutInitialization = this.instanceWalletWithoutInitialization.bind(this)
|
||||
this._instanceWallet = this._instanceWallet.bind(this)
|
||||
this.openWallet = this.openWallet.bind(this)
|
||||
this.instanceWallet = this.instanceWallet.bind(this)
|
||||
this.incrementNextAddress = this.incrementNextAddress.bind(this)
|
||||
this.getKeyPair = this.getKeyPair.bind(this)
|
||||
this.optimize = this.optimize.bind(this)
|
||||
this.getBalance = this.getBalance.bind(this)
|
||||
}
|
||||
|
||||
// This is used for initializing the wallet, without waiting to update the wallet
|
||||
|
||||
+85
-5
@@ -1,16 +1,17 @@
|
||||
/*
|
||||
Adapter library for working with a wallet.
|
||||
This library is specific to bch-dex. The other wallet adapter library is
|
||||
inherited from ipfs-service-provider.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import BchWallet from 'minimal-slp-wallet'
|
||||
import BchTokenSweep from 'bch-token-sweep/index.js'
|
||||
|
||||
import bitcoinJs from 'bitcoincashjs-lib'
|
||||
import RetryQueue from '@chris.troutner/retry-queue'
|
||||
|
||||
// Local libraries
|
||||
import JsonFiles from './json-files.js'
|
||||
|
||||
import config from '../../config/index.js'
|
||||
|
||||
// Hack to get __dirname back.
|
||||
@@ -35,9 +36,25 @@ class WalletAdapter {
|
||||
this.BchWallet = BchWallet
|
||||
this.bchWallet = {} // Will be replaced when initialized.
|
||||
// this.advancedConfig = localConfig.advancedConfig
|
||||
this.retryQueue = new RetryQueue({
|
||||
concurrency: 1,
|
||||
attempts: 5,
|
||||
retryPeriod: 5000
|
||||
})
|
||||
|
||||
// Bind the 'this' object
|
||||
this.openWallet = this.openWallet.bind(this)
|
||||
this.instanceWallet = this.instanceWallet.bind(this)
|
||||
this.incrementNextAddress = this.incrementNextAddress.bind(this)
|
||||
this.getKeyPair = this.getKeyPair.bind(this)
|
||||
this.generateSignature = this.generateSignature.bind(this)
|
||||
this.generatePartialTx = this.generatePartialTx.bind(this)
|
||||
this.moveTokens = this.moveTokens.bind(this)
|
||||
this.moveBch = this.moveBch.bind(this)
|
||||
this.deseralizeTx = this.deseralizeTx.bind(this)
|
||||
this.completeTx = this.completeTx.bind(this)
|
||||
this.reclaimTokens = this.reclaimTokens.bind(this)
|
||||
this.moveTokensFromCustomWallet = this.moveTokensFromCustomWallet.bind(this)
|
||||
}
|
||||
|
||||
// Open the wallet file, or create one if the file doesn't exist.
|
||||
@@ -345,7 +362,7 @@ class WalletAdapter {
|
||||
throw err
|
||||
}
|
||||
|
||||
// return true
|
||||
// return true
|
||||
}
|
||||
|
||||
// Move tokens to an address controlled by the HD wallet, to generate a
|
||||
@@ -441,7 +458,7 @@ class WalletAdapter {
|
||||
|
||||
return txObj2
|
||||
} catch (err) {
|
||||
console.error('Error in wallet.js/deserializePartialTx()')
|
||||
console.error('Error in wallet.js/deserializeTx()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -450,7 +467,15 @@ class WalletAdapter {
|
||||
// then broadcasting the transaction to the network.
|
||||
async completeTx (hex, hdIndex) {
|
||||
try {
|
||||
// Input validation
|
||||
if (!hex || typeof hex !== 'string') {
|
||||
throw new Error('hex must be a string!')
|
||||
}
|
||||
if (typeof hdIndex !== 'number' || hdIndex < 0) {
|
||||
throw new Error('hdIndex must be a non-negative number!')
|
||||
}
|
||||
// console.log('hex: ', hex)
|
||||
console.log('completeTx() hdIndex: ', hdIndex)
|
||||
|
||||
const bchjs = this.bchWallet.bchjs
|
||||
|
||||
@@ -475,6 +500,8 @@ class WalletAdapter {
|
||||
// 'mainnet'
|
||||
// )
|
||||
|
||||
console.log('completeTx() this.walletInfo: ', this.walletInfo)
|
||||
|
||||
// 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)}`)
|
||||
@@ -508,7 +535,9 @@ class WalletAdapter {
|
||||
// return csTxHex
|
||||
|
||||
// Broadcast transaction to the network
|
||||
const txid = await this.bchWallet.ar.sendTx(csTxHex)
|
||||
// const txid = await this.bchWallet.ar.sendTx(csTxHex)
|
||||
const txid = await this.retryQueue.addToQueue(this.bchWallet.broadcast, { hex: csTxHex })
|
||||
console.log('completeTx() txid: ', txid)
|
||||
|
||||
return txid
|
||||
} catch (err) {
|
||||
@@ -557,6 +586,57 @@ class WalletAdapter {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async moveTokensFromCustomWallet (inObj = {}) {
|
||||
try {
|
||||
const { tokenId, qty, wallet } = inObj
|
||||
// Input validation
|
||||
if (!tokenId || typeof tokenId !== 'string') {
|
||||
throw new Error('tokenId must be a string!')
|
||||
}
|
||||
if (!qty) {
|
||||
throw new Error('qty must be a number!')
|
||||
}
|
||||
if (!wallet) {
|
||||
throw new Error('wallet is required!')
|
||||
}
|
||||
|
||||
const keyPair = await this.getKeyPair()
|
||||
console.log('keyPair: ', keyPair)
|
||||
|
||||
const receiver = {
|
||||
address: keyPair.cashAddress,
|
||||
tokenId,
|
||||
qty
|
||||
}
|
||||
console.log('receiver: ', receiver)
|
||||
|
||||
// Update the UTXO store of the wallet.
|
||||
await wallet.initialize()
|
||||
|
||||
// Get the token type of the token being moved.
|
||||
// Combine Fungible and NFT token UTXOs.
|
||||
let tokenUtxos = wallet.utxos.utxoStore.slpUtxos.type1.tokens.concat(
|
||||
wallet.utxos.utxoStore.slpUtxos.nft.tokens)
|
||||
// Get token UTXOs that match the token in the order.
|
||||
tokenUtxos = tokenUtxos.filter(
|
||||
x => x.tokenId === tokenId
|
||||
)
|
||||
|
||||
const txid = await wallet.sendTokens(receiver, 3)
|
||||
const utxoInfo = {
|
||||
txid,
|
||||
vout: 1,
|
||||
hdIndex: keyPair.hdIndex,
|
||||
tokenType: tokenUtxos[0].tokenType
|
||||
}
|
||||
|
||||
return utxoInfo
|
||||
} catch (err) {
|
||||
console.error('Error in wallet.js/moveTokensFromCustomWallet()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletAdapter
|
||||
|
||||
@@ -21,6 +21,13 @@ class Controllers {
|
||||
this.useCases = new UseCases({ adapters: this.adapters })
|
||||
this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases })
|
||||
this.config = config
|
||||
|
||||
// Bind 'this' object to all subfunction
|
||||
this.initAdapters = this.initAdapters.bind(this)
|
||||
this.initUseCases = this.initUseCases.bind(this)
|
||||
this.attachRESTControllers = this.attachRESTControllers.bind(this)
|
||||
this.attachControllers = this.attachControllers.bind(this)
|
||||
this.attachRPCControllers = this.attachRPCControllers.bind(this)
|
||||
}
|
||||
|
||||
// Spin up any adapter libraries that have async startup needs.
|
||||
|
||||
@@ -17,6 +17,7 @@ import EntryRouter from './entry/index.js'
|
||||
import OfferRouter from './offer/index.js'
|
||||
import OrderRouter from './order/index.js'
|
||||
import P2WDBRouter from './p2wdb/index.js'
|
||||
import UsageRESTController from './usage/index.js'
|
||||
|
||||
class RESTControllers {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -34,6 +35,9 @@ class RESTControllers {
|
||||
)
|
||||
}
|
||||
|
||||
// Bind 'this' object to all subfunctions.
|
||||
this.attachRESTControllers = this.attachRESTControllers.bind(this)
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
}
|
||||
@@ -78,6 +82,10 @@ class RESTControllers {
|
||||
|
||||
const p2wdbRouter = new P2WDBRouter(dependencies)
|
||||
p2wdbRouter.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /usage route
|
||||
const usageRESTController = new UsageRESTController(dependencies)
|
||||
usageRESTController.attach(app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
import wlogger from '../../../adapters/wlogger.js'
|
||||
|
||||
let _this
|
||||
|
||||
class OfferRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
@@ -26,7 +24,13 @@ class OfferRESTControllerLib {
|
||||
this.OfferModel = this.adapters.localdb.Offer
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
_this = this
|
||||
// Bind 'this' object to all subfunctions.
|
||||
this.createOffer = this.createOffer.bind(this)
|
||||
this.listOffers = this.listOffers.bind(this)
|
||||
this.listNftOffers = this.listNftOffers.bind(this)
|
||||
this.listFungibleOffers = this.listFungibleOffers.bind(this)
|
||||
this.takeOffer = this.takeOffer.bind(this)
|
||||
this.handleError = this.handleError.bind(this)
|
||||
}
|
||||
|
||||
// No api-doc documentation because this wont be a public endpoint
|
||||
@@ -36,7 +40,7 @@ class OfferRESTControllerLib {
|
||||
|
||||
const offerObj = ctx.request.body
|
||||
|
||||
await _this.useCases.offer.createOffer(offerObj)
|
||||
await this.useCases.offer.createOffer(offerObj)
|
||||
|
||||
ctx.body = {
|
||||
success: true
|
||||
@@ -45,7 +49,7 @@ class OfferRESTControllerLib {
|
||||
// console.log(`err.message: ${err.message}`)
|
||||
// console.log('err: ', err)
|
||||
// ctx.throw(422, err.message)
|
||||
_this.handleError(ctx, err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +59,12 @@ class OfferRESTControllerLib {
|
||||
let page = ctx.params.page
|
||||
if (!page) page = 0
|
||||
|
||||
const offers = await _this.useCases.offer.listOffers(page)
|
||||
const offers = await this.useCases.offer.listOffers(page)
|
||||
|
||||
ctx.body = offers
|
||||
} catch (err) {
|
||||
console.log('Error in listOffers REST API handler: ', err)
|
||||
_this.handleError(ctx, err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,12 +74,12 @@ class OfferRESTControllerLib {
|
||||
let page = ctx.params.page
|
||||
if (!page) page = 0
|
||||
|
||||
const offers = await _this.useCases.offer.listNftOffers(page)
|
||||
const offers = await this.useCases.offer.listNftOffers(page)
|
||||
|
||||
ctx.body = offers
|
||||
} catch (err) {
|
||||
console.log('Error in listNftOffers REST API handler: ', err)
|
||||
_this.handleError(ctx, err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,12 +89,12 @@ class OfferRESTControllerLib {
|
||||
let page = ctx.params.page
|
||||
if (!page) page = 0
|
||||
|
||||
const offers = await _this.useCases.offer.listFungibleOffers(page)
|
||||
const offers = await this.useCases.offer.listFungibleOffers(page)
|
||||
|
||||
ctx.body = offers
|
||||
} catch (err) {
|
||||
console.log('Error in Fungible REST API handler: ', err)
|
||||
_this.handleError(ctx, err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,14 +104,14 @@ class OfferRESTControllerLib {
|
||||
try {
|
||||
console.log('REST API controller, body: ', ctx.request.body)
|
||||
|
||||
const offerCid = ctx.request.body.offerCid
|
||||
const nostrEventId = ctx.request.body.nostrEventId
|
||||
|
||||
const hash = await _this.useCases.offer.takeOffer(offerCid)
|
||||
const { eventId, noteId } = await this.useCases.offer.takeOffer(nostrEventId)
|
||||
|
||||
ctx.body = { hash }
|
||||
ctx.body = { eventId, noteId }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in takeOffer() REST API handler.')
|
||||
_this.handleError(ctx, err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,13 @@ class OfferRouter {
|
||||
)
|
||||
}
|
||||
|
||||
// 12/3/24 CT:
|
||||
// Note: The createOffer() path was used by a P2WDB webhook to generate an
|
||||
// Offer from and Order. This has been deprecated and Offers are now created
|
||||
// by a Timer Controller monitoring a Nostr topic.
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', _this.offerRESTController.createOffer)
|
||||
// this.router.post('/', _this.offerRESTController.createOffer) // Deprecated.
|
||||
this.router.post('/take', _this.offerRESTController.takeOffer)
|
||||
this.router.get('/list/all/:page', _this.offerRESTController.listOffers)
|
||||
this.router.get('/list/nft/:page', _this.offerRESTController.listNftOffers)
|
||||
|
||||
@@ -35,11 +35,13 @@ class OrderRESTControllerLib {
|
||||
// console.log('body: ', ctx.request.body)
|
||||
|
||||
const orderObj = ctx.request.body.order
|
||||
const user = ctx.state.user
|
||||
orderObj.userId = user._id
|
||||
console.log('orderObj: ', orderObj)
|
||||
|
||||
const hash = await _this.useCases.order.createOrder(orderObj)
|
||||
const { eventId, noteId } = await _this.useCases.order.createOrder(orderObj)
|
||||
|
||||
ctx.body = { hash }
|
||||
ctx.body = { eventId, noteId }
|
||||
} catch (err) {
|
||||
// console.log(`err.message: ${err.message}`)
|
||||
// console.log('err: ', err)
|
||||
|
||||
@@ -7,6 +7,7 @@ import Router from 'koa-router'
|
||||
|
||||
// Local libraries.
|
||||
import OrderRESTControllerLib from './controller.js'
|
||||
import Validators from '../middleware/validators.js'
|
||||
|
||||
let _this
|
||||
|
||||
@@ -32,12 +33,14 @@ class OrderRouter {
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.validators = new Validators()
|
||||
this.orderRESTController = new OrderRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/order'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
this.createOrder = this.createOrder.bind(this)
|
||||
_this = this
|
||||
}
|
||||
|
||||
@@ -49,7 +52,7 @@ class OrderRouter {
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', _this.orderRESTController.createOrder)
|
||||
this.router.post('/', this.createOrder)
|
||||
this.router.get('/list/all/:page', _this.orderRESTController.listOrders)
|
||||
this.router.post('/delete', _this.orderRESTController.deleteOrder)
|
||||
|
||||
@@ -57,6 +60,11 @@ class OrderRouter {
|
||||
app.use(_this.router.routes())
|
||||
app.use(_this.router.allowedMethods())
|
||||
}
|
||||
|
||||
async createOrder (ctx, next) {
|
||||
await _this.validators.ensureUser(ctx, next)
|
||||
await _this.orderRESTController.createOrder(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderRouter
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
REST API Controller library for the /usage route
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
|
||||
// Local libraries
|
||||
import wlogger from '../../../adapters/wlogger.js'
|
||||
|
||||
class UsageRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.getStatus = this.getStatus.bind(this)
|
||||
this.getTopIps = this.getTopIps.bind(this)
|
||||
this.getTopEndpoints = this.getTopEndpoints.bind(this)
|
||||
this.handleError = this.handleError.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /usage Get status on IPFS infrastructure
|
||||
* @apiPermission public
|
||||
* @apiName GetUsageStatus
|
||||
* @apiGroup REST Usage
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage
|
||||
*
|
||||
*/
|
||||
getStatus (ctx) {
|
||||
try {
|
||||
// const status = await this.adapters.ipfs.getStatus()
|
||||
const status = this.useCases.usage.getRestSummary()
|
||||
|
||||
ctx.body = { status }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in usage/controller.js/getStatus(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /usage/ips Get top IP addresses consuming the REST API
|
||||
* @apiPermission public
|
||||
* @apiName GetUsageIPs
|
||||
* @apiGroup REST Usage
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/ips
|
||||
*
|
||||
*/
|
||||
getTopIps (ctx) {
|
||||
try {
|
||||
const ips = this.useCases.usage.getTopIps()
|
||||
|
||||
ctx.body = { ips }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in usage/controller.js/getTopIps(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /usage/endpoints Get top endpoints consumed from the REST API
|
||||
* @apiPermission public
|
||||
* @apiName GetUsageEndpoints
|
||||
* @apiGroup REST Usage
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/endpoints
|
||||
*
|
||||
*/
|
||||
getTopEndpoints (ctx) {
|
||||
try {
|
||||
const endpoints = this.useCases.usage.getTopEndpoints()
|
||||
|
||||
ctx.body = { endpoints }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in usage/controller.js/getTopEndpoints(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, 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 = IpfsRESTControllerLib
|
||||
export default UsageRESTControllerLib
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
REST API library for the /usage route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
import Router from 'koa-router'
|
||||
|
||||
// Local libraries.
|
||||
import UsageRESTControllerLib from './controller.js'
|
||||
import Validators from '../middleware/validators.js'
|
||||
|
||||
// let _this
|
||||
|
||||
class UsageRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.usageRESTController = new UsageRESTControllerLib(dependencies)
|
||||
this.validators = new Validators()
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/usage'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
// _this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.get('/', this.usageRESTController.getStatus)
|
||||
this.router.get('/ips', this.usageRESTController.getTopIps)
|
||||
this.router.get('/endpoints', this.usageRESTController.getTopEndpoints)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = BchRouter
|
||||
export default UsageRouter
|
||||
@@ -10,6 +10,8 @@ import UserRESTControllerLib from './controller.js'
|
||||
|
||||
import Validators from '../middleware/validators.js'
|
||||
|
||||
import config from '../../../../config/index.js'
|
||||
|
||||
let _this
|
||||
|
||||
class UserRouter {
|
||||
@@ -34,6 +36,7 @@ class UserRouter {
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.config = config
|
||||
this.userRESTController = new UserRESTControllerLib(dependencies)
|
||||
this.validators = new Validators()
|
||||
|
||||
@@ -52,7 +55,7 @@ class UserRouter {
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', this.userRESTController.createUser)
|
||||
this.router.post('/', this.createUser)
|
||||
this.router.get('/', this.getAll)
|
||||
this.router.get('/:id', this.getById)
|
||||
this.router.put('/:id', this.updateUser)
|
||||
@@ -63,6 +66,14 @@ class UserRouter {
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
|
||||
async createUser (ctx, next) {
|
||||
if (process.env.DISABLE_NEW_ACCOUNTS) {
|
||||
await _this.validators.ensureAdmin(ctx, next)
|
||||
}
|
||||
await _this.userRESTController.createUser(ctx, next)
|
||||
return true
|
||||
}
|
||||
|
||||
async getAll (ctx, next) {
|
||||
await _this.validators.ensureUser(ctx, next)
|
||||
await _this.userRESTController.getUsers(ctx, next)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
This Controller library is concerned with timer-based functions that are
|
||||
kicked off periodicially.
|
||||
kicked off periodically.
|
||||
*/
|
||||
|
||||
import config from '../../config/index.js'
|
||||
@@ -29,35 +29,49 @@ class TimerControllers {
|
||||
this.gcOrders = this.gcOrders.bind(this)
|
||||
this.gcOffers = this.gcOffers.bind(this)
|
||||
this.checkDupOffers = this.checkDupOffers.bind(this)
|
||||
this.loadOffers = this.loadOffers.bind(this)
|
||||
// Bind 'this' object to all subfunctions.
|
||||
// this.exampleTimerFunc = this.exampleTimerFunc.bind(this)
|
||||
this.cleanUsage = this.cleanUsage.bind(this)
|
||||
|
||||
// State
|
||||
this.gcOrdersInt = null
|
||||
this.gcOffersInt = null
|
||||
this.checkDupOffersInt = null
|
||||
|
||||
this.startTimers()
|
||||
this.loadOffersInt = null
|
||||
}
|
||||
|
||||
// Start all the time-based controllers.
|
||||
startTimers () {
|
||||
this.gcOrdersInt = setInterval(this.gcOrders, 60000 * 5)
|
||||
this.gcOffersInt = setInterval(this.gcOffers, 60000 * 5)
|
||||
this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5)
|
||||
// this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5)
|
||||
this.loadOffersInt = setInterval(this.loadOffers, 60000 * 2)
|
||||
// Any new timer control functions can be added here. They will be started
|
||||
// when the server starts.
|
||||
// this.optimizeWalletHandle = setInterval(this.exampleTimerFunc, 60000 * 60)
|
||||
this.cleanUsageHandle = setInterval(this.cleanUsage, 60000 * 60) // 1 hour
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
stopTimers () {
|
||||
clearInterval(this.gcOrdersInt)
|
||||
clearInterval(this.gcOffersInt)
|
||||
clearInterval(this.checkDupOffers)
|
||||
// clearInterval(this.checkDupOffers)
|
||||
clearInterval(this.optimizeWalletHandle)
|
||||
clearInterval(this.cleanUsageHandle)
|
||||
}
|
||||
|
||||
// Garbage Collect the Orders.
|
||||
gcOrders () {
|
||||
try {
|
||||
this.useCases.order.removeStaleOrders()
|
||||
return true
|
||||
} catch (err) {
|
||||
// Do not throw an error. This is a top-level function.
|
||||
console.log('Error in timer-controllers.js/gcOrders(): ', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,9 +79,11 @@ class TimerControllers {
|
||||
gcOffers () {
|
||||
try {
|
||||
this.useCases.offer.removeStaleOffers()
|
||||
return true
|
||||
} catch (err) {
|
||||
// Do not throw an error. This is a top-level function.
|
||||
console.log('Error in timer-controllers.js/gcOffers(): ', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +91,40 @@ class TimerControllers {
|
||||
checkDupOffers () {
|
||||
try {
|
||||
this.useCases.offer.removeDuplicateOffers()
|
||||
return true
|
||||
} catch (err) {
|
||||
// Do not throw an error. This is a top-level function.
|
||||
console.log('Error in timer-controllers.js/checkDupOffers(): ', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Load offers From nostr .
|
||||
async loadOffers () {
|
||||
try {
|
||||
await this.useCases.offer.loadOffers()
|
||||
return true
|
||||
} catch (err) {
|
||||
// Do not throw an error. This is a top-level function.
|
||||
console.log('Error in timer-controllers.js/loadOffers(): ', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Clean the usage state so that stats reflect the last 24 hours.
|
||||
cleanUsage () {
|
||||
try {
|
||||
const now = new Date()
|
||||
console.log(`cleanUsage() Timer Controller executing at ${now.toLocaleString()}`)
|
||||
|
||||
this.useCases.usage.cleanUsage()
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in time-controller.js/cleanUsage(): ', err)
|
||||
|
||||
// Note: Do not throw an error. This is a top-level function.
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -17,6 +17,8 @@ class OfferEntity {
|
||||
)
|
||||
}
|
||||
|
||||
// console.log('offer entity validate() offerData: ', offerData)
|
||||
|
||||
const {
|
||||
messageType,
|
||||
messageClass,
|
||||
@@ -30,7 +32,8 @@ class OfferEntity {
|
||||
offerStatus,
|
||||
makerAddr,
|
||||
ticker,
|
||||
tokenType
|
||||
tokenType,
|
||||
nostrEventId
|
||||
} = offerData.data
|
||||
|
||||
// Input Validation
|
||||
@@ -73,6 +76,9 @@ class OfferEntity {
|
||||
if (!tokenType || typeof tokenType !== 'number') {
|
||||
throw new Error("Property 'tokenType' must be a number.")
|
||||
}
|
||||
if (!nostrEventId || typeof nostrEventId !== 'string') {
|
||||
throw new Error("Property 'nostrEventId' must be a string.")
|
||||
}
|
||||
|
||||
// Convert the timestamp to a number.
|
||||
let timestamp = new Date(offerData.timestamp)
|
||||
@@ -96,8 +102,10 @@ class OfferEntity {
|
||||
offerStatus: offerStatus || this.offerStatus[0],
|
||||
makerAddr,
|
||||
ticker,
|
||||
tokenType
|
||||
tokenType,
|
||||
nostrEventId
|
||||
}
|
||||
// console.log('offer entity validatedOfferData: ', validatedOfferData)
|
||||
|
||||
return validatedOfferData
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
https://troutsblog.com/blog/clean-architecture
|
||||
*/
|
||||
|
||||
// Local libraries
|
||||
import UserUseCases from './user.js'
|
||||
import EntryUseCases from './entry.js'
|
||||
import OfferUseCases from './offer/index.js'
|
||||
import OrderUseCases from './order.js'
|
||||
import { UsageUseCases } from './usage-use-cases.js'
|
||||
|
||||
class UseCases {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -24,6 +26,7 @@ class UseCases {
|
||||
this.order = new OrderUseCases(localConfig)
|
||||
localConfig.order = this.order
|
||||
this.offer = new OfferUseCases(localConfig)
|
||||
this.usage = new UsageUseCases(localConfig)
|
||||
}
|
||||
|
||||
// Run any startup Use Cases at the start of the app.
|
||||
|
||||
+259
-89
@@ -37,7 +37,7 @@ class OfferUseCases {
|
||||
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.'
|
||||
'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,20 +49,37 @@ class OfferUseCases {
|
||||
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
|
||||
|
||||
// Bind 'this' object to functions
|
||||
this.createOffer = this.createOffer.bind(this)
|
||||
this.detectNsfw = this.detectNsfw.bind(this)
|
||||
this.categorizeToken = this.categorizeToken.bind(this)
|
||||
this.listOffers = this.listOffers.bind(this)
|
||||
this.listNftOffers = this.listNftOffers.bind(this)
|
||||
this.listFungibleOffers = this.listFungibleOffers.bind(this)
|
||||
this.takeOffer = this.takeOffer.bind(this)
|
||||
this.ensureFunds = this.ensureFunds.bind(this)
|
||||
this.findOfferByEvent = this.findOfferByEvent.bind(this)
|
||||
this.findOfferByTxid = this.findOfferByTxid.bind(this)
|
||||
this.acceptCounterOffer = this.acceptCounterOffer.bind(this)
|
||||
this.removeDuplicateOffers = this.removeDuplicateOffers.bind(this)
|
||||
this.removeStaleOffers = this.removeStaleOffers.bind(this)
|
||||
this.flagOffer = this.flagOffer.bind(this)
|
||||
this.loadOffers = this.loadOffers.bind(this)
|
||||
|
||||
// State
|
||||
this.seenOffers = []
|
||||
this.seenCounterOffers = []
|
||||
}
|
||||
|
||||
// This method is called by the POST /offer REST API controller, which is
|
||||
// triggered by a P2WDB webhook.
|
||||
// This method is called by timer controller to load offers from a Nostr topic.
|
||||
async createOffer (offerObj) {
|
||||
try {
|
||||
console.log('Use Case createOffer(offerObj): ', offerObj)
|
||||
// console.log('Use Case createOffer(offerObj): ', offerObj)
|
||||
|
||||
// Return if Offer already exists in database with the same P2WDB CID.
|
||||
// Return if Offer already exists in database with the same utxo transaction id.
|
||||
try {
|
||||
await this.findOfferByHash(offerObj.hash)
|
||||
await this.findOfferByTxid(offerObj.data.utxoTxid)
|
||||
|
||||
console.log('Offer already found in local database.')
|
||||
// console.log('Offer already found in local database.')
|
||||
return false
|
||||
} catch (err) { /* exit quietly */ }
|
||||
|
||||
@@ -71,9 +88,17 @@ class OfferUseCases {
|
||||
// Input Validation
|
||||
// TODO: This is a hack. Find a better way to protect against the corner-
|
||||
// case of counter-offers getting routed here.
|
||||
if (offerObj.data.dataType === 'counter-offer') {
|
||||
console.log('WARN: Counter Offer innappropriately routed to createOffer()')
|
||||
// if (offerObj.data.dataType === 'counter-offer') {
|
||||
// console.log('WARN: Counter Offer innappropriately routed to createOffer()')
|
||||
// }
|
||||
|
||||
// Quickly skip over offers that have already been processed.
|
||||
const eventId = offerObj.data.nostrEventId
|
||||
if (this.seenOffers.includes(eventId)) {
|
||||
// console.log(`Offer with event ID ${eventId} already processed. Skipping.`)
|
||||
return false
|
||||
}
|
||||
this.seenOffers.push(eventId)
|
||||
|
||||
// Verify that UTXO in offer is unspent. If it is spent, then ignore the
|
||||
// offer.
|
||||
@@ -83,21 +108,26 @@ class OfferUseCases {
|
||||
}
|
||||
// const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
// console.log('utxoStatus: ', utxoStatus)
|
||||
// if (utxoStatus === null) return false
|
||||
if (!utxoStatus) return false
|
||||
|
||||
// A new offer gets a status of 'posted'
|
||||
offerObj.data.offerStatus = 'posted'
|
||||
|
||||
// Set timestamp
|
||||
offerObj.timestamp = new Date().getTime()
|
||||
|
||||
const offerEntity = this.offerEntity.validate(offerObj)
|
||||
console.log('offerEntity: ', offerEntity)
|
||||
// console.log('offerEntity: ', offerEntity)
|
||||
|
||||
console.log(`New Offer for token ID ${offerEntity.tokenId} detected from Nostr post ${eventId}`)
|
||||
|
||||
// Get data about the token.
|
||||
const tokenId = offerEntity.tokenId
|
||||
// const tokenData = await this.adapters.wallet.bchWallet.getTokenData(tokenId)
|
||||
const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
|
||||
console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
||||
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
||||
|
||||
// Generate a 'display category' for the token. This will allow the
|
||||
// front end UI to figure out how to display the token.
|
||||
@@ -106,10 +136,9 @@ class OfferUseCases {
|
||||
offerEntity.displayCategory = displayCategory
|
||||
|
||||
// Detect if user set the NSFW flag.
|
||||
let nsfw = false
|
||||
// nsfw = await this.detectNsfw(tokenData)
|
||||
nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData)
|
||||
offerEntity.nsfw = nsfw
|
||||
// const nsfw = false
|
||||
// nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData)
|
||||
// offerEntity.nsfw = nsfw
|
||||
|
||||
// Add offer to the local database.
|
||||
const offerModel = new this.OfferModel(offerEntity)
|
||||
@@ -117,7 +146,7 @@ class OfferUseCases {
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in createOffer()')
|
||||
console.error('Error in createOffer()', err.message)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -139,16 +168,29 @@ class OfferUseCases {
|
||||
|
||||
// Retrieve the mutable data from Filecoin/IPFS.
|
||||
// const url = `https://${cid}.ipfs.w3s.link/data.json`
|
||||
const url = `${this.config.ipfsGateway}${cid}/data.json`
|
||||
const result = await axios.get(url)
|
||||
const mutableData = result.data
|
||||
console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`)
|
||||
let mutableData = {}
|
||||
|
||||
try {
|
||||
// Try the conventional data URL.
|
||||
const url = `${this.config.ipfsGateway}${cid}/data.json`
|
||||
const result = await this.axios.get(url)
|
||||
mutableData = result.data
|
||||
// console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`)
|
||||
} catch (err) {
|
||||
// Try the newer data URL.
|
||||
const url = `${this.config.ipfsGateway}${cid}`
|
||||
const result = await this.axios.get(url)
|
||||
mutableData = result.data
|
||||
// console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`)
|
||||
}
|
||||
console.log('mutableData: ', mutableData)
|
||||
|
||||
// Logical tests
|
||||
const hasNsfw = !!mutableData.nsfw
|
||||
const nsfwSetTrue = mutableData.nsfw === true
|
||||
const nsfwStringTrue = mutableData.nsfw === 'true'
|
||||
const nsfwDetected = hasNsfw && (nsfwSetTrue || nsfwStringTrue)
|
||||
// console.log(`hasNsfw: ${hasNsfw}, nsfwSetTrue: ${nsfwSetTrue}, nsfwStringTrue: ${nsfwStringTrue}, nsfwDetected: ${nsfwDetected}`)
|
||||
|
||||
if (nsfwDetected) {
|
||||
console.log('NSFW flag set as true')
|
||||
@@ -207,11 +249,11 @@ class OfferUseCases {
|
||||
async listOffers (page = 0) {
|
||||
try {
|
||||
const data = await this.OfferModel.find({})
|
||||
// Sort entries so newest entries show first.
|
||||
// Sort entries so newest entries show first.
|
||||
.sort('-timestamp')
|
||||
// Skip to the start of the selected page.
|
||||
// Skip to the start of the selected page.
|
||||
.skip(page * DEFAULT_ENTRIES_PER_PAGE)
|
||||
// Only return 20 results.
|
||||
// Only return 20 results.
|
||||
.limit(DEFAULT_ENTRIES_PER_PAGE)
|
||||
|
||||
return data
|
||||
@@ -227,11 +269,11 @@ class OfferUseCases {
|
||||
displayCategory: { $ne: 'fungible' },
|
||||
nsfw
|
||||
})
|
||||
// Sort entries so newest entries show first.
|
||||
// Sort entries so newest entries show first.
|
||||
.sort('-timestamp')
|
||||
// Skip to the start of the selected page.
|
||||
// Skip to the start of the selected page.
|
||||
.skip(page * NFT_ENTRIES_PER_PAGE)
|
||||
// Only return 20 results.
|
||||
// Only return 20 results.
|
||||
.limit(NFT_ENTRIES_PER_PAGE)
|
||||
|
||||
// console.log('listNftOffers() returning this data: ', data)
|
||||
@@ -246,11 +288,11 @@ class OfferUseCases {
|
||||
async listFungibleOffers (page = 0) {
|
||||
try {
|
||||
const data = await this.OfferModel.find({ displayCategory: 'fungible' })
|
||||
// Sort entries so newest entries show first.
|
||||
// Sort entries so newest entries show first.
|
||||
.sort('-timestamp')
|
||||
// Skip to the start of the selected page.
|
||||
// Skip to the start of the selected page.
|
||||
.skip(page * FUNGIBLE_ENTRIES_PER_PAGE)
|
||||
// Only return 20 results.
|
||||
// Only return 20 results.
|
||||
.limit(FUNGIBLE_ENTRIES_PER_PAGE)
|
||||
|
||||
// console.log('listFungibleOffers() returning this data: ', data)
|
||||
@@ -270,12 +312,12 @@ class OfferUseCases {
|
||||
// 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) {
|
||||
async takeOffer (eventId) {
|
||||
try {
|
||||
console.log('offerCid: ', offerCid)
|
||||
if (!eventId || typeof eventId !== 'string') throw new Error('eventId must be a string')
|
||||
|
||||
// Get the Offer information
|
||||
const offerInfo = await this.findOfferByHash(offerCid)
|
||||
const offerInfo = await this.findOfferByEvent(eventId)
|
||||
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
|
||||
|
||||
// Ensure the offer is in a 'posted' state and not already 'taken'
|
||||
@@ -288,8 +330,10 @@ class OfferUseCases {
|
||||
tx_hash: offerInfo.utxoTxid,
|
||||
tx_pos: offerInfo.utxoVout
|
||||
}
|
||||
|
||||
// Note : should be added to retry-queue?
|
||||
const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
// console.log('utxoStatus: ', utxoStatus)
|
||||
if (!utxoStatus) {
|
||||
console.log(`utxo txid: ${offerInfo.utxoTxid}, vout: ${offerInfo.utxoVout}`)
|
||||
|
||||
@@ -302,11 +346,12 @@ class OfferUseCases {
|
||||
await this.ensureFunds(offerInfo)
|
||||
|
||||
// Get UTXOs.
|
||||
const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore
|
||||
console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||
// 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 = Math.ceil(offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit))
|
||||
console.log('satsToMove', satsToMove, offerInfo)
|
||||
if (isNaN(satsToMove)) {
|
||||
throw new Error('Could not calculate the amount of BCH to generate counter offer')
|
||||
}
|
||||
@@ -332,9 +377,9 @@ class OfferUseCases {
|
||||
// Create valid Offer object
|
||||
const takenOfferInfo = Object.assign({}, offerInfo)
|
||||
takenOfferInfo.partialTxHex = partialTxHex
|
||||
delete takenOfferInfo.p2wdbHash
|
||||
delete takenOfferInfo.nostrEventId
|
||||
delete takenOfferInfo._id
|
||||
takenOfferInfo.offerHash = offerInfo.p2wdbHash
|
||||
takenOfferInfo.offerHash = offerInfo.nostrEventId
|
||||
|
||||
// Add P2WDB specific flag for signaling that this is a new offer.
|
||||
takenOfferInfo.dataType = 'counter-offer'
|
||||
@@ -342,19 +387,21 @@ class OfferUseCases {
|
||||
// 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 nostrData = {
|
||||
// wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
|
||||
data: takenOfferInfo
|
||||
// appId: this.config.p2wdbAppId
|
||||
}
|
||||
const hash = await this.adapters.p2wdb.write(p2wdbObj)
|
||||
const resultEventId = await this.adapters.nostr.post(JSON.stringify(nostrData))
|
||||
|
||||
const noteId = this.adapters.nostr.eventId2note(resultEventId)
|
||||
|
||||
// Delete the Offer from the database, so that the user doesn't attempt
|
||||
// to take the offer more than once.
|
||||
offerInfo.remove()
|
||||
// offerInfo.remove()
|
||||
|
||||
// Return the P2WDB CID
|
||||
return hash
|
||||
return { eventId: resultEventId, noteId }
|
||||
|
||||
// return 'fake-hash'
|
||||
} catch (err) {
|
||||
@@ -377,9 +424,10 @@ class OfferUseCases {
|
||||
await this.adapters.wallet.bchWallet.initialize()
|
||||
|
||||
// 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.')
|
||||
// Note : this validation should be deprecated for nostr functionality?
|
||||
// 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
|
||||
@@ -398,7 +446,7 @@ class OfferUseCases {
|
||||
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.')
|
||||
@@ -417,57 +465,130 @@ class OfferUseCases {
|
||||
}
|
||||
}
|
||||
|
||||
async findOfferByHash (p2wdbHash) {
|
||||
// Retrieve an Order model from the database. Find it by its event Id.
|
||||
async findOfferByEvent (nostrEventId) {
|
||||
try {
|
||||
if (typeof nostrEventId !== 'string' || !nostrEventId) {
|
||||
throw new Error('nostrEventId must be a string')
|
||||
}
|
||||
|
||||
const order = await this.OfferModel.findOne({ nostrEventId })
|
||||
|
||||
if (!order) {
|
||||
throw new Error('offer not found')
|
||||
}
|
||||
|
||||
const orderObject = order.toObject()
|
||||
// return this.offerEntity.validateFromModel(offerObject)
|
||||
|
||||
return orderObject
|
||||
} catch (err) {
|
||||
console.error('Error in findOfferByEvent()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async findOfferByTxid (utxoTxid) {
|
||||
// try {
|
||||
if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
|
||||
throw new Error('p2wdbHash must be a string')
|
||||
// try {
|
||||
if (typeof utxoTxid !== 'string' || !utxoTxid) {
|
||||
throw new Error('utxoTxid must be a string')
|
||||
}
|
||||
|
||||
const offer = await this.OfferModel.findOne({ p2wdbHash })
|
||||
const offer = await this.OfferModel.findOne({ utxoTxid })
|
||||
|
||||
// TODO: Offer should be found by TXID, then if there is more than one
|
||||
// result, they should be filtered by the vout property. That will leave
|
||||
// one remaining UTXO.
|
||||
|
||||
if (!offer) {
|
||||
throw new Error('offer not found')
|
||||
}
|
||||
|
||||
return offer
|
||||
|
||||
// const offerObject = offer.toObject()
|
||||
// return this.offerEntity.validateFromModel(offerObject)
|
||||
|
||||
// return offerObject
|
||||
// } catch (err) {
|
||||
// // console.error('Error in findOffer(): ', err)
|
||||
// throw err
|
||||
// } catch (error) {
|
||||
// // console.error('Error in use-cases/offer/findOfferByTxid(): ', error.message)
|
||||
// throw error
|
||||
// }
|
||||
}
|
||||
|
||||
// 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
|
||||
// to this function. It does due diligence on the Counter Offer, then signs
|
||||
// and broadcasts the transaction to accept the Counter Offer.
|
||||
async acceptCounterOffer (p2wdbData) {
|
||||
async acceptCounterOffer (offerData) {
|
||||
try {
|
||||
console.log(`acceptCounterOffer() p2wdbData: ${JSON.stringify(p2wdbData, null, 2)}`)
|
||||
// console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`)
|
||||
|
||||
// Quickly skip over offers that have already been processed.
|
||||
const eventId = offerData.data.nostrEventId
|
||||
if (this.seenOffers.includes(eventId)) {
|
||||
// console.log(`Offer with event ID ${eventId} already processed. Skipping.`)
|
||||
return false
|
||||
}
|
||||
|
||||
// See if this instance of bch-dex is managing the Order associated with
|
||||
// the incoming Counter Offer.
|
||||
const orderHash = p2wdbData.data.offerHash
|
||||
|
||||
// Note : this should be handled by nostrEvent id or UtxoId?
|
||||
// const orderHash = offerData.data.nostrEventId
|
||||
let orderData = {}
|
||||
try {
|
||||
orderData = await this.orderUseCase.findOrderByEvent(orderHash)
|
||||
orderData = await this.orderUseCase.findOrderByUtxo(offerData)
|
||||
console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`)
|
||||
} catch (err) {
|
||||
console.log('Order matching this Counter Offer is not managed by this instance of bch-dex. Exiting.')
|
||||
console.log('Order matching this Counter Offer is not managed by this instance of bch-dex. Skipping.')
|
||||
|
||||
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
|
||||
this.seenOffers.push(eventId)
|
||||
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
// Verify that the UTXO for sale is valid.
|
||||
let utxoStatus = null
|
||||
try {
|
||||
// Get the status of the UTXO associate with this Offer.
|
||||
const utxo = {
|
||||
tx_hash: offerData.data.utxoTxid,
|
||||
tx_pos: offerData.data.utxoVout
|
||||
}
|
||||
// console.log(`Checking this UTXO: ${JSON.stringify(utxo, null, 2)}`)
|
||||
|
||||
// utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||
utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
} catch (err) {
|
||||
console.log('acceptCounterOffer() err validating UTXO: ', err)
|
||||
|
||||
// Handle corner case of bad-data in the Offer model.
|
||||
if (err.isAxiosError) {
|
||||
console.log('Error trying to contact wallet service: ', err)
|
||||
return 'N/A'
|
||||
} else {
|
||||
return 'N/A'
|
||||
}
|
||||
}
|
||||
|
||||
// If the Offer UTXO is spent, exit.
|
||||
if (utxoStatus === false) {
|
||||
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
|
||||
this.seenOffers.push(eventId)
|
||||
|
||||
console.log(`Aborting Counter Offer from Event ID ${eventId}`)
|
||||
console.log(`https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(eventId)}`)
|
||||
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
// Deserialize the partially signed transaction.
|
||||
const txHex = p2wdbData.data.partialTxHex
|
||||
const txHex = offerData.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 = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit))
|
||||
console.log('Ceil', satsToReceive)
|
||||
if (isNaN(satsToReceive)) {
|
||||
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
|
||||
}
|
||||
@@ -492,7 +613,7 @@ class OfferUseCases {
|
||||
|
||||
return txid
|
||||
} catch (err) {
|
||||
console.error('Error in acceptCounterOffer()')
|
||||
console.error('Error in acceptCounterOffer(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -580,7 +701,7 @@ class OfferUseCases {
|
||||
|
||||
// If the Offer UTXO is spent, delete the Offer model.
|
||||
if (utxoStatus === false) {
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
// console.log('utxoStatus: ', utxoStatus)
|
||||
console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`)
|
||||
await thisOffer.remove()
|
||||
}
|
||||
@@ -600,32 +721,81 @@ class OfferUseCases {
|
||||
}
|
||||
}
|
||||
|
||||
async flagOffer (flagData) {
|
||||
console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`)
|
||||
// Flag offers as NSFW.
|
||||
async flagOffer (flagData = {}) {
|
||||
try {
|
||||
if (!flagData.data) throw new Error('"data" property is required')
|
||||
|
||||
const p2wdbHash = flagData.data.p2wdbHash
|
||||
console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`)
|
||||
|
||||
// Get the offer from the database.
|
||||
const offer = await this.findOfferByHash(p2wdbHash)
|
||||
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
|
||||
const eventId = flagData.data.nostrEventId
|
||||
|
||||
if (!offer) {
|
||||
throw new Error(`Offer ${p2wdbHash} not found in the database.`)
|
||||
// Get the offer from the database.
|
||||
const offer = await this.findOfferByEvent(eventId)
|
||||
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
|
||||
|
||||
if (!offer) {
|
||||
throw new Error(`Offer ${eventId} not found in the database.`)
|
||||
}
|
||||
|
||||
// Add the raw flag data to the database model.
|
||||
offer.flags.push(flagData)
|
||||
|
||||
// If flag count is 3 or more, mark the Offer as NSFW
|
||||
const flagCnt = offer.flags.length
|
||||
if (flagCnt >= 3) {
|
||||
offer.nsfw = true
|
||||
}
|
||||
|
||||
// Save the updated offer data to the database.
|
||||
await offer.save()
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error in flagOffer(): ', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Add the raw flag data to the database model.
|
||||
offer.flags.push(flagData)
|
||||
// Get offers data from nostr.
|
||||
async loadOffers () {
|
||||
try {
|
||||
// Retrieve offers array.
|
||||
const offers = await this.adapters.nostr.read()
|
||||
// console.log('offers: ', offers)
|
||||
|
||||
// If flag count is 3 or more, mark the Offer as NSFW
|
||||
const flagCnt = offer.flags.length
|
||||
if (flagCnt >= 3) {
|
||||
offer.nsfw = true
|
||||
for (let i = 0; i < offers.length; i++) {
|
||||
try {
|
||||
const offer = offers[i]
|
||||
|
||||
// offer data
|
||||
const offerObj = JSON.parse(offer.content)
|
||||
|
||||
// Append the Nostr Event ID to the offer object
|
||||
offerObj.data.nostrEventId = offer.eventId
|
||||
// console.log('loadOffers() offerObj: ', offerObj)
|
||||
|
||||
if (offerObj.data.dataType === 'offer') {
|
||||
// Try to create new offer
|
||||
await this.createOffer(offerObj)
|
||||
}
|
||||
|
||||
if (offerObj.data.dataType === 'counter-offer') {
|
||||
// console.log('Counter offer detected: ', offerObj)
|
||||
// console.log('Counter offer detected: ', offerObj.data.nostrEventId)
|
||||
console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
|
||||
await this.acceptCounterOffer(offerObj)
|
||||
}
|
||||
|
||||
// Ignore the post if it doesn't fit the above filters.
|
||||
} catch (error) {
|
||||
/* exit quietly */
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in loadOffers(): ', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Save the updated offer data to the database.
|
||||
await offer.save()
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+80
-20
@@ -25,58 +25,88 @@ class OrderLib {
|
||||
// Encapsulate dependencies
|
||||
this.orderEntity = new OrderEntity()
|
||||
this.OrderModel = this.adapters.localdb.Order
|
||||
this.UserModel = this.adapters.localdb.Users
|
||||
this.bch = this.adapters.bch
|
||||
this.config = config
|
||||
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
|
||||
|
||||
// Bind subfunctions to the 'this' object.
|
||||
this.ensureFunds = this.ensureFunds.bind(this)
|
||||
this.createOrder = this.createOrder.bind(this)
|
||||
this.findOrderByUtxo = this.findOrderByUtxo.bind(this)
|
||||
}
|
||||
|
||||
// Create a new order model and add it to the Mongo database.
|
||||
async createOrder (entryObj) {
|
||||
try {
|
||||
console.log('createOrder(entryObj): ', entryObj)
|
||||
// console.log('createOrder(entryObj): ', entryObj)
|
||||
if (!entryObj) return false
|
||||
|
||||
if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
|
||||
|
||||
const user = await this.UserModel.findById(entryObj.userId)
|
||||
if (!user) throw new Error('user not found')
|
||||
|
||||
console.log(`Using FullStack.cash: ${this.config.useFullStackCash}`)
|
||||
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 with the user's mnemonic.
|
||||
const BchWallet = this.adapters.wallet.BchWallet
|
||||
const userWallet = new BchWallet(user.mnemonic, advancedConfig)
|
||||
|
||||
// Wait for wallet to initialize.
|
||||
await userWallet.walletInfoPromise
|
||||
await userWallet.initialize()
|
||||
|
||||
// Specify the address to send payment.
|
||||
entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress
|
||||
console.log('userWallet.walletInfo: ', userWallet.walletInfo)
|
||||
entryObj.makerAddr = userWallet.walletInfo.cashAddress
|
||||
console.log('entryObj.makerAddr: ', entryObj.makerAddr)
|
||||
// console.log('entryObj.makerAddr: ', entryObj.makerAddr)
|
||||
|
||||
// Input Validation
|
||||
const orderEntity = this.orderEntity.inputValidate(entryObj)
|
||||
console.log('orderEntity: ', orderEntity)
|
||||
// console.log('orderEntity: ', orderEntity)
|
||||
|
||||
// Optimize the wallet to speed up working with it.
|
||||
console.log('Optimizing wallet before creating new order.')
|
||||
await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
|
||||
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
|
||||
await userWallet.optimize()
|
||||
|
||||
// Ensure sufficient tokens exist to create the order.
|
||||
// await this.ensureFunds(orderEntity)
|
||||
await this.retryQueue.addToQueue(this.ensureFunds, orderEntity)
|
||||
// await this.retryQueue.addToQueue(this.ensureFunds, orderEntity)
|
||||
|
||||
// Get Ticker for token ID.
|
||||
// const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId])
|
||||
const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTxData, [entryObj.tokenId])
|
||||
// const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTxData, [entryObj.tokenId])
|
||||
const tokenData = await userWallet.getTxData([entryObj.tokenId])
|
||||
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
||||
orderEntity.ticker = tokenData[0].tokenTicker
|
||||
|
||||
// Move the tokens to holding address.
|
||||
const moveObj = {
|
||||
tokenId: orderEntity.tokenId,
|
||||
qty: orderEntity.numTokens
|
||||
qty: orderEntity.numTokens,
|
||||
wallet: userWallet
|
||||
}
|
||||
// const utxoInfo = await this.adapters.wallet.moveTokens(moveObj)
|
||||
const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokens, moveObj)
|
||||
const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokensFromCustomWallet, moveObj)
|
||||
// console.log('utxoInfo: ', utxoInfo)
|
||||
|
||||
// Update the UTXO store for the wallet.
|
||||
await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000)
|
||||
await userWallet.bchjs.Util.sleep(3000)
|
||||
// await this.adapters.wallet.bchWallet.getUtxos()
|
||||
await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
|
||||
|
||||
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
|
||||
await userWallet.initialize()
|
||||
// Update the order with the new UTXO information.
|
||||
orderEntity.utxoTxid = utxoInfo.txid
|
||||
orderEntity.utxoVout = utxoInfo.vout
|
||||
@@ -102,10 +132,12 @@ class OrderLib {
|
||||
const order = new this.OrderModel(orderEntity)
|
||||
await order.save()
|
||||
|
||||
return eventId
|
||||
const noteId = this.adapters.nostr.eventId2note(eventId)
|
||||
|
||||
return { eventId, noteId }
|
||||
} catch (err) {
|
||||
// console.log("Error in use-cases/entry.js/createEntry()", err.message)
|
||||
wlogger.error('Error in use-cases/order.js/createOrder())')
|
||||
wlogger.error('Error in use-cases/order.js/createOrder(): ', err)
|
||||
console.log('error entryObj: ', entryObj)
|
||||
throw err
|
||||
}
|
||||
@@ -129,7 +161,7 @@ class OrderLib {
|
||||
|
||||
// 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 (orderEntity.buyOrSell.includes('sell')) {
|
||||
// Sell Order
|
||||
@@ -155,7 +187,7 @@ class OrderLib {
|
||||
)
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
} else {
|
||||
// Buy Order
|
||||
throw new Error('Buy orders are not supported yet.')
|
||||
@@ -191,6 +223,34 @@ class OrderLib {
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve an Order model from the database. Find it by its UTXO (TXID & Vout)
|
||||
async findOrderByUtxo (offerData = {}) {
|
||||
try {
|
||||
// console.log('findOrderByUtxo() offerData: ', offerData)
|
||||
|
||||
// const order = await this.OrderModel.findOne({ nostrEventId })
|
||||
const order = await this.OrderModel.findOne({ utxoTxid: offerData.data.utxoTxid, utxoVout: offerData.data.utxoVout })
|
||||
// const order = await this.OrderModel.findOne({ utxoTxid: offerData.data.utxoTxid })
|
||||
// const order = await this.OrderModel.findOne({ tokenId: offerData.data.tokenId })
|
||||
console.log('findOrderByUtxo() order: ', order)
|
||||
|
||||
if (!order) {
|
||||
throw new Error('order not found')
|
||||
}
|
||||
if (!order.nostrEventId) {
|
||||
throw new Error('order not found')
|
||||
}
|
||||
|
||||
const orderObject = order.toObject()
|
||||
// return this.offerEntity.validateFromModel(offerObject)
|
||||
|
||||
return orderObject
|
||||
} catch (err) {
|
||||
console.error('Error in findOrderByUtxo(): ', 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.
|
||||
@@ -201,7 +261,7 @@ class OrderLib {
|
||||
|
||||
// Get all Orders in the database.
|
||||
const orders = await this.OrderModel.find({})
|
||||
console.log('orders: ', orders)
|
||||
// console.log('orders: ', orders)
|
||||
|
||||
// Loop through each Order and ensure the UTXO is still valid.
|
||||
for (let i = 0; i < orders.length; i++) {
|
||||
@@ -239,7 +299,7 @@ class OrderLib {
|
||||
|
||||
// If the Order UTXO is spent, delete the Order model.
|
||||
if (utxoStatus === false) {
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
// console.log('utxoStatus: ', utxoStatus)
|
||||
console.log(`Spent UTXO detected. Deleting this Order: ${JSON.stringify(thisOrder, null, 2)}`)
|
||||
await thisOrder.remove()
|
||||
}
|
||||
@@ -254,11 +314,11 @@ class OrderLib {
|
||||
try {
|
||||
const data = await this.OrderModel.find({})
|
||||
|
||||
// Sort entries so newest entries show first.
|
||||
// Sort entries so newest entries show first.
|
||||
.sort('-timestamp')
|
||||
// Skip to the start of the selected page.
|
||||
// Skip to the start of the selected page.
|
||||
.skip(page * DEFAULT_ENTRIES_PER_PAGE)
|
||||
// Only return 20 results.
|
||||
// Only return 20 results.
|
||||
.limit(DEFAULT_ENTRIES_PER_PAGE)
|
||||
|
||||
return data
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
Use Case library for tracking usage. This library contains business logic
|
||||
for tracking the usage of REST API and JSON RPC calls. This library is used
|
||||
by admins to keep an eye on how many API calls were made in a 24-hour and
|
||||
1-hour time period.
|
||||
*/
|
||||
|
||||
// This global variable is used to share data between the REST middleware and
|
||||
// the Usage Use Case class instance.
|
||||
let restCalls = []
|
||||
|
||||
class UsageUseCases {
|
||||
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 Usage Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.cleanUsage = this.cleanUsage.bind(this)
|
||||
this.getRestSummary = this.getRestSummary.bind(this)
|
||||
this.getTopIps = this.getTopIps.bind(this)
|
||||
this.getTopEndpoints = this.getTopEndpoints.bind(this)
|
||||
|
||||
// State
|
||||
}
|
||||
|
||||
// Clean up the state by removing entries that are older than 24 hours. This
|
||||
// ensures stats reflect only the last 24 hours.
|
||||
// This function is called by a Timer Controller.
|
||||
cleanUsage () {
|
||||
try {
|
||||
const now = new Date()
|
||||
const twentyFourHoursAgo = now.getTime() - (60000 * 60 * 24)
|
||||
|
||||
console.log('cleanUsage() now: ', now)
|
||||
console.log('cleanUsage() restCalls.length before filtering: ', restCalls.length)
|
||||
restCalls = restCalls.filter(x => x.timestamp > twentyFourHoursAgo)
|
||||
console.log('cleanUsage() restCalls.length after filtering: ', restCalls.length)
|
||||
|
||||
return restCalls
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/cleanUsage()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Track the calls to a REST API
|
||||
getRestSummary (inObj = {}) {
|
||||
try {
|
||||
console.log(`getRestSummary(): There have been ${restCalls.length} REST calls`)
|
||||
|
||||
return restCalls.length
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/getRestSummary()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the top 20 IP addresses from the stats.
|
||||
getTopIps () {
|
||||
try {
|
||||
const ips = restCalls.map(x => x.ip)
|
||||
// Create a Map to count occurrences of each IP address string
|
||||
const countMap = new Map()
|
||||
ips.forEach(ip => {
|
||||
countMap.set(ip, (countMap.get(ip) || 0) + 1)
|
||||
})
|
||||
|
||||
// Convert the Map into an array of objects with `str` and `cnt` properties
|
||||
const result = Array.from(countMap, ([ip, cnt]) => ({ ip, cnt }))
|
||||
|
||||
// Sort the results by the `cnt` property in descending order
|
||||
result.sort((a, b) => b.cnt - a.cnt)
|
||||
|
||||
// Ensure the result has at most 20 elements
|
||||
return result.slice(0, 20)
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/getTopIps()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the top 20 most consumed endpoints.
|
||||
getTopEndpoints () {
|
||||
try {
|
||||
const endpoints = restCalls.map(x => `${x.method} ${x.url}`)
|
||||
|
||||
// Create a Map to count occurrences of each IP address string
|
||||
const countMap = new Map()
|
||||
endpoints.forEach(endpoint => {
|
||||
countMap.set(endpoint, (countMap.get(endpoint) || 0) + 1)
|
||||
})
|
||||
|
||||
// Convert the Map into an array of objects with `str` and `cnt` properties
|
||||
const result = Array.from(countMap, ([endpoint, cnt]) => ({ endpoint, cnt }))
|
||||
|
||||
// Sort the results by the `cnt` property in descending order
|
||||
result.sort((a, b) => b.cnt - a.cnt)
|
||||
|
||||
// Ensure the result has at most 20 elements
|
||||
return result.slice(0, 20)
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/getTopEndpoints()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This Koa middleware is called any time there is a REST API. It logs the
|
||||
// details from the request object.
|
||||
function usageMiddleware () {
|
||||
return async (ctx, next) => {
|
||||
try {
|
||||
await next()
|
||||
|
||||
// console.log('ctx.request: ', ctx.request)
|
||||
const now = new Date()
|
||||
|
||||
const reqObj = {
|
||||
ip: ctx.request.ip,
|
||||
url: ctx.request.url,
|
||||
method: ctx.request.method,
|
||||
timestamp: now.getTime()
|
||||
}
|
||||
// console.log('reqObj: ', reqObj)
|
||||
|
||||
restCalls.push(reqObj)
|
||||
} catch (err) {
|
||||
ctx.status = err.status || 500
|
||||
ctx.body = err.message
|
||||
ctx.app.emit('error', err, ctx)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { UsageUseCases, usageMiddleware, restCalls }
|
||||
+10
-1
@@ -16,7 +16,7 @@ class UserLib {
|
||||
'Instance of adapters must be passed in when instantiating User Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
this.BchWallet = this.adapters.wallet.BchWallet
|
||||
// Encapsulate dependencies
|
||||
this.UserEntity = new UserEntity()
|
||||
this.UserModel = this.adapters.localdb.Users
|
||||
@@ -30,6 +30,11 @@ class UserLib {
|
||||
const userEntity = this.UserEntity.validate(userObj)
|
||||
const user = new this.UserModel(userEntity)
|
||||
|
||||
const wallet = new this.BchWallet()
|
||||
const walletInfo = await wallet.walletInfoPromise
|
||||
const mnemonic = walletInfo.mnemonic
|
||||
user.mnemonic = mnemonic
|
||||
|
||||
// Enforce default value of 'user'
|
||||
user.type = 'user'
|
||||
// console.log('user: ', user)
|
||||
@@ -102,6 +107,7 @@ class UserLib {
|
||||
|
||||
// Input Validation
|
||||
// Optional inputs, but they must be strings if included.
|
||||
|
||||
if (newData.email && typeof newData.email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
@@ -111,6 +117,9 @@ class UserLib {
|
||||
if (newData.password && typeof newData.password !== 'string') {
|
||||
throw new Error("Property 'password' must be a string!")
|
||||
}
|
||||
if (newData.mnemonic) {
|
||||
throw new Error("Property 'mnemonic' cannot be updated!")
|
||||
}
|
||||
|
||||
// Save a copy of the original user type.
|
||||
const userType = existingUser.type
|
||||
|
||||
@@ -173,6 +173,60 @@ if (!config.noMongo) {
|
||||
)
|
||||
assert.property(result.data, 'token', 'Token property exists.')
|
||||
assert.equal(result.data.user.type, 'user')
|
||||
assert.property(result.data.user, 'mnemonic')
|
||||
})
|
||||
it('should reject signup when DISABLE_NEW_ACCOUNTS is true', async () => {
|
||||
try {
|
||||
process.env.DISABLE_NEW_ACCOUNTS = true
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
email: 'test2@test.com',
|
||||
password: 'supersecretpassword',
|
||||
name: 'test3'
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
it('admin can create a user when DISABLE_NEW_ACCOUNTS is true', async () => {
|
||||
process.env.DISABLE_NEW_ACCOUNTS = true
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.adminJWT}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'fromAdmin@test.com',
|
||||
password: 'supersecretpassword',
|
||||
name: 'test3'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
|
||||
context.user = result.data.user
|
||||
context.token = result.data.token
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'fromAdmin@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
assert.property(result.data, 'token', 'Token property exists.')
|
||||
assert.equal(result.data.user.type, 'user')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -265,7 +319,7 @@ if (!config.noMongo) {
|
||||
const users = result.data.users
|
||||
// console.log(`users: ${util.inspect(users)}`)
|
||||
|
||||
assert.hasAnyKeys(users[0], ['type', '_id', 'email'])
|
||||
assert.hasAnyKeys(users[0], ['type', '_id', 'email', 'mnemonic'])
|
||||
assert.isNumber(users.length)
|
||||
})
|
||||
|
||||
@@ -375,6 +429,7 @@ if (!config.noMongo) {
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'mnemonic')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
|
||||
@@ -62,6 +62,7 @@ describe('Admin', () => {
|
||||
describe('createSystemUser()', () => {
|
||||
it('should create admin', async () => {
|
||||
try {
|
||||
await uut.deleteExistingSystemUser()
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
assert.property(result, 'email')
|
||||
@@ -72,44 +73,58 @@ describe('Admin', () => {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
it('should update admin password', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
const error2 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 500
|
||||
}
|
||||
// The loginAdmin() function in some use cases is recursive
|
||||
// after handling the 422 error, it gets called again
|
||||
sandbox
|
||||
.stub(uut.axios, 'request')
|
||||
.onFirstCall()
|
||||
.throws(error1)
|
||||
.onSecondCall()
|
||||
.throws(error2)
|
||||
uut.config.adminPassword = 'newpassword'
|
||||
|
||||
await uut.createSystemUser()
|
||||
const fakeUser = {
|
||||
password: 'oldpassword',
|
||||
save: () => { return 'token' },
|
||||
generateToken: () => { return 'token' }
|
||||
}
|
||||
|
||||
sandbox.stub(uut.User, 'findOne').resolves(fakeUser)
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
|
||||
assert.equal(fakeUser.password, 'newpassword', 'password should be updated')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.User, 'findOne').throws(new Error('test error'))
|
||||
await uut.createSystemUser()
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle errors when remove user', async () => {
|
||||
describe('deleteExistingSystemUser()', () => {
|
||||
it('should delete admin', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
sandbox.stub(uut.axios, 'request').throws(error1)
|
||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
|
||||
await uut.createSystemUser()
|
||||
sandbox.stub(uut.User, 'deleteOne').resolves(true)
|
||||
const result = await uut.deleteExistingSystemUser()
|
||||
assert.isTrue(result)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error when deleting admin', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
await uut.deleteExistingSystemUser()
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
End-to-end tests for /usage endpoints.
|
||||
*/
|
||||
|
||||
import config from '../../../config/index.js'
|
||||
import { assert } from 'chai'
|
||||
import axios from 'axios'
|
||||
import sinon from 'sinon'
|
||||
import util from 'util'
|
||||
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
let sandbox
|
||||
|
||||
describe('Usage', () => {
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('GET /usage', () => {
|
||||
it('should return usage status', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'get',
|
||||
url: `${LOCALHOST}/usage`
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.property(result.data, 'status')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /usage/ips', () => {
|
||||
it('should return ips', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'get',
|
||||
url: `${LOCALHOST}/usage/ips`
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.property(result.data, 'ips')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /usage/endpoints', () => {
|
||||
it('should return ips', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'get',
|
||||
url: `${LOCALHOST}/usage/endpoints`
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.property(result.data, 'endpoints')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,7 @@
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import { peerIdFromString } from '@libp2p/peer-id'
|
||||
// import { peerIdFromString } from '@libp2p/peer-id'
|
||||
|
||||
// Local libraries
|
||||
import IPFSLib from '../../../src/adapters/ipfs/ipfs.js'
|
||||
@@ -165,17 +165,17 @@ describe('#IPFS-adapter', () => {
|
||||
uut.config.isCircuitRelay = false
|
||||
|
||||
// Mock dependencies and force desired code path.
|
||||
let beenCalled = false
|
||||
sandbox.stub(uut, 'getKeychain').resolves({
|
||||
exportPeerId: async () => {
|
||||
if (!beenCalled) {
|
||||
beenCalled = true
|
||||
throw new Error('test error')
|
||||
}
|
||||
return peerIdFromString('12D3KooWSXF1PnEfiA8bCG8SJduCvzdwHtvhVPK4WC6zzDoto2XP')
|
||||
},
|
||||
createKey: async () => {}
|
||||
})
|
||||
// const beenCalled = false
|
||||
// sandbox.stub(uut, 'getKeychain').resolves({
|
||||
// exportPeerId: async () => {
|
||||
// if (!beenCalled) {
|
||||
// beenCalled = true
|
||||
// throw new Error('test error')
|
||||
// }
|
||||
// return peerIdFromString('12D3KooWSXF1PnEfiA8bCG8SJduCvzdwHtvhVPK4WC6zzDoto2XP')
|
||||
// },
|
||||
// createKey: async () => {}
|
||||
// })
|
||||
sandbox.stub(uut, 'createLibp2p').resolves()
|
||||
sandbox.stub(uut, 'createHelia').resolves({})
|
||||
|
||||
|
||||
@@ -47,7 +47,15 @@ describe('#OrderPagination', () => {
|
||||
timestamp: 'timestamp',
|
||||
localTimestamp: 'localTimestamp',
|
||||
txid: 'txid',
|
||||
p2wdbHash: 'p2wdbHash'
|
||||
p2wdbHash: 'p2wdbHash',
|
||||
hdIndex: 0,
|
||||
nostrEventId: 'nostrEventId',
|
||||
dataType: 'dataType',
|
||||
makerAddr: 'makerAddr',
|
||||
rateInBaseUnit: 'rateInBaseUnit',
|
||||
minUnitsToExchange: 'minUnitsToExchange',
|
||||
ticker: 'ticker',
|
||||
tokenType: 1
|
||||
})
|
||||
await order.save()
|
||||
})
|
||||
|
||||
@@ -31,7 +31,8 @@ describe('#User-Adapter', () => {
|
||||
testuser = new User({
|
||||
email: 'test983@test.com',
|
||||
name: 'test983',
|
||||
password: 'password'
|
||||
password: 'password',
|
||||
mnemonic: '12 words mnemonic'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
|
||||
/*
|
||||
Unit tests for the adapter/wallet.js library.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import WalletAdapter from '../../../src/adapters/wallet.js'
|
||||
import { MockBchWallet } from '../mocks/adapters/wallet.js'
|
||||
|
||||
describe('#wallet', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new WalletAdapter()
|
||||
uut.bchWallet = new MockBchWallet()
|
||||
uut.bitcoinJs = {
|
||||
Transaction: {
|
||||
fromHex: () => { return { } },
|
||||
SIGHASH_ALL: () => { return { } }
|
||||
},
|
||||
TransactionBuilder: {
|
||||
fromTransaction: () => { return { sign: () => { return { } }, build: () => { return { toHex: () => { return 'hex' } } } } }
|
||||
},
|
||||
ECPair: {
|
||||
fromWIF: () => { return { } }
|
||||
}
|
||||
}
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#moveTokensFromCustomWallet', () => {
|
||||
it('should move tokens from a custom wallet', async () => {
|
||||
sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
cashAddress: 'cashAddress',
|
||||
wif: 'wif',
|
||||
hdIndex: 11
|
||||
})
|
||||
|
||||
const customWallet = new MockBchWallet()
|
||||
customWallet.sendTokens = () => { return 'move token tx id result' }
|
||||
customWallet.utxos.utxoStore.slpUtxos.type1.tokens = [
|
||||
{
|
||||
tokenId: 'tokenId',
|
||||
tokenType: 1,
|
||||
qty: 1
|
||||
}
|
||||
]
|
||||
const inObj = {
|
||||
qty: 1,
|
||||
wallet: customWallet,
|
||||
tokenId: 'tokenId'
|
||||
}
|
||||
const result = await uut.moveTokensFromCustomWallet(inObj)
|
||||
assert.equal(result.txid, 'move token tx id result')
|
||||
assert.equal(result.hdIndex, 11)
|
||||
assert.equal(result.tokenType, 1)
|
||||
assert.equal(result.vout, 1)
|
||||
})
|
||||
it('should throw an error if the wallet is not provided', async () => {
|
||||
try {
|
||||
const inObj = {
|
||||
qty: 1,
|
||||
tokenId: 'tokenId'
|
||||
}
|
||||
await uut.moveTokensFromCustomWallet(inObj)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'wallet is required!')
|
||||
}
|
||||
})
|
||||
it('should throw an error if the tokenId is not provided', async () => {
|
||||
try {
|
||||
const inObj = {
|
||||
qty: 1,
|
||||
wallet: new MockBchWallet()
|
||||
}
|
||||
await uut.moveTokensFromCustomWallet(inObj)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'tokenId must be a string!')
|
||||
}
|
||||
})
|
||||
it('should throw an error if the qty is not provided', async () => {
|
||||
try {
|
||||
const inObj = {
|
||||
tokenId: 'tokenId',
|
||||
wallet: new MockBchWallet()
|
||||
}
|
||||
await uut.moveTokensFromCustomWallet(inObj)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'qty must be a number!')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#completeTx', () => {
|
||||
it('should complete a transaction', async () => {
|
||||
sandbox.stub(uut, 'getKeyPair').resolves({
|
||||
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
wif: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
|
||||
hdIndex: 11
|
||||
})
|
||||
sandbox.stub(uut, 'deseralizeTx').resolves({
|
||||
txid: 'complete tx id result'
|
||||
})
|
||||
sandbox.stub(uut.retryQueue, 'addToQueue').resolves('complete tx id result')
|
||||
|
||||
const hex = 'hex'
|
||||
const hdIndex = 11
|
||||
const txid = await uut.completeTx(hex, hdIndex)
|
||||
assert.equal(txid, 'complete tx id result')
|
||||
})
|
||||
it('should throw an error if the hex is not provided', async () => {
|
||||
try {
|
||||
const hdIndex = 11
|
||||
await uut.completeTx(null, hdIndex)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'hex must be a string!')
|
||||
}
|
||||
})
|
||||
it('should throw an error if the hdIndex is not provided', async () => {
|
||||
try {
|
||||
const hex = 'hex'
|
||||
await uut.completeTx(hex, null)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'hdIndex must be a non-negative number!')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Unit tests for the REST API middleware that handle response errors.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local libraries
|
||||
import errorMiddleware from '../../../../../src/controllers/rest-api/middleware/error.js'
|
||||
import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js'
|
||||
|
||||
describe('#Validators', () => {
|
||||
let ctx
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#errorMiddleware', () => {
|
||||
it('should run next function', async () => {
|
||||
// Spy on next
|
||||
const next = sinon.spy(() => { })
|
||||
errorMiddleware()(ctx, next)
|
||||
|
||||
assert.isTrue(next.calledOnce)
|
||||
})
|
||||
|
||||
it('should handle unknown status error', async () => {
|
||||
try {
|
||||
const next = async () => {
|
||||
const e = new Error('test error')
|
||||
e.status = null
|
||||
throw e
|
||||
}
|
||||
|
||||
await errorMiddleware()(ctx, next)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(ctx.status, 500)
|
||||
assert.equal(ctx.body, 'test error')
|
||||
}
|
||||
})
|
||||
it('should handle known status error', async () => {
|
||||
try {
|
||||
const next = async () => {
|
||||
const e = new Error('test error')
|
||||
e.status = 422
|
||||
throw e
|
||||
}
|
||||
|
||||
await errorMiddleware()(ctx, next)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(ctx.status, 422)
|
||||
assert.equal(ctx.body, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -65,13 +65,17 @@ describe('#Order-REST-Router', () => {
|
||||
ctx.request.body = {
|
||||
order: {}
|
||||
}
|
||||
ctx.state.user = {
|
||||
id: 'testUserId'
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.useCases.order, 'createOrder').resolves('testHash')
|
||||
sandbox.stub(uut.useCases.order, 'createOrder').resolves({ eventId: 'testEventId', noteId: 'testNoteId' })
|
||||
|
||||
await uut.createOrder(ctx)
|
||||
|
||||
assert.equal(ctx.body.hash, 'testHash')
|
||||
assert.equal(ctx.body.eventId, 'testEventId')
|
||||
assert.equal(ctx.body.noteId, 'testNoteId')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
@@ -79,6 +83,9 @@ describe('#Order-REST-Router', () => {
|
||||
ctx.request.body = {
|
||||
order: {}
|
||||
}
|
||||
ctx.state.user = {
|
||||
id: 'testUserId'
|
||||
}
|
||||
|
||||
// Force an error
|
||||
sandbox
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /usage endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local support libraries
|
||||
import adapters from '../../../mocks/adapters/index.js'
|
||||
import UseCasesMock from '../../../mocks/use-cases/index.js'
|
||||
import UsageController from '../../../../../src/controllers/rest-api/usage/controller.js'
|
||||
|
||||
import { context as mockContext } from '../../../mocks/ctx-mock.js'
|
||||
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
describe('#Usage-REST-Controller', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new UsageController({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageController()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageController({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#Get /usage', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.useCases.usage, 'getRestSummary').throws(new Error('test error'))
|
||||
await uut.getStatus(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
await uut.getStatus(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'status')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#Get /ips', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.useCases.usage, 'getTopIps').throws(new Error('test error'))
|
||||
await uut.getTopIps(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
await uut.getTopIps(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'ips')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#Get /endpoints', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.useCases.usage, 'getTopEndpoints').throws(new Error('test error'))
|
||||
await uut.getTopEndpoints(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
await uut.getTopEndpoints(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'endpoints')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#handleError', () => {
|
||||
it('should pass an error message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 422,
|
||||
message: 'Unprocessable Entity'
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Unprocessable Entity')
|
||||
}
|
||||
})
|
||||
it('should still throw error if there is no message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 404
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Not Found')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /usage endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local support libraries
|
||||
import adapters from '../../../mocks/adapters/index.js'
|
||||
|
||||
import UseCasesMock from '../../../mocks/use-cases/index.js'
|
||||
|
||||
import UsageRouter from '../../../../../src/controllers/rest-api/usage/index.js'
|
||||
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Usage-REST-Router', () => {
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new UsageRouter({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageRouter()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageRouter({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#attach', () => {
|
||||
it('should throw an error if app is not passed in.', () => {
|
||||
try {
|
||||
uut.attach()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -56,27 +56,85 @@ describe('#Timer-Controllers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// describe('#startTimers', () => {
|
||||
// it('should start the timers', () => {
|
||||
// const result = uut.startTimers()
|
||||
//
|
||||
// // uut.stopTimers()
|
||||
//
|
||||
// assert.equal(result, true)
|
||||
// })
|
||||
// })
|
||||
describe('#startTimers', () => {
|
||||
it('should start the timers', () => {
|
||||
const result = uut.startTimers()
|
||||
|
||||
// describe('#exampleTimerFunc', () => {
|
||||
// it('should kick off the Use Case', async () => {
|
||||
// const result = await uut.exampleTimerFunc()
|
||||
//
|
||||
// assert.equal(result, true)
|
||||
// })
|
||||
//
|
||||
// it('should return false on error', async () => {
|
||||
// const result = await uut.exampleTimerFunc(true)
|
||||
//
|
||||
// assert.equal(result, false)
|
||||
// })
|
||||
// })
|
||||
// uut.stopTimers()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#gcOrders', () => {
|
||||
it('should kick off the Use Case', async () => {
|
||||
const result = await uut.gcOrders()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return false on error', async () => {
|
||||
sandbox.stub(uut.useCases.order, 'removeStaleOrders').throws(new Error('test error'))
|
||||
const result = await uut.gcOrders()
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
})
|
||||
describe('#gcOffers', () => {
|
||||
it('should kick off the Use Case', async () => {
|
||||
const result = await uut.gcOffers()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return false on error', async () => {
|
||||
sandbox.stub(uut.useCases.offer, 'removeStaleOffers').throws(new Error('test error'))
|
||||
const result = await uut.gcOffers()
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
})
|
||||
describe('#checkDupOffers', () => {
|
||||
it('should kick off the Use Case', async () => {
|
||||
const result = await uut.checkDupOffers()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return false on error', async () => {
|
||||
sandbox.stub(uut.useCases.offer, 'removeDuplicateOffers').throws(new Error('test error'))
|
||||
const result = await uut.checkDupOffers()
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
})
|
||||
describe('#loadOffers', () => {
|
||||
it('should kick off the Use Case', async () => {
|
||||
const result = await uut.loadOffers()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return false on error', async () => {
|
||||
sandbox.stub(uut.useCases.offer, 'loadOffers').throws(new Error('test error'))
|
||||
const result = await uut.loadOffers()
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#cleanUsage', () => {
|
||||
it('should kick off the Use Case', async () => {
|
||||
const result = await uut.cleanUsage()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return false on error', async () => {
|
||||
sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error'))
|
||||
const result = await uut.cleanUsage()
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -237,6 +237,36 @@ describe('#Offer-Entity', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if nostrEventId is not included', () => {
|
||||
try {
|
||||
const offerData = {
|
||||
data: {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy',
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 350,
|
||||
numTokens: 1,
|
||||
utxoTxid: 'fakeTxid',
|
||||
utxoVout: 0,
|
||||
offerStatus: 'posted',
|
||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
tokenType: 1
|
||||
}
|
||||
}
|
||||
uut.validate(offerData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'nostrEventId' must be a string."
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should validate a new offer', () => {
|
||||
const offerObj = {
|
||||
appId: 'swapTest555',
|
||||
@@ -255,7 +285,8 @@ describe('#Offer-Entity', () => {
|
||||
offerStatus: 'posted',
|
||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
ticker: 'TROUT',
|
||||
tokenType: 1
|
||||
tokenType: 1,
|
||||
nostrEventId: 'test'
|
||||
},
|
||||
timestamp: '2021-09-20T17:54:26.395Z',
|
||||
localTimeStamp: '9/20/2021, 10:54:26 AM',
|
||||
|
||||
@@ -150,8 +150,14 @@ const wallet = {
|
||||
return { cashAddress: 'fakeAddr', wif: 'fakeWif', hdIndex: 1 }
|
||||
},
|
||||
bchWallet: new MockBchWallet(),
|
||||
BchWallet: MockBchWallet,
|
||||
moveTokens: async () => {},
|
||||
reclaimTokens: async ()=>{}
|
||||
moveTokensFromCustomWallet: async () => {},
|
||||
moveBch: async () => {},
|
||||
reclaimTokens: async ()=>{},
|
||||
generatePartialTx: async ()=>{},
|
||||
deseralizeTx:async ()=>{},
|
||||
completeTx:async ()=>{ return ''},
|
||||
}
|
||||
|
||||
const p2wdb = {
|
||||
@@ -162,7 +168,8 @@ const p2wdb = {
|
||||
|
||||
const nostr = {
|
||||
post: async () => {return true },
|
||||
read: async () => { return true }
|
||||
read: async () => { return true },
|
||||
eventId2note: () => { return 'testNoteId' }
|
||||
}
|
||||
|
||||
export default { ipfs, localdb, bch, wallet, p2wdb, bchjs, nostr}
|
||||
|
||||
@@ -24,6 +24,7 @@ class MockBchWallet {
|
||||
this.sendTokens = async () => {
|
||||
return 'fakeTxid';
|
||||
};
|
||||
this.utxoIsValid =async ()=>{}
|
||||
this.getUtxos = async () => { };
|
||||
this.getBalance = async () => { };
|
||||
this.listTokens = async () => { };
|
||||
|
||||
@@ -41,12 +41,42 @@ class Offer {
|
||||
async createOffer() {
|
||||
return {}
|
||||
}
|
||||
async removeStaleOffers(){
|
||||
|
||||
}
|
||||
async removeDuplicateOffers(){
|
||||
|
||||
}
|
||||
async loadOffers(){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
async createOrder() {
|
||||
return {}
|
||||
}
|
||||
async removeStaleOrders(){
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
class UsageUseCaseMock {
|
||||
async cleanUsage() {
|
||||
return {}
|
||||
}
|
||||
|
||||
async getRestSummary() {
|
||||
return true
|
||||
}
|
||||
|
||||
async getTopIps(params) {
|
||||
return true
|
||||
}
|
||||
|
||||
async getTopEndpoints(existingUser, newData) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class UseCasesMock {
|
||||
@@ -58,6 +88,7 @@ class UseCasesMock {
|
||||
entry = new EntryUseCaseMock()
|
||||
offer = new Offer()
|
||||
order = new Order()
|
||||
usage = new UsageUseCaseMock()
|
||||
}
|
||||
|
||||
export default UseCasesMock;
|
||||
|
||||
@@ -3,45 +3,45 @@
|
||||
*/
|
||||
|
||||
const nftOffer01 = {
|
||||
"messageType": 1,
|
||||
"messageClass": 1,
|
||||
"tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b",
|
||||
"buyOrSell": "sell",
|
||||
"rateInBaseUnit": 7672536,
|
||||
"minUnitsToExchange": 7672536,
|
||||
"numTokens": 1,
|
||||
"utxoTxid": "c736d73e274df20e9b069b4990d1a264fb80aa98d67cc6c7a39e42bff48e7c04",
|
||||
"utxoVout": 1,
|
||||
"timestamp": 1662930309998,
|
||||
"globaltimestamp": "2022-09-11T21:05:09.998Z",
|
||||
"localTimestamp": "9/11/2022, 9:05:09 PM",
|
||||
"txid": "4266862b8358664996038d8c29d49dc0f3d3058fd1ef3d567b084e6b16ceb5b2",
|
||||
"p2wdbHash": "zdpuArq7rCuVCGPyTnWpLjy9AYsh8yrbwjGdKwg2GDDALkM8t",
|
||||
"offerStatus": "posted",
|
||||
"makerAddr": "bitcoincash:qrqlz63cwmu0hcmsrfnd8jemn3atkpaqds6tf4ksrr",
|
||||
"ticker": "TV001",
|
||||
"tokenType": 65
|
||||
"messageType": 1,
|
||||
"messageClass": 1,
|
||||
"tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b",
|
||||
"buyOrSell": "sell",
|
||||
"rateInBaseUnit": 7672536,
|
||||
"minUnitsToExchange": 7672536,
|
||||
"numTokens": 1,
|
||||
"utxoTxid": "c736d73e274df20e9b069b4990d1a264fb80aa98d67cc6c7a39e42bff48e7c04",
|
||||
"utxoVout": 1,
|
||||
"timestamp": 1662930309998,
|
||||
"globaltimestamp": "2022-09-11T21:05:09.998Z",
|
||||
"localTimestamp": "9/11/2022, 9:05:09 PM",
|
||||
"txid": "4266862b8358664996038d8c29d49dc0f3d3058fd1ef3d567b084e6b16ceb5b2",
|
||||
"p2wdbHash": "zdpuArq7rCuVCGPyTnWpLjy9AYsh8yrbwjGdKwg2GDDALkM8t",
|
||||
"offerStatus": "posted",
|
||||
"makerAddr": "bitcoincash:qrqlz63cwmu0hcmsrfnd8jemn3atkpaqds6tf4ksrr",
|
||||
"ticker": "TV001",
|
||||
"tokenType": 65
|
||||
}
|
||||
|
||||
const nftTokenData01 = {
|
||||
"genesisData": {
|
||||
"type": 65,
|
||||
"ticker": "TV001",
|
||||
"name": "Introduction to NFTs on BCH",
|
||||
"tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b",
|
||||
"documentUri": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4",
|
||||
"documentHash": "c1731268f4873f1928438abdaf6ffc546d86a1817dc6f3c6bc73fbdfb4664f10",
|
||||
"decimals": 0,
|
||||
"mintBatonIsActive": false,
|
||||
"tokensInCirculationBN": "1",
|
||||
"tokensInCirculationStr": "1",
|
||||
"blockCreated": 740395,
|
||||
"totalBurned": "0",
|
||||
"totalMinted": "1",
|
||||
"parentGroupId": "030563ddd65772d8e9b79b825529ed53c7d27037507b57c528788612b4911107"
|
||||
},
|
||||
"immutableData": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4",
|
||||
"mutableData": "ipfs://bafybeifzhunfpodsztsj5x4ypopngkxuapbxwaxaxkahaybdktk6joqtlq"
|
||||
"genesisData": {
|
||||
"type": 65,
|
||||
"ticker": "TV001",
|
||||
"name": "Introduction to NFTs on BCH",
|
||||
"tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b",
|
||||
"documentUri": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4",
|
||||
"documentHash": "c1731268f4873f1928438abdaf6ffc546d86a1817dc6f3c6bc73fbdfb4664f10",
|
||||
"decimals": 0,
|
||||
"mintBatonIsActive": false,
|
||||
"tokensInCirculationBN": "1",
|
||||
"tokensInCirculationStr": "1",
|
||||
"blockCreated": 740395,
|
||||
"totalBurned": "0",
|
||||
"totalMinted": "1",
|
||||
"parentGroupId": "030563ddd65772d8e9b79b825529ed53c7d27037507b57c528788612b4911107"
|
||||
},
|
||||
"immutableData": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4",
|
||||
"mutableData": "ipfs://bafybeifzhunfpodsztsj5x4ypopngkxuapbxwaxaxkahaybdktk6joqtlq"
|
||||
}
|
||||
|
||||
const simpleNftOffer01 = {
|
||||
@@ -126,11 +126,57 @@ const fungibleTokenData01 = {
|
||||
"mutableData": ""
|
||||
}
|
||||
|
||||
const offerMockData = {
|
||||
data: {
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId:
|
||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
buyOrSell: 'sell',
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 10,
|
||||
numTokens: 0.02,
|
||||
utxoTxid:
|
||||
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
utxoVout: 0,
|
||||
makerAddr: 'address',
|
||||
tokenType: 1,
|
||||
nostrEventId: 'test'
|
||||
}
|
||||
}
|
||||
|
||||
const deserealizeTxMock = {
|
||||
//...
|
||||
vout: [
|
||||
{
|
||||
value: 0,
|
||||
scriptPubKey: {
|
||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
scriptPubKey: {
|
||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
scriptPubKey: {
|
||||
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
|
||||
}
|
||||
},
|
||||
//...
|
||||
]
|
||||
}
|
||||
|
||||
export default {
|
||||
nftOffer01,
|
||||
nftTokenData01,
|
||||
simpleNftOffer01,
|
||||
simpleNftTokenData01,
|
||||
fungibleOffer01,
|
||||
fungibleTokenData01
|
||||
fungibleTokenData01,
|
||||
offerMockData,
|
||||
deserealizeTxMock
|
||||
};
|
||||
|
||||
@@ -48,79 +48,70 @@ describe('#offer-use-case', () => {
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if order use cases are not passed in', () => {
|
||||
try {
|
||||
uut = new OfferLib({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
console.log(uut) // linter
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createOffer', () => {
|
||||
// it('should ignore an offer if utxo has been spent', async () => {
|
||||
// const offerObj = {
|
||||
// appId: 'swapTest555',
|
||||
// data: {
|
||||
// messageType: 1,
|
||||
// messageClass: 1,
|
||||
// tokenId:
|
||||
// '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
// buyOrSell: 'sell',
|
||||
// rateInSats: 1000,
|
||||
// minSatsToExchange: 10,
|
||||
// numTokens: 0.02,
|
||||
// utxoTxid:
|
||||
// '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
// utxoVout: 0
|
||||
// },
|
||||
// timestamp: '2021-09-20T17:54:26.395Z',
|
||||
// localTimeStamp: '9/20/2021, 10:54:26 AM',
|
||||
// txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
|
||||
// hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
|
||||
// }
|
||||
//
|
||||
// // Mock dependencies
|
||||
// // sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
|
||||
// sandbox.stub(uut.adapters.wallet.bchWallet, 'getTokenData').resolves({})
|
||||
// sandbox.stub(uut, 'categorizeToken').resolves('nft')
|
||||
// sandbox.stub(uut, 'detectNsfw').resolves(false)
|
||||
//
|
||||
// const result = await uut.createOffer(offerObj)
|
||||
// // console.log('result: ', result)
|
||||
//
|
||||
// assert.equal(result, false)
|
||||
// })
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
await uut.createOffer()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'Cannot read properties of undefined')
|
||||
}
|
||||
})
|
||||
|
||||
// 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',
|
||||
// tokenType: 1
|
||||
// },
|
||||
// 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.bchWallet, 'utxoIsValid').resolves(true)
|
||||
// sandbox.stub(uut, 'categorizeToken').resolves('fungible')
|
||||
// sandbox.stub(uut.adapters.wallet.bchWallet, 'getTokenData').resolves({})
|
||||
// sandbox.stub(uut, 'detectNsfw').resolves(false)
|
||||
//
|
||||
// const result = await uut.createOffer(offerObj)
|
||||
// // console.log('result: ', result)
|
||||
//
|
||||
// assert.equal(result, true)
|
||||
// })
|
||||
it('should return false if offer already exist', async () => {
|
||||
const offerObj = mockData.offerMockData
|
||||
|
||||
// Mock dependencies
|
||||
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
|
||||
sandbox.stub(uut, 'findOfferByTxid').resolves({})
|
||||
|
||||
const result = await uut.createOffer(offerObj)
|
||||
assert.isFalse(result)
|
||||
})
|
||||
|
||||
it('should return false for invalid utxo', async () => {
|
||||
const offerObj = mockData.offerMockData
|
||||
|
||||
// Mock dependencies
|
||||
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
|
||||
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
|
||||
sandbox.stub(uut.retryQueue, 'addToQueue').resolves(null)
|
||||
|
||||
const result = await uut.createOffer(offerObj)
|
||||
assert.isFalse(result)
|
||||
})
|
||||
|
||||
it('should create offer', async () => {
|
||||
const tokenDataMock = mockData.simpleNftTokenData01
|
||||
const offerObj = mockData.offerMockData
|
||||
|
||||
// Mock dependencies
|
||||
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
|
||||
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
|
||||
sandbox.stub(uut.retryQueue, 'addToQueue')
|
||||
.onCall(0).resolves({}) // Utxo Status call
|
||||
.onCall(1).resolves(tokenDataMock) // Token Data call
|
||||
.onCall(2).resolves(false) // detectNsfw call
|
||||
|
||||
const result = await uut.createOffer(offerObj)
|
||||
assert.isTrue(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#categorizeToken', () => {
|
||||
@@ -159,15 +150,29 @@ describe('#offer-use-case', () => {
|
||||
|
||||
assert.equal(result, 'fungible')
|
||||
})
|
||||
|
||||
it('should unknow type', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
const offerData = mockData.fungibleOffer01
|
||||
const tokenData = { genesisData: {} }
|
||||
|
||||
await uut.categorizeToken(offerData, tokenData)
|
||||
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'Unknown token type:')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#removeDuplicateOffers', () => {
|
||||
it('should remove duplicate entries and return true', async () => {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.OfferModel, 'find').resolves([
|
||||
{ p2wdbHash: 'a', remove: async () => {} },
|
||||
{ p2wdbHash: 'a', remove: async () => {} },
|
||||
{ p2wdbHash: 'b', remove: async () => {} }
|
||||
{ p2wdbHash: 'a', remove: async () => { } },
|
||||
{ p2wdbHash: 'a', remove: async () => { } },
|
||||
{ p2wdbHash: 'b', remove: async () => { } }
|
||||
])
|
||||
// sandbox.stub(uut.OfferModel, 'remove').resolves()
|
||||
|
||||
@@ -180,8 +185,8 @@ describe('#offer-use-case', () => {
|
||||
it('should return false if there are no duplicate entries', async () => {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.OfferModel, 'find').resolves([
|
||||
{ p2wdbHash: 'a', remove: async () => {} },
|
||||
{ p2wdbHash: 'b', remove: async () => {} }
|
||||
{ p2wdbHash: 'a', remove: async () => { } },
|
||||
{ p2wdbHash: 'b', remove: async () => { } }
|
||||
])
|
||||
// sandbox.stub(uut.OfferModel, 'remove').resolves()
|
||||
|
||||
@@ -190,5 +195,595 @@ describe('#offer-use-case', () => {
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
|
||||
// sandbox.stub(uut.OfferModel, 'remove').resolves()
|
||||
|
||||
await uut.removeDuplicateOffers()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#removeStaleOffers', () => {
|
||||
it('remove offer with wrong utxoState', async () => {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
|
||||
sandbox.stub(uut.retryQueue, 'addToQueue').resolves(false)
|
||||
|
||||
await uut.removeStaleOffers()
|
||||
})
|
||||
|
||||
it('remove offer with wrong txid', async () => {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
|
||||
sandbox.stub(uut.retryQueue, 'addToQueue').throws(new Error('txid needs to be a proper transaction ID'))
|
||||
|
||||
await uut.removeStaleOffers()
|
||||
})
|
||||
|
||||
it('remove expired offer ', async () => {
|
||||
const tsMock = new Date()
|
||||
tsMock.setMonth(tsMock.getMonth() - 3)
|
||||
|
||||
const timestamp = tsMock.getTime()
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.OfferModel, 'find').resolves([{ timestamp, remove: async () => { } }])
|
||||
sandbox.stub(uut.retryQueue, 'addToQueue').resolves(true)
|
||||
|
||||
await uut.removeStaleOffers()
|
||||
})
|
||||
|
||||
it('should handle axios error ', async () => {
|
||||
const testErr = new Error()
|
||||
testErr.isAxiosError = true
|
||||
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
|
||||
sandbox.stub(uut.retryQueue, 'addToQueue').throws(testErr)
|
||||
|
||||
await uut.removeStaleOffers()
|
||||
})
|
||||
|
||||
it('should handle error ', async () => {
|
||||
try {
|
||||
const testErr = new Error('unknow error')
|
||||
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
|
||||
sandbox.stub(uut.retryQueue, 'addToQueue').throws(testErr)
|
||||
|
||||
await uut.removeStaleOffers()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'unknow error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#findOfferByTxid', () => {
|
||||
it('should throw an error if input is not provided', async () => {
|
||||
try {
|
||||
await uut.findOfferByTxid()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'utxoTxid must be a string')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if offer is not found', async () => {
|
||||
try {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.OfferModel, 'findOne').resolves(null)
|
||||
|
||||
await uut.findOfferByTxid('241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87')
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'offer not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return offer', async () => {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.OfferModel, 'findOne').resolves(mockData.nftOffer01)
|
||||
|
||||
const result = await uut.findOfferByTxid('241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87')
|
||||
assert.isObject(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#detectNsfw', () => {
|
||||
it('should return false for wrong cid format', async () => {
|
||||
const result = await uut.detectNsfw({ mutableData: '' })
|
||||
assert.isFalse(result)
|
||||
})
|
||||
|
||||
it('should return true if nft boolean detected', async () => {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.axios, 'get').resolves({ data: { nsfw: true } })
|
||||
|
||||
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
|
||||
assert.isTrue(result)
|
||||
})
|
||||
|
||||
it('should return true if nft string detected', async () => {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.axios, 'get').resolves({ data: { nsfw: 'true' } })
|
||||
|
||||
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
|
||||
assert.isTrue(result)
|
||||
})
|
||||
|
||||
it('should return false if nfsw property does not exist', async () => {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.axios, 'get').resolves({ data: {} })
|
||||
|
||||
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
|
||||
assert.isFalse(result)
|
||||
})
|
||||
|
||||
it('should return false on error', async () => {
|
||||
// Mock dependencies and force desired code path.
|
||||
sandbox.stub(uut.axios, 'get').throws(new Error('test error'))
|
||||
|
||||
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
|
||||
assert.isFalse(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#listOffers', () => {
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
|
||||
|
||||
await uut.listOffers()
|
||||
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should list orders', async () => {
|
||||
const queryMock = {
|
||||
sort () {
|
||||
return this
|
||||
},
|
||||
skip () {
|
||||
return this
|
||||
},
|
||||
limit () { return [] }
|
||||
}
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
|
||||
|
||||
const result = await uut.listOffers()
|
||||
assert.isArray(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#listNftOffers', () => {
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
|
||||
|
||||
await uut.listNftOffers()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should list orders', async () => {
|
||||
const queryMock = {
|
||||
sort () {
|
||||
return this
|
||||
},
|
||||
skip () {
|
||||
return this
|
||||
},
|
||||
limit () { return [] }
|
||||
}
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
|
||||
|
||||
const result = await uut.listNftOffers(1, true)
|
||||
assert.isArray(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#listFungibleOffers', () => {
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
|
||||
|
||||
await uut.listFungibleOffers()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should list orders', async () => {
|
||||
const queryMock = {
|
||||
sort () {
|
||||
return this
|
||||
},
|
||||
skip () {
|
||||
return this
|
||||
},
|
||||
limit () { return [] }
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
|
||||
|
||||
const result = await uut.listFungibleOffers()
|
||||
assert.isArray(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#takeOffer', () => {
|
||||
it('should handle error if input is not provided', async () => {
|
||||
try {
|
||||
await uut.takeOffer()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'eventId must be a string')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error for wrong offer status', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'completed' })
|
||||
|
||||
await uut.takeOffer('eventId')
|
||||
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'offer status is not "posted", so offer is dead and can not be countered.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error for invalid utxo', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(false)
|
||||
|
||||
await uut.takeOffer('eventId')
|
||||
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'UTXO does not exist. Aborting.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle insufficient funds', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
|
||||
sandbox.stub(uut, 'ensureFunds').throws(new Error('test error'))
|
||||
|
||||
await uut.takeOffer('eventId')
|
||||
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error if counter offer cant be calculated', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
|
||||
sandbox.stub(uut, 'ensureFunds').resolves(true)
|
||||
|
||||
await uut.takeOffer('eventId')
|
||||
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Could not calculate the amount of BCH to generate counter offer')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error if counter offer cant be calculated', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
|
||||
sandbox.stub(uut, 'ensureFunds').resolves(true)
|
||||
|
||||
await uut.takeOffer('eventId')
|
||||
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Could not calculate the amount of BCH to generate counter offer')
|
||||
}
|
||||
})
|
||||
|
||||
it('should take offer', async () => {
|
||||
// Mock data
|
||||
const offerMock = Object.assign({}, mockData.offerMockData.data)
|
||||
offerMock.remove = async () => { }
|
||||
offerMock.offerStatus = 'posted'
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'findOfferByEvent').returns(offerMock)
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
|
||||
sandbox.stub(uut, 'ensureFunds').resolves(true)
|
||||
sandbox.stub(uut.adapters.wallet, 'moveBch').resolves({ sats: 1 })
|
||||
|
||||
await uut.takeOffer('eventId')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#ensureFunds', () => {
|
||||
// it('should handle insufficient funds to use p2wdb', async () => {
|
||||
// try {
|
||||
// // Mock dependencies
|
||||
// sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(false)
|
||||
//
|
||||
// await uut.ensureFunds(mockData.offerMockData.data)
|
||||
//
|
||||
// assert.fail('Unexpected code path')
|
||||
// } catch (err) {
|
||||
// assert.include(err.message, 'App wallet does not have funds for writing to the P2WDB')
|
||||
// }
|
||||
// })
|
||||
|
||||
it('should throw error if sats needed could no be able to calculated', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true)
|
||||
|
||||
// Mock Input
|
||||
const mock = Object.assign({}, mockData.offerMockData.data)
|
||||
mock.rateInBaseUnit = null
|
||||
|
||||
await uut.ensureFunds(mock)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Could not calculate sats needed!')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if app wallet does not have enough bch', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true)
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'getBalance').resolves(0)
|
||||
|
||||
await uut.ensureFunds(mockData.offerMockData.data)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'App wallet does not control enough BCH to purchase the tokens.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle BUY offer', async () => {
|
||||
try {
|
||||
const mock = Object.assign({}, mockData.offerMockData.data)
|
||||
mock.buyOrSell = 'buy'
|
||||
|
||||
await uut.ensureFunds(mock)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Buy offers are not supported yet.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return true', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true)
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'getBalance').resolves(10 * 10 ** 6)
|
||||
|
||||
const result = await uut.ensureFunds(mockData.offerMockData.data)
|
||||
assert.isTrue(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#findOrderByEvent', () => {
|
||||
it('should throw an error if hash is not provided', async () => {
|
||||
try {
|
||||
await uut.findOfferByEvent()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'nostrEventId must be a string')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if order is not found!', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.OfferModel, 'findOne').resolves(null)
|
||||
|
||||
await uut.findOfferByEvent('eventId')
|
||||
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'offer not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return offer by eventId', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.OfferModel, 'findOne').resolves({ toObject: () => { return { hash: 'hash' } } })
|
||||
|
||||
const result = await uut.findOfferByEvent('eventId')
|
||||
assert.isObject(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#flagOffer', () => {
|
||||
it('should throw an error if input is not provided', async () => {
|
||||
try {
|
||||
await uut.flagOffer()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, '"data" property is required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if offer is not found!', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'findOfferByEvent').resolves(null)
|
||||
|
||||
const input = {
|
||||
data:
|
||||
{
|
||||
nostrEventId: 'eventId'
|
||||
}
|
||||
}
|
||||
await uut.flagOffer(input)
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'not found in the database')
|
||||
}
|
||||
})
|
||||
|
||||
it('should flag offer', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'findOfferByEvent').resolves({ flags: ['a', 'b', 'c'], save: () => { } })
|
||||
|
||||
const input = {
|
||||
data:
|
||||
{
|
||||
nostrEventId: 'eventId'
|
||||
}
|
||||
}
|
||||
const result = await uut.flagOffer(input)
|
||||
assert.isTrue(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#loadOffers', () => {
|
||||
it('should handle nostr error', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.nostr, 'read').throws(new Error('test error'))
|
||||
|
||||
await uut.loadOffers()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should skip internal function errors ', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.nostr, 'read').resolves([mockData.offerMockData])
|
||||
|
||||
await uut.loadOffers()
|
||||
})
|
||||
|
||||
it('should review and load offers', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.nostr, 'read').resolves([JSON.stringify(mockData.offerMockData)])
|
||||
|
||||
await uut.loadOffers()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#acceptCounterOffer', () => {
|
||||
it('should return if order is not found!', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').throws(new Error('test error'))
|
||||
|
||||
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
|
||||
assert.equal(result, 'N/A')
|
||||
})
|
||||
|
||||
it('should handle error if counter offer cant be calculated', async () => {
|
||||
try {
|
||||
// Mock Data
|
||||
const mock = Object.assign({}, mockData.offerMockData.data)
|
||||
mock.rateInBaseUnit = null
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
|
||||
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
|
||||
|
||||
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
|
||||
console.log('result: ', result)
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Could not calculate the amount of BCH offered in the Counter Offer')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error for wrong transaction output', async () => {
|
||||
try {
|
||||
// Mock Data
|
||||
const mock = Object.assign({}, mockData.offerMockData.data)
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
|
||||
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mockData.offerMockData.data)
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
|
||||
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
|
||||
|
||||
await uut.acceptCounterOffer({ data: mock })
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'The Counter Offer has an output of ')
|
||||
assert.include(err.message, 'which does not match the required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error for wrong transaction output address', async () => {
|
||||
try {
|
||||
// Mock data
|
||||
const mock = Object.assign({}, mockData.offerMockData.data)
|
||||
mock.makerAddr = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477' // Unknow Adress
|
||||
mock.rateInBaseUnit = 0
|
||||
mock.numTokens = 0
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
|
||||
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
|
||||
|
||||
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
|
||||
|
||||
await uut.acceptCounterOffer({ data: { /** .... */ } })
|
||||
assert.fail('unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'The Counter Offer has an output address of')
|
||||
assert.include(err.message, 'which does not match the Maker address')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return tx id', async () => {
|
||||
// Mock data
|
||||
const mock = Object.assign({}, mockData.offerMockData.data)
|
||||
mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0] // Maker Address
|
||||
mock.rateInBaseUnit = 0
|
||||
mock.numTokens = 0
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
|
||||
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
|
||||
|
||||
//
|
||||
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
|
||||
assert.isString(result)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -173,15 +173,66 @@ describe('#order-use-case', () => {
|
||||
// Mock dependencies and force expected code path
|
||||
sandbox.stub(uut.orderEntity, 'inputValidate').returns(entryObj)
|
||||
sandbox.stub(uut, 'ensureFunds').resolves()
|
||||
sandbox.stub(uut.UserModel, 'findById').resolves({ mnemonic: 'testMnemonic' })
|
||||
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, 'moveTokensFromCustomWallet').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'initialize').resolves()
|
||||
sandbox.stub(uut.adapters.nostr, 'post').resolves('fakeEvenetId')
|
||||
|
||||
const result = await uut.createOrder(entryObj)
|
||||
console.log('result: ', result)
|
||||
|
||||
assert.isString(result)
|
||||
assert.property(result, 'eventId')
|
||||
assert.property(result, 'noteId')
|
||||
})
|
||||
it('should create an order with consumer-api', async () => {
|
||||
uut.config.useFullStackCash = false
|
||||
const entryObj = {
|
||||
lokadId: 'SWP',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'token-id',
|
||||
buyOrSell: 'sell',
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 1250,
|
||||
numTokens: 1,
|
||||
ticker: 'TEST'
|
||||
}
|
||||
|
||||
// Mock dependencies and force expected code path
|
||||
sandbox.stub(uut.orderEntity, 'inputValidate').returns(entryObj)
|
||||
sandbox.stub(uut, 'ensureFunds').resolves()
|
||||
sandbox.stub(uut.UserModel, 'findById').resolves({ mnemonic: 'testMnemonic' })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.Util, 'sleep').resolves()
|
||||
sandbox.stub(uut.adapters.wallet, 'moveTokensFromCustomWallet').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'initialize').resolves()
|
||||
sandbox.stub(uut.adapters.nostr, 'post').resolves('fakeEvenetId')
|
||||
|
||||
const result = await uut.createOrder(entryObj)
|
||||
console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'eventId')
|
||||
assert.property(result, 'noteId')
|
||||
})
|
||||
it('should throw error if user is not found', async () => {
|
||||
try {
|
||||
const entryObj = {
|
||||
lokadId: 'SWP',
|
||||
messageType: 1,
|
||||
messageClass: 1,
|
||||
tokenId: 'token-id',
|
||||
buyOrSell: 'sell',
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 1250,
|
||||
numTokens: 1,
|
||||
ticker: 'TEST'
|
||||
}
|
||||
await uut.createOrder(entryObj)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'user not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
Unit tests for the use-cases/usage-use-cases.js business logic library.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local support libraries
|
||||
import adapters from '../mocks/adapters/index.js'
|
||||
|
||||
// Mock
|
||||
import { context as mockContext } from '../mocks/ctx-mock.js'
|
||||
|
||||
// Unit under test (uut)
|
||||
import { UsageUseCases, restCalls, usageMiddleware } from '../../../src/use-cases/usage-use-cases.js'
|
||||
|
||||
describe('#usage-use-case', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
before(async () => {
|
||||
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
uut = new UsageUseCases({ adapters })
|
||||
|
||||
// Set as empty array
|
||||
restCalls.splice(0, restCalls.length)
|
||||
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageUseCases()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of adapters must be passed in when instantiating Usage Use Cases library.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#cleanUsage', () => {
|
||||
it('should delete older data than 24 hours', () => {
|
||||
const now = new Date() // Mock date
|
||||
|
||||
// set older mock data
|
||||
restCalls.push({
|
||||
timestamp: now.getTime() - (60000 * 60 * 48), // 48 hours ago
|
||||
ip: '127.0.0.1'
|
||||
})
|
||||
|
||||
// Set recently mock data
|
||||
restCalls.push({
|
||||
timestamp: now.getTime(),
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
const result = uut.cleanUsage()
|
||||
|
||||
assert.isArray(result)
|
||||
assert.equal(result.length, 1)
|
||||
assert.equal(result[0].ip, 'localhost')
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(restCalls, 'filter').throws(new Error('uut error'))
|
||||
|
||||
uut.cleanUsage()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'uut error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getRestSummary', () => {
|
||||
it('should get the number of rest calls', () => {
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
const result = uut.getRestSummary()
|
||||
|
||||
assert.isNumber(result)
|
||||
assert.equal(result, 1)
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(console, 'log').throws(new Error('uut error'))
|
||||
|
||||
uut.getRestSummary()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'uut error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getTopIps', () => {
|
||||
it('should get top IPs', () => {
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
const result = uut.getTopIps()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'ip')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result[0].ip, 'localhost')
|
||||
assert.equal(result[0].cnt, '2')
|
||||
})
|
||||
|
||||
it('should return a maximum of 20 values', () => {
|
||||
// Fill Array with 21 values
|
||||
for (let i = 0; i < 21; i++) {
|
||||
restCalls.push({
|
||||
ip: `localhost-${i}`
|
||||
})
|
||||
}
|
||||
|
||||
const result = uut.getTopIps()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'ip')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result.length, 20)
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Set mock data
|
||||
restCalls.push(null)
|
||||
|
||||
uut.getTopIps()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'Cannot read properties')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getTopEndpoints', () => {
|
||||
it('should get top Endpoints', () => {
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost',
|
||||
url: '/api/v1/users',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost',
|
||||
url: '/api/v1/users',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
const result = uut.getTopEndpoints()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'endpoint')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result[0].endpoint, 'GET /api/v1/users')
|
||||
assert.equal(result[0].cnt, '2')
|
||||
})
|
||||
|
||||
it('should return a maximum of 20 values', () => {
|
||||
// Fill Array with 21 values
|
||||
for (let i = 0; i < 21; i++) {
|
||||
restCalls.push({
|
||||
ip: 'localhost',
|
||||
url: `/api/v1/users-${i}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
const result = uut.getTopEndpoints()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'endpoint')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result.length, 20)
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Set mock data
|
||||
restCalls.push(null)
|
||||
|
||||
uut.getTopEndpoints()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'Cannot read properties')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#usageMiddleware', () => {
|
||||
it('should update restCalls state', async () => {
|
||||
// Spy on next
|
||||
const next = sinon.spy(() => { })
|
||||
|
||||
await usageMiddleware()(ctx, next)
|
||||
|
||||
assert.equal(restCalls.length, 1)
|
||||
assert.isTrue(next.called)
|
||||
})
|
||||
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
const next = () => { throw new Error('uut error') }
|
||||
|
||||
await usageMiddleware()(ctx, next)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'uut error')
|
||||
assert.equal(ctx.status, 500)
|
||||
assert.equal(restCalls.length, 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -117,6 +117,29 @@ describe('#users-use-case', () => {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
it('should handle wallet errors', async () => {
|
||||
try {
|
||||
// Force an error with the database.
|
||||
class MockErrorBchWallet {
|
||||
constructor () {
|
||||
this.walletInfoPromise = Promise.reject(new Error('test error'))
|
||||
}
|
||||
}
|
||||
uut.BchWallet = MockErrorBchWallet
|
||||
|
||||
const usrObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'password',
|
||||
name: 'test'
|
||||
}
|
||||
|
||||
await uut.createUser(usrObj)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should create a new user in the DB', async () => {
|
||||
// Note: The user created in this test is used by the getUser, update,
|
||||
@@ -129,7 +152,6 @@ describe('#users-use-case', () => {
|
||||
}
|
||||
|
||||
const { userData, token } = await uut.createUser(usrObj)
|
||||
|
||||
testUser = userData
|
||||
|
||||
// Commented out because there is some sophisticated mocking required that
|
||||
@@ -327,6 +349,26 @@ describe('#users-use-case', () => {
|
||||
)
|
||||
}
|
||||
})
|
||||
it('should throw an error if mnemonic is provided', async () => {
|
||||
try {
|
||||
const newData = {
|
||||
email: 'test@test.com',
|
||||
password: 'password',
|
||||
name: 'test',
|
||||
mnemonic: 'test'
|
||||
}
|
||||
|
||||
await uut.updateUser(testUser, newData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'mnemonic' cannot be updated!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should update the user model', async () => {
|
||||
const newData = {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import mongoose from 'mongoose'
|
||||
import config from '../../config/index.js'
|
||||
import User from '../../src/adapters/localdb/models/users.js'
|
||||
|
||||
const EMAIL = 'test@test.com'
|
||||
const PASSWORD = 'pass'
|
||||
const EMAIL = process.env.EMAIL || 'test@test3.com'
|
||||
const PASSWORD = process.env.PASSWORD || 'pass'
|
||||
|
||||
async function addUser () {
|
||||
// Connect to the Mongo Database.
|
||||
@@ -13,8 +14,6 @@ async function addUser () {
|
||||
{ useNewUrlParser: true, useUnifiedTopology: true }
|
||||
)
|
||||
|
||||
const User = require('../../src/models/users')
|
||||
|
||||
const userData = {
|
||||
email: EMAIL,
|
||||
password: PASSWORD
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import mongoose from 'mongoose'
|
||||
import config from '../../config/index.js'
|
||||
import User from '../../src/models/users.js'
|
||||
import User from '../../src/adapters/localdb/models/users.js'
|
||||
|
||||
async function getUsers () {
|
||||
// Connect to the Mongo Database.
|
||||
@@ -0,0 +1,21 @@
|
||||
import mongoose from 'mongoose'
|
||||
// import config from '../../config/index.js'
|
||||
import User from '../../../src/adapters/localdb/models/users.js'
|
||||
|
||||
const mongooseConnectStr = 'mongodb://172.17.0.1:5666/bch-swap-service-prod'
|
||||
|
||||
async function getUsers () {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(
|
||||
mongooseConnectStr,
|
||||
{ useNewUrlParser: true, useUnifiedTopology: true }
|
||||
)
|
||||
|
||||
const users = await User.find({}, '-password')
|
||||
console.log(`users: ${JSON.stringify(users, null, 2)}`)
|
||||
|
||||
mongoose.connection.close()
|
||||
}
|
||||
getUsers()
|
||||
@@ -9,10 +9,10 @@
|
||||
// Public npm libraries
|
||||
import BCHJS from '@psf/bch-js'
|
||||
|
||||
import BchTokenSweep from 'bch-token-sweep/index'
|
||||
import BchTokenSweep from 'bch-token-sweep'
|
||||
|
||||
// Local libraries
|
||||
import WalletAdapter from '../../src/adapters/wallet'
|
||||
import WalletAdapter from '../../src/adapters/wallet.js'
|
||||
|
||||
// Constants
|
||||
const EMTPY_ADDR_CUTOFF = 15
|
||||
@@ -22,6 +22,7 @@ async function sweepFunds () {
|
||||
// Open the wallet files.
|
||||
const wallet = new WalletAdapter()
|
||||
const walletInfo = await wallet.openWallet()
|
||||
const bchWallet = await wallet.instanceWallet(walletInfo)
|
||||
console.log('walletInfo: ', walletInfo)
|
||||
|
||||
const rootAddr = walletInfo.cashAddress
|
||||
@@ -49,7 +50,7 @@ async function sweepFunds () {
|
||||
const sweeper = new BchTokenSweep(
|
||||
wifToSweep,
|
||||
rootWif,
|
||||
bchjs,
|
||||
bchWallet,
|
||||
550,
|
||||
rootAddr
|
||||
)
|
||||
@@ -68,7 +69,7 @@ async function sweepFunds () {
|
||||
// Wait between loop iterations.
|
||||
await bchjs.Util.sleep(3000)
|
||||
} catch (err) {
|
||||
console.log(`error message with index ${hdIndex}: ${err.message}`)
|
||||
console.log(`error message with index ${hdIndex}: ${err}`)
|
||||
emptyAddrCnt++
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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.
|
||||
|
||||
This version will traverse the HD index of the wallet until it reaches
|
||||
the value in the nextAddress property. This ensures that all UTXOs
|
||||
that could be used by the wallet have been swept.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import BCHJS from '@psf/bch-js'
|
||||
|
||||
import BchTokenSweep from 'bch-token-sweep'
|
||||
|
||||
// Local libraries
|
||||
import WalletAdapter from '../../src/adapters/wallet.js'
|
||||
|
||||
async function sweepFunds () {
|
||||
try {
|
||||
// Open the wallet files.
|
||||
const wallet = new WalletAdapter()
|
||||
const walletInfo = await wallet.openWallet()
|
||||
const bchWallet = await wallet.instanceWallet(walletInfo)
|
||||
console.log('walletInfo: ', walletInfo)
|
||||
|
||||
const lastIndex = walletInfo.nextAddress
|
||||
|
||||
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 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 HD Index ${hdIndex} with address ${cashAddress}`)
|
||||
|
||||
try {
|
||||
// Sweep tokens from address
|
||||
const sweeper = new BchTokenSweep(
|
||||
wifToSweep,
|
||||
rootWif,
|
||||
bchWallet,
|
||||
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}`)
|
||||
|
||||
// Wait between loop iterations.
|
||||
await bchjs.Util.sleep(3000)
|
||||
} catch (err) {
|
||||
console.log(`error message with index ${hdIndex}: ${err}`)
|
||||
}
|
||||
|
||||
hdIndex++
|
||||
} while (hdIndex <= lastIndex)
|
||||
|
||||
console.log(`${lastIndex} 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()
|
||||
Reference in New Issue
Block a user