Compare commits

...
4 Commits
12 changed files with 498 additions and 400 deletions
+408 -332
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -28,6 +28,8 @@
},
"repository": "Permissionless-Software-Foundation/bch-dex",
"dependencies": {
"@chris.troutner/retry-queue": "1.0.5",
"@psf/bch-js": "^6.4.5",
"axios": "0.27.2",
"bch-message-lib": "2.2.1",
"bcryptjs": "2.4.3",
@@ -51,7 +53,7 @@
"koa2-ratelimit": "0.9.1",
"libp2p": "^0.36.2",
"line-reader": "0.4.0",
"minimal-slp-wallet": "5.1.0",
"minimal-slp-wallet": "../minimal-slp-wallet/",
"mongoose": "5.13.14",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.7.5",
+4
View File
@@ -27,6 +27,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
View File
@@ -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.
+5 -5
View File
@@ -29,7 +29,7 @@ class OfferEntity {
utxoVout,
offerStatus,
makerAddr,
ticker,
// ticker,
tokenType
} = offerData.data
@@ -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.")
}
@@ -95,7 +95,7 @@ class OfferEntity {
p2wdbHash: offerData.hash,
offerStatus: offerStatus || this.offerStatus[0],
makerAddr,
ticker,
// ticker,
tokenType
}
+10 -8
View File
@@ -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
+27 -12
View File
@@ -15,6 +15,7 @@
// Global npm libraries
import axios from 'axios'
import RetryQueue from '@chris.troutner/retry-queue'
// Local libraries
import OfferEntity from '../../entities/offer.js'
@@ -43,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
@@ -73,14 +77,23 @@ 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 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'
@@ -90,18 +103,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.
@@ -165,7 +180,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)}`)
+22 -15
View File
@@ -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)}`)
entryObj.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
+9 -9
View File
@@ -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.")
+2
View File
@@ -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) {
+2 -13
View File
@@ -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)
+3 -5
View File
@@ -106,14 +106,12 @@ describe('#order-use-case', () => {
numTokens: 1
}
// 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)