mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-22 09:11:59 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d9a1d7cbf | ||
|
|
e6c942d532 | ||
|
|
1206b6576e | ||
|
|
dbd5845e3a | ||
|
|
d14dcae6f5 |
@@ -6,6 +6,7 @@ const Offer = new mongoose.Schema({
|
||||
tokenId: { type: String },
|
||||
utxoTxid: { type: String },
|
||||
utxoVout: { type: Number },
|
||||
ticker: { type: String },
|
||||
|
||||
// Trade data
|
||||
buyOrSell: { type: String },
|
||||
|
||||
@@ -13,6 +13,7 @@ const Order = new mongoose.Schema({
|
||||
tokenId: { type: String },
|
||||
utxoTxid: { type: String },
|
||||
utxoVout: { type: Number },
|
||||
ticker: { type: String },
|
||||
|
||||
// Trade data
|
||||
buyOrSell: { type: String },
|
||||
|
||||
@@ -6,18 +6,12 @@
|
||||
|
||||
// Public npm libraries.
|
||||
|
||||
// Load the Clean Architecture Adapters library
|
||||
// Local libraries
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
// Load the JSON RPC Controller.
|
||||
const JSONRPC = require('./json-rpc')
|
||||
|
||||
// Load the Clean Architecture Use Case libraries.
|
||||
const UseCases = require('../use-cases')
|
||||
// const useCases = new UseCases({ adapters })
|
||||
|
||||
// Load the REST API Controllers.
|
||||
const RESTControllers = require('./rest-api')
|
||||
const TimerControllers = require('./timer-controllers')
|
||||
|
||||
class Controllers {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -33,6 +27,7 @@ class Controllers {
|
||||
// this.attachRESTControllers(app)
|
||||
|
||||
// this.attachRPCControllers()
|
||||
this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases })
|
||||
}
|
||||
|
||||
// Top-level function for this library.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
This Controller library is concerned with timer-based functions that are
|
||||
kicked off periodicially.
|
||||
*/
|
||||
|
||||
// Used to retain scope of 'this', when the scope is lost.
|
||||
let _this
|
||||
|
||||
class TimerControllers {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating Timer Controller libraries.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating Timer Controller libraries.'
|
||||
)
|
||||
}
|
||||
|
||||
this.debugLevel = localConfig.debugLevel
|
||||
|
||||
_this = this
|
||||
|
||||
this.startTimers()
|
||||
}
|
||||
|
||||
// Start all the time-based controllers.
|
||||
startTimers () {
|
||||
setInterval(this.gcOrders, 60000 * 5)
|
||||
setInterval(this.gcOffers, 60000 * 5)
|
||||
}
|
||||
|
||||
// Garbage Collect the Orders.
|
||||
gcOrders () {
|
||||
try {
|
||||
_this.useCases.order.removeStaleOrders()
|
||||
} catch (err) {
|
||||
// Do not throw an error. This is a top-level function.
|
||||
console.log('Error in timer-controllers.js/gcOrders(): ', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Garbage Collect the Offers.
|
||||
gcOffers () {
|
||||
try {
|
||||
_this.useCases.offer.removeStaleOffers()
|
||||
} catch (err) {
|
||||
// Do not throw an error. This is a top-level function.
|
||||
console.log('Error in timer-controllers.js/gcOffers(): ', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TimerControllers
|
||||
@@ -28,7 +28,8 @@ class OfferEntity {
|
||||
utxoTxid,
|
||||
utxoVout,
|
||||
offerStatus,
|
||||
makerAddr
|
||||
makerAddr,
|
||||
ticker
|
||||
} = offerData.data
|
||||
|
||||
// Input Validation
|
||||
@@ -65,6 +66,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.")
|
||||
}
|
||||
|
||||
const validatedOfferData = {
|
||||
messageType,
|
||||
@@ -81,7 +85,8 @@ class OfferEntity {
|
||||
txid: offerData.txid,
|
||||
p2wdbHash: offerData.hash,
|
||||
offerStatus: offerStatus || this.offerStatus[0],
|
||||
makerAddr
|
||||
makerAddr,
|
||||
ticker
|
||||
}
|
||||
|
||||
return validatedOfferData
|
||||
|
||||
@@ -16,7 +16,8 @@ class Order {
|
||||
rateInBaseUnit,
|
||||
minUnitsToExchange,
|
||||
numTokens,
|
||||
makerAddr
|
||||
makerAddr,
|
||||
ticker
|
||||
} = data
|
||||
|
||||
// Input Validation
|
||||
@@ -44,6 +45,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.")
|
||||
}
|
||||
|
||||
const offerData = {
|
||||
messageType,
|
||||
@@ -53,7 +57,8 @@ class Order {
|
||||
rateInBaseUnit,
|
||||
minUnitsToExchange,
|
||||
numTokens,
|
||||
makerAddr
|
||||
makerAddr,
|
||||
ticker
|
||||
}
|
||||
|
||||
return offerData
|
||||
|
||||
@@ -29,7 +29,7 @@ class OfferUseCases {
|
||||
this.orderUseCase = localConfig.order
|
||||
if (!this.orderUseCase) {
|
||||
throw new Error(
|
||||
'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.'
|
||||
'Instance of Offer Use Cases must be passed in when instantiating Offer Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -299,6 +299,53 @@ class OfferUseCases {
|
||||
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.
|
||||
async removeStaleOffers () {
|
||||
try {
|
||||
const now = new Date()
|
||||
console.log(`Starting garbage collection for Offers at ${now.toLocaleString()}`)
|
||||
|
||||
// Get all Offers in the database.
|
||||
const offers = await this.OfferModel.find({})
|
||||
// console.log('offers: ', offers)
|
||||
|
||||
// Loop through each Offer and ensure the UTXO is still valid.
|
||||
for (let i = 0; i < offers.length; i++) {
|
||||
const thisOffer = offers[i]
|
||||
|
||||
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
|
||||
)
|
||||
// console.log('utxoStatus: ', utxoStatus)
|
||||
} catch (err) {
|
||||
// Handle corner case of bad-data in the Offer model.
|
||||
if (err.message.includes('txid needs to be a proper transaction ID')) {
|
||||
console.log(`Deleting Offer with bad data: ${JSON.stringify(thisOffer, null, 2)}`)
|
||||
await thisOffer.remove()
|
||||
continue
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// If the Offer UTXO is spent, delete the Offer model.
|
||||
if (utxoStatus === null) {
|
||||
console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`)
|
||||
await thisOffer.remove()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in removeStaleOffers()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OfferUseCases
|
||||
|
||||
@@ -33,6 +33,11 @@ class OrderLib {
|
||||
entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress
|
||||
console.log('entryObj.makerAddr: ', entryObj.makerAddr)
|
||||
|
||||
// Get Ticker for token ID.
|
||||
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
|
||||
const orderEntity = this.orderEntity.validate(entryObj)
|
||||
console.log('orderEntity: ', orderEntity)
|
||||
@@ -158,6 +163,53 @@ class OrderLib {
|
||||
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.
|
||||
async removeStaleOrders () {
|
||||
try {
|
||||
const now = new Date()
|
||||
console.log(`Starting garbage collection for Orders at ${now.toLocaleString()}`)
|
||||
|
||||
// Get all Orders in the database.
|
||||
const orders = await this.OrderModel.find({})
|
||||
// console.log('orders: ', orders)
|
||||
|
||||
// Loop through each Order and ensure the UTXO is still valid.
|
||||
for (let i = 0; i < orders.length; i++) {
|
||||
const thisOrder = orders[i]
|
||||
|
||||
let utxoStatus = null
|
||||
try {
|
||||
// Get the status of the UTXO associate with this Order.
|
||||
utxoStatus = await this.adapters.bchjs.Blockchain.getTxOut(
|
||||
thisOrder.utxoTxid,
|
||||
thisOrder.utxoVout
|
||||
)
|
||||
// console.log('utxoStatus: ', utxoStatus)
|
||||
} catch (err) {
|
||||
// Handle corner case of bad-data in the Order model.
|
||||
if (err.message.includes('txid needs to be a proper transaction ID')) {
|
||||
console.log(`Deleting Order with bad data: ${JSON.stringify(thisOrder, null, 2)}`)
|
||||
await thisOrder.remove()
|
||||
continue
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// If the Order UTXO is spent, delete the Order model.
|
||||
if (utxoStatus === null) {
|
||||
console.log(`Spent UTXO detected. Deleting this Order: ${JSON.stringify(thisOrder, null, 2)}`)
|
||||
await thisOrder.remove()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in removeStaleOrders()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OrderLib
|
||||
|
||||
@@ -253,12 +253,14 @@ describe('#Offer-Entity', () => {
|
||||
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
utxoVout: 0,
|
||||
offerStatus: 'posted',
|
||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00'
|
||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
ticker: 'TROUT'
|
||||
},
|
||||
timestamp: '2021-09-20T17:54:26.395Z',
|
||||
localTimeStamp: '9/20/2021, 10:54:26 AM',
|
||||
txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
|
||||
hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
|
||||
|
||||
}
|
||||
|
||||
const result = uut.validate(offerObj)
|
||||
|
||||
@@ -29,6 +29,11 @@ class MockBchWallet {
|
||||
}
|
||||
this.getUtxos = async () => {
|
||||
}
|
||||
this.getTxData = async () => {
|
||||
return [{
|
||||
tokenTicker: 'TROUT'
|
||||
}]
|
||||
}
|
||||
|
||||
// Environment variable is used by wallet-balance.unit.js to force an error.
|
||||
if (process.env.NO_UTXO) {
|
||||
|
||||
@@ -95,7 +95,8 @@ describe('#offer-use-case', () => {
|
||||
utxoTxid:
|
||||
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
|
||||
utxoVout: 0,
|
||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00'
|
||||
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
ticker: 'TROUT'
|
||||
},
|
||||
timestamp: '2021-09-20T17:54:26.395Z',
|
||||
localTimeStamp: '9/20/2021, 10:54:26 AM',
|
||||
|
||||
@@ -109,13 +109,12 @@ describe('#order-use-case', () => {
|
||||
|
||||
// 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)
|
||||
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, 'generateSignature')
|
||||
// .resolves('fakeSignature')
|
||||
sandbox.stub(uut.adapters.p2wdb, 'write').resolves('fakeHash')
|
||||
|
||||
const result = await uut.createOrder(entryObj)
|
||||
|
||||
Reference in New Issue
Block a user