diff --git a/package-lock.json b/package-lock.json index ba9563e7..8aee6fa0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,15 +11,15 @@ "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/random": "^5.7.0", + "@iexec/dataprotector": "^2.0.0-beta.21", "buffer": "^6.0.3", "ethers": "^6.13.2", "graphql-request": "^6.1.0", - "iexec": "^8.20.0", + "iexec": "^8.22.0", "kubo-rpc-client": "^4.1.1", "yup": "^1.1.1" }, "devDependencies": { - "@iexec/dataprotector": "^2.0.0-beta.19", "@jest/globals": "^29.7.0", "@swc/core": "^1.3.96", "@swc/jest": "^0.2.29", @@ -830,10 +830,9 @@ "license": "BSD-3-Clause" }, "node_modules/@iexec/dataprotector": { - "version": "2.0.0-beta.19", - "resolved": "https://registry.npmjs.org/@iexec/dataprotector/-/dataprotector-2.0.0-beta.19.tgz", - "integrity": "sha512-nKfM8H2AGFPmSHt96FhNSOIctqRWyQt34zh9chVeI7PSy6TVFQMnEV4rj1ce+O8yFO9TM/8YXxu+V2izBO00WQ==", - "dev": true, + "version": "2.0.0-beta.21", + "resolved": "https://registry.npmjs.org/@iexec/dataprotector/-/dataprotector-2.0.0-beta.21.tgz", + "integrity": "sha512-mKkkT0N8M3/lIZTnTZV+dCw7orAcYGoMwl3L4HBZqIeVS1OmxctwtFapnUpkdoIRL2oFHeALC0EowDQns8rkpQ==", "license": "Apache-2.0", "dependencies": { "@ethersproject/bytes": "^5.7.0", @@ -845,14 +844,111 @@ "debug": "^4.3.4", "ethers": "^6.13.2", "graphql-request": "^6.0.0", - "iexec": "^8.18.0", + "iexec": "^8.22.0", "jszip": "^3.7.1", - "kubo-rpc-client": "^4.1.1", + "kubo-rpc-client": "^5.4.1", "magic-bytes.js": "^1.0.15", "typechain": "^8.3.2", "yup": "^1.0.2" } }, + "node_modules/@iexec/dataprotector/node_modules/@libp2p/interface": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@libp2p/interface/-/interface-2.11.0.tgz", + "integrity": "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/dns": "^1.0.6", + "@multiformats/multiaddr": "^12.4.4", + "it-pushable": "^3.2.3", + "it-stream-types": "^2.0.2", + "main-event": "^1.0.1", + "multiformats": "^13.3.6", + "progress-events": "^1.0.1", + "uint8arraylist": "^2.4.8" + } + }, + "node_modules/@iexec/dataprotector/node_modules/@libp2p/logger": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@libp2p/logger/-/logger-5.2.0.tgz", + "integrity": "sha512-OEFS529CnIKfbWEHmuCNESw9q0D0hL8cQ8klQfjIVPur15RcgAEgc1buQ7Y6l0B6tCYg120bp55+e9tGvn8c0g==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.11.0", + "@multiformats/multiaddr": "^12.4.4", + "interface-datastore": "^8.3.1", + "multiformats": "^13.3.6", + "weald": "^1.0.4" + } + }, + "node_modules/@iexec/dataprotector/node_modules/@libp2p/peer-id": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@libp2p/peer-id/-/peer-id-5.1.9.tgz", + "integrity": "sha512-cVDp7lX187Epmi/zr0Qq2RsEMmueswP9eIxYSFoMcHL/qcvRFhsxOfUGB8361E26s2WJvC9sXZ0oJS9XVueJhQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "multiformats": "^13.3.6", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@iexec/dataprotector/node_modules/@multiformats/multiaddr-to-uri": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr-to-uri/-/multiaddr-to-uri-11.0.2.tgz", + "integrity": "sha512-SiLFD54zeOJ0qMgo9xv1Tl9O5YktDKAVDP4q4hL16mSq4O4sfFNagNADz8eAofxd6TfQUzGQ3TkRRG9IY2uHRg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/multiaddr": "^12.3.0" + } + }, + "node_modules/@iexec/dataprotector/node_modules/kubo-rpc-client": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/kubo-rpc-client/-/kubo-rpc-client-5.4.1.tgz", + "integrity": "sha512-v86bQWtyA//pXTrt9y4iEwjW6pt1gA18Z1famWXIR/HN5TFdYwQ3yHOlRE6JSWBDQ0rR6FOMyrrGy8To78mXow==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-cbor": "^9.0.0", + "@ipld/dag-json": "^10.0.0", + "@ipld/dag-pb": "^4.0.0", + "@libp2p/crypto": "^5.0.0", + "@libp2p/interface": "^2.0.0", + "@libp2p/logger": "^5.0.0", + "@libp2p/peer-id": "^5.0.0", + "@multiformats/multiaddr": "^12.2.1", + "@multiformats/multiaddr-to-uri": "^11.0.0", + "any-signal": "^4.1.1", + "blob-to-it": "^2.0.5", + "browser-readablestream-to-it": "^2.0.5", + "dag-jose": "^5.0.0", + "electron-fetch": "^1.9.1", + "err-code": "^3.0.1", + "ipfs-unixfs": "^11.1.4", + "iso-url": "^1.2.1", + "it-all": "^3.0.4", + "it-first": "^3.0.4", + "it-glob": "^3.0.1", + "it-last": "^3.0.4", + "it-map": "^3.0.5", + "it-peekable": "^3.0.3", + "it-to-stream": "^1.0.0", + "merge-options": "^3.0.4", + "multiformats": "^13.1.0", + "nanoid": "^5.0.7", + "native-fetch": "^4.0.2", + "parse-duration": "^2.1.2", + "react-native-fetch-api": "^3.0.0", + "stream-to-it": "^1.0.1", + "uint8arrays": "^5.0.3", + "wherearewe": "^2.0.1" + } + }, + "node_modules/@iexec/dataprotector/node_modules/parse-duration": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-2.1.4.tgz", + "integrity": "sha512-b98m6MsCh+akxfyoz9w9dt0AlH2dfYLOBss5SdDsr9pkhKNvkWBXU/r8A4ahmIGByBOLV2+4YwfCuFxbDDaGyg==", + "license": "MIT" + }, "node_modules/@iexec/interface": { "version": "3.0.35-8", "resolved": "https://registry.npmjs.org/@iexec/interface/-/interface-3.0.35-8.tgz", @@ -1796,6 +1892,74 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "license": "MIT" }, + "node_modules/@libp2p/crypto": { + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@libp2p/crypto/-/crypto-5.1.13.tgz", + "integrity": "sha512-8NN9cQP3jDn+p9+QE9ByiEoZ2lemDFf/unTgiKmS3JF93ph240EUVdbCyyEgOMfykzb0okTM4gzvwfx9osJebQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^3.1.0", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", + "multiformats": "^13.4.0", + "protons-runtime": "^5.6.0", + "uint8arraylist": "^2.4.8", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@libp2p/crypto/node_modules/@libp2p/interface": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@libp2p/interface/-/interface-3.1.0.tgz", + "integrity": "sha512-RE7/XyvC47fQBe1cHxhMvepYKa5bFCUyFrrpj8PuM0E7JtzxU7F+Du5j4VXbg2yLDcToe0+j8mB7jvwE2AThYw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/dns": "^1.0.6", + "@multiformats/multiaddr": "^13.0.1", + "main-event": "^1.0.1", + "multiformats": "^13.4.0", + "progress-events": "^1.0.1", + "uint8arraylist": "^2.4.8" + } + }, + "node_modules/@libp2p/crypto/node_modules/@multiformats/multiaddr": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr/-/multiaddr-13.0.1.tgz", + "integrity": "sha512-XToN915cnfr6Lr9EdGWakGJbPT0ghpg/850HvdC+zFX8XvpLZElwa8synCiwa8TuvKNnny6m8j8NVBNCxhIO3g==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1", + "multiformats": "^13.0.0", + "uint8-varint": "^2.0.1", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/@libp2p/crypto/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@libp2p/crypto/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@libp2p/interface": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@libp2p/interface/-/interface-1.7.0.tgz", @@ -2227,7 +2391,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.5.1.tgz", "integrity": "sha512-F+GklO8jBWlsaVV+9oHaPh5NJdd6rAKN4tklGfInX1Q7h0xPgVLP39Jl3eCulPB5qexI71ZFHwbljx4ZXNfouA==", - "dev": true, "license": "MIT", "dependencies": { "lodash": "^4.17.15", @@ -2377,7 +2540,6 @@ "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "dev": true, "license": "MIT" }, "node_modules/@types/semver": { @@ -2785,7 +2947,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3079,7 +3240,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -3121,7 +3281,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/borsh/-/borsh-2.0.0.tgz", "integrity": "sha512-kc9+BgR3zz9+cjbwM8ODoUB4fs3X3I5A/HtX7LZKxCLaMrEeDFoBpnhZY//DTS1VZBSs6S5v46RZRbZjRFspEg==", - "dev": true, "license": "Apache-2.0" }, "node_modules/brace-expansion": { @@ -3330,7 +3489,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -3522,7 +3680,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", - "dev": true, "license": "MIT", "dependencies": { "array-back": "^3.1.0", @@ -3538,7 +3695,6 @@ "version": "6.1.3", "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", - "dev": true, "license": "MIT", "dependencies": { "array-back": "^4.0.2", @@ -3554,7 +3710,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -3567,7 +3722,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3577,7 +3731,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -3592,7 +3745,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -3602,14 +3754,12 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, "license": "MIT" }, "node_modules/command-line-usage/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.0" @@ -3619,7 +3769,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3629,7 +3778,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -3642,7 +3790,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3661,7 +3808,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/confusing-browser-globals": { @@ -5015,7 +5161,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", - "dev": true, "license": "MIT", "dependencies": { "array-back": "^3.0.1" @@ -5097,7 +5242,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -5453,7 +5597,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5592,9 +5735,9 @@ "license": "BSD-3-Clause" }, "node_modules/iexec": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/iexec/-/iexec-8.20.0.tgz", - "integrity": "sha512-ce0l1eJWqOj85kWXbUJ7yF6IhbHiHz1hRqLdDEAJhCvlIAArdyrwJcWtXz6gmuYoSQ0F8gkKvEWC/FHg4Gr/8w==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/iexec/-/iexec-8.22.0.tgz", + "integrity": "sha512-6mqR0Vp6eHAcEelAjeXuGn8EzmsbPdHsSdBfSXlrM8hHMKOCVFPbgkRind1VFlDxhvKvHaY8szk0xUELa5YCnA==", "license": "Apache-2.0", "dependencies": { "@ensdomains/ens-contracts": "^1.2.5", @@ -5612,6 +5755,8 @@ "inquirer": "^12.5.0", "is-docker": "^3.0.0", "jszip": "^3.10.1", + "kubo-rpc-client": "^5.3.0", + "multiformats": "^13.4.1", "node-forge": "^1.3.1", "ora": "^8.2.0", "prettyjson": "^1.2.5", @@ -5625,6 +5770,56 @@ "iexec": "dist/esm/cli/cmd/iexec.js" } }, + "node_modules/iexec/node_modules/@libp2p/interface": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@libp2p/interface/-/interface-2.11.0.tgz", + "integrity": "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/dns": "^1.0.6", + "@multiformats/multiaddr": "^12.4.4", + "it-pushable": "^3.2.3", + "it-stream-types": "^2.0.2", + "main-event": "^1.0.1", + "multiformats": "^13.3.6", + "progress-events": "^1.0.1", + "uint8arraylist": "^2.4.8" + } + }, + "node_modules/iexec/node_modules/@libp2p/logger": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@libp2p/logger/-/logger-5.2.0.tgz", + "integrity": "sha512-OEFS529CnIKfbWEHmuCNESw9q0D0hL8cQ8klQfjIVPur15RcgAEgc1buQ7Y6l0B6tCYg120bp55+e9tGvn8c0g==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface": "^2.11.0", + "@multiformats/multiaddr": "^12.4.4", + "interface-datastore": "^8.3.1", + "multiformats": "^13.3.6", + "weald": "^1.0.4" + } + }, + "node_modules/iexec/node_modules/@libp2p/peer-id": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@libp2p/peer-id/-/peer-id-5.1.9.tgz", + "integrity": "sha512-cVDp7lX187Epmi/zr0Qq2RsEMmueswP9eIxYSFoMcHL/qcvRFhsxOfUGB8361E26s2WJvC9sXZ0oJS9XVueJhQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/crypto": "^5.1.8", + "@libp2p/interface": "^2.11.0", + "multiformats": "^13.3.6", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/iexec/node_modules/@multiformats/multiaddr-to-uri": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr-to-uri/-/multiaddr-to-uri-11.0.2.tgz", + "integrity": "sha512-SiLFD54zeOJ0qMgo9xv1Tl9O5YktDKAVDP4q4hL16mSq4O4sfFNagNADz8eAofxd6TfQUzGQ3TkRRG9IY2uHRg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/multiaddr": "^12.3.0" + } + }, "node_modules/iexec/node_modules/graphql-request": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.2.0.tgz", @@ -5637,6 +5832,53 @@ "graphql": "14 - 16" } }, + "node_modules/iexec/node_modules/kubo-rpc-client": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/kubo-rpc-client/-/kubo-rpc-client-5.4.1.tgz", + "integrity": "sha512-v86bQWtyA//pXTrt9y4iEwjW6pt1gA18Z1famWXIR/HN5TFdYwQ3yHOlRE6JSWBDQ0rR6FOMyrrGy8To78mXow==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-cbor": "^9.0.0", + "@ipld/dag-json": "^10.0.0", + "@ipld/dag-pb": "^4.0.0", + "@libp2p/crypto": "^5.0.0", + "@libp2p/interface": "^2.0.0", + "@libp2p/logger": "^5.0.0", + "@libp2p/peer-id": "^5.0.0", + "@multiformats/multiaddr": "^12.2.1", + "@multiformats/multiaddr-to-uri": "^11.0.0", + "any-signal": "^4.1.1", + "blob-to-it": "^2.0.5", + "browser-readablestream-to-it": "^2.0.5", + "dag-jose": "^5.0.0", + "electron-fetch": "^1.9.1", + "err-code": "^3.0.1", + "ipfs-unixfs": "^11.1.4", + "iso-url": "^1.2.1", + "it-all": "^3.0.4", + "it-first": "^3.0.4", + "it-glob": "^3.0.1", + "it-last": "^3.0.4", + "it-map": "^3.0.5", + "it-peekable": "^3.0.3", + "it-to-stream": "^1.0.0", + "merge-options": "^3.0.4", + "multiformats": "^13.1.0", + "nanoid": "^5.0.7", + "native-fetch": "^4.0.2", + "parse-duration": "^2.1.2", + "react-native-fetch-api": "^3.0.0", + "stream-to-it": "^1.0.1", + "uint8arrays": "^5.0.3", + "wherearewe": "^2.0.1" + } + }, + "node_modules/iexec/node_modules/parse-duration": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-2.1.4.tgz", + "integrity": "sha512-b98m6MsCh+akxfyoz9w9dt0AlH2dfYLOBss5SdDsr9pkhKNvkWBXU/r8A4ahmIGByBOLV2+4YwfCuFxbDDaGyg==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5705,7 +5947,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -7056,7 +7297,6 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "dev": true, "license": "MIT" }, "node_modules/js-tokens": { @@ -7284,14 +7524,12 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -7355,9 +7593,14 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", - "dev": true, "license": "MIT" }, + "node_modules/main-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/main-event/-/main-event-1.0.1.tgz", + "integrity": "sha512-NWtdGrAca/69fm6DIVd8T9rtfDII4Q8NQbIbsKQq2VzS9eqOGYs8uaNQjcuaCq/d9H/o625aOTJX2Qoxzqw0Pw==", + "license": "Apache-2.0 OR MIT" + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -7486,7 +7729,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -7502,9 +7744,9 @@ "license": "MIT" }, "node_modules/multiformats": { - "version": "13.3.7", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.7.tgz", - "integrity": "sha512-meL9DERHj+fFVWoOX9fXqfcYcSpUfSYJPcFvDPKrxitICbwAoWR+Ut4j5NO9zAT917HUHLQmqzQbAsGNHlDcxQ==", + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", "license": "Apache-2.0 OR MIT" }, "node_modules/mute-stream": { @@ -7733,7 +7975,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -8021,7 +8262,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8176,7 +8416,6 @@ "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, "license": "MIT", "bin": { "prettier": "bin-prettier.js" @@ -8404,7 +8643,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9048,7 +9286,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", - "dev": true, "license": "WTFPL OR MIT" }, "node_modules/string-length": { @@ -9217,7 +9454,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -9243,7 +9479,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", - "dev": true, "license": "MIT", "dependencies": { "array-back": "^4.0.1", @@ -9259,7 +9494,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9269,7 +9503,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9387,7 +9620,6 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", - "dev": true, "license": "ISC", "dependencies": { "chalk": "^4.1.0", @@ -9403,7 +9635,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", - "dev": true, "license": "MIT", "peerDependencies": { "typescript": ">=3.7.0" @@ -9514,7 +9745,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/prettier": "^2.1.1", @@ -9539,7 +9769,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9550,7 +9779,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", @@ -9566,7 +9794,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -9587,7 +9814,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -9597,7 +9823,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -9610,7 +9835,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -9698,7 +9922,6 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -9712,7 +9935,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10073,7 +10295,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", - "dev": true, "license": "MIT", "dependencies": { "reduce-flatten": "^2.0.0", @@ -10087,7 +10308,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10131,7 +10351,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index 06e1c23c..a0473924 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,7 @@ "scripts": { "build": "rm -rf dist && tsc --project tsconfig.build.json", "check-types": "tsc --noEmit", - "test:prepare": "node tests/scripts/prepare-bellecour-fork-for-tests.js", - "test": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/**/*.test.ts\" --forceExit -b", + "test:prepare": "node tests/scripts/prepare-bellecour-fork-for-tests.js && node tests/scripts/prepare-iexec.js", "test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/**/*.test.ts\" --forceExit --coverage", "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/unit/**/*.test.ts\" -b", "test:unit:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/unit/**/*.unit.ts\" --coverage", @@ -31,7 +30,7 @@ "format": "prettier --write \"(src|tests)/**/*.ts\"", "check-format": "prettier --check \"(src|tests)/**/*.ts\"", "stop-test-stack": "cd tests && docker compose down --volumes --remove-orphans", - "start-test-stack": "cd tests && npm run stop-test-stack && node scripts/prepare-test-env.js && docker compose build && docker compose up -d && node scripts/prepare-bellecour-fork-for-tests.js" + "start-test-stack": "cd tests && npm run stop-test-stack && node scripts/prepare-test-env.js && docker compose build && docker compose up -d && npm run test:prepare" }, "repository": { "type": "git", @@ -49,15 +48,15 @@ "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/random": "^5.7.0", + "@iexec/dataprotector": "^2.0.0-beta.21", "buffer": "^6.0.3", "ethers": "^6.13.2", "graphql-request": "^6.1.0", - "iexec": "^8.20.0", + "iexec": "^8.22.0", "kubo-rpc-client": "^4.1.1", "yup": "^1.1.1" }, "devDependencies": { - "@iexec/dataprotector": "^2.0.0-beta.19", "@jest/globals": "^29.7.0", "@swc/core": "^1.3.96", "@swc/jest": "^0.2.29", diff --git a/src/utils/subgraphQuery.ts b/src/utils/subgraphQuery.ts index 13d1b811..e995643a 100644 --- a/src/utils/subgraphQuery.ts +++ b/src/utils/subgraphQuery.ts @@ -71,20 +71,23 @@ export const getValidContact = async ( ); // Convert protectedData[] into Contact[] using the map for constant time lookups - return protectedDataList.map(({ id, name }) => { - const contact = contactsMap.get(id); - if (contact) { - return { - address: id, - name: name, - remainingAccess: contact.remainingAccess, - accessPrice: contact.accessPrice, - owner: contact.owner, - accessGrantTimestamp: contact.accessGrantTimestamp, - isUserStrict: contact.isUserStrict, - }; - } - }); + return protectedDataList + .map(({ id, name }) => { + const contact = contactsMap.get(id); + if (contact) { + return { + address: id, + name: name, + remainingAccess: contact.remainingAccess, + accessPrice: contact.accessPrice, + owner: contact.owner, + accessGrantTimestamp: contact.accessGrantTimestamp, + isUserStrict: contact.isUserStrict, + grantedAccess: contact.grantedAccess, + }; + } + }) + .filter((contact) => !!contact); } catch (error) { throw new WorkflowError({ message: 'Failed to fetch subgraph', diff --git a/src/utils/validators.ts b/src/utils/validators.ts index 87ce92db..24d4edb2 100644 --- a/src/utils/validators.ts +++ b/src/utils/validators.ts @@ -1,6 +1,7 @@ import { isAddress } from 'ethers'; import { IExec } from 'iexec'; -import { ValidationError, boolean, number, string } from 'yup'; +import { NULL_ADDRESS } from 'iexec/utils'; +import { ValidationError, boolean, number, object, string } from 'yup'; export const isValidProvider = async (iexec: IExec) => { const client = await iexec.config.resolveContractsClient(); @@ -61,3 +62,69 @@ export const positiveNumberSchema = () => export const booleanSchema = () => boolean().strict().typeError('${path} should be a boolean'); + +const isPositiveIntegerStringTest = (value: string) => /^\d+$/.test(value); + +const stringSchema = () => + string().strict().typeError('${path} should be a string'); + +const positiveIntegerStringSchema = () => + string().test( + 'is-positive-int', + '${path} should be a positive integer', + (value) => isUndefined(value) || isPositiveIntegerStringTest(value) + ); + +const positiveStrictIntegerStringSchema = () => + string().test( + 'is-positive-strict-int', + '${path} should be a strictly positive integer', + (value) => + isUndefined(value) || + (value !== '0' && isPositiveIntegerStringTest(value)) + ); + +export const campaignRequestSchema = () => + object({ + app: addressSchema().required(), + appmaxprice: positiveIntegerStringSchema().required(), + workerpool: addressSchema().required(), + workerpoolmaxprice: positiveIntegerStringSchema().required(), + dataset: addressSchema().oneOf([NULL_ADDRESS]).required(), + datasetmaxprice: positiveIntegerStringSchema().oneOf(['0']).required(), + params: stringSchema() + .test( + 'is-valid-bulk-params', + '${path} should be a valid JSON string with bulk_cid field', + (value) => { + try { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { bulk_cid } = JSON.parse(value); + if (typeof bulk_cid === 'string') { + return true; + } + } catch {} + return false; + } + ) + .required(), + requester: addressSchema().required(), + beneficiary: addressSchema().required(), + callback: addressSchema().required(), + category: positiveIntegerStringSchema().required(), + volume: positiveStrictIntegerStringSchema().required(), + tag: stringSchema().required(), + trust: positiveIntegerStringSchema().required(), + salt: stringSchema().required(), + sign: stringSchema().required(), + }) + .strict() + .typeError('${path} should be a BulkRequest object') + .test('is-defined', '${path} is required', (value) => { + // Check if value is undefined, null, or an empty object (which would be coerced from undefined) + return ( + value !== undefined && + value !== null && + !(typeof value === 'object' && Object.keys(value).length === 0) + ); + }); diff --git a/src/web3mail/IExecWeb3mail.ts b/src/web3mail/IExecWeb3mail.ts index 400b850c..7c70ba21 100644 --- a/src/web3mail/IExecWeb3mail.ts +++ b/src/web3mail/IExecWeb3mail.ts @@ -1,9 +1,12 @@ import { AbstractProvider, AbstractSigner, Eip1193Provider } from 'ethers'; import { IExec } from 'iexec'; +import { IExecDataProtectorCore } from '@iexec/dataprotector'; import { GraphQLClient } from 'graphql-request'; import { fetchUserContacts } from './fetchUserContacts.js'; import { fetchMyContacts } from './fetchMyContacts.js'; import { sendEmail } from './sendEmail.js'; +import { prepareEmailCampaign } from './prepareEmailCampaign.js'; +import { sendEmailCampaign } from './sendEmailCampaign.js'; import { Contact, FetchUserContactsParams, @@ -13,6 +16,10 @@ import { SendEmailResponse, Web3SignerProvider, FetchMyContactsParams, + PrepareEmailCampaignParams, + PrepareEmailCampaignResponse, + SendEmailCampaignParams, + SendEmailCampaignResponse, } from './types.js'; import { isValidProvider } from '../utils/validators.js'; import { getChainIdFromProvider } from '../utils/getChainId.js'; @@ -34,6 +41,7 @@ interface Web3mailResolvedConfig { ipfsGateway: string; defaultWorkerpool: string; iexec: IExec; + dataProtector: IExecDataProtectorCore; } export class IExecWeb3mail { @@ -51,6 +59,8 @@ export class IExecWeb3mail { private iexec!: IExec; + private dataProtector!: IExecDataProtectorCore; + private initPromise: Promise | null = null; private ethProvider: EthersCompatibleProvider; @@ -75,6 +85,7 @@ export class IExecWeb3mail { this.ipfsGateway = config.ipfsGateway; this.defaultWorkerpool = config.defaultWorkerpool; this.iexec = config.iexec; + this.dataProtector = config.dataProtector; }); } return this.initPromise; @@ -121,6 +132,36 @@ export class IExecWeb3mail { }); } + async prepareEmailCampaign( + args: PrepareEmailCampaignParams + ): Promise { + await this.init(); + await isValidProvider(this.iexec); + return prepareEmailCampaign({ + ...args, + workerpoolAddressOrEns: + args.workerpoolAddressOrEns || this.defaultWorkerpool, + iexec: this.iexec, + dataProtector: this.dataProtector, + ipfsNode: this.ipfsNode, + ipfsGateway: this.ipfsGateway, + dappAddressOrENS: this.dappAddressOrENS, + }); + } + + async sendEmailCampaign( + args: SendEmailCampaignParams + ): Promise { + await this.init(); + await isValidProvider(this.iexec); + return sendEmailCampaign({ + ...args, + workerpoolAddressOrEns: + args.workerpoolAddressOrEns || this.defaultWorkerpool, + dataProtector: this.dataProtector, + }); + } + private async resolveConfig(): Promise { const chainId = await getChainIdFromProvider(this.ethProvider); const chainDefaultConfig = getChainDefaultConfig(chainId, { @@ -184,6 +225,17 @@ export class IExecWeb3mail { throw new Error(`Failed to create GraphQLClient: ${error.message}`); } + const dataProtector = new IExecDataProtectorCore(this.ethProvider, { + iexecOptions: { + ipfsGatewayURL: ipfsGateway, + ...this.options?.iexecOptions, + allowExperimentalNetworks: this.options.allowExperimentalNetworks, + }, + ipfsGateway, + ipfsNode, + subgraphUrl, + }); + return { dappAddressOrENS, dappWhitelistAddress: dappWhitelistAddress.toLowerCase(), @@ -192,6 +244,7 @@ export class IExecWeb3mail { ipfsNode, ipfsGateway, iexec, + dataProtector, }; } } diff --git a/src/web3mail/fetchMyContacts.ts b/src/web3mail/fetchMyContacts.ts index f7c6e7ae..4c062314 100644 --- a/src/web3mail/fetchMyContacts.ts +++ b/src/web3mail/fetchMyContacts.ts @@ -16,6 +16,7 @@ export const fetchMyContacts = async ({ dappAddressOrENS = throwIfMissing(), dappWhitelistAddress = throwIfMissing(), isUserStrict = false, + bulkOnly = false, }: IExecConsumer & SubgraphConsumer & DappAddressConsumer & @@ -24,6 +25,7 @@ export const fetchMyContacts = async ({ const vIsUserStrict = booleanSchema() .label('isUserStrict') .validateSync(isUserStrict); + const vBulkOnly = booleanSchema().label('bulkOnly').validateSync(bulkOnly); const userAddress = await iexec.wallet.getAddress(); return fetchUserContacts({ @@ -33,5 +35,6 @@ export const fetchMyContacts = async ({ dappWhitelistAddress, userAddress, isUserStrict: vIsUserStrict, + bulkOnly: vBulkOnly, }); }; diff --git a/src/web3mail/fetchUserContacts.ts b/src/web3mail/fetchUserContacts.ts index 8b89d6ac..9e7ceb03 100644 --- a/src/web3mail/fetchUserContacts.ts +++ b/src/web3mail/fetchUserContacts.ts @@ -27,6 +27,7 @@ export const fetchUserContacts = async ({ dappWhitelistAddress = throwIfMissing(), userAddress, isUserStrict = false, + bulkOnly = false, }: IExecConsumer & SubgraphConsumer & DappAddressConsumer & @@ -47,6 +48,7 @@ export const fetchUserContacts = async ({ const vIsUserStrict = booleanSchema() .label('isUserStrict') .validateSync(isUserStrict); + const vBulkOnly = booleanSchema().label('bulkOnly').validateSync(bulkOnly); try { const [dappOrders, whitelistOrders] = await Promise.all([ @@ -55,12 +57,14 @@ export const fetchUserContacts = async ({ userAddress: vUserAddress, appAddress: vDappAddressOrENS, isUserStrict: vIsUserStrict, + bulkOnly: vBulkOnly, }), fetchAllOrdersByApp({ iexec, userAddress: vUserAddress, appAddress: vDappWhitelistAddress, isUserStrict: vIsUserStrict, + bulkOnly: vBulkOnly, }), ]); @@ -84,6 +88,18 @@ export const fetchUserContacts = async ({ accessPrice: order.order.datasetprice, accessGrantTimestamp: order.publicationTimestamp, isUserStrict: order.order.requesterrestrict !== ZeroAddress, + grantedAccess: { + dataset: order.order.dataset, + datasetprice: order.order.datasetprice.toString(), + volume: order.order.volume.toString(), + tag: order.order.tag.toString(), + apprestrict: order.order.apprestrict, + workerpoolrestrict: order.order.workerpoolrestrict, + requesterrestrict: order.order.requesterrestrict, + salt: order.order.salt, + sign: order.order.sign, + remainingAccess: order.remaining, + }, }; myContacts.push(contact); } @@ -107,23 +123,24 @@ async function fetchAllOrdersByApp({ userAddress, appAddress, isUserStrict, + bulkOnly, }: { iexec: IExec; userAddress: string; appAddress: string; isUserStrict: boolean; + bulkOnly: boolean; }): Promise { - const ordersFirstPage = iexec.orderbook.fetchDatasetOrderbook( - ANY_DATASET_ADDRESS, - { - app: appAddress, - requester: userAddress, - isAppStrict: true, - isRequesterStrict: isUserStrict, - // Use maxPageSize here to avoid too many round-trips (we want everything anyway) - pageSize: 1000, - } - ); + const ordersFirstPage = iexec.orderbook.fetchDatasetOrderbook({ + dataset: ANY_DATASET_ADDRESS, + app: appAddress, + requester: userAddress, + isAppStrict: true, + isRequesterStrict: isUserStrict, + bulkOnly, + // Use maxPageSize here to avoid too many round-trips (we want everything anyway) + pageSize: 1000, + }); const { orders: allOrders } = await autoPaginateRequest({ request: ordersFirstPage, }); diff --git a/src/web3mail/internalTypes.ts b/src/web3mail/internalTypes.ts index d6ea0549..a68a98b6 100644 --- a/src/web3mail/internalTypes.ts +++ b/src/web3mail/internalTypes.ts @@ -1,4 +1,5 @@ import { IExec } from 'iexec'; +import { IExecDataProtectorCore } from '@iexec/dataprotector'; import { AddressOrENS } from './types.js'; import { GraphQLClient } from 'graphql-request'; @@ -34,3 +35,7 @@ export type IExecConsumer = { export type SubgraphConsumer = { graphQLClient: GraphQLClient; }; + +export type DataProtectorConsumer = { + dataProtector: IExecDataProtectorCore; +}; diff --git a/src/web3mail/prepareEmailCampaign.ts b/src/web3mail/prepareEmailCampaign.ts new file mode 100644 index 00000000..6f40d6cd --- /dev/null +++ b/src/web3mail/prepareEmailCampaign.ts @@ -0,0 +1,170 @@ +import { Buffer } from 'buffer'; +import { + DEFAULT_CONTENT_TYPE, + MAX_DESIRED_APP_ORDER_PRICE, + MAX_DESIRED_WORKERPOOL_ORDER_PRICE, +} from '../config/config.js'; +import { handleIfProtocolError, WorkflowError } from '../utils/errors.js'; +import * as ipfs from '../utils/ipfs-service.js'; +import { + addressOrEnsSchema, + contentTypeSchema, + emailContentSchema, + emailSubjectSchema, + labelSchema, + positiveNumberSchema, + senderNameSchema, + throwIfMissing, +} from '../utils/validators.js'; +import { + PrepareEmailCampaignParams, + PrepareEmailCampaignResponse, +} from './types.js'; +import { + DappAddressConsumer, + DataProtectorConsumer, + IExecConsumer, + IpfsGatewayConfigConsumer, + IpfsNodeConfigConsumer, +} from './internalTypes.js'; + +export type PrepareEmailCampaign = typeof prepareEmailCampaign; + +export const prepareEmailCampaign = async ({ + iexec = throwIfMissing(), + dataProtector = throwIfMissing(), + workerpoolAddressOrEns, + dappAddressOrENS, + ipfsNode, + ipfsGateway, + senderName, + emailSubject, + emailContent, + contentType = DEFAULT_CONTENT_TYPE, + label, + appMaxPrice = MAX_DESIRED_APP_ORDER_PRICE, + workerpoolMaxPrice = MAX_DESIRED_WORKERPOOL_ORDER_PRICE, + grantedAccesses, + maxProtectedDataPerTask, +}: IExecConsumer & + DappAddressConsumer & + IpfsNodeConfigConsumer & + IpfsGatewayConfigConsumer & + DataProtectorConsumer & + PrepareEmailCampaignParams): Promise => { + try { + const vWorkerpoolAddressOrEns = addressOrEnsSchema() + .label('WorkerpoolAddressOrEns') + .validateSync(workerpoolAddressOrEns); + + const vSenderName = senderNameSchema() + .label('senderName') + .validateSync(senderName); + + const vEmailSubject = emailSubjectSchema() + .required() + .label('emailSubject') + .validateSync(emailSubject); + + const vEmailContent = emailContentSchema() + .required() + .label('emailContent') + .validateSync(emailContent); + + const vContentType = contentTypeSchema() + .label('contentType') + .validateSync(contentType); + + const vLabel = labelSchema().label('label').validateSync(label); + + const vDappAddressOrENS = addressOrEnsSchema() + .required() + .label('dappAddressOrENS') + .validateSync(dappAddressOrENS); + + const vAppMaxPrice = positiveNumberSchema() + .label('appMaxPrice') + .validateSync(appMaxPrice); + + const vWorkerpoolMaxPrice = positiveNumberSchema() + .label('workerpoolMaxPrice') + .validateSync(workerpoolMaxPrice); + + const vMaxProtectedDataPerTask = positiveNumberSchema() + .label('maxProtectedDataPerTask') + .validateSync(maxProtectedDataPerTask); + + // TODO: factor this + // Encrypt email content + const emailContentEncryptionKey = iexec.dataset.generateEncryptionKey(); + const encryptedFile = await iexec.dataset + .encrypt(Buffer.from(vEmailContent, 'utf8'), emailContentEncryptionKey) + .catch((e) => { + throw new WorkflowError({ + message: 'Failed to encrypt email content', + errorCause: e, + }); + }); + + // Push email content to IPFS + const cid = await ipfs + .add(encryptedFile, { + ipfsNode, + ipfsGateway, + }) + .catch((e) => { + throw new WorkflowError({ + message: 'Failed to upload encrypted email content', + errorCause: e, + }); + }); + + const multiaddr = `/ipfs/${cid}`; + + // Prepare secrets for the requester + // Use a positive integer as secret ID (required by iexec) + // Using "1" as a fixed ID for the requester secret + const requesterSecretId = 1; + const secrets = { + [requesterSecretId]: JSON.stringify({ + emailSubject: vEmailSubject, + emailContentMultiAddr: multiaddr, + contentType: vContentType, + senderName: vSenderName, + emailContentEncryptionKey, + useCallback: true, + }), + }; + + // TODO: end factor this + const { bulkRequest: campaignRequest } = + await dataProtector.prepareBulkRequest({ + app: vDappAddressOrENS, + appMaxPrice: vAppMaxPrice, + workerpoolMaxPrice: vWorkerpoolMaxPrice, + workerpool: vWorkerpoolAddressOrEns, + args: vLabel, + inputFiles: [], + secrets, + bulkAccesses: grantedAccesses, + maxProtectedDataPerTask: vMaxProtectedDataPerTask, + }); + + return { campaignRequest }; + } catch (error) { + // Protocol error detected, re-throwing as-is + if ((error as any)?.isProtocolError === true) { + throw error; + } + + // Handle protocol errors - this will throw if it's an ApiCallError + // handleIfProtocolError transforms ApiCallError into a WorkflowError with isProtocolError=true + handleIfProtocolError(error); + + // For all other errors + throw new WorkflowError({ + message: 'Failed to prepareEmailCampaign', + errorCause: error, + }); + } +}; diff --git a/src/web3mail/sendEmail.ts b/src/web3mail/sendEmail.ts index 533b7978..8f08c487 100644 --- a/src/web3mail/sendEmail.ts +++ b/src/web3mail/sendEmail.ts @@ -67,43 +67,55 @@ export const sendEmail = async ({ .required() .label('protectedData') .validateSync(protectedData); + const vEmailSubject = emailSubjectSchema() .required() .label('emailSubject') .validateSync(emailSubject); + const vEmailContent = emailContentSchema() .required() .label('emailContent') .validateSync(emailContent); + const vContentType = contentTypeSchema() .required() .label('contentType') .validateSync(contentType); + const vSenderName = senderNameSchema() .label('senderName') .validateSync(senderName); + const vLabel = labelSchema().label('label').validateSync(label); + const vWorkerpoolAddressOrEns = addressOrEnsSchema() .required() .label('WorkerpoolAddressOrEns') .validateSync(workerpoolAddressOrEns); + const vDappAddressOrENS = addressOrEnsSchema() .required() .label('dappAddressOrENS') .validateSync(dappAddressOrENS); + const vDappWhitelistAddress = addressSchema() .required() .label('dappWhitelistAddress') .validateSync(dappWhitelistAddress); + const vDataMaxPrice = positiveNumberSchema() .label('dataMaxPrice') .validateSync(dataMaxPrice); + const vAppMaxPrice = positiveNumberSchema() .label('appMaxPrice') .validateSync(appMaxPrice); + const vWorkerpoolMaxPrice = positiveNumberSchema() .label('workerpoolMaxPrice') .validateSync(workerpoolMaxPrice); + const vUseVoucher = booleanSchema() .label('useVoucher') .validateSync(useVoucher); @@ -113,6 +125,7 @@ export const sendEmail = async ({ graphQLClient, vDatasetAddress ); + if (!isValidProtectedData) { throw new Error( 'This protected data does not contain "email:string" in its schema.' @@ -145,7 +158,8 @@ export const sendEmail = async ({ ] = await Promise.all([ // Fetch dataset order for web3mail app iexec.orderbook - .fetchDatasetOrderbook(vDatasetAddress, { + .fetchDatasetOrderbook({ + dataset: vDatasetAddress, app: dappAddressOrENS, requester: requesterAddress, }) @@ -155,9 +169,11 @@ export const sendEmail = async ({ ); return desiredPriceDataOrderbook[0]?.order; // may be undefined }), + // Fetch dataset order for web3mail whitelist iexec.orderbook - .fetchDatasetOrderbook(vDatasetAddress, { + .fetchDatasetOrderbook({ + dataset: vDatasetAddress, app: vDappWhitelistAddress, requester: requesterAddress, }) @@ -167,9 +183,11 @@ export const sendEmail = async ({ ); return desiredPriceDataOrderbook[0]?.order; // may be undefined }), + // Fetch app order iexec.orderbook - .fetchAppOrderbook(dappAddressOrENS, { + .fetchAppOrderbook({ + app: dappAddressOrENS, minTag: ['tee', 'scone'], maxTag: ['tee', 'scone'], workerpool: workerpoolAddressOrEns, @@ -184,6 +202,7 @@ export const sendEmail = async ({ } return desiredPriceAppOrder; }), + // Fetch workerpool order for App or AppWhitelist Promise.all([ // for app @@ -219,9 +238,11 @@ export const sendEmail = async ({ useVoucher: vUseVoucher, userVoucher, }); + if (!desiredPriceWorkerpoolOrder) { throw new Error('No Workerpool order found for the desired price'); } + return desiredPriceWorkerpoolOrder; } ), @@ -247,6 +268,7 @@ export const sendEmail = async ({ errorCause: e, }); }); + const cid = await ipfs .add(encryptedFile, { ipfsNode: ipfsNode, @@ -258,6 +280,7 @@ export const sendEmail = async ({ errorCause: e, }); }); + const multiaddr = `/ipfs/${cid}`; await iexec.secrets.pushRequesterSecret( @@ -289,10 +312,11 @@ export const sendEmail = async ({ iexec_args: vLabel, }, }); + const requestorder = await iexec.order.signRequestorder(requestorderToSign); // Match orders and compute task ID - const { dealid } = await iexec.order.matchOrders( + const { dealid: dealId } = await iexec.order.matchOrders( { apporder: apporder, datasetorder: datasetorder, @@ -301,10 +325,12 @@ export const sendEmail = async ({ }, { useVoucher: vUseVoucher } ); - const taskId = await iexec.deal.computeTaskId(dealid, 0); + + const taskId = await iexec.deal.computeTaskId(dealId, 0); return { taskId, + dealId, }; } catch (error) { handleIfProtocolError(error); diff --git a/src/web3mail/sendEmailCampaign.ts b/src/web3mail/sendEmailCampaign.ts new file mode 100644 index 00000000..15f4b37d --- /dev/null +++ b/src/web3mail/sendEmailCampaign.ts @@ -0,0 +1,69 @@ +import { NULL_ADDRESS } from 'iexec/utils'; +import { ValidationError } from 'yup'; +import { handleIfProtocolError, WorkflowError } from '../utils/errors.js'; +import { + addressOrEnsSchema, + campaignRequestSchema, + throwIfMissing, +} from '../utils/validators.js'; +import { + CampaignRequest, + SendEmailCampaignParams, + SendEmailCampaignResponse, +} from './types.js'; +import { DataProtectorConsumer } from './internalTypes.js'; + +export type SendEmailCampaign = typeof sendEmailCampaign; + +export const sendEmailCampaign = async ({ + dataProtector = throwIfMissing(), + workerpoolAddressOrEns = throwIfMissing(), + campaignRequest, +}: DataProtectorConsumer & + SendEmailCampaignParams): Promise => { + const vCampaignRequest = campaignRequestSchema() + .required() + .label('campaignRequest') + .validateSync(campaignRequest) as CampaignRequest; + + const vWorkerpoolAddressOrEns = addressOrEnsSchema() + .required() + .label('workerpoolAddressOrEns') + .validateSync(workerpoolAddressOrEns); + + if ( + vCampaignRequest.workerpool !== NULL_ADDRESS && + vCampaignRequest.workerpool.toLowerCase() !== + vWorkerpoolAddressOrEns.toLowerCase() + ) { + throw new ValidationError( + "workerpoolAddressOrEns doesn't match campaignRequest workerpool" + ); + } + + try { + // Process the prepared bulk request + const processBulkRequestResponse = await dataProtector.processBulkRequest({ + bulkRequest: vCampaignRequest, + workerpool: vWorkerpoolAddressOrEns, + waitForResult: false, + }); + + return processBulkRequestResponse; + } catch (error) { + // Protocol error detected, re-throwing as-is + if ((error as any)?.isProtocolError === true) { + throw error; + } + + // Handle protocol errors - this will throw if it's an ApiCallError + // handleIfProtocolError transforms ApiCallError into a WorkflowError with isProtocolError=true + handleIfProtocolError(error); + + // For all other errors + throw new WorkflowError({ + message: 'Failed to sendEmailCampaign', + errorCause: error, + }); + } +}; diff --git a/src/web3mail/types.ts b/src/web3mail/types.ts index b5c97337..18cee01b 100644 --- a/src/web3mail/types.ts +++ b/src/web3mail/types.ts @@ -1,5 +1,6 @@ import { EnhancedWallet } from 'iexec'; import { IExecConfigOptions } from 'iexec/IExecConfig'; +import type { BulkRequest } from '@iexec/dataprotector'; export type Web3SignerProvider = EnhancedWallet; @@ -11,14 +12,44 @@ export type Address = string; export type TimeStamp = string; +/** + * request to send email in bulk + * + * use `prepareEmailCampaign()` to create a `CampaignRequest` + * + * then use `sendEmailCampaign()` to send the campaign + */ +export type CampaignRequest = BulkRequest; + +/** + * authorization signed by the data owner granting access to this contact + * + * `GrantedAccess` are obtained by fetching contacts (e.g. `fetchMyContacts()` or `fetchUserContacts()`) + * + * `GrantedAccess` can be consumed for email campaigns (e.g. `prepareEmailCampaign()` then `sendEmailCampaign()`) + */ +export type GrantedAccess = { + dataset: string; + datasetprice: string; + volume: string; + tag: string; + apprestrict: string; + workerpoolrestrict: string; + requesterrestrict: string; + salt: string; + sign: string; + remainingAccess: number; +}; + export type Contact = { address: Address; owner: Address; accessGrantTimestamp: TimeStamp; isUserStrict: boolean; - name: string; + name?: string; remainingAccess: number; accessPrice: number; + grantedAccess: GrantedAccess; }; export type SendEmailParams = { @@ -40,6 +71,10 @@ export type FetchMyContactsParams = { * Get contacts for this specific user only */ isUserStrict?: boolean; + /** + * If true, returns only contacts with bulk processing access grants + */ + bulkOnly?: boolean; }; export type FetchUserContactsParams = { @@ -50,7 +85,14 @@ export type FetchUserContactsParams = { } & FetchMyContactsParams; export type SendEmailResponse = { + /** + * ID of the task + */ taskId: string; + /** + * ID of the deal containing the task + */ + dealId: string; }; /** @@ -100,3 +142,62 @@ export type Web3MailConfigOptions = { */ allowExperimentalNetworks?: boolean; }; + +export type PrepareEmailCampaignParams = { + /** + * List of `GrantedAccess` to contacts to send emails to in bulk. + * + * use `fetchMyContacts({ bulkOnly: true })` to get granted accesses. + */ + grantedAccesses: GrantedAccess[]; + maxProtectedDataPerTask?: number; + senderName?: string; + emailSubject: string; + emailContent: string; + contentType?: string; + label?: string; + workerpoolAddressOrEns?: AddressOrENS; + dataMaxPrice?: number; + appMaxPrice?: number; + workerpoolMaxPrice?: number; +}; + +export type PrepareEmailCampaignResponse = { + /** + * The prepared campaign request + * + * Use this in `sendEmailCampaign()` to start or continue sending the campaign + */ + campaignRequest: CampaignRequest; +}; + +export type SendEmailCampaignParams = { + /** + * The prepared campaign request from `prepareEmailCampaign()` + */ + campaignRequest: CampaignRequest; + /** + * Workerpool address or ENS to use for processing + */ + workerpoolAddressOrEns?: AddressOrENS; +}; + +export type SendEmailCampaignResponse = { + /** + * List of tasks created for the campaign + */ + tasks: Array<{ + /** + * ID of the task + */ + taskId: string; + /** + * ID of the deal containing the task + */ + dealId: string; + /** + * Index of the task in the bulk request + */ + bulkIndex: number; + }>; +}; diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index e0b2fca8..fd6faec6 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -98,7 +98,7 @@ services: - 6379:6379 market-watcher: - image: iexechub/iexec-market-watcher:6.4 + image: iexechub/iexec-market-watcher:7.0.0 restart: unless-stopped environment: CHAIN: BELLECOUR @@ -116,7 +116,7 @@ services: condition: service_started market-api: - image: iexechub/iexec-market-api:6.4 + image: iexechub/iexec-market-api:7.1.0 restart: unless-stopped ports: - 3000:3000 diff --git a/tests/e2e/constructor.test.ts b/tests/e2e/constructor.test.ts index 06d89323..25f34b67 100644 --- a/tests/e2e/constructor.test.ts +++ b/tests/e2e/constructor.test.ts @@ -126,9 +126,17 @@ describe('IExecWeb3mail()', () => { const wallet = Wallet.createRandom(); // --- WHEN/THEN - await expect( - web3mail.fetchUserContacts({ userAddress: wallet.address }) - ).resolves.not.toThrow(); + // fetchUserContacts should work without throwing + // With isUserStrict: true, a random wallet with no grants should return empty array + // With isUserStrict: false (default), it may return public orders + const contacts = await web3mail.fetchUserContacts({ + userAddress: wallet.address, + isUserStrict: true, // Only get orders specific to this user + }); + expect(contacts).toBeDefined(); + expect(Array.isArray(contacts)).toBe(true); + // For a random wallet with no specific grants, we expect an empty array + expect(contacts.length).toBe(0); }, MAX_EXPECTED_WEB2_SERVICES_TIME ); @@ -146,7 +154,7 @@ describe('IExecWeb3mail()', () => { it('should throw a configuration error', async () => { const web3mail = new IExecWeb3mail(experimentalNetworkSigner); await expect(web3mail.init()).rejects.toThrow( - 'Missing required configuration for chainId 421614: dataProtectorSubgraph, dappAddress, whitelistSmartContract, ipfsGateway, prodWorkerpoolAddress, ipfsUploadUrl' + 'Missing required configuration for chainId 421614: dataProtectorSubgraph, whitelistSmartContract, ipfsGateway, prodWorkerpoolAddress, ipfsUploadUrl' ); }); }); diff --git a/tests/e2e/fetchMyContacts.test.ts b/tests/e2e/fetchMyContacts.test.ts index b84af59a..ecbc8579 100644 --- a/tests/e2e/fetchMyContacts.test.ts +++ b/tests/e2e/fetchMyContacts.test.ts @@ -8,6 +8,7 @@ import { NULL_ADDRESS } from 'iexec/utils'; import { IExecWeb3mail } from '../../src/index.js'; import { MAX_EXPECTED_BLOCKTIME, + MAX_EXPECTED_SUBGRAPH_INDEXING_TIME, MAX_EXPECTED_WEB2_SERVICES_TIME, deployRandomDataset, getTestConfig, @@ -144,4 +145,112 @@ describe('web3mail.fetchMyContacts()', () => { }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME ); + + describe('bulkOnly parameter', () => { + let protectedDataWithBulk: any; + let protectedDataWithoutBulk: any; + const defaultConfig = getChainDefaultConfig(DEFAULT_CHAIN_ID); + + beforeAll(async () => { + protectedDataWithBulk = await dataProtector.protectData({ + data: { email: 'bulk@test.com' }, + name: 'test bulk access', + }); + protectedDataWithoutBulk = await dataProtector.protectData({ + data: { email: 'nobulk@test.com' }, + name: 'test no bulk access', + }); + await waitSubgraphIndexing(); + }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME); + + it( + 'should return only contacts with bulk access when bulkOnly is true', + async () => { + // Grant access with allowBulk: true + await dataProtector.grantAccess({ + authorizedApp: defaultConfig.dappAddress, + protectedData: protectedDataWithBulk.address, + authorizedUser: wallet.address, + allowBulk: true, + }); + + // Grant access with allowBulk: false (or default) + await dataProtector.grantAccess({ + authorizedApp: defaultConfig.dappAddress, + protectedData: protectedDataWithoutBulk.address, + authorizedUser: wallet.address, + allowBulk: false, + }); + + await waitSubgraphIndexing(); + + // Fetch contacts with bulkOnly: true + const contactsWithBulkOnly = await web3mail.fetchMyContacts({ + bulkOnly: true, + }); + + // Should only include the contact with bulk access + const bulkContact = contactsWithBulkOnly.find( + (contact) => + contact.address === protectedDataWithBulk.address.toLowerCase() + ); + const noBulkContact = contactsWithBulkOnly.find( + (contact) => + contact.address === protectedDataWithoutBulk.address.toLowerCase() + ); + + expect(bulkContact).toBeDefined(); + expect(noBulkContact).toBeUndefined(); + }, + MAX_EXPECTED_BLOCKTIME + + MAX_EXPECTED_SUBGRAPH_INDEXING_TIME + + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + + it( + 'should return all contacts when bulkOnly is false', + async () => { + // Fetch contacts with bulkOnly: false (default) + const contactsWithoutBulkOnly = await web3mail.fetchMyContacts({ + bulkOnly: false, + }); + + // Should include both contacts + const bulkContact = contactsWithoutBulkOnly.find( + (contact) => + contact.address === protectedDataWithBulk.address.toLowerCase() + ); + const noBulkContact = contactsWithoutBulkOnly.find( + (contact) => + contact.address === protectedDataWithoutBulk.address.toLowerCase() + ); + + expect(bulkContact).toBeDefined(); + expect(noBulkContact).toBeDefined(); + }, + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + + it( + 'should return all contacts when bulkOnly is not specified (default)', + async () => { + // Fetch contacts without specifying bulkOnly (defaults to false) + const contactsDefault = await web3mail.fetchMyContacts(); + + // Should include both contacts + const bulkContact = contactsDefault.find( + (contact) => + contact.address === protectedDataWithBulk.address.toLowerCase() + ); + const noBulkContact = contactsDefault.find( + (contact) => + contact.address === protectedDataWithoutBulk.address.toLowerCase() + ); + + expect(bulkContact).toBeDefined(); + expect(noBulkContact).toBeDefined(); + }, + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + }); }); diff --git a/tests/e2e/fetchUserContacts.test.ts b/tests/e2e/fetchUserContacts.test.ts index cde8217e..e3c9a94c 100644 --- a/tests/e2e/fetchUserContacts.test.ts +++ b/tests/e2e/fetchUserContacts.test.ts @@ -11,6 +11,7 @@ import { import { IExecWeb3mail, WorkflowError } from '../../src/index.js'; import { MAX_EXPECTED_BLOCKTIME, + MAX_EXPECTED_SUBGRAPH_INDEXING_TIME, MAX_EXPECTED_WEB2_SERVICES_TIME, getTestConfig, waitSubgraphIndexing, @@ -247,4 +248,120 @@ describe('web3mail.fetchMyContacts()', () => { 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME ); }); + + describe('bulkOnly parameter', () => { + let protectedDataWithBulk: ProtectedDataWithSecretProps; + let protectedDataWithoutBulk: ProtectedDataWithSecretProps; + let userWithAccess: string; + + beforeAll(async () => { + userWithAccess = Wallet.createRandom().address; + protectedDataWithBulk = await dataProtector.protectData({ + data: { email: 'bulk@test.com' }, + name: 'test bulk access user', + }); + protectedDataWithoutBulk = await dataProtector.protectData({ + data: { email: 'nobulk@test.com' }, + name: 'test no bulk access user', + }); + await waitSubgraphIndexing(); + }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME); + + it( + 'should return only contacts with bulk access when bulkOnly is true', + async () => { + const defaultConfig = getChainDefaultConfig(DEFAULT_CHAIN_ID); + expect(defaultConfig).not.toBeNull(); + + // Grant access with allowBulk: true + await dataProtector.grantAccess({ + authorizedApp: defaultConfig!.dappAddress, + protectedData: protectedDataWithBulk.address, + authorizedUser: userWithAccess, + allowBulk: true, + }); + + // Grant access with allowBulk: false (or default) + await dataProtector.grantAccess({ + authorizedApp: defaultConfig!.dappAddress, + protectedData: protectedDataWithoutBulk.address, + authorizedUser: userWithAccess, + allowBulk: false, + }); + + await waitSubgraphIndexing(); + + // Fetch contacts with bulkOnly: true + const contactsWithBulkOnly = await web3mail.fetchUserContacts({ + userAddress: userWithAccess, + bulkOnly: true, + }); + + // Should only include the contact with bulk access + const bulkContact = contactsWithBulkOnly.find( + (contact) => + contact.address === protectedDataWithBulk.address.toLowerCase() + ); + const noBulkContact = contactsWithBulkOnly.find( + (contact) => + contact.address === protectedDataWithoutBulk.address.toLowerCase() + ); + + expect(bulkContact).toBeDefined(); + expect(noBulkContact).toBeUndefined(); + }, + MAX_EXPECTED_BLOCKTIME + + MAX_EXPECTED_SUBGRAPH_INDEXING_TIME + + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + + it( + 'should return all contacts when bulkOnly is false', + async () => { + // Fetch contacts with bulkOnly: false + const contactsWithoutBulkOnly = await web3mail.fetchUserContacts({ + userAddress: userWithAccess, + bulkOnly: false, + }); + + // Should include both contacts + const bulkContact = contactsWithoutBulkOnly.find( + (contact) => + contact.address === protectedDataWithBulk.address.toLowerCase() + ); + const noBulkContact = contactsWithoutBulkOnly.find( + (contact) => + contact.address === protectedDataWithoutBulk.address.toLowerCase() + ); + + expect(bulkContact).toBeDefined(); + expect(noBulkContact).toBeDefined(); + }, + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + + it( + 'should return all contacts when bulkOnly is not specified (default)', + async () => { + // Fetch contacts without specifying bulkOnly (defaults to false) + const contactsDefault = await web3mail.fetchUserContacts({ + userAddress: userWithAccess, + }); + + // Should include both contacts + const bulkContact = contactsDefault.find( + (contact) => + contact.address === protectedDataWithBulk.address.toLowerCase() + ); + const noBulkContact = contactsDefault.find( + (contact) => + contact.address === protectedDataWithoutBulk.address.toLowerCase() + ); + + expect(bulkContact).toBeDefined(); + expect(noBulkContact).toBeDefined(); + }, + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + }); }); diff --git a/tests/e2e/prepareEmailCampaign.test.ts b/tests/e2e/prepareEmailCampaign.test.ts new file mode 100644 index 00000000..0d4aec28 --- /dev/null +++ b/tests/e2e/prepareEmailCampaign.test.ts @@ -0,0 +1,147 @@ +import { + IExecDataProtectorCore, + ProtectedDataWithSecretProps, +} from '@iexec/dataprotector'; +import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; +import { HDNodeWallet } from 'ethers'; +import { + DEFAULT_CHAIN_ID, + getChainDefaultConfig, +} from '../../src/config/config.js'; +import { Contact, IExecWeb3mail } from '../../src/index.js'; +import { + MAX_EXPECTED_BLOCKTIME, + MAX_EXPECTED_WEB2_SERVICES_TIME, + MAX_EXPECTED_SUBGRAPH_INDEXING_TIME, + TEST_CHAIN, + createAndPublishAppOrders, + getRandomWallet, + getTestConfig, + getTestIExecOption, + getTestWeb3SignerProvider, + waitSubgraphIndexing, +} from '../test-utils.js'; +import { IExec } from 'iexec'; + +describe('web3mail.prepareEmailCampaign()', () => { + let consumerWallet: HDNodeWallet; + let providerWallet: HDNodeWallet; + let web3mail: IExecWeb3mail; + let dataProtector: IExecDataProtectorCore; + let validProtectedData1: ProtectedDataWithSecretProps; + let validProtectedData2: ProtectedDataWithSecretProps; + let validProtectedData3: ProtectedDataWithSecretProps; + const iexecOptions = getTestIExecOption(); + const prodWorkerpoolPublicPrice = 1000; + const defaultConfig = getChainDefaultConfig(DEFAULT_CHAIN_ID); + + beforeAll(async () => { + // Create app orders + providerWallet = getRandomWallet(); + const ethProvider = getTestWeb3SignerProvider( + TEST_CHAIN.appOwnerWallet.privateKey + ); + const resourceProvider = new IExec({ ethProvider }, iexecOptions); + await createAndPublishAppOrders( + resourceProvider, + defaultConfig!.dappAddress + ); + + dataProtector = new IExecDataProtectorCore( + ...getTestConfig(providerWallet.privateKey) + ); + + // create valid protected data + validProtectedData1 = await dataProtector.protectData({ + data: { email: 'user1@example.com' }, + name: 'bulk test 1', + }); + + validProtectedData2 = await dataProtector.protectData({ + data: { email: 'user2@example.com' }, + name: 'bulk test 2', + }); + + validProtectedData3 = await dataProtector.protectData({ + data: { email: 'user3@example.com' }, + name: 'bulk test 3', + }); + + await waitSubgraphIndexing(); + }, 5 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + 5_000); + + beforeEach(async () => { + consumerWallet = getRandomWallet(); + + // Grant access with allowBulk for bulk processing + await dataProtector.grantAccess({ + authorizedApp: defaultConfig.dappAddress, + protectedData: validProtectedData1.address, + authorizedUser: consumerWallet.address, + allowBulk: true, + }); + + await dataProtector.grantAccess({ + authorizedApp: defaultConfig.dappAddress, + protectedData: validProtectedData2.address, + authorizedUser: consumerWallet.address, + allowBulk: true, + }); + + await dataProtector.grantAccess({ + authorizedApp: defaultConfig.dappAddress, + protectedData: validProtectedData3.address, + authorizedUser: consumerWallet.address, + allowBulk: true, + }); + + await waitSubgraphIndexing(); + + web3mail = new IExecWeb3mail(...getTestConfig(consumerWallet.privateKey)); + }, 3 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_SUBGRAPH_INDEXING_TIME + 10_000); + + it( + 'should prepare an email campaignRequest', + async () => { + // Fetch contacts with allowBulk access + const contacts: Contact[] = await web3mail.fetchMyContacts({ + bulkOnly: true, + }); + expect(contacts.length).toBeGreaterThanOrEqual(3); + + const bulkOrders = contacts.map((contact) => contact.grantedAccess); + + // Process the bulk request + const result = await web3mail.prepareEmailCampaign({ + emailSubject: 'Bulk test subject', + emailContent: 'Bulk test message', + senderName: 'Bulk test sender', + grantedAccesses: bulkOrders, + maxProtectedDataPerTask: 3, + workerpoolMaxPrice: prodWorkerpoolPublicPrice, + }); + + // Verify the result + expect(result).toBeDefined(); + expect(result.campaignRequest).toEqual({ + app: expect.any(String), + appmaxprice: expect.any(String), + workerpool: expect.any(String), + workerpoolmaxprice: expect.any(String), + dataset: '0x0000000000000000000000000000000000000000', + datasetmaxprice: '0', + callback: '0x0000000000000000000000000000000000000000', + params: expect.any(String), + beneficiary: consumerWallet.address, + category: '0', + requester: consumerWallet.address, + salt: expect.any(String), + sign: expect.any(String), + tag: '0x0000000000000000000000000000000000000000000000000000000000000003', + trust: '0', + volume: '1', + }); + }, + 30 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + 60_000 + ); +}); diff --git a/tests/e2e/sendEmail.test.ts b/tests/e2e/sendEmail.test.ts index ab09fcdf..cdd6d8bc 100644 --- a/tests/e2e/sendEmail.test.ts +++ b/tests/e2e/sendEmail.test.ts @@ -171,7 +171,10 @@ describe('web3mail.sendEmail()', () => { protectedData: validProtectedData.address, workerpoolMaxPrice: prodWorkerpoolPublicPrice, }); - expect(sendEmailResponse.taskId).toBeDefined(); + expect(sendEmailResponse).toStrictEqual({ + taskId: expect.any(String), + dealId: expect.any(String), + }); }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME ); @@ -273,7 +276,10 @@ describe('web3mail.sendEmail()', () => { protectedData: validProtectedData.address, workerpoolAddressOrEns: learnProdWorkerpoolAddress, }); - expect(sendEmailResponse.taskId).toBeDefined(); + expect(sendEmailResponse).toStrictEqual({ + taskId: expect.any(String), + dealId: expect.any(String), + }); }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME ); @@ -303,7 +309,10 @@ describe('web3mail.sendEmail()', () => { protectedData: protectedDataForWhitelist.address, workerpoolAddressOrEns: learnProdWorkerpoolAddress, }); - expect(sendEmailResponse.taskId).toBeDefined(); + expect(sendEmailResponse).toStrictEqual({ + taskId: expect.any(String), + dealId: expect.any(String), + }); }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME ); @@ -319,7 +328,10 @@ describe('web3mail.sendEmail()', () => { contentType: 'text/html', workerpoolAddressOrEns: learnProdWorkerpoolAddress, }); - expect(sendEmailResponse.taskId).toBeDefined(); + expect(sendEmailResponse).toStrictEqual({ + taskId: expect.any(String), + dealId: expect.any(String), + }); }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME ); @@ -334,7 +346,10 @@ describe('web3mail.sendEmail()', () => { senderName: 'Product Team', workerpoolAddressOrEns: learnProdWorkerpoolAddress, }); - expect(sendEmailResponse.taskId).toBeDefined(); + expect(sendEmailResponse).toStrictEqual({ + taskId: expect.any(String), + dealId: expect.any(String), + }); }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME ); @@ -353,7 +368,10 @@ describe('web3mail.sendEmail()', () => { senderName: 'Product Team', workerpoolAddressOrEns: learnProdWorkerpoolAddress, }); - expect(sendEmailResponse.taskId).toBeDefined(); + expect(sendEmailResponse).toStrictEqual({ + taskId: expect.any(String), + dealId: expect.any(String), + }); }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME ); @@ -368,7 +386,10 @@ describe('web3mail.sendEmail()', () => { workerpoolAddressOrEns: learnProdWorkerpoolAddress, label: 'ID1234678', }); - expect(sendEmailResponse.taskId).toBeDefined(); + expect(sendEmailResponse).toStrictEqual({ + taskId: expect.any(String), + dealId: expect.any(String), + }); // TODO check label in created deal }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME @@ -457,7 +478,10 @@ describe('web3mail.sendEmail()', () => { // workerpoolAddressOrEns: prodWorkerpoolAddress, // default useVoucher: true, }); - expect(sendEmailResponse.taskId).toBeDefined(); + expect(sendEmailResponse).toStrictEqual({ + taskId: expect.any(String), + dealId: expect.any(String), + }); }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + @@ -567,7 +591,10 @@ describe('web3mail.sendEmail()', () => { workerpoolMaxPrice: nonSponsoredAmount, useVoucher: true, }); - expect(sendEmailResponse.taskId).toBeDefined(); + expect(sendEmailResponse).toStrictEqual({ + taskId: expect.any(String), + dealId: expect.any(String), + }); }, 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + diff --git a/tests/e2e/sendEmailCampaign.test.ts b/tests/e2e/sendEmailCampaign.test.ts new file mode 100644 index 00000000..d112c706 --- /dev/null +++ b/tests/e2e/sendEmailCampaign.test.ts @@ -0,0 +1,497 @@ +import { + IExecDataProtectorCore, + ProtectedDataWithSecretProps, +} from '@iexec/dataprotector'; +import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; +import { HDNodeWallet } from 'ethers'; +import { ValidationError } from 'yup'; +import { + DEFAULT_CHAIN_ID, + getChainDefaultConfig, +} from '../../src/config/config.js'; +import { + Contact, + IExecWeb3mail, + WorkflowError as Web3mailWorkflowError, +} from '../../src/index.js'; +import { + MAX_EXPECTED_BLOCKTIME, + MAX_EXPECTED_SUBGRAPH_INDEXING_TIME, + MAX_EXPECTED_WEB2_SERVICES_TIME, + TEST_CHAIN, + createAndPublishAppOrders, + createAndPublishWorkerpoolOrder, + ensureSufficientStake, + getRandomWallet, + getTestConfig, + getTestIExecOption, + getTestWeb3SignerProvider, + waitSubgraphIndexing, +} from '../test-utils.js'; +import { IExec } from 'iexec'; +import { NULL_ADDRESS } from 'iexec/utils'; + +describe('web3mail.sendEmailCampaign()', () => { + let consumerWallet: HDNodeWallet; + let providerWallet: HDNodeWallet; + let web3mail: IExecWeb3mail; + let dataProtector: IExecDataProtectorCore; + let validProtectedData1: ProtectedDataWithSecretProps; + let validProtectedData2: ProtectedDataWithSecretProps; + let validProtectedData3: ProtectedDataWithSecretProps; + let consumerIExecInstance: IExec; + let learnProdWorkerpoolAddress: string; + let prodWorkerpoolAddress: string; + const iexecOptions = getTestIExecOption(); + const prodWorkerpoolPublicPrice = 1000; + const defaultConfig = getChainDefaultConfig(DEFAULT_CHAIN_ID); + + beforeAll(async () => { + // (default) prod workerpool (not free) always available + await createAndPublishWorkerpoolOrder( + TEST_CHAIN.prodWorkerpool, + TEST_CHAIN.prodWorkerpoolOwnerWallet, + NULL_ADDRESS, + 1_000, + prodWorkerpoolPublicPrice + ); + // learn prod pool (free) assumed always available + await createAndPublishWorkerpoolOrder( + TEST_CHAIN.learnProdWorkerpool, + TEST_CHAIN.learnProdWorkerpoolOwnerWallet, + NULL_ADDRESS, + 0, + 10_000 + ); + // apporder always available + providerWallet = getRandomWallet(); + const ethProvider = getTestWeb3SignerProvider( + TEST_CHAIN.appOwnerWallet.privateKey + ); + const resourceProvider = new IExec({ ethProvider }, iexecOptions); + await createAndPublishAppOrders( + resourceProvider, + defaultConfig!.dappAddress + ); + + learnProdWorkerpoolAddress = await resourceProvider.ens.resolveName( + TEST_CHAIN.learnProdWorkerpool + ); + prodWorkerpoolAddress = await resourceProvider.ens.resolveName( + TEST_CHAIN.prodWorkerpool + ); + + // Create valid protected data + dataProtector = new IExecDataProtectorCore( + ...getTestConfig(providerWallet.privateKey) + ); + + validProtectedData1 = await dataProtector.protectData({ + data: { email: 'user1@example.com' }, + name: 'bulk test 1', + }); + + validProtectedData2 = await dataProtector.protectData({ + data: { email: 'user2@example.com' }, + name: 'bulk test 2', + }); + + validProtectedData3 = await dataProtector.protectData({ + data: { email: 'user3@example.com' }, + name: 'bulk test 3', + }); + + await waitSubgraphIndexing(); + }, 5 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + 5_000); + + beforeEach(async () => { + consumerWallet = getRandomWallet(); + const consumerEthProvider = getTestWeb3SignerProvider( + consumerWallet.privateKey + ); + consumerIExecInstance = new IExec( + { ethProvider: consumerEthProvider }, + iexecOptions + ); + + // Grant access with allowBulk for bulk processing + await dataProtector.grantAccess({ + authorizedApp: defaultConfig.dappAddress, + protectedData: validProtectedData1.address, + authorizedUser: consumerWallet.address, + allowBulk: true, + }); + + await dataProtector.grantAccess({ + authorizedApp: defaultConfig.dappAddress, + protectedData: validProtectedData2.address, + authorizedUser: consumerWallet.address, + allowBulk: true, + }); + + await dataProtector.grantAccess({ + authorizedApp: defaultConfig.dappAddress, + protectedData: validProtectedData3.address, + authorizedUser: consumerWallet.address, + allowBulk: true, + }); + + await waitSubgraphIndexing(); + + web3mail = new IExecWeb3mail(...getTestConfig(consumerWallet.privateKey)); + }, MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_SUBGRAPH_INDEXING_TIME); + + describe('when using the default (not free) prod workerpool', () => { + it( + 'should throw an error if the user cannot pay with its account', + async () => { + // Prepare campaign first + const contacts: Contact[] = await web3mail.fetchMyContacts({ + bulkOnly: true, + }); + expect(contacts.length).toBeGreaterThanOrEqual(3); + + const bulkOrders = contacts.map((contact) => contact.grantedAccess); + + const campaignRequest = await web3mail.prepareEmailCampaign({ + emailSubject: 'Bulk test subject', + emailContent: 'Bulk test message', + senderName: 'Bulk test sender', + grantedAccesses: bulkOrders, + maxProtectedDataPerTask: 3, + workerpoolMaxPrice: prodWorkerpoolPublicPrice, + }); + + // Try to send campaign without sufficient stake + const workerpoolToUse = campaignRequest.campaignRequest.workerpool; + let error: Error; + await web3mail + .sendEmailCampaign({ + campaignRequest: campaignRequest.campaignRequest, + workerpoolAddressOrEns: workerpoolToUse, + }) + .catch((e) => (error = e)); + + expect(error).toBeDefined(); + // The error can be ValidationError (from workerpool mismatch) or WorkflowError (from processing) + const isWorkflowError = error instanceof Web3mailWorkflowError; + expect(isWorkflowError).toBe(true); + // Check message only if it's a WorkflowError + expect(error.message === 'Failed to sendEmailCampaign').toBe(true); + // The error cause should indicate insufficient funds or order matching issues + // Error can be nested: error.cause might be a WorkflowError with error.cause.cause being the actual Error + const getNestedErrorMessage = (err: any, depth = 0): string => { + if (depth > 5) return ''; // Prevent infinite recursion + if (err instanceof Error) { + const message = err.message || ''; + // If this error has a cause, try to get message from cause too + if (err.cause && depth < 3) { + const causeMessage = getNestedErrorMessage(err.cause, depth + 1); + return causeMessage || message; + } + return message; + } + if (err?.cause) { + return getNestedErrorMessage(err.cause, depth + 1); + } + return String(err || ''); + }; + const errorMessage = + getNestedErrorMessage(error) || error.message || ''; + expect( + errorMessage.includes('stake') || + errorMessage.includes('Cost per task') || + errorMessage.includes('balance') || + errorMessage.includes("Orders can't be matched") || + errorMessage.includes('insufficient') || + errorMessage.includes('matched') + ).toBe(true); + }, + 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + + it( + 'should successfully send email campaign when the user can pay with its account', + async () => { + await ensureSufficientStake( + consumerIExecInstance, + prodWorkerpoolPublicPrice + ); + + // Prepare campaign first + const contacts: Contact[] = await web3mail.fetchMyContacts({ + bulkOnly: true, + }); + expect(contacts.length).toBeGreaterThanOrEqual(3); + + const bulkOrders = contacts.map((contact) => contact.grantedAccess); + + const campaignRequest = await web3mail.prepareEmailCampaign({ + emailSubject: 'Bulk test subject', + emailContent: 'Bulk test message', + senderName: 'Bulk test sender', + grantedAccesses: bulkOrders, + maxProtectedDataPerTask: 3, + workerpoolMaxPrice: prodWorkerpoolPublicPrice, + workerpoolAddressOrEns: prodWorkerpoolAddress, + }); + + // Send campaign + const result = await web3mail.sendEmailCampaign({ + campaignRequest: campaignRequest.campaignRequest, + workerpoolAddressOrEns: prodWorkerpoolAddress, + }); + + // Verify the result + expect(result).toBeDefined(); + expect('tasks' in result).toBe(true); + expect(result.tasks).toBeDefined(); + expect(Array.isArray(result.tasks)).toBe(true); + expect(result.tasks.length).toBeGreaterThan(0); + result.tasks.forEach((task) => { + expect(task.taskId).toBeDefined(); + expect(task.dealId).toBeDefined(); + expect(task.bulkIndex).toBeDefined(); + }); + }, + 30 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + 60_000 + ); + }); + + describe('when using a free prod workerpool', () => { + it( + 'should successfully send email campaign', + async () => { + // Prepare campaign first + const contacts: Contact[] = await web3mail.fetchMyContacts({ + bulkOnly: true, + }); + expect(contacts.length).toBeGreaterThanOrEqual(3); + + const bulkOrders = contacts.map((contact) => contact.grantedAccess); + + const campaignRequest = await web3mail.prepareEmailCampaign({ + emailSubject: 'Bulk test subject', + emailContent: 'Bulk test message', + senderName: 'Bulk test sender', + grantedAccesses: bulkOrders, + maxProtectedDataPerTask: 3, + workerpoolAddressOrEns: learnProdWorkerpoolAddress, + }); + + // Send campaign + const result = await web3mail.sendEmailCampaign({ + campaignRequest: campaignRequest.campaignRequest, + workerpoolAddressOrEns: learnProdWorkerpoolAddress, + }); + + // Verify the result + expect(result).toBeDefined(); + expect('tasks' in result).toBe(true); + expect(result.tasks).toBeDefined(); + expect(Array.isArray(result.tasks)).toBe(true); + expect(result.tasks.length).toBeGreaterThan(0); + result.tasks.forEach((task) => { + expect(task.taskId).toBeDefined(); + expect(task.dealId).toBeDefined(); + expect(task.bulkIndex).toBeDefined(); + }); + }, + 30 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + 60_000 + ); + }); + + describe('validation errors', () => { + it( + 'should throw an error if campaignRequest is missing', + async () => { + let error: Error; + await web3mail + .sendEmailCampaign({ + campaignRequest: undefined as any, // Testing missing campaignRequest + workerpoolAddressOrEns: learnProdWorkerpoolAddress, + }) + .catch((e) => (error = e)); + + expect(error).toBeDefined(); + + const isValidationrror = error instanceof ValidationError; + expect(isValidationrror).toBe(true); + expect(error.message === 'campaignRequest is a required field').toBe( + true + ); + }, + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + + it( + 'should throw an error if workerpoolAddressOrEns is invalid', + async () => { + // Prepare campaign first + const contacts: Contact[] = await web3mail.fetchMyContacts({ + bulkOnly: true, + }); + const bulkOrders = contacts.map((contact) => contact.grantedAccess); + + const campaignRequest = await web3mail.prepareEmailCampaign({ + emailSubject: 'Bulk test subject', + emailContent: 'Bulk test message', + grantedAccesses: bulkOrders, + maxProtectedDataPerTask: 3, + workerpoolAddressOrEns: learnProdWorkerpoolAddress, + }); + + let error: Error; + await web3mail + .sendEmailCampaign({ + campaignRequest: campaignRequest.campaignRequest, + workerpoolAddressOrEns: 'invalid-address', + }) + .catch((e) => (error = e)); + + expect(error).toBeDefined(); + // Invalid address throws ValidationError from addressOrEnsSchema validation + const isValidationError = error instanceof ValidationError; + expect(isValidationError).toBe(true); + expect( + error.message === + 'workerpoolAddressOrEns should be an ethereum address or a ENS name' + ).toBe(true); + }, + 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + }); + + describe('protocol errors', () => { + it( + 'should throw a protocol error if a service is not available', + async () => { + // Call getTestConfig to get the default configuration + const [ethProvider, defaultOptions] = getTestConfig( + providerWallet.privateKey + ); + + const options = { + ...defaultOptions, + iexecOptions: { + ...defaultOptions.iexecOptions, + iexecGatewayURL: 'https://test', + }, + }; + let error: Web3mailWorkflowError | undefined; + try { + // Pass the modified options to IExecWeb3mail + const invalidWeb3mail = new IExecWeb3mail(ethProvider, options); + + // Prepare campaign first + const contacts: Contact[] = await invalidWeb3mail.fetchMyContacts({ + bulkOnly: true, + }); + const bulkOrders = contacts.map((contact) => contact.grantedAccess); + + const campaignRequest = await invalidWeb3mail.prepareEmailCampaign({ + emailSubject: 'Bulk test subject', + emailContent: 'Bulk test message', + grantedAccesses: bulkOrders, + maxProtectedDataPerTask: 3, + workerpoolAddressOrEns: learnProdWorkerpoolAddress, + }); + + await invalidWeb3mail.sendEmailCampaign({ + campaignRequest: campaignRequest.campaignRequest, + workerpoolAddressOrEns: learnProdWorkerpoolAddress, + }); + } catch (err) { + error = err as Web3mailWorkflowError; + } + + expect(error).toBeInstanceOf(Web3mailWorkflowError); + expect(error?.message).toBe( + "A service in the iExec protocol appears to be unavailable. You can retry later or contact iExec's technical support for help." + ); + expect(error?.isProtocolError).toBe(true); + }, + 2 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + ); + }); + + describe('integration with prepareEmailCampaign', () => { + it( + 'should successfully send campaign prepared by prepareEmailCampaign', + async () => { + // Fetch contacts with allowBulk access + const contacts: Contact[] = await web3mail.fetchMyContacts({ + bulkOnly: true, + }); + expect(contacts.length).toBeGreaterThanOrEqual(3); + + const bulkOrders = contacts.map((contact) => contact.grantedAccess); + + // Prepare email campaign + const campaignRequest = await web3mail.prepareEmailCampaign({ + emailSubject: 'Integration test subject', + emailContent: 'Integration test message', + senderName: 'Integration Test', + grantedAccesses: bulkOrders, + maxProtectedDataPerTask: 3, + workerpoolAddressOrEns: learnProdWorkerpoolAddress, + }); + + // Verify campaign request was created + expect(campaignRequest).toBeDefined(); + expect(campaignRequest.campaignRequest).toBeDefined(); + + // Send the campaign + const result = await web3mail.sendEmailCampaign({ + campaignRequest: campaignRequest.campaignRequest, + workerpoolAddressOrEns: learnProdWorkerpoolAddress, + }); + + // Verify the result + expect(result).toBeDefined(); + expect('tasks' in result).toBe(true); + expect(result.tasks).toBeDefined(); + expect(Array.isArray(result.tasks)).toBe(true); + expect(result.tasks.length).toBeGreaterThan(0); + }, + 30 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + 60_000 + ); + + it( + 'should handle multiple protected data per task correctly', + async () => { + // Fetch contacts + const contacts: Contact[] = await web3mail.fetchMyContacts({ + bulkOnly: true, + }); + expect(contacts.length).toBeGreaterThanOrEqual(3); + + const bulkOrders = contacts.map((contact) => contact.grantedAccess); + + // Prepare campaign with maxProtectedDataPerTask: 2 + const campaignRequest = await web3mail.prepareEmailCampaign({ + emailSubject: 'Bulk test subject', + emailContent: 'Bulk test message', + grantedAccesses: bulkOrders, + maxProtectedDataPerTask: 2, // Process 2 emails per task + workerpoolAddressOrEns: learnProdWorkerpoolAddress, + }); + + // Send campaign + const result = await web3mail.sendEmailCampaign({ + campaignRequest: campaignRequest.campaignRequest, + workerpoolAddressOrEns: learnProdWorkerpoolAddress, + }); + + // Verify tasks were created + expect(result).toBeDefined(); + expect('tasks' in result).toBe(true); + expect(result.tasks.length).toBeGreaterThan(0); + + // With 3 protected data and maxProtectedDataPerTask: 2, we should have at least 2 tasks + expect(result.tasks.length).toBeGreaterThanOrEqual(1); + }, + 30 * MAX_EXPECTED_BLOCKTIME + MAX_EXPECTED_WEB2_SERVICES_TIME + 60_000 + ); + }); +}); diff --git a/tests/scripts/prepare-iexec.js b/tests/scripts/prepare-iexec.js new file mode 100644 index 00000000..30c8b041 --- /dev/null +++ b/tests/scripts/prepare-iexec.js @@ -0,0 +1,29 @@ +import { readFile, writeFile } from 'fs/promises'; +import { resolve } from 'path'; + +const disableCheckImplementedOnChain = async () => { + const configModulePath = resolve( + 'node_modules/iexec/dist/esm/common/utils/config.js' + ); + + const configModule = await readFile(configModulePath, 'utf8'); + + const OG_CODE_SNIPPET = + 'export const checkImplementedOnChain = (chainId, featureName) => {'; + + const REPLACEMENT_CODE_SNIPPET = + 'export const checkImplementedOnChain = (chainId, featureName) => { return;'; + + if (!configModule.includes(REPLACEMENT_CODE_SNIPPET)) { + console.log('disabling checkImplementedOnChain implementation...'); + const patchedConfigModule = configModule.replace( + OG_CODE_SNIPPET, + REPLACEMENT_CODE_SNIPPET + ); + + await writeFile(configModulePath, patchedConfigModule, 'utf8'); + } +}; + +disableCheckImplementedOnChain(); + diff --git a/tests/test-utils.ts b/tests/test-utils.ts index 46a5eb6f..b4a2a29c 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -10,6 +10,8 @@ import { randomInt } from 'crypto'; import { getSignerFromPrivateKey } from 'iexec/utils'; export const TEST_CHAIN = { + ipfsGateway: 'http://127.0.0.1:8080', + ipfsNode: 'http://127.0.0.1:5001', rpcURL: 'http://127.0.0.1:8545', chainId: '134', smsURL: 'http://127.0.0.1:13300', @@ -75,6 +77,8 @@ export const getTestIExecOption = () => ({ iexecGatewayURL: TEST_CHAIN.iexecGatewayURL, voucherHubAddress: TEST_CHAIN.voucherHubAddress, voucherSubgraphURL: TEST_CHAIN.voucherSubgraphURL, + ipfsGateway: TEST_CHAIN.ipfsGateway, + ipfsNode: TEST_CHAIN.ipfsNode, }); export const getTestConfig = ( @@ -239,7 +243,58 @@ export const ensureSufficientStake = async ( if (BigInt(account.stake.toString()) < BigInt(requiredStake.toString())) { await setNRlcBalance(walletAddress, requiredStake); - await iexec.account.deposit(requiredStake); + try { + await iexec.account.deposit(requiredStake); + } catch (error: any) { + // Handle "transaction already imported" error - this can happen when + // multiple tests run in parallel and try to deposit simultaneously. + // If the transaction is already submitted, we can check if the balance + // will be sufficient after it's mined, or just wait a bit and retry. + if ( + error?.message?.includes('transaction already imported') || + error?.code === -32003 || + (error?.cause?.code === -32003 && + error?.cause?.message?.includes('transaction already imported')) + ) { + // Wait a bit for the transaction to be mined + await new Promise((resolve) => setTimeout(resolve, 2000)); + // Re-check balance - if it's now sufficient, we're good + const updatedAccount = await iexec.account.checkBalance(walletAddress); + if ( + BigInt(updatedAccount.stake.toString()) >= + BigInt(requiredStake.toString()) + ) { + // Balance is sufficient, transaction was already processed + return; + } + // If still insufficient, the transaction might be pending or failed + // Try one more time after waiting + try { + await iexec.account.deposit(requiredStake); + } catch (retryError: any) { + // If it still fails with the same error, check balance one more time + if ( + retryError?.message?.includes('transaction already imported') || + retryError?.code === -32003 + ) { + const finalAccount = await iexec.account.checkBalance( + walletAddress + ); + if ( + BigInt(finalAccount.stake.toString()) >= + BigInt(requiredStake.toString()) + ) { + return; + } + } + // If balance is still insufficient, throw the original error + throw retryError; + } + } else { + // For other errors, re-throw + throw error; + } + } } }; diff --git a/tests/unit/fetchMyContacts.test.ts b/tests/unit/fetchMyContacts.test.ts index 323a7ea8..5fc38602 100644 --- a/tests/unit/fetchMyContacts.test.ts +++ b/tests/unit/fetchMyContacts.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it, jest } from '@jest/globals'; import { Address } from 'iexec'; -import { type FetchMyContacts } from '../../src/web3mail/fetchMyContacts.js'; -import { getRandomAddress } from '../test-utils.js'; import { DEFAULT_CHAIN_ID, getChainDefaultConfig, } from '../../src/config/config.js'; +import { type FetchMyContacts } from '../../src/web3mail/fetchMyContacts.js'; +import { getRandomAddress } from '../test-utils.js'; jest.unstable_mockModule('../../src/utils/subgraphQuery.js', () => ({ getValidContact: jest.fn(), @@ -14,6 +14,7 @@ jest.unstable_mockModule('../../src/utils/subgraphQuery.js', () => ({ describe('fetchMyContacts', () => { let testedModule: any; let fetchMyContacts: FetchMyContacts; + const defaultConfig = getChainDefaultConfig(DEFAULT_CHAIN_ID); beforeAll(async () => { // import tested module after all mocked modules @@ -45,7 +46,7 @@ describe('fetchMyContacts', () => { // --- GIVEN const { getValidContact } = (await import( '../../src/utils/subgraphQuery.js' - )) as unknown as { getValidContact: jest.Mock<() => Promise<[]>> }; + )) as unknown as { getValidContact: jest.Mock<() => Promise> }; getValidContact.mockResolvedValue([]); const mockFetchDatasetOrderbook: any = jest.fn().mockImplementation(() => { @@ -78,43 +79,35 @@ describe('fetchMyContacts', () => { iexec: iexec, // @ts-expect-error No need for graphQLClient here graphQLClient: {}, - dappAddressOrENS: getChainDefaultConfig(DEFAULT_CHAIN_ID).dappAddress, - dappWhitelistAddress: - getChainDefaultConfig(DEFAULT_CHAIN_ID).whitelistSmartContract, - isUserStrict: false, + dappAddressOrENS: defaultConfig.dappAddress, + dappWhitelistAddress: defaultConfig.whitelistSmartContract, }); const userAddress = (await iexec.wallet.getAddress()).toLowerCase(); - expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith( - 1, - 'any', - { - app: getChainDefaultConfig(DEFAULT_CHAIN_ID).dappAddress.toLowerCase(), - requester: userAddress, - isAppStrict: true, - isRequesterStrict: false, - pageSize: 1000, - } - ); - expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith( - 2, - 'any', - { - app: getChainDefaultConfig( - DEFAULT_CHAIN_ID - ).whitelistSmartContract.toLowerCase(), - requester: userAddress, - isAppStrict: true, - isRequesterStrict: false, - pageSize: 1000, - } - ); + expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith(1, { + dataset: 'any', + app: defaultConfig.dappAddress.toLowerCase(), + requester: userAddress, + isAppStrict: true, + isRequesterStrict: false, + bulkOnly: false, + pageSize: 1000, + }); + expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith(2, { + dataset: 'any', + app: defaultConfig.whitelistSmartContract.toLowerCase(), + requester: userAddress, + isAppStrict: true, + isRequesterStrict: false, + bulkOnly: false, + pageSize: 1000, + }); }); it('should fetch granted access with isRequesterStrict param equal to true', async () => { // --- GIVEN const { getValidContact } = (await import( '../../src/utils/subgraphQuery.js' - )) as unknown as { getValidContact: jest.Mock<() => Promise<[]>> }; + )) as unknown as { getValidContact: jest.Mock<() => Promise> }; getValidContact.mockResolvedValue([]); const mockFetchDatasetOrderbook: any = jest.fn().mockImplementation(() => { @@ -147,37 +140,284 @@ describe('fetchMyContacts', () => { iexec: iexec, // @ts-expect-error No need for graphQLClient here graphQLClient: {}, - dappAddressOrENS: getChainDefaultConfig(DEFAULT_CHAIN_ID).dappAddress, - dappWhitelistAddress: - getChainDefaultConfig( - DEFAULT_CHAIN_ID - ).whitelistSmartContract.toLowerCase(), + dappAddressOrENS: defaultConfig.dappAddress, + dappWhitelistAddress: defaultConfig.whitelistSmartContract, isUserStrict: true, }); const userAddress = (await iexec.wallet.getAddress()).toLowerCase(); - expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith( - 1, - 'any', + expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith(1, { + dataset: 'any', + app: defaultConfig.dappAddress.toLowerCase(), + requester: userAddress, + isAppStrict: true, + isRequesterStrict: true, + bulkOnly: false, + pageSize: 1000, + }); + expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith(2, { + dataset: 'any', + app: defaultConfig.whitelistSmartContract.toLowerCase(), + requester: userAddress, + isAppStrict: true, + isRequesterStrict: true, + bulkOnly: false, + pageSize: 1000, + }); + }); + + it('should include grantedAccess property in returned contacts', async () => { + // --- GIVEN + const { getValidContact } = (await import( + '../../src/utils/subgraphQuery.js' + )) as unknown as { getValidContact: jest.Mock<() => Promise> }; + + const mockContacts = [ { - app: getChainDefaultConfig(DEFAULT_CHAIN_ID).dappAddress.toLowerCase(), + address: '0x35396912Db97ff130411301Ec722Fc92Ac37B00d', + owner: '0xD52C27CC2c7D3fb5BA4440ffa825c12EA5658D60', + remainingAccess: 10, + accessPrice: 0, + accessGrantTimestamp: '2023-06-15T16:39:22.713Z', + isUserStrict: false, + grantedAccess: MOCK_ORDER.order, + }, + ]; + getValidContact.mockResolvedValue(mockContacts); + + const mockFetchDatasetOrderbook: any = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + count: 1, + nextPage: 1, + orders: [MOCK_ORDER], + }); + }); + + const iexec = { + wallet: { + getAddress: jest + .fn<() => Promise
>() + .mockResolvedValue(getRandomAddress()), + }, + ens: { + resolveName: jest + .fn<() => Promise
>() + .mockResolvedValue(getRandomAddress()), + }, + orderbook: { + fetchDatasetOrderbook: mockFetchDatasetOrderbook, + }, + }; + + // --- WHEN + const result = await fetchMyContacts({ + // @ts-expect-error Minimal iexec implementation with only what's necessary for this test + iexec: iexec, + // @ts-expect-error No need for graphQLClient here + graphQLClient: {}, + dappAddressOrENS: defaultConfig.dappAddress, + dappWhitelistAddress: defaultConfig.whitelistSmartContract, + }); + + // --- THEN + expect(result).toEqual(mockContacts); + expect(result[0]).toHaveProperty('grantedAccess'); + expect(result[0].grantedAccess).toEqual(MOCK_ORDER.order); + }); + + describe('bulkOnly parameter', () => { + it('should pass bulkOnly=false (default) when fetching contacts', async () => { + // --- GIVEN + const { getValidContact } = (await import( + '../../src/utils/subgraphQuery.js' + )) as unknown as { getValidContact: jest.Mock<() => Promise> }; + getValidContact.mockResolvedValue([]); + + const mockFetchDatasetOrderbook: any = jest + .fn() + .mockImplementation(() => { + return Promise.resolve({ + ok: true, + count: 1, + nextPage: 1, + orders: [MOCK_ORDER], + }); + }); + + const iexec = { + wallet: { + getAddress: jest + .fn<() => Promise
>() + .mockResolvedValue(getRandomAddress()), + }, + ens: { + resolveName: jest + .fn<() => Promise
>() + .mockResolvedValue(getRandomAddress()), + }, + orderbook: { + fetchDatasetOrderbook: mockFetchDatasetOrderbook, + }, + }; + + await fetchMyContacts({ + // @ts-expect-error Minimal iexec implementation with only what's necessary for this test + iexec: iexec, + // @ts-expect-error No need for graphQLClient here + graphQLClient: {}, + dappAddressOrENS: defaultConfig.dappAddress, + dappWhitelistAddress: defaultConfig.whitelistSmartContract, + bulkOnly: false, + }); + const userAddress = (await iexec.wallet.getAddress()).toLowerCase(); + expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith(1, { + dataset: 'any', + app: defaultConfig.dappAddress.toLowerCase(), + requester: userAddress, + isAppStrict: true, + isRequesterStrict: false, + bulkOnly: false, + pageSize: 1000, + }); + expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith(2, { + dataset: 'any', + app: defaultConfig.whitelistSmartContract.toLowerCase(), + requester: userAddress, + isAppStrict: true, + isRequesterStrict: false, + bulkOnly: false, + pageSize: 1000, + }); + }); + + it('should pass bulkOnly=true when fetching contacts', async () => { + // --- GIVEN + const { getValidContact } = (await import( + '../../src/utils/subgraphQuery.js' + )) as unknown as { getValidContact: jest.Mock<() => Promise> }; + getValidContact.mockResolvedValue([]); + + const mockFetchDatasetOrderbook: any = jest + .fn() + .mockImplementation(() => { + return Promise.resolve({ + ok: true, + count: 1, + nextPage: 1, + orders: [MOCK_ORDER], + }); + }); + + const iexec = { + wallet: { + getAddress: jest + .fn<() => Promise
>() + .mockResolvedValue(getRandomAddress()), + }, + ens: { + resolveName: jest + .fn<() => Promise
>() + .mockResolvedValue(getRandomAddress()), + }, + orderbook: { + fetchDatasetOrderbook: mockFetchDatasetOrderbook, + }, + }; + + await fetchMyContacts({ + // @ts-expect-error Minimal iexec implementation with only what's necessary for this test + iexec: iexec, + // @ts-expect-error No need for graphQLClient here + graphQLClient: {}, + dappAddressOrENS: defaultConfig.dappAddress, + dappWhitelistAddress: defaultConfig.whitelistSmartContract, + bulkOnly: true, + }); + const userAddress = (await iexec.wallet.getAddress()).toLowerCase(); + expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith(1, { + dataset: 'any', + app: defaultConfig.dappAddress.toLowerCase(), + requester: userAddress, + isAppStrict: true, + isRequesterStrict: false, + bulkOnly: true, + pageSize: 1000, + }); + expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith(2, { + dataset: 'any', + app: defaultConfig.whitelistSmartContract.toLowerCase(), + requester: userAddress, + isAppStrict: true, + isRequesterStrict: false, + bulkOnly: true, + pageSize: 1000, + }); + }); + + it('should work with both isUserStrict and bulkOnly parameters', async () => { + // --- GIVEN + const { getValidContact } = (await import( + '../../src/utils/subgraphQuery.js' + )) as unknown as { getValidContact: jest.Mock<() => Promise> }; + getValidContact.mockResolvedValue([]); + + const mockFetchDatasetOrderbook: any = jest + .fn() + .mockImplementation(() => { + return Promise.resolve({ + ok: true, + count: 1, + nextPage: 1, + orders: [MOCK_ORDER], + }); + }); + + const iexec = { + wallet: { + getAddress: jest + .fn<() => Promise
>() + .mockResolvedValue(getRandomAddress()), + }, + ens: { + resolveName: jest + .fn<() => Promise
>() + .mockResolvedValue(getRandomAddress()), + }, + orderbook: { + fetchDatasetOrderbook: mockFetchDatasetOrderbook, + }, + }; + + await fetchMyContacts({ + // @ts-expect-error Minimal iexec implementation with only what's necessary for this test + iexec: iexec, + // @ts-expect-error No need for graphQLClient here + graphQLClient: {}, + dappAddressOrENS: defaultConfig.dappAddress, + dappWhitelistAddress: defaultConfig.whitelistSmartContract, + isUserStrict: true, + bulkOnly: true, + }); + + const userAddress = (await iexec.wallet.getAddress()).toLowerCase(); + expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith(1, { + dataset: 'any', + app: defaultConfig.dappAddress.toLowerCase(), requester: userAddress, isAppStrict: true, isRequesterStrict: true, + bulkOnly: true, pageSize: 1000, - } - ); - expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith( - 2, - 'any', - { - app: getChainDefaultConfig( - DEFAULT_CHAIN_ID - ).whitelistSmartContract.toLowerCase(), + }); + expect(iexec.orderbook.fetchDatasetOrderbook).toHaveBeenNthCalledWith(2, { + dataset: 'any', + app: defaultConfig.whitelistSmartContract.toLowerCase(), requester: userAddress, isAppStrict: true, isRequesterStrict: true, + bulkOnly: true, pageSize: 1000, - } - ); + }); + }); }); }); diff --git a/tests/unit/utils/subgraphQuery.test.ts b/tests/unit/utils/subgraphQuery.test.ts index f7d5e946..ba06b81a 100644 --- a/tests/unit/utils/subgraphQuery.test.ts +++ b/tests/unit/utils/subgraphQuery.test.ts @@ -13,6 +13,18 @@ describe('getValidContact', () => { beforeAll(() => { // Initialize the variables in the beforeAll hook + const mockGrantedAccess = { + dataset: '0x0000000000000000000000000000000000000000', + datasetprice: '0', + volume: '1', + tag: '0x0000000000000000000000000000000000000000000000000000000000000000', + apprestrict: '0x0000000000000000000000000000000000000000', + workerpoolrestrict: '0x0000000000000000000000000000000000000000', + requesterrestrict: '0x0000000000000000000000000000000000000000', + salt: '0x0000000000000000000000000000000000000000000000000000000000000000', + sign: '0x0000000000000000000000000000000000000000000000000000000000000000', + remainingAccess: 1, + }; contacts = [ { address: 'address1', @@ -21,6 +33,7 @@ describe('getValidContact', () => { remainingAccess: 1, accessPrice: 0, isUserStrict: true, + grantedAccess: mockGrantedAccess, }, { address: 'address2', @@ -29,6 +42,7 @@ describe('getValidContact', () => { remainingAccess: 1, accessPrice: 0, isUserStrict: true, + grantedAccess: mockGrantedAccess, }, { address: 'address3', @@ -37,6 +51,7 @@ describe('getValidContact', () => { remainingAccess: 1, accessPrice: 0, isUserStrict: true, + grantedAccess: mockGrantedAccess, }, ]; @@ -64,6 +79,7 @@ describe('getValidContact', () => { remainingAccess: 1, accessPrice: 0, isUserStrict: true, + grantedAccess: contacts[0].grantedAccess, }, { address: 'address3', @@ -73,6 +89,7 @@ describe('getValidContact', () => { remainingAccess: 1, accessPrice: 0, isUserStrict: true, + grantedAccess: contacts[2].grantedAccess, }, ]);