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 | |
|---|---|---|---|
|
|
c1a8210fa7 | ||
|
|
9f3b482f57 | ||
|
|
9487027852 | ||
|
|
9065900608 | ||
|
|
2da319f896 | ||
|
|
449a98d80c | ||
|
|
2343120fcf | ||
|
|
2d4fa523cd | ||
|
|
c8cc3adbc5 | ||
|
|
169e0ad3d2 | ||
|
|
e20a0a5d27 | ||
|
|
6dda29bb14 | ||
|
|
dd662e9fe9 | ||
|
|
9b775c5bf6 | ||
|
|
2047673862 | ||
|
|
5b545aebeb | ||
|
|
1e75816125 | ||
|
|
455fd807a4 | ||
|
|
8b75ac0f32 | ||
|
|
f1a5ae70e2 | ||
|
|
46bd532eeb | ||
|
|
6f99b215cd | ||
|
|
3482540fc8 | ||
|
|
fb999c7bd4 | ||
|
|
a09e5a5ba7 | ||
|
|
ba14b97258 | ||
|
|
79ac3729c9 | ||
|
|
bd77754686 | ||
|
|
b3b5322226 | ||
|
|
ec1f950ffb | ||
|
|
b18bc51885 | ||
|
|
e8c35dd1f3 | ||
|
|
8be0accdca | ||
|
|
94c56a06b7 | ||
|
|
42f64e3304 | ||
|
|
cc9bbc3c5d | ||
|
|
0c6696875b | ||
|
|
37b2e426a5 | ||
|
|
f3713985ed | ||
|
|
4ed9994414 | ||
|
|
140707ce6b | ||
|
|
c289e82b0e | ||
|
|
84210d07c4 | ||
|
|
6d285c77cd | ||
|
|
7cefbd71ef | ||
|
|
85d6aa7d46 |
Vendored
+4
-1
@@ -113,5 +113,8 @@ export default {
|
||||
? parseInt(process.env.IPFS_API_PORT)
|
||||
: 5001,
|
||||
|
||||
chatPubSubChan: 'psf-ipfs-chat-001'
|
||||
chatPubSubChan: 'psf-ipfs-chat-001',
|
||||
|
||||
// IPFS gateway
|
||||
ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://p2wdb-gateway-678.fullstack.cash/ipfs/'
|
||||
}
|
||||
|
||||
Generated
+484
-211
File diff suppressed because it is too large
Load Diff
+7
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bch-dex",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"description": "A DEX on BCH for trading SLP tokens.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -28,6 +28,8 @@
|
||||
},
|
||||
"repository": "Permissionless-Software-Foundation/bch-dex",
|
||||
"dependencies": {
|
||||
"@chris.troutner/retry-queue": "1.0.8",
|
||||
"@psf/bch-js": "6.5.1",
|
||||
"axios": "0.27.2",
|
||||
"bch-message-lib": "2.2.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
@@ -49,15 +51,15 @@
|
||||
"koa-router": "10.0.0",
|
||||
"koa-static": "5.0.0",
|
||||
"koa2-ratelimit": "0.9.1",
|
||||
"libp2p": "^0.36.2",
|
||||
"libp2p": "0.36.2",
|
||||
"line-reader": "0.4.0",
|
||||
"minimal-slp-wallet": "5.1.0",
|
||||
"minimal-slp-wallet": "5.4.3",
|
||||
"mongoose": "5.13.14",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
"nodemailer": "6.7.5",
|
||||
"p2wdb": "2.2.3",
|
||||
"p2wdb-esm": "2.2.8",
|
||||
"passport-local": "1.0.0",
|
||||
"public-ip": "^4.0.4",
|
||||
"public-ip": "4.0.4",
|
||||
"winston": "3.3.3",
|
||||
"winston-daily-rotate-file": "4.5.0"
|
||||
},
|
||||
|
||||
@@ -72,7 +72,10 @@ RUN npm run docs
|
||||
#EXPOSE 5010
|
||||
|
||||
# Start the application.
|
||||
#COPY start-production.sh start-production.sh
|
||||
CMD ["./start-production.sh"]
|
||||
|
||||
#CMD ["npm", "start"]
|
||||
|
||||
# Used for debugging
|
||||
#COPY dummy-app.js dummy-app.js
|
||||
#CMD ["node", "dummy-app.js"]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
setInterval(function () {
|
||||
const now = new Date()
|
||||
console.log(`timestamp: ${now.toLocaleString()}`)
|
||||
}, 5000)
|
||||
@@ -24,7 +24,7 @@ export DBURL=mongodb://172.17.0.1:5666/bch-swap-service-prod
|
||||
export PORT=5700
|
||||
|
||||
# bch-dex specific env vars
|
||||
export BCH_DEX=production
|
||||
export BCH_DEX=prod
|
||||
export WEBHOOKSERVICE=http://172.17.0.1:5667/webhook
|
||||
export WEBHOOKTARGET=http://172.17.0.1:5700/p2wdb
|
||||
export APP_ID=bch-dex-001
|
||||
|
||||
@@ -70,10 +70,10 @@ services:
|
||||
restart: always
|
||||
|
||||
bch-dex:
|
||||
#build:
|
||||
# context: ./bch-dex/
|
||||
# dockerfile: Dockerfile
|
||||
image: christroutner/bch-dex:v2.0.0
|
||||
build:
|
||||
context: ./bch-dex/
|
||||
dockerfile: Dockerfile
|
||||
#image: christroutner/bch-dex:v2.0.8
|
||||
container_name: bch-dex
|
||||
environment:
|
||||
CONSUMER_URL: 'https://free-bch.fullstack.cash'
|
||||
@@ -94,10 +94,10 @@ services:
|
||||
restart: always
|
||||
|
||||
dex-ui:
|
||||
#build:
|
||||
# context: ./bch-dex-ui/
|
||||
# dockerfile: Dockerfile
|
||||
image: christroutner/bch-dex-ui:v1.12.0
|
||||
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'
|
||||
|
||||
@@ -24,7 +24,7 @@ export DBURL=mongodb://172.17.0.1:5666/bch-swap-service-prod
|
||||
export PORT=5700
|
||||
|
||||
# bch-dex specific env vars
|
||||
export BCH_DEX=production
|
||||
export BCH_DEX=prod
|
||||
export WEBHOOKSERVICE=http://172.17.0.1:5667/webhook
|
||||
export WEBHOOKTARGET=http://172.17.0.1:5700/p2wdb
|
||||
export APP_ID=bch-dex-001
|
||||
|
||||
@@ -43,7 +43,7 @@ services:
|
||||
#build:
|
||||
# context: ./p2wdb/
|
||||
# dockerfile: Dockerfile
|
||||
image: christroutner/p2wdb-rpi:v3.1.5
|
||||
image: christroutner/p2wdb-rpi:v3.1.9
|
||||
container_name: p2wdb
|
||||
environment:
|
||||
CONSUMER_URL: 'https://wa-usa-bch-consumer.fullstackcash.nl'
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
#build:
|
||||
# context: ./bch-dex/
|
||||
# dockerfile: Dockerfile
|
||||
image: christroutner/bch-dex-rpi:v1.13.0
|
||||
image: christroutner/bch-dex-rpi:v2.0.8
|
||||
container_name: bch-dex
|
||||
environment:
|
||||
#CONSUMER_URL: 'https://free-bch.fullstack.cash'
|
||||
@@ -95,7 +95,7 @@ services:
|
||||
#build:
|
||||
# context: ./bch-dex-ui/
|
||||
# dockerfile: Dockerfile
|
||||
image: christroutner/bch-dex-ui-rpi:v1.2.2
|
||||
image: christroutner/bch-dex-ui-rpi:v1.2.4
|
||||
container_name: dex-ui
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
|
||||
@@ -43,6 +43,6 @@ export IPFS_TCP_PORT=4001
|
||||
export IPFS_WS_PORT=4003
|
||||
|
||||
# P2WDB specific env vars
|
||||
export ORBITDB_NAME=/orbitdb/zdpuAqNiwLiJBfbRK7uihV2hAbNSXj78ufzv5VyQb8GuvRwDh/psf-bch-p2wdb-keyvalue-v3.0.0-0001
|
||||
#export ORBITDB_NAME=/orbitdb/zdpuAqNiwLiJBfbRK7uihV2hAbNSXj78ufzv5VyQb8GuvRwDh/psf-bch-p2wdb-keyvalue-v3.0.0-0001
|
||||
|
||||
npm start
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
the DEX app wallet in order to be acceped.
|
||||
*/
|
||||
|
||||
const axios = require('axios')
|
||||
import axios from 'axios'
|
||||
|
||||
const LOCALHOST = 'http://localhost:5700'
|
||||
// const LOCALHOST = 'http://172.17.0.1:5700'
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
*/
|
||||
|
||||
// Public NPM libraries
|
||||
const BchWallet = require('minimal-slp-wallet/index')
|
||||
const fs = require('fs').promises
|
||||
import BchWallet from 'minimal-slp-wallet'
|
||||
|
||||
import { promises as fs } from 'fs'
|
||||
|
||||
async function createWallet () {
|
||||
try {
|
||||
@@ -15,9 +16,9 @@ async function createWallet () {
|
||||
}
|
||||
|
||||
// Wait for the wallet to be created.
|
||||
this.bchWallet = new BchWallet(undefined, advancedConfig)
|
||||
await this.bchWallet.walletInfoPromise
|
||||
const walletData = this.bchWallet.walletInfo
|
||||
const bchWallet = new BchWallet(undefined, advancedConfig)
|
||||
await bchWallet.walletInfoPromise
|
||||
const walletData = bchWallet.walletInfo
|
||||
walletData.nextAddress = 1
|
||||
|
||||
// Save the wallet file to disk.
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
const BchWallet = require('minimal-slp-wallet/index')
|
||||
import BchWallet from 'minimal-slp-wallet'
|
||||
|
||||
// Local libraries
|
||||
const WalletAdapter = require('../../src/adapters/wallet')
|
||||
import WalletAdapter from '../../src/adapters/wallet.js'
|
||||
|
||||
// Constants
|
||||
const EMTPY_ADDR_CUTOFF = 10
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
// Public npm libraries
|
||||
// const BCHJS = require('@psf/bch-js')
|
||||
const BchTokenSweep = require('bch-token-sweep/index')
|
||||
import BchTokenSweep from 'bch-token-sweep/index.js'
|
||||
|
||||
// Local libraries
|
||||
const WalletAdapter = require('../../src/adapters/wallet')
|
||||
import WalletAdapter from '../../src/adapters/wallet.js'
|
||||
|
||||
// Constants
|
||||
const EMTPY_ADDR_CUTOFF = 10
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
import BCHJS from '@psf/bch-js'
|
||||
|
||||
import MsgLib from 'bch-message-lib'
|
||||
import BchWallet from 'minimal-slp-wallet/index.js'
|
||||
import BchWallet from 'minimal-slp-wallet'
|
||||
|
||||
class Bch {
|
||||
constructor () {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import IpfsCoord from 'ipfs-coord-esm'
|
||||
|
||||
// import BCHJS from '@psf/bch-js';
|
||||
import SlpWallet from 'minimal-slp-wallet/index.js'
|
||||
import SlpWallet from 'minimal-slp-wallet'
|
||||
import publicIp from 'public-ip'
|
||||
|
||||
// Local libraries
|
||||
|
||||
@@ -6,7 +6,7 @@ const Offer = new mongoose.Schema({
|
||||
tokenId: { type: String },
|
||||
utxoTxid: { type: String },
|
||||
utxoVout: { type: Number },
|
||||
ticker: { type: String },
|
||||
ticker: { type: String, default: '' },
|
||||
tokenType: { type: Number },
|
||||
|
||||
// Trade data
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
// Public npm libraries.
|
||||
import axios from 'axios'
|
||||
|
||||
import { Write, Read } from 'p2wdb/index.js'
|
||||
import P2WDB from 'p2wdb-esm'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../config/index.js'
|
||||
const { Write, Read } = P2WDB
|
||||
|
||||
class P2wdbAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -27,6 +28,10 @@ class P2wdbAdapter {
|
||||
|
||||
// Allow the localConfig to overwrite the config file values.
|
||||
this.p2wdbURL = localConfig.p2wdbURL || config.p2wdbUrl
|
||||
|
||||
// Bind the 'this' object to all functions
|
||||
this.write = this.write.bind(this)
|
||||
this.checkForSufficientFunds = this.checkForSufficientFunds.bind(this)
|
||||
}
|
||||
|
||||
// Write some data to the P2WDB
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import BchWallet from 'minimal-slp-wallet/index.js'
|
||||
import BchWallet from 'minimal-slp-wallet'
|
||||
|
||||
import bitcoinJs from 'bitcoincashjs-lib'
|
||||
|
||||
@@ -31,6 +31,9 @@ class WalletAdapter {
|
||||
this.bitcoinJs = bitcoinJs
|
||||
this.BchWallet = BchWallet
|
||||
this.bchWallet = {} // Will be replaced when initialized.
|
||||
|
||||
// Bind the 'this' object
|
||||
this.moveTokens = this.moveTokens.bind(this)
|
||||
}
|
||||
|
||||
// Open the wallet file, or create one if the file doesn't exist.
|
||||
@@ -85,6 +88,7 @@ class WalletAdapter {
|
||||
}
|
||||
|
||||
const advancedConfig = {}
|
||||
console.log(`Using FullStack.cash: ${this.config.useFullStackCash}`)
|
||||
if (this.config.useFullStackCash) {
|
||||
advancedConfig.interface = 'rest-api'
|
||||
advancedConfig.restURL = this.config.apiServer
|
||||
@@ -356,7 +360,7 @@ class WalletAdapter {
|
||||
}
|
||||
|
||||
// Update the UTXO store of the wallet.
|
||||
await this.bchWallet.getUtxos()
|
||||
await this.bchWallet.initialize()
|
||||
|
||||
// Get the token type of the token being moved.
|
||||
// Combine Fungible and NFT token UTXOs.
|
||||
|
||||
@@ -33,6 +33,7 @@ class TimerControllers {
|
||||
startTimers () {
|
||||
setInterval(this.gcOrders, 60000 * 5)
|
||||
setInterval(this.gcOffers, 60000 * 5)
|
||||
setInterval(this.checkDupOffers, 60000 * 4.5)
|
||||
}
|
||||
|
||||
// Garbage Collect the Orders.
|
||||
@@ -54,6 +55,16 @@ class TimerControllers {
|
||||
console.log('Error in timer-controllers.js/gcOffers(): ', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicate Offers
|
||||
checkDupOffers () {
|
||||
try {
|
||||
_this.useCases.offer.removeDuplicateOffers()
|
||||
} catch (err) {
|
||||
// Do not throw an error. This is a top-level function.
|
||||
console.log('Error in timer-controllers.js/checkDupOffers(): ', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TimerControllers
|
||||
|
||||
@@ -67,9 +67,9 @@ class OfferEntity {
|
||||
if (!makerAddr || typeof makerAddr !== 'string') {
|
||||
throw new Error("Property 'makerAddr' must be a string.")
|
||||
}
|
||||
if (!ticker || typeof ticker !== 'string') {
|
||||
throw new Error("Property 'ticker' must be a string.")
|
||||
}
|
||||
// if (!ticker || typeof ticker !== 'string') {
|
||||
// throw new Error("Property 'ticker' must be a string.")
|
||||
// }
|
||||
if (!tokenType || typeof tokenType !== 'number') {
|
||||
throw new Error("Property 'tokenType' must be a number.")
|
||||
}
|
||||
|
||||
+10
-8
@@ -7,7 +7,10 @@
|
||||
The Order tracks the hdIndex address used to hold tokens or BCH for sale.
|
||||
*/
|
||||
class Order {
|
||||
validate (data) {
|
||||
// TODO: Create a fullValidate() function that validates a fully-hydrated
|
||||
// order model.
|
||||
|
||||
inputValidate (data) {
|
||||
const {
|
||||
messageType,
|
||||
messageClass,
|
||||
@@ -16,8 +19,7 @@ class Order {
|
||||
rateInBaseUnit,
|
||||
minUnitsToExchange,
|
||||
numTokens,
|
||||
makerAddr,
|
||||
ticker
|
||||
makerAddr
|
||||
} = data
|
||||
|
||||
// Input Validation
|
||||
@@ -45,9 +47,9 @@ class Order {
|
||||
if (!makerAddr || typeof makerAddr !== 'string') {
|
||||
throw new Error("Property 'makerAddr' must be a string.")
|
||||
}
|
||||
if (!ticker || typeof ticker !== 'string') {
|
||||
throw new Error("Property 'ticker' must be a string.")
|
||||
}
|
||||
// if (!ticker || typeof ticker !== 'string') {
|
||||
// throw new Error("Property 'ticker' must be a string.")
|
||||
// }
|
||||
|
||||
const offerData = {
|
||||
messageType,
|
||||
@@ -57,8 +59,8 @@ class Order {
|
||||
rateInBaseUnit,
|
||||
minUnitsToExchange,
|
||||
numTokens,
|
||||
makerAddr,
|
||||
ticker
|
||||
makerAddr
|
||||
// ticker
|
||||
}
|
||||
|
||||
return offerData
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
|
||||
// Global npm libraries
|
||||
import axios from 'axios'
|
||||
import RetryQueue from '@chris.troutner/retry-queue'
|
||||
|
||||
// Local libraries
|
||||
import OfferEntity from '../../entities/offer.js'
|
||||
|
||||
import config from '../../../config/index.js'
|
||||
|
||||
const DEFAULT_ENTRIES_PER_PAGE = 20
|
||||
@@ -44,9 +44,12 @@ class OfferUseCases {
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
this.axios = axios
|
||||
|
||||
this.offerEntity = new OfferEntity()
|
||||
this.OfferModel = this.adapters.localdb.Offer
|
||||
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
|
||||
|
||||
// Bind 'this' object to functions
|
||||
this.detectNsfw = this.detectNsfw.bind(this)
|
||||
}
|
||||
|
||||
// This method is called by the POST /offer REST API controller, which is
|
||||
@@ -74,14 +77,15 @@ class OfferUseCases {
|
||||
|
||||
// Verify that UTXO in offer is unspent. If it is spent, then ignore the
|
||||
// offer.
|
||||
const txid = offerObj.data.utxoTxid
|
||||
const vout = offerObj.data.utxoVout
|
||||
const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
|
||||
txid,
|
||||
vout
|
||||
)
|
||||
const utxo = {
|
||||
tx_hash: offerObj.data.utxoTxid,
|
||||
tx_pos: offerObj.data.utxoVout
|
||||
}
|
||||
// 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)
|
||||
if (utxoStatus === null) return false
|
||||
// if (utxoStatus === null) return false
|
||||
if (!utxoStatus) return false
|
||||
|
||||
// A new offer gets a status of 'posted'
|
||||
offerObj.data.offerStatus = 'posted'
|
||||
@@ -91,18 +95,20 @@ class OfferUseCases {
|
||||
|
||||
// Get data about the token.
|
||||
const tokenId = offerEntity.tokenId
|
||||
const tokenData = await this.adapters.wallet.bchWallet.getTokenData(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)}`)
|
||||
|
||||
// Generate a 'display category' for the token. This will allow the
|
||||
// front end UI to figure out how to display the token.
|
||||
const displayCategory = await this.categorizeToken(offerEntity, tokenData)
|
||||
const displayCategory = this.categorizeToken(offerEntity, tokenData)
|
||||
console.log('displayCategory: ', displayCategory)
|
||||
offerEntity.displayCategory = displayCategory
|
||||
|
||||
// Detect if user set the NSFW flag.
|
||||
let nsfw = false
|
||||
nsfw = await this.detectNsfw(tokenData)
|
||||
// nsfw = await this.detectNsfw(tokenData)
|
||||
nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData)
|
||||
offerEntity.nsfw = nsfw
|
||||
|
||||
// Add offer to the local database.
|
||||
@@ -132,7 +138,8 @@ class OfferUseCases {
|
||||
const cid = mutableCid.substring(7)
|
||||
|
||||
// Retrieve the mutable data from Filecoin/IPFS.
|
||||
const url = `https://${cid}.ipfs.w3s.link/data.json`
|
||||
// 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)}`)
|
||||
@@ -165,7 +172,7 @@ class OfferUseCases {
|
||||
// The first three are easy to categorize. The simple-nft is a fungible token
|
||||
// with a quantity of 1, decimals of 0, and no minting baton. Categorizing this
|
||||
// type of token is the main reason why this function exists.
|
||||
async categorizeToken (offerData, tokenData) {
|
||||
categorizeToken (offerData, tokenData) {
|
||||
try {
|
||||
// console.log(`categorizeToken(): ${JSON.stringify(offerData, null, 2)}`)
|
||||
|
||||
@@ -277,18 +284,11 @@ class OfferUseCases {
|
||||
}
|
||||
|
||||
// Verify that UTXO for sale is unspent. Abort if it's been spent.
|
||||
// const txid = offerInfo.utxoTxid
|
||||
// const vout = offerInfo.utxoVout
|
||||
|
||||
const utxo = {
|
||||
tx_hash: offerInfo.utxoTxid,
|
||||
tx_pos: offerInfo.utxoVout
|
||||
}
|
||||
const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||
// const utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
|
||||
// txid,
|
||||
// vout
|
||||
// )
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
if (!utxoStatus) {
|
||||
console.log(`utxo txid: ${offerInfo.utxoTxid}, vout: ${offerInfo.utxoVout}`)
|
||||
@@ -306,7 +306,7 @@ class OfferUseCases {
|
||||
console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||
|
||||
// Calculate amount of sats to generate a counter offer.
|
||||
let satsToMove = offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)
|
||||
let satsToMove = Math.ceil(offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit))
|
||||
if (isNaN(satsToMove)) {
|
||||
throw new Error('Could not calculate the amount of BCH to generate counter offer')
|
||||
}
|
||||
@@ -385,7 +385,7 @@ class OfferUseCases {
|
||||
// Sell Offer
|
||||
|
||||
// Calculate the sats needed
|
||||
const satsNeeded = offerEntity.numTokens * parseInt(offerEntity.rateInBaseUnit)
|
||||
const satsNeeded = Math.ceil(offerEntity.numTokens * parseInt(offerEntity.rateInBaseUnit))
|
||||
if (isNaN(satsNeeded)) {
|
||||
throw new Error('Could not calculate sats needed!')
|
||||
}
|
||||
@@ -467,7 +467,7 @@ class OfferUseCases {
|
||||
console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`)
|
||||
|
||||
// Ensure the 3rd output (vout=2) contains the required amount of BCH.
|
||||
const satsToReceive = orderData.numTokens * parseInt(orderData.rateInBaseUnit)
|
||||
const satsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit))
|
||||
if (isNaN(satsToReceive)) {
|
||||
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
|
||||
}
|
||||
@@ -497,6 +497,45 @@ class OfferUseCases {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Write unit tests for this function, then add it to the Timer Controllers
|
||||
// Looks for duplicate offers and removes the duplicate.
|
||||
// Duplicates are checked for when an offer is created, and so should not
|
||||
// exist. But emperical testing shows that they do. This function is called
|
||||
// periodically by a timer, to clean up any duplicates that slipped through
|
||||
// the cracks.
|
||||
async removeDuplicateOffers () {
|
||||
try {
|
||||
const now = new Date()
|
||||
console.log(`Starting removeDuplicateOffers() at ${now.toLocaleString()}`)
|
||||
|
||||
let duplicateFound = false
|
||||
|
||||
// Get all Offers in the database.
|
||||
const offers = await this.OfferModel.find({})
|
||||
// console.log('offers: ', offers)
|
||||
|
||||
const offerZcids = []
|
||||
|
||||
for (let i = 0; i < offers.length; i++) {
|
||||
const thisOffer = offers[i]
|
||||
|
||||
if (offerZcids.includes(thisOffer.p2wdbHash)) {
|
||||
await thisOffer.remove()
|
||||
duplicateFound = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the zcid to the array.
|
||||
offerZcids.push(thisOffer.p2wdbHash)
|
||||
}
|
||||
|
||||
return duplicateFound
|
||||
} catch (err) {
|
||||
console.error('Error in removeDuplicateOffers()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// This function is called by the garbage collection timer controller. It
|
||||
// checks the UTXO associated with each Offer in the database. If the UTXO
|
||||
// has been spent, the Offer is deleted from the database.
|
||||
@@ -515,12 +554,6 @@ class OfferUseCases {
|
||||
|
||||
let utxoStatus = null
|
||||
try {
|
||||
// Get the status of the UTXO associate with this Offer.
|
||||
// utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
|
||||
// thisOffer.utxoTxid,
|
||||
// thisOffer.utxoVout
|
||||
// )
|
||||
|
||||
// Get the status of the UTXO associate with this Offer.
|
||||
const utxo = {
|
||||
tx_hash: thisOffer.utxoTxid,
|
||||
@@ -528,7 +561,8 @@ class OfferUseCases {
|
||||
}
|
||||
// console.log(`Checking this UTXO: ${JSON.stringify(utxo, null, 2)}`)
|
||||
|
||||
utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||
// 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) {
|
||||
// Handle corner case of bad-data in the Offer model.
|
||||
@@ -545,7 +579,8 @@ class OfferUseCases {
|
||||
}
|
||||
|
||||
// If the Offer UTXO is spent, delete the Offer model.
|
||||
if (!utxoStatus) {
|
||||
if (utxoStatus === false) {
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`)
|
||||
await thisOffer.remove()
|
||||
}
|
||||
|
||||
+28
-18
@@ -2,9 +2,11 @@
|
||||
Order use-case library.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import RetryQueue from '@chris.troutner/retry-queue'
|
||||
|
||||
// Local libraries
|
||||
import wlogger from '../adapters/wlogger.js'
|
||||
|
||||
import OrderEntity from '../entities/order.js'
|
||||
import config from '../../config/index.js'
|
||||
|
||||
@@ -23,6 +25,10 @@ class OrderLib {
|
||||
this.OrderModel = this.adapters.localdb.Order
|
||||
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)
|
||||
}
|
||||
|
||||
// Create a new order model and add it to the Mongo database.
|
||||
@@ -37,33 +43,33 @@ class OrderLib {
|
||||
entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress
|
||||
console.log('entryObj.makerAddr: ', entryObj.makerAddr)
|
||||
|
||||
// Get Ticker for token ID.
|
||||
// TODO: Move this below the orderEntity.validate() call.
|
||||
const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId])
|
||||
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
|
||||
entryObj.ticker = tokenData[0].tokenTicker
|
||||
|
||||
// Input Validation
|
||||
// TODO: Remove ticker from validation.
|
||||
// TODO: Rename validate() to inputValidate(). Create fullValidate() that
|
||||
// validates a fully hydrated Order entity.
|
||||
const orderEntity = this.orderEntity.validate(entryObj)
|
||||
const orderEntity = this.orderEntity.inputValidate(entryObj)
|
||||
console.log('orderEntity: ', orderEntity)
|
||||
|
||||
// Ensure sufficient tokens exist to create the order.
|
||||
await this.ensureFunds(orderEntity)
|
||||
// await 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])
|
||||
// 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
|
||||
}
|
||||
const utxoInfo = await this.adapters.wallet.moveTokens(moveObj)
|
||||
// const utxoInfo = await this.adapters.wallet.moveTokens(moveObj)
|
||||
const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokens, moveObj)
|
||||
console.log('utxoInfo: ', utxoInfo)
|
||||
|
||||
// Update the UTXO store for the wallet.
|
||||
await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000)
|
||||
await this.adapters.wallet.bchWallet.getUtxos()
|
||||
// await this.adapters.wallet.bchWallet.getUtxos()
|
||||
await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
|
||||
|
||||
// Update the order with the new UTXO information.
|
||||
orderEntity.utxoTxid = utxoInfo.txid
|
||||
@@ -79,7 +85,8 @@ class OrderLib {
|
||||
data: orderEntity,
|
||||
appId: this.config.p2wdbAppId
|
||||
}
|
||||
const hash = await this.adapters.p2wdb.write(p2wdbObj)
|
||||
// const hash = await this.adapters.p2wdb.write(p2wdbObj)
|
||||
const hash = await this.retryQueue.addToQueue(this.adapters.p2wdb.write, p2wdbObj)
|
||||
// console.log('hash: ', hash)
|
||||
|
||||
// Create a MongoDB model to hold the Order
|
||||
@@ -108,6 +115,7 @@ class OrderLib {
|
||||
|
||||
// Update the wallet balance
|
||||
await this.adapters.wallet.bchWallet.getUtxos()
|
||||
// console.log(`wallet UTXOs: ${JSON.stringify(this.adapters.wallet.bchWallet.utxos.utxoStore, null, 2)}`)
|
||||
|
||||
// Ensure the app wallet has enough funds to write to the P2WDB.
|
||||
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
|
||||
@@ -116,7 +124,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
|
||||
@@ -209,7 +217,8 @@ class OrderLib {
|
||||
tx_hash: thisOrder.utxoTxid,
|
||||
tx_pos: thisOrder.utxoVout
|
||||
}
|
||||
utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
|
||||
// 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) {
|
||||
// Handle corner case of bad-data in the Order model.
|
||||
@@ -226,7 +235,8 @@ class OrderLib {
|
||||
}
|
||||
|
||||
// If the Order UTXO is spent, delete the Order model.
|
||||
if (!utxoStatus) {
|
||||
if (utxoStatus === false) {
|
||||
console.log('utxoStatus: ', utxoStatus)
|
||||
console.log(`Spent UTXO detected. Deleting this Order: ${JSON.stringify(thisOrder, null, 2)}`)
|
||||
await thisOrder.remove()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// const BCHJS = require('@psf/bch-js')
|
||||
import BchWallet from 'minimal-slp-wallet/index.js'
|
||||
import BchWallet from 'minimal-slp-wallet'
|
||||
|
||||
// Local libraries.
|
||||
import P2wdbAdapter from '../../../src/adapters/p2wdb-adapter.js'
|
||||
|
||||
@@ -21,10 +21,10 @@ describe('#Order-Entity', () => {
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#validate', () => {
|
||||
describe('#inputValidate', () => {
|
||||
it('should throw an error if data is not provided', () => {
|
||||
try {
|
||||
uut.validate()
|
||||
uut.inputValidate()
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'Cannot destructure property')
|
||||
@@ -34,7 +34,7 @@ describe('#Order-Entity', () => {
|
||||
it('should throw an error if messageType is not included', () => {
|
||||
try {
|
||||
const data = {}
|
||||
uut.validate(data)
|
||||
uut.inputValidate(data)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
@@ -47,7 +47,7 @@ describe('#Order-Entity', () => {
|
||||
it('should throw an error if messageClass is not included', () => {
|
||||
try {
|
||||
const data = { messageType: 1 }
|
||||
uut.validate(data)
|
||||
uut.inputValidate(data)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
@@ -60,7 +60,7 @@ describe('#Order-Entity', () => {
|
||||
it('should throw an error if tokenId is not included', () => {
|
||||
try {
|
||||
const data = { messageType: 1, messageClass: 1 }
|
||||
uut.validate(data)
|
||||
uut.inputValidate(data)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'tokenId' must be a string.")
|
||||
@@ -70,7 +70,7 @@ describe('#Order-Entity', () => {
|
||||
it('should throw an error if buyOrSell is not included', () => {
|
||||
try {
|
||||
const data = { messageType: 1, messageClass: 1, tokenId: 'fakeId' }
|
||||
uut.validate(data)
|
||||
uut.inputValidate(data)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'buyOrSell' must be a string.")
|
||||
@@ -85,7 +85,7 @@ describe('#Order-Entity', () => {
|
||||
tokenId: 'fakeId',
|
||||
buyOrSell: 'buy'
|
||||
}
|
||||
uut.validate(data)
|
||||
uut.inputValidate(data)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
@@ -104,7 +104,7 @@ describe('#Order-Entity', () => {
|
||||
buyOrSell: 'buy',
|
||||
rateInBaseUnit: 1000
|
||||
}
|
||||
uut.validate(data)
|
||||
uut.inputValidate(data)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
@@ -124,7 +124,7 @@ describe('#Order-Entity', () => {
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 350
|
||||
}
|
||||
uut.validate(data)
|
||||
uut.inputValidate(data)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'numTokens' must be a number.")
|
||||
|
||||
@@ -35,6 +35,8 @@ class MockBchWallet {
|
||||
}]
|
||||
}
|
||||
this.getTokenData = async () => {}
|
||||
this.initialize = async () => {}
|
||||
this.utxoIsValid = async () => {}
|
||||
|
||||
// Environment variable is used by wallet-balance.unit.js to force an error.
|
||||
if (process.env.NO_UTXO) {
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('#offer-use-case', () => {
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.bchjs.Blockchain, 'getTxOut').resolves(null)
|
||||
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)
|
||||
@@ -111,18 +111,7 @@ describe('#offer-use-case', () => {
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.bchjs.Blockchain, 'getTxOut').resolves({
|
||||
bestblock:
|
||||
'000000000000000000d2060b83f90f8187b92fcccb4a42aaa19ce5a305fe0ae3',
|
||||
confirmations: 0,
|
||||
value: 0,
|
||||
scriptPubKey: {
|
||||
asm: 'OP_RETURN 5262419 1 1145980243 38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0 00000000001e8480 0000000009a7ec80',
|
||||
hex: '6a04534c500001010453454e442038e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b00800000000001e8480080000000009a7ec80',
|
||||
type: 'nulldata'
|
||||
},
|
||||
coinbase: false
|
||||
})
|
||||
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)
|
||||
@@ -171,4 +160,35 @@ describe('#offer-use-case', () => {
|
||||
assert.equal(result, 'fungible')
|
||||
})
|
||||
})
|
||||
|
||||
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 () => {} }
|
||||
])
|
||||
// sandbox.stub(uut.OfferModel, 'remove').resolves()
|
||||
|
||||
const result = await uut.removeDuplicateOffers()
|
||||
console.log('result: ', result)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
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 () => {} }
|
||||
])
|
||||
// sandbox.stub(uut.OfferModel, 'remove').resolves()
|
||||
|
||||
const result = await uut.removeDuplicateOffers()
|
||||
console.log('result: ', result)
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -103,17 +103,16 @@ describe('#order-use-case', () => {
|
||||
buyOrSell: 'sell',
|
||||
rateInBaseUnit: 1000,
|
||||
minUnitsToExchange: 1250,
|
||||
numTokens: 1
|
||||
numTokens: 1,
|
||||
ticker: 'TEST'
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
// sandbox.stub(uut.adapters.wallet, 'burnPsf').resolves('fakeTxid')
|
||||
// sandbox.stub(uut.adapters.wallet.bchWallet, 'getTxData').resolves({ tokenTicker: 'TROUT' })
|
||||
sandbox.stub(uut.orderEntity, 'validate').returns(entryObj)
|
||||
// Mock dependencies and force expected code path
|
||||
sandbox.stub(uut.orderEntity, 'inputValidate').returns(entryObj)
|
||||
sandbox.stub(uut, 'ensureFunds').resolves()
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.Util, 'sleep').resolves()
|
||||
sandbox.stub(uut.adapters.wallet, 'moveTokens').resolves({ txid: 'fakeTxid', vout: 0, hdIndex: 1 })
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'getUtxos').resolves()
|
||||
sandbox.stub(uut.adapters.wallet.bchWallet, 'initialize').resolves()
|
||||
sandbox.stub(uut.adapters.p2wdb, 'write').resolves('fakeHash')
|
||||
|
||||
const result = await uut.createOrder(entryObj)
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
This script has not been customized for offers yet.
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose')
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
// Force test environment
|
||||
// make sure environment variable is set before this file gets called.
|
||||
// see test script in package.json.
|
||||
// process.env.KOA_ENV = 'test'
|
||||
const config = require('../../config')
|
||||
import config from '../../config'
|
||||
|
||||
const User = require('../../src/models/users')
|
||||
import User from '../../src/models/users'
|
||||
|
||||
async function deleteUsers () {
|
||||
// Connect to the Mongo Database.
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
Get all Offers in the database.
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose')
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
const Offer = require('../../src/adapters/localdb/models/offer')
|
||||
import config from '../../config/index.js'
|
||||
import Offer from '../../src/adapters/localdb/models/offer.js'
|
||||
|
||||
async function getOffers () {
|
||||
// Connect to the Mongo Database.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const mongoose = require('mongoose')
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
// Force test environment
|
||||
// make sure environment variable is set before this file gets called.
|
||||
// see test script in package.json.
|
||||
// process.env.KOA_ENV = 'test'
|
||||
const config = require('../../config')
|
||||
import config from '../../config'
|
||||
|
||||
const User = require('../../src/models/users')
|
||||
import User from '../../src/models/users'
|
||||
|
||||
async function deleteUsers () {
|
||||
// Connect to the Mongo Database.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
const Order = require('../../src/adapters/localdb/models/order')
|
||||
import mongoose from 'mongoose'
|
||||
import config from '../../config/index.js'
|
||||
import Order from '../../src/adapters/localdb/models/order.js'
|
||||
|
||||
async function getOrder () {
|
||||
// Connect to the Mongo Database.
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const BchTokenSweep = require('bch-token-sweep/index')
|
||||
import BCHJS from '@psf/bch-js'
|
||||
|
||||
import BchTokenSweep from 'bch-token-sweep/index'
|
||||
|
||||
// Local libraries
|
||||
const WalletAdapter = require('../../src/adapters/wallet')
|
||||
import WalletAdapter from '../../src/adapters/wallet'
|
||||
|
||||
// Constants
|
||||
const EMTPY_ADDR_CUTOFF = 15
|
||||
|
||||
Reference in New Issue
Block a user