diff --git a/package-lock.json b/package-lock.json index 3ff2b492..432f5efa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@iexec/dataprotector": "0.1.2", "@iexec/web3mail": "0.4.0", "@leap-ai/sdk": "^0.0.119", - "@talentlayer/client": "^0.1.1", "@web3modal/ethereum": "^2.7.1", "@web3modal/react": "^2.7.1", "@xmtp/xmtp-js": "^9.1.0", @@ -2260,336 +2259,6 @@ "ethers": "^5.7.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -9801,17 +9470,6 @@ "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" } }, - "node_modules/@talentlayer/client": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@talentlayer/client/-/client-0.1.1.tgz", - "integrity": "sha512-r5Aj1w/+yrbnrBjCwuLa4pKLXaVtObdOyPs4XaixKlZDQ97i82jij05RGR63HBPU6UAXhJW0IEB6SumgmLw+mQ==", - "dependencies": { - "axios": "^1.5.0", - "ipfs-http-client": "59.0.0", - "tsup": "7.2.0", - "viem": "^1.10.14" - } - }, "node_modules/@tanstack/query-core": { "version": "4.35.0", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.35.0.tgz", @@ -13010,11 +12668,6 @@ "node": ">=4" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, "node_modules/any-signal": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/any-signal/-/any-signal-3.0.1.tgz", @@ -14145,20 +13798,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bundle-require": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.2.tgz", - "integrity": "sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.17" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -14180,14 +13819,6 @@ "node": ">= 0.8" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/cacheable-lookup": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.1.0.tgz", @@ -15307,6 +14938,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "devOptional": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -16297,42 +15929,6 @@ "ext": "^1.1.2" } }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -16969,6 +16565,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "devOptional": true, "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -18973,6 +18570,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "devOptional": true, "engines": { "node": ">=10.17.0" } @@ -20392,7 +19990,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true }, "node_modules/iso-url": { "version": "1.2.1", @@ -22829,14 +22428,6 @@ "@sideway/pinpoint": "^2.0.0" } }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "engines": { - "node": ">=10" - } - }, "node_modules/js-sdsl": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz", @@ -23637,6 +23228,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "dev": true, "engines": { "node": ">=10" } @@ -23644,7 +23236,8 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "node_modules/lit": { "version": "2.7.6", @@ -23719,14 +23312,6 @@ "node": ">=0.10.0" } }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -25779,16 +25364,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nano-base32": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nano-base32/-/nano-base32-1.0.1.tgz", @@ -26148,6 +25723,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "devOptional": true, "dependencies": { "path-key": "^3.0.0" }, @@ -26704,6 +26280,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, "engines": { "node": ">=8" } @@ -26882,6 +26459,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "devOptional": true, "engines": { "node": ">= 6" } @@ -26977,7 +26555,7 @@ "version": "8.4.19", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "opencollective", @@ -27104,7 +26682,7 @@ "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "devOptional": true, + "dev": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -28964,6 +28542,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -28975,6 +28554,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, "engines": { "node": ">=8" } @@ -29755,6 +29335,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "devOptional": true, "engines": { "node": ">=6" } @@ -29819,67 +29400,6 @@ } } }, - "node_modules/sucrase": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", - "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/sudo-prompt": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", @@ -30308,25 +29828,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/thread-stream": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", @@ -30520,14 +30021,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "bin": { - "tree-kill": "cli.js" - } - }, "node_modules/ts-algebra": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-1.2.0.tgz", @@ -30546,11 +30039,6 @@ "typescript": ">=4.2.0" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" - }, "node_modules/ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -30686,143 +30174,6 @@ "integrity": "sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw==", "peer": true }, - "node_modules/tsup": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-7.2.0.tgz", - "integrity": "sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==", - "dependencies": { - "bundle-require": "^4.0.0", - "cac": "^6.7.12", - "chokidar": "^3.5.1", - "debug": "^4.3.1", - "esbuild": "^0.18.2", - "execa": "^5.0.0", - "globby": "^11.0.3", - "joycon": "^3.0.1", - "postcss-load-config": "^4.0.1", - "resolve-from": "^5.0.0", - "rollup": "^3.2.5", - "source-map": "0.8.0-beta.0", - "sucrase": "^3.20.3", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=16.14" - }, - "peerDependencies": { - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/tsup/node_modules/postcss-load-config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^2.1.1" - }, - "engines": { - "node": ">= 14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/tsup/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/tsup/node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/tsup/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tsup/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/tsup/node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "node_modules/tsup/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/tsup/node_modules/yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", - "engines": { - "node": ">= 14" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -32137,6 +31488,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, "dependencies": { "isexe": "^2.0.0" }, diff --git a/package.json b/package.json index e64bcf06..05d8916a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "@iexec/dataprotector": "0.1.2", "@iexec/web3mail": "0.4.0", "@leap-ai/sdk": "^0.0.119", - "@talentlayer/client": "^0.1.1", "@web3modal/ethereum": "^2.7.1", "@web3modal/react": "^2.7.1", "@xmtp/xmtp-js": "^9.1.0", @@ -64,10 +63,10 @@ "@tailwindcss/forms": "^0.5.3", "@tailwindcss/line-clamp": "^0.4.2", "@types/jest": "^29.5.4", + "@types/level-js": "^4.0.2", "@types/node": "^18.17.0", "@types/react": "^18.0.23", "@types/react-dom": "^18.0.7", - "@types/level-js": "^4.0.2", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "autoprefixer": "^10.4.12", @@ -86,4 +85,4 @@ "engines": { "node": ">=18.17" } -} \ No newline at end of file +} diff --git a/src/components/DisputeButton.tsx b/src/components/DisputeButton.tsx new file mode 100644 index 00000000..153ab3aa --- /dev/null +++ b/src/components/DisputeButton.tsx @@ -0,0 +1,130 @@ +import { ITransaction, IUser, TransactionStatusEnum } from '../types'; +import { arbitrationFeeTimeout, payArbitrationFee } from '../contracts/disputes'; +import { usePublicClient, useWalletClient } from 'wagmi'; +import { useRouter } from 'next/router'; +import { useChainId } from '../hooks/useChainId'; +import { useConfig } from '../hooks/useConfig'; + +function DisputeButton({ + user, + transaction, + disabled, + arbitrationFee, + content, +}: { + user: IUser; + transaction: ITransaction; + disabled: boolean; + arbitrationFee: BigInt; + content?: string; +}) { + const chainId = useChainId(); + const { data: walletClient } = useWalletClient({ chainId }); + const publicClient = usePublicClient({ chainId }); + const router = useRouter(); + const transactionId = transaction?.id; + const config = useConfig(); + + const isSender = !!user && !!transaction && user.id === transaction.sender.id; + + const isReceiver = !!user && !!transaction && user.id === transaction.receiver.id; + + const userIsSenderAndHasPaid = + isSender && transaction.status === TransactionStatusEnum.WaitingReceiver; + + const userIsSenderAndHasNotPaid = + isSender && transaction.status === TransactionStatusEnum.WaitingSender; + + const userIsReceiverAndHasPaid = + isReceiver && transaction.status === TransactionStatusEnum.WaitingSender; + + const userIsReceiverAndHasNotPaid = + isReceiver && transaction.status === TransactionStatusEnum.WaitingReceiver; + + const noDispute = transaction.status === TransactionStatusEnum.NoDispute; + + const disputeCreated = transaction.status === TransactionStatusEnum.DisputeCreated; + + const disputeResolved = transaction.status === TransactionStatusEnum.Resolved; + + const payFee = () => { + if (walletClient && arbitrationFee) { + return payArbitrationFee( + walletClient, + publicClient, + arbitrationFee, + isSender, + transactionId, + router, + config, + ); + } + }; + const timeout = () => { + if (walletClient) { + return arbitrationFeeTimeout(walletClient, publicClient, transactionId, router, config); + } + }; + + return ( + <> + {userIsSenderAndHasNotPaid && ( + + )} + {noDispute && ( + + )} + {(userIsReceiverAndHasPaid || userIsSenderAndHasPaid) && ( + + )} + {userIsReceiverAndHasNotPaid && ( + + )} + {disputeCreated && ( + + Waiting for arbitration... + + )} + {disputeResolved && ( + + Dispute resolved + + )} + + ); +} + +export default DisputeButton; diff --git a/src/components/Form/EvidenceForm.tsx b/src/components/Form/EvidenceForm.tsx new file mode 100644 index 00000000..b9afa95b --- /dev/null +++ b/src/components/Form/EvidenceForm.tsx @@ -0,0 +1,136 @@ +import { ErrorMessage, Field, Form, Formik } from 'formik'; +import { useContext, useState } from 'react'; +import * as Yup from 'yup'; +import SubmitButton from './SubmitButton'; +import FileDropper from '../../modules/Disputes/components/FileDropper'; +import { postToIPFS } from '../../utils/ipfs'; +import { showErrorTransactionToast } from '../../utils/toast'; +import { generateEvidence } from '../../modules/Disputes/utils/dispute'; +import TalentLayerContext from '../../context/talentLayer'; +import { useWeb3Modal } from '@web3modal/react'; +import { submitEvidence } from '../../contracts/disputes'; +import { usePublicClient, useWalletClient } from 'wagmi'; +import { useChainId } from '../../hooks/useChainId'; +import { useConfig } from '../../hooks/useConfig'; + +interface IFormValues { + title: string; + about: string; + file: File | null; +} + +const validationSchema = Yup.object({ + title: Yup.string().required('Please provide a title for your evidence'), + about: Yup.string().required('Please provide a description of your evidence'), + file: Yup.mixed().required('Please provide a file'), +}); + +const initialValues: IFormValues = { + title: '', + about: '', + file: null, +}; + +function EvidenceForm({ transactionId }: { transactionId: string }) { + const { account, user } = useContext(TalentLayerContext); + const chainId = useChainId(); + const { data: walletClient } = useWalletClient({ chainId }); + const publicClient = usePublicClient({ chainId }); + const { open: openConnectModal } = useWeb3Modal(); + const [fileSelected, setFileSelected] = useState(); + const config = useConfig(); + + const onSubmit = async ( + values: IFormValues, + { + setSubmitting, + resetForm, + }: { setSubmitting: (isSubmitting: boolean) => void; resetForm: () => void }, + ) => { + if (account?.isConnected === true && publicClient && walletClient && user?.id) { + try { + const fileExtension = values.file?.name.split('.').pop(); + if (!values.file) return; + const arr = await values?.file.arrayBuffer(); + const fileCid = await postToIPFS(arr); + + const evidence = generateEvidence( + values.title, + values.about, + fileCid, + fileExtension as string, + ); + const evidenceCid = await postToIPFS(JSON.stringify(evidence)); + + await submitEvidence(walletClient, publicClient, user?.id, transactionId, evidenceCid, chainId,config); + setSubmitting(false); + resetForm(); + setFileSelected(undefined); + } catch (error) { + showErrorTransactionToast(error); + } + } else { + openConnectModal(); + } + }; + + return ( + + {({ isSubmitting, dirty, isValid }) => ( + <> +
+

Add evidence:

+
+
+ + + +
+ + +
+
+ +
+
+ + )} +
+ ); +} + +export default EvidenceForm; \ No newline at end of file diff --git a/src/components/Form/ProposalForm.tsx b/src/components/Form/ProposalForm.tsx index 93812266..c835c610 100644 --- a/src/components/Form/ProposalForm.tsx +++ b/src/components/Form/ProposalForm.tsx @@ -22,13 +22,14 @@ import SubmitButton from './SubmitButton'; import useTalentLayerClient from '../../hooks/useTalentLayerClient'; import usePlatform from '../../hooks/usePlatform'; import { chains } from '../../pages/_app'; +import MetaEvidenceModal from '../../modules/Disputes/components/MetaEvidenceModal'; interface IFormValues { about: string; rateToken: string; rateAmount: number; expirationDate: number; - video_url: string; + videoUrl: string; } const validationSchema = Yup.object({ @@ -56,6 +57,7 @@ function ProposalForm({ const { platformHasAccess } = useContext(Web3MailContext); const [aiLoading, setAiLoading] = useState(false); const talentLayerClient = useTalentLayerClient(); + const [conditionsValidated, setConditionsValidated] = useState(false); const currentChain = chains.find(chain => chain.id === chainId); const platform = usePlatform(process.env.NEXT_PUBLIC_PLATFORM_ID as string); @@ -89,7 +91,7 @@ function ProposalForm({ rateToken: existingProposal?.rateToken.address || '', rateAmount: existingRateTokenAmount || 0, expirationDate: existingExpirationDate || 15, - video_url: existingProposal?.description?.video_url || '', + videoUrl: existingProposal?.description?.video_url || '', }; const askAI = async (input: string, setFieldValue: any) => { @@ -134,7 +136,7 @@ function ProposalForm({ const proposal = { about: values.about, - video_url: values.video_url, + video_url: values.videoUrl, }; let tx, cid, proposalResponse; @@ -318,7 +320,21 @@ function ProposalForm({ {currentChain?.nativeCurrency.symbol} )} - +
+ token.address === values.rateToken)[0]} + seller={user} + /> + +
)} diff --git a/src/components/Form/SubmitButton.tsx b/src/components/Form/SubmitButton.tsx index da6c00dd..ae847818 100644 --- a/src/components/Form/SubmitButton.tsx +++ b/src/components/Form/SubmitButton.tsx @@ -4,9 +4,11 @@ import { useAccount } from 'wagmi'; function SubmitButton({ isSubmitting, label = 'Create', + disabled = false, }: { isSubmitting: boolean; label?: string; + disabled?: boolean; }) { const { isConnected } = useAccount(); const { open: openConnectModal } = useWeb3Modal(); @@ -36,7 +38,14 @@ function SubmitButton({ Loading... ) : isConnected ? ( - ) : ( diff --git a/src/components/Modal/ValidateProposalModal.tsx b/src/components/Modal/ValidateProposalModal.tsx index 269265a2..44225382 100644 --- a/src/components/Modal/ValidateProposalModal.tsx +++ b/src/components/Modal/ValidateProposalModal.tsx @@ -5,14 +5,24 @@ import { FEE_RATE_DIVIDER } from '../../config'; import { validateProposal } from '../../contracts/acceptProposal'; import useFees from '../../hooks/useFees'; import ContactButton from '../../modules/Messaging/components/ContactButton'; -import { IAccount, IProposal } from '../../types'; +import { IAccount, IProposal, IService } from '../../types'; import { renderTokenAmount } from '../../utils/conversion'; import Step from '../Step'; import { useChainId } from '../../hooks/useChainId'; import useTalentLayerClient from '../../hooks/useTalentLayerClient'; import { ZERO_ADDRESS } from '../../utils/constant'; +import { postToIPFS } from '../../utils/ipfs'; +import { generateMetaEvidence } from '../../modules/Disputes/utils/dispute'; -function ValidateProposalModal({ proposal, account }: { proposal: IProposal; account: IAccount }) { +function ValidateProposalModal({ + proposal, + service, + account, +}: { + proposal: IProposal; + service: IService; + account: IAccount; +}) { const chainId = useChainId(); const { data: walletClient } = useWalletClient({ chainId, @@ -50,12 +60,27 @@ function ValidateProposalModal({ proposal, account }: { proposal: IProposal; acc return; } + const metaEvidence = generateMetaEvidence( + service.description?.about || '', + proposal.description?.about || '', + service.buyer.handle, + proposal.seller.handle, + proposal.rateToken.address, + proposal.rateToken.symbol, + proposal.rateAmount, + service.description?.title || '', + service.description?.startDate, + service.description?.expectedEndDate, + ); + const metaEvidenceCid = await postToIPFS(JSON.stringify(metaEvidence)); + await validateProposal( talentLayerClient, publicClient, proposal.service.id, proposal.id, proposal.rateToken.address, + metaEvidenceCid, ); setShow(false); }; @@ -203,16 +228,14 @@ function ValidateProposalModal({ proposal, account }: { proposal: IProposal; acc {isProposalUseEth && ethBalance && (

- {ethBalance.formatted} ETH + {ethBalance.formatted} MATIC

0 - ? 'bg-redpraha' - : 'bg-red-500' + isProposalUseEth && hasEnoughBalance() ? 'bg-green-500' : 'bg-red-500' } p-1 text-xs font-medium text-white rounded-full`}> - {(isProposalUseEth && hasEnoughBalance()) || ethBalance.value > 0 ? ( + {isProposalUseEth && hasEnoughBalance() ? ( ) : ( diff --git a/src/components/ProposalItem.tsx b/src/components/ProposalItem.tsx index db773146..b0ace641 100644 --- a/src/components/ProposalItem.tsx +++ b/src/components/ProposalItem.tsx @@ -65,7 +65,7 @@ function ProposalItem({ proposal }: { proposal: IProposal }) { {renderTokenAmount(proposal.rateToken, proposal.rateAmount)}

{account && isBuyer && proposal.status === ProposalStatusEnum.Pending && ( - + )}
{account && diff --git a/src/components/ServiceDetail.tsx b/src/components/ServiceDetail.tsx index 84f3d094..d0e40443 100644 --- a/src/components/ServiceDetail.tsx +++ b/src/components/ServiceDetail.tsx @@ -6,7 +6,7 @@ import usePaymentsByService from '../hooks/usePaymentsByService'; import useProposalsByService from '../hooks/useProposalsByService'; import useReviewsByService from '../hooks/useReviewsByService'; import ContactButton from '../modules/Messaging/components/ContactButton'; -import { IService, ProposalStatusEnum, ServiceStatusEnum } from '../types'; +import { IService, ProposalStatusEnum, ServiceStatusEnum, TransactionStatusEnum } from '../types'; import { renderTokenAmountFromConfig } from '../utils/conversion'; import { formatDate } from '../utils/dates'; import PaymentModal from './Modal/PaymentModal'; @@ -16,6 +16,7 @@ import ReviewItem from './ReviewItem'; import ServiceStatus from './ServiceStatus'; import Stars from './Stars'; import { useChainId } from '../hooks/useChainId'; +import { ZERO_ADDRESS } from '../utils/constant'; function ServiceDetail({ service }: { service: IService }) { const chainId = useChainId(); @@ -101,7 +102,7 @@ function ServiceDetail({ service }: { service: IService }) { ))}

- +
{!isBuyer && service.status == ServiceStatusEnum.Opened && ( @@ -130,6 +131,19 @@ function ServiceDetail({ service }: { service: IService }) { {account && (isBuyer || isSeller) && service.status !== ServiceStatusEnum.Opened && ( )} + {account && + service.status !== ServiceStatusEnum.Opened && + validatedProposal && + service.transaction.arbitrator !== ZERO_ADDRESS && ( + + {service.transaction.status === TransactionStatusEnum.NoDispute + ? 'Raise dispute' + : 'View dispute'} + + )}
diff --git a/src/components/TimeoutCountDown.tsx b/src/components/TimeoutCountDown.tsx new file mode 100644 index 00000000..cd7c578c --- /dev/null +++ b/src/components/TimeoutCountDown.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from 'react'; + +interface TimeLeft { + days: number; + hours: number; + minutes: number; + seconds: number; +} +function TimeOutCountDown({ targetDate }: { targetDate: number }) { + const [timeLeft, setTimeLeft] = useState(); + const calculateTimeLeft = (): TimeLeft => { + const difference = targetDate - Date.now(); + let timeLeft: TimeLeft = { days: 0, hours: 0, minutes: 0, seconds: 0 }; + if (difference > 0) { + timeLeft = { + days: Math.floor(difference / (1000 * 60 * 60 * 24)), + hours: Math.floor((difference / (1000 * 60 * 60)) % 24), + minutes: Math.floor((difference / 1000 / 60) % 60), + seconds: Math.floor((difference / 1000) % 60), + }; + } + return timeLeft; + }; + + useEffect(() => { + const timer = setTimeout(() => { + setTimeLeft(calculateTimeLeft()); + }, 1000); + + return () => clearTimeout(timer); + }); + + return ( + <> +

+ Timeout: +

+
+ {timeLeft && timeLeft.days !== 0 && ( +
+
{timeLeft.days}
+
days
+
+ )} + + {timeLeft && (timeLeft.hours !== 0 || timeLeft.days !== 0) && ( +
+
+
{timeLeft.hours}
+
hours
+
+ )} + + {timeLeft && (timeLeft.minutes !== 0 || timeLeft.hours !== 0 || timeLeft.days !== 0) && ( +
+
{timeLeft.minutes}
+
min
+
+ )} + + {timeLeft?.days === 0 && ( +
+
{timeLeft.seconds}
+
sec
+
+ )} +
+ {!timeLeft &&

Fee timeout passed

} + + ); +} + +export default TimeOutCountDown; \ No newline at end of file diff --git a/src/contracts/acceptProposal.tsx b/src/contracts/acceptProposal.tsx index 045d955d..ee10c131 100644 --- a/src/contracts/acceptProposal.tsx +++ b/src/contracts/acceptProposal.tsx @@ -5,15 +5,13 @@ import { Address, PublicClient } from 'viem'; import { ZERO_ADDRESS } from '../utils/constant'; import { TalentLayerClient } from '@talentlayer/client'; -// TODO: need to generate this json duynamically and post it to IPFS to be use for dispute resolution -export const metaEvidenceCid = 'QmQ2hcACF6r2Gf8PDxG4NcBdurzRUopwcaYQHNhSah6a8v'; - export const validateProposal = async ( talentLayerClient: TalentLayerClient, publicClient: PublicClient, serviceId: string, proposalId: string, rateToken: Address, + metaEvidenceCid: string, ): Promise => { try { if (rateToken === ZERO_ADDRESS) { diff --git a/src/contracts/disputes.tsx b/src/contracts/disputes.tsx new file mode 100644 index 00000000..654ba369 --- /dev/null +++ b/src/contracts/disputes.tsx @@ -0,0 +1,106 @@ +import TalentLayerEscrow from './ABI/TalentLayerEscrow.json'; +import { toast } from 'react-toastify'; +import TransactionToast from '../components/TransactionToast'; +import { NextRouter } from 'next/router'; +import { createMultiStepsTransactionToast } from '../utils/toast'; +import { getContract } from 'viem'; +import { PublicClient, WalletClient } from 'wagmi'; +import { useConfig } from '../hooks/useConfig'; +import { Config } from '../config'; + +export const getEscrowContract = (walletClient: WalletClient,config: Config) => { + return getContract({ + address: config.contracts.talentLayerEscrow, + abi: TalentLayerEscrow.abi, + walletClient + }); +}; +export const payArbitrationFee = async ( + walletClient: WalletClient, + publicClient: PublicClient, + arbitrationFee: BigInt, + isSender: boolean, + transactionId: string, + router: NextRouter, + config: Config +) => { + if (walletClient) { + const contract = getEscrowContract(walletClient,config); + try { + const tx = isSender + ? await contract.write.payArbitrationFeeBySender([transactionId], { value: arbitrationFee }) + : await contract.write.payArbitrationFeeByReceiver([transactionId], { value: arbitrationFee }); + const receipt = await toast.promise(publicClient.waitForTransactionReceipt({ hash: tx }), { + pending: { + render() { + return ( + + ); + }, + }, + success: 'Arbitration fees have been paid', + error: 'An error occurred while paying arbitration fees', + }); + if (receipt.status !== 'success') { + throw new Error('Transaction failed'); + } + router.reload(); + } catch (error) { + console.error(error); + } + } +}; +export const arbitrationFeeTimeout = async ( + walletClient: WalletClient, + publicClient: PublicClient, + transactionId: string, + router: NextRouter, + config: Config +) => { + if (walletClient) { + const contract = getEscrowContract(walletClient,config); + try { + const tx = await contract.write.arbitrationFeeTimeout([transactionId]); + const receipt = await toast.promise(publicClient.waitForTransactionReceipt({ hash: tx }), { + pending: { + render() { + return ; + }, + }, + success: 'The dispute has been timed-out', + error: 'An error occurred while calling timeout', + }); + if (receipt.status !== 'success') { + throw new Error('Transaction failed'); + } + router.reload(); + } catch (error) { + console.error(error); + } + } +}; + +export const submitEvidence = async ( + walletClient: WalletClient, + publicClient: PublicClient, + userId: string, + transactionId: string, + evidenceCid: string, + chainId: number, + config: Config +) => { + const contract = getEscrowContract(walletClient,config); + const tx = await contract.write.submitEvidence([userId, transactionId, evidenceCid]); + await createMultiStepsTransactionToast( + chainId, + { + pending: 'Submitting evidence...', + success: 'Your evidence has been submitted', + error: 'Your evidence has been submitted', + }, + publicClient, + tx, + 'evidence', + evidenceCid, + ); +}; \ No newline at end of file diff --git a/src/hooks/useArbitrationCost.ts b/src/hooks/useArbitrationCost.ts new file mode 100644 index 00000000..0594bcaa --- /dev/null +++ b/src/hooks/useArbitrationCost.ts @@ -0,0 +1,36 @@ +import { useState, useEffect } from 'react'; +import TalentLayerArbitrator from '../contracts/ABI/TalentLayerArbitrator.json'; +import { useChainId, usePublicClient } from 'wagmi'; +import { ZERO_ADDRESS } from '../utils/constant'; +import { Address } from 'viem'; + +const useArbitrationCost = (arbitratorAddress: Address | undefined): BigInt | null => { + const [arbitrationCost, setArbitrationCost] = useState(null); + const chainId = useChainId(); + const publicClient = usePublicClient({ chainId }); + useEffect(() => { + const fetchData = async () => { + try { + if (publicClient && arbitratorAddress && arbitratorAddress !== ZERO_ADDRESS) { + const response: any = await publicClient.readContract({ + address: arbitratorAddress, + abi: TalentLayerArbitrator.abi, + functionName: 'arbitrationPrice', + args: [process.env.NEXT_PUBLIC_PLATFORM_ID], + }); + if (response) { + setArbitrationCost(response); + } + } + } catch (err: any) { + // eslint-disable-next-line no-console + console.error(err); + } + }; + fetchData(); + }, [arbitratorAddress]); + + return arbitrationCost; +}; + +export default useArbitrationCost; \ No newline at end of file diff --git a/src/hooks/useEvidences.ts b/src/hooks/useEvidences.ts new file mode 100644 index 00000000..e4131d06 --- /dev/null +++ b/src/hooks/useEvidences.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from 'react'; +import { IEvidence } from '../types'; +import { getEvidencesTransactionId } from '../queries/evidences'; +import { useChainId } from './useChainId'; + +const useEvidences = (transactionID?: string): IEvidence[] => { + const [evidences, setEvidences] = useState([]); + const chainId = useChainId(); + useEffect(() => { + const fetchData = async () => { + try { + if (!transactionID) return; + const response = await getEvidencesTransactionId(chainId,transactionID); + + if (response?.data?.data?.evidences) { + setEvidences(response.data.data.evidences); + } + } catch (error: any) { + // eslint-disable-next-line no-console + console.error(error); + } + }; + fetchData(); + }, [transactionID]); + + return evidences; +}; + +export default useEvidences; \ No newline at end of file diff --git a/src/hooks/useIpfsFile.ts b/src/hooks/useIpfsFile.ts new file mode 100644 index 00000000..3b4adec4 --- /dev/null +++ b/src/hooks/useIpfsFile.ts @@ -0,0 +1,28 @@ +import { useState, useEffect } from 'react'; +import { readFileFromIpfs } from '../utils/ipfs'; + +const useIpfsFile = (cid: false | string): any => { + const [file, setFile] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + if (!cid) { + return; + } + const response = await readFileFromIpfs(cid); + if (response) { + setFile(response); + } + } catch (err: any) { + // eslint-disable-next-line no-console + console.error(err); + } + }; + fetchData(); + }, [cid]); + + return file; +}; + +export default useIpfsFile; \ No newline at end of file diff --git a/src/hooks/useIpfsJsonData.ts b/src/hooks/useIpfsJsonData.ts new file mode 100644 index 00000000..4045fa61 --- /dev/null +++ b/src/hooks/useIpfsJsonData.ts @@ -0,0 +1,28 @@ +import { useState, useEffect } from 'react'; +import { readFromIpfs } from '../utils/ipfs'; + +const useIpfsJsonData = (cid: false | string): any => { + const [file, setFile] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + if (!cid) { + return; + } + const response = await readFromIpfs(cid); + if (response) { + setFile(response); + } + } catch (err: any) { + // eslint-disable-next-line no-console + console.error(err); + } + }; + fetchData(); + }, [cid]); + + return file; +}; + +export default useIpfsJsonData; \ No newline at end of file diff --git a/src/hooks/useTransactionsById.ts b/src/hooks/useTransactionsById.ts new file mode 100644 index 00000000..04cd4e81 --- /dev/null +++ b/src/hooks/useTransactionsById.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; +import { ITransaction } from '../types'; +import { getTransactionById } from '../queries/transactions'; +import { useChainId } from './useChainId'; + +const useTransactionsById = (id?: string | undefined): ITransaction | undefined => { + const [transaction, setTransaction] = useState(); + const chainId = useChainId(); + useEffect(() => { + const fetchData = async () => { + if (id) { + try { + const response = await getTransactionById(chainId,id); + if (response?.data?.data?.transactions[0]) { + setTransaction(response.data.data.transactions[0]); + } + } catch (error: any) { + // eslint-disable-next-line no-console + console.error(error); + } + } else { + setTransaction(undefined); + } + }; + fetchData(); + }, [id]); + + return transaction; +}; + +export default useTransactionsById; \ No newline at end of file diff --git a/src/modules/Disputes/components/DisputeStatusCard.tsx b/src/modules/Disputes/components/DisputeStatusCard.tsx new file mode 100644 index 00000000..781d7ca3 --- /dev/null +++ b/src/modules/Disputes/components/DisputeStatusCard.tsx @@ -0,0 +1,73 @@ +import DisputeStatusDetail from './DisputeStatusDetail'; +import { ITransaction, IUser, TransactionStatusEnum } from '../../../types'; +import TimeOutCountDown from '../../../components/TimeoutCountDown'; +import DisputeButton from '../../../components/DisputeButton'; + +function DisputeStatusCard({ + transaction, + user, + arbitrationFee, +}: { + transaction: ITransaction; + user: IUser; + arbitrationFee: BigInt; +}) { + const isSender = (): boolean => { + return !!user && !!transaction && user.id === transaction.sender.id; + }; + + const isReceiver = (): boolean => { + return !!user && !!transaction && user.id === transaction.receiver.id; + }; + const getTargetDate = () => { + if ( + isSender() && + transaction && + (transaction.senderFeePaidAt || transaction.receiverFeePaidAt) + ) { + return ( + (Number(transaction.senderFeePaidAt) + Number(transaction.arbitrationFeeTimeout)) * 1000 + ); + } + + if ( + isReceiver() && + transaction && + (transaction.senderFeePaidAt || transaction.receiverFeePaidAt) + ) { + return ( + (Number(transaction.senderFeePaidAt || transaction.receiverFeePaidAt) + + Number(transaction.arbitrationFeeTimeout)) * + 1000 + ); + } + return 0; + }; + + return ( + <> + +
+
+ {(transaction.status === TransactionStatusEnum.WaitingReceiver || + transaction.status === TransactionStatusEnum.WaitingSender) && ( + + )} +
+ Date.now()} + /> + {transaction && transaction.ruling && ( + + {transaction.ruling === 1 ? 'Sender Wins' : 'Receiver Wins'} + + )} +
+ + ); +} + +export default DisputeStatusCard; \ No newline at end of file diff --git a/src/modules/Disputes/components/DisputeStatusDetail.tsx b/src/modules/Disputes/components/DisputeStatusDetail.tsx new file mode 100644 index 00000000..b0a0de06 --- /dev/null +++ b/src/modules/Disputes/components/DisputeStatusDetail.tsx @@ -0,0 +1,47 @@ +import { ITokenFormattedValues, ITransaction } from '../../../types'; +import { formatRateAmount } from '../../../utils/conversion'; + +function DisputeStatusDetail({ + transaction, + arbitrationFee, +}: { + transaction: ITransaction; + arbitrationFee: BigInt | null; +}) { + return ( +
+

+ Status: {transaction?.status} +

+

+ Arbitration fee:{' '} + {arbitrationFee && + transaction?.token && + formatRateAmount( + arbitrationFee.toString(), + transaction?.token.address, + transaction?.token.decimals, + ).exactValue}{' '} + MATIC +

+
+ Buyer fee:{' '} + {transaction?.senderFeePaidAt ? ( + Paid + ) : ( + Not paid + )} +
+

+ Seller fee:{' '} + {transaction?.receiverFeePaidAt ? ( + Paid + ) : ( + Not paid + )} +

+
+ ); +} + +export default DisputeStatusDetail; \ No newline at end of file diff --git a/src/modules/Disputes/components/EvidenceDetails.tsx b/src/modules/Disputes/components/EvidenceDetails.tsx new file mode 100644 index 00000000..7045ec2c --- /dev/null +++ b/src/modules/Disputes/components/EvidenceDetails.tsx @@ -0,0 +1,52 @@ +import MetaEvidenceModal from './MetaEvidenceModal'; +import useEvidences from '../../../hooks/useEvidences'; +import { IProposal, ITransaction } from '../../../types'; +import Evidences from './Evidences'; +import { formatUnits } from 'viem'; + +function EvidenceDetails({ + transaction, + proposal, +}: { + transaction: ITransaction; + proposal: IProposal; +}) { + const evidences = useEvidences(transaction.id); + + const buyerEvidences = evidences.filter( + evidence => evidence.party.id === proposal?.service.buyer.id, + ); + const sellerEvidences = evidences.filter(evidence => evidence.party.id === proposal?.seller.id); + return ( +
+ + + {transaction.receiver && proposal.service && proposal.description && proposal.description.video_url && ( +

+ Meta evidence: + +

+ )} +
+ ); +} + +export default EvidenceDetails; diff --git a/src/modules/Disputes/components/EvidenceModal.tsx b/src/modules/Disputes/components/EvidenceModal.tsx new file mode 100644 index 00000000..bdaeb526 --- /dev/null +++ b/src/modules/Disputes/components/EvidenceModal.tsx @@ -0,0 +1,84 @@ +import { useState } from 'react'; + +interface IMetaEvidenceModalProps { + id: string; + partyHandle: string; + fileHash: string; + fileTypeExtension: string; + name: string; + description: string; +} + +function EvidenceModal({ + fileHash, + partyHandle, + fileTypeExtension, + name, + description, + id, +}: IMetaEvidenceModalProps) { + const [show, setShow] = useState(false); + return ( + <> +
+

setShow(true)} className={'cursor-pointer hover:underline'}> + {name} +

+
+
+
+
+
+

+ Evidence {' : '} + {name} +

+ +
+
+

+ {partyHandle} Submitted the following evidence as a{' '} + {fileTypeExtension} file. Find here its description and access the associated file + on ipfs. +

+
+
+
+

Description

+

{description}

+
+ + See evidence file + +
+
+
+
+ + ); +} + +export default EvidenceModal; \ No newline at end of file diff --git a/src/modules/Disputes/components/Evidences.tsx b/src/modules/Disputes/components/Evidences.tsx new file mode 100644 index 00000000..13fe2f79 --- /dev/null +++ b/src/modules/Disputes/components/Evidences.tsx @@ -0,0 +1,37 @@ +import EvidenceModal from './EvidenceModal'; +import { IEvidence } from '../../../types'; + +function Evidences({ + evidences, + partyHandle, + title, +}: { + evidences: IEvidence[]; + title: string; + partyHandle?: string; +}) { + return ( + <> +

{title}

+

+ {evidences.length > 0 && + evidences.map(evidence => { + return ( + evidence.description && ( + + ) + ); + })} +

+ + ); +} + +export default Evidences; \ No newline at end of file diff --git a/src/modules/Disputes/components/FileDropper.tsx b/src/modules/Disputes/components/FileDropper.tsx new file mode 100644 index 00000000..6f7dc6b0 --- /dev/null +++ b/src/modules/Disputes/components/FileDropper.tsx @@ -0,0 +1,100 @@ +import React, { ChangeEvent, useRef } from 'react'; +import { useFormikContext } from 'formik'; + +const FileDropper = ({ + fileSelected, + setFileSelected, +}: { + fileSelected: File | undefined; + setFileSelected: React.Dispatch>; +}) => { + const formikProps = useFormikContext(); + const fileInputRef = useRef(); + + const handleDragEnter = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + const handleDragLeave = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + const file = [...event.dataTransfer.files][0]; + formikProps.setFieldValue('file', file); + + setFileSelected(file); + }; + + const handleFileChange = (e: ChangeEvent) => { + if (e.target.files) { + const file = e.target.files[0]; + formikProps.setFieldValue('file', file); + + setFileSelected(file); + } + }; + + const removeFile = () => { + setFileSelected(undefined); + formikProps.setFieldValue('file', null); + }; + + return !fileSelected ? ( + <> + Evidence +
) => handleDrop(e)} + onDragOver={(e: React.DragEvent) => handleDragOver(e)} + onDragEnter={(e: React.DragEvent) => handleDragEnter(e)} + onDragLeave={(e: React.DragEvent) => handleDragLeave(e)} + ref={fileInputRef}> + + Drop your file here + Or click to upload +
+ + ) : ( + <> +

Your file

+
+
+
+ {fileSelected.name} +
+ +
+
+ + ); +}; + +export default FileDropper; \ No newline at end of file diff --git a/src/modules/Disputes/components/MetaEvidenceModal.tsx b/src/modules/Disputes/components/MetaEvidenceModal.tsx new file mode 100644 index 00000000..4e548902 --- /dev/null +++ b/src/modules/Disputes/components/MetaEvidenceModal.tsx @@ -0,0 +1,189 @@ +import { Dispatch, SetStateAction, useState } from 'react'; +import { IService, IToken, IUser } from '../../../types'; + +interface ProposalData { + about: string; + rateToken: string | number; + rateAmount: string | number; + expirationDate: string | number; + videoUrl: string; +} +interface IMetaEvidenceModalProps { + conditionsValidated?: boolean; + setConditionsValidated?: Dispatch>; + serviceData: IService; + token: IToken; + seller: IUser; + proposalData?: ProposalData; +} + +function MetaEvidenceModal({ + conditionsValidated, + setConditionsValidated, + serviceData, + token, + proposalData, + seller, +}: IMetaEvidenceModalProps) { + const [show, setShow] = useState(false); + return ( + <> + {setConditionsValidated ? ( +
+ setConditionsValidated(!conditionsValidated)} + className='ml-2 w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600' + /> + +
+ ) : ( +
+ +
+ )} + +
+
+
+
+

Dispute Conditions

+ +
+
+

Information

+

+ The TalentLayer protocol has implemented a decentralized dispute system should any + issue arise during this service. Should any of the parties involved in this service + pretend that the contractual agreement has not been respected, they can raise a + dispute. The dispute will be reviewed by the jurors of the arbitrator selected by + the buyer. A dispute is raised by paying arbitrator fees, and each party can provide + as many evidence as they wish. Once a party has paid the arbitrator fees, the other + party has a certain amount of time (decided by the arbitrator) to pay the arbitrator + fees as well. If the other party does not pay the arbitrator fees, the party that + paid the arbitrator fees first can timeout the dispute and be declared as winner. + Once a dispute has been reviewed by the jurors, and the jurors have voted in favor + of one party, this party will be refunded the arbitrator fees and will be paid (in + case of a seller win) or reimbursed (in case of a seller win) the remaining amount + of the service in escrow. +

+

+ The platform {serviceData.platform?.name} has chosen as arbitrator for potential + dispute resolutions: {serviceData.platform?.arbitrator} Please review carefully the + service & proposal conditions & details, as they will set a context to potential + future disputes, should any arise. +

+
+
+
+

Title

+
+
+

+ The TalentLayer user {serviceData.buyer.handle} posted the following service:{' '} + {serviceData?.description?.title} to which the TalentLayer + user {seller.handle} submitted the following proposal +

+
+
+

Description

+

+ The TalentLayer user {serviceData.buyer.handle} agreed to complete the service + provided by {seller.handle} in the way described in the proposal below. The + completion of this agreement shall result in the payment using the token which + ethereum address is the following {proposalData?.rateToken} for an amount of:{' '} + + {proposalData?.rateAmount as string} {token?.symbol} + + . +

+
+ {/*
*/} +

+ Service description: +

+
+

+ {serviceData?.description?.about} +

+
+

+ Proposal description: +

+
+

{proposalData?.about}

+ {/*
*/} +
+
+

Parties

+
+
+

+ Buyer: + {serviceData?.buyer.handle} : {serviceData?.buyer.address} +

+

+ Seller: + {seller.handle} : {seller.address} +

+
+
+

Ruling

+
+

+ The abritrator can rule in favor of either party, resulting in the following + outcomes: +

+

+
+ - Buyer wins: {serviceData?.buyer.handle} will be refunded the + remaining amount in escrow for the service with id: {serviceData.id}, payable + with the token {proposalData?.rateToken} +
+ - Seller wins: {seller.handle} will be paid the remaining + amount in escrow for the service with id: {serviceData.id}, payable with the + token {proposalData?.rateToken} +

+
+
+
+
+
+
+ + ); +} + +export default MetaEvidenceModal; \ No newline at end of file diff --git a/src/modules/Disputes/utils/dispute.ts b/src/modules/Disputes/utils/dispute.ts new file mode 100644 index 00000000..20345420 --- /dev/null +++ b/src/modules/Disputes/utils/dispute.ts @@ -0,0 +1,92 @@ +import { IERC1497Evidence, IMetaEvidence } from './types'; + +export const generateMetaEvidence = ( + serviceDescription: string, + proposalDescription: string, + buyerHandle: string, + sellerHandle: string, + tokenAddress: string, + tokenSymbol: string, + tokenAmount: string, + title: string, + startDate?: string, + expectedEndDate?: string, +): IMetaEvidence => { + //TODO generate fileUriHash + //TODO mention users addresses somewhere ? + + return { + fileURI: '', + fileHash: '', + fileTypeExtension: 'pdf', + category: 'Escrow', + title: title, + description: generateMetaEvidenceDescription( + serviceDescription, + proposalDescription, + buyerHandle, + sellerHandle, + tokenAddress, + tokenSymbol, + tokenAmount, + startDate, + expectedEndDate, + ), + aliases: { + ['buyer']: buyerHandle, + ['seller']: sellerHandle, + }, + question: + 'Did the seller deliver what was agreed on and deserves to be paid, or does the buyer deserve to be reimbursed ?', + rulingOptions: { + type: 'single-select', + precision: 1, + titles: ['Reimburse Buyer', 'Release funds to Seller'], + descriptions: ['Select to reimburse the buyer', 'Select to release funds to the seller'], + }, + evidenceDisplayInterfaceURI: '', + evidenceDisplayInterfaceHash: '', + dynamicScriptURI: '', + dynamicScriptHash: '', + }; +}; + +const generateMetaEvidenceDescription = ( + buyerVersion: string, + sellerVersion: string, + buyerHandle: string, + sellerHandle: string, + tokenAddress: string, + tokenSymbol: string, + tokenAmount: string, + startDate?: string, + expectedEndDate?: string, +): string => { + let description = `The TalentLayer user ${buyerHandle} has posted a service request, to which the user ${sellerHandle} has submitted a proposal for ${tokenAmount} ${tokenSymbol} tokens. + The service description is: ${buyerVersion}. + The proposal description is: ${sellerVersion}. + The buyer has put in escrow ${tokenAmount} ${tokenSymbol} tokens to the contract address ${tokenAddress}. + The amount in escrow will be released to the seller upon completion of the terms described in the proposal, or reimbursed to the buyer.`; + + startDate && expectedEndDate + ? (description = + description + + ` The service is expected to start on ${startDate} and end on ${expectedEndDate}.`) + : ''; + return description; +}; + +export const generateEvidence = ( + title: string, + description: string, + evidenceCid: string, + fileExtension: string, +): IERC1497Evidence => { + return { + fileUri: `/ipfs/${evidenceCid}`, + fileHash: evidenceCid, + fileTypeExtension: fileExtension, + name: title, + description: description, + }; +}; \ No newline at end of file diff --git a/src/modules/Disputes/utils/types.ts b/src/modules/Disputes/utils/types.ts new file mode 100644 index 00000000..2c2f0f73 --- /dev/null +++ b/src/modules/Disputes/utils/types.ts @@ -0,0 +1,31 @@ +export type IMetaEvidence = { + fileURI: string; + fileHash: string; + fileTypeExtension: string; + category: string; + title: string; + description: string; + aliases: { + [string: string]: string; + }; + question: string; + rulingOptions: { + type: string; + precision: number; + titles: string[]; + descriptions: string[]; + }; + evidenceDisplayInterfaceURI: string; + evidenceDisplayInterfaceHash: string; + dynamicScriptURI: string; + dynamicScriptHash: string; + }; + + export type IERC1497Evidence = { + fileUri: string; + fileHash: string; + fileTypeExtension: string; + name: string; + description: string; + }; + \ No newline at end of file diff --git a/src/pages/admin/dispute.tsx b/src/pages/admin/dispute.tsx index 07b2da8d..9b0703b4 100644 --- a/src/pages/admin/dispute.tsx +++ b/src/pages/admin/dispute.tsx @@ -53,7 +53,7 @@ function AdminDispute() { if (config) { availableArbitrators = [ { - value: config.contracts.talentLayerArbitrator, + value: '0xEd016d125211C719d59824A05DABE0aA1280605f', label: 'TalentLayer Arbitrator', }, { value: ZERO_ADDRESS, label: 'None' }, diff --git a/src/pages/dispute/[id].tsx b/src/pages/dispute/[id].tsx new file mode 100644 index 00000000..70cbabc4 --- /dev/null +++ b/src/pages/dispute/[id].tsx @@ -0,0 +1,101 @@ +import { useContext } from 'react'; +import TalentLayerContext from '../../context/talentLayer'; +import { useRouter } from 'next/router'; +import Loading from '../../components/Loading'; +import useProposalById from '../../hooks/useProposalById'; +import EvidenceForm from '../../components/Form/EvidenceForm'; +import { TransactionStatusEnum } from '../../types'; +import useTransactionsById from '../../hooks/useTransactionsById'; +import DisputeButton from '../../components/DisputeButton'; +import useArbitrationCost from '../../hooks/useArbitrationCost'; +import Steps from '../../components/Steps'; +import DisputeStatusCard from '../../modules/Disputes/components/DisputeStatusCard'; +import EvidenceDetails from '../../modules/Disputes/components/EvidenceDetails'; +import { ZERO_ADDRESS } from '../../utils/constant'; + +function Dispute() { + const router = useRouter(); + const { id: proposalId } = router.query; + const { account, user } = useContext(TalentLayerContext); + const proposal = useProposalById(proposalId as string); + const transactionId = proposal?.service?.transaction?.id; + const transaction = useTransactionsById(transactionId as string); + const arbitrationFee = useArbitrationCost(transaction?.arbitrator); + + if (!transactionId) { + return ; + } + + if (!user) { + return ; + } + return ( +
+

+ Raise a dispute +

+ {account?.isConnected && + user?.id !== proposal?.service.buyer.id && + user?.id !== proposal?.seller.id ? ( +
+

You are not related to this transaction

+
+ ) : account?.isConnected && + transaction && + (!transaction?.arbitrator || transaction?.arbitrator === ZERO_ADDRESS) ? ( +
+

+ Your platform has not implemented an arbitrator, you cannot raise a dispute yet :( +

+
+ ) : ( + user && ( + <> +
+

Dispute details:

+
+
+

+ Service: {proposal.service.description?.title} +

+ {transaction && } +
+ {transaction && arbitrationFee && ( +
+ +
+ )} +
+ {account?.isConnected && + transactionId && + transaction?.status !== TransactionStatusEnum.Resolved && ( + + )} + {transaction?.status === TransactionStatusEnum.NoDispute && arbitrationFee && ( +
+ +
+ )} +
+ + ) + )} +
+ ); +} + +export default Dispute; \ No newline at end of file diff --git a/src/queries/evidences.ts b/src/queries/evidences.ts new file mode 100644 index 00000000..b1821295 --- /dev/null +++ b/src/queries/evidences.ts @@ -0,0 +1,25 @@ +import { processRequest } from '../utils/graphql'; + +export const getEvidencesTransactionId = (chainId: number,transactionId: string): Promise => { + let condition = `where: {transaction_: {id: "${transactionId}"}`; + condition += '}, orderBy: id, orderDirection: asc'; + const query = ` + { + evidences(${condition}) { + id + cid + party { + handle + id + } + description { + name + fileTypeExtension + description + fileHash + } + } + } + `; + return processRequest(chainId, query); +}; \ No newline at end of file diff --git a/src/queries/proposals.ts b/src/queries/proposals.ts index 8e84e70d..8482048d 100644 --- a/src/queries/proposals.ts +++ b/src/queries/proposals.ts @@ -109,6 +109,30 @@ export const getProposalById = (chainId: number, id: string): Promise => { } status expirationDate + seller { + id + } + service { + buyer { + handle + address + } + description { + about + title + } + transaction { + id + status + } + buyer { + id + } + platform { + name + arbitrator + } + } } } `; diff --git a/src/queries/services.ts b/src/queries/services.ts index e7fcaa5f..202425c8 100644 --- a/src/queries/services.ts +++ b/src/queries/services.ts @@ -18,6 +18,7 @@ const serviceQueryFields = ` cid transaction { id + status } buyer { id @@ -35,6 +36,12 @@ const serviceQueryFields = ` proposals { id } + platform { + name + arbitrator + arbitratorExtraData + arbitrationFeeTimeout + }, validatedProposal: proposals(where: {status: "Validated"}){ id, rateToken { diff --git a/src/queries/transactions.ts b/src/queries/transactions.ts new file mode 100644 index 00000000..fd6ef61b --- /dev/null +++ b/src/queries/transactions.ts @@ -0,0 +1,44 @@ +import { processRequest } from '../utils/graphql'; + +export const getTransactionById = (chainId:number, id: string): Promise => { + const query = ` + { + transactions (where: {id: "${id}"}) { + id + sender { + id + } + receiver { + id + handle + address + } + token { + decimals + symbol + } + arbitrationFeeTimeout + amount + disputeId + senderFee + receiverFee + lastInteraction + senderFeePaidAt + receiverFeePaidAt + arbitrator + status + ruling + evidences { + cid + party { + address + createdAt + handle + id + } + } + } + } + `; + return processRequest(chainId, query); +}; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 8aecef02..da7c7e23 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,8 +71,32 @@ export type IAccount = { // TODO: add the rest of the fields export type ITransaction = { id: string; + sender: IUser; + receiver: IUser; + token: IToken; + status: TransactionStatusEnum; + senderFee: number; + receiverFee: number; + lastInteraction: number; + senderFeePaidAt: number; + receiverFeePaidAt: number; + arbitrationFeeTimeout: number; + amount: number; + disputeId: number; + // arbitrator: string; + arbitrator: `0x${string}`; + ruling: number; + evidences: IEvidence[]; }; +export enum TransactionStatusEnum { + NoDispute = 'NoDispute', + WaitingSender = 'WaitingSender', + WaitingReceiver = 'WaitingReceiver', + DisputeCreated = 'DisputeCreated', + Resolved = 'Resolved', +} + export type IService = { id: string; status: ServiceStatusEnum; @@ -88,6 +112,7 @@ export type IService = { proposals: IProposal[]; validatedProposal: IProposal[]; description?: IServiceDetails; + }; export type IFeeType = { @@ -302,3 +327,19 @@ export type IUserGain = { token: IToken; totalGain: string; }; + +export type IEvidence = { + id: string; + transaction: ITransaction; + createdAt: string; + party: IUser; + cid: string; + description?: IEvidenceDetails; +}; + +export type IEvidenceDetails = { + name: string; + fileTypeExtension: string; + description: string; + fileHash: string; +}; \ No newline at end of file diff --git a/src/utils/conversion.ts b/src/utils/conversion.ts index 62805688..99a92f81 100644 --- a/src/utils/conversion.ts +++ b/src/utils/conversion.ts @@ -1,6 +1,7 @@ -import { formatUnits } from 'viem'; +import { formatEther, formatUnits } from 'viem'; import { getConfig } from '../config'; -import { IToken } from '../types'; +import { IToken, ITokenFormattedValues } from '../types'; +import { ZERO_ADDRESS } from './constant'; export const renderTokenAmount = (token: IToken, value: string): string => { const formattedValue = formatUnits(BigInt(value), token.decimals); @@ -21,3 +22,28 @@ export const renderTokenAmountFromConfig = ( const formattedValue = formatUnits(BigInt(value), config.tokens[tokenAddress].decimals); return `${formattedValue} ${symbol}`; }; + + +export const formatRateAmount = ( + rateAmount: string, + rateToken: string, + tokenDecimals: number, +): ITokenFormattedValues => { + if (rateToken === ZERO_ADDRESS) { + const valueInEther = formatEther(BigInt(rateAmount)); + const roundedValue = parseFloat(valueInEther).toFixed(2).toString(); + const exactValue = Number(valueInEther).toString(); + return { + roundedValue, + exactValue, + }; + } + + const valueInToken = formatUnits(BigInt(rateAmount), tokenDecimals); + const roundedValue = parseFloat(valueInToken).toFixed(2).toString(); + const exactValue = Number(valueInToken).toString(); + return { + roundedValue, + exactValue, + }; +}; \ No newline at end of file diff --git a/src/utils/ipfs.ts b/src/utils/ipfs.ts index 4fb00cae..1dab3ffa 100644 --- a/src/utils/ipfs.ts +++ b/src/utils/ipfs.ts @@ -35,3 +35,21 @@ export const IpfsIsSynced = async (cid: string): Promise => { }, 5000); }); }; + +export const readFromIpfs = async (cid: string): Promise => { + try { + const response = await fetch(process.env.NEXT_PUBLIC_IPFS_BASE_URL + cid); + return await response.json(); + } catch (error) { + console.error('IPFS error ', error); + } +}; + +export const readFileFromIpfs = async (cid: string): Promise => { + try { + const response = await fetch(process.env.NEXT_PUBLIC_IPFS_BASE_URL + cid); + return await response.arrayBuffer(); + } catch (error) { + console.error('IPFS error ', error); + } +}; \ No newline at end of file