diff --git a/package-lock.json b/package-lock.json index b6ef82e..1cca1b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@psf/bch-js": "6.7.3", "axios": "0.27.2", "bch-message-lib": "2.2.1", - "bch-nostr": "1.3.2", + "bch-nostr": "1.3.3", "bch-token-sweep": "2.2.1", "bcryptjs": "2.4.3", "bitcoincashjs-lib": "3.3.3", @@ -53,10 +53,11 @@ "koa2-ratelimit": "0.9.1", "libp2p": "1.9.1", "line-reader": "0.4.0", - "minimal-slp-wallet": "5.12.0", + "minimal-slp-wallet": "5.12.1", "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", @@ -7420,9 +7421,9 @@ } }, "node_modules/bch-consumer": { - "version": "1.6.1", - "resolved": "http://94.130.170.209:4873/bch-consumer/-/bch-consumer-1.6.1.tgz", - "integrity": "sha512-ZhyZ54kgF6f3sB7J4ylMBJHSoBXozUdM2bvsm2nTz31515LFfHra9d9w4Ommlmy5BvuDkdX4lUeGSLRvggXnFg==", + "version": "1.5.1", + "resolved": "http://94.130.170.209:4873/bch-consumer/-/bch-consumer-1.5.1.tgz", + "integrity": "sha512-tEmds6Mk7g074+fGflm0d9At2ZV8DFhWErEN0nUzzW4vG5Ega9+yjREY16P/QuwwykzodnleQqOsNwcmvvgnAg==", "license": "MIT", "dependencies": { "@psf/bch-js": "6.7.3", @@ -7597,9 +7598,9 @@ } }, "node_modules/bch-nostr": { - "version": "1.3.2", - "resolved": "http://94.130.170.209:4873/bch-nostr/-/bch-nostr-1.3.2.tgz", - "integrity": "sha512-MdbuuTns1MhEd205uabl7Y8X+caphnb6ZgHs574bet+cSE0aw4QoMr6hpc+EmLtuKK90r5Eqob1peuQ9jzVlIg==", + "version": "1.3.3", + "resolved": "http://94.130.170.209:4873/bch-nostr/-/bch-nostr-1.3.3.tgz", + "integrity": "sha512-2Mod/PEDr/GJzQq20oSJKwJDtiXYm/Qj5KxIvFqsvBY1SYsHGyDu1FMBQ1UvOhY3ZP5fgiVLbIGvMRuWXqS4oQ==", "license": "MIT", "dependencies": { "apidoc": "0.53.0", @@ -7610,6 +7611,51 @@ "ws": "8.18.0" } }, + "node_modules/bch-nostr/node_modules/@noble/ciphers": { + "version": "0.5.3", + "resolved": "http://94.130.170.209:4873/@noble%2fciphers/-/ciphers-0.5.3.tgz", + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/bch-nostr/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "http://94.130.170.209:4873/@noble%2fcurves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/bch-nostr/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "http://94.130.170.209:4873/@noble%2fhashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/bch-nostr/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "http://94.130.170.209:4873/@noble%2fhashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/bch-nostr/node_modules/apidoc": { "version": "0.53.0", "resolved": "http://94.130.170.209:4873/apidoc/-/apidoc-0.53.0.tgz", @@ -7680,6 +7726,31 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/bch-nostr/node_modules/nostr-tools": { + "version": "2.10.1", + "resolved": "http://94.130.170.209:4873/nostr-tools/-/nostr-tools-2.10.1.tgz", + "integrity": "sha512-fnAxLi92UgyMAEw4fMEhDKzH53uBJ0WkkVPTNfX0b3bspeWWn0n5tDZtKRJbvW0wAGOPmU6LYWi/IKPOsq3p2Q==", + "license": "Unlicense", + "dependencies": { + "@noble/ciphers": "^0.5.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.1", + "@scure/base": "1.1.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1" + }, + "optionalDependencies": { + "nostr-wasm": "v0.1.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/bch-token-sweep": { "version": "2.2.1", "resolved": "http://94.130.170.209:4873/bch-token-sweep/-/bch-token-sweep-2.2.1.tgz", @@ -17185,15 +17256,15 @@ } }, "node_modules/minimal-slp-wallet": { - "version": "5.12.0", - "resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-5.12.0.tgz", - "integrity": "sha512-zSJcBLF/tt7NH0xkuukTJ4BxnC2GIR8kjUJRWW91/LwhI6c2K9qNVV/G5E7SjYx125aHkGk5lq3pjGePsJ18rA==", + "version": "5.12.1", + "resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-5.12.1.tgz", + "integrity": "sha512-DnggKv1KC/Y6qxT6GiplrxfKPgBOdYbu7uK4bWVO+6RgTtcgMxrd4pxCFh4kHV3nV7vKBpE1wsmVZgtZ3x9qcg==", "license": "MIT", "dependencies": { "@chris.troutner/retry-queue-commonjs": "1.0.8", "@psf/bch-js": "6.8.0", "apidoc": "0.51.0", - "bch-consumer": "1.6.1", + "bch-consumer": "1.5.1", "bch-donation": "1.1.2", "crypto-js": "4.0.0" } @@ -18234,9 +18305,9 @@ } }, "node_modules/nostr-tools": { - "version": "2.10.1", - "resolved": "http://94.130.170.209:4873/nostr-tools/-/nostr-tools-2.10.1.tgz", - "integrity": "sha512-fnAxLi92UgyMAEw4fMEhDKzH53uBJ0WkkVPTNfX0b3bspeWWn0n5tDZtKRJbvW0wAGOPmU6LYWi/IKPOsq3p2Q==", + "version": "2.10.4", + "resolved": "http://94.130.170.209:4873/nostr-tools/-/nostr-tools-2.10.4.tgz", + "integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==", "license": "Unlicense", "dependencies": { "@noble/ciphers": "^0.5.1", @@ -18247,7 +18318,7 @@ "@scure/bip39": "1.2.1" }, "optionalDependencies": { - "nostr-wasm": "v0.1.0" + "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" diff --git a/package.json b/package.json index 393144b..729ed63 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@psf/bch-js": "6.7.3", "axios": "0.27.2", "bch-message-lib": "2.2.1", - "bch-nostr": "1.3.2", + "bch-nostr": "1.3.3", "bch-token-sweep": "2.2.1", "bcryptjs": "2.4.3", "bitcoincashjs-lib": "3.3.3", @@ -72,10 +72,11 @@ "koa2-ratelimit": "0.9.1", "libp2p": "1.9.1", "line-reader": "0.4.0", - "minimal-slp-wallet": "5.12.0", + "minimal-slp-wallet": "5.12.1", "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", diff --git a/production/docker/bch-dex-ui/Dockerfile b/production/docker/bch-dex-ui/Dockerfile index 7a308d3..3f5a523 100644 --- a/production/docker/bch-dex-ui/Dockerfile +++ b/production/docker/bch-dex-ui/Dockerfile @@ -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/ diff --git a/production/docker/bch-dex/Dockerfile b/production/docker/bch-dex/Dockerfile index 282a204..3f34fcf 100644 --- a/production/docker/bch-dex/Dockerfile +++ b/production/docker/bch-dex/Dockerfile @@ -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/ diff --git a/production/docker/docker-compose.yml b/production/docker/docker-compose.yml index f6a8607..0fd0fe8 100644 --- a/production/docker/docker-compose.yml +++ b/production/docker/docker-compose.yml @@ -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' # : 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: - # : - - 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,8 +28,8 @@ services: max-file: '10' mem_limit: 1000mb links: - - mongo-p2wdb-service - - p2wdb + - mongo-dex + #- p2wdb ports: - '5700:5700' # : volumes: @@ -96,13 +38,13 @@ services: 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: diff --git a/production/docker/p2wdb/Dockerfile b/production/docker/p2wdb/Dockerfile deleted file mode 100644 index 6bc5965..0000000 --- a/production/docker/p2wdb/Dockerfile +++ /dev/null @@ -1,71 +0,0 @@ -# Create a Docker container for the P2WDB -# - -#IMAGE BUILD COMMANDS -FROM ubuntu:20.04 -MAINTAINER Chris Troutner - -#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"] diff --git a/production/docker/p2wdb/dummyapp.js b/production/docker/p2wdb/dummyapp.js deleted file mode 100644 index 8cd4613..0000000 --- a/production/docker/p2wdb/dummyapp.js +++ /dev/null @@ -1,4 +0,0 @@ -setInterval(function () { - const now = new Date() - console.log(`ping ${now.toLocaleString()}`) -}, 10000) diff --git a/production/docker/p2wdb/start-production.sh b/production/docker/p2wdb/start-production.sh deleted file mode 100755 index 8daafad..0000000 --- a/production/docker/p2wdb/start-production.sh +++ /dev/null @@ -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 diff --git a/src/adapters/nostr.js b/src/adapters/nostr.js index 7812b16..07bdb63 100644 --- a/src/adapters/nostr.js +++ b/src/adapters/nostr.js @@ -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} `) @@ -116,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 diff --git a/src/adapters/wallet.js b/src/adapters/wallet.js index a40e99b..7055e27 100644 --- a/src/adapters/wallet.js +++ b/src/adapters/wallet.js @@ -5,12 +5,11 @@ // 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,6 +34,11 @@ 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.moveTokens = this.moveTokens.bind(this) @@ -441,7 +445,7 @@ class WalletAdapter { return txObj2 } catch (err) { - console.error('Error in wallet.js/deserializePartialTx()') + console.error('Error in wallet.js/deserializeTx()') throw err } } @@ -508,7 +512,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) { diff --git a/src/controllers/rest-api/offer/controller.js b/src/controllers/rest-api/offer/controller.js index 10daa64..198c3d8 100644 --- a/src/controllers/rest-api/offer/controller.js +++ b/src/controllers/rest-api/offer/controller.js @@ -106,9 +106,9 @@ class OfferRESTControllerLib { const nostrEventId = ctx.request.body.nostrEventId - const eventId = await this.useCases.offer.takeOffer(nostrEventId) + const { eventId, noteId } = await this.useCases.offer.takeOffer(nostrEventId) - ctx.body = { eventId } + ctx.body = { eventId, noteId } } catch (err) { wlogger.error('Error in takeOffer() REST API handler.') this.handleError(ctx, err) diff --git a/src/controllers/rest-api/order/controller.js b/src/controllers/rest-api/order/controller.js index 555a802..6c49209 100644 --- a/src/controllers/rest-api/order/controller.js +++ b/src/controllers/rest-api/order/controller.js @@ -37,9 +37,9 @@ class OrderRESTControllerLib { const orderObj = ctx.request.body.order 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) diff --git a/src/controllers/timer-controllers.js b/src/controllers/timer-controllers.js index 7fa35c7..fa4e149 100644 --- a/src/controllers/timer-controllers.js +++ b/src/controllers/timer-controllers.js @@ -30,6 +30,7 @@ class TimerControllers { this.gcOffers = this.gcOffers.bind(this) this.checkDupOffers = this.checkDupOffers.bind(this) this.loadOffers = this.loadOffers.bind(this) + // State this.gcOrdersInt = null this.gcOffersInt = null @@ -41,7 +42,7 @@ class TimerControllers { 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) return true } @@ -49,7 +50,7 @@ class TimerControllers { stopTimers () { clearInterval(this.gcOrdersInt) clearInterval(this.gcOffersInt) - clearInterval(this.checkDupOffers) + // clearInterval(this.checkDupOffers) } // Garbage Collect the Orders. diff --git a/src/entities/offer.js b/src/entities/offer.js index c0116d7..8e2caed 100644 --- a/src/entities/offer.js +++ b/src/entities/offer.js @@ -17,7 +17,7 @@ class OfferEntity { ) } - console.log('offer entity validate() offerData: ', offerData) + // console.log('offer entity validate() offerData: ', offerData) const { messageType, @@ -105,7 +105,7 @@ class OfferEntity { tokenType, nostrEventId } - console.log('offer entity validatedOfferData: ', validatedOfferData) + // console.log('offer entity validatedOfferData: ', validatedOfferData) return validatedOfferData } diff --git a/src/use-cases/offer/index.js b/src/use-cases/offer/index.js index ceb2114..9cd360b 100644 --- a/src/use-cases/offer/index.js +++ b/src/use-cases/offer/index.js @@ -49,11 +49,25 @@ 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 timer controller to load offers from a Nostr topic. @@ -94,7 +108,7 @@ 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 @@ -107,6 +121,8 @@ class OfferUseCases { const offerEntity = this.offerEntity.validate(offerObj) // 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) @@ -175,7 +191,7 @@ class OfferUseCases { 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}`) + // console.log(`hasNsfw: ${hasNsfw}, nsfwSetTrue: ${nsfwSetTrue}, nsfwStringTrue: ${nsfwStringTrue}, nsfwDetected: ${nsfwDetected}`) if (nsfwDetected) { console.log('NSFW flag set as true') @@ -318,7 +334,7 @@ class OfferUseCases { // 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}`) @@ -373,18 +389,20 @@ class OfferUseCases { // TODO: This will trigger the webhook. Find some way of triggering the // webhook on new offers, but not on counteroffers const nostrData = { - wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, - data: takenOfferInfo, - appId: this.config.p2wdbAppId + // wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, + data: takenOfferInfo + // appId: this.config.p2wdbAppId } - const resultEventId = await this.adapters.nostr.post(nostrData) + 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 resultEventId + return { eventId: resultEventId, noteId } // return 'fake-hash' } catch (err) { @@ -472,27 +490,27 @@ class OfferUseCases { } async findOfferByTxid (utxoTxid) { - try { - // try { - if (typeof utxoTxid !== 'string' || !utxoTxid) { - throw new Error('utxoTxid must be a string') - } - - 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 - } catch (error) { - console.error('Error in use-cases/offer/findOfferByTxid(): ', error.message) - throw error + // try { + // try { + if (typeof utxoTxid !== 'string' || !utxoTxid) { + throw new Error('utxoTxid must be a string') } + + 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 + // } 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 @@ -501,19 +519,66 @@ class OfferUseCases { // and broadcasts the transaction to accept the Counter Offer. async acceptCounterOffer (offerData) { try { - console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, 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. - // Note : this should be handle by nostrEvent id or UtxoId? - const orderHash = offerData.data.nostrEventId + // Note : this should be handled by nostrEvent id or UtxoId? + // const orderHash = offerData.data.nostrEventId let orderData = {} try { - orderData = await this.orderUseCase.findOrderByEvent(orderHash) - console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`) + 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' } @@ -549,7 +614,7 @@ class OfferUseCases { return txid } catch (err) { - console.error('Error in acceptCounterOffer()') + console.error('Error in acceptCounterOffer(): ', err) throw err } } @@ -637,7 +702,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() } @@ -698,7 +763,7 @@ class OfferUseCases { try { // Retrieve offers array. const offers = await this.adapters.nostr.read() - console.log('offers: ', offers) + // console.log('offers: ', offers) for (let i = 0; i < offers.length; i++) { try { @@ -709,9 +774,21 @@ class OfferUseCases { // Append the Nostr Event ID to the offer object offerObj.data.nostrEventId = offer.eventId + // console.log('loadOffers() offerObj: ', offerObj) - // Try to create new offer - await this.createOffer(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 */ } diff --git a/src/use-cases/order.js b/src/use-cases/order.js index 3aa6841..68aa2e1 100644 --- a/src/use-cases/order.js +++ b/src/use-cases/order.js @@ -31,6 +31,8 @@ class OrderLib { // 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. @@ -102,7 +104,9 @@ 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(): ', err) @@ -191,6 +195,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 +233,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 +271,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() } diff --git a/test/unit/controllers/rest-api/order/order.rest.controller.unit.js b/test/unit/controllers/rest-api/order/order.rest.controller.unit.js index 1b70732..43e354b 100644 --- a/test/unit/controllers/rest-api/order/order.rest.controller.unit.js +++ b/test/unit/controllers/rest-api/order/order.rest.controller.unit.js @@ -67,11 +67,12 @@ describe('#Order-REST-Router', () => { } // 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 () => { diff --git a/test/unit/mocks/adapters/index.js b/test/unit/mocks/adapters/index.js index 72e68d2..dce660d 100644 --- a/test/unit/mocks/adapters/index.js +++ b/test/unit/mocks/adapters/index.js @@ -166,7 +166,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} diff --git a/test/unit/use-cases/offer.use-case.unit.js b/test/unit/use-cases/offer.use-case.unit.js index 005b857..05fed70 100644 --- a/test/unit/use-cases/offer.use-case.unit.js +++ b/test/unit/use-cases/offer.use-case.unit.js @@ -48,6 +48,7 @@ describe('#offer-use-case', () => { ) } }) + it('should throw an error if order use cases are not passed in', () => { try { uut = new OfferLib({ adapters }) @@ -72,6 +73,7 @@ describe('#offer-use-case', () => { assert.include(error.message, 'Cannot read properties of undefined') } }) + it('should return false if offer already exist', async () => { const offerObj = mockData.offerMockData @@ -82,6 +84,7 @@ describe('#offer-use-case', () => { const result = await uut.createOffer(offerObj) assert.isFalse(result) }) + it('should return false for invalid utxo', async () => { const offerObj = mockData.offerMockData @@ -147,6 +150,7 @@ describe('#offer-use-case', () => { assert.equal(result, 'fungible') }) + it('should unknow type', async () => { try { // Mock dependencies @@ -191,6 +195,7 @@ describe('#offer-use-case', () => { assert.equal(result, false) }) + it('should handle error', async () => { try { // Mock dependencies and force desired code path. @@ -204,6 +209,7 @@ describe('#offer-use-case', () => { } }) }) + describe('#removeStaleOffers', () => { it('remove offer with wrong utxoState', async () => { // Mock dependencies and force desired code path. @@ -212,6 +218,7 @@ describe('#offer-use-case', () => { 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 () => { } }]) @@ -219,6 +226,7 @@ describe('#offer-use-case', () => { await uut.removeStaleOffers() }) + it('remove expired offer ', async () => { const tsMock = new Date() tsMock.setMonth(tsMock.getMonth() - 3) @@ -230,6 +238,7 @@ describe('#offer-use-case', () => { await uut.removeStaleOffers() }) + it('should handle axios error ', async () => { const testErr = new Error() testErr.isAxiosError = true @@ -240,6 +249,7 @@ describe('#offer-use-case', () => { await uut.removeStaleOffers() }) + it('should handle error ', async () => { try { const testErr = new Error('unknow error') @@ -255,6 +265,7 @@ describe('#offer-use-case', () => { } }) }) + describe('#findOfferByTxid', () => { it('should throw an error if input is not provided', async () => { try { @@ -264,6 +275,7 @@ describe('#offer-use-case', () => { 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. @@ -284,6 +296,7 @@ describe('#offer-use-case', () => { assert.isObject(result) }) }) + describe('#detectNsfw', () => { it('should return false for wrong cid format', async () => { const result = await uut.detectNsfw({ mutableData: '' }) @@ -297,6 +310,7 @@ describe('#offer-use-case', () => { 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' } }) @@ -304,6 +318,7 @@ describe('#offer-use-case', () => { 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: {} }) @@ -311,6 +326,7 @@ describe('#offer-use-case', () => { 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')) @@ -319,6 +335,7 @@ describe('#offer-use-case', () => { assert.isFalse(result) }) }) + describe('#listOffers', () => { it('should handle error', async () => { try { @@ -350,6 +367,7 @@ describe('#offer-use-case', () => { assert.isArray(result) }) }) + describe('#listNftOffers', () => { it('should handle error', async () => { try { @@ -380,6 +398,7 @@ describe('#offer-use-case', () => { assert.isArray(result) }) }) + describe('#listFungibleOffers', () => { it('should handle error', async () => { try { @@ -411,6 +430,7 @@ describe('#offer-use-case', () => { assert.isArray(result) }) }) + describe('#takeOffer', () => { it('should handle error if input is not provided', async () => { try { @@ -420,6 +440,7 @@ describe('#offer-use-case', () => { assert.include(err.message, 'eventId must be a string') } }) + it('should handle error for wrong offer status', async () => { try { // Mock dependencies @@ -432,6 +453,7 @@ describe('#offer-use-case', () => { 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 @@ -460,6 +482,7 @@ describe('#offer-use-case', () => { assert.include(err.message, 'test error') } }) + it('should handle error if counter offer cant be calculated', async () => { try { // Mock dependencies @@ -474,6 +497,7 @@ describe('#offer-use-case', () => { 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 @@ -488,6 +512,7 @@ describe('#offer-use-case', () => { 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) @@ -561,6 +586,7 @@ describe('#offer-use-case', () => { 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) @@ -570,6 +596,7 @@ describe('#offer-use-case', () => { assert.isTrue(result) }) }) + describe('#findOrderByEvent', () => { it('should throw an error if hash is not provided', async () => { try { @@ -579,6 +606,7 @@ describe('#offer-use-case', () => { assert.include(err.message, 'nostrEventId must be a string') } }) + it('should throw an error if order is not found!', async () => { try { // Mock dependencies @@ -591,6 +619,7 @@ describe('#offer-use-case', () => { 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' } } }) @@ -599,6 +628,7 @@ describe('#offer-use-case', () => { assert.isObject(result) }) }) + describe('#flagOffer', () => { it('should throw an error if input is not provided', async () => { try { @@ -608,6 +638,7 @@ describe('#offer-use-case', () => { assert.include(err.message, '"data" property is required') } }) + it('should throw an error if offer is not found!', async () => { try { // Mock dependencies @@ -625,6 +656,7 @@ describe('#offer-use-case', () => { 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: () => { } }) @@ -652,12 +684,14 @@ describe('#offer-use-case', () => { 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)]) @@ -665,6 +699,7 @@ describe('#offer-use-case', () => { await uut.loadOffers() }) }) + describe('#acceptCounterOffer', () => { it('should return if order is not found!', async () => { // Mock dependencies @@ -673,6 +708,7 @@ describe('#offer-use-case', () => { 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 @@ -681,28 +717,35 @@ describe('#offer-use-case', () => { // Mock dependencies sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock) + sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) - await uut.acceptCounterOffer({ data: { /** .... */ } }) + 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 dependencies + // 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: { /** .... */ } }) + 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 @@ -713,6 +756,7 @@ describe('#offer-use-case', () => { // 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) @@ -724,6 +768,7 @@ describe('#offer-use-case', () => { 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) diff --git a/test/unit/use-cases/order.use-case.unit.js b/test/unit/use-cases/order.use-case.unit.js index 96fbdaa..d619802 100644 --- a/test/unit/use-cases/order.use-case.unit.js +++ b/test/unit/use-cases/order.use-case.unit.js @@ -181,7 +181,8 @@ describe('#order-use-case', () => { const result = await uut.createOrder(entryObj) console.log('result: ', result) - assert.isString(result) + assert.property(result, 'eventId') + assert.property(result, 'noteId') }) it('should catch and throw an error', async () => {