Compare commits

..
6 Commits
20 changed files with 349 additions and 278 deletions
+87 -16
View File
@@ -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"
+3 -2
View File
@@ -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",
+2 -2
View File
@@ -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/
+2 -2
View File
@@ -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/
+13 -71
View File
@@ -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,8 +28,8 @@ services:
max-file: '10'
mem_limit: 1000mb
links:
- mongo-p2wdb-service
- p2wdb
- mongo-dex
#- p2wdb
ports:
- '5700:5700' # <host port>:<container port>
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:
-71
View File
@@ -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"]
-4
View File
@@ -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
+16 -1
View File
@@ -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
+10 -4
View File
@@ -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) {
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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)
+3 -2
View File
@@ -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.
+2 -2
View File
@@ -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
}
+117 -40
View File
@@ -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 */
}
+35 -3
View File
@@ -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()
}
@@ -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 () => {
+2 -1
View File
@@ -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}
+48 -3
View File
@@ -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)
+2 -1
View File
@@ -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 () => {