Compare commits

...
9 Commits
10 changed files with 137 additions and 20 deletions
+5 -1
View File
@@ -191,5 +191,9 @@ export default {
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
// Admin password
adminPassword: process.env.ADMIN_PASSWORD
adminPassword: process.env.ADMIN_PASSWORD,
// The Operator of BCH DEX can elect to receive a percentage of each sale.
operatorAddress: process.env.OPERATOR_ADDRESS ? process.env.OPERATOR_ADDRESS : 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
operatorPercentage: process.env.OPERATOR_PERCENTAGE ? parseFloat(process.env.OPERATOR_PERCENTAGE) : 2.0
}
+4 -1
View File
@@ -35,8 +35,11 @@ const Offer = new mongoose.Schema({
lokadId: { type: String },
messageType: { type: Number },
messageClass: { type: Number },
nostrEventId: { type: String } // Nostr Event Id.
nostrEventId: { type: String }, // Nostr Event Id.
// Operator fee and address
operatorAddress: { type: String },
operatorPercentage: { type: Number }
})
export default mongoose.model('offer', Offer)
+5 -1
View File
@@ -43,7 +43,11 @@ const Order = new mongoose.Schema({
// Additional properties found in createOrder
dataType: { type: String, required: true },
userId: { type: String, required: true }
userId: { type: String, required: true },
// Operator fee and address
operatorAddress: { type: String },
operatorPercentage: { type: Number }
})
export default mongoose.model('order', Order)
+12 -2
View File
@@ -1,6 +1,6 @@
/*
Offer Entity
An ffer is created when a new Signal is detected via the P2WDB webhook.
An offer is created when a new Signal is detected via the P2WDB webhook.
It's destroyed when the UTXO described in the Signal has been detected as spent.
*/
@@ -33,7 +33,9 @@ class OfferEntity {
makerAddr,
ticker,
tokenType,
nostrEventId
nostrEventId,
operatorAddress,
operatorPercentage
} = offerData.data
// Input Validation
@@ -80,6 +82,14 @@ class OfferEntity {
throw new Error("Property 'nostrEventId' must be a string.")
}
// Add the operator address and percentage to the offer.
if (!operatorAddress || typeof operatorAddress !== 'string') {
throw new Error("Property 'operatorAddress' must be a string.")
}
if (!operatorPercentage || typeof operatorPercentage !== 'number') {
throw new Error("Property 'operatorPercentage' must be a number.")
}
// Convert the timestamp to a number.
let timestamp = new Date(offerData.timestamp)
timestamp = timestamp.getTime()
+46 -6
View File
@@ -111,7 +111,10 @@ class OfferUseCases {
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
// console.log('utxoStatus: ', utxoStatus)
// if (utxoStatus === null) return false
if (!utxoStatus) return false
if (!utxoStatus) {
console.log(`UTXO txid: ${offerObj.data.utxoTxid}, vout: ${offerObj.data.utxoVout} has been spent. Skipping.`)
return false
}
// A new offer gets a status of 'posted'
offerObj.data.offerStatus = 'posted'
@@ -513,9 +516,8 @@ class OfferUseCases {
// }
}
// This function is called by the P2WDB webhook REST API handler. When a
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
// to this function. It does due diligence on the Counter Offer, then signs
// This function is called by loadOffers().
// It does due diligence on the Counter Offer, then signs
// and broadcasts the transaction to accept the Counter Offer.
async acceptCounterOffer (offerData) {
try {
@@ -528,6 +530,8 @@ class OfferUseCases {
return false
}
console.log(`New counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerData.data.nostrEventId)}`)
// See if this instance of bch-dex is managing the Order associated with
// the incoming Counter Offer.
@@ -599,6 +603,16 @@ class OfferUseCases {
throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`)
}
// Ensure the Counter Offer has an output for the Operator of bch-dex.
if (!txObj.vout[3]) {
console.log('The Counter Offer does not have an output for the Operator.')
// 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'
}
// Ensure the 3rd output (vout=2) is going to the maker address specified
// in the Offer.
const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0]
@@ -608,6 +622,31 @@ class OfferUseCases {
throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`)
}
// Ensure the 4th output (vout=3) is going to the operator address specified in the Offer.
const operatorAddr = orderData.operatorAddress
const hasCorrectOperatorAddr = operatorAddr === txObj.vout[3].scriptPubKey.addresses[0]
if (!hasCorrectOperatorAddr) {
throw new Error(`The Counter Offer has an output address of ${txObj.vout[3].scriptPubKey.addresses[0]}, which does not match the Operator address of ${operatorAddr} in the Offer.`)
}
// Ensure the 4th output (vout=3) contains the required amount of BCH.
const operatorSatsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit))
if (isNaN(operatorSatsToReceive)) {
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
}
const operatorSatsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[3].value)
let estimatedOperatorFee = Math.floor(txObj.vout[3].value * orderData.operatorPercentage / 100)
if (estimatedOperatorFee < 546) estimatedOperatorFee = 546
console.log('operatorSatsOut: ', operatorSatsOut, 'estimatedOperatorFee: ', estimatedOperatorFee)
if (operatorSatsOut < estimatedOperatorFee) {
console.log(`Skipping: The Counter Offer has an output of ${operatorSatsOut}, which is less than the estimated operator fee of ${estimatedOperatorFee}.`)
// 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'
}
// Get the User ID from the Order model.
const userId = orderData.userId
@@ -718,7 +757,8 @@ class OfferUseCases {
await thisOffer.remove()
}
// If the Offer is older than 7 days, delete it.
// TODO: Instead of deleting the offer, send the token back to the Makers wallet.
// If the Offer is older than a threshold, delete it.
const nowMs = now.getTime()
const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
const eightWeeksAgo = nowMs - eightWeeks
@@ -795,7 +835,7 @@ class OfferUseCases {
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)}`)
// console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
await this.acceptCounterOffer(offerObj)
}
+7 -7
View File
@@ -44,6 +44,7 @@ class OrderLib {
if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
// Get the user data from the database, for the user creating the new Order.
const user = await this.UserModel.findById(entryObj.userId)
if (!user) throw new Error('user not found')
@@ -81,13 +82,7 @@ class OrderLib {
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
await userWallet.optimize()
// Ensure sufficient tokens exist to create the order.
// 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])
const tokenData = await userWallet.getTxData([entryObj.tokenId])
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
orderEntity.ticker = tokenData[0].tokenTicker
@@ -107,15 +102,20 @@ class OrderLib {
// await this.adapters.wallet.bchWallet.getUtxos()
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
await userWallet.initialize()
// Update the order with the new UTXO information.
orderEntity.utxoTxid = utxoInfo.txid
orderEntity.utxoVout = utxoInfo.vout
orderEntity.tokenType = utxoInfo.tokenType
// Add P2WDB specific flag for signaling that this is a new offer.
// Add flag to signal that this is a new offer.
orderEntity.dataType = 'offer'
orderEntity.userId = user._id
// Add the operator address and percentage to the order.
orderEntity.operatorAddress = this.config.operatorAddress
orderEntity.operatorPercentage = this.config.operatorPercentage
// Post the new Order information to Nostr under the topic set in the
// config file.
const postObj = {
+3 -1
View File
@@ -286,7 +286,9 @@ describe('#Offer-Entity', () => {
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
ticker: 'TROUT',
tokenType: 1,
nostrEventId: 'test'
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
+35 -1
View File
@@ -142,10 +142,37 @@ const offerMockData = {
utxoVout: 0,
makerAddr: 'address',
tokenType: 1,
nostrEventId: 'test'
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478',
operatorPercentage: 10
}
}
const deserealizeTxMockNoOperatorOut = {
//...
vout: [
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//...
]
}
const deserealizeTxMock = {
//...
vout: [
@@ -167,6 +194,12 @@ const deserealizeTxMock = {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//...
]
}
@@ -179,5 +212,6 @@ export default {
fungibleOffer01,
fungibleTokenData01,
offerMockData,
deserealizeTxMockNoOperatorOut,
deserealizeTxMock
};
@@ -746,6 +746,24 @@ describe('#offer-use-case', () => {
}
})
it('should skip transactions that do not have an output for the operator', async () => {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477' // Unknow Adress
mock.rateInBaseUnit = 0
mock.numTokens = 0
// 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.deserealizeTxMockNoOperatorOut)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should handle error for wrong transaction output address', async () => {
try {
// Mock data
@@ -185,6 +185,7 @@ describe('#order-use-case', () => {
assert.property(result, 'eventId')
assert.property(result, 'noteId')
})
it('should create an order with consumer-api', async () => {
uut.config.useFullStackCash = false
const entryObj = {
@@ -214,6 +215,7 @@ describe('#order-use-case', () => {
assert.property(result, 'eventId')
assert.property(result, 'noteId')
})
it('should throw error if user is not found', async () => {
try {
const entryObj = {