From 9437b8fb80c045e03256c3bb58e9642c7ee697fd Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sat, 7 Dec 2024 10:54:34 -0800 Subject: [PATCH] fix(offers): Fixed workflow for creating offers from Nostr posts --- package-lock.json | 47 +++++++++--------- package.json | 2 +- src/adapters/nostr.js | 3 +- src/entities/offer.js | 12 ++++- src/use-cases/offer/index.js | 51 +++++++++++++++----- src/use-cases/order.js | 10 ++-- test/unit/entities/offer.entity.unit.js | 33 ++++++++++++- test/unit/mocks/use-cases/offer-mock-data.js | 5 +- 8 files changed, 116 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 021dac5..b6ef82e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@psf/bch-js": "6.7.3", "axios": "0.27.2", "bch-message-lib": "2.2.1", - "bch-nostr": "1.3.1", + "bch-nostr": "1.3.2", "bch-token-sweep": "2.2.1", "bcryptjs": "2.4.3", "bitcoincashjs-lib": "3.3.3", @@ -5503,7 +5503,7 @@ }, "node_modules/@scure/base": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "resolved": "http://94.130.170.209:4873/@scure%2fbase/-/base-1.1.1.tgz", "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", "funding": [ { @@ -5515,7 +5515,7 @@ }, "node_modules/@scure/bip32": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "resolved": "http://94.130.170.209:4873/@scure%2fbip32/-/bip32-1.3.1.tgz", "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", "license": "MIT", "dependencies": { @@ -5529,7 +5529,7 @@ }, "node_modules/@scure/bip32/node_modules/@noble/curves": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "resolved": "http://94.130.170.209:4873/@noble%2fcurves/-/curves-1.1.0.tgz", "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", "license": "MIT", "dependencies": { @@ -5541,7 +5541,7 @@ }, "node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "resolved": "http://94.130.170.209:4873/@noble%2fhashes/-/hashes-1.3.1.tgz", "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", "license": "MIT", "engines": { @@ -5553,7 +5553,7 @@ }, "node_modules/@scure/bip32/node_modules/@noble/hashes": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "resolved": "http://94.130.170.209:4873/@noble%2fhashes/-/hashes-1.3.3.tgz", "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", "license": "MIT", "engines": { @@ -5565,7 +5565,7 @@ }, "node_modules/@scure/bip39": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "resolved": "http://94.130.170.209:4873/@scure%2fbip39/-/bip39-1.2.1.tgz", "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", "license": "MIT", "dependencies": { @@ -5578,7 +5578,7 @@ }, "node_modules/@scure/bip39/node_modules/@noble/hashes": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "resolved": "http://94.130.170.209:4873/@noble%2fhashes/-/hashes-1.3.3.tgz", "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", "license": "MIT", "engines": { @@ -7383,7 +7383,7 @@ }, "node_modules/base58-js": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base58-js/-/base58-js-2.0.0.tgz", + "resolved": "http://94.130.170.209:4873/base58-js/-/base58-js-2.0.0.tgz", "integrity": "sha512-nAV5d32QXuGcGptSApkKpC1gGakWBnfJMNjKrYTBh4tb0szfZF+ooueFLy8T4VrY+o4SrE/TyrtUnRZcwZchaA==", "license": "MIT", "engines": { @@ -7597,9 +7597,9 @@ } }, "node_modules/bch-nostr": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/bch-nostr/-/bch-nostr-1.3.1.tgz", - "integrity": "sha512-bSq70zQkJ0ELXnZHqY2NjaZ9ovwL6eMOdvxbajPzuq3ZcNce5r8mTgyFgJASLdloDxFUjfP6yeJBJj0HZzE8Pg==", + "version": "1.3.2", + "resolved": "http://94.130.170.209:4873/bch-nostr/-/bch-nostr-1.3.2.tgz", + "integrity": "sha512-MdbuuTns1MhEd205uabl7Y8X+caphnb6ZgHs574bet+cSE0aw4QoMr6hpc+EmLtuKK90r5Eqob1peuQ9jzVlIg==", "license": "MIT", "dependencies": { "apidoc": "0.53.0", @@ -7612,7 +7612,7 @@ }, "node_modules/bch-nostr/node_modules/apidoc": { "version": "0.53.0", - "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.53.0.tgz", + "resolved": "http://94.130.170.209:4873/apidoc/-/apidoc-0.53.0.tgz", "integrity": "sha512-H4hYToCrI6TgSksdVHxS7h5/Xoqr/rR9m9/IOQaYqXbX07cPVbg0O3RM4+bR4tJKG1aqBWKKm+xojQ8+4i7O5A==", "license": "MIT", "os": [ @@ -7653,7 +7653,7 @@ }, "node_modules/bch-nostr/node_modules/bch-message-lib": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/bch-message-lib/-/bch-message-lib-2.3.1.tgz", + "resolved": "http://94.130.170.209:4873/bch-message-lib/-/bch-message-lib-2.3.1.tgz", "integrity": "sha512-xAWMRvmcP2mYTdp9KgjSQfxlJF4DTloCJrjGx0TumD3HPF9mmYZ/CiCGUAFdQk3LBCavnK0AZi9v3+JTNSnrcA==", "license": "MIT", "peerDependencies": { @@ -7662,9 +7662,8 @@ }, "node_modules/bch-nostr/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "resolved": "http://94.130.170.209:4873/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -17971,7 +17970,7 @@ }, "node_modules/noble-secp256k1": { "version": "1.2.14", - "resolved": "https://registry.npmjs.org/noble-secp256k1/-/noble-secp256k1-1.2.14.tgz", + "resolved": "http://94.130.170.209:4873/noble-secp256k1/-/noble-secp256k1-1.2.14.tgz", "integrity": "sha512-GSCXyoZBUaaPwVWdYncMEmzlSUjF9J/YeEHpklYJCyg8wPuJP3NzDx0BkiwArzINkdX2HJHvUJhL6vVWPOQQcg==", "deprecated": "Switch to namespaced @noble/secp256k1 for security and feature updates", "license": "MIT" @@ -18226,7 +18225,7 @@ }, "node_modules/nostr": { "version": "0.2.8", - "resolved": "https://registry.npmjs.org/nostr/-/nostr-0.2.8.tgz", + "resolved": "http://94.130.170.209:4873/nostr/-/nostr-0.2.8.tgz", "integrity": "sha512-p25OQiEB5x8rWWoWLRRbxtxQiaQgypaQQakSKw5+cp/SW0DXea/soMzqmTXei1I6HrcARuyfvedGTdoEU471Ow==", "license": "MIT", "dependencies": { @@ -18236,7 +18235,7 @@ }, "node_modules/nostr-tools": { "version": "2.10.1", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.1.tgz", + "resolved": "http://94.130.170.209:4873/nostr-tools/-/nostr-tools-2.10.1.tgz", "integrity": "sha512-fnAxLi92UgyMAEw4fMEhDKzH53uBJ0WkkVPTNfX0b3bspeWWn0n5tDZtKRJbvW0wAGOPmU6LYWi/IKPOsq3p2Q==", "license": "Unlicense", "dependencies": { @@ -18261,7 +18260,7 @@ }, "node_modules/nostr-tools/node_modules/@noble/ciphers": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", + "resolved": "http://94.130.170.209:4873/@noble%2fciphers/-/ciphers-0.5.3.tgz", "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", "license": "MIT", "funding": { @@ -18270,7 +18269,7 @@ }, "node_modules/nostr-tools/node_modules/@noble/curves": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "resolved": "http://94.130.170.209:4873/@noble%2fcurves/-/curves-1.2.0.tgz", "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "license": "MIT", "dependencies": { @@ -18282,7 +18281,7 @@ }, "node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "resolved": "http://94.130.170.209:4873/@noble%2fhashes/-/hashes-1.3.2.tgz", "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "license": "MIT", "engines": { @@ -18294,7 +18293,7 @@ }, "node_modules/nostr-tools/node_modules/@noble/hashes": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "resolved": "http://94.130.170.209:4873/@noble%2fhashes/-/hashes-1.3.1.tgz", "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", "license": "MIT", "engines": { @@ -18306,7 +18305,7 @@ }, "node_modules/nostr-wasm": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "resolved": "http://94.130.170.209:4873/nostr-wasm/-/nostr-wasm-0.1.0.tgz", "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", "license": "MIT", "optional": true diff --git a/package.json b/package.json index a4078fc..393144b 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@psf/bch-js": "6.7.3", "axios": "0.27.2", "bch-message-lib": "2.2.1", - "bch-nostr": "1.3.1", + "bch-nostr": "1.3.2", "bch-token-sweep": "2.2.1", "bcryptjs": "2.4.3", "bitcoincashjs-lib": "3.3.3", diff --git a/src/adapters/nostr.js b/src/adapters/nostr.js index b44fb0d..7812b16 100644 --- a/src/adapters/nostr.js +++ b/src/adapters/nostr.js @@ -103,7 +103,8 @@ class NostrAdapter { }) pool.on('event', (relay, subId, ev) => { - messages.push(ev.content) + // console.log('ev: ', ev) + messages.push({ content: ev.content, eventId: ev.id }) }) }) diff --git a/src/entities/offer.js b/src/entities/offer.js index 87b01f7..c0116d7 100644 --- a/src/entities/offer.js +++ b/src/entities/offer.js @@ -17,6 +17,8 @@ class OfferEntity { ) } + console.log('offer entity validate() offerData: ', offerData) + const { messageType, messageClass, @@ -30,7 +32,8 @@ class OfferEntity { offerStatus, makerAddr, ticker, - tokenType + tokenType, + nostrEventId } = offerData.data // Input Validation @@ -73,6 +76,9 @@ class OfferEntity { if (!tokenType || typeof tokenType !== 'number') { throw new Error("Property 'tokenType' must be a number.") } + if (!nostrEventId || typeof nostrEventId !== 'string') { + throw new Error("Property 'nostrEventId' must be a string.") + } // Convert the timestamp to a number. let timestamp = new Date(offerData.timestamp) @@ -96,8 +102,10 @@ class OfferEntity { offerStatus: offerStatus || this.offerStatus[0], makerAddr, ticker, - tokenType + tokenType, + nostrEventId } + console.log('offer entity validatedOfferData: ', validatedOfferData) return validatedOfferData } diff --git a/src/use-cases/offer/index.js b/src/use-cases/offer/index.js index 3f61050..ceb2114 100644 --- a/src/use-cases/offer/index.js +++ b/src/use-cases/offer/index.js @@ -51,18 +51,21 @@ class OfferUseCases { // Bind 'this' object to functions this.detectNsfw = this.detectNsfw.bind(this) this.loadOffers = this.loadOffers.bind(this) + + // State + this.seenOffers = [] } // This method is called by timer controller to load offers from a Nostr topic. async createOffer (offerObj) { try { - console.log('Use Case createOffer(offerObj): ', offerObj) + // console.log('Use Case createOffer(offerObj): ', offerObj) // Return if Offer already exists in database with the same utxo transaction id. try { await this.findOfferByTxid(offerObj.data.utxoTxid) - console.log('Offer already found in local database.') + // console.log('Offer already found in local database.') return false } catch (err) { /* exit quietly */ } @@ -75,6 +78,14 @@ class OfferUseCases { // console.log('WARN: Counter Offer innappropriately routed to createOffer()') // } + // Quickly skip over offers that have already been processed. + const eventId = offerObj.data.nostrEventId + if (this.seenOffers.includes(eventId)) { + // console.log(`Offer with event ID ${eventId} already processed. Skipping.`) + return false + } + this.seenOffers.push(eventId) + // Verify that UTXO in offer is unspent. If it is spent, then ignore the // offer. const utxo = { @@ -94,13 +105,13 @@ class OfferUseCases { offerObj.timestamp = new Date().getTime() const offerEntity = this.offerEntity.validate(offerObj) - console.log('offerEntity: ', offerEntity) + // console.log('offerEntity: ', offerEntity) // 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)}`) + // 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. @@ -142,16 +153,29 @@ class OfferUseCases { // Retrieve the mutable data from Filecoin/IPFS. // const url = `https://${cid}.ipfs.w3s.link/data.json` - const url = `${this.config.ipfsGateway}${cid}/data.json` - const result = await this.axios.get(url) - const mutableData = result.data - console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`) + let mutableData = {} + + try { + // Try the conventional data URL. + const url = `${this.config.ipfsGateway}${cid}/data.json` + const result = await this.axios.get(url) + mutableData = result.data + // console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`) + } catch (err) { + // Try the newer data URL. + const url = `${this.config.ipfsGateway}${cid}` + const result = await this.axios.get(url) + mutableData = result.data + // console.log(`mutableData: ${JSON.stringify(mutableData, null, 2)}`) + } + console.log('mutableData: ', mutableData) // Logical tests const hasNsfw = !!mutableData.nsfw const nsfwSetTrue = mutableData.nsfw === true const nsfwStringTrue = mutableData.nsfw === 'true' const nsfwDetected = hasNsfw && (nsfwSetTrue || nsfwStringTrue) + console.log(`hasNsfw: ${hasNsfw}, nsfwSetTrue: ${nsfwSetTrue}, nsfwStringTrue: ${nsfwStringTrue}, nsfwDetected: ${nsfwDetected}`) if (nsfwDetected) { console.log('NSFW flag set as true') @@ -307,8 +331,8 @@ class OfferUseCases { await this.ensureFunds(offerInfo) // Get UTXOs. - const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore - console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) + // const utxos = this.adapters.wallet.bchWallet.utxos.utxoStore + // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) // Calculate amount of sats to generate a counter offer. let satsToMove = Math.ceil(offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)) @@ -674,12 +698,17 @@ class OfferUseCases { try { // Retrieve offers array. const offers = await this.adapters.nostr.read() + console.log('offers: ', offers) + for (let i = 0; i < offers.length; i++) { try { const offer = offers[i] // offer data - const offerObj = JSON.parse(offer) + const offerObj = JSON.parse(offer.content) + + // Append the Nostr Event ID to the offer object + offerObj.data.nostrEventId = offer.eventId // Try to create new offer await this.createOffer(offerObj) diff --git a/src/use-cases/order.js b/src/use-cases/order.js index bed43f5..3aa6841 100644 --- a/src/use-cases/order.js +++ b/src/use-cases/order.js @@ -36,18 +36,18 @@ class OrderLib { // Create a new order model and add it to the Mongo database. async createOrder (entryObj) { try { - console.log('createOrder(entryObj): ', entryObj) + // console.log('createOrder(entryObj): ', entryObj) if (!entryObj) return false if (!entryObj.tokenId) throw new Error('entry does not contain required properties') // Specify the address to send payment. entryObj.makerAddr = this.adapters.wallet.bchWallet.walletInfo.cashAddress - console.log('entryObj.makerAddr: ', entryObj.makerAddr) + // console.log('entryObj.makerAddr: ', entryObj.makerAddr) // Input Validation const orderEntity = this.orderEntity.inputValidate(entryObj) - console.log('orderEntity: ', orderEntity) + // console.log('orderEntity: ', orderEntity) // Optimize the wallet to speed up working with it. console.log('Optimizing wallet before creating new order.') @@ -105,7 +105,7 @@ class OrderLib { return eventId } catch (err) { // console.log("Error in use-cases/entry.js/createEntry()", err.message) - wlogger.error('Error in use-cases/order.js/createOrder())') + wlogger.error('Error in use-cases/order.js/createOrder(): ', err) console.log('error entryObj: ', entryObj) throw err } @@ -129,7 +129,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 diff --git a/test/unit/entities/offer.entity.unit.js b/test/unit/entities/offer.entity.unit.js index 51af24d..090fcb8 100644 --- a/test/unit/entities/offer.entity.unit.js +++ b/test/unit/entities/offer.entity.unit.js @@ -237,6 +237,36 @@ describe('#Offer-Entity', () => { } }) + it('should throw an error if nostrEventId is not included', () => { + try { + const offerData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1, + utxoTxid: 'fakeTxid', + utxoVout: 0, + offerStatus: 'posted', + makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + tokenType: 1 + } + } + uut.validate(offerData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include( + err.message, + "Property 'nostrEventId' must be a string." + ) + } + }) + it('should validate a new offer', () => { const offerObj = { appId: 'swapTest555', @@ -255,7 +285,8 @@ describe('#Offer-Entity', () => { offerStatus: 'posted', makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', ticker: 'TROUT', - tokenType: 1 + tokenType: 1, + nostrEventId: 'test' }, timestamp: '2021-09-20T17:54:26.395Z', localTimeStamp: '9/20/2021, 10:54:26 AM', diff --git a/test/unit/mocks/use-cases/offer-mock-data.js b/test/unit/mocks/use-cases/offer-mock-data.js index 83fc742..4cd3395 100644 --- a/test/unit/mocks/use-cases/offer-mock-data.js +++ b/test/unit/mocks/use-cases/offer-mock-data.js @@ -140,14 +140,15 @@ const offerMockData = { '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87', utxoVout: 0, makerAddr: 'address', - tokenType: 1 + tokenType: 1, + nostrEventId: 'test' } } const deserealizeTxMock = { //... vout: [ - { + { value: 0, scriptPubKey: { addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']