Compare commits

...
31 Commits
Author SHA1 Message Date
Chris Troutner 1a96eac8c9 Merge pull request #115 from Permissionless-Software-Foundation/ct-unstable
Adding deletedChat and deletedPost DB models
2025-10-25 16:03:55 -07:00
Chris Troutner a2acdef539 fix(database): Adding deletedChat and deletedPost DB models 2025-10-25 16:02:35 -07:00
Chris Troutner 58ddbbcc4e fix(updateUser): Adding util scripts for dev & prod 2025-10-25 14:35:22 -07:00
Chris Troutner 5ff33ff19f Merge remote-tracking branch 'upstream/master' into ct-unstable 2025-10-25 14:34:05 -07:00
Chris Troutner 682f599261 fixing updateUser script 2025-10-25 14:33:51 -07:00
Chris Troutner 7c26e33a8c Merge pull request #181 from Permissionless-Software-Foundation/ct-unstable
fix(util): Fixing problem with updateUser.js script
2025-10-25 14:33:12 -07:00
Chris Troutner 2595ee8295 fix(util): Fixing problem with updateUser.js script 2025-10-25 14:31:33 -07:00
Chris Troutner 531685c0e4 Fixing merge conflicts with upstream 2025-10-25 14:26:16 -07:00
Chris Troutner 476fdcd0aa Merge pull request #180 from Permissionless-Software-Foundation/ct-unstable
fix(utils): Adding script to update users password or convert to admin
2025-10-25 14:24:43 -07:00
Chris Troutner b6762b215e fix(utils): Adding script to update users password or convert to admin 2025-10-25 14:23:28 -07:00
Chris Troutner 3cdf49bb56 Merge pull request #114 from Permissionless-Software-Foundation/dh-update-endpoint
feat(update): Added Update button REST endpoint
2025-10-25 11:25:02 -07:00
Chris Troutner 28615403b8 Formatting and adding dev comments 2025-10-25 11:22:13 -07:00
Daniel Gonzalez bb10bd819a feat(update): Added Update button REST endpoint 2025-10-24 19:58:56 -04:00
Chris Troutner 3882217e6a Merge pull request #113 from Permissionless-Software-Foundation/dh-user-data
feat(userdata): Store user data on the database
2025-10-21 12:57:45 -07:00
Chris Troutner 8cd2896115 linting 2025-10-21 12:56:23 -07:00
Chris Troutner e570519ec8 Switching default back end to free-bch 2025-10-21 12:56:07 -07:00
Chris Troutner 2f74b74110 fix(createOffer()): Handling API errors when token data can not be retrieved 2025-10-21 12:44:48 -07:00
Chris Troutner fddb5499f0 Making offer use-case unit tests less flaky 2025-10-21 12:29:23 -07:00
Daniel Gonzalez fed98d51be feat(userdata): Store user data on the database 2025-10-20 18:37:54 -04:00
Chris Troutner 8e3ee3a0fb Merge pull request #112 from Permissionless-Software-Foundation/dh-offer-model
feat(offer): Store mutable data in bch-dex Offer db model
2025-10-17 22:30:36 -07:00
Daniel Gonzalez e57faf8f26 feat(offer): Store mutable data in bch-dex Offer db model 2025-10-16 18:38:06 -04:00
Chris Troutner 61fa6b0ed6 Merge pull request #111 from Permissionless-Software-Foundation/dh-pagination
feat(NFTs): Added pagination controls for NFTs
2025-10-09 07:21:07 -07:00
Daniel Gonzalez 99843061ab feat(NFTs): Added pagination controls for NFTs 2025-10-08 20:20:11 -04:00
Chris Troutner 3a23763800 Adding an extra property to offer db model 2025-10-04 09:44:15 -07:00
Chris Troutner 3f4d49a228 Merge pull request #110 from Permissionless-Software-Foundation/ct-unstable
fix(offer): Adding new properties to offer db model
2025-10-04 09:41:39 -07:00
Chris Troutner 88cfadbc17 fix(offer): Adding new properties to offer db model 2025-10-04 09:40:07 -07:00
Chris Troutner 6c1da41cb3 Syncing with upstream. Fixing merge conflicts 2025-10-04 09:00:38 -07:00
Chris Troutner 0e6a411012 Merge pull request #179 from Permissionless-Software-Foundation/ct-unstable
fix(IPFS): Adding API docs for the IPFS endpoints that need it
2025-10-04 08:32:55 -07:00
Chris Troutner 55e9e35c1f fix(IPFS): Adding API docs for the IPFS endpoints that need it 2025-10-04 08:31:17 -07:00
Chris Troutner 84603356ef Merge pull request #109 from Permissionless-Software-Foundation/dh-access-validation
feat(test): Added user/admin access validation to endpoint
2025-10-03 06:17:40 -07:00
Daniel Gonzalez 2f6b4dfc09 feat(test): Added user/admin access validation to endpoint 2025-10-01 17:24:29 -04:00
20 changed files with 630 additions and 46 deletions
+2 -2
View File
@@ -47,8 +47,8 @@ export default {
useFullStackCash: process.env.USE_FULLSTACKCASH ? true : false,
consumerUrl: process.env.CONSUMER_URL
? process.env.CONSUMER_URL
// : 'https://free-bch.fullstack.cash',
: 'https://dev-consumer.psfoundation.info',
: 'https://free-bch.fullstack.cash',
// : 'https://dev-consumer.psfoundation.info',
// : 'https://wa-usa-bch-consumer.fullstackcash.nl',
// P2WDB URL that will accept API calls from the p2wdb npm library.
@@ -0,0 +1,16 @@
/*
Database model for tracking deleted Nostr chat messages.
*/
import mongoose from 'mongoose'
const DeletedChat = new mongoose.Schema({
eventId: { type: String, required: true, default: null }, // Event ID of the deleted message.
npub: { type: String, default: null }, // Nostr public key of the user who created the message.
bchAddr: { type: String, default: null }, // bch address of the user who created the message.
pubkey: { type: String, default: null }, // Nostr public key of the user who created the message.
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
})
export default mongoose.model('deletedChat', DeletedChat)
@@ -0,0 +1,16 @@
/*
Database model for tracking deleted Nostr posts.
*/
import mongoose from 'mongoose'
const DeletedPost = new mongoose.Schema({
eventId: { type: String, required: true, default: null }, // Event ID of the deleted post.
npub: { type: String, default: null }, // Nostr public key of the user who created the post.
bchAddr: { type: String, default: null }, // bch address of the user who created the post.
pubkey: { type: String, default: null }, // Nostr public key of the user who created the post.
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
})
export default mongoose.model('deletedPost', DeletedPost)
+10 -1
View File
@@ -39,7 +39,16 @@ const Offer = new mongoose.Schema({
// Operator fee and address
operatorAddress: { type: String },
operatorPercentage: { type: Number }
operatorPercentage: { type: Number },
// Token Data
immutableDataCid: { type: String, default: null },
mutableDataCid: { type: String, default: null },
tokenIconUrl: { type: String, default: null },
tokenCategories: { type: Array, default: [] },
tokenTags: { type: Array, default: [] },
userDataStr: { type: String }, // Token user data
lastUpdatedTokenData: { type: String, default: null } // ISO timestamp of the last time token data was updated.
})
export default mongoose.model('offer', Offer)
+20
View File
@@ -56,6 +56,7 @@ class WalletAdapter {
this.completeTx = this.completeTx.bind(this)
this.reclaimTokens = this.reclaimTokens.bind(this)
this.moveTokensFromCustomWallet = this.moveTokensFromCustomWallet.bind(this)
this.cid2json = this.cid2json.bind(this)
}
// Open the wallet file, or create one if the file doesn't exist.
@@ -677,6 +678,25 @@ class WalletAdapter {
throw err
}
}
async cid2json (urlOrCid) {
try {
// Input validation
if (!urlOrCid || typeof urlOrCid !== 'string') {
throw new Error('urlOrCid must be a string!')
}
// Extract the cid from the url or cid.
const cid = urlOrCid.split('/').pop()
console.log('cid to json: ', cid)
const jsonRes = await this.bchWallet.cid2json({ cid })
const json = jsonRes.json
// console.log('json: ', json)
return json
} catch (err) {
console.error('Error in wallet.js/cid2json()', err.message)
throw err
}
}
}
export default WalletAdapter
+33 -11
View File
@@ -71,16 +71,25 @@ class IpfsRESTControllerLib {
}
/**
* @api {POST} /ipfs/peers Get IPFS peers
* @api {post} /ipfs/peers Get information on IPFS peers this node is connected to
* @apiPermission public
* @apiName GetIpfsPeers
* @apiGroup REST IPFS
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "showAll": true }' localhost:5001/ipfs/peers
* @apiParam {Boolean} [showAll=false] Whether to include detailed peer data
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/peers \
* -d '{"showAll": false}'
*
* @apiSuccess {Object[]} peers Array of peer objects
* @apiSuccess {String} peers[].peer Peer ID
* @apiSuccess {String} peers[].name Peer name
* @apiSuccess {String} peers[].protocol Protocol used by the peer
* @apiSuccess {String} peers[].version Peer version
* @apiSuccess {String} peers[].connectionAddr Connection address
* @apiSuccess {Object} [peers[].peerData] Detailed peer data (when showAll=true)
*/
// Return information on IPFS peers this node is connected to.
async getPeers (ctx) {
try {
const showAll = ctx.request.body.showAll
@@ -96,16 +105,22 @@ class IpfsRESTControllerLib {
}
/**
* @api {POST} /ipfs/relays Get IPFS relays
* @api {post} /ipfs/relays Get data about the known Circuit Relays
* @apiPermission public
* @apiName GetIpfsRelays
* @apiGroup REST IPFS
*
* @apiDescription Returns information about Circuit Relays, both v1 and v2, that this node knows about. V2 relays are hydrated with peer data from the connected peers list.
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/relays
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* @apiSuccess {Object} relays Object containing relay information
* @apiSuccess {Object[]} relays.v2Relays Array of v2 Circuit Relay objects
* @apiSuccess {String} relays.v2Relays[].ipfsId IPFS ID of the relay
* @apiSuccess {String} relays.v2Relays[].name Name of the relay (hydrated from peer data)
* @apiSuccess {String} relays.v2Relays[].description Description of the relay (hydrated from peer data)
* @apiSuccess {Object[]} relays.v1Relays Array of v1 Circuit Relay configurations
*/
// Get data about the known Circuit Relays. Hydrate with data from peers list.
async getRelays (ctx) {
@@ -121,15 +136,22 @@ class IpfsRESTControllerLib {
}
/**
*
* @api {POST} /ipfs/connect Connect to an IPFS peer
* @api {post} /ipfs/connect Connect to a specific IPFS peer
* @apiPermission public
* @apiName ConnectToIpfsPeer
* @apiGroup REST IPFS
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "multiaddr": "/ip4/123.45.67.89/tcp/4001/p2p/1234567890", "getDetails": true }' localhost:5001/ipfs/connect
* @apiDescription Attempts to establish a connection to a specific IPFS peer using the provided multiaddr. Optionally returns detailed information about the connection.
*
* @apiParam {String} multiaddr Multiaddress of the peer to connect to (required)
* @apiParam {Boolean} [getDetails=false] Whether to return detailed connection information
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/connect \
* -d '{"multiaddr": "/ip4/161.35.99.207/tcp/4001/p2p/12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu", "getDetails": false}'
*
* @apiSuccess {Boolean} success Indicates whether the connection attempt was successful
* @apiSuccess {Object} [details] Additional connection details (when getDetails=true)
*/
async connect (ctx) {
try {
@@ -34,6 +34,7 @@ class OfferRESTControllerLib {
this.listFungibleOffers = this.listFungibleOffers.bind(this)
this.takeOffer = this.takeOffer.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this)
this.syncOfferMutableData = this.syncOfferMutableData.bind(this)
this.handleError = this.handleError.bind(this)
}
@@ -395,6 +396,18 @@ class OfferRESTControllerLib {
}
}
async syncOfferMutableData (ctx) {
try {
const tokenId = ctx.request.body.tokenId
const offer = await this.useCases.offer.syncOfferMutableData(tokenId)
ctx.body = offer
} catch (err) {
console.log('Error in syncOfferMutableData REST API handler: ', err)
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', err.message)
+1
View File
@@ -55,6 +55,7 @@ class OfferRouter {
// Define the routes and attach the controller.
// this.router.post('/', _this.offerRESTController.createOffer) // Deprecated.
this.router.post('/take', this.offerRESTController.takeOffer)
this.router.post('/mutable/sync/', this.offerRESTController.syncOfferMutableData)
this.router.get('/list/all/:page', this.offerRESTController.listOffers)
this.router.get('/list/nft/:page', this.offerRESTController.listNftOffers)
this.router.get('/list/fungible/:page', this.offerRESTController.listFungibleOffers)
+6 -1
View File
@@ -54,7 +54,7 @@ class OrderRouter {
// Define the routes and attach the controller.
this.router.post('/', this.createOrder)
this.router.get('/list/all/:page', _this.orderRESTController.listOrders)
this.router.post('/delete', _this.orderRESTController.deleteOrder)
this.router.post('/delete', this.deleteOrder)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
@@ -65,6 +65,11 @@ class OrderRouter {
await _this.validators.ensureUser(ctx, next)
await _this.orderRESTController.createOrder(ctx, next)
}
async deleteOrder (ctx, next) {
await _this.validators.ensureUser(ctx, next)
await _this.orderRESTController.deleteOrder(ctx, next)
}
}
export default OrderRouter
+172 -14
View File
@@ -65,6 +65,7 @@ class OfferUseCases {
this.flagOffer = this.flagOffer.bind(this)
this.loadOffers = this.loadOffers.bind(this)
this.listOffersByAddress = this.listOffersByAddress.bind(this)
this.syncOfferMutableData = this.syncOfferMutableData.bind(this)
// State
this.seenOffers = []
@@ -129,20 +130,57 @@ class OfferUseCases {
// Get data about the token.
const tokenId = offerEntity.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)}`)
let tokenData = null
try {
tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
} catch (err) {
console.error('Error in OfferUseCases/createOffer() getting token data: ', err.message)
}
// Generate a 'display category' for the token. This will allow the
// front end UI to figure out how to display the token.
const displayCategory = this.categorizeToken(offerEntity, tokenData)
console.log('displayCategory: ', displayCategory)
offerEntity.displayCategory = displayCategory
// Do additional data analysis if the token data was successfully retrieved.
if (tokenData) {
// Store the mutable and immutable data cids.
const mutableDataCid = tokenData.mutableData
const immutableDataCid = tokenData.immutableData
offerEntity.mutableDataCid = mutableDataCid
offerEntity.immutableDataCid = immutableDataCid
// Detect if user set the NSFW flag.
// const nsfw = false
// nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData)
// offerEntity.nsfw = nsfw
// Get the mutable data from the cid if it exists.
if (mutableDataCid && typeof mutableDataCid === 'string') {
let mutableData = null
try {
mutableData = await this.retryQueue.addToQueue(this.adapters.wallet.cid2json, mutableDataCid)
} catch (err) {
console.error('Error in OfferUseCases/createOffer() getting mutable data: ', err.message)
}
console.log('mutableData: ', mutableData)
if (mutableData) {
try {
offerEntity.tokenIconUrl = mutableData.tokenIcon
offerEntity.tokenCategories = mutableData.category
offerEntity.tokenTags = mutableData.tags
offerEntity.lastUpdatedTokenData = new Date().getTime()
offerEntity.userDataStr = JSON.stringify(mutableData.userData)
} catch (error) {
// skip error
}
}
}
console.log('offerEntity: ', offerEntity)
// Generate a 'display category' for the token. This will allow the
// front end UI to figure out how to display the token.
const displayCategory = this.categorizeToken(offerEntity, tokenData)
console.log('displayCategory: ', displayCategory)
offerEntity.displayCategory = displayCategory
// Detect if user set the NSFW flag.
// const nsfw = false
// nsfw = await this.retryQueue.addToQueue(this.detectNsfw, tokenData)
// offerEntity.nsfw = nsfw
}
// Add offer to the local database.
const offerModel = new this.OfferModel(offerEntity)
@@ -150,7 +188,7 @@ class OfferUseCases {
return true
} catch (err) {
console.error('Error in createOffer()', err.message)
console.error('\n\nError in createOffer()', err.message, '\n\n', err)
throw err
}
}
@@ -267,7 +305,7 @@ class OfferUseCases {
}
}
async listNftOffers (page = 0, nsfw = false) {
/* async listNftOffers (page = 0, nsfw = false) {
try {
const data = await this.OfferModel.find({
displayCategory: { $ne: 'fungible' },
@@ -287,6 +325,42 @@ class OfferUseCases {
console.error('Error in use-cases/offer/listNftOffers()')
throw error
}
} */
async listNftOffers (page = 0, nsfw = false) {
try {
const query = {
displayCategory: { $ne: 'fungible' },
nsfw
}
// Total query offers
const totalOffers = await this.OfferModel.countDocuments(query)
// Total pages
const totalPages = Math.ceil(totalOffers / NFT_ENTRIES_PER_PAGE)
const data = await this.OfferModel.find(query)
// Sort entries so newest entries show first.
.sort('-timestamp')
// Skip to the start of the selected page.
.skip(page * NFT_ENTRIES_PER_PAGE)
// Only return 20 results.
.limit(NFT_ENTRIES_PER_PAGE)
// console.log('listNftOffers() returning this data: ', data)
return {
data,
pagination: {
currentPage: page,
totalPages,
totalOffers,
pageSize: NFT_ENTRIES_PER_PAGE
}
}
} catch (error) {
console.error('Error in use-cases/offer/listNftOffers()')
throw error
}
}
async listFungibleOffers (page = 0) {
@@ -862,6 +936,90 @@ class OfferUseCases {
throw err
}
}
// update offer model mutable data
async syncOfferMutableData (tokenId) {
try {
// validate input
if (!tokenId || typeof tokenId !== 'string') {
throw new Error('tokenId must be a string!')
}
// validate existing offer with the associated tokenId
const offer = await this.OfferModel.findOne({ tokenId })
if (!offer) throw new Error('Associated offer not found!')
// Verify last update timestamp. This prevents users from spamming the API with requests.
// It only updates the mutable data if it has been more than 5 minutes since the last update.
const lastUpdateTs = Number(offer.lastUpdatedTokenData)
const now = new Date().getTime()
const period = 5
if (lastUpdateTs) {
// add 5 minutes to the last update
const lastUpdate = new Date(lastUpdateTs)
console.log('lastUpdate', lastUpdate)
lastUpdate.setMinutes(lastUpdate.getMinutes() + period)
console.log(new Date().getMinutes() + ' ' + lastUpdate.getMinutes())
// if now is less than the lastUpdate + 5 minutos , them skip.
if (now < lastUpdate.getTime()) {
console.log('Skipping , token lastUpdate is less than 5 minutes.')
return offer
}
}
let tokenData = null
try {
tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTokenData, tokenId)
} catch (err) {
// Dev Note: If getTokenData() fails, the code below will
console.error('Error in OfferUseCases/createOffer() getting token data: ', err.message)
}
// Do additional data analysis if the token data was successfully retrieved.
if (tokenData) {
// Store the mutable and immutable data cids.
const mutableDataCid = tokenData.mutableData
const immutableDataCid = tokenData.immutableData
offer.mutableDataCid = mutableDataCid
offer.immutableDataCid = immutableDataCid
// Get the mutable data from the cid if it exists.
if (mutableDataCid && typeof mutableDataCid === 'string') {
let mutableData = null
try {
mutableData = await this.retryQueue.addToQueue(this.adapters.wallet.cid2json, mutableDataCid)
} catch (err) {
console.error('Error in OfferUseCases/createOffer() getting mutable data: ', err.message)
}
console.log('mutableData: ', mutableData)
if (mutableData) {
try {
offer.tokenIconUrl = mutableData.tokenIcon
offer.tokenCategories = mutableData.category
offer.tokenTags = mutableData.tags
offer.userDataStr = JSON.stringify(mutableData.userData)
} catch (error) {
// skip error
}
}
}
// Save update time stamp
offer.lastUpdatedTokenData = new Date().getTime()
}
// Save the updated offer data to the database.
await offer.save()
// Return the updated offer data.
return offer
} catch (err) {
console.error('Error in syncOfferMutableData(): ', err)
throw err
}
}
}
export default OfferUseCases
+31
View File
@@ -493,4 +493,35 @@ describe('#wallet', () => {
}
})
})
describe('#cid2json', () => {
it('should throw error if cid is not provided', async () => {
try {
await uut.cid2json()
assert.fail('Unexpected code path')
} catch (error) {
assert.equal(error.message, 'urlOrCid must be a string!')
}
})
it('should convert cid to json', async () => {
try {
sandbox.stub(uut.bchWallet, 'cid2json').resolves({ json: offerMockData.mutableDataMock })
const cid = 'bafkreidr6wfd6mcmwpea7abm5uk5rrprc2wfbcvo5wdcmtlolrpjab5oqm'
const result = await uut.cid2json(cid)
assert.isObject(result)
} catch (error) {
assert.fail('Unexpected code path')
}
})
it('should convert ipfs url to json', async () => {
try {
sandbox.stub(uut.bchWallet, 'cid2json').resolves({ json: offerMockData.mutableDataMock })
const url = 'https://ipfs.io/ipfs/bafkreidr6wfd6mcmwpea7abm5uk5rrprc2wfbcvo5wdcmtlolrpjab5oqm'
const result = await uut.cid2json(url)
assert.isObject(result)
} catch (error) {
assert.fail('Unexpected code path')
}
})
})
})
@@ -229,6 +229,27 @@ describe('#Offer-REST-Router', () => {
}
})
})
describe('#syncOfferMutableData', () => {
it('should sync mutable data', async () => {
ctx.request.body = { tokenId: 'tokenId' }
sandbox.stub(uut.useCases.offer, 'syncOfferMutableData').resolves({})
await uut.syncOfferMutableData(ctx)
assert.isObject(ctx.body)
})
it('should catch and throw an error', async () => {
try {
ctx.request.body = { tokenId: 'tokenId' }
sandbox
.stub(uut.useCases.offer, 'syncOfferMutableData')
.throws(new Error('test error'))
await uut.syncOfferMutableData(ctx)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#handleError', () => {
it('should still throw error if there is no message', () => {
@@ -83,4 +83,13 @@ describe('#Order-REST-Router', () => {
assert.isTrue(spy.calledOnce)
})
})
describe('#deleteOrder', () => {
it('should route to controller', async () => {
sandbox.stub(uut.validators, 'ensureUser').resolves(true)
const spy = sandbox.stub(uut.orderRESTController, 'deleteOrder').resolves(true)
await uut.deleteOrder()
assert.isTrue(spy.calledOnce)
})
})
})
+1 -1
View File
@@ -149,7 +149,7 @@ const localdb = {
static findById () {}
static find () {}
static findOne () {}
static countDocuments(){}
async save () {
return {}
}
+1
View File
@@ -54,6 +54,7 @@ class MockBchWallet {
}
};
this.optimize = async () => { };
this.cid2json = async () => {};
this.ar = new AdapterRoute()
// Environment variable is used by wallet-balance.unit.js to force an error.
if (process.env.NO_UTXO) {
+3
View File
@@ -71,6 +71,9 @@ class Offer {
async acceptCounterOffer() {
return {}
}
async syncOfferMutableData(){
return {}
}
}
class Order {
+16 -1
View File
@@ -126,6 +126,20 @@ const fungibleTokenData01 = {
"immutableData": "troutsblog.com",
"mutableData": ""
}
const mutableDataMock = {
schema: 'ps007-v1.0.3',
schemaDocs: 'https://github.com/Permissionless-Software-Foundation/specifications/blob/master/ps007-token-data-schema.md',
tokenIcon: 'https://files.tokentiger.com/ipfs/view/bafkreidr6wfd6mcmwpea7abm5uk5rrprc2wfbcvo5wdcmtlolrpjab5oqm',
fullSizedUrl: '',
nsfw: false,
userData: { currentUrl: 'https://tokentiger.com' },
jsonLd: {},
about: 'This is AI generated art.',
category: '',
tags: [],
mediaType: 'image',
currentOwner: {},
}
const offerMockData = {
data: {
@@ -214,5 +228,6 @@ export default {
fungibleTokenData01,
offerMockData,
deserealizeTxMockNoOperatorOut,
deserealizeTxMock
deserealizeTxMock,
mutableDataMock
};
+196 -15
View File
@@ -108,6 +108,73 @@ describe('#offer-use-case', () => {
assert.isFalse(result)
})
it('should create offer with mutable data', async () => {
const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
// Mock dependencies
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves({}) // Utxo Status call
.onCall(1).resolves(tokenDataMock) // Token Data call
.onCall(2).resolves(mutableDataMock)
const result = await uut.createOffer(offerObj)
assert.isTrue(result)
})
it('should skip userData stringify error', async () => {
const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
mutableDataMock.userData = { n: 10n }
// Mock dependencies
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves({}) // Utxo Status call
.onCall(1).resolves(tokenDataMock) // Token Data call
.onCall(2).resolves(mutableDataMock)
const result = await uut.createOffer(offerObj)
assert.isTrue(result)
})
it('should handle error getting token data', async () => {
// const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
mutableDataMock.userData = { n: 10n }
// Mock dependencies
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves({}) // Utxo Status call
.onCall(1).throws(new Error('test error'))
const result = await uut.createOffer(offerObj)
assert.isTrue(result)
})
it('should handle error getting mutable data', async () => {
const tokenDataMock = mockData.nftTokenData01
const offerObj = mockData.offerMockData
const mutableDataMock = mockData.mutableDataMock
mutableDataMock.userData = { n: 10n }
// Mock dependencies
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
sandbox.stub(uut, 'findOfferByTxid').throws(new Error('offer not found'))
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves({}) // Utxo Status call
.onCall(1).resolves(tokenDataMock) // Token Data call
.onCall(2).throws(new Error('test error')) // cid2json call
const result = await uut.createOffer(offerObj)
assert.isTrue(result)
})
it('should create offer', async () => {
const tokenDataMock = mockData.simpleNftTokenData01
const offerObj = mockData.offerMockData
@@ -400,7 +467,7 @@ describe('#offer-use-case', () => {
}
})
it('should list orders', async () => {
it('should list offers', async () => {
const queryMock = {
sort () {
return this
@@ -414,7 +481,11 @@ describe('#offer-use-case', () => {
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
const result = await uut.listNftOffers(1, true)
assert.isArray(result)
assert.isObject(result)
assert.property(result, 'data')
assert.property(result, 'pagination')
assert.isArray(result.data)
assert.isObject(result.pagination)
})
})
@@ -781,27 +852,29 @@ describe('#offer-use-case', () => {
assert.equal(result, 'N/A')
})
it('should return if utxo cant be validated', async () => {
it('should return N/A on axios error', async () => {
// Mock dependencies
const mock = Object.assign({}, mockData.offerMockData.data)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(new Error('test error'))
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(new Error('test error'))
const axiosErr = new Error('axios err')
axiosErr.isAxiosError = true
sandbox.stub(uut.retryQueue, 'addToQueue').throws(axiosErr)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should return if utxo can not be validated', async () => {
// Mock dependencies
const mock = Object.assign({}, mockData.offerMockData.data)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(new Error('test error'))
sandbox.stub(uut.retryQueue, 'addToQueue').throws(new Error('test error'))
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should handle axios error', async () => {
// Mock dependencies
const mock = Object.assign({}, mockData.offerMockData.data)
const stubErr = new Error()
stubErr.isAxiosError = true
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(stubErr)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should return if utxo is invalid', async () => {
// Mock dependencies
const mock = Object.assign({}, mockData.offerMockData.data)
@@ -949,4 +1022,112 @@ describe('#offer-use-case', () => {
assert.notEqual('N/A')
})
})
describe('#syncOfferMutableData', () => {
it('should throw error if tokenId is not provided!', async () => {
try {
await uut.syncOfferMutableData()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'tokenId must be a string!')
}
})
it('should throw error if tokenId is not found!', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'findOne').resolves(null)
await uut.syncOfferMutableData('tokenId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Associated offer not found!')
}
})
it('should return current data if lastUpdatedTokenData is less than 5 minutes', async () => {
// Mock dependencies
const lastUpdatedTokenData = new Date().getTime()
const offerMock = Object.assign({}, mockData.offerMockData)
offerMock.lastUpdatedTokenData = lastUpdatedTokenData
// stub
sandbox.stub(uut.OfferModel, 'findOne').returns(offerMock)
const spy = sandbox.stub(uut.retryQueue, 'addToQueue').resolves(true)
const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer)
assert.isTrue(spy.notCalled, 'it should not call retryQueue functions.')
})
it('should sync offer', async () => {
// create a timestamp 6 minutes in the past
// Create offer mock
const offerMock = Object.assign({}, mockData.offerMockData)
offerMock.save = () => { }
// Stub functions
sandbox.stub(uut.OfferModel, 'findOne').returns(offerMock)
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves(mockData.simpleNftTokenData01) // Token Data call
.onCall(1).resolves(mockData.mutableDataMock)
const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer)
assert.isNumber(offer.lastUpdatedTokenData)
})
it('should skip userData stringify error', async () => {
// create mutable data mock
const mutableDataMock = mockData.mutableDataMock
mutableDataMock.userData = { n: 10n }
// create offer mock
const offerMock = Object.assign({}, mockData.offerMockData)
offerMock.save = () => { }
// Stub functions
sandbox.stub(uut.OfferModel, 'findOne').returns(offerMock)
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves(mockData.simpleNftTokenData01) // Token Data call
.onCall(1).resolves(mutableDataMock)
const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer)
assert.isNumber(offer.lastUpdatedTokenData)
})
it('should handle error getting token data', async () => {
// create offer mock
const offerMock = Object.assign({}, mockData.offerMockData)
offerMock.save = () => { }
// Stub functions
sandbox.stub(uut.OfferModel, 'findOne').returns(offerMock)
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).throws(new Error('tokendata error')) // Token Data call
.onCall(1).resolves(mockData.mutableDataMock)
const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer)
})
it('should handle error getting mutable data', async () => {
// create offer mock
const offerMock = Object.assign({}, mockData.offerMockData)
offerMock.save = () => { }
// Stub functions
sandbox.stub(uut.OfferModel, 'findOne').returns(offerMock)
sandbox.stub(uut.retryQueue, 'addToQueue')
.onCall(0).resolves(mockData.simpleNftTokenData01) // Token Data call
.onCall(1).throws(new Error('mutable data error'))
const offer = await uut.syncOfferMutableData('tokenId')
assert.isObject(offer)
assert.isNumber(offer.lastUpdatedTokenData)
})
})
})
+31
View File
@@ -0,0 +1,31 @@
import mongoose from 'mongoose'
import config from '../../../config/index.js'
import User from '../../../src/adapters/localdb/models/users.js'
async function getUsers () {
// Connect to the Mongo Database.
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(
config.database,
{ useNewUrlParser: true, useUnifiedTopology: true }
)
// Find the user by email.
const user = await User.findOne({
email: 'test@test.com'
}, '-password')
// console.log('user: ', user)
// Update the users password
user.password = 'test'
// Change the user to an admin
// user.type = 'admin'
// Save the changes to the database.
await user.save()
mongoose.connection.close()
}
getUsers()
+32
View File
@@ -0,0 +1,32 @@
import mongoose from 'mongoose'
// import config from '../../config/index.js'
import User from '../../src/adapters/localdb/models/users.js'
const mongooseConnectStr = 'mongodb://172.17.0.1:5666/bch-swap-service-prod'
async function getUsers () {
// Connect to the Mongo Database.
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(
mongooseConnectStr,
{ useNewUrlParser: true, useUnifiedTopology: true }
)
// Find the user by email.
const user = await User.findOne({
email: 'test@test.com'
}, '-password')
// Update the users password
// user.password = 'newpassword'
// Change the user to an admin
// user.type = 'admin'
// Save the changes to the database.
await user.save()
mongoose.connection.close()
}
getUsers()