Compare commits

...
18 Commits
Author SHA1 Message Date
Chris Troutner c92ffac645 Merge pull request #82 from Permissionless-Software-Foundation/ct-unstable
Refactored to use Nostr instead of P2WDB
2024-12-29 09:16:40 -08:00
Chris Troutner 216f0b6910 fix(tests): fixed broken tests 2024-12-29 09:14:49 -08:00
Chris Troutner c31bf96986 fix(timers): Disabling unneeded timer controller designed for P2WDB issues. 2024-12-28 13:59:27 -08:00
Chris Troutner 7f8ef994a7 feat(Docker): Updating docker containers 2024-12-16 17:53:36 -08:00
Chris Troutner 72ba1c48c2 Can create and take offers using Nostr 2024-12-16 05:29:03 -08:00
Chris Troutner 2a78145170 Debugging counter-offer 2024-12-07 20:33:25 -08:00
Chris Troutner d15fee7c7a Merge pull request #81 from Permissionless-Software-Foundation/ct-unstable
fix(offers): Fixed workflow for creating offers from Nostr posts
2024-12-07 10:56:11 -08:00
Chris Troutner 9437b8fb80 fix(offers): Fixed workflow for creating offers from Nostr posts 2024-12-07 10:54:34 -08:00
Chris Troutner b611663379 Merge pull request #80 from Permissionless-Software-Foundation/dh-create-offr-nostr
feat(offer): Trigger createOffer() from Nostr topic
2024-12-06 09:43:58 -08:00
Chris Troutner 03b7ed980a fix(IPFS): Updating IPFS gateway for getting token data 2024-12-06 09:35:33 -08:00
Chris Troutner bf74d50bb0 linting 2024-12-04 17:12:40 -08:00
Chris Troutner 003480bcb5 fix(offer): Refactored _this in rest-api controller 2024-12-04 17:12:26 -08:00
Daniel Gonzalez 9dbd18d0d6 Increased test coverage 2024-12-04 06:51:28 -04:00
Chris Troutner b3ba7f03e4 Commenting out POST /offer/ webhook endpoint 2024-12-03 09:28:52 -08:00
Daniel Gonzalez 0a8bb52241 feat(offer): Trigger createOffer() from Nostr topic 2024-12-02 22:19:54 -04:00
Chris Troutner 5eeafd7ead Merge pull request #79 from Permissionless-Software-Foundation/dh-create-order-fix
fix(order): Refactored createOrder() to use Nostr
2024-11-29 04:02:53 -08:00
Chris Troutner df146382cb Reducing info posted to Nostr on Order 2024-11-29 04:00:10 -08:00
Daniel Gonzalez af8a7c3ff3 fix(order): Refactored createOrder() to use Nostr 2024-11-28 12:37:47 -04:00
30 changed files with 1615 additions and 526 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ export default {
chatPubSubChan: 'psf-ipfs-chat-001',
// IPFS gateway
ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://p2wdb-gateway-678.fullstack.cash/ipfs/',
ipfsGateway: process.env.IPFS_GATEWAY ? process.env.IPFS_GATEWAY : 'https://pin.fullstack.cash/ipfs/download/',
// This can add specific Circuit Relay v2 servers to connect to.
bootstrapRelays: [
+105 -35
View File
@@ -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.3",
"bch-token-sweep": "2.2.1",
"bcryptjs": "2.4.3",
"bitcoincashjs-lib": "3.3.3",
@@ -53,10 +53,11 @@
"koa2-ratelimit": "0.9.1",
"libp2p": "1.9.1",
"line-reader": "0.4.0",
"minimal-slp-wallet": "5.12.0",
"minimal-slp-wallet": "5.12.1",
"mongoose": "5.13.14",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.7.5",
"nostr-tools": "2.10.4",
"p2wdb-esm": "2.2.9",
"passport-local": "1.0.0",
"public-ip": "6.0.1",
@@ -5503,7 +5504,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 +5516,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 +5530,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 +5542,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 +5554,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 +5566,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 +5579,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 +7384,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": {
@@ -7420,9 +7421,9 @@
}
},
"node_modules/bch-consumer": {
"version": "1.6.1",
"resolved": "http://94.130.170.209:4873/bch-consumer/-/bch-consumer-1.6.1.tgz",
"integrity": "sha512-ZhyZ54kgF6f3sB7J4ylMBJHSoBXozUdM2bvsm2nTz31515LFfHra9d9w4Ommlmy5BvuDkdX4lUeGSLRvggXnFg==",
"version": "1.5.1",
"resolved": "http://94.130.170.209:4873/bch-consumer/-/bch-consumer-1.5.1.tgz",
"integrity": "sha512-tEmds6Mk7g074+fGflm0d9At2ZV8DFhWErEN0nUzzW4vG5Ega9+yjREY16P/QuwwykzodnleQqOsNwcmvvgnAg==",
"license": "MIT",
"dependencies": {
"@psf/bch-js": "6.7.3",
@@ -7597,9 +7598,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.3",
"resolved": "http://94.130.170.209:4873/bch-nostr/-/bch-nostr-1.3.3.tgz",
"integrity": "sha512-2Mod/PEDr/GJzQq20oSJKwJDtiXYm/Qj5KxIvFqsvBY1SYsHGyDu1FMBQ1UvOhY3ZP5fgiVLbIGvMRuWXqS4oQ==",
"license": "MIT",
"dependencies": {
"apidoc": "0.53.0",
@@ -7610,9 +7611,54 @@
"ws": "8.18.0"
}
},
"node_modules/bch-nostr/node_modules/@noble/ciphers": {
"version": "0.5.3",
"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": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/bch-nostr/node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "http://94.130.170.209:4873/@noble%2fcurves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/bch-nostr/node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "http://94.130.170.209:4873/@noble%2fhashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/bch-nostr/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "http://94.130.170.209:4873/@noble%2fhashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"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 +7699,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 +7708,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",
@@ -7681,6 +7726,31 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/bch-nostr/node_modules/nostr-tools": {
"version": "2.10.1",
"resolved": "http://94.130.170.209:4873/nostr-tools/-/nostr-tools-2.10.1.tgz",
"integrity": "sha512-fnAxLi92UgyMAEw4fMEhDKzH53uBJ0WkkVPTNfX0b3bspeWWn0n5tDZtKRJbvW0wAGOPmU6LYWi/IKPOsq3p2Q==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "^0.5.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"
},
"optionalDependencies": {
"nostr-wasm": "v0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/bch-token-sweep": {
"version": "2.2.1",
"resolved": "http://94.130.170.209:4873/bch-token-sweep/-/bch-token-sweep-2.2.1.tgz",
@@ -17186,15 +17256,15 @@
}
},
"node_modules/minimal-slp-wallet": {
"version": "5.12.0",
"resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-5.12.0.tgz",
"integrity": "sha512-zSJcBLF/tt7NH0xkuukTJ4BxnC2GIR8kjUJRWW91/LwhI6c2K9qNVV/G5E7SjYx125aHkGk5lq3pjGePsJ18rA==",
"version": "5.12.1",
"resolved": "http://94.130.170.209:4873/minimal-slp-wallet/-/minimal-slp-wallet-5.12.1.tgz",
"integrity": "sha512-DnggKv1KC/Y6qxT6GiplrxfKPgBOdYbu7uK4bWVO+6RgTtcgMxrd4pxCFh4kHV3nV7vKBpE1wsmVZgtZ3x9qcg==",
"license": "MIT",
"dependencies": {
"@chris.troutner/retry-queue-commonjs": "1.0.8",
"@psf/bch-js": "6.8.0",
"apidoc": "0.51.0",
"bch-consumer": "1.6.1",
"bch-consumer": "1.5.1",
"bch-donation": "1.1.2",
"crypto-js": "4.0.0"
}
@@ -17971,7 +18041,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 +18296,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": {
@@ -18235,9 +18305,9 @@
}
},
"node_modules/nostr-tools": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.1.tgz",
"integrity": "sha512-fnAxLi92UgyMAEw4fMEhDKzH53uBJ0WkkVPTNfX0b3bspeWWn0n5tDZtKRJbvW0wAGOPmU6LYWi/IKPOsq3p2Q==",
"version": "2.10.4",
"resolved": "http://94.130.170.209:4873/nostr-tools/-/nostr-tools-2.10.4.tgz",
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "^0.5.1",
@@ -18248,7 +18318,7 @@
"@scure/bip39": "1.2.1"
},
"optionalDependencies": {
"nostr-wasm": "v0.1.0"
"nostr-wasm": "0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
@@ -18261,7 +18331,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 +18340,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 +18352,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 +18364,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 +18376,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
+3 -2
View File
@@ -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.3",
"bch-token-sweep": "2.2.1",
"bcryptjs": "2.4.3",
"bitcoincashjs-lib": "3.3.3",
@@ -72,10 +72,11 @@
"koa2-ratelimit": "0.9.1",
"libp2p": "1.9.1",
"line-reader": "0.4.0",
"minimal-slp-wallet": "5.12.0",
"minimal-slp-wallet": "5.12.1",
"mongoose": "5.13.14",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.7.5",
"nostr-tools": "2.10.4",
"p2wdb-esm": "2.2.9",
"passport-local": "1.0.0",
"public-ip": "6.0.1",
+2 -2
View File
@@ -12,7 +12,7 @@ RUN apt-get update
RUN apt-get install -y sudo git curl nano gnupg wget
#Install Node and NPM
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs build-essential
@@ -34,7 +34,7 @@ WORKDIR /home/safeuser
# Update to the latest version of npm.
#RUN npm install -g npm@7.23.0
RUN npm install -g npm
#RUN npm install -g npm
# npm mirror to prevent direct dependency on npm.
#RUN npm set registry http://94.130.170.209:4873/
+2 -2
View File
@@ -12,7 +12,7 @@ RUN apt-get update
RUN apt-get install -y sudo git curl nano gnupg wget
#Install Node and NPM
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
RUN curl -sL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs build-essential
@@ -34,7 +34,7 @@ WORKDIR /home/safeuser
# Update to the latest version of npm.
#RUN npm install -g npm@7.23.0
RUN npm install -g npm
#RUN npm install -g npm
# npm mirror to prevent direct dependency on npm.
RUN npm set registry http://94.130.170.209:4873/
+13 -71
View File
@@ -3,9 +3,9 @@
version: '3.9'
services:
mongo-p2wdb-service:
mongo-dex:
image: mongo:4.2.0
container_name: mongo-p2wdb-service
container_name: mongo-dex
ports:
- '5666:27017' # <host port>:<container port>
volumes:
@@ -13,69 +13,11 @@ services:
command: mongod --logpath=/dev/null # -- quiet
restart: always
ipfs:
# https://github.com/christroutner/trickle-ipfs
image: christroutner/trickle-ipfs:v1.0.1
#build:
# context: ./
# dockerfile: Dockerfile
container_name: ipfs
environment:
IPFS_DAEMON_ARGUMENTS: '--enable-pubsub-experiment --migrate=true --agent-version-suffix=docker --routing=dhtclient'
UPLOAD_KBPS: '1000'
DOWNLOAD_KBPS: '1000'
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '10'
mem_limit: 2000mb
ports:
- 4001:4001
- 4003:4003
- 172.17.0.1:5001:5001
- 172.17.0.1:8080:8080
command: [
'./start-ipfs.sh'
]
volumes:
- ../data/go-ipfs/data:/root/.ipfs
restart: always
p2wdb:
#build:
# context: ./p2wdb/
# dockerfile: Dockerfile
image: christroutner/p2wdb:v4.1.3
container_name: p2wdb
environment:
CONSUMER_URL: 'https://free-bch.fullstack.cash'
#ENABLE_BCH_PAYMENT: 1
DEBUG_LEVEL: 1
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '10'
mem_limit: 1gb
links:
- mongo-p2wdb-service
ports:
# <host port>:<container port>
- 5667:5667 # REST API
#- 5668:5668 # IPFS TCP port
#- 5669:5669 # IPFS WS Port
volumes:
- ../data/ipfsdata/p2wdb:/home/safeuser/ipfs-p2wdb-service/.ipfsdata/p2wdb
- ./p2wdb/start-production.sh:/home/safeuser/ipfs-p2wdb-service/start-production.sh
#- ../scripts/wallet.json:/home/safeuser/ipfs-p2wdb-service/wallet.json
restart: always
bch-dex:
#build:
# context: ./bch-dex/
# dockerfile: Dockerfile
image: christroutner/bch-dex:v2.2.4
build:
context: ./bch-dex/
dockerfile: Dockerfile
#image: christroutner/bch-dex:v2.2.4
container_name: bch-dex
environment:
CONSUMER_URL: 'https://free-bch.fullstack.cash'
@@ -86,8 +28,8 @@ services:
max-file: '10'
mem_limit: 1000mb
links:
- mongo-p2wdb-service
- p2wdb
- mongo-dex
#- p2wdb
ports:
- '5700:5700' # <host port>:<container port>
volumes:
@@ -96,13 +38,13 @@ services:
restart: always
dex-ui:
#build:
# context: ./bch-dex-ui/
# dockerfile: Dockerfile
image: christroutner/bch-dex-ui:v1.2.4
build:
context: ./bch-dex-ui/
dockerfile: Dockerfile
#image: christroutner/bch-dex-ui:v1.2.4
container_name: dex-ui
environment:
SERVER: 'http://192.168.2.3'
SERVER: 'http://172.17.0.1'
logging:
driver: 'json-file'
options:
-71
View File
@@ -1,71 +0,0 @@
# Create a Docker container for the P2WDB
#
#IMAGE BUILD COMMANDS
FROM ubuntu:20.04
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
#Update the OS and install any OS packages needed.
RUN apt-get update
RUN apt-get install -y sudo git curl nano gnupg wget
#Install Node and NPM
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs build-essential
#Create the user 'safeuser' and add them to the sudo group.
RUN useradd -ms /bin/bash safeuser
RUN adduser safeuser sudo
#Set password to 'abcd8765' change value below if you want a different password
RUN echo safeuser:abcd8765 | chpasswd
#Set the working directory to be the users home directory
WORKDIR /home/safeuser
#Setup NPM for non-root global install (like on a mac)
RUN mkdir /home/safeuser/.npm-global
RUN chown -R safeuser .npm-global
RUN echo "export PATH=~/.npm-global/bin:$PATH" >> /home/safeuser/.profile
RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
# Update to the latest version of npm.
#RUN npm install -g npm@7.23.0
RUN npm install -g npm
# npm mirror to prevent direct dependency on npm.
RUN npm set registry http://94.130.170.209:4873/
# Switch to user account.
#USER safeuser
# Prep 'sudo' commands.
#RUN echo 'abcd8765' | sudo -S pwd
# Clone the rest.bitcoin.com repository
RUN git clone https://github.com/Permissionless-Software-Foundation/ipfs-p2wdb-service
WORKDIR /home/safeuser/ipfs-p2wdb-service
RUN mkdir .ipfsdata
RUN git checkout ct-unstable
#RUN npm install fsevents
# Install dependencies
#RUN mkdir .ipfsdata
RUN npm install
# Generate the API docs
RUN npm run docs
# Expose the port the API will be served on.
#EXPOSE 5010
# Start the application.
#COPY start-production.sh start-production.sh
CMD ["./start-production.sh"]
#CMD ["npm", "start"]
# Used for debugging
#COPY dummyapp.js dummyapp.js
#CMD ["node", "dummyapp.js"]
-4
View File
@@ -1,4 +0,0 @@
setInterval(function () {
const now = new Date()
console.log(`ping ${now.toLocaleString()}`)
}, 10000)
@@ -1,47 +0,0 @@
#!/bin/bash
# BEGIN: Optional configuration settings
# This mnemonic is used to set up persistent public key for e2ee
# Replace this with your own 12-word mnemonic.
export MNEMONIC="mass response fiscal world message exact series swallow forward confirm canoe festival"
# The human readable name this IPFS node identifies as.
export COORD_NAME=generic-p2wdb-production
# Allow this node to function as a circuit relay. It must not be behind a firewall.
#export ENABLE_CIRCUIT_RELAY=true
# For browsers to use your circuit realy, you must set up a domain, SSL certificate,
# and you must forward that subdomain to the IPFS_WS_PORT.
#export CR_DOMAIN=subdomain.yourdomain.com
# Debug level. 0 = minimal info. 2 = max info.
# This value set by docker-compose.yml
#export DEBUG_LEVEL=1
# Log-in information for retrieving a JWT token from FullStack.cash.
export FULLSTACKLOGIN=demo@demo.com
export FULLSTACKPASS=demo
# END: Optional configuration settings
# Production database connection string.
export DBURL=mongodb://172.17.0.1:5666/p2wdb-service-dev
# Configure IPFS ports
#export IPFS_TCP_PORT=5668
#export IPFS_WS_PORT=5669
# Configure REST API port
export PORT=5667
export P2W_ENV=production
export IPFS_HOST=172.17.0.1
export IPFS_API_PORT=5001
export IPFS_TCP_PORT=4001
# P2WDB specific env vars
#export ORBITDB_NAME=/orbitdb/zdpuAm4fPafpjs5mgbBUJV8Az7kd8Xj1ttD6M1JaAxF8RMRrw/psf-bch-p2wdb-keyvalue-v1.0.0-0004
npm start
+2 -1
View File
@@ -34,7 +34,8 @@ const Offer = new mongoose.Schema({
// SWaP Protocol Properties
lokadId: { type: String },
messageType: { type: Number },
messageClass: { type: Number }
messageClass: { type: Number },
nostrEventId: { type: String } // Nostr Event Id.
})
+2 -1
View File
@@ -37,7 +37,8 @@ const Order = new mongoose.Schema({
// SWaP Protocol Properties
lokadId: { type: String },
messageType: { type: Number },
messageClass: { type: Number }
messageClass: { type: Number },
nostrEventId: { type: String } // Nostr Event Id.
})
export default mongoose.model('order', Order)
+18 -2
View File
@@ -2,8 +2,11 @@
Adapter library for working with Nostr.
*/
// Global npm libraries
import BchNostr from 'bch-nostr'
import { RelayPool } from 'nostr'
import RetryQueue from '@chris.troutner/retry-queue'
import * as nip19 from 'nostr-tools/nip19'
class NostrAdapter {
constructor (localConfig = { nostrRelay: '', nostrTopic: '' }) {
@@ -25,11 +28,17 @@ class NostrAdapter {
// Encapsulate dependencies
this.bchNostr = new BchNostr()
this.RelayPool = RelayPool
this.retryQueue = new RetryQueue({
concurrency: 1,
attempts: 5,
retryPeriod: 1000
})
// Bind the 'this' object
this.start = this.start.bind(this)
this.post = this.post.bind(this)
this.read = this.read.bind(this)
this.eventId2note = this.eventId2note.bind(this)
}
// Create nostr keys.
@@ -71,7 +80,7 @@ class NostrAdapter {
msg,
tags: [['t', this.topic]]
}
const eventId = await this.bchNostr.post.uploadToNostr(inObj)
const eventId = await this.retryQueue.addToQueue(this.bchNostr.post.uploadToNostr, inObj)
return eventId
} catch (error) {
console.log(`Error in nostr.js/post() ${error.message} `)
@@ -103,7 +112,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 })
})
})
@@ -115,6 +125,12 @@ class NostrAdapter {
throw error
}
}
// Convert an Event ID into a `noteabc..` syntax that Astral expects.
// This can be used to generate a link to Astral to display the post.
eventId2note (eventId) {
return nip19.noteEncode(eventId)
}
}
export default NostrAdapter
+10 -4
View File
@@ -5,12 +5,11 @@
// Public npm libraries
import BchWallet from 'minimal-slp-wallet'
import BchTokenSweep from 'bch-token-sweep/index.js'
import bitcoinJs from 'bitcoincashjs-lib'
import RetryQueue from '@chris.troutner/retry-queue'
// Local libraries
import JsonFiles from './json-files.js'
import config from '../../config/index.js'
// Hack to get __dirname back.
@@ -35,6 +34,11 @@ class WalletAdapter {
this.BchWallet = BchWallet
this.bchWallet = {} // Will be replaced when initialized.
// this.advancedConfig = localConfig.advancedConfig
this.retryQueue = new RetryQueue({
concurrency: 1,
attempts: 5,
retryPeriod: 5000
})
// Bind the 'this' object
this.moveTokens = this.moveTokens.bind(this)
@@ -441,7 +445,7 @@ class WalletAdapter {
return txObj2
} catch (err) {
console.error('Error in wallet.js/deserializePartialTx()')
console.error('Error in wallet.js/deserializeTx()')
throw err
}
}
@@ -508,7 +512,9 @@ class WalletAdapter {
// return csTxHex
// Broadcast transaction to the network
const txid = await this.bchWallet.ar.sendTx(csTxHex)
// const txid = await this.bchWallet.ar.sendTx(csTxHex)
const txid = await this.retryQueue.addToQueue(this.bchWallet.broadcast, { hex: csTxHex })
console.log('completeTx() txid: ', txid)
return txid
} catch (err) {
+19 -15
View File
@@ -4,8 +4,6 @@
import wlogger from '../../../adapters/wlogger.js'
let _this
class OfferRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
@@ -26,7 +24,13 @@ class OfferRESTControllerLib {
this.OfferModel = this.adapters.localdb.Offer
// this.userUseCases = this.useCases.user
_this = this
// Bind 'this' object to all subfunctions.
this.createOffer = this.createOffer.bind(this)
this.listOffers = this.listOffers.bind(this)
this.listNftOffers = this.listNftOffers.bind(this)
this.listFungibleOffers = this.listFungibleOffers.bind(this)
this.takeOffer = this.takeOffer.bind(this)
this.handleError = this.handleError.bind(this)
}
// No api-doc documentation because this wont be a public endpoint
@@ -36,7 +40,7 @@ class OfferRESTControllerLib {
const offerObj = ctx.request.body
await _this.useCases.offer.createOffer(offerObj)
await this.useCases.offer.createOffer(offerObj)
ctx.body = {
success: true
@@ -45,7 +49,7 @@ class OfferRESTControllerLib {
// console.log(`err.message: ${err.message}`)
// console.log('err: ', err)
// ctx.throw(422, err.message)
_this.handleError(ctx, err)
this.handleError(ctx, err)
}
}
@@ -55,12 +59,12 @@ class OfferRESTControllerLib {
let page = ctx.params.page
if (!page) page = 0
const offers = await _this.useCases.offer.listOffers(page)
const offers = await this.useCases.offer.listOffers(page)
ctx.body = offers
} catch (err) {
console.log('Error in listOffers REST API handler: ', err)
_this.handleError(ctx, err)
this.handleError(ctx, err)
}
}
@@ -70,12 +74,12 @@ class OfferRESTControllerLib {
let page = ctx.params.page
if (!page) page = 0
const offers = await _this.useCases.offer.listNftOffers(page)
const offers = await this.useCases.offer.listNftOffers(page)
ctx.body = offers
} catch (err) {
console.log('Error in listNftOffers REST API handler: ', err)
_this.handleError(ctx, err)
this.handleError(ctx, err)
}
}
@@ -85,12 +89,12 @@ class OfferRESTControllerLib {
let page = ctx.params.page
if (!page) page = 0
const offers = await _this.useCases.offer.listFungibleOffers(page)
const offers = await this.useCases.offer.listFungibleOffers(page)
ctx.body = offers
} catch (err) {
console.log('Error in Fungible REST API handler: ', err)
_this.handleError(ctx, err)
this.handleError(ctx, err)
}
}
@@ -100,14 +104,14 @@ class OfferRESTControllerLib {
try {
console.log('REST API controller, body: ', ctx.request.body)
const offerCid = ctx.request.body.offerCid
const nostrEventId = ctx.request.body.nostrEventId
const hash = await _this.useCases.offer.takeOffer(offerCid)
const { eventId, noteId } = await this.useCases.offer.takeOffer(nostrEventId)
ctx.body = { hash }
ctx.body = { eventId, noteId }
} catch (err) {
wlogger.error('Error in takeOffer() REST API handler.')
_this.handleError(ctx, err)
this.handleError(ctx, err)
}
}
+6 -1
View File
@@ -48,8 +48,13 @@ class OfferRouter {
)
}
// 12/3/24 CT:
// Note: The createOffer() path was used by a P2WDB webhook to generate an
// Offer from and Order. This has been deprecated and Offers are now created
// by a Timer Controller monitoring a Nostr topic.
// Define the routes and attach the controller.
this.router.post('/', _this.offerRESTController.createOffer)
// this.router.post('/', _this.offerRESTController.createOffer) // Deprecated.
this.router.post('/take', _this.offerRESTController.takeOffer)
this.router.get('/list/all/:page', _this.offerRESTController.listOffers)
this.router.get('/list/nft/:page', _this.offerRESTController.listNftOffers)
+4 -4
View File
@@ -37,9 +37,9 @@ class OrderRESTControllerLib {
const orderObj = ctx.request.body.order
console.log('orderObj: ', orderObj)
const hash = await _this.useCases.order.createOrder(orderObj)
const { eventId, noteId } = await _this.useCases.order.createOrder(orderObj)
ctx.body = { hash }
ctx.body = { eventId, noteId }
} catch (err) {
// console.log(`err.message: ${err.message}`)
// console.log('err: ', err)
@@ -69,10 +69,10 @@ class OrderRESTControllerLib {
try {
// console.log('body: ', ctx.request.body)
const p2wdbHash = ctx.request.body.p2wdbHash
const nostrEventId = ctx.request.body.nostrEventId
// console.log('p2wdbHash: ', p2wdbHash)
const txid = await _this.useCases.order.deleteOrder(p2wdbHash)
const txid = await _this.useCases.order.deleteOrder(nostrEventId)
ctx.body = { txid }
} catch (err) {
+24 -4
View File
@@ -29,35 +29,39 @@ class TimerControllers {
this.gcOrders = this.gcOrders.bind(this)
this.gcOffers = this.gcOffers.bind(this)
this.checkDupOffers = this.checkDupOffers.bind(this)
this.loadOffers = this.loadOffers.bind(this)
// State
this.gcOrdersInt = null
this.gcOffersInt = null
this.checkDupOffersInt = null
this.startTimers()
this.loadOffersInt = null
}
// Start all the time-based controllers.
startTimers () {
this.gcOrdersInt = setInterval(this.gcOrders, 60000 * 5)
this.gcOffersInt = setInterval(this.gcOffers, 60000 * 5)
this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5)
// this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5)
this.loadOffersInt = setInterval(this.loadOffers, 60000 * 2)
return true
}
stopTimers () {
clearInterval(this.gcOrdersInt)
clearInterval(this.gcOffersInt)
clearInterval(this.checkDupOffers)
// clearInterval(this.checkDupOffers)
}
// Garbage Collect the Orders.
gcOrders () {
try {
this.useCases.order.removeStaleOrders()
return true
} catch (err) {
// Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/gcOrders(): ', err)
return false
}
}
@@ -65,9 +69,11 @@ class TimerControllers {
gcOffers () {
try {
this.useCases.offer.removeStaleOffers()
return true
} catch (err) {
// Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/gcOffers(): ', err)
return false
}
}
@@ -75,9 +81,23 @@ class TimerControllers {
checkDupOffers () {
try {
this.useCases.offer.removeDuplicateOffers()
return true
} catch (err) {
// Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/checkDupOffers(): ', err)
return false
}
}
// Load offers From nostr .
async loadOffers () {
try {
await this.useCases.offer.loadOffers()
return true
} catch (err) {
// Do not throw an error. This is a top-level function.
console.log('Error in timer-controllers.js/loadOffers(): ', err)
return false
}
}
}
+10 -2
View File
@@ -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
}
+256 -85
View File
@@ -37,7 +37,7 @@ class OfferUseCases {
this.orderUseCase = localConfig.order
if (!this.orderUseCase) {
throw new Error(
'Instance of Offer Use Cases must be passed in when instantiating Offer Use Cases library.'
'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.'
)
}
@@ -49,20 +49,37 @@ class OfferUseCases {
this.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 3 })
// Bind 'this' object to functions
this.createOffer = this.createOffer.bind(this)
this.detectNsfw = this.detectNsfw.bind(this)
this.categorizeToken = this.categorizeToken.bind(this)
this.listOffers = this.listOffers.bind(this)
this.listNftOffers = this.listNftOffers.bind(this)
this.listFungibleOffers = this.listFungibleOffers.bind(this)
this.takeOffer = this.takeOffer.bind(this)
this.ensureFunds = this.ensureFunds.bind(this)
this.findOfferByEvent = this.findOfferByEvent.bind(this)
this.findOfferByTxid = this.findOfferByTxid.bind(this)
this.acceptCounterOffer = this.acceptCounterOffer.bind(this)
this.removeDuplicateOffers = this.removeDuplicateOffers.bind(this)
this.removeStaleOffers = this.removeStaleOffers.bind(this)
this.flagOffer = this.flagOffer.bind(this)
this.loadOffers = this.loadOffers.bind(this)
// State
this.seenOffers = []
this.seenCounterOffers = []
}
// This method is called by the POST /offer REST API controller, which is
// triggered by a P2WDB webhook.
// 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 P2WDB CID.
// Return if Offer already exists in database with the same utxo transaction id.
try {
await this.findOfferByHash(offerObj.hash)
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 */ }
@@ -71,9 +88,17 @@ class OfferUseCases {
// Input Validation
// TODO: This is a hack. Find a better way to protect against the corner-
// case of counter-offers getting routed here.
if (offerObj.data.dataType === 'counter-offer') {
console.log('WARN: Counter Offer innappropriately routed to createOffer()')
// if (offerObj.data.dataType === 'counter-offer') {
// 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.
@@ -83,21 +108,26 @@ class OfferUseCases {
}
// 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)
// console.log('utxoStatus: ', utxoStatus)
// if (utxoStatus === null) return false
if (!utxoStatus) return false
// A new offer gets a status of 'posted'
offerObj.data.offerStatus = 'posted'
// Set timestamp
offerObj.timestamp = new Date().getTime()
const offerEntity = this.offerEntity.validate(offerObj)
console.log('offerEntity: ', offerEntity)
// console.log('offerEntity: ', offerEntity)
console.log(`New Offer for token ID ${offerEntity.tokenId} detected from Nostr post ${eventId}`)
// 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.
@@ -117,7 +147,7 @@ class OfferUseCases {
return true
} catch (err) {
console.error('Error in createOffer()')
console.error('Error in createOffer()', err.message)
throw err
}
}
@@ -139,16 +169,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 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')
@@ -207,11 +250,11 @@ class OfferUseCases {
async listOffers (page = 0) {
try {
const data = await this.OfferModel.find({})
// Sort entries so newest entries show first.
// Sort entries so newest entries show first.
.sort('-timestamp')
// Skip to the start of the selected page.
// Skip to the start of the selected page.
.skip(page * DEFAULT_ENTRIES_PER_PAGE)
// Only return 20 results.
// Only return 20 results.
.limit(DEFAULT_ENTRIES_PER_PAGE)
return data
@@ -227,11 +270,11 @@ class OfferUseCases {
displayCategory: { $ne: 'fungible' },
nsfw
})
// Sort entries so newest entries show first.
// Sort entries so newest entries show first.
.sort('-timestamp')
// Skip to the start of the selected page.
// Skip to the start of the selected page.
.skip(page * NFT_ENTRIES_PER_PAGE)
// Only return 20 results.
// Only return 20 results.
.limit(NFT_ENTRIES_PER_PAGE)
// console.log('listNftOffers() returning this data: ', data)
@@ -246,11 +289,11 @@ class OfferUseCases {
async listFungibleOffers (page = 0) {
try {
const data = await this.OfferModel.find({ displayCategory: 'fungible' })
// Sort entries so newest entries show first.
// Sort entries so newest entries show first.
.sort('-timestamp')
// Skip to the start of the selected page.
// Skip to the start of the selected page.
.skip(page * FUNGIBLE_ENTRIES_PER_PAGE)
// Only return 20 results.
// Only return 20 results.
.limit(FUNGIBLE_ENTRIES_PER_PAGE)
// console.log('listFungibleOffers() returning this data: ', data)
@@ -270,12 +313,12 @@ class OfferUseCases {
// provide functionality to take less than the total amount of tokens offered
// (offerInfo.numTokens). Taking less than the offered amount will be added
// in the future.
async takeOffer (offerCid) {
async takeOffer (eventId) {
try {
console.log('offerCid: ', offerCid)
if (!eventId || typeof eventId !== 'string') throw new Error('eventId must be a string')
// Get the Offer information
const offerInfo = await this.findOfferByHash(offerCid)
const offerInfo = await this.findOfferByEvent(eventId)
console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`)
// Ensure the offer is in a 'posted' state and not already 'taken'
@@ -288,8 +331,10 @@ class OfferUseCases {
tx_hash: offerInfo.utxoTxid,
tx_pos: offerInfo.utxoVout
}
// Note : should be added to retry-queue?
const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
console.log('utxoStatus: ', utxoStatus)
// console.log('utxoStatus: ', utxoStatus)
if (!utxoStatus) {
console.log(`utxo txid: ${offerInfo.utxoTxid}, vout: ${offerInfo.utxoVout}`)
@@ -302,11 +347,12 @@ 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))
console.log('satsToMove', satsToMove, offerInfo)
if (isNaN(satsToMove)) {
throw new Error('Could not calculate the amount of BCH to generate counter offer')
}
@@ -332,9 +378,9 @@ class OfferUseCases {
// Create valid Offer object
const takenOfferInfo = Object.assign({}, offerInfo)
takenOfferInfo.partialTxHex = partialTxHex
delete takenOfferInfo.p2wdbHash
delete takenOfferInfo.nostrEventId
delete takenOfferInfo._id
takenOfferInfo.offerHash = offerInfo.p2wdbHash
takenOfferInfo.offerHash = offerInfo.nostrEventId
// Add P2WDB specific flag for signaling that this is a new offer.
takenOfferInfo.dataType = 'counter-offer'
@@ -342,19 +388,21 @@ class OfferUseCases {
// Write offer info to the P2WDB
// TODO: This will trigger the webhook. Find some way of triggering the
// webhook on new offers, but not on counteroffers
const p2wdbObj = {
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
data: takenOfferInfo,
appId: this.config.p2wdbAppId
const nostrData = {
// wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
data: takenOfferInfo
// appId: this.config.p2wdbAppId
}
const hash = await this.adapters.p2wdb.write(p2wdbObj)
const resultEventId = await this.adapters.nostr.post(JSON.stringify(nostrData))
const noteId = this.adapters.nostr.eventId2note(resultEventId)
// Delete the Offer from the database, so that the user doesn't attempt
// to take the offer more than once.
offerInfo.remove()
// offerInfo.remove()
// Return the P2WDB CID
return hash
return { eventId: resultEventId, noteId }
// return 'fake-hash'
} catch (err) {
@@ -377,9 +425,10 @@ class OfferUseCases {
await this.adapters.wallet.bchWallet.initialize()
// Ensure the app wallet has enough funds to write to the P2WDB.
const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
// Note : this validation should be deprecated for nostr functionality?
// const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey
// const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif)
// if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.')
if (offerEntity.buyOrSell.includes('sell')) {
// Sell Offer
@@ -398,7 +447,7 @@ class OfferUseCases {
throw new Error('App wallet does not control enough BCH to purchase the tokens.')
}
//
//
} else {
// Buy Offer
throw new Error('Buy offers are not supported yet.')
@@ -417,27 +466,50 @@ class OfferUseCases {
}
}
async findOfferByHash (p2wdbHash) {
// Retrieve an Order model from the database. Find it by its event Id.
async findOfferByEvent (nostrEventId) {
try {
if (typeof nostrEventId !== 'string' || !nostrEventId) {
throw new Error('nostrEventId must be a string')
}
const order = await this.OfferModel.findOne({ nostrEventId })
if (!order) {
throw new Error('offer not found')
}
const orderObject = order.toObject()
// return this.offerEntity.validateFromModel(offerObject)
return orderObject
} catch (err) {
console.error('Error in findOfferByEvent()')
throw err
}
}
async findOfferByTxid (utxoTxid) {
// try {
if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
throw new Error('p2wdbHash must be a string')
// try {
if (typeof utxoTxid !== 'string' || !utxoTxid) {
throw new Error('utxoTxid must be a string')
}
const offer = await this.OfferModel.findOne({ p2wdbHash })
const offer = await this.OfferModel.findOne({ utxoTxid })
// TODO: Offer should be found by TXID, then if there is more than one
// result, they should be filtered by the vout property. That will leave
// one remaining UTXO.
if (!offer) {
throw new Error('offer not found')
}
return offer
// const offerObject = offer.toObject()
// return this.offerEntity.validateFromModel(offerObject)
// return offerObject
// } catch (err) {
// // console.error('Error in findOffer(): ', err)
// throw err
// } catch (error) {
// // console.error('Error in use-cases/offer/findOfferByTxid(): ', error.message)
// throw error
// }
}
@@ -445,29 +517,79 @@ class OfferUseCases {
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
// to this function. It does due dilligence on the Counter Offer, then signs
// and broadcasts the transaction to accept the Counter Offer.
async acceptCounterOffer (p2wdbData) {
async acceptCounterOffer (offerData) {
try {
console.log(`acceptCounterOffer() p2wdbData: ${JSON.stringify(p2wdbData, null, 2)}`)
// console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`)
// Quickly skip over offers that have already been processed.
const eventId = offerData.data.nostrEventId
if (this.seenOffers.includes(eventId)) {
// console.log(`Offer with event ID ${eventId} already processed. Skipping.`)
return false
}
// See if this instance of bch-dex is managing the Order associated with
// the incoming Counter Offer.
const orderHash = p2wdbData.data.offerHash
// Note : this should be handled by nostrEvent id or UtxoId?
// const orderHash = offerData.data.nostrEventId
let orderData = {}
try {
orderData = await this.orderUseCase.findOrderByHash(orderHash)
console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`)
orderData = await this.orderUseCase.findOrderByUtxo(offerData)
// console.log(`orderData: ${JSON.stringify(orderData, null, 2)}`)
} catch (err) {
console.log('Order matching this Counter Offer is not managed by this instance of bch-dex. Exiting.')
console.log('Order matching this Counter Offer is not managed by this instance of bch-dex. Skipping.')
// 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'
}
// Verify that the UTXO for sale is valid.
let utxoStatus = null
try {
// Get the status of the UTXO associate with this Offer.
const utxo = {
tx_hash: offerData.data.utxoTxid,
tx_pos: offerData.data.utxoVout
}
// console.log(`Checking this UTXO: ${JSON.stringify(utxo, null, 2)}`)
// utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo)
utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
console.log('utxoStatus: ', utxoStatus)
} catch (err) {
console.log('acceptCounterOffer() err validating UTXO: ', err)
// Handle corner case of bad-data in the Offer model.
if (err.isAxiosError) {
console.log('Error trying to contact wallet service: ', err)
return 'N/A'
} else {
return 'N/A'
}
}
// If the Offer UTXO is spent, exit.
if (utxoStatus === false) {
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
console.log(`Aborting Counter Offer from Event ID ${eventId}`)
console.log(`https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(eventId)}`)
return 'N/A'
}
// Deserialize the partially signed transaction.
const txHex = p2wdbData.data.partialTxHex
const txHex = offerData.data.partialTxHex
const txObj = await this.adapters.wallet.deseralizeTx(txHex)
console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`)
// Ensure the 3rd output (vout=2) contains the required amount of BCH.
const satsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit))
console.log('Ceil', satsToReceive)
if (isNaN(satsToReceive)) {
throw new Error('Could not calculate the amount of BCH offered in the Counter Offer')
}
@@ -492,7 +614,7 @@ class OfferUseCases {
return txid
} catch (err) {
console.error('Error in acceptCounterOffer()')
console.error('Error in acceptCounterOffer(): ', err)
throw err
}
}
@@ -580,7 +702,7 @@ class OfferUseCases {
// If the Offer UTXO is spent, delete the Offer model.
if (utxoStatus === false) {
console.log('utxoStatus: ', utxoStatus)
// console.log('utxoStatus: ', utxoStatus)
console.log(`Spent UTXO detected. Deleting this Offer: ${JSON.stringify(thisOffer, null, 2)}`)
await thisOffer.remove()
}
@@ -600,32 +722,81 @@ class OfferUseCases {
}
}
async flagOffer (flagData) {
console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`)
// Flag offers as NSFW.
async flagOffer (flagData = {}) {
try {
if (!flagData.data) throw new Error('"data" property is required')
const p2wdbHash = flagData.data.p2wdbHash
console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`)
// Get the offer from the database.
const offer = await this.findOfferByHash(p2wdbHash)
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
const eventId = flagData.data.nostrEventId
if (!offer) {
throw new Error(`Offer ${p2wdbHash} not found in the database.`)
// Get the offer from the database.
const offer = await this.findOfferByEvent(eventId)
console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`)
if (!offer) {
throw new Error(`Offer ${eventId} not found in the database.`)
}
// Add the raw flag data to the database model.
offer.flags.push(flagData)
// If flag count is 3 or more, mark the Offer as NSFW
const flagCnt = offer.flags.length
if (flagCnt >= 3) {
offer.nsfw = true
}
// Save the updated offer data to the database.
await offer.save()
return true
} catch (error) {
console.error('Error in flagOffer(): ', error)
throw error
}
}
// Add the raw flag data to the database model.
offer.flags.push(flagData)
// Get offers data from nostr.
async loadOffers () {
try {
// Retrieve offers array.
const offers = await this.adapters.nostr.read()
// console.log('offers: ', offers)
// If flag count is 3 or more, mark the Offer as NSFW
const flagCnt = offer.flags.length
if (flagCnt >= 3) {
offer.nsfw = true
for (let i = 0; i < offers.length; i++) {
try {
const offer = offers[i]
// offer data
const offerObj = JSON.parse(offer.content)
// Append the Nostr Event ID to the offer object
offerObj.data.nostrEventId = offer.eventId
// console.log('loadOffers() offerObj: ', offerObj)
if (offerObj.data.dataType === 'offer') {
// Try to create new offer
await this.createOffer(offerObj)
}
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)}`)
await this.acceptCounterOffer(offerObj)
}
// Ignore the post if it doesn't fit the above filters.
} catch (error) {
/* exit quietly */
}
}
} catch (error) {
console.error('Error in loadOffers(): ', error)
throw error
}
// Save the updated offer data to the database.
await offer.save()
return true
}
}
+56 -28
View File
@@ -31,23 +31,25 @@ class OrderLib {
// Bind subfunctions to the 'this' object.
this.ensureFunds = this.ensureFunds.bind(this)
this.createOrder = this.createOrder.bind(this)
this.findOrderByUtxo = this.findOrderByUtxo.bind(this)
}
// 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.')
@@ -55,7 +57,7 @@ class OrderLib {
// Ensure sufficient tokens exist to create the order.
// await this.ensureFunds(orderEntity)
await this.retryQueue.addToQueue(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])
@@ -70,7 +72,7 @@ class OrderLib {
}
// const utxoInfo = await this.adapters.wallet.moveTokens(moveObj)
const utxoInfo = await this.retryQueue.addToQueue(this.adapters.wallet.moveTokens, moveObj)
console.log('utxoInfo: ', utxoInfo)
// console.log('utxoInfo: ', utxoInfo)
// Update the UTXO store for the wallet.
await this.adapters.wallet.bchWallet.bchjs.Util.sleep(3000)
@@ -85,28 +87,29 @@ class OrderLib {
// Add P2WDB specific flag for signaling that this is a new offer.
orderEntity.dataType = 'offer'
// Add order to P2WDB.
const p2wdbObj = {
wif: this.adapters.wallet.bchWallet.walletInfo.privateKey,
data: orderEntity,
appId: this.config.p2wdbAppId
// Post the new Order information to Nostr under the topic set in the
// config file.
const postObj = {
data: orderEntity
}
// const hash = await this.adapters.p2wdb.write(p2wdbObj)
const hash = await this.retryQueue.addToQueue(this.adapters.p2wdb.write, p2wdbObj)
const postMsg = JSON.stringify(postObj)
const eventId = await this.adapters.nostr.post(postMsg)
// console.log('hash: ', hash)
// Create a MongoDB model to hold the Order
orderEntity.hdIndex = utxoInfo.hdIndex
orderEntity.p2wdbHash = hash
orderEntity.nostrEventId = eventId
console.log(`creating new order model: ${JSON.stringify(orderEntity, null, 2)}`)
const order = new this.OrderModel(orderEntity)
await order.save()
return hash
const noteId = this.adapters.nostr.eventId2note(eventId)
return { eventId, noteId }
} 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
}
@@ -137,12 +140,10 @@ class OrderLib {
// Combine Fungible and NFT token UTXOs.
let tokenUtxos = utxos.slpUtxos.type1.tokens.concat(utxos.slpUtxos.nft.tokens)
// Get token UTXOs that match the token in the order.
tokenUtxos = tokenUtxos.filter(
x => x.tokenId === orderEntity.tokenId
)
console.log('tokenUtxos: ', tokenUtxos)
// Get the total amount of tokens in the wallet that match the token
// in the order.
@@ -171,14 +172,14 @@ class OrderLib {
}
}
// Retrieve an Order model from the database. Find it by its P2WDB CID.
async findOrderByHash (p2wdbHash) {
// Retrieve an Order model from the database. Find it by its event Id.
async findOrderByEvent (nostrEventId) {
try {
if (typeof p2wdbHash !== 'string' || !p2wdbHash) {
throw new Error('p2wdbHash must be a string')
if (typeof nostrEventId !== 'string' || !nostrEventId) {
throw new Error('nostrEventId must be a string')
}
const order = await this.OrderModel.findOne({ p2wdbHash })
const order = await this.OrderModel.findOne({ nostrEventId })
if (!order) {
throw new Error('order not found')
@@ -189,7 +190,35 @@ class OrderLib {
return orderObject
} catch (err) {
console.error('Error in findOrder()')
console.error('Error in findOrderByEvent()')
throw err
}
}
// Retrieve an Order model from the database. Find it by its UTXO (TXID & Vout)
async findOrderByUtxo (offerData = {}) {
try {
// console.log('findOrderByUtxo() offerData: ', offerData)
// const order = await this.OrderModel.findOne({ nostrEventId })
const order = await this.OrderModel.findOne({ utxoTxid: offerData.data.utxoTxid, utxoVout: offerData.data.utxoVout })
// const order = await this.OrderModel.findOne({ utxoTxid: offerData.data.utxoTxid })
// const order = await this.OrderModel.findOne({ tokenId: offerData.data.tokenId })
console.log('findOrderByUtxo() order: ', order)
if (!order) {
throw new Error('order not found')
}
if (!order.nostrEventId) {
throw new Error('order not found')
}
const orderObject = order.toObject()
// return this.offerEntity.validateFromModel(offerObject)
return orderObject
} catch (err) {
console.error('Error in findOrderByUtxo(): ', err)
throw err
}
}
@@ -242,7 +271,7 @@ class OrderLib {
// If the Order UTXO is spent, delete the Order model.
if (utxoStatus === false) {
console.log('utxoStatus: ', utxoStatus)
// console.log('utxoStatus: ', utxoStatus)
console.log(`Spent UTXO detected. Deleting this Order: ${JSON.stringify(thisOrder, null, 2)}`)
await thisOrder.remove()
}
@@ -256,6 +285,7 @@ class OrderLib {
async listOrders (page = 0) {
try {
const data = await this.OrderModel.find({})
// Sort entries so newest entries show first.
.sort('-timestamp')
// Skip to the start of the selected page.
@@ -272,12 +302,10 @@ class OrderLib {
// Delete an Order model by sending the token back to the root address. This
// will allow the garbage collectors to delete the Order and the Offer.
async deleteOrder (p2wdbHash) {
async deleteOrder (nostrEventId) {
try {
console.log('p2wdbHash: ', p2wdbHash)
// Find the order by the given hash.
const order = await this.findOrderByHash(p2wdbHash)
const order = await this.findOrderByEvent(nostrEventId)
console.log('order: ', order)
// Reclaim the tokens
@@ -67,11 +67,12 @@ describe('#Order-REST-Router', () => {
}
// Mock dependencies
sandbox.stub(uut.useCases.order, 'createOrder').resolves('testHash')
sandbox.stub(uut.useCases.order, 'createOrder').resolves({ eventId: 'testEventId', noteId: 'testNoteId' })
await uut.createOrder(ctx)
assert.equal(ctx.body.hash, 'testHash')
assert.equal(ctx.body.eventId, 'testEventId')
assert.equal(ctx.body.noteId, 'testNoteId')
})
it('should catch and throw an error', async () => {
+65 -22
View File
@@ -56,27 +56,70 @@ describe('#Timer-Controllers', () => {
})
})
// describe('#startTimers', () => {
// it('should start the timers', () => {
// const result = uut.startTimers()
//
// // uut.stopTimers()
//
// assert.equal(result, true)
// })
// })
describe('#startTimers', () => {
it('should start the timers', () => {
const result = uut.startTimers()
// describe('#exampleTimerFunc', () => {
// it('should kick off the Use Case', async () => {
// const result = await uut.exampleTimerFunc()
//
// assert.equal(result, true)
// })
//
// it('should return false on error', async () => {
// const result = await uut.exampleTimerFunc(true)
//
// assert.equal(result, false)
// })
// })
// uut.stopTimers()
assert.equal(result, true)
})
})
describe('#gcOrders', () => {
it('should kick off the Use Case', async () => {
const result = await uut.gcOrders()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.order, 'removeStaleOrders').throws(new Error('test error'))
const result = await uut.gcOrders()
assert.equal(result, false)
})
})
describe('#gcOffers', () => {
it('should kick off the Use Case', async () => {
const result = await uut.gcOffers()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.offer, 'removeStaleOffers').throws(new Error('test error'))
const result = await uut.gcOffers()
assert.equal(result, false)
})
})
describe('#checkDupOffers', () => {
it('should kick off the Use Case', async () => {
const result = await uut.checkDupOffers()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.offer, 'removeDuplicateOffers').throws(new Error('test error'))
const result = await uut.checkDupOffers()
assert.equal(result, false)
})
})
describe('#loadOffers', () => {
it('should kick off the Use Case', async () => {
const result = await uut.loadOffers()
assert.equal(result, true)
})
it('should return false on error', async () => {
sandbox.stub(uut.useCases.offer, 'loadOffers').throws(new Error('test error'))
const result = await uut.loadOffers()
assert.equal(result, false)
})
})
})
+32 -1
View File
@@ -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',
+13 -2
View File
@@ -150,7 +150,12 @@ const wallet = {
return { cashAddress: 'fakeAddr', wif: 'fakeWif', hdIndex: 1 }
},
bchWallet: new MockBchWallet(),
moveTokens: async () => {}
moveTokens: async () => {},
moveBch: async () => {},
reclaimTokens: async ()=>{},
generatePartialTx: async ()=>{},
deseralizeTx:async ()=>{},
completeTx:async ()=>{ return ''},
}
const p2wdb = {
@@ -159,4 +164,10 @@ const p2wdb = {
checkForSufficientFunds: async () => true
}
export default { ipfs, localdb, bch, wallet, p2wdb, bchjs}
const nostr = {
post: async () => {return true },
read: async () => { return true },
eventId2note: () => { return 'testNoteId' }
}
export default { ipfs, localdb, bch, wallet, p2wdb, bchjs, nostr}
+1
View File
@@ -24,6 +24,7 @@ class MockBchWallet {
this.sendTokens = async () => {
return 'fakeTxid';
};
this.utxoIsValid =async ()=>{}
this.getUtxos = async () => { };
this.getBalance = async () => { };
this.listTokens = async () => { };
+12
View File
@@ -41,12 +41,24 @@ class Offer {
async createOffer() {
return {}
}
async removeStaleOffers(){
}
async removeDuplicateOffers(){
}
async loadOffers(){
}
}
class Order {
async createOrder() {
return {}
}
async removeStaleOrders(){
}
}
class UseCasesMock {
+83 -37
View File
@@ -3,45 +3,45 @@
*/
const nftOffer01 = {
"messageType": 1,
"messageClass": 1,
"tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b",
"buyOrSell": "sell",
"rateInBaseUnit": 7672536,
"minUnitsToExchange": 7672536,
"numTokens": 1,
"utxoTxid": "c736d73e274df20e9b069b4990d1a264fb80aa98d67cc6c7a39e42bff48e7c04",
"utxoVout": 1,
"timestamp": 1662930309998,
"globaltimestamp": "2022-09-11T21:05:09.998Z",
"localTimestamp": "9/11/2022, 9:05:09 PM",
"txid": "4266862b8358664996038d8c29d49dc0f3d3058fd1ef3d567b084e6b16ceb5b2",
"p2wdbHash": "zdpuArq7rCuVCGPyTnWpLjy9AYsh8yrbwjGdKwg2GDDALkM8t",
"offerStatus": "posted",
"makerAddr": "bitcoincash:qrqlz63cwmu0hcmsrfnd8jemn3atkpaqds6tf4ksrr",
"ticker": "TV001",
"tokenType": 65
"messageType": 1,
"messageClass": 1,
"tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b",
"buyOrSell": "sell",
"rateInBaseUnit": 7672536,
"minUnitsToExchange": 7672536,
"numTokens": 1,
"utxoTxid": "c736d73e274df20e9b069b4990d1a264fb80aa98d67cc6c7a39e42bff48e7c04",
"utxoVout": 1,
"timestamp": 1662930309998,
"globaltimestamp": "2022-09-11T21:05:09.998Z",
"localTimestamp": "9/11/2022, 9:05:09 PM",
"txid": "4266862b8358664996038d8c29d49dc0f3d3058fd1ef3d567b084e6b16ceb5b2",
"p2wdbHash": "zdpuArq7rCuVCGPyTnWpLjy9AYsh8yrbwjGdKwg2GDDALkM8t",
"offerStatus": "posted",
"makerAddr": "bitcoincash:qrqlz63cwmu0hcmsrfnd8jemn3atkpaqds6tf4ksrr",
"ticker": "TV001",
"tokenType": 65
}
const nftTokenData01 = {
"genesisData": {
"type": 65,
"ticker": "TV001",
"name": "Introduction to NFTs on BCH",
"tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b",
"documentUri": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4",
"documentHash": "c1731268f4873f1928438abdaf6ffc546d86a1817dc6f3c6bc73fbdfb4664f10",
"decimals": 0,
"mintBatonIsActive": false,
"tokensInCirculationBN": "1",
"tokensInCirculationStr": "1",
"blockCreated": 740395,
"totalBurned": "0",
"totalMinted": "1",
"parentGroupId": "030563ddd65772d8e9b79b825529ed53c7d27037507b57c528788612b4911107"
},
"immutableData": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4",
"mutableData": "ipfs://bafybeifzhunfpodsztsj5x4ypopngkxuapbxwaxaxkahaybdktk6joqtlq"
"genesisData": {
"type": 65,
"ticker": "TV001",
"name": "Introduction to NFTs on BCH",
"tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b",
"documentUri": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4",
"documentHash": "c1731268f4873f1928438abdaf6ffc546d86a1817dc6f3c6bc73fbdfb4664f10",
"decimals": 0,
"mintBatonIsActive": false,
"tokensInCirculationBN": "1",
"tokensInCirculationStr": "1",
"blockCreated": 740395,
"totalBurned": "0",
"totalMinted": "1",
"parentGroupId": "030563ddd65772d8e9b79b825529ed53c7d27037507b57c528788612b4911107"
},
"immutableData": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4",
"mutableData": "ipfs://bafybeifzhunfpodsztsj5x4ypopngkxuapbxwaxaxkahaybdktk6joqtlq"
}
const simpleNftOffer01 = {
@@ -126,11 +126,57 @@ const fungibleTokenData01 = {
"mutableData": ""
}
const offerMockData = {
data: {
messageType: 1,
messageClass: 1,
tokenId:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInBaseUnit: 1000,
minUnitsToExchange: 10,
numTokens: 0.02,
utxoTxid:
'241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
utxoVout: 0,
makerAddr: 'address',
tokenType: 1,
nostrEventId: 'test'
}
}
const deserealizeTxMock = {
//...
vout: [
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//...
]
}
export default {
nftOffer01,
nftTokenData01,
simpleNftOffer01,
simpleNftTokenData01,
fungibleOffer01,
fungibleTokenData01
fungibleTokenData01,
offerMockData,
deserealizeTxMock
};
+669 -74
View File
@@ -48,79 +48,70 @@ describe('#offer-use-case', () => {
)
}
})
it('should throw an error if order use cases are not passed in', () => {
try {
uut = new OfferLib({ adapters })
assert.fail('Unexpected code path')
console.log(uut) // linter
} catch (err) {
assert.include(
err.message,
'Instance of Order Use Cases must be passed in when instantiating Offer Use Cases library.'
)
}
})
})
describe('#createOffer', () => {
// it('should ignore an offer if utxo has been spent', async () => {
// const offerObj = {
// appId: 'swapTest555',
// data: {
// messageType: 1,
// messageClass: 1,
// tokenId:
// '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
// buyOrSell: 'sell',
// rateInSats: 1000,
// minSatsToExchange: 10,
// numTokens: 0.02,
// utxoTxid:
// '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
// utxoVout: 0
// },
// timestamp: '2021-09-20T17:54:26.395Z',
// localTimeStamp: '9/20/2021, 10:54:26 AM',
// txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
// hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
// }
//
// // Mock dependencies
// // 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)
//
// const result = await uut.createOffer(offerObj)
// // console.log('result: ', result)
//
// assert.equal(result, false)
// })
it('should handle error', async () => {
try {
await uut.createOffer()
assert.fail('unexpected code path')
} catch (error) {
assert.include(error.message, 'Cannot read properties of undefined')
}
})
// it('should create an offer and return the hash', async () => {
// const offerObj = {
// appId: 'swapTest555',
// data: {
// messageType: 1,
// messageClass: 1,
// tokenId:
// '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
// buyOrSell: 'sell',
// rateInBaseUnit: 1000,
// minUnitsToExchange: 10,
// numTokens: 0.02,
// utxoTxid:
// '241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87',
// utxoVout: 0,
// makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
// ticker: 'TROUT',
// tokenType: 1
// },
// timestamp: '2021-09-20T17:54:26.395Z',
// localTimeStamp: '9/20/2021, 10:54:26 AM',
// txid: '46f50f2a0cf44e3ed70dfb0618ef3ebfee57aabcf229b5d2d17c07322b54a8d7',
// hash: 'zdpuB2X25AZCKo3wpr4sSbw44vqPWJRqcxWQRHZccK5BdtoGD'
// }
//
// // Mock dependencies
// // 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)
//
// const result = await uut.createOffer(offerObj)
// // console.log('result: ', result)
//
// assert.equal(result, true)
// })
it('should return false if offer already exist', async () => {
const offerObj = mockData.offerMockData
// Mock dependencies
// sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false)
sandbox.stub(uut, 'findOfferByTxid').resolves({})
const result = await uut.createOffer(offerObj)
assert.isFalse(result)
})
it('should return false for invalid utxo', async () => {
const offerObj = mockData.offerMockData
// 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').resolves(null)
const result = await uut.createOffer(offerObj)
assert.isFalse(result)
})
it('should create offer', async () => {
const tokenDataMock = mockData.simpleNftTokenData01
const offerObj = mockData.offerMockData
// 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(false) // detectNsfw call
const result = await uut.createOffer(offerObj)
assert.isTrue(result)
})
})
describe('#categorizeToken', () => {
@@ -159,15 +150,29 @@ describe('#offer-use-case', () => {
assert.equal(result, 'fungible')
})
it('should unknow type', async () => {
try {
// Mock dependencies
const offerData = mockData.fungibleOffer01
const tokenData = { genesisData: {} }
await uut.categorizeToken(offerData, tokenData)
assert.fail('unexpected code path')
} catch (error) {
assert.include(error.message, 'Unknown token type:')
}
})
})
describe('#removeDuplicateOffers', () => {
it('should remove duplicate entries and return true', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([
{ p2wdbHash: 'a', remove: async () => {} },
{ p2wdbHash: 'a', remove: async () => {} },
{ p2wdbHash: 'b', remove: async () => {} }
{ p2wdbHash: 'a', remove: async () => { } },
{ p2wdbHash: 'a', remove: async () => { } },
{ p2wdbHash: 'b', remove: async () => { } }
])
// sandbox.stub(uut.OfferModel, 'remove').resolves()
@@ -180,8 +185,8 @@ describe('#offer-use-case', () => {
it('should return false if there are no duplicate entries', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([
{ p2wdbHash: 'a', remove: async () => {} },
{ p2wdbHash: 'b', remove: async () => {} }
{ p2wdbHash: 'a', remove: async () => { } },
{ p2wdbHash: 'b', remove: async () => { } }
])
// sandbox.stub(uut.OfferModel, 'remove').resolves()
@@ -190,5 +195,595 @@ describe('#offer-use-case', () => {
assert.equal(result, false)
})
it('should handle error', async () => {
try {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
// sandbox.stub(uut.OfferModel, 'remove').resolves()
await uut.removeDuplicateOffers()
assert.fail('unexpected code path')
} catch (error) {
assert.equal(error.message, 'test error')
}
})
})
describe('#removeStaleOffers', () => {
it('remove offer with wrong utxoState', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
sandbox.stub(uut.retryQueue, 'addToQueue').resolves(false)
await uut.removeStaleOffers()
})
it('remove offer with wrong txid', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
sandbox.stub(uut.retryQueue, 'addToQueue').throws(new Error('txid needs to be a proper transaction ID'))
await uut.removeStaleOffers()
})
it('remove expired offer ', async () => {
const tsMock = new Date()
tsMock.setMonth(tsMock.getMonth() - 3)
const timestamp = tsMock.getTime()
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([{ timestamp, remove: async () => { } }])
sandbox.stub(uut.retryQueue, 'addToQueue').resolves(true)
await uut.removeStaleOffers()
})
it('should handle axios error ', async () => {
const testErr = new Error()
testErr.isAxiosError = true
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
sandbox.stub(uut.retryQueue, 'addToQueue').throws(testErr)
await uut.removeStaleOffers()
})
it('should handle error ', async () => {
try {
const testErr = new Error('unknow error')
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }])
sandbox.stub(uut.retryQueue, 'addToQueue').throws(testErr)
await uut.removeStaleOffers()
assert.fail('unexpected code path')
} catch (error) {
assert.equal(error.message, 'unknow error')
}
})
})
describe('#findOfferByTxid', () => {
it('should throw an error if input is not provided', async () => {
try {
await uut.findOfferByTxid()
assert.fail('unexpected code path')
} catch (error) {
assert.equal(error.message, 'utxoTxid must be a string')
}
})
it('should throw an error if offer is not found', async () => {
try {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'findOne').resolves(null)
await uut.findOfferByTxid('241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87')
assert.fail('unexpected code path')
} catch (error) {
assert.equal(error.message, 'offer not found')
}
})
it('should return offer', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.OfferModel, 'findOne').resolves(mockData.nftOffer01)
const result = await uut.findOfferByTxid('241c06bf61384b8623477e419bf4779edbcc7e3bc862f0f179a9ed2967069b87')
assert.isObject(result)
})
})
describe('#detectNsfw', () => {
it('should return false for wrong cid format', async () => {
const result = await uut.detectNsfw({ mutableData: '' })
assert.isFalse(result)
})
it('should return true if nft boolean detected', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.axios, 'get').resolves({ data: { nsfw: true } })
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isTrue(result)
})
it('should return true if nft string detected', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.axios, 'get').resolves({ data: { nsfw: 'true' } })
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isTrue(result)
})
it('should return false if nfsw property does not exist', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.axios, 'get').resolves({ data: {} })
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isFalse(result)
})
it('should return false on error', async () => {
// Mock dependencies and force desired code path.
sandbox.stub(uut.axios, 'get').throws(new Error('test error'))
const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' })
assert.isFalse(result)
})
})
describe('#listOffers', () => {
it('should handle error', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
await uut.listOffers()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should list orders', async () => {
const queryMock = {
sort () {
return this
},
skip () {
return this
},
limit () { return [] }
}
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
const result = await uut.listOffers()
assert.isArray(result)
})
})
describe('#listNftOffers', () => {
it('should handle error', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
await uut.listNftOffers()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should list orders', async () => {
const queryMock = {
sort () {
return this
},
skip () {
return this
},
limit () { return [] }
}
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
const result = await uut.listNftOffers(1, true)
assert.isArray(result)
})
})
describe('#listFungibleOffers', () => {
it('should handle error', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error'))
await uut.listFungibleOffers()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should list orders', async () => {
const queryMock = {
sort () {
return this
},
skip () {
return this
},
limit () { return [] }
}
// Mock dependencies
sandbox.stub(uut.OfferModel, 'find').returns(queryMock)
const result = await uut.listFungibleOffers()
assert.isArray(result)
})
})
describe('#takeOffer', () => {
it('should handle error if input is not provided', async () => {
try {
await uut.takeOffer()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'eventId must be a string')
}
})
it('should handle error for wrong offer status', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'completed' })
await uut.takeOffer('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'offer status is not "posted", so offer is dead and can not be countered.')
}
})
it('should handle error for invalid utxo', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(false)
await uut.takeOffer('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'UTXO does not exist. Aborting.')
}
})
it('should handle insufficient funds', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
sandbox.stub(uut, 'ensureFunds').throws(new Error('test error'))
await uut.takeOffer('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should handle error if counter offer cant be calculated', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
sandbox.stub(uut, 'ensureFunds').resolves(true)
await uut.takeOffer('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Could not calculate the amount of BCH to generate counter offer')
}
})
it('should handle error if counter offer cant be calculated', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' })
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
sandbox.stub(uut, 'ensureFunds').resolves(true)
await uut.takeOffer('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Could not calculate the amount of BCH to generate counter offer')
}
})
it('should take offer', async () => {
// Mock data
const offerMock = Object.assign({}, mockData.offerMockData.data)
offerMock.remove = async () => { }
offerMock.offerStatus = 'posted'
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').returns(offerMock)
sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true)
sandbox.stub(uut, 'ensureFunds').resolves(true)
sandbox.stub(uut.adapters.wallet, 'moveBch').resolves({ sats: 1 })
await uut.takeOffer('eventId')
})
})
describe('#ensureFunds', () => {
// it('should handle insufficient funds to use p2wdb', async () => {
// try {
// // Mock dependencies
// sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(false)
//
// await uut.ensureFunds(mockData.offerMockData.data)
//
// assert.fail('Unexpected code path')
// } catch (err) {
// assert.include(err.message, 'App wallet does not have funds for writing to the P2WDB')
// }
// })
it('should throw error if sats needed could no be able to calculated', async () => {
try {
// Mock dependencies
sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true)
// Mock Input
const mock = Object.assign({}, mockData.offerMockData.data)
mock.rateInBaseUnit = null
await uut.ensureFunds(mock)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Could not calculate sats needed!')
}
})
it('should throw error if app wallet does not have enough bch', async () => {
try {
// Mock dependencies
sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true)
sandbox.stub(uut.adapters.wallet.bchWallet, 'getBalance').resolves(0)
await uut.ensureFunds(mockData.offerMockData.data)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'App wallet does not control enough BCH to purchase the tokens.')
}
})
it('should handle BUY offer', async () => {
try {
const mock = Object.assign({}, mockData.offerMockData.data)
mock.buyOrSell = 'buy'
await uut.ensureFunds(mock)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Buy offers are not supported yet.')
}
})
it('should return true', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true)
sandbox.stub(uut.adapters.wallet.bchWallet, 'getBalance').resolves(10 * 10 ** 6)
const result = await uut.ensureFunds(mockData.offerMockData.data)
assert.isTrue(result)
})
})
describe('#findOrderByEvent', () => {
it('should throw an error if hash is not provided', async () => {
try {
await uut.findOfferByEvent()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'nostrEventId must be a string')
}
})
it('should throw an error if order is not found!', async () => {
try {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'findOne').resolves(null)
await uut.findOfferByEvent('eventId')
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'offer not found')
}
})
it('should return offer by eventId', async () => {
// Mock dependencies
sandbox.stub(uut.OfferModel, 'findOne').resolves({ toObject: () => { return { hash: 'hash' } } })
const result = await uut.findOfferByEvent('eventId')
assert.isObject(result)
})
})
describe('#flagOffer', () => {
it('should throw an error if input is not provided', async () => {
try {
await uut.flagOffer()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, '"data" property is required')
}
})
it('should throw an error if offer is not found!', async () => {
try {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').resolves(null)
const input = {
data:
{
nostrEventId: 'eventId'
}
}
await uut.flagOffer(input)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'not found in the database')
}
})
it('should flag offer', async () => {
// Mock dependencies
sandbox.stub(uut, 'findOfferByEvent').resolves({ flags: ['a', 'b', 'c'], save: () => { } })
const input = {
data:
{
nostrEventId: 'eventId'
}
}
const result = await uut.flagOffer(input)
assert.isTrue(result)
})
})
describe('#loadOffers', () => {
it('should handle nostr error', async () => {
try {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'read').throws(new Error('test error'))
await uut.loadOffers()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should skip internal function errors ', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'read').resolves([mockData.offerMockData])
await uut.loadOffers()
})
it('should review and load offers', async () => {
// Mock dependencies
sandbox.stub(uut.adapters.nostr, 'read').resolves([JSON.stringify(mockData.offerMockData)])
await uut.loadOffers()
})
})
describe('#acceptCounterOffer', () => {
it('should return if order is not found!', async () => {
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').throws(new Error('test error'))
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.equal(result, 'N/A')
})
it('should handle error if counter offer cant be calculated', async () => {
try {
// Mock Data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.rateInBaseUnit = null
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
console.log('result: ', result)
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'Could not calculate the amount of BCH offered in the Counter Offer')
}
})
it('should handle error for wrong transaction output', async () => {
try {
// Mock Data
const mock = Object.assign({}, mockData.offerMockData.data)
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mockData.offerMockData.data)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
await uut.acceptCounterOffer({ data: mock })
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'The Counter Offer has an output of ')
assert.include(err.message, 'which does not match the required')
}
})
it('should handle error for wrong transaction output address', async () => {
try {
// 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.deserealizeTxMock)
await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'The Counter Offer has an output address of')
assert.include(err.message, 'which does not match the Maker address')
}
})
it('should return tx id', async () => {
// Mock data
const mock = Object.assign({}, mockData.offerMockData.data)
mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0] // Maker Address
mock.rateInBaseUnit = 0
mock.numTokens = 0
// Mock dependencies
sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock)
//
const result = await uut.acceptCounterOffer({ data: { /** .... */ } })
assert.isString(result)
})
})
})
+199 -2
View File
@@ -62,6 +62,69 @@ describe('#order-use-case', () => {
assert.equal(result, true)
})
it('should handle insufficient funds', async () => {
try {
sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(false)
const orderEntity = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 0,
numTokens: 1
}
await uut.ensureFunds(orderEntity)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'App wallet does not have funds for writing to the P2WDB')
}
})
it('should handle insufficient tokens to satisfy the order', async () => {
try {
const orderEntity = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
buyOrSell: 'sell',
rateInSats: 1000,
minSatsToExchange: 0,
numTokens: 1000
}
await uut.ensureFunds(orderEntity)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'App wallet does not have enough tokens to satisfy the SELL order.')
}
})
it('should handle BUY order', async () => {
try {
const orderEntity = {
lokadId: 'SWP',
messageType: 1,
messageClass: 1,
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
buyOrSell: 'buy',
rateInSats: 1000,
minSatsToExchange: 0,
numTokens: 1
}
await uut.ensureFunds(orderEntity)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Buy orders are not supported yet')
}
})
})
// describe('#moveTokens', () => {
@@ -113,12 +176,13 @@ describe('#order-use-case', () => {
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, 'initialize').resolves()
sandbox.stub(uut.adapters.p2wdb, 'write').resolves('fakeHash')
sandbox.stub(uut.adapters.nostr, 'post').resolves('fakeEvenetId')
const result = await uut.createOrder(entryObj)
console.log('result: ', result)
assert.isString(result)
assert.property(result, 'eventId')
assert.property(result, 'noteId')
})
it('should catch and throw an error', async () => {
@@ -137,4 +201,137 @@ describe('#order-use-case', () => {
assert.equal(result, false)
})
})
describe('#findOrderByEvent', () => {
it('should throw an error if hash is not provided', async () => {
try {
await uut.findOrderByEvent()
} catch (err) {
assert.include(err.message, 'nostrEventId must be a string')
}
})
it('should throw an error if order is not found!', async () => {
try {
sandbox.stub(uut.OrderModel, 'findOne').resolves(null)
await uut.findOrderByEvent('hash')
} catch (err) {
assert.include(err.message, 'order not found')
}
})
it('should return order by hash', async () => {
sandbox.stub(uut.OrderModel, 'findOne').resolves({ toObject: () => { return { hash: 'hash' } } })
const result = await uut.findOrderByEvent('hash')
assert.isObject(result)
})
})
describe('#removeStaleOrders', () => {
it('should remove stale orders', async () => {
const spy = sinon.spy() // Spy the function
// Create 2 orders mock .
const ordersMock = new Array(2)
ordersMock.fill({ remove: () => { spy.call() } })
sandbox.stub(uut.OrderModel, 'find').resolves(ordersMock)
sandbox.stub(uut.retryQueue, 'addToQueue').resolves(false)
await uut.removeStaleOrders()
assert.equal(spy.callCount, 2, 'Expected to be called twice.')
})
it('should handle wrong txid', async () => {
const spy = sinon.spy() // Spy the function
// Create 2 orders mock .
const ordersMock = new Array(2)
ordersMock.fill({ remove: () => { spy.call() } })
sandbox.stub(uut.OrderModel, 'find').resolves(ordersMock)
sandbox.stub(uut.retryQueue, 'addToQueue').throws(new Error('txid needs to be a proper transaction ID'))
await uut.removeStaleOrders()
assert.equal(spy.callCount, 2, 'Expected to be called twice.')
})
it('should skip order on axios error', async () => {
const spy = sinon.spy() // Spy the function
// Create 2 orders mock .
const ordersMock = new Array(2)
ordersMock.fill({ remove: () => { spy.call() } })
const axiosError = new Error()
axiosError.isAxiosError = true
sandbox.stub(uut.OrderModel, 'find').resolves(ordersMock)
sandbox.stub(uut.retryQueue, 'addToQueue').throws(axiosError)
await uut.removeStaleOrders()
assert.equal(spy.callCount, 0, 'Expected to no be called.')
})
it('should handle unknow error', async () => {
try {
// Create 2 orders mock .
const ordersMock = new Array(2)
ordersMock.fill({ remove: () => { } })
sandbox.stub(uut.OrderModel, 'find').resolves(ordersMock)
sandbox.stub(uut.retryQueue, 'addToQueue').throws(new Error('test error'))
await uut.removeStaleOrders()
assert.fail('unexpected code path')
} catch (error) {
assert.include(error.message, 'test error')
}
})
})
describe('#listOrders', () => {
it('should handle error', async () => {
try {
sandbox.stub(uut.OrderModel, 'find').throws(new Error('test error'))
await uut.listOrders()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should list orders', async () => {
const queryMock = {
sort () {
return this
},
skip () {
return this
},
limit () { return [] }
}
sandbox.stub(uut.OrderModel, 'find').returns(queryMock)
const result = await uut.listOrders()
assert.isArray(result)
})
})
describe('#deleteOrder', () => {
it('should handle error', async () => {
try {
sandbox.stub(uut, 'findOrderByEvent').throws(new Error('test error'))
await uut.deleteOrder()
assert.fail('unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should delete orders', async () => {
sandbox.stub(uut, 'findOrderByEvent').resolves([])
sandbox.stub(uut.adapters.wallet, 'reclaimTokens').resolves('txid')
const result = await uut.deleteOrder()
assert.isString(result)
})
})
})
+5 -4
View File
@@ -9,10 +9,10 @@
// Public npm libraries
import BCHJS from '@psf/bch-js'
import BchTokenSweep from 'bch-token-sweep/index'
import BchTokenSweep from 'bch-token-sweep'
// Local libraries
import WalletAdapter from '../../src/adapters/wallet'
import WalletAdapter from '../../src/adapters/wallet.js'
// Constants
const EMTPY_ADDR_CUTOFF = 15
@@ -22,6 +22,7 @@ async function sweepFunds () {
// Open the wallet files.
const wallet = new WalletAdapter()
const walletInfo = await wallet.openWallet()
const bchWallet = await wallet.instanceWallet(walletInfo)
console.log('walletInfo: ', walletInfo)
const rootAddr = walletInfo.cashAddress
@@ -49,7 +50,7 @@ async function sweepFunds () {
const sweeper = new BchTokenSweep(
wifToSweep,
rootWif,
bchjs,
bchWallet,
550,
rootAddr
)
@@ -68,7 +69,7 @@ async function sweepFunds () {
// Wait between loop iterations.
await bchjs.Util.sleep(3000)
} catch (err) {
console.log(`error message with index ${hdIndex}: ${err.message}`)
console.log(`error message with index ${hdIndex}: ${err}`)
emptyAddrCnt++
}