diff --git a/ethos/.vscode/settings.json b/ethos/.vscode/settings.json index a643da7..019766d 100644 --- a/ethos/.vscode/settings.json +++ b/ethos/.vscode/settings.json @@ -69,7 +69,10 @@ ["ClassName \\=([^;]*);", "'([^']*)'"], ["ClassName \\=([^;]*);", "\"([^\"]*)\""], ["ClassName \\=([^;]*);", "\\`([^\\`]*)\\`"], - ["classNames=\\{([^}]*)\\}", "['\"`]([^'\"`]*)['\"`]"] + ["classNames=\\{([^}]*)\\}", "['\"`]([^'\"`]*)['\"`]"], + ["[a-zA-Z]+ClassName=\"([^\"]*)\""], + ["[a-zA-Z]+ClassName='([^']*)'"], + ["[a-zA-Z]+ClassName={['\"`]([^'\"`]*)['\"`]}"] ], "tailwindCSS.classAttributes": [ "class", diff --git a/ethos/.vscode/settings.json.rej b/ethos/.vscode/settings.json.rej new file mode 100644 index 0000000..32abd3a --- /dev/null +++ b/ethos/.vscode/settings.json.rej @@ -0,0 +1,13 @@ +diff a/ethos/.vscode/settings.json b/ethos/.vscode/settings.json (rejected hunks) +@@ -69,7 +69,10 @@ + ["ClassName \\=([^;]*);", "'([^']*)'"], + ["ClassName \\=([^;]*);", "\"([^\"]*)\""], + ["ClassName \\=([^;]*);", "\\`([^\\`]*)\\`"], +- ["classNames=\\{([^}]*)\\}", "['\"`]([^'\"`]*)['\"`]"] ++ ["classNames=\\{([^}]*)\\}", "['\"`]([^'\"`]*)['\"`]"], ++ ["[a-zA-Z]+ClassName=\"([^\"]*)\""], ++ ["[a-zA-Z]+ClassName='([^']*)'"], ++ ["[a-zA-Z]+ClassName={['\"`]([^'\"`]*)['\"`]}"] + ], + "tailwindCSS.classAttributes": [ + "class", diff --git a/ethos/package-lock.json b/ethos/package-lock.json index eed0e9d..4757d5c 100644 --- a/ethos/package-lock.json +++ b/ethos/package-lock.json @@ -21,37 +21,37 @@ "standalone/*" ], "devDependencies": { - "@dotenvx/dotenvx": "^1.26.1", - "@next/eslint-plugin-next": "^15.1.2", - "@stylistic/eslint-plugin-js": "^2.11.0", - "@tanstack/eslint-plugin-query": "^5.62.1", + "@dotenvx/dotenvx": "^1.32.0", + "@next/eslint-plugin-next": "^15.1.3", + "@stylistic/eslint-plugin-js": "^2.12.1", + "@tanstack/eslint-plugin-query": "^5.62.16", "@types/inquirer": "^9.0.7", - "@types/node": "^22.10.1", + "@types/node": "^22.10.2", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "@vitest/coverage-v8": "^2.1.8", - "concurrently": "^9.1.0", + "concurrently": "^9.1.1", "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-love": "^44.0.0", "eslint-config-prettier": "^9.1.0", - "eslint-import-resolver-typescript": "^3.6.3", + "eslint-import-resolver-typescript": "^3.7.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-n": "^16.6.2", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^6.6.0", - "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react": "^7.37.3", + "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-unicorn": "^56.0.1", "eslint-plugin-vitest": "^0.4.1", "glob": "^11.0.0", "husky": "^9.1.7", "inquirer": "^8.2.6", - "knip": "^5.39.0", + "knip": "^5.41.1", "open-cli": "^8.0.0", "patch-package": "^8.0.0", "picocolors": "^1.1.1", - "prettier": "3.4.1", + "prettier": "3.4.2", "serve": "^14.2.4", "ts-node": "^10.9.2", "tsx": "^4.19.1", @@ -100,23 +100,23 @@ } }, "node_modules/@amplitude/analytics-browser": { - "version": "2.11.10", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.11.10.tgz", - "integrity": "sha512-+bc+KvAvrFTQefmq+iGDuzdtPdJm9HXHPI58yTnHJeXhXDhneH8cGmT6dfL1SvW/W2kGvBdFXiZMoPuHcR0xKg==", + "version": "2.11.11", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.11.11.tgz", + "integrity": "sha512-AdpNNPwoNPezojeeU2ITcyqKcrrW8edVBHlCEvDNIXYkf5Y0i5Blbes3x6rgONsOeV2hx85trTXhhVkilWgHcg==", "dependencies": { - "@amplitude/analytics-client-common": "^2.3.6", + "@amplitude/analytics-client-common": "^2.3.7", "@amplitude/analytics-core": "^2.5.5", "@amplitude/analytics-remote-config": "^0.4.0", "@amplitude/analytics-types": "^2.8.4", "@amplitude/plugin-autocapture-browser": "^1.0.2", - "@amplitude/plugin-page-view-tracking-browser": "^2.3.6", + "@amplitude/plugin-page-view-tracking-browser": "^2.3.7", "tslib": "^2.4.1" } }, "node_modules/@amplitude/analytics-client-common": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.6.tgz", - "integrity": "sha512-4KAaqevqzYJIUzgjBmH80QDmYgzUAE0JpKz2s7w6cjeoX9DrB1gBa+RqWySULiUeIDfx+DTlhsZPMzFn+og17w==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.7.tgz", + "integrity": "sha512-HuwP2MFoeCTZWFIxkeYZOy5GP9ydjRO+n2KUMhHXTXGUx1M9vxIx1BUHsHKOZ4BZ5qEUTacgmznyc6uJJUiCWg==", "dependencies": { "@amplitude/analytics-connector": "^1.4.8", "@amplitude/analytics-core": "^2.5.5", @@ -193,11 +193,11 @@ } }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.6.tgz", - "integrity": "sha512-3NsXskUxIA9m4GedGOCIKOkQ1uGUhuYnP/DpvSXXosJEMRSi82vFxBRX7FnYT1AmV32Du2Z2626CEAEy5W4yeQ==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.7.tgz", + "integrity": "sha512-9LEzU33vpQ1OdPwVn0nwcCqPLkfK3P19hLmFTflx+aBM70TH9xCwvJL6nJ5eyc4kkmE9x7r0mRVnQIxaHfTxGg==", "dependencies": { - "@amplitude/analytics-client-common": "^2.3.6", + "@amplitude/analytics-client-common": "^2.3.7", "@amplitude/analytics-types": "^2.8.4", "tslib": "^2.4.1" } @@ -309,11 +309,11 @@ } }, "node_modules/@ant-design/colors": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.1.0.tgz", - "integrity": "sha512-MMoDGWn1y9LdQJQSHiCC20x3uZ3CwQnv9QMz6pCmJOrqdgM9YxsoVVY0wtrdXbmfSgnV0KNk6zi09NAhMR2jvg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.0.tgz", + "integrity": "sha512-bjTObSnZ9C/O8MB/B4OUtd/q9COomuJAR2SYfhxLyHvCKn4EKwCN3e+fWGMo7H5InAyV0wL17jdE9ALrdOW/6A==", "dependencies": { - "@ctrl/tinycolor": "^3.6.1" + "@ant-design/fast-color": "^2.0.6" } }, "node_modules/@ant-design/cssinjs": { @@ -2586,14 +2586,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@ctrl/tinycolor": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", - "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", - "engines": { - "node": ">=10" - } - }, "node_modules/@devnomic/marquee": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@devnomic/marquee/-/marquee-1.0.2.tgz", @@ -2614,11 +2606,10 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.26.1", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.26.1.tgz", - "integrity": "sha512-XsMk9WjHN3yZilBzwotWCBtA9Yc1R/4ca9zuXgAXshVr6YhKWVkNXbkm64Mlhw3SERHlCcrr4OVBd+B7B0Lhpw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.32.0.tgz", + "integrity": "sha512-oQaGYijYfQx6pY9D+FQ08gUOckF1R0RSVK7Jqk+Ma2RyeceoMIawQl1KoogRaJ12i0SmyVWhiGyQxDU01/k13g==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "commander": "^11.1.0", "dotenv": "^16.4.5", @@ -4205,10 +4196,6 @@ "resolved": "packages/analytics", "link": true }, - "node_modules/@ethos/attestation": { - "resolved": "packages/attestation", - "link": true - }, "node_modules/@ethos/blockchain-manager": { "resolved": "packages/blockchain-manager", "link": true @@ -5012,10 +4999,9 @@ } }, "node_modules/@google-cloud/firestore": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.10.0.tgz", - "integrity": "sha512-VFNhdHvfnmqcHHs6YhmSNHHxQqaaD64GwiL0c+e1qz85S8SWZPC2XFRf8p9yHRTF40Kow424s1KBU9f0fdQa+Q==", - "license": "Apache-2.0", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.0.tgz", + "integrity": "sha512-88uZ+jLsp1aVMj7gh3EKYH1aulTAMFAp8sH/v5a9w8q8iqSG27RiWLoxSAFr/XocZ9hGiWH1kEnBw+zl3xAgNA==", "optional": true, "dependencies": { "@opentelemetry/api": "^1.3.0", @@ -5958,7 +5944,6 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "license": "MIT", "optional": true, "funding": { "type": "opencollective", @@ -7313,11 +7298,10 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.2.tgz", - "integrity": "sha512-sgfw3+WdaYOGPKCvM1L+UucBmRfh8V2Ygefp7ELON0+0vY7uohQwXXnVWg3rY7mXDKharQR3o7uedpfvnU2hlQ==", + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.3.tgz", + "integrity": "sha512-oeP1vnc5Cq9UoOb8SYHAEPbCXMzOgG70l+Zfd+Ie00R25FOm+CCVNrcIubJvB1tvBgakXE37MmqSycksXVPRqg==", "dev": true, - "license": "MIT", "dependencies": { "fast-glob": "3.3.1" } @@ -7881,81 +7865,81 @@ } }, "node_modules/@nomicfoundation/edr": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.6.4.tgz", - "integrity": "sha512-YgrSuT3yo5ZQkbvBGqQ7hG+RDvz3YygSkddg4tb1Z0Y6pLXFzwrcEwWaJCFAVeeZxdxGfCgGMUYgRVneK+WXkw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.6.5.tgz", + "integrity": "sha512-tAqMslLP+/2b2sZP4qe9AuGxG3OkQ5gGgHE4isUuq6dUVjwCRPFhAOhpdFl+OjY5P3yEv3hmq9HjUGRa2VNjng==", "dev": true, "dependencies": { - "@nomicfoundation/edr-darwin-arm64": "0.6.4", - "@nomicfoundation/edr-darwin-x64": "0.6.4", - "@nomicfoundation/edr-linux-arm64-gnu": "0.6.4", - "@nomicfoundation/edr-linux-arm64-musl": "0.6.4", - "@nomicfoundation/edr-linux-x64-gnu": "0.6.4", - "@nomicfoundation/edr-linux-x64-musl": "0.6.4", - "@nomicfoundation/edr-win32-x64-msvc": "0.6.4" + "@nomicfoundation/edr-darwin-arm64": "0.6.5", + "@nomicfoundation/edr-darwin-x64": "0.6.5", + "@nomicfoundation/edr-linux-arm64-gnu": "0.6.5", + "@nomicfoundation/edr-linux-arm64-musl": "0.6.5", + "@nomicfoundation/edr-linux-x64-gnu": "0.6.5", + "@nomicfoundation/edr-linux-x64-musl": "0.6.5", + "@nomicfoundation/edr-win32-x64-msvc": "0.6.5" }, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-darwin-arm64": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.6.4.tgz", - "integrity": "sha512-QNQErISLgssV9+qia8sIjRANqtbW8snSDvjspixT/kSQ5ZSGxxctTg7x72wPSrcu8+EBEveIe5uqENIp5GH8HQ==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.6.5.tgz", + "integrity": "sha512-A9zCCbbNxBpLgjS1kEJSpqxIvGGAX4cYbpDYCU2f3jVqOwaZ/NU761y1SvuCRVpOwhoCXqByN9b7HPpHi0L4hw==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-darwin-x64": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.6.4.tgz", - "integrity": "sha512-cjVmREiwByyc9+oGfvAh49IAw+oVJHF9WWYRD+Tm/ZlSpnEVWxrGNBak2bd/JSYjn+mZE7gmWS4SMRi4nKaLUg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.6.5.tgz", + "integrity": "sha512-x3zBY/v3R0modR5CzlL6qMfFMdgwd6oHrWpTkuuXnPFOX8SU31qq87/230f4szM+ukGK8Hi+mNq7Ro2VF4Fj+w==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.6.4.tgz", - "integrity": "sha512-96o9kRIVD6W5VkgKvUOGpWyUGInVQ5BRlME2Fa36YoNsRQMaKtmYJEU0ACosYES6ZTpYC8U5sjMulvPtVoEfOA==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.6.5.tgz", + "integrity": "sha512-HGpB8f1h8ogqPHTyUpyPRKZxUk2lu061g97dOQ/W4CxevI0s/qiw5DB3U3smLvSnBHKOzYS1jkxlMeGN01ky7A==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-linux-arm64-musl": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.6.4.tgz", - "integrity": "sha512-+JVEW9e5plHrUfQlSgkEj/UONrIU6rADTEk+Yp9pbe+mzNkJdfJYhs5JYiLQRP4OjxH4QOrXI97bKU6FcEbt5Q==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.6.5.tgz", + "integrity": "sha512-ESvJM5Y9XC03fZg9KaQg3Hl+mbx7dsSkTIAndoJS7X2SyakpL9KZpOSYrDk135o8s9P9lYJdPOyiq+Sh+XoCbQ==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-linux-x64-gnu": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.6.4.tgz", - "integrity": "sha512-nzYWW+fO3EZItOeP4CrdMgDXfaGBIBkKg0Y/7ySpUxLqzut40O4Mb0/+quqLAFkacUSWMlFp8nsmypJfOH5zoA==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.6.5.tgz", + "integrity": "sha512-HCM1usyAR1Ew6RYf5AkMYGvHBy64cPA5NMbaeY72r0mpKaH3txiMyydcHibByOGdQ8iFLWpyUdpl1egotw+Tgg==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-linux-x64-musl": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.6.4.tgz", - "integrity": "sha512-QFRoE9qSQ2boRrVeQ1HdzU+XN7NUgwZ1SIy5DQt4d7jCP+5qTNsq8LBNcqhRBOATgO63nsweNUhxX/Suj5r1Sw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.6.5.tgz", + "integrity": "sha512-nB2uFRyczhAvWUH7NjCsIO6rHnQrof3xcCe6Mpmnzfl2PYcGyxN7iO4ZMmRcQS7R1Y670VH6+8ZBiRn8k43m7A==", "dev": true, "engines": { "node": ">= 18" } }, "node_modules/@nomicfoundation/edr-win32-x64-msvc": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.6.4.tgz", - "integrity": "sha512-2yopjelNkkCvIjUgBGhrn153IBPLwnsDeNiq6oA0WkeM8tGmQi4td+PGi9jAriUDAkc59Yoi2q9hYA6efiY7Zw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.6.5.tgz", + "integrity": "sha512-B9QD/4DSSCFtWicO8A3BrsnitO1FPv7axB62wq5Q+qeJ50yJlTmyeGY3cw62gWItdvy2mh3fRM6L1LpnHiB77A==", "dev": true, "engines": { "node": ">= 18" @@ -9372,11 +9356,10 @@ } }, "node_modules/@openzeppelin/hardhat-upgrades": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-3.6.0.tgz", - "integrity": "sha512-RuVuCciCfFOqCyKSJ2D4Zffp3hxhvXTn16JzTlD9cx3A7V/2d3JA75tpRHD7RVPic+dcSFIf+BZRWOHuhc2ayg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-3.8.0.tgz", + "integrity": "sha512-NwRk14ykTVilQqB0Vzd0vOWvUE8gpyn+SSwdzyHECRc5fuSSPDt/cIdadG9Hd6AKMPXfY+CS6G7q0+nDLng2Zw==", "dev": true, - "license": "MIT", "dependencies": { "@openzeppelin/defender-sdk-base-client": "^1.14.4", "@openzeppelin/defender-sdk-deploy-client": "^1.14.4", @@ -9843,9 +9826,9 @@ "dev": true }, "node_modules/@prisma/client": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.1.0.tgz", - "integrity": "sha512-AbQYc5+EJKm1Ydfq3KxwcGiy7wIbm4/QbjCKWWoNROtvy7d6a3gmAGkKjK0iUCzh+rHV8xDhD5Cge8ke/kiy5Q==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.1.tgz", + "integrity": "sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA==", "hasInstallScript": true, "engines": { "node": ">=18.18" @@ -9860,43 +9843,43 @@ } }, "node_modules/@prisma/debug": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.1.0.tgz", - "integrity": "sha512-0himsvcM4DGBTtvXkd2Tggv6sl2JyUYLzEGXXleFY+7Kp6rZeSS3hiTW9mwtUlXrwYbJP6pwlVNB7jYElrjWUg==" + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.1.tgz", + "integrity": "sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==" }, "node_modules/@prisma/engines": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.1.0.tgz", - "integrity": "sha512-GnYJbCiep3Vyr1P/415ReYrgJUjP79fBNc1wCo7NP6Eia0CzL2Ot9vK7Infczv3oK7JLrCcawOSAxFxNFsAERQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.2.1.tgz", + "integrity": "sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ==", "hasInstallScript": true, "dependencies": { - "@prisma/debug": "6.1.0", - "@prisma/engines-version": "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959", - "@prisma/fetch-engine": "6.1.0", - "@prisma/get-platform": "6.1.0" + "@prisma/debug": "6.2.1", + "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "@prisma/fetch-engine": "6.2.1", + "@prisma/get-platform": "6.2.1" } }, "node_modules/@prisma/engines-version": { - "version": "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959.tgz", - "integrity": "sha512-PdJqmYM2Fd8K0weOOtQThWylwjsDlTig+8Pcg47/jszMuLL9iLIaygC3cjWJLda69siRW4STlCTMSgOjZzvKPQ==" + "version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz", + "integrity": "sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ==" }, "node_modules/@prisma/fetch-engine": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.1.0.tgz", - "integrity": "sha512-asdFi7TvPlEZ8CzSZ/+Du5wZ27q6OJbRSXh+S8ISZguu+S9KtS/gP7NeXceZyb1Jv1SM1S5YfiCv+STDsG6rrg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.2.1.tgz", + "integrity": "sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A==", "dependencies": { - "@prisma/debug": "6.1.0", - "@prisma/engines-version": "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959", - "@prisma/get-platform": "6.1.0" + "@prisma/debug": "6.2.1", + "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", + "@prisma/get-platform": "6.2.1" } }, "node_modules/@prisma/get-platform": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.1.0.tgz", - "integrity": "sha512-ia8bNjboBoHkmKGGaWtqtlgQOhCi7+f85aOkPJKgNwWvYrT6l78KgojLekE8zMhVk0R9lWcifV0Pf8l3/15V0Q==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.2.1.tgz", + "integrity": "sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q==", "dependencies": { - "@prisma/debug": "6.1.0" + "@prisma/debug": "6.2.1" } }, "node_modules/@prisma/instrumentation": { @@ -9940,9 +9923,9 @@ } }, "node_modules/@privy-io/api-base": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@privy-io/api-base/-/api-base-1.4.0.tgz", - "integrity": "sha512-8Pm/8bx6WvNt8uLtYOOj9acYL+JjUJxeChlBEvSywmre1l5o8naK6J4SeAb5v8b8p4178VNI4AYhd+rFh4HCsA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@privy-io/api-base/-/api-base-1.4.1.tgz", + "integrity": "sha512-q98uQGVBIY5SBHjJWL/udpbxM9ISpZl8Lwwjd0p0XHSMJMOgEhS4GLjcO7l3clfNrqL0fAuinQaa+seCaYOzng==", "dependencies": { "zod": "^3.21.4" }, @@ -9952,10 +9935,9 @@ } }, "node_modules/@privy-io/js-sdk-core": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@privy-io/js-sdk-core/-/js-sdk-core-0.32.0.tgz", - "integrity": "sha512-ZTvGhGLYkY3Hlia/zAE5n/EdMHG8Y0oAJB4LaulM7duossTCiwp9bzP8gb2y1nyxAhsqdLITF3spP3PrdW/RAg==", - "license": "Apache-2.0", + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/@privy-io/js-sdk-core/-/js-sdk-core-0.36.1.tgz", + "integrity": "sha512-EPa8pYL+fh2Vj6s9ZvxowPiuNcqftXMydcAqGq974fz+ksjyB8kmTY0KHipifH7d8n6a+hqiOts9eE6ds3xyYg==", "dependencies": { "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", @@ -9963,8 +9945,8 @@ "@ethersproject/providers": "^5.7.2", "@ethersproject/transactions": "^5.7.0", "@ethersproject/units": "^5.7.0", - "@privy-io/api-base": "^1.4.0", - "@privy-io/public-api": "2.12.2", + "@privy-io/api-base": "^1.4.1", + "@privy-io/public-api": "2.15.8", "eventemitter3": "^5.0.1", "fetch-retry": "^5.0.6", "jose": "^4.15.5", @@ -9972,24 +9954,34 @@ "libphonenumber-js": "^1.10.44", "set-cookie-parser": "^2.6.0", "uuid": ">=8 <10" + }, + "peerDependencies": { + "permissionless": "^0.2.10", + "viem": "^2.21.36" + }, + "peerDependenciesMeta": { + "permissionless": { + "optional": true + }, + "viem": { + "optional": true + } } }, "node_modules/@privy-io/js-sdk-core/node_modules/js-cookie": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "license": "MIT", "engines": { "node": ">=14" } }, "node_modules/@privy-io/public-api": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/@privy-io/public-api/-/public-api-2.12.2.tgz", - "integrity": "sha512-b7oZkL5sQrUnZdKnfZFiYCm65lBP5KNVQA0BKNWMRuZ9d38ZR7oxvgyYKYQjTDJoeo2Y07DnplvWUVkSwMdLBg==", - "license": "Apache-2.0", + "version": "2.15.8", + "resolved": "https://registry.npmjs.org/@privy-io/public-api/-/public-api-2.15.8.tgz", + "integrity": "sha512-Aj29hJVKMy0mg5lOBRwl0JzV3F4CpaJd3T3RP2RnaL3no/ar65x/s2xfIyCOYZI/3D9DQBzE/JZzVr/vOnP4NA==", "dependencies": { - "@privy-io/api-base": "1.4.0", + "@privy-io/api-base": "1.4.1", "bs58": "^5.0.0", "ethers": "^5.7.2", "libphonenumber-js": "^1.10.31", @@ -10003,14 +9995,12 @@ "node_modules/@privy-io/public-api/node_modules/base-x": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==", - "license": "MIT" + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" }, "node_modules/@privy-io/public-api/node_modules/bs58": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "license": "MIT", "dependencies": { "base-x": "^4.0.0" } @@ -10029,7 +10019,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT", "dependencies": { "@ethersproject/abi": "5.7.0", "@ethersproject/abstract-provider": "5.7.0", @@ -10064,10 +10053,9 @@ } }, "node_modules/@privy-io/react-auth": { - "version": "1.93.0", - "resolved": "https://registry.npmjs.org/@privy-io/react-auth/-/react-auth-1.93.0.tgz", - "integrity": "sha512-+s2QV/RUpltrQk595J3SVKJ0saQAz9rwdL+BzLLe14yx8EDYVV6QY2uGyenr/phG9ZsVaJ/NIimBG5wn9M572g==", - "license": "Apache-2.0", + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@privy-io/react-auth/-/react-auth-1.98.4.tgz", + "integrity": "sha512-Uj91SnwqB0JORTIxy2SDqMB7f0vuGRmPTKftoC6N2tXGvOl0swMH/vWfawR+7jwFr+Y0VpPTRQOHzyjYfUHJLA==", "dependencies": { "@coinbase/wallet-sdk": "4.0.3", "@ethersproject/abstract-signer": "^5.7.0", @@ -10081,11 +10069,11 @@ "@ethersproject/transactions": "^5.7.0", "@ethersproject/units": "^5.7.0", "@floating-ui/react": "^0.26.22", - "@headlessui/react": "^1.7.18", + "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.1.1", "@marsidev/react-turnstile": "^0.4.1", "@metamask/eth-sig-util": "^6.0.0", - "@privy-io/js-sdk-core": "0.32.0", + "@privy-io/js-sdk-core": "0.36.1", "@simplewebauthn/browser": "^9.0.1", "@solana/wallet-adapter-base": "^0.9.23", "@solana/wallet-standard-wallet-adapter-base": "^1.1.2", @@ -10104,7 +10092,6 @@ "md5": "^2.3.0", "mipd": "^0.0.7", "ofetch": "^1.3.4", - "permissionless": "^0.2.10", "pino-pretty": "^10.0.0", "qrcode": "^1.5.1", "react-device-detect": "^2.2.2", @@ -10115,11 +10102,13 @@ "uuid": ">=8 <10", "viem": "^2.21.9", "web3-core": "^1.8.0", - "web3-core-helpers": "^1.8.0" + "web3-core-helpers": "^1.8.0", + "zustand": "^5.0.1" }, "peerDependencies": { - "@abstract-foundation/agw-client": "^0.0.1-beta.14", - "@solana/web3.js": "^1.95.3", + "@abstract-foundation/agw-client": "^0.1.0", + "@solana/web3.js": "^1.95.8", + "permissionless": "^0.2.10", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, @@ -10129,6 +10118,9 @@ }, "@solana/web3.js": { "optional": true + }, + "permissionless": { + "optional": true } } }, @@ -10248,15 +10240,42 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/@privy-io/react-auth/node_modules/zustand": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz", + "integrity": "sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/@privy-io/server-auth": { - "version": "1.16.6", - "resolved": "https://registry.npmjs.org/@privy-io/server-auth/-/server-auth-1.16.6.tgz", - "integrity": "sha512-bHn9WGbgn8JG4vo1VDdWrmvqspsZRD+ChGzrdSTyhMCguk7LML3yxwbc7urubnwYD1zOGNQUUPIexUyaQVm/eA==", - "license": "Apache-2.0", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@privy-io/server-auth/-/server-auth-1.17.1.tgz", + "integrity": "sha512-gHz2hr3EBm67gsP/eKnq2mUWhbXv+ukRBeCQA7Fig1yTWwJX/1CFMn+CLKp9vZ41SrskI0QjJD+i4JDx6DCe4Q==", "dependencies": { "@noble/curves": "^1.6.0", "@noble/hashes": "^1.5.0", - "@solana/web3.js": "^1.95.3", + "@solana/web3.js": "^1.95.8", "canonicalize": "^2.0.0", "dotenv": "^16.0.3", "jose": "^4.10.4", @@ -10307,14 +10326,12 @@ } }, "node_modules/@privy-io/wagmi": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@privy-io/wagmi/-/wagmi-0.2.12.tgz", - "integrity": "sha512-MJQVnrsA1BVT4SUOLCGiDB58qj0JvEaXBSle7R8yTXtmLuEfsRsXZTA9b0CKMe9wGUQfX4U6BligLBZBhreT0Q==", - "license": "Apache-2.0", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@privy-io/wagmi/-/wagmi-0.2.13.tgz", + "integrity": "sha512-NOyM43Kbc1K+hu3OwaSpKE1twhUEof9+jPefX4f1OLhbnEZGfcKAYP75O9xjc5rxTPgKfod9FSmt/xbKxJfUHw==", "peerDependencies": { "@privy-io/react-auth": "^1.64.1", - "react": "^18", - "react-dom": "^18", + "react": ">=18", "viem": "^2", "wagmi": "^2" } @@ -14349,10 +14366,9 @@ } }, "node_modules/@solana/web3.js": { - "version": "1.95.4", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.95.4.tgz", - "integrity": "sha512-sdewnNEA42ZSMxqkzdwEWi6fDgzwtJHaQa5ndUGEJYtoOnM6X5cvPmjoTUp7/k7bRrVAxfBgDnvQQHD6yhlLYw==", - "license": "MIT", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.0.tgz", + "integrity": "sha512-nz3Q5OeyGFpFCR+erX2f6JPt3sKhzhYcSycBCSPkWjzSVDh/Rr1FqTVMRe58FKO16/ivTUcuJjeS5MyBvpkbzA==", "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -14612,9 +14628,9 @@ } }, "node_modules/@stylistic/eslint-plugin-js": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.11.0.tgz", - "integrity": "sha512-btchD0P3iij6cIk5RR5QMdEhtCCV0+L6cNheGhGCd//jaHILZMTi/EOqgEDAf1s4ZoViyExoToM+S2Iwa3U9DA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.12.1.tgz", + "integrity": "sha512-5ybogtEgWIGCR6dMnaabztbWyVdAPDsf/5XOk6jBonWug875Q9/a6gm9QxnU3rhdyDEnckWKX7dduwYJMOWrVA==", "dev": true, "dependencies": { "eslint-visitor-keys": "^4.2.0", @@ -14627,6 +14643,19 @@ "eslint": ">=8.40.0" } }, + "node_modules/@superfaceai/passport-twitter-oauth2": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@superfaceai/passport-twitter-oauth2/-/passport-twitter-oauth2-1.2.4.tgz", + "integrity": "sha512-f2P4X1HqBHJuAnQefINpgJvMGLHx+nT9APbfJzN/CCBcazvP9NOD9IiENHMZMXEgfcHU40V3KwKiIZTBH5Q4Wg==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "^1.6.1" + }, + "optionalDependencies": { + "@types/passport": "1.x", + "@types/passport-oauth2": ">=1.4" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -14654,12 +14683,12 @@ } }, "node_modules/@tanstack/eslint-plugin-query": { - "version": "5.62.1", - "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.1.tgz", - "integrity": "sha512-1886D5U+re1TW0wSH4/kUGG36yIoW5Wkz4twVEzlk3ZWmjF3XkRSWgB+Sc7n+Lyzt8usNV8ZqkZE6DA7IC47fQ==", + "version": "5.62.16", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.16.tgz", + "integrity": "sha512-VhnHSQ/hc62olLzGhlLJ4BJGWynwjs3cDMsByasKJ3zjW1YZ+6raxOv0gHHISm+VEnAY42pkMowmSWrXfL4NTw==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "^8.15.0" + "@typescript-eslint/utils": "^8.18.1" }, "funding": { "type": "github", @@ -14670,13 +14699,13 @@ } }, "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", - "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", + "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0" + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -14687,9 +14716,9 @@ } }, "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", - "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", + "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -14700,13 +14729,13 @@ } }, "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", - "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", + "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/visitor-keys": "8.15.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -14721,22 +14750,20 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", - "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", + "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.15.0", - "@typescript-eslint/types": "8.15.0", - "@typescript-eslint/typescript-estree": "8.15.0" + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -14746,21 +14773,17 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", - "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", + "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.15.0", + "@typescript-eslint/types": "8.19.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -14772,30 +14795,29 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.62.3", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.3.tgz", - "integrity": "sha512-Jp/nYoz8cnO7kqhOlSv8ke/0MJRJVGuZ0P/JO9KQ+f45mpN90hrerzavyTKeSoT/pOzeoOUkv1Xd0wPsxAWXfg==", + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz", + "integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/query-devtools": { - "version": "5.61.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.61.4.tgz", - "integrity": "sha512-21Tw+u8E3IJJj4A/Bct4H0uBaDTEu7zBrR79FeSyY+mS2gx5/m316oDtJiKkILc819VSTYt+sFzODoJNcpPqZQ==", - "license": "MIT", + "version": "5.62.16", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz", + "integrity": "sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/query-persist-client-core": { - "version": "5.62.3", - "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.62.3.tgz", - "integrity": "sha512-2cyrQAF1+yn/9GJf3kZnXjtFhlcCWOLL9PXG/JeMxRz8Ni0qvBbRIAXryKVJuDh570K0mmaaGs33pQb+FbmnEQ==", + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.64.1.tgz", + "integrity": "sha512-5SGe5IHKXJmkxL2gwKjHQrap18hY57kF0DVUd2yh6QLFn5pruUG4QO6DsjEuQGk33ZfxkX4Z7uJdZBgfMedA8g==", "dependencies": { - "@tanstack/query-core": "5.62.3" + "@tanstack/query-core": "5.64.1" }, "funding": { "type": "github", @@ -14803,11 +14825,11 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.62.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.3.tgz", - "integrity": "sha512-y2zDNKuhgiuMgsKkqd4AcsLIBiCfEO8U11AdrtAUihmLbRNztPrlcZqx2lH1GacZsx+y1qRRbCcJLYTtF1vKsw==", + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz", + "integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==", "dependencies": { - "@tanstack/query-core": "5.62.3" + "@tanstack/query-core": "5.64.1" }, "funding": { "type": "github", @@ -14818,34 +14840,34 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.62.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.62.3.tgz", - "integrity": "sha512-4iaQap/iP5ErS094u1WehFntHtjRo6g5HJMvyHovBVbsxnvgPc6AtKAw7qxPPoKy6Wj5Bew0045eYP5phiiBmw==", + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.64.1.tgz", + "integrity": "sha512-8ajcGE3wXYlb4KuJnkFYkJwJKc/qmPNTpQD7YTgLRMBPTGGp1xk7VMzxL87DoXuweO8luplUUblJJ3noVs/luQ==", "dependencies": { - "@tanstack/query-devtools": "5.61.4" + "@tanstack/query-devtools": "5.62.16" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.62.3", + "@tanstack/react-query": "^5.64.1", "react": "^18 || ^19" } }, "node_modules/@tanstack/react-query-persist-client": { - "version": "5.62.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.62.3.tgz", - "integrity": "sha512-MoJzwerYUyjtBclcTcCwOYCfxVwyPKn7nDXNl7yAHlkLDRAJSxMl1bHIYNotIXSqbhenVLk9LICCOTg6RDgMxw==", + "version": "5.64.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.64.1.tgz", + "integrity": "sha512-2GmjTFU/QGF3p56UwnFW+N+WsKnzuNry3RQsyDtJ6/AQvy5nd567Mf58hc4bAj35nvI8MkuKCrzegRmHC8Wjxw==", "dependencies": { - "@tanstack/query-persist-client-core": "5.62.3" + "@tanstack/query-persist-client-core": "5.64.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.62.3", + "@tanstack/react-query": "^5.64.1", "react": "^18 || ^19" } }, @@ -15016,11 +15038,10 @@ } }, "node_modules/@types/amqplib": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.5.tgz", - "integrity": "sha512-/cSykxROY7BWwDoi4Y4/jLAuZTshZxd8Ey1QYa/VaXriMotBDoou7V/twJiOSHzU6t1Kp1AHAUXGCgqq+6DNeg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.6.tgz", + "integrity": "sha512-vQLVypBS1JQcfTXhl1Td1EEeLdtb+vuulOb4TrzYiLyP2aYLMAEzB3pNmEA0jBm0xIXu946Y7Xwl19Eidl32SQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -15458,7 +15479,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -15471,7 +15492,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -15480,6 +15501,16 @@ "@types/send": "*" } }, + "node_modules/@types/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/filesystem": { "version": "0.0.36", "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", @@ -15641,7 +15672,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT", "optional": true }, "node_modules/@types/lru-cache": { @@ -15702,10 +15732,9 @@ } }, "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", - "license": "MIT", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dependencies": { "undici-types": "~6.20.0" } @@ -15732,6 +15761,16 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/on-finished": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/on-finished/-/on-finished-2.3.4.tgz", @@ -15746,6 +15785,28 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-oauth2": { + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz", + "integrity": "sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, "node_modules/@types/pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.2.tgz", @@ -15815,6 +15876,26 @@ "@types/react": "*" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/request": { "version": "2.48.12", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", @@ -17598,133 +17679,133 @@ "license": "(Apache-2.0 AND MIT)" }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -18184,17 +18265,17 @@ } }, "node_modules/antd": { - "version": "5.22.5", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.22.5.tgz", - "integrity": "sha512-+0UP8w+ULVv2OIzCDVz7j6I0UfH6mMLHSWO6qzpBc+9psOoVQLRbyAE21XnZM/eGrt2MNsEDL5fmlhXL/V8JyQ==", + "version": "5.23.1", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.23.1.tgz", + "integrity": "sha512-rg5xd5LotHw0IRyo/nsiUN/EEV3e+xU4V4UmIb/62hMN9+3APyz1Ohjf17a+fN13jC8sNY1hP1K252SU2Th0xA==", "dependencies": { - "@ant-design/colors": "^7.1.0", - "@ant-design/cssinjs": "^1.21.1", + "@ant-design/colors": "^7.2.0", + "@ant-design/cssinjs": "^1.22.0", "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", "@ant-design/icons": "^5.5.2", "@ant-design/react-slick": "~1.1.2", - "@babel/runtime": "^7.25.7", - "@ctrl/tinycolor": "^3.6.1", + "@babel/runtime": "^7.26.0", "@rc-component/color-picker": "~2.0.1", "@rc-component/mutate-observer": "^1.1.0", "@rc-component/qrcode": "~1.0.0", @@ -18203,38 +18284,38 @@ "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.11", - "rc-cascader": "~3.30.0", - "rc-checkbox": "~3.3.0", + "rc-cascader": "~3.33.0", + "rc-checkbox": "~3.5.0", "rc-collapse": "~3.9.0", "rc-dialog": "~9.6.0", "rc-drawer": "~7.2.0", "rc-dropdown": "~4.2.1", "rc-field-form": "~2.7.0", "rc-image": "~7.11.0", - "rc-input": "~1.6.4", - "rc-input-number": "~9.3.0", - "rc-mentions": "~2.17.0", + "rc-input": "~1.7.2", + "rc-input-number": "~9.4.0", + "rc-mentions": "~2.19.1", "rc-menu": "~9.16.0", - "rc-motion": "^2.9.4", + "rc-motion": "^2.9.5", "rc-notification": "~5.6.2", "rc-pagination": "~5.0.0", - "rc-picker": "~4.8.3", + "rc-picker": "~4.9.2", "rc-progress": "~4.0.0", "rc-rate": "~2.13.0", - "rc-resize-observer": "^1.4.1", - "rc-segmented": "~2.5.0", - "rc-select": "~14.16.4", - "rc-slider": "~11.1.7", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.5", + "rc-slider": "~11.1.8", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", - "rc-table": "~7.49.0", - "rc-tabs": "~15.4.0", - "rc-textarea": "~1.8.2", - "rc-tooltip": "~6.2.1", - "rc-tree": "~5.10.1", - "rc-tree-select": "~5.24.5", + "rc-table": "~7.50.2", + "rc-tabs": "~15.5.0", + "rc-textarea": "~1.9.0", + "rc-tooltip": "~6.3.2", + "rc-tree": "~5.13.0", + "rc-tree-select": "~5.27.0", "rc-upload": "~4.8.1", - "rc-util": "^5.44.2", + "rc-util": "^5.44.3", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, @@ -18334,13 +18415,13 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -18451,15 +18532,15 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -18485,19 +18566,18 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -19139,6 +19219,15 @@ } ] }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -19898,15 +19987,41 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -21020,9 +21135,9 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/concurrently": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.0.tgz", - "integrity": "sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.1.tgz", + "integrity": "sha512-6VX8lrBIycgZKTwBsWS+bLrmkGRkDmvtGsYylRN9b93CygN6CbK46HmnQ3rdSOR8HRjdahDrxb5MqD9cEFOg5Q==", "dev": true, "dependencies": { "chalk": "^4.1.2", @@ -21076,6 +21191,18 @@ "node": ">= 0.10.0" } }, + "node_modules/connect-redis": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-8.0.1.tgz", + "integrity": "sha512-7iOI214/r15ahvu0rqKCHhsgpMdOgyLwqlw/icSTnnAR75xFvMyfxAE+je4M87rZLjDlKzKcTc48XxQXYFsMgA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "express-session": ">=1" + } + }, "node_modules/connect/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -21160,6 +21287,27 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cookies-next": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-5.0.2.tgz", + "integrity": "sha512-Ft5yXMbN6wMfgLiL5TMyPnxsjkW6UEjZw0YMoDMiF3F6iYdFPjiJEMugx/sivUr8G+0xPG80lBYqI2b+VquSuw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1" + }, + "peerDependencies": { + "next": ">=15.0.0" + } + }, + "node_modules/cookies-next/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -21607,6 +21755,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -22013,15 +22175,70 @@ "node": ">= 6" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", + "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -22031,29 +22248,29 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -22122,6 +22339,14 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -22689,6 +22914,19 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexify": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", @@ -22850,6 +23088,20 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -22898,57 +23150,60 @@ } }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.23.8", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.8.tgz", + "integrity": "sha512-lfab8IzDn6EpI1ibZakcgS6WsfEBiB+43cuJo+wgylx1xKXf+Sp+YR3vFuQwC/u3sxYwV8Cxe3B0DpVUu/WiJQ==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.6", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.0", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -22958,12 +23213,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -22977,26 +23229,27 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz", - "integrity": "sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", + "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.3", - "safe-array-concat": "^1.1.2" + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" }, "engines": { "node": ">= 0.4" @@ -23011,7 +23264,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -23043,14 +23295,14 @@ } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -23453,19 +23705,19 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz", - "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", "dev": true, "dependencies": { "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.3.5", + "debug": "^4.3.7", "enhanced-resolve": "^5.15.0", - "eslint-module-utils": "^2.8.1", "fast-glob": "^3.3.2", "get-tsconfig": "^4.7.5", "is-bun-module": "^1.0.2", - "is-glob": "^4.0.3" + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -23488,12 +23740,12 @@ } }, "node_modules/eslint-import-resolver-typescript/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -23504,6 +23756,12 @@ } } }, + "node_modules/eslint-import-resolver-typescript/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/eslint-module-utils": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", @@ -23731,28 +23989,28 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", - "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", + "version": "7.37.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz", + "integrity": "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==", "dev": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", + "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.1.0", + "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11", + "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "engines": { @@ -23763,9 +24021,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz", - "integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", + "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", "dev": true, "engines": { "node": ">=10" @@ -24850,9 +25108,9 @@ "peer": true }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -24874,7 +25132,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -24891,6 +25149,55 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -25208,8 +25515,7 @@ "node_modules/fetch-retry": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.6.tgz", - "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==", - "license": "MIT" + "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==" }, "node_modules/fflate": { "version": "0.4.8", @@ -25539,27 +25845,27 @@ } }, "node_modules/firebase-admin": { - "version": "12.7.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz", - "integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==", - "license": "Apache-2.0", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.0.2.tgz", + "integrity": "sha512-YWVpoN+tZVSRXF0qC0gojoF5bSqvBRbnBk8+xUtFiguM2L4vB7f0moAwV1VVWDDHvTnvQ68OyTMpdp6wKo/clw==", "dependencies": { "@fastify/busboy": "^3.0.0", - "@firebase/database-compat": "1.0.8", - "@firebase/database-types": "1.0.5", - "@types/node": "^22.0.1", + "@firebase/database-compat": "^2.0.0", + "@firebase/database-types": "^1.0.6", + "@types/node": "^22.8.7", "farmhash-modern": "^1.1.0", + "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", - "uuid": "^10.0.0" + "uuid": "^11.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "optionalDependencies": { - "@google-cloud/firestore": "^7.7.0", - "@google-cloud/storage": "^7.7.0" + "@google-cloud/firestore": "^7.11.0", + "@google-cloud/storage": "^7.14.0" } }, "node_modules/firebase-admin/node_modules/@fastify/busboy": { @@ -25568,17 +25874,107 @@ "integrity": "sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==", "license": "MIT" }, + "node_modules/firebase-admin/node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==" + }, + "node_modules/firebase-admin/node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==" + }, + "node_modules/firebase-admin/node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==" + }, + "node_modules/firebase-admin/node_modules/@firebase/component": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.11.tgz", + "integrity": "sha512-eQbeCgPukLgsKD0Kw5wQgsMDX5LeoI1MIrziNDjmc6XDq5ZQnuUymANQgAb2wp1tSF9zDSXyxJmIUXaKgN58Ug==", + "dependencies": { + "@firebase/util": "1.10.2", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/database": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.10.tgz", + "integrity": "sha512-sWp2g92u7xT4BojGbTXZ80iaSIaL6GAL0pwvM0CO/hb0nHSnABAqsH7AhnWGsGvXuEvbPr7blZylPaR9J+GSuQ==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.11", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.2", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/database-compat": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.1.tgz", + "integrity": "sha512-IsFivOjdE1GrjTeKoBU/ZMenESKDXidFDzZzHBPQ/4P20ptGdrl3oLlWrV/QJqJ9lND4IidE3z4Xr5JyfUW1vg==", + "dependencies": { + "@firebase/component": "0.6.11", + "@firebase/database": "1.0.10", + "@firebase/database-types": "1.0.7", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.2", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/database-types": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.7.tgz", + "integrity": "sha512-I7zcLfJXrM0WM+ksFmFdAMdlq/DFmpeMNa+/GNsLyFo5u/lX5zzkPzGe3srVWqaBQBY5KprylDGxOsP6ETfL0A==", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.10.2" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/logger": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz", + "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/util": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.2.tgz", + "integrity": "sha512-qnSHIoE9FK+HYnNhTI8q14evyqbc/vHRivfB4TgCIUOl4tosmKSQlp7ltymOlMP4xVIJTg5wrkfcZ60X4nUf7Q==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/firebase-admin/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/flat": { @@ -25695,7 +26091,6 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "dev": true, "engines": { "node": ">=0.4.x" } @@ -25837,15 +26232,17 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -25858,7 +26255,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "license": "MIT", "optional": true }, "node_modules/functions-have-names": { @@ -25885,7 +26281,6 @@ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", "license": "Apache-2.0", - "optional": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -25902,7 +26297,6 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "license": "MIT", - "optional": true, "dependencies": { "debug": "^4.3.4" }, @@ -25915,7 +26309,6 @@ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "license": "MIT", - "optional": true, "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -25929,7 +26322,6 @@ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", "license": "Apache-2.0", - "optional": true, "dependencies": { "gaxios": "^6.0.0", "json-bigint": "^1.0.0" @@ -25975,15 +26367,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -26049,14 +26446,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -26344,7 +26741,6 @@ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.2.tgz", "integrity": "sha512-R+FRIfk1GBo3RdlRYWPdwk8nmtVUOn6+BkDomAC46KoU8kzXzE1HLmOasSCbWUByMMAGkknVF0G5kQ69Vj7dlA==", "license": "Apache-2.0", - "optional": true, "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -26361,7 +26757,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz", "integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==", - "license": "Apache-2.0", "optional": true, "dependencies": { "@grpc/grpc-js": "^1.10.9", @@ -26382,10 +26777,9 @@ } }, "node_modules/google-gax/node_modules/@grpc/grpc-js": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", - "integrity": "sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==", - "license": "Apache-2.0", + "version": "1.12.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz", + "integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==", "optional": true, "dependencies": { "@grpc/proto-loader": "^0.7.13", @@ -26396,11 +26790,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -26447,7 +26841,6 @@ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", "license": "MIT", - "optional": true, "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" @@ -26523,14 +26916,14 @@ } }, "node_modules/hardhat": { - "version": "2.22.16", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.22.16.tgz", - "integrity": "sha512-d52yQZ09u0roL6GlgJSvtknsBtIuj9JrJ/U8VMzr/wue+gO5v2tQayvOX6llerlR57Zw2EOTQjLAt6RpHvjwHA==", + "version": "2.22.17", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.22.17.tgz", + "integrity": "sha512-tDlI475ccz4d/dajnADUTRc1OJ3H8fpP9sWhXhBPpYsQOg8JHq5xrDimo53UhWPl7KJmAeDCm1bFG74xvpGRpg==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.1.2", "@metamask/eth-sig-util": "^4.0.0", - "@nomicfoundation/edr": "^0.6.4", + "@nomicfoundation/edr": "^0.6.5", "@nomicfoundation/ethereumjs-common": "4.0.4", "@nomicfoundation/ethereumjs-tx": "5.0.4", "@nomicfoundation/ethereumjs-util": "9.0.4", @@ -26833,10 +27226,13 @@ } }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -26861,9 +27257,13 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -26872,9 +27272,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -26930,6 +27330,16 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-estree": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz", @@ -27709,6 +28119,71 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -27777,6 +28252,21 @@ "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", "license": "MIT" }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -27801,6 +28291,20 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-entities": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", @@ -28332,14 +28836,14 @@ } }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -28476,13 +28980,14 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -28501,7 +29006,6 @@ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", "dev": true, - "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -28513,12 +29017,15 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -28536,13 +29043,13 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", + "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -28625,11 +29132,13 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -28640,12 +29149,13 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -28703,13 +29213,15 @@ } }, "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -28821,7 +29333,6 @@ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -28846,25 +29357,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -28950,6 +29450,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -28959,13 +29467,15 @@ } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -28979,7 +29489,6 @@ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -28988,12 +29497,12 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -29034,12 +29543,13 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -29049,12 +29559,14 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -29064,11 +29576,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -29100,7 +29612,6 @@ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -29109,26 +29620,28 @@ } }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", + "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -29310,17 +29823,17 @@ } }, "node_modules/iterator.prototype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", - "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", + "integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==", "dev": true, - "license": "MIT", "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "reflect.getprototypeof": "^1.0.8", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -29721,6 +30234,166 @@ "signal-exit": "^3.0.2" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", + "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -29737,7 +30410,6 @@ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "license": "MIT", - "optional": true, "dependencies": { "bignumber.js": "^9.0.0" } @@ -29968,7 +30640,6 @@ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", "license": "MIT", - "optional": true, "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -30021,7 +30692,6 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", "license": "MIT", - "optional": true, "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" @@ -30093,9 +30763,9 @@ } }, "node_modules/knip": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/knip/-/knip-5.39.0.tgz", - "integrity": "sha512-GL6tGe5SCyLm4JtKFlHq5TOm7jbzzrXyk/xemSmWgdehN89ZKdFQkpJXL/lOTrQV6jnqgx9LlFMN1UnICx3FYQ==", + "version": "5.41.1", + "resolved": "https://registry.npmjs.org/knip/-/knip-5.41.1.tgz", + "integrity": "sha512-yNpCCe2REU7U3VRvMASnXSEtfEC2HmOoDW9Vp9teQ9FktJYnuagvSZD3xWq8Ru7sPABkmvbC5TVWuMzIaeADNA==", "dev": true, "funding": [ { @@ -30111,7 +30781,6 @@ "url": "https://polar.sh/webpro-nl" } ], - "license": "ISC", "dependencies": { "@nodelib/fs.walk": "1.2.8", "@snyk/github-codeowners": "1.1.0", @@ -30214,10 +30883,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.11.14", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.14.tgz", - "integrity": "sha512-sexvAfwcW1Lqws4zFp8heAtAEXbEDnvkYCEGzvOoMgZR7JhXo/IkE9MkkGACgBed5fWqh3ShBGnJBdDnU9N8EQ==", - "license": "MIT" + "version": "1.11.17", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.17.tgz", + "integrity": "sha512-Jr6v8thd5qRlOlc6CslSTzGzzQW03uiscab7KHQZX1Dfo4R6n6FDhZ0Hri6/X7edLIDv9gl4VMZXhxTjLnl0VQ==" }, "node_modules/lighthouse-logger": { "version": "1.4.2", @@ -30614,6 +31282,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lowlight/node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru_map": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", @@ -30739,6 +31434,14 @@ "license": "Apache-2.0", "peer": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -35170,6 +35873,20 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==" }, + "node_modules/nwsapi": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", + "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/oauth": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", + "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==", + "license": "MIT" + }, "node_modules/ob1": { "version": "0.81.0", "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.81.0.tgz", @@ -35248,9 +35965,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -35291,14 +36011,16 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -35355,12 +36077,13 @@ } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -35617,6 +36340,23 @@ "dev": true, "license": "MIT" }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ox": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ox/-/ox-0.1.2.tgz", @@ -35877,6 +36617,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -35885,6 +36639,52 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/patch-package": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", @@ -36120,9 +36920,10 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -36148,6 +36949,11 @@ "node": "*" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", @@ -36279,6 +37085,8 @@ "resolved": "https://registry.npmjs.org/permissionless/-/permissionless-0.2.15.tgz", "integrity": "sha512-S5+FP9e6xY7AEC+9w4mJ66826Cn7gHqHxKMFopevM2+ZPTpYpvYnB4APLljNsCfuIqMVNutTbgcuLZTasNvMEQ==", "license": "MIT", + "optional": true, + "peer": true, "peerDependencies": { "viem": "^2.21.22" } @@ -36903,11 +37711,10 @@ } }, "node_modules/prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", - "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, - "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -36981,12 +37788,12 @@ } }, "node_modules/prisma": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.1.0.tgz", - "integrity": "sha512-aFI3Yi+ApUxkwCJJwyQSwpyzUX7YX3ihzuHNHOyv4GJg3X5tQsmRaJEnZ+ZyfHpMtnyahhmXVfbTZ+lS8ZtfKw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.2.1.tgz", + "integrity": "sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA==", "hasInstallScript": true, "dependencies": { - "@prisma/engines": "6.1.0" + "@prisma/engines": "6.2.1" }, "bin": { "prisma": "build/index.js" @@ -36998,6 +37805,15 @@ "fsevents": "2.3.3" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/proc-log": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", @@ -37145,7 +37961,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", - "license": "Apache-2.0", "optional": true, "dependencies": { "protobufjs": "^7.2.5" @@ -37601,6 +38416,15 @@ "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "license": "MIT" }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -37677,14 +38501,14 @@ } }, "node_modules/rc-cascader": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.30.0.tgz", - "integrity": "sha512-rrzSbk1Bdqbu+pDwiLCLHu72+lwX9BZ28+JKzoi0DWZ4N29QYFeip8Gctl33QVd2Xg3Rf14D3yAOG76ElJw16w==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.33.0.tgz", + "integrity": "sha512-JvZrMbKBXIbEDmpIORxqvedY/bck6hGbs3hxdWT8eS9wSQ1P7//lGxbyKjOSyQiVBbgzNWriSe6HoMcZO/+0rQ==", "dependencies": { "@babel/runtime": "^7.25.7", "classnames": "^2.3.1", "rc-select": "~14.16.2", - "rc-tree": "~5.10.1", + "rc-tree": "~5.13.0", "rc-util": "^5.43.0" }, "peerDependencies": { @@ -37693,9 +38517,9 @@ } }, "node_modules/rc-checkbox": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.3.0.tgz", - "integrity": "sha512-Ih3ZaAcoAiFKJjifzwsGiT/f/quIkxJoklW4yKGho14Olulwn8gN7hOBve0/WGDg5o/l/5mL0w7ff7/YGvefVw==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", @@ -37803,9 +38627,9 @@ } }, "node_modules/rc-input": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.6.4.tgz", - "integrity": "sha512-lBZhfRD4NSAUW0zOKLUeI6GJuXkxeZYi0hr8VcJgJpyTNOvHw1ysrKWAHcEOAAHj7guxgmWYSi6xWrEdfrSAsA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.7.2.tgz", + "integrity": "sha512-g3nYONnl4edWj2FfVoxsU3Ec4XTE+Hb39Kfh2MFxMZjp/0gGyPUgy/v7ZhS27ZxUFNkuIDYXm9PJsLyJbtg86A==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -37817,14 +38641,14 @@ } }, "node_modules/rc-input-number": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.3.0.tgz", - "integrity": "sha512-JQ363ywqRyxwgVxpg2z2kja3CehTpYdqR7emJ/6yJjRdbvo+RvfE83fcpBCIJRq3zLp8SakmEXq60qzWyZ7Usw==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.4.0.tgz", + "integrity": "sha512-Tiy4DcXcFXAf9wDhN8aUAyMeCLHJUHA/VA/t7Hj8ZEx5ETvxG7MArDOSE6psbiSCo+vJPm4E3fGN710ITVn6GA==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", - "rc-input": "~1.6.0", + "rc-input": "~1.7.1", "rc-util": "^5.40.1" }, "peerDependencies": { @@ -37833,16 +38657,16 @@ } }, "node_modules/rc-mentions": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.17.0.tgz", - "integrity": "sha512-sfHy+qLvc+p8jx8GUsujZWXDOIlIimp6YQz7N5ONQ6bHsa2kyG+BLa5k2wuxgebBbH97is33wxiyq5UkiXRpHA==", + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.19.1.tgz", + "integrity": "sha512-KK3bAc/bPFI993J3necmaMXD2reZTzytZdlTvkeBbp50IGH1BDPDvxLdHDUrpQx2b2TGaVJsn+86BvYa03kGqA==", "dependencies": { "@babel/runtime": "^7.22.5", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", - "rc-input": "~1.6.0", + "rc-input": "~1.7.1", "rc-menu": "~9.16.0", - "rc-textarea": "~1.8.0", + "rc-textarea": "~1.9.0", "rc-util": "^5.34.1" }, "peerDependencies": { @@ -37868,9 +38692,9 @@ } }, "node_modules/rc-motion": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.4.tgz", - "integrity": "sha512-TAPUUufDqhPO669qJobI0d9U0XZ/VPNQyZTivUxxzU1EyuPe3PtHSx7Kb902KuzQgu7sS18z8GguaxZEALV/ww==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -37929,9 +38753,9 @@ } }, "node_modules/rc-picker": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.8.3.tgz", - "integrity": "sha512-hJ45qoEs4mfxXPAJdp1n3sKwADul874Cd0/HwnsEOE60H+tgiJUGgbOD62As3EG/rFVNS5AWRfBCDJJfmRqOVQ==", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.9.2.tgz", + "integrity": "sha512-SLW4PRudODOomipKI0dvykxW4P8LOqtMr17MOaLU6NQJhkh9SZeh44a/8BMxwv5T6e3kiIeYc9k5jFg2Mv35Pg==", "dependencies": { "@babel/runtime": "^7.24.7", "@rc-component/trigger": "^2.0.0", @@ -37998,9 +38822,9 @@ } }, "node_modules/rc-resize-observer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.1.tgz", - "integrity": "sha512-JbAeFDsaaZRPwaTlXnCqgeO9c6E7qoaE/hxsub08cdnnPn6767c/j9+r/TifUdfvwXtdcfHygKbZ7ecM/PXo/Q==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", @@ -38013,9 +38837,9 @@ } }, "node_modules/rc-segmented": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.5.0.tgz", - "integrity": "sha512-B28Fe3J9iUFOhFJET3RoXAPFJ2u47QvLSYcZWC4tFYNGPEjug5LAxEasZlA/PpAxhdOPqGWsGbSj7ftneukJnw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz", + "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -38028,9 +38852,9 @@ } }, "node_modules/rc-select": { - "version": "14.16.4", - "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.4.tgz", - "integrity": "sha512-jP6qf7+vjnxGvPpfPWbGYfFlSl3h8L2XcD4O7g2GYXmEeBC0mw+nPD7i++OOE8v3YGqP8xtYjRKAWCMLfjgxlw==", + "version": "14.16.5", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.5.tgz", + "integrity": "sha512-cRls713egTcitJ7WUXhHEf22h3U1OMC8nbw9+HN4Fniew8Xo3avgEDvIeGRwhbiyPNbQR23AwP+tt6KWUcB4IA==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.1.1", @@ -38049,9 +38873,9 @@ } }, "node_modules/rc-slider": { - "version": "11.1.7", - "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.7.tgz", - "integrity": "sha512-ytYbZei81TX7otdC0QvoYD72XSlxvTihNth5OeZ6PMXyEDq/vHdWFulQmfDGyXK1NwKwSlKgpvINOa88uT5g2A==", + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.8.tgz", + "integrity": "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", @@ -38097,15 +38921,15 @@ } }, "node_modules/rc-table": { - "version": "7.49.0", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.49.0.tgz", - "integrity": "sha512-/FoPLX94muAQOxVpi1jhnpKjOIqUbT81eELQPAzSXOke4ky4oCWYUXOcVpL31ZCO90xScwVSXRd7coqtgtB1Ng==", + "version": "7.50.2", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.2.tgz", + "integrity": "sha512-+nJbzxzstBriLb5sr9U7Vjs7+4dO8cWlouQbMwBVYghk2vr508bBdkHJeP/z9HVjAIKmAgMQKxmtbgDd3gc5wA==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/context": "^1.4.0", "classnames": "^2.2.5", "rc-resize-observer": "^1.1.0", - "rc-util": "^5.41.0", + "rc-util": "^5.44.3", "rc-virtual-list": "^3.14.2" }, "engines": { @@ -38117,9 +38941,9 @@ } }, "node_modules/rc-tabs": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.4.0.tgz", - "integrity": "sha512-llKuyiAVqmXm2z7OrmhX5cNb2ueZaL8ZyA2P4R+6/72NYYcbEgOXibwHiQCFY2RiN3swXl53SIABi2CumUS02g==", + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.0.tgz", + "integrity": "sha512-NrDcTaUJLh9UuDdMBkjKTn97U9iXG44s9D03V5NHkhEDWO5/nC6PwC3RhkCWFMKB9hh+ryqgZ+TIr1b9Jd/hnQ==", "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "2.x", @@ -38138,13 +38962,13 @@ } }, "node_modules/rc-textarea": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.8.2.tgz", - "integrity": "sha512-UFAezAqltyR00a8Lf0IPAyTd29Jj9ee8wt8DqXyDMal7r/Cg/nDt3e1OOv3Th4W6mKaZijjgwuPXhAfVNTN8sw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.9.0.tgz", + "integrity": "sha512-dQW/Bc/MriPBTugj2Kx9PMS5eXCCGn2cxoIaichjbNvOiARlaHdI99j4DTxLl/V8+PIfW06uFy7kjfUIDDKyxQ==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", - "rc-input": "~1.6.0", + "rc-input": "~1.7.1", "rc-resize-observer": "^1.0.0", "rc-util": "^5.27.0" }, @@ -38154,9 +38978,9 @@ } }, "node_modules/rc-tooltip": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.2.1.tgz", - "integrity": "sha512-rws0duD/3sHHsD905Nex7FvoUGy2UBQRhTkKxeEvr2FB+r21HsOxcDJI0TzyO8NHhnAA8ILr8pfbSBg5Jj5KBg==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.3.2.tgz", + "integrity": "sha512-oA4HZIiZJbUQ5ojigM0y4XtWxaH/aQlJSzknjICRWNpqyemy1sL3X3iEQV2eSPBWEq+bqU3+aSs81z+28j9luA==", "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/trigger": "^2.0.0", @@ -38168,9 +38992,9 @@ } }, "node_modules/rc-tree": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.10.1.tgz", - "integrity": "sha512-FPXb3tT/u39mgjr6JNlHaUTYfHkVGW56XaGDahDpEFLGsnPxGcVLNTjcqoQb/GNbSCycl7tD7EvIymwOTP0+Yw==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.0.tgz", + "integrity": "sha512-2+lFvoVRnvHQ1trlpXMOWtF8BUgF+3TiipG72uOfhpL5CUdXCk931kvDdUkTL/IZVtNEDQKwEEmJbAYJSA5NnA==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -38187,14 +39011,14 @@ } }, "node_modules/rc-tree-select": { - "version": "5.24.5", - "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.24.5.tgz", - "integrity": "sha512-PnyR8LZJWaiEFw0SHRqo4MNQWyyZsyMs8eNmo68uXZWjxc7QqeWcjPPoONN0rc90c3HZqGF9z+Roz+GLzY5GXA==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", "dependencies": { "@babel/runtime": "^7.25.7", "classnames": "2.x", "rc-select": "~14.16.2", - "rc-tree": "~5.10.1", + "rc-tree": "~5.13.0", "rc-util": "^5.43.0" }, "peerDependencies": { @@ -38217,9 +39041,9 @@ } }, "node_modules/rc-util": { - "version": "5.44.2", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.2.tgz", - "integrity": "sha512-uGSk3hpPBLa3/0QAcKhCjgl4SFnhQCJDLvvpoLdbR6KgDuXrujG+dQaUeUvBJr2ZWak1O/9n+cYbJiWmmk95EQ==", + "version": "5.44.3", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.3.tgz", + "integrity": "sha512-q6KCcOFk3rv/zD3MckhJteZxb0VjAIFuf622B7ElK4vfrZdAzs16XR5p3VTdy3+U5jfJU5ACz4QnhLSuAGe5dA==", "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" @@ -38235,9 +39059,9 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/rc-virtual-list": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.15.0.tgz", - "integrity": "sha512-dF2YQztqrU3ijAeWOqscTshCEr7vpimzSqAVjO1AyAmaqcHulaXpnGR0ptK5PXfxTUy48VkJOiglMIxlkYGs0w==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.17.0.tgz", + "integrity": "sha512-h0jPHWt8/Ots9eiGVSGQTxwrSuQ3kxqL/ERKubv8zzIMICGQaDDWm/JoUa31MdQUC7PKDMiy5KDLkNfHcWo+iQ==", "dependencies": { "@babel/runtime": "^7.20.0", "classnames": "^2.2.6", @@ -38718,6 +39542,23 @@ } } }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/react-use-intercom": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/react-use-intercom/-/react-use-intercom-5.4.1.tgz", @@ -39021,19 +39862,19 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", + "integrity": "sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", + "dunder-proto": "^1.0.1", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "which-builtin-type": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -39042,6 +39883,122 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -39082,15 +40039,15 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -39748,6 +40705,39 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", + "integrity": "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==", + "license": "MIT", + "dependencies": { + "array-flatten": "3.0.0", + "is-promise": "4.0.0", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "^8.0.0", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router/node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/rpc-websockets": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.0.4.tgz", @@ -39795,6 +40785,14 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -39865,14 +40863,15 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -39901,15 +40900,31 @@ } ] }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -39959,6 +40974,20 @@ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/sc-istanbul": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/sc-istanbul/-/sc-istanbul-0.4.6.tgz", @@ -40863,14 +41892,65 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -41437,6 +42517,12 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -41689,23 +42775,24 @@ "license": "MIT" }, "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -41725,15 +42812,18 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -41743,15 +42833,19 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -42114,6 +43208,14 @@ "whatwg-fetch": "^3.4.1" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/sync-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", @@ -43059,6 +44161,28 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz", + "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tldts-core": "^6.1.70" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz", + "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -43538,30 +44662,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -43571,17 +44695,18 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -43591,17 +44716,17 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -43708,6 +44833,24 @@ "node": ">=0.8.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/uint8arrays": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.0.tgz", @@ -43718,15 +44861,18 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -44466,16 +45612,15 @@ } }, "node_modules/vaul": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.1.tgz", - "integrity": "sha512-+ejzF6ffQKPcfgS7uOrGn017g39F8SO4yLPXbBhpC7a0H+oPqPna8f1BUfXaz8eU4+pxbQcmjxW+jWBSbxjaFg==", - "license": "MIT", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "node_modules/vfile": { @@ -44726,9 +45871,9 @@ } }, "node_modules/vite-tsconfig-paths": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.3.tgz", - "integrity": "sha512-0bz+PDlLpGfP2CigeSKL9NFTF1KtXkeHGZSSaGQSuPZH77GhoiQaA8IjYgOaynSuwlDTolSUEU0ErVvju3NURg==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", "dev": true, "dependencies": { "debug": "^4.1.1", @@ -45367,6 +46512,20 @@ "dev": true, "license": "MIT" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/wagmi": { "version": "2.12.30", "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.12.30.tgz", @@ -45684,15 +46843,15 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", @@ -45958,12 +47117,37 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -45988,40 +47172,43 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-builtin-type": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", - "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, - "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", + "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", + "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", - "which-typed-array": "^1.1.15" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -46035,7 +47222,6 @@ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, - "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -46055,14 +47241,15 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "for-each": "^0.3.3", - "gopd": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -46302,6 +47489,25 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/xmlhttprequest-ssl": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", @@ -46467,9 +47673,9 @@ "license": "MIT" }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -46538,7 +47744,7 @@ "version": "1.0.0", "license": "UNLICENSED", "dependencies": { - "@amplitude/analytics-browser": "^2.11.10", + "@amplitude/analytics-browser": "^2.11.11", "@amplitude/analytics-node": "^1.3.6", "@amplitude/plugin-session-replay-browser": "^1.12.0" }, @@ -46546,11 +47752,6 @@ "@amplitude/analytics-types": "^2.8.0" } }, - "packages/attestation": { - "name": "@ethos/attestation", - "version": "1.0.0", - "license": "UNLICENSED" - }, "packages/blockchain-manager": { "name": "@ethos/blockchain-manager", "version": "1.0.0", @@ -46594,13 +47795,13 @@ "@ant-design/icons": "^5.5.2", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "antd": "^5.22.5", + "antd": "^5.23.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, "peerDependencies": { "@ant-design/icons": "^5.5.2", - "antd": "^5.22.5", + "antd": "^5.23.1", "react": "^18.3.1", "react-dom": "^18.3.1" } @@ -46639,10 +47840,10 @@ "@nomicfoundation/hardhat-verify": "^2.0.12", "@openzeppelin/contracts": "^5.1.0", "@openzeppelin/contracts-upgradeable": "^5.1.0", - "@openzeppelin/hardhat-upgrades": "^3.6.0", + "@openzeppelin/hardhat-upgrades": "^3.8.0", "@prb/math": "^4.1.0", "dotenv": "^16.4.5", - "hardhat": "^2.22.16", + "hardhat": "^2.22.17", "hardhat-gas-reporter": "^1.0.10", "solhint": "^5.0.3", "solidity-coverage": "^0.8.14", @@ -46659,7 +47860,8 @@ "dependencies": { "@ethos/blockchain-manager": "^1.0.0", "@ethos/score": "^1.0.0", - "viem": "^2.21.51" + "viem": "^2.21.51", + "zod": "^3.24.1" } }, "packages/echo-client": { @@ -46715,7 +47917,6 @@ "name": "@ethos/echo", "version": "0.0.1", "dependencies": { - "@ethos/attestation": "^1.0.0", "@ethos/blockchain-manager": "^1.0.0", "@ethos/config": "^1.0.0", "@ethos/contracts": "^1.0.0", @@ -46724,20 +47925,24 @@ "@ethos/helpers": "^1.0.0", "@ethos/logger": "^1.1.0", "@ethos/score": "^1.0.0", - "@prisma/client": "^6.1.0", - "@privy-io/server-auth": "^1.16.5", + "@prisma/client": "^6.2.1", + "@privy-io/server-auth": "^1.17.1", "@sentry/node": "^8.38.0", "@sentry/profiling-node": "^8.30.0", + "@superfaceai/passport-twitter-oauth2": "^1.2.4", "@the-convocation/twitter-scraper": "^0.14.1", "amqp-connection-manager": "^4.1.14", "amqplib": "^0.10.5", "compression": "^1.7.5", + "connect-redis": "^8.0.1", + "cookie": "^1.0.2", "cors": "^2.8.5", "cron": "^3.2.1", "date-fns": "^4.1.0", "ethers": "^6.13.4", "express": "^4.21.1", - "firebase-admin": "^12.7.0", + "express-session": "^1.18.1", + "firebase-admin": "^13.0.2", "helmet": "^8.0.0", "ioredis": "^5.4.1", "jwt-decode": "^4.0.0", @@ -46745,6 +47950,7 @@ "lru-cache": "^11.0.1", "on-finished": "^2.4.1", "parse-duration": "^1.1.0", + "passport": "^0.7.0", "pino-sentry-transport": "^1.3.0", "prom-client": "^15.1.3", "redlock2": "^5.0.0-beta.3", @@ -46754,17 +47960,29 @@ "zod": "^3.23.8" }, "devDependencies": { - "@types/amqplib": "^0.10.5", + "@types/amqplib": "^0.10.6", "@types/compression": "^1.7.5", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", + "@types/express-session": "^1.18.1", "@types/on-finished": "^2.3.4", + "@types/passport": "^1.0.17", + "@types/passport-oauth2": "^1.4.17", "pino": "^9.5.0", - "prisma": "^6.1.0", + "prisma": "^6.2.1", "tsx": "^4.19.2", "type-fest": "^4.26.1" } }, + "services/echo/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "services/echo/node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -46775,6 +47993,212 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "services/echo/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "services/echo/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "services/echo/node_modules/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", + "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.0.1", + "content-disposition": "^1.0.0", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "^1.2.1", + "debug": "4.3.6", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "^2.0.0", + "fresh": "2.0.0", + "http-errors": "2.0.0", + "merge-descriptors": "^2.0.0", + "methods": "~1.1.2", + "mime-types": "^3.0.0", + "on-finished": "2.4.1", + "once": "1.4.0", + "parseurl": "~1.3.3", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "router": "^2.0.0", + "safe-buffer": "5.2.1", + "send": "^1.1.0", + "serve-static": "^2.1.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "^2.0.0", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "services/echo/node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "services/echo/node_modules/express/node_modules/type-is": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", + "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "services/echo/node_modules/finalhandler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.0.0.tgz", + "integrity": "sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "services/echo/node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "services/echo/node_modules/finalhandler/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "services/echo/node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "services/echo/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "services/echo/node_modules/iconv-lite": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "services/echo/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "services/echo/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "services/echo/node_modules/mime-db": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", + "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "services/echo/node_modules/mime-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", + "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.53.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "services/echo/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "services/echo/node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -46786,6 +48210,116 @@ "node": ">= 0.8" } }, + "services/echo/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "services/echo/node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "services/echo/node_modules/send": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", + "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "http-errors": "^2.0.0", + "mime-types": "^2.1.35", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "services/echo/node_modules/send/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "services/echo/node_modules/send/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "services/echo/node_modules/send/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "services/echo/node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "services/echo/node_modules/serve-static": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", + "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "services/echo/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "services/emporos": { "name": "@ethos/emporos", "dependencies": { @@ -46803,24 +48337,25 @@ "@nivo/core": "^0.88.0", "@nivo/line": "^0.88.0", "@nivo/scales": "^0.88.0", - "@privy-io/react-auth": "^1.93.0", - "@privy-io/server-auth": "^1.16.6", - "@privy-io/wagmi": "^0.2.12", + "@privy-io/react-auth": "^1.98.4", + "@privy-io/server-auth": "^1.17.1", + "@privy-io/wagmi": "^0.2.13", "@radix-ui/react-focus-scope": "^1.1.0", "@remix-run/node": "^2.15.2", "@remix-run/react": "^2.15.2", "@remix-run/serve": "^2.15.2", "@resvg/resvg-js": "^2.6.2", - "@tanstack/react-query": "^5.62.3", - "antd": "^5.22.5", + "@tanstack/react-query": "^5.64.1", + "antd": "^5.23.1", "clsx": "^2.1.1", "cookie": "^1.0.2", "d3": "^7.9.0", "ethers": "^6.13.4", "ethers-decode-error": "^2.1.3", "isbot": "^5.1.17", + "lottie-react": "^2.4.0", "motion": "^11.12.0", - "prisma": "^6.1.0", + "prisma": "^6.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hotkeys-hook": "^4.6.1", @@ -46830,7 +48365,7 @@ "tailwindcss": "^3.4.15", "tailwindcss-safe-area": "^0.6.0", "ua-parser-js": "^2.0.0", - "vaul": "^1.1.1", + "vaul": "^1.1.2", "viem": "^2.21.51", "wagmi": "^2.12.30", "zod": "^3.23.8" @@ -46847,7 +48382,7 @@ "vite": "^5.4.11", "vite-plugin-cjs-interop": "^2.1.6", "vite-plugin-node-polyfills": "^0.22.0", - "vite-tsconfig-paths": "^5.1.3" + "vite-tsconfig-paths": "^5.1.4" } }, "services/emporos/node_modules/cookie": { @@ -46963,7 +48498,6 @@ "@ant-design/nextjs-registry": "^1.0.2", "@emotion/react": "^11.13.5", "@ethos/analytics": "^1.0.0", - "@ethos/attestation": "^1.0.0", "@ethos/blockchain-manager": "^1.0.0", "@ethos/common-ui": "^1.0.0", "@ethos/config": "^1.0.0", @@ -46976,16 +48510,17 @@ "@nivo/core": "^0.88.0", "@nivo/line": "^0.88.0", "@nivo/pie": "^0.88.0", - "@privy-io/react-auth": "^1.92.6", - "@privy-io/wagmi": "^0.2.12", + "@privy-io/react-auth": "^1.98.4", + "@privy-io/wagmi": "^0.2.13", "@react-aria/overlays": "^3.23.4", "@sentry/nextjs": "^8.38.0", "@statsig/react-bindings": "^3.7.0", - "@tanstack/react-query": "^5.62.3", - "@tanstack/react-query-devtools": "^5.62.3", - "@tanstack/react-query-persist-client": "^5.62.3", - "antd": "^5.22.5", + "@tanstack/react-query": "^5.64.1", + "@tanstack/react-query-devtools": "^5.64.1", + "@tanstack/react-query-persist-client": "^5.64.1", + "antd": "^5.23.1", "async-retry": "^1.3.3", + "cookies-next": "^5.0.2", "d3": "^7.9.0", "ethereum-blockies-base64": "^1.0.2", "ethers": "^6.13.4", @@ -47007,6 +48542,7 @@ "react-infinite-scroll-component": "^6.1.0", "react-intersection-observer": "^9.13.1", "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.6.1", "react-use-intercom": "^5.4.1", "serialize-error": "^11.0.3", "ua-parser-js": "^2.0.0", @@ -47021,6 +48557,7 @@ "@types/lodash-es": "^4.17.12", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", + "@types/react-syntax-highlighter": "^15.5.13", "@types/webpack-env": "^1.18.5", "type-fest": "^4.26.1" } @@ -47120,7 +48657,7 @@ "copy-webpack-plugin": "^12.0.2", "cross-env": "^7.0.3", "typescript": "5.7.2", - "webpack": "5.96.1", + "webpack": "5.97.1", "webpack-cli": "5.1.4", "webpack-glob-entries": "^1.0.1" } diff --git a/ethos/package-lock.json.rej b/ethos/package-lock.json.rej new file mode 100644 index 0000000..d0ff77b --- /dev/null +++ b/ethos/package-lock.json.rej @@ -0,0 +1,5016 @@ +diff a/ethos/package-lock.json b/ethos/package-lock.json (rejected hunks) +@@ -21,37 +21,37 @@ + "standalone/*" + ], + "devDependencies": { +- "@dotenvx/dotenvx": "^1.26.1", +- "@next/eslint-plugin-next": "^15.1.2", +- "@stylistic/eslint-plugin-js": "^2.11.0", +- "@tanstack/eslint-plugin-query": "^5.62.1", ++ "@dotenvx/dotenvx": "^1.32.0", ++ "@next/eslint-plugin-next": "^15.1.3", ++ "@stylistic/eslint-plugin-js": "^2.12.1", ++ "@tanstack/eslint-plugin-query": "^5.62.16", + "@types/inquirer": "^9.0.7", +- "@types/node": "^22.10.1", ++ "@types/node": "^22.10.2", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@vitest/coverage-v8": "^2.1.8", +- "concurrently": "^9.1.0", ++ "concurrently": "^9.1.1", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-config-love": "^44.0.0", + "eslint-config-prettier": "^9.1.0", +- "eslint-import-resolver-typescript": "^3.6.3", ++ "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-promise": "^6.6.0", +- "eslint-plugin-react": "^7.37.2", +- "eslint-plugin-react-hooks": "^5.0.0", ++ "eslint-plugin-react": "^7.37.3", ++ "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-vitest": "^0.4.1", + "glob": "^11.0.0", + "husky": "^9.1.7", + "inquirer": "^8.2.6", +- "knip": "^5.39.0", ++ "knip": "^5.41.1", + "open-cli": "^8.0.0", + "patch-package": "^8.0.0", + "picocolors": "^1.1.1", +- "prettier": "3.4.1", ++ "prettier": "3.4.2", + "serve": "^14.2.4", + "ts-node": "^10.9.2", + "tsx": "^4.19.1", +@@ -100,23 +100,23 @@ + } + }, + "node_modules/@amplitude/analytics-browser": { +- "version": "2.11.10", +- "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.11.10.tgz", +- "integrity": "sha512-+bc+KvAvrFTQefmq+iGDuzdtPdJm9HXHPI58yTnHJeXhXDhneH8cGmT6dfL1SvW/W2kGvBdFXiZMoPuHcR0xKg==", ++ "version": "2.11.11", ++ "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.11.11.tgz", ++ "integrity": "sha512-AdpNNPwoNPezojeeU2ITcyqKcrrW8edVBHlCEvDNIXYkf5Y0i5Blbes3x6rgONsOeV2hx85trTXhhVkilWgHcg==", + "dependencies": { +- "@amplitude/analytics-client-common": "^2.3.6", ++ "@amplitude/analytics-client-common": "^2.3.7", + "@amplitude/analytics-core": "^2.5.5", + "@amplitude/analytics-remote-config": "^0.4.0", + "@amplitude/analytics-types": "^2.8.4", + "@amplitude/plugin-autocapture-browser": "^1.0.2", +- "@amplitude/plugin-page-view-tracking-browser": "^2.3.6", ++ "@amplitude/plugin-page-view-tracking-browser": "^2.3.7", + "tslib": "^2.4.1" + } + }, + "node_modules/@amplitude/analytics-client-common": { +- "version": "2.3.6", +- "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.6.tgz", +- "integrity": "sha512-4KAaqevqzYJIUzgjBmH80QDmYgzUAE0JpKz2s7w6cjeoX9DrB1gBa+RqWySULiUeIDfx+DTlhsZPMzFn+og17w==", ++ "version": "2.3.7", ++ "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.7.tgz", ++ "integrity": "sha512-HuwP2MFoeCTZWFIxkeYZOy5GP9ydjRO+n2KUMhHXTXGUx1M9vxIx1BUHsHKOZ4BZ5qEUTacgmznyc6uJJUiCWg==", + "dependencies": { + "@amplitude/analytics-connector": "^1.4.8", + "@amplitude/analytics-core": "^2.5.5", +@@ -193,11 +193,11 @@ + } + }, + "node_modules/@amplitude/plugin-page-view-tracking-browser": { +- "version": "2.3.6", +- "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.6.tgz", +- "integrity": "sha512-3NsXskUxIA9m4GedGOCIKOkQ1uGUhuYnP/DpvSXXosJEMRSi82vFxBRX7FnYT1AmV32Du2Z2626CEAEy5W4yeQ==", ++ "version": "2.3.7", ++ "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.7.tgz", ++ "integrity": "sha512-9LEzU33vpQ1OdPwVn0nwcCqPLkfK3P19hLmFTflx+aBM70TH9xCwvJL6nJ5eyc4kkmE9x7r0mRVnQIxaHfTxGg==", + "dependencies": { +- "@amplitude/analytics-client-common": "^2.3.6", ++ "@amplitude/analytics-client-common": "^2.3.7", + "@amplitude/analytics-types": "^2.8.4", + "tslib": "^2.4.1" + } +@@ -309,11 +309,11 @@ + } + }, + "node_modules/@ant-design/colors": { +- "version": "7.1.0", +- "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.1.0.tgz", +- "integrity": "sha512-MMoDGWn1y9LdQJQSHiCC20x3uZ3CwQnv9QMz6pCmJOrqdgM9YxsoVVY0wtrdXbmfSgnV0KNk6zi09NAhMR2jvg==", ++ "version": "7.2.0", ++ "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.0.tgz", ++ "integrity": "sha512-bjTObSnZ9C/O8MB/B4OUtd/q9COomuJAR2SYfhxLyHvCKn4EKwCN3e+fWGMo7H5InAyV0wL17jdE9ALrdOW/6A==", + "dependencies": { +- "@ctrl/tinycolor": "^3.6.1" ++ "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/cssinjs": { +@@ -2586,14 +2586,6 @@ + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, +- "node_modules/@ctrl/tinycolor": { +- "version": "3.6.1", +- "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", +- "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", +- "engines": { +- "node": ">=10" +- } +- }, + "node_modules/@devnomic/marquee": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@devnomic/marquee/-/marquee-1.0.2.tgz", +@@ -2614,11 +2606,10 @@ + } + }, + "node_modules/@dotenvx/dotenvx": { +- "version": "1.26.1", +- "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.26.1.tgz", +- "integrity": "sha512-XsMk9WjHN3yZilBzwotWCBtA9Yc1R/4ca9zuXgAXshVr6YhKWVkNXbkm64Mlhw3SERHlCcrr4OVBd+B7B0Lhpw==", ++ "version": "1.32.0", ++ "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.32.0.tgz", ++ "integrity": "sha512-oQaGYijYfQx6pY9D+FQ08gUOckF1R0RSVK7Jqk+Ma2RyeceoMIawQl1KoogRaJ12i0SmyVWhiGyQxDU01/k13g==", + "dev": true, +- "license": "BSD-3-Clause", + "dependencies": { + "commander": "^11.1.0", + "dotenv": "^16.4.5", +@@ -4205,10 +4196,6 @@ + "resolved": "packages/analytics", + "link": true + }, +- "node_modules/@ethos/attestation": { +- "resolved": "packages/attestation", +- "link": true +- }, + "node_modules/@ethos/blockchain-manager": { + "resolved": "packages/blockchain-manager", + "link": true +@@ -5012,10 +4999,9 @@ + } + }, + "node_modules/@google-cloud/firestore": { +- "version": "7.10.0", +- "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.10.0.tgz", +- "integrity": "sha512-VFNhdHvfnmqcHHs6YhmSNHHxQqaaD64GwiL0c+e1qz85S8SWZPC2XFRf8p9yHRTF40Kow424s1KBU9f0fdQa+Q==", +- "license": "Apache-2.0", ++ "version": "7.11.0", ++ "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.0.tgz", ++ "integrity": "sha512-88uZ+jLsp1aVMj7gh3EKYH1aulTAMFAp8sH/v5a9w8q8iqSG27RiWLoxSAFr/XocZ9hGiWH1kEnBw+zl3xAgNA==", + "optional": true, + "dependencies": { + "@opentelemetry/api": "^1.3.0", +@@ -5958,7 +5944,6 @@ + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", +- "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", +@@ -7313,11 +7298,10 @@ + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { +- "version": "15.1.2", +- "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.2.tgz", +- "integrity": "sha512-sgfw3+WdaYOGPKCvM1L+UucBmRfh8V2Ygefp7ELON0+0vY7uohQwXXnVWg3rY7mXDKharQR3o7uedpfvnU2hlQ==", ++ "version": "15.1.3", ++ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.3.tgz", ++ "integrity": "sha512-oeP1vnc5Cq9UoOb8SYHAEPbCXMzOgG70l+Zfd+Ie00R25FOm+CCVNrcIubJvB1tvBgakXE37MmqSycksXVPRqg==", + "dev": true, +- "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } +@@ -7881,81 +7865,81 @@ + } + }, + "node_modules/@nomicfoundation/edr": { +- "version": "0.6.4", +- "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.6.4.tgz", +- "integrity": "sha512-YgrSuT3yo5ZQkbvBGqQ7hG+RDvz3YygSkddg4tb1Z0Y6pLXFzwrcEwWaJCFAVeeZxdxGfCgGMUYgRVneK+WXkw==", ++ "version": "0.6.5", ++ "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.6.5.tgz", ++ "integrity": "sha512-tAqMslLP+/2b2sZP4qe9AuGxG3OkQ5gGgHE4isUuq6dUVjwCRPFhAOhpdFl+OjY5P3yEv3hmq9HjUGRa2VNjng==", + "dev": true, + "dependencies": { +- "@nomicfoundation/edr-darwin-arm64": "0.6.4", +- "@nomicfoundation/edr-darwin-x64": "0.6.4", +- "@nomicfoundation/edr-linux-arm64-gnu": "0.6.4", +- "@nomicfoundation/edr-linux-arm64-musl": "0.6.4", +- "@nomicfoundation/edr-linux-x64-gnu": "0.6.4", +- "@nomicfoundation/edr-linux-x64-musl": "0.6.4", +- "@nomicfoundation/edr-win32-x64-msvc": "0.6.4" ++ "@nomicfoundation/edr-darwin-arm64": "0.6.5", ++ "@nomicfoundation/edr-darwin-x64": "0.6.5", ++ "@nomicfoundation/edr-linux-arm64-gnu": "0.6.5", ++ "@nomicfoundation/edr-linux-arm64-musl": "0.6.5", ++ "@nomicfoundation/edr-linux-x64-gnu": "0.6.5", ++ "@nomicfoundation/edr-linux-x64-musl": "0.6.5", ++ "@nomicfoundation/edr-win32-x64-msvc": "0.6.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-darwin-arm64": { +- "version": "0.6.4", +- "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.6.4.tgz", +- "integrity": "sha512-QNQErISLgssV9+qia8sIjRANqtbW8snSDvjspixT/kSQ5ZSGxxctTg7x72wPSrcu8+EBEveIe5uqENIp5GH8HQ==", ++ "version": "0.6.5", ++ "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.6.5.tgz", ++ "integrity": "sha512-A9zCCbbNxBpLgjS1kEJSpqxIvGGAX4cYbpDYCU2f3jVqOwaZ/NU761y1SvuCRVpOwhoCXqByN9b7HPpHi0L4hw==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-darwin-x64": { +- "version": "0.6.4", +- "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.6.4.tgz", +- "integrity": "sha512-cjVmREiwByyc9+oGfvAh49IAw+oVJHF9WWYRD+Tm/ZlSpnEVWxrGNBak2bd/JSYjn+mZE7gmWS4SMRi4nKaLUg==", ++ "version": "0.6.5", ++ "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.6.5.tgz", ++ "integrity": "sha512-x3zBY/v3R0modR5CzlL6qMfFMdgwd6oHrWpTkuuXnPFOX8SU31qq87/230f4szM+ukGK8Hi+mNq7Ro2VF4Fj+w==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { +- "version": "0.6.4", +- "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.6.4.tgz", +- "integrity": "sha512-96o9kRIVD6W5VkgKvUOGpWyUGInVQ5BRlME2Fa36YoNsRQMaKtmYJEU0ACosYES6ZTpYC8U5sjMulvPtVoEfOA==", ++ "version": "0.6.5", ++ "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.6.5.tgz", ++ "integrity": "sha512-HGpB8f1h8ogqPHTyUpyPRKZxUk2lu061g97dOQ/W4CxevI0s/qiw5DB3U3smLvSnBHKOzYS1jkxlMeGN01ky7A==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-arm64-musl": { +- "version": "0.6.4", +- "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.6.4.tgz", +- "integrity": "sha512-+JVEW9e5plHrUfQlSgkEj/UONrIU6rADTEk+Yp9pbe+mzNkJdfJYhs5JYiLQRP4OjxH4QOrXI97bKU6FcEbt5Q==", ++ "version": "0.6.5", ++ "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.6.5.tgz", ++ "integrity": "sha512-ESvJM5Y9XC03fZg9KaQg3Hl+mbx7dsSkTIAndoJS7X2SyakpL9KZpOSYrDk135o8s9P9lYJdPOyiq+Sh+XoCbQ==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-gnu": { +- "version": "0.6.4", +- "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.6.4.tgz", +- "integrity": "sha512-nzYWW+fO3EZItOeP4CrdMgDXfaGBIBkKg0Y/7ySpUxLqzut40O4Mb0/+quqLAFkacUSWMlFp8nsmypJfOH5zoA==", ++ "version": "0.6.5", ++ "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.6.5.tgz", ++ "integrity": "sha512-HCM1usyAR1Ew6RYf5AkMYGvHBy64cPA5NMbaeY72r0mpKaH3txiMyydcHibByOGdQ8iFLWpyUdpl1egotw+Tgg==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-linux-x64-musl": { +- "version": "0.6.4", +- "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.6.4.tgz", +- "integrity": "sha512-QFRoE9qSQ2boRrVeQ1HdzU+XN7NUgwZ1SIy5DQt4d7jCP+5qTNsq8LBNcqhRBOATgO63nsweNUhxX/Suj5r1Sw==", ++ "version": "0.6.5", ++ "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.6.5.tgz", ++ "integrity": "sha512-nB2uFRyczhAvWUH7NjCsIO6rHnQrof3xcCe6Mpmnzfl2PYcGyxN7iO4ZMmRcQS7R1Y670VH6+8ZBiRn8k43m7A==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@nomicfoundation/edr-win32-x64-msvc": { +- "version": "0.6.4", +- "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.6.4.tgz", +- "integrity": "sha512-2yopjelNkkCvIjUgBGhrn153IBPLwnsDeNiq6oA0WkeM8tGmQi4td+PGi9jAriUDAkc59Yoi2q9hYA6efiY7Zw==", ++ "version": "0.6.5", ++ "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.6.5.tgz", ++ "integrity": "sha512-B9QD/4DSSCFtWicO8A3BrsnitO1FPv7axB62wq5Q+qeJ50yJlTmyeGY3cw62gWItdvy2mh3fRM6L1LpnHiB77A==", + "dev": true, + "engines": { + "node": ">= 18" +@@ -9372,11 +9356,10 @@ + } + }, + "node_modules/@openzeppelin/hardhat-upgrades": { +- "version": "3.6.0", +- "resolved": "https://registry.npmjs.org/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-3.6.0.tgz", +- "integrity": "sha512-RuVuCciCfFOqCyKSJ2D4Zffp3hxhvXTn16JzTlD9cx3A7V/2d3JA75tpRHD7RVPic+dcSFIf+BZRWOHuhc2ayg==", ++ "version": "3.8.0", ++ "resolved": "https://registry.npmjs.org/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-3.8.0.tgz", ++ "integrity": "sha512-NwRk14ykTVilQqB0Vzd0vOWvUE8gpyn+SSwdzyHECRc5fuSSPDt/cIdadG9Hd6AKMPXfY+CS6G7q0+nDLng2Zw==", + "dev": true, +- "license": "MIT", + "dependencies": { + "@openzeppelin/defender-sdk-base-client": "^1.14.4", + "@openzeppelin/defender-sdk-deploy-client": "^1.14.4", +@@ -9843,9 +9826,9 @@ + "dev": true + }, + "node_modules/@prisma/client": { +- "version": "6.1.0", +- "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.1.0.tgz", +- "integrity": "sha512-AbQYc5+EJKm1Ydfq3KxwcGiy7wIbm4/QbjCKWWoNROtvy7d6a3gmAGkKjK0iUCzh+rHV8xDhD5Cge8ke/kiy5Q==", ++ "version": "6.2.1", ++ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.1.tgz", ++ "integrity": "sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA==", + "hasInstallScript": true, + "engines": { + "node": ">=18.18" +@@ -9860,43 +9843,43 @@ + } + }, + "node_modules/@prisma/debug": { +- "version": "6.1.0", +- "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.1.0.tgz", +- "integrity": "sha512-0himsvcM4DGBTtvXkd2Tggv6sl2JyUYLzEGXXleFY+7Kp6rZeSS3hiTW9mwtUlXrwYbJP6pwlVNB7jYElrjWUg==" ++ "version": "6.2.1", ++ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.1.tgz", ++ "integrity": "sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==" + }, + "node_modules/@prisma/engines": { +- "version": "6.1.0", +- "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.1.0.tgz", +- "integrity": "sha512-GnYJbCiep3Vyr1P/415ReYrgJUjP79fBNc1wCo7NP6Eia0CzL2Ot9vK7Infczv3oK7JLrCcawOSAxFxNFsAERQ==", ++ "version": "6.2.1", ++ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.2.1.tgz", ++ "integrity": "sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ==", + "hasInstallScript": true, + "dependencies": { +- "@prisma/debug": "6.1.0", +- "@prisma/engines-version": "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959", +- "@prisma/fetch-engine": "6.1.0", +- "@prisma/get-platform": "6.1.0" ++ "@prisma/debug": "6.2.1", ++ "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", ++ "@prisma/fetch-engine": "6.2.1", ++ "@prisma/get-platform": "6.2.1" + } + }, + "node_modules/@prisma/engines-version": { +- "version": "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959", +- "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959.tgz", +- "integrity": "sha512-PdJqmYM2Fd8K0weOOtQThWylwjsDlTig+8Pcg47/jszMuLL9iLIaygC3cjWJLda69siRW4STlCTMSgOjZzvKPQ==" ++ "version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", ++ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz", ++ "integrity": "sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ==" + }, + "node_modules/@prisma/fetch-engine": { +- "version": "6.1.0", +- "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.1.0.tgz", +- "integrity": "sha512-asdFi7TvPlEZ8CzSZ/+Du5wZ27q6OJbRSXh+S8ISZguu+S9KtS/gP7NeXceZyb1Jv1SM1S5YfiCv+STDsG6rrg==", ++ "version": "6.2.1", ++ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.2.1.tgz", ++ "integrity": "sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A==", + "dependencies": { +- "@prisma/debug": "6.1.0", +- "@prisma/engines-version": "6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959", +- "@prisma/get-platform": "6.1.0" ++ "@prisma/debug": "6.2.1", ++ "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", ++ "@prisma/get-platform": "6.2.1" + } + }, + "node_modules/@prisma/get-platform": { +- "version": "6.1.0", +- "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.1.0.tgz", +- "integrity": "sha512-ia8bNjboBoHkmKGGaWtqtlgQOhCi7+f85aOkPJKgNwWvYrT6l78KgojLekE8zMhVk0R9lWcifV0Pf8l3/15V0Q==", ++ "version": "6.2.1", ++ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.2.1.tgz", ++ "integrity": "sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q==", + "dependencies": { +- "@prisma/debug": "6.1.0" ++ "@prisma/debug": "6.2.1" + } + }, + "node_modules/@prisma/instrumentation": { +@@ -9940,9 +9923,9 @@ + } + }, + "node_modules/@privy-io/api-base": { +- "version": "1.4.0", +- "resolved": "https://registry.npmjs.org/@privy-io/api-base/-/api-base-1.4.0.tgz", +- "integrity": "sha512-8Pm/8bx6WvNt8uLtYOOj9acYL+JjUJxeChlBEvSywmre1l5o8naK6J4SeAb5v8b8p4178VNI4AYhd+rFh4HCsA==", ++ "version": "1.4.1", ++ "resolved": "https://registry.npmjs.org/@privy-io/api-base/-/api-base-1.4.1.tgz", ++ "integrity": "sha512-q98uQGVBIY5SBHjJWL/udpbxM9ISpZl8Lwwjd0p0XHSMJMOgEhS4GLjcO7l3clfNrqL0fAuinQaa+seCaYOzng==", + "dependencies": { + "zod": "^3.21.4" + }, +@@ -9952,10 +9935,9 @@ + } + }, + "node_modules/@privy-io/js-sdk-core": { +- "version": "0.32.0", +- "resolved": "https://registry.npmjs.org/@privy-io/js-sdk-core/-/js-sdk-core-0.32.0.tgz", +- "integrity": "sha512-ZTvGhGLYkY3Hlia/zAE5n/EdMHG8Y0oAJB4LaulM7duossTCiwp9bzP8gb2y1nyxAhsqdLITF3spP3PrdW/RAg==", +- "license": "Apache-2.0", ++ "version": "0.36.1", ++ "resolved": "https://registry.npmjs.org/@privy-io/js-sdk-core/-/js-sdk-core-0.36.1.tgz", ++ "integrity": "sha512-EPa8pYL+fh2Vj6s9ZvxowPiuNcqftXMydcAqGq974fz+ksjyB8kmTY0KHipifH7d8n6a+hqiOts9eE6ds3xyYg==", + "dependencies": { + "@ethersproject/abstract-signer": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", +@@ -9963,8 +9945,8 @@ + "@ethersproject/providers": "^5.7.2", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/units": "^5.7.0", +- "@privy-io/api-base": "^1.4.0", +- "@privy-io/public-api": "2.12.2", ++ "@privy-io/api-base": "^1.4.1", ++ "@privy-io/public-api": "2.15.8", + "eventemitter3": "^5.0.1", + "fetch-retry": "^5.0.6", + "jose": "^4.15.5", +@@ -9972,24 +9954,34 @@ + "libphonenumber-js": "^1.10.44", + "set-cookie-parser": "^2.6.0", + "uuid": ">=8 <10" ++ }, ++ "peerDependencies": { ++ "permissionless": "^0.2.10", ++ "viem": "^2.21.36" ++ }, ++ "peerDependenciesMeta": { ++ "permissionless": { ++ "optional": true ++ }, ++ "viem": { ++ "optional": true ++ } + } + }, + "node_modules/@privy-io/js-sdk-core/node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", +- "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@privy-io/public-api": { +- "version": "2.12.2", +- "resolved": "https://registry.npmjs.org/@privy-io/public-api/-/public-api-2.12.2.tgz", +- "integrity": "sha512-b7oZkL5sQrUnZdKnfZFiYCm65lBP5KNVQA0BKNWMRuZ9d38ZR7oxvgyYKYQjTDJoeo2Y07DnplvWUVkSwMdLBg==", +- "license": "Apache-2.0", ++ "version": "2.15.8", ++ "resolved": "https://registry.npmjs.org/@privy-io/public-api/-/public-api-2.15.8.tgz", ++ "integrity": "sha512-Aj29hJVKMy0mg5lOBRwl0JzV3F4CpaJd3T3RP2RnaL3no/ar65x/s2xfIyCOYZI/3D9DQBzE/JZzVr/vOnP4NA==", + "dependencies": { +- "@privy-io/api-base": "1.4.0", ++ "@privy-io/api-base": "1.4.1", + "bs58": "^5.0.0", + "ethers": "^5.7.2", + "libphonenumber-js": "^1.10.31", +@@ -10003,14 +9995,12 @@ + "node_modules/@privy-io/public-api/node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", +- "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==", +- "license": "MIT" ++ "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + }, + "node_modules/@privy-io/public-api/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", +- "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" + } +@@ -10029,7 +10019,6 @@ + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], +- "license": "MIT", + "dependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", +@@ -10064,10 +10053,9 @@ + } + }, + "node_modules/@privy-io/react-auth": { +- "version": "1.93.0", +- "resolved": "https://registry.npmjs.org/@privy-io/react-auth/-/react-auth-1.93.0.tgz", +- "integrity": "sha512-+s2QV/RUpltrQk595J3SVKJ0saQAz9rwdL+BzLLe14yx8EDYVV6QY2uGyenr/phG9ZsVaJ/NIimBG5wn9M572g==", +- "license": "Apache-2.0", ++ "version": "1.98.4", ++ "resolved": "https://registry.npmjs.org/@privy-io/react-auth/-/react-auth-1.98.4.tgz", ++ "integrity": "sha512-Uj91SnwqB0JORTIxy2SDqMB7f0vuGRmPTKftoC6N2tXGvOl0swMH/vWfawR+7jwFr+Y0VpPTRQOHzyjYfUHJLA==", + "dependencies": { + "@coinbase/wallet-sdk": "4.0.3", + "@ethersproject/abstract-signer": "^5.7.0", +@@ -10081,11 +10069,11 @@ + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/units": "^5.7.0", + "@floating-ui/react": "^0.26.22", +- "@headlessui/react": "^1.7.18", ++ "@headlessui/react": "^2.2.0", + "@heroicons/react": "^2.1.1", + "@marsidev/react-turnstile": "^0.4.1", + "@metamask/eth-sig-util": "^6.0.0", +- "@privy-io/js-sdk-core": "0.32.0", ++ "@privy-io/js-sdk-core": "0.36.1", + "@simplewebauthn/browser": "^9.0.1", + "@solana/wallet-adapter-base": "^0.9.23", + "@solana/wallet-standard-wallet-adapter-base": "^1.1.2", +@@ -10104,7 +10092,6 @@ + "md5": "^2.3.0", + "mipd": "^0.0.7", + "ofetch": "^1.3.4", +- "permissionless": "^0.2.10", + "pino-pretty": "^10.0.0", + "qrcode": "^1.5.1", + "react-device-detect": "^2.2.2", +@@ -10115,11 +10102,13 @@ + "uuid": ">=8 <10", + "viem": "^2.21.9", + "web3-core": "^1.8.0", +- "web3-core-helpers": "^1.8.0" ++ "web3-core-helpers": "^1.8.0", ++ "zustand": "^5.0.1" + }, + "peerDependencies": { +- "@abstract-foundation/agw-client": "^0.0.1-beta.14", +- "@solana/web3.js": "^1.95.3", ++ "@abstract-foundation/agw-client": "^0.1.0", ++ "@solana/web3.js": "^1.95.8", ++ "permissionless": "^0.2.10", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, +@@ -10129,6 +10118,9 @@ + }, + "@solana/web3.js": { + "optional": true ++ }, ++ "permissionless": { ++ "optional": true + } + } + }, +@@ -10248,15 +10240,42 @@ + "atomic-sleep": "^1.0.0" + } + }, ++ "node_modules/@privy-io/react-auth/node_modules/zustand": { ++ "version": "5.0.2", ++ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz", ++ "integrity": "sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==", ++ "engines": { ++ "node": ">=12.20.0" ++ }, ++ "peerDependencies": { ++ "@types/react": ">=18.0.0", ++ "immer": ">=9.0.6", ++ "react": ">=18.0.0", ++ "use-sync-external-store": ">=1.2.0" ++ }, ++ "peerDependenciesMeta": { ++ "@types/react": { ++ "optional": true ++ }, ++ "immer": { ++ "optional": true ++ }, ++ "react": { ++ "optional": true ++ }, ++ "use-sync-external-store": { ++ "optional": true ++ } ++ } ++ }, + "node_modules/@privy-io/server-auth": { +- "version": "1.16.6", +- "resolved": "https://registry.npmjs.org/@privy-io/server-auth/-/server-auth-1.16.6.tgz", +- "integrity": "sha512-bHn9WGbgn8JG4vo1VDdWrmvqspsZRD+ChGzrdSTyhMCguk7LML3yxwbc7urubnwYD1zOGNQUUPIexUyaQVm/eA==", +- "license": "Apache-2.0", ++ "version": "1.17.1", ++ "resolved": "https://registry.npmjs.org/@privy-io/server-auth/-/server-auth-1.17.1.tgz", ++ "integrity": "sha512-gHz2hr3EBm67gsP/eKnq2mUWhbXv+ukRBeCQA7Fig1yTWwJX/1CFMn+CLKp9vZ41SrskI0QjJD+i4JDx6DCe4Q==", + "dependencies": { + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", +- "@solana/web3.js": "^1.95.3", ++ "@solana/web3.js": "^1.95.8", + "canonicalize": "^2.0.0", + "dotenv": "^16.0.3", + "jose": "^4.10.4", +@@ -10307,14 +10326,12 @@ + } + }, + "node_modules/@privy-io/wagmi": { +- "version": "0.2.12", +- "resolved": "https://registry.npmjs.org/@privy-io/wagmi/-/wagmi-0.2.12.tgz", +- "integrity": "sha512-MJQVnrsA1BVT4SUOLCGiDB58qj0JvEaXBSle7R8yTXtmLuEfsRsXZTA9b0CKMe9wGUQfX4U6BligLBZBhreT0Q==", +- "license": "Apache-2.0", ++ "version": "0.2.13", ++ "resolved": "https://registry.npmjs.org/@privy-io/wagmi/-/wagmi-0.2.13.tgz", ++ "integrity": "sha512-NOyM43Kbc1K+hu3OwaSpKE1twhUEof9+jPefX4f1OLhbnEZGfcKAYP75O9xjc5rxTPgKfod9FSmt/xbKxJfUHw==", + "peerDependencies": { + "@privy-io/react-auth": "^1.64.1", +- "react": "^18", +- "react-dom": "^18", ++ "react": ">=18", + "viem": "^2", + "wagmi": "^2" + } +@@ -14349,10 +14366,9 @@ + } + }, + "node_modules/@solana/web3.js": { +- "version": "1.95.4", +- "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.95.4.tgz", +- "integrity": "sha512-sdewnNEA42ZSMxqkzdwEWi6fDgzwtJHaQa5ndUGEJYtoOnM6X5cvPmjoTUp7/k7bRrVAxfBgDnvQQHD6yhlLYw==", +- "license": "MIT", ++ "version": "1.98.0", ++ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.0.tgz", ++ "integrity": "sha512-nz3Q5OeyGFpFCR+erX2f6JPt3sKhzhYcSycBCSPkWjzSVDh/Rr1FqTVMRe58FKO16/ivTUcuJjeS5MyBvpkbzA==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", +@@ -14612,9 +14628,9 @@ + } + }, + "node_modules/@stylistic/eslint-plugin-js": { +- "version": "2.11.0", +- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.11.0.tgz", +- "integrity": "sha512-btchD0P3iij6cIk5RR5QMdEhtCCV0+L6cNheGhGCd//jaHILZMTi/EOqgEDAf1s4ZoViyExoToM+S2Iwa3U9DA==", ++ "version": "2.12.1", ++ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.12.1.tgz", ++ "integrity": "sha512-5ybogtEgWIGCR6dMnaabztbWyVdAPDsf/5XOk6jBonWug875Q9/a6gm9QxnU3rhdyDEnckWKX7dduwYJMOWrVA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^4.2.0", +@@ -14627,6 +14643,19 @@ + "eslint": ">=8.40.0" + } + }, ++ "node_modules/@superfaceai/passport-twitter-oauth2": { ++ "version": "1.2.4", ++ "resolved": "https://registry.npmjs.org/@superfaceai/passport-twitter-oauth2/-/passport-twitter-oauth2-1.2.4.tgz", ++ "integrity": "sha512-f2P4X1HqBHJuAnQefINpgJvMGLHx+nT9APbfJzN/CCBcazvP9NOD9IiENHMZMXEgfcHU40V3KwKiIZTBH5Q4Wg==", ++ "license": "MIT", ++ "dependencies": { ++ "passport-oauth2": "^1.6.1" ++ }, ++ "optionalDependencies": { ++ "@types/passport": "1.x", ++ "@types/passport-oauth2": ">=1.4" ++ } ++ }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", +@@ -14654,12 +14683,12 @@ + } + }, + "node_modules/@tanstack/eslint-plugin-query": { +- "version": "5.62.1", +- "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.1.tgz", +- "integrity": "sha512-1886D5U+re1TW0wSH4/kUGG36yIoW5Wkz4twVEzlk3ZWmjF3XkRSWgB+Sc7n+Lyzt8usNV8ZqkZE6DA7IC47fQ==", ++ "version": "5.62.16", ++ "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.16.tgz", ++ "integrity": "sha512-VhnHSQ/hc62olLzGhlLJ4BJGWynwjs3cDMsByasKJ3zjW1YZ+6raxOv0gHHISm+VEnAY42pkMowmSWrXfL4NTw==", + "dev": true, + "dependencies": { +- "@typescript-eslint/utils": "^8.15.0" ++ "@typescript-eslint/utils": "^8.18.1" + }, + "funding": { + "type": "github", +@@ -14670,13 +14699,13 @@ + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": { +- "version": "8.15.0", +- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz", +- "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==", ++ "version": "8.19.0", ++ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", ++ "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", + "dev": true, + "dependencies": { +- "@typescript-eslint/types": "8.15.0", +- "@typescript-eslint/visitor-keys": "8.15.0" ++ "@typescript-eslint/types": "8.19.0", ++ "@typescript-eslint/visitor-keys": "8.19.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" +@@ -14687,9 +14716,9 @@ + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": { +- "version": "8.15.0", +- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz", +- "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==", ++ "version": "8.19.0", ++ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", ++ "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" +@@ -14700,13 +14729,13 @@ + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": { +- "version": "8.15.0", +- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz", +- "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==", ++ "version": "8.19.0", ++ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", ++ "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", + "dev": true, + "dependencies": { +- "@typescript-eslint/types": "8.15.0", +- "@typescript-eslint/visitor-keys": "8.15.0", ++ "@typescript-eslint/types": "8.19.0", ++ "@typescript-eslint/visitor-keys": "8.19.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", +@@ -14721,22 +14750,20 @@ + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, +- "peerDependenciesMeta": { +- "typescript": { +- "optional": true +- } ++ "peerDependencies": { ++ "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": { +- "version": "8.15.0", +- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz", +- "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==", ++ "version": "8.19.0", ++ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", ++ "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", +- "@typescript-eslint/scope-manager": "8.15.0", +- "@typescript-eslint/types": "8.15.0", +- "@typescript-eslint/typescript-estree": "8.15.0" ++ "@typescript-eslint/scope-manager": "8.19.0", ++ "@typescript-eslint/types": "8.19.0", ++ "@typescript-eslint/typescript-estree": "8.19.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" +@@ -14746,21 +14773,17 @@ + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { +- "eslint": "^8.57.0 || ^9.0.0" +- }, +- "peerDependenciesMeta": { +- "typescript": { +- "optional": true +- } ++ "eslint": "^8.57.0 || ^9.0.0", ++ "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": { +- "version": "8.15.0", +- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz", +- "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==", ++ "version": "8.19.0", ++ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", ++ "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", + "dev": true, + "dependencies": { +- "@typescript-eslint/types": "8.15.0", ++ "@typescript-eslint/types": "8.19.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { +@@ -14772,30 +14795,29 @@ + } + }, + "node_modules/@tanstack/query-core": { +- "version": "5.62.3", +- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.3.tgz", +- "integrity": "sha512-Jp/nYoz8cnO7kqhOlSv8ke/0MJRJVGuZ0P/JO9KQ+f45mpN90hrerzavyTKeSoT/pOzeoOUkv1Xd0wPsxAWXfg==", ++ "version": "5.64.1", ++ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz", ++ "integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { +- "version": "5.61.4", +- "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.61.4.tgz", +- "integrity": "sha512-21Tw+u8E3IJJj4A/Bct4H0uBaDTEu7zBrR79FeSyY+mS2gx5/m316oDtJiKkILc819VSTYt+sFzODoJNcpPqZQ==", +- "license": "MIT", ++ "version": "5.62.16", ++ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz", ++ "integrity": "sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-persist-client-core": { +- "version": "5.62.3", +- "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.62.3.tgz", +- "integrity": "sha512-2cyrQAF1+yn/9GJf3kZnXjtFhlcCWOLL9PXG/JeMxRz8Ni0qvBbRIAXryKVJuDh570K0mmaaGs33pQb+FbmnEQ==", ++ "version": "5.64.1", ++ "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.64.1.tgz", ++ "integrity": "sha512-5SGe5IHKXJmkxL2gwKjHQrap18hY57kF0DVUd2yh6QLFn5pruUG4QO6DsjEuQGk33ZfxkX4Z7uJdZBgfMedA8g==", + "dependencies": { +- "@tanstack/query-core": "5.62.3" ++ "@tanstack/query-core": "5.64.1" + }, + "funding": { + "type": "github", +@@ -14803,11 +14825,11 @@ + } + }, + "node_modules/@tanstack/react-query": { +- "version": "5.62.3", +- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.3.tgz", +- "integrity": "sha512-y2zDNKuhgiuMgsKkqd4AcsLIBiCfEO8U11AdrtAUihmLbRNztPrlcZqx2lH1GacZsx+y1qRRbCcJLYTtF1vKsw==", ++ "version": "5.64.1", ++ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz", ++ "integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==", + "dependencies": { +- "@tanstack/query-core": "5.62.3" ++ "@tanstack/query-core": "5.64.1" + }, + "funding": { + "type": "github", +@@ -14818,34 +14840,34 @@ + } + }, + "node_modules/@tanstack/react-query-devtools": { +- "version": "5.62.3", +- "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.62.3.tgz", +- "integrity": "sha512-4iaQap/iP5ErS094u1WehFntHtjRo6g5HJMvyHovBVbsxnvgPc6AtKAw7qxPPoKy6Wj5Bew0045eYP5phiiBmw==", ++ "version": "5.64.1", ++ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.64.1.tgz", ++ "integrity": "sha512-8ajcGE3wXYlb4KuJnkFYkJwJKc/qmPNTpQD7YTgLRMBPTGGp1xk7VMzxL87DoXuweO8luplUUblJJ3noVs/luQ==", + "dependencies": { +- "@tanstack/query-devtools": "5.61.4" ++ "@tanstack/query-devtools": "5.62.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { +- "@tanstack/react-query": "^5.62.3", ++ "@tanstack/react-query": "^5.64.1", + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-persist-client": { +- "version": "5.62.3", +- "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.62.3.tgz", +- "integrity": "sha512-MoJzwerYUyjtBclcTcCwOYCfxVwyPKn7nDXNl7yAHlkLDRAJSxMl1bHIYNotIXSqbhenVLk9LICCOTg6RDgMxw==", ++ "version": "5.64.1", ++ "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.64.1.tgz", ++ "integrity": "sha512-2GmjTFU/QGF3p56UwnFW+N+WsKnzuNry3RQsyDtJ6/AQvy5nd567Mf58hc4bAj35nvI8MkuKCrzegRmHC8Wjxw==", + "dependencies": { +- "@tanstack/query-persist-client-core": "5.62.3" ++ "@tanstack/query-persist-client-core": "5.64.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { +- "@tanstack/react-query": "^5.62.3", ++ "@tanstack/react-query": "^5.64.1", + "react": "^18 || ^19" + } + }, +@@ -15016,11 +15038,10 @@ + } + }, + "node_modules/@types/amqplib": { +- "version": "0.10.5", +- "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.5.tgz", +- "integrity": "sha512-/cSykxROY7BWwDoi4Y4/jLAuZTshZxd8Ey1QYa/VaXriMotBDoou7V/twJiOSHzU6t1Kp1AHAUXGCgqq+6DNeg==", ++ "version": "0.10.6", ++ "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.6.tgz", ++ "integrity": "sha512-vQLVypBS1JQcfTXhl1Td1EEeLdtb+vuulOb4TrzYiLyP2aYLMAEzB3pNmEA0jBm0xIXu946Y7Xwl19Eidl32SQ==", + "dev": true, +- "license": "MIT", + "dependencies": { + "@types/node": "*" + } +@@ -15458,7 +15479,7 @@ + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", +- "dev": true, ++ "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", +@@ -15471,7 +15492,7 @@ + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", +- "dev": true, ++ "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", +@@ -15480,6 +15501,16 @@ + "@types/send": "*" + } + }, ++ "node_modules/@types/express-session": { ++ "version": "1.18.1", ++ "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz", ++ "integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==", ++ "dev": true, ++ "license": "MIT", ++ "dependencies": { ++ "@types/express": "*" ++ } ++ }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", +@@ -15641,7 +15672,6 @@ + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", +- "license": "MIT", + "optional": true + }, + "node_modules/@types/lru-cache": { +@@ -15702,10 +15732,9 @@ + } + }, + "node_modules/@types/node": { +- "version": "22.10.1", +- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", +- "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", +- "license": "MIT", ++ "version": "22.10.2", ++ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", ++ "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dependencies": { + "undici-types": "~6.20.0" + } +@@ -15732,6 +15761,16 @@ + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, ++ "node_modules/@types/oauth": { ++ "version": "0.9.6", ++ "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", ++ "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", ++ "devOptional": true, ++ "license": "MIT", ++ "dependencies": { ++ "@types/node": "*" ++ } ++ }, + "node_modules/@types/on-finished": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/on-finished/-/on-finished-2.3.4.tgz", +@@ -15746,6 +15785,28 @@ + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, ++ "node_modules/@types/passport": { ++ "version": "1.0.17", ++ "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", ++ "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", ++ "devOptional": true, ++ "license": "MIT", ++ "dependencies": { ++ "@types/express": "*" ++ } ++ }, ++ "node_modules/@types/passport-oauth2": { ++ "version": "1.4.17", ++ "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz", ++ "integrity": "sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==", ++ "devOptional": true, ++ "license": "MIT", ++ "dependencies": { ++ "@types/express": "*", ++ "@types/oauth": "*", ++ "@types/passport": "*" ++ } ++ }, + "node_modules/@types/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.2.tgz", +@@ -17598,133 +17669,133 @@ + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/@webassemblyjs/ast": { +- "version": "1.12.1", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", +- "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", ++ "version": "1.14.1", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", ++ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dependencies": { +- "@webassemblyjs/helper-numbers": "1.11.6", +- "@webassemblyjs/helper-wasm-bytecode": "1.11.6" ++ "@webassemblyjs/helper-numbers": "1.13.2", ++ "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { +- "version": "1.11.6", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", +- "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" ++ "version": "1.13.2", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", ++ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==" + }, + "node_modules/@webassemblyjs/helper-api-error": { +- "version": "1.11.6", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", +- "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" ++ "version": "1.13.2", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", ++ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" + }, + "node_modules/@webassemblyjs/helper-buffer": { +- "version": "1.12.1", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", +- "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" ++ "version": "1.14.1", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", ++ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==" + }, + "node_modules/@webassemblyjs/helper-numbers": { +- "version": "1.11.6", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", +- "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", ++ "version": "1.13.2", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", ++ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dependencies": { +- "@webassemblyjs/floating-point-hex-parser": "1.11.6", +- "@webassemblyjs/helper-api-error": "1.11.6", ++ "@webassemblyjs/floating-point-hex-parser": "1.13.2", ++ "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { +- "version": "1.11.6", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", +- "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" ++ "version": "1.13.2", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", ++ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { +- "version": "1.12.1", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", +- "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", ++ "version": "1.14.1", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", ++ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dependencies": { +- "@webassemblyjs/ast": "1.12.1", +- "@webassemblyjs/helper-buffer": "1.12.1", +- "@webassemblyjs/helper-wasm-bytecode": "1.11.6", +- "@webassemblyjs/wasm-gen": "1.12.1" ++ "@webassemblyjs/ast": "1.14.1", ++ "@webassemblyjs/helper-buffer": "1.14.1", ++ "@webassemblyjs/helper-wasm-bytecode": "1.13.2", ++ "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { +- "version": "1.11.6", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", +- "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", ++ "version": "1.13.2", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", ++ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { +- "version": "1.11.6", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", +- "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", ++ "version": "1.13.2", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", ++ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { +- "version": "1.11.6", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", +- "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" ++ "version": "1.13.2", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", ++ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==" + }, + "node_modules/@webassemblyjs/wasm-edit": { +- "version": "1.12.1", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", +- "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", ++ "version": "1.14.1", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", ++ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dependencies": { +- "@webassemblyjs/ast": "1.12.1", +- "@webassemblyjs/helper-buffer": "1.12.1", +- "@webassemblyjs/helper-wasm-bytecode": "1.11.6", +- "@webassemblyjs/helper-wasm-section": "1.12.1", +- "@webassemblyjs/wasm-gen": "1.12.1", +- "@webassemblyjs/wasm-opt": "1.12.1", +- "@webassemblyjs/wasm-parser": "1.12.1", +- "@webassemblyjs/wast-printer": "1.12.1" ++ "@webassemblyjs/ast": "1.14.1", ++ "@webassemblyjs/helper-buffer": "1.14.1", ++ "@webassemblyjs/helper-wasm-bytecode": "1.13.2", ++ "@webassemblyjs/helper-wasm-section": "1.14.1", ++ "@webassemblyjs/wasm-gen": "1.14.1", ++ "@webassemblyjs/wasm-opt": "1.14.1", ++ "@webassemblyjs/wasm-parser": "1.14.1", ++ "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { +- "version": "1.12.1", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", +- "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", ++ "version": "1.14.1", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", ++ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dependencies": { +- "@webassemblyjs/ast": "1.12.1", +- "@webassemblyjs/helper-wasm-bytecode": "1.11.6", +- "@webassemblyjs/ieee754": "1.11.6", +- "@webassemblyjs/leb128": "1.11.6", +- "@webassemblyjs/utf8": "1.11.6" ++ "@webassemblyjs/ast": "1.14.1", ++ "@webassemblyjs/helper-wasm-bytecode": "1.13.2", ++ "@webassemblyjs/ieee754": "1.13.2", ++ "@webassemblyjs/leb128": "1.13.2", ++ "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { +- "version": "1.12.1", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", +- "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", ++ "version": "1.14.1", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", ++ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dependencies": { +- "@webassemblyjs/ast": "1.12.1", +- "@webassemblyjs/helper-buffer": "1.12.1", +- "@webassemblyjs/wasm-gen": "1.12.1", +- "@webassemblyjs/wasm-parser": "1.12.1" ++ "@webassemblyjs/ast": "1.14.1", ++ "@webassemblyjs/helper-buffer": "1.14.1", ++ "@webassemblyjs/wasm-gen": "1.14.1", ++ "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { +- "version": "1.12.1", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", +- "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", ++ "version": "1.14.1", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", ++ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dependencies": { +- "@webassemblyjs/ast": "1.12.1", +- "@webassemblyjs/helper-api-error": "1.11.6", +- "@webassemblyjs/helper-wasm-bytecode": "1.11.6", +- "@webassemblyjs/ieee754": "1.11.6", +- "@webassemblyjs/leb128": "1.11.6", +- "@webassemblyjs/utf8": "1.11.6" ++ "@webassemblyjs/ast": "1.14.1", ++ "@webassemblyjs/helper-api-error": "1.13.2", ++ "@webassemblyjs/helper-wasm-bytecode": "1.13.2", ++ "@webassemblyjs/ieee754": "1.13.2", ++ "@webassemblyjs/leb128": "1.13.2", ++ "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { +- "version": "1.12.1", +- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", +- "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", ++ "version": "1.14.1", ++ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", ++ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dependencies": { +- "@webassemblyjs/ast": "1.12.1", ++ "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, +@@ -18184,17 +18255,17 @@ + } + }, + "node_modules/antd": { +- "version": "5.22.5", +- "resolved": "https://registry.npmjs.org/antd/-/antd-5.22.5.tgz", +- "integrity": "sha512-+0UP8w+ULVv2OIzCDVz7j6I0UfH6mMLHSWO6qzpBc+9psOoVQLRbyAE21XnZM/eGrt2MNsEDL5fmlhXL/V8JyQ==", ++ "version": "5.23.1", ++ "resolved": "https://registry.npmjs.org/antd/-/antd-5.23.1.tgz", ++ "integrity": "sha512-rg5xd5LotHw0IRyo/nsiUN/EEV3e+xU4V4UmIb/62hMN9+3APyz1Ohjf17a+fN13jC8sNY1hP1K252SU2Th0xA==", + "dependencies": { +- "@ant-design/colors": "^7.1.0", +- "@ant-design/cssinjs": "^1.21.1", ++ "@ant-design/colors": "^7.2.0", ++ "@ant-design/cssinjs": "^1.22.0", + "@ant-design/cssinjs-utils": "^1.1.3", ++ "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.5.2", + "@ant-design/react-slick": "~1.1.2", +- "@babel/runtime": "^7.25.7", +- "@ctrl/tinycolor": "^3.6.1", ++ "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.0.0", +@@ -18203,38 +18274,38 @@ + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", +- "rc-cascader": "~3.30.0", +- "rc-checkbox": "~3.3.0", ++ "rc-cascader": "~3.33.0", ++ "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.2.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.0", + "rc-image": "~7.11.0", +- "rc-input": "~1.6.4", +- "rc-input-number": "~9.3.0", +- "rc-mentions": "~2.17.0", ++ "rc-input": "~1.7.2", ++ "rc-input-number": "~9.4.0", ++ "rc-mentions": "~2.19.1", + "rc-menu": "~9.16.0", +- "rc-motion": "^2.9.4", ++ "rc-motion": "^2.9.5", + "rc-notification": "~5.6.2", + "rc-pagination": "~5.0.0", +- "rc-picker": "~4.8.3", ++ "rc-picker": "~4.9.2", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.0", +- "rc-resize-observer": "^1.4.1", +- "rc-segmented": "~2.5.0", +- "rc-select": "~14.16.4", +- "rc-slider": "~11.1.7", ++ "rc-resize-observer": "^1.4.3", ++ "rc-segmented": "~2.7.0", ++ "rc-select": "~14.16.5", ++ "rc-slider": "~11.1.8", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", +- "rc-table": "~7.49.0", +- "rc-tabs": "~15.4.0", +- "rc-textarea": "~1.8.2", +- "rc-tooltip": "~6.2.1", +- "rc-tree": "~5.10.1", +- "rc-tree-select": "~5.24.5", ++ "rc-table": "~7.50.2", ++ "rc-tabs": "~15.5.0", ++ "rc-textarea": "~1.9.0", ++ "rc-tooltip": "~6.3.2", ++ "rc-tree": "~5.13.0", ++ "rc-tree-select": "~5.27.0", + "rc-upload": "~4.8.1", +- "rc-util": "^5.44.2", ++ "rc-util": "^5.44.3", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, +@@ -18334,13 +18405,13 @@ + } + }, + "node_modules/array-buffer-byte-length": { +- "version": "1.0.1", +- "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", +- "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", ++ "version": "1.0.2", ++ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", ++ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.5", +- "is-array-buffer": "^3.0.4" ++ "call-bound": "^1.0.3", ++ "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" +@@ -18451,15 +18522,15 @@ + } + }, + "node_modules/array.prototype.flatmap": { +- "version": "1.3.2", +- "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", +- "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", ++ "version": "1.3.3", ++ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", ++ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.2", +- "define-properties": "^1.2.0", +- "es-abstract": "^1.22.1", +- "es-shim-unscopables": "^1.0.0" ++ "call-bind": "^1.0.8", ++ "define-properties": "^1.2.1", ++ "es-abstract": "^1.23.5", ++ "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" +@@ -18485,19 +18556,18 @@ + } + }, + "node_modules/arraybuffer.prototype.slice": { +- "version": "1.0.3", +- "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", +- "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", ++ "version": "1.0.4", ++ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", ++ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", +- "call-bind": "^1.0.5", ++ "call-bind": "^1.0.8", + "define-properties": "^1.2.1", +- "es-abstract": "^1.22.3", +- "es-errors": "^1.2.1", +- "get-intrinsic": "^1.2.3", +- "is-array-buffer": "^3.0.4", +- "is-shared-array-buffer": "^1.0.2" ++ "es-abstract": "^1.23.5", ++ "es-errors": "^1.3.0", ++ "get-intrinsic": "^1.2.6", ++ "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" +@@ -19139,6 +19209,15 @@ + } + ] + }, ++ "node_modules/base64url": { ++ "version": "3.0.1", ++ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", ++ "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", ++ "license": "MIT", ++ "engines": { ++ "node": ">=6.0.0" ++ } ++ }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", +@@ -19898,15 +19977,41 @@ + } + }, + "node_modules/call-bind": { +- "version": "1.0.7", +- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", +- "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", ++ "version": "1.0.8", ++ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", ++ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { ++ "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", +- "es-errors": "^1.3.0", +- "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", +- "set-function-length": "^1.2.1" ++ "set-function-length": "^1.2.2" ++ }, ++ "engines": { ++ "node": ">= 0.4" ++ }, ++ "funding": { ++ "url": "https://github.com/sponsors/ljharb" ++ } ++ }, ++ "node_modules/call-bind-apply-helpers": { ++ "version": "1.0.1", ++ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", ++ "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", ++ "dependencies": { ++ "es-errors": "^1.3.0", ++ "function-bind": "^1.1.2" ++ }, ++ "engines": { ++ "node": ">= 0.4" ++ } ++ }, ++ "node_modules/call-bound": { ++ "version": "1.0.3", ++ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", ++ "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", ++ "dependencies": { ++ "call-bind-apply-helpers": "^1.0.1", ++ "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" +@@ -21020,9 +21125,9 @@ + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/concurrently": { +- "version": "9.1.0", +- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.0.tgz", +- "integrity": "sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==", ++ "version": "9.1.1", ++ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.1.tgz", ++ "integrity": "sha512-6VX8lrBIycgZKTwBsWS+bLrmkGRkDmvtGsYylRN9b93CygN6CbK46HmnQ3rdSOR8HRjdahDrxb5MqD9cEFOg5Q==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", +@@ -21076,6 +21181,18 @@ + "node": ">= 0.10.0" + } + }, ++ "node_modules/connect-redis": { ++ "version": "8.0.1", ++ "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-8.0.1.tgz", ++ "integrity": "sha512-7iOI214/r15ahvu0rqKCHhsgpMdOgyLwqlw/icSTnnAR75xFvMyfxAE+je4M87rZLjDlKzKcTc48XxQXYFsMgA==", ++ "license": "MIT", ++ "engines": { ++ "node": ">=18" ++ }, ++ "peerDependencies": { ++ "express-session": ">=1" ++ } ++ }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", +@@ -21160,6 +21277,27 @@ + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, ++ "node_modules/cookies-next": { ++ "version": "5.0.2", ++ "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-5.0.2.tgz", ++ "integrity": "sha512-Ft5yXMbN6wMfgLiL5TMyPnxsjkW6UEjZw0YMoDMiF3F6iYdFPjiJEMugx/sivUr8G+0xPG80lBYqI2b+VquSuw==", ++ "license": "MIT", ++ "dependencies": { ++ "cookie": "^1.0.1" ++ }, ++ "peerDependencies": { ++ "next": ">=15.0.0" ++ } ++ }, ++ "node_modules/cookies-next/node_modules/cookie": { ++ "version": "1.0.2", ++ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", ++ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", ++ "license": "MIT", ++ "engines": { ++ "node": ">=18" ++ } ++ }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", +@@ -21607,6 +21745,20 @@ + "node": ">=4" + } + }, ++ "node_modules/cssstyle": { ++ "version": "4.1.0", ++ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", ++ "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "rrweb-cssom": "^0.7.1" ++ }, ++ "engines": { ++ "node": ">=18" ++ } ++ }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", +@@ -22013,15 +22165,70 @@ + "node": ">= 6" + } + }, ++ "node_modules/data-urls": { ++ "version": "5.0.0", ++ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", ++ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "whatwg-mimetype": "^4.0.0", ++ "whatwg-url": "^14.0.0" ++ }, ++ "engines": { ++ "node": ">=18" ++ } ++ }, ++ "node_modules/data-urls/node_modules/tr46": { ++ "version": "5.0.0", ++ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", ++ "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "punycode": "^2.3.1" ++ }, ++ "engines": { ++ "node": ">=18" ++ } ++ }, ++ "node_modules/data-urls/node_modules/webidl-conversions": { ++ "version": "7.0.0", ++ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", ++ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", ++ "license": "BSD-2-Clause", ++ "optional": true, ++ "peer": true, ++ "engines": { ++ "node": ">=12" ++ } ++ }, ++ "node_modules/data-urls/node_modules/whatwg-url": { ++ "version": "14.1.0", ++ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", ++ "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "tr46": "^5.0.0", ++ "webidl-conversions": "^7.0.0" ++ }, ++ "engines": { ++ "node": ">=18" ++ } ++ }, + "node_modules/data-view-buffer": { +- "version": "1.0.1", +- "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", +- "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", ++ "version": "1.0.2", ++ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", ++ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.6", ++ "call-bound": "^1.0.3", + "es-errors": "^1.3.0", +- "is-data-view": "^1.0.1" ++ "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" +@@ -22031,29 +22238,29 @@ + } + }, + "node_modules/data-view-byte-length": { +- "version": "1.0.1", +- "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", +- "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", ++ "version": "1.0.2", ++ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", ++ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.7", ++ "call-bound": "^1.0.3", + "es-errors": "^1.3.0", +- "is-data-view": "^1.0.1" ++ "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { +- "url": "https://github.com/sponsors/ljharb" ++ "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { +- "version": "1.0.0", +- "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", +- "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", ++ "version": "1.0.1", ++ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", ++ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.6", ++ "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, +@@ -22122,6 +22329,14 @@ + "node": ">=0.10.0" + } + }, ++ "node_modules/decimal.js": { ++ "version": "10.4.3", ++ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", ++ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true ++ }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", +@@ -22689,6 +22904,19 @@ + "url": "https://dotenvx.com" + } + }, ++ "node_modules/dunder-proto": { ++ "version": "1.0.1", ++ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", ++ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", ++ "dependencies": { ++ "call-bind-apply-helpers": "^1.0.1", ++ "es-errors": "^1.3.0", ++ "gopd": "^1.2.0" ++ }, ++ "engines": { ++ "node": ">= 0.4" ++ } ++ }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", +@@ -22850,6 +23078,20 @@ + "node": ">=8.6" + } + }, ++ "node_modules/entities": { ++ "version": "4.5.0", ++ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", ++ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", ++ "license": "BSD-2-Clause", ++ "optional": true, ++ "peer": true, ++ "engines": { ++ "node": ">=0.12" ++ }, ++ "funding": { ++ "url": "https://github.com/fb55/entities?sponsor=1" ++ } ++ }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", +@@ -22898,57 +23140,60 @@ + } + }, + "node_modules/es-abstract": { +- "version": "1.23.3", +- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", +- "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", ++ "version": "1.23.8", ++ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.8.tgz", ++ "integrity": "sha512-lfab8IzDn6EpI1ibZakcgS6WsfEBiB+43cuJo+wgylx1xKXf+Sp+YR3vFuQwC/u3sxYwV8Cxe3B0DpVUu/WiJQ==", + "dev": true, + "dependencies": { +- "array-buffer-byte-length": "^1.0.1", +- "arraybuffer.prototype.slice": "^1.0.3", ++ "array-buffer-byte-length": "^1.0.2", ++ "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", +- "call-bind": "^1.0.7", +- "data-view-buffer": "^1.0.1", +- "data-view-byte-length": "^1.0.1", +- "data-view-byte-offset": "^1.0.0", +- "es-define-property": "^1.0.0", ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.3", ++ "data-view-buffer": "^1.0.2", ++ "data-view-byte-length": "^1.0.2", ++ "data-view-byte-offset": "^1.0.1", ++ "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", +- "es-to-primitive": "^1.2.1", +- "function.prototype.name": "^1.1.6", +- "get-intrinsic": "^1.2.4", +- "get-symbol-description": "^1.0.2", +- "globalthis": "^1.0.3", +- "gopd": "^1.0.1", ++ "es-to-primitive": "^1.3.0", ++ "function.prototype.name": "^1.1.8", ++ "get-intrinsic": "^1.2.6", ++ "get-symbol-description": "^1.1.0", ++ "globalthis": "^1.0.4", ++ "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", +- "has-proto": "^1.0.3", +- "has-symbols": "^1.0.3", ++ "has-proto": "^1.2.0", ++ "has-symbols": "^1.1.0", + "hasown": "^2.0.2", +- "internal-slot": "^1.0.7", +- "is-array-buffer": "^3.0.4", ++ "internal-slot": "^1.1.0", ++ "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", +- "is-data-view": "^1.0.1", +- "is-negative-zero": "^2.0.3", +- "is-regex": "^1.1.4", +- "is-shared-array-buffer": "^1.0.3", +- "is-string": "^1.0.7", +- "is-typed-array": "^1.1.13", +- "is-weakref": "^1.0.2", +- "object-inspect": "^1.13.1", ++ "is-data-view": "^1.0.2", ++ "is-regex": "^1.2.1", ++ "is-shared-array-buffer": "^1.0.4", ++ "is-string": "^1.1.1", ++ "is-typed-array": "^1.1.15", ++ "is-weakref": "^1.1.0", ++ "math-intrinsics": "^1.1.0", ++ "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", +- "object.assign": "^4.1.5", +- "regexp.prototype.flags": "^1.5.2", +- "safe-array-concat": "^1.1.2", +- "safe-regex-test": "^1.0.3", +- "string.prototype.trim": "^1.2.9", +- "string.prototype.trimend": "^1.0.8", ++ "object.assign": "^4.1.7", ++ "own-keys": "^1.0.0", ++ "regexp.prototype.flags": "^1.5.3", ++ "safe-array-concat": "^1.1.3", ++ "safe-push-apply": "^1.0.0", ++ "safe-regex-test": "^1.1.0", ++ "string.prototype.trim": "^1.2.10", ++ "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", +- "typed-array-buffer": "^1.0.2", +- "typed-array-byte-length": "^1.0.1", +- "typed-array-byte-offset": "^1.0.2", +- "typed-array-length": "^1.0.6", +- "unbox-primitive": "^1.0.2", +- "which-typed-array": "^1.1.15" ++ "typed-array-buffer": "^1.0.3", ++ "typed-array-byte-length": "^1.0.3", ++ "typed-array-byte-offset": "^1.0.4", ++ "typed-array-length": "^1.0.7", ++ "unbox-primitive": "^1.1.0", ++ "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" +@@ -22958,12 +23203,9 @@ + } + }, + "node_modules/es-define-property": { +- "version": "1.0.0", +- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", +- "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", +- "dependencies": { +- "get-intrinsic": "^1.2.4" +- }, ++ "version": "1.0.1", ++ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", ++ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } +@@ -22977,26 +23219,27 @@ + } + }, + "node_modules/es-iterator-helpers": { +- "version": "1.1.0", +- "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz", +- "integrity": "sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==", ++ "version": "1.2.1", ++ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", ++ "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, +- "license": "MIT", + "dependencies": { +- "call-bind": "^1.0.7", ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.3", + "define-properties": "^1.2.1", +- "es-abstract": "^1.23.3", ++ "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", +- "get-intrinsic": "^1.2.4", ++ "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", ++ "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", +- "has-proto": "^1.0.3", +- "has-symbols": "^1.0.3", +- "internal-slot": "^1.0.7", +- "iterator.prototype": "^1.1.3", +- "safe-array-concat": "^1.1.2" ++ "has-proto": "^1.2.0", ++ "has-symbols": "^1.1.0", ++ "internal-slot": "^1.1.0", ++ "iterator.prototype": "^1.1.4", ++ "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" +@@ -23011,7 +23254,6 @@ + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", +- "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, +@@ -23043,14 +23285,14 @@ + } + }, + "node_modules/es-to-primitive": { +- "version": "1.2.1", +- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", +- "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", ++ "version": "1.3.0", ++ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", ++ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "dependencies": { +- "is-callable": "^1.1.4", +- "is-date-object": "^1.0.1", +- "is-symbol": "^1.0.2" ++ "is-callable": "^1.2.7", ++ "is-date-object": "^1.0.5", ++ "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" +@@ -23453,19 +23695,19 @@ + } + }, + "node_modules/eslint-import-resolver-typescript": { +- "version": "3.6.3", +- "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz", +- "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==", ++ "version": "3.7.0", ++ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", ++ "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", + "dev": true, + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", +- "debug": "^4.3.5", ++ "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", +- "eslint-module-utils": "^2.8.1", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", +- "is-glob": "^4.0.3" ++ "is-glob": "^4.0.3", ++ "stable-hash": "^0.0.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" +@@ -23488,12 +23730,12 @@ + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/debug": { +- "version": "4.3.6", +- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", +- "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", ++ "version": "4.4.0", ++ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", ++ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { +- "ms": "2.1.2" ++ "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" +@@ -23504,6 +23746,12 @@ + } + } + }, ++ "node_modules/eslint-import-resolver-typescript/node_modules/ms": { ++ "version": "2.1.3", ++ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", ++ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", ++ "dev": true ++ }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", +@@ -23731,28 +23979,28 @@ + } + }, + "node_modules/eslint-plugin-react": { +- "version": "7.37.2", +- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", +- "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", ++ "version": "7.37.3", ++ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz", ++ "integrity": "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", +- "array.prototype.flatmap": "^1.3.2", ++ "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", +- "es-iterator-helpers": "^1.1.0", ++ "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", +- "object.values": "^1.2.0", ++ "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", +- "string.prototype.matchall": "^4.0.11", ++ "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { +@@ -23763,9 +24011,9 @@ + } + }, + "node_modules/eslint-plugin-react-hooks": { +- "version": "5.0.0", +- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz", +- "integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==", ++ "version": "5.1.0", ++ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", ++ "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", + "dev": true, + "engines": { + "node": ">=10" +@@ -24889,8 +25137,61 @@ + }, + "engines": { + "node": ">= 0.10.0" ++ }, ++ "funding": { ++ "type": "opencollective", ++ "url": "https://opencollective.com/express" ++ } ++ }, ++ "node_modules/express-session": { ++ "version": "1.18.1", ++ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", ++ "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", ++ "license": "MIT", ++ "dependencies": { ++ "cookie": "0.7.2", ++ "cookie-signature": "1.0.7", ++ "debug": "2.6.9", ++ "depd": "~2.0.0", ++ "on-headers": "~1.0.2", ++ "parseurl": "~1.3.3", ++ "safe-buffer": "5.2.1", ++ "uid-safe": "~2.1.5" ++ }, ++ "engines": { ++ "node": ">= 0.8.0" ++ } ++ }, ++ "node_modules/express-session/node_modules/cookie": { ++ "version": "0.7.2", ++ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", ++ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", ++ "license": "MIT", ++ "engines": { ++ "node": ">= 0.6" ++ } ++ }, ++ "node_modules/express-session/node_modules/cookie-signature": { ++ "version": "1.0.7", ++ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", ++ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", ++ "license": "MIT" ++ }, ++ "node_modules/express-session/node_modules/debug": { ++ "version": "2.6.9", ++ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", ++ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", ++ "license": "MIT", ++ "dependencies": { ++ "ms": "2.0.0" + } + }, ++ "node_modules/express-session/node_modules/ms": { ++ "version": "2.0.0", ++ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", ++ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", ++ "license": "MIT" ++ }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", +@@ -25208,8 +25509,7 @@ + "node_modules/fetch-retry": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.6.tgz", +- "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==", +- "license": "MIT" ++ "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==" + }, + "node_modules/fflate": { + "version": "0.4.8", +@@ -25539,27 +25839,27 @@ + } + }, + "node_modules/firebase-admin": { +- "version": "12.7.0", +- "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz", +- "integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==", +- "license": "Apache-2.0", ++ "version": "13.0.2", ++ "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.0.2.tgz", ++ "integrity": "sha512-YWVpoN+tZVSRXF0qC0gojoF5bSqvBRbnBk8+xUtFiguM2L4vB7f0moAwV1VVWDDHvTnvQ68OyTMpdp6wKo/clw==", + "dependencies": { + "@fastify/busboy": "^3.0.0", +- "@firebase/database-compat": "1.0.8", +- "@firebase/database-types": "1.0.5", +- "@types/node": "^22.0.1", ++ "@firebase/database-compat": "^2.0.0", ++ "@firebase/database-types": "^1.0.6", ++ "@types/node": "^22.8.7", + "farmhash-modern": "^1.1.0", ++ "google-auth-library": "^9.14.2", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", +- "uuid": "^10.0.0" ++ "uuid": "^11.0.2" + }, + "engines": { +- "node": ">=14" ++ "node": ">=18" + }, + "optionalDependencies": { +- "@google-cloud/firestore": "^7.7.0", +- "@google-cloud/storage": "^7.7.0" ++ "@google-cloud/firestore": "^7.11.0", ++ "@google-cloud/storage": "^7.14.0" + } + }, + "node_modules/firebase-admin/node_modules/@fastify/busboy": { +@@ -25568,17 +25868,107 @@ + "integrity": "sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==", + "license": "MIT" + }, ++ "node_modules/firebase-admin/node_modules/@firebase/app-check-interop-types": { ++ "version": "0.3.3", ++ "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", ++ "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==" ++ }, ++ "node_modules/firebase-admin/node_modules/@firebase/app-types": { ++ "version": "0.9.3", ++ "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", ++ "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==" ++ }, ++ "node_modules/firebase-admin/node_modules/@firebase/auth-interop-types": { ++ "version": "0.2.4", ++ "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", ++ "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==" ++ }, ++ "node_modules/firebase-admin/node_modules/@firebase/component": { ++ "version": "0.6.11", ++ "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.11.tgz", ++ "integrity": "sha512-eQbeCgPukLgsKD0Kw5wQgsMDX5LeoI1MIrziNDjmc6XDq5ZQnuUymANQgAb2wp1tSF9zDSXyxJmIUXaKgN58Ug==", ++ "dependencies": { ++ "@firebase/util": "1.10.2", ++ "tslib": "^2.1.0" ++ }, ++ "engines": { ++ "node": ">=18.0.0" ++ } ++ }, ++ "node_modules/firebase-admin/node_modules/@firebase/database": { ++ "version": "1.0.10", ++ "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.10.tgz", ++ "integrity": "sha512-sWp2g92u7xT4BojGbTXZ80iaSIaL6GAL0pwvM0CO/hb0nHSnABAqsH7AhnWGsGvXuEvbPr7blZylPaR9J+GSuQ==", ++ "dependencies": { ++ "@firebase/app-check-interop-types": "0.3.3", ++ "@firebase/auth-interop-types": "0.2.4", ++ "@firebase/component": "0.6.11", ++ "@firebase/logger": "0.4.4", ++ "@firebase/util": "1.10.2", ++ "faye-websocket": "0.11.4", ++ "tslib": "^2.1.0" ++ }, ++ "engines": { ++ "node": ">=18.0.0" ++ } ++ }, ++ "node_modules/firebase-admin/node_modules/@firebase/database-compat": { ++ "version": "2.0.1", ++ "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.1.tgz", ++ "integrity": "sha512-IsFivOjdE1GrjTeKoBU/ZMenESKDXidFDzZzHBPQ/4P20ptGdrl3oLlWrV/QJqJ9lND4IidE3z4Xr5JyfUW1vg==", ++ "dependencies": { ++ "@firebase/component": "0.6.11", ++ "@firebase/database": "1.0.10", ++ "@firebase/database-types": "1.0.7", ++ "@firebase/logger": "0.4.4", ++ "@firebase/util": "1.10.2", ++ "tslib": "^2.1.0" ++ }, ++ "engines": { ++ "node": ">=18.0.0" ++ } ++ }, ++ "node_modules/firebase-admin/node_modules/@firebase/database-types": { ++ "version": "1.0.7", ++ "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.7.tgz", ++ "integrity": "sha512-I7zcLfJXrM0WM+ksFmFdAMdlq/DFmpeMNa+/GNsLyFo5u/lX5zzkPzGe3srVWqaBQBY5KprylDGxOsP6ETfL0A==", ++ "dependencies": { ++ "@firebase/app-types": "0.9.3", ++ "@firebase/util": "1.10.2" ++ } ++ }, ++ "node_modules/firebase-admin/node_modules/@firebase/logger": { ++ "version": "0.4.4", ++ "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz", ++ "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==", ++ "dependencies": { ++ "tslib": "^2.1.0" ++ }, ++ "engines": { ++ "node": ">=18.0.0" ++ } ++ }, ++ "node_modules/firebase-admin/node_modules/@firebase/util": { ++ "version": "1.10.2", ++ "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.2.tgz", ++ "integrity": "sha512-qnSHIoE9FK+HYnNhTI8q14evyqbc/vHRivfB4TgCIUOl4tosmKSQlp7ltymOlMP4xVIJTg5wrkfcZ60X4nUf7Q==", ++ "dependencies": { ++ "tslib": "^2.1.0" ++ }, ++ "engines": { ++ "node": ">=18.0.0" ++ } ++ }, + "node_modules/firebase-admin/node_modules/uuid": { +- "version": "10.0.0", +- "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", +- "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", ++ "version": "11.0.5", ++ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", ++ "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], +- "license": "MIT", + "bin": { +- "uuid": "dist/bin/uuid" ++ "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/flat": { +@@ -25695,7 +26085,6 @@ + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", +- "dev": true, + "engines": { + "node": ">=0.4.x" + } +@@ -25837,15 +26226,17 @@ + } + }, + "node_modules/function.prototype.name": { +- "version": "1.1.6", +- "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", +- "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", ++ "version": "1.1.8", ++ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", ++ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.2", +- "define-properties": "^1.2.0", +- "es-abstract": "^1.22.1", +- "functions-have-names": "^1.2.3" ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.3", ++ "define-properties": "^1.2.1", ++ "functions-have-names": "^1.2.3", ++ "hasown": "^2.0.2", ++ "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" +@@ -25858,7 +26249,6 @@ + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", +- "license": "MIT", + "optional": true + }, + "node_modules/functions-have-names": { +@@ -25885,7 +26275,6 @@ + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", +- "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", +@@ -25902,7 +26291,6 @@ + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "license": "MIT", +- "optional": true, + "dependencies": { + "debug": "^4.3.4" + }, +@@ -25915,7 +26303,6 @@ + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "license": "MIT", +- "optional": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" +@@ -25929,7 +26316,6 @@ + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "license": "Apache-2.0", +- "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" +@@ -25975,15 +26361,20 @@ + } + }, + "node_modules/get-intrinsic": { +- "version": "1.2.4", +- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", +- "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", ++ "version": "1.2.6", ++ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", ++ "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "dependencies": { ++ "call-bind-apply-helpers": "^1.0.1", ++ "dunder-proto": "^1.0.0", ++ "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", ++ "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", +- "has-proto": "^1.0.1", +- "has-symbols": "^1.0.3", +- "hasown": "^2.0.0" ++ "gopd": "^1.2.0", ++ "has-symbols": "^1.1.0", ++ "hasown": "^2.0.2", ++ "math-intrinsics": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" +@@ -26049,14 +26440,14 @@ + } + }, + "node_modules/get-symbol-description": { +- "version": "1.0.2", +- "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", +- "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", ++ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.5", ++ "call-bound": "^1.0.3", + "es-errors": "^1.3.0", +- "get-intrinsic": "^1.2.4" ++ "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" +@@ -26344,7 +26735,6 @@ + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.2.tgz", + "integrity": "sha512-R+FRIfk1GBo3RdlRYWPdwk8nmtVUOn6+BkDomAC46KoU8kzXzE1HLmOasSCbWUByMMAGkknVF0G5kQ69Vj7dlA==", + "license": "Apache-2.0", +- "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", +@@ -26361,7 +26751,6 @@ + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz", + "integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==", +- "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", +@@ -26382,10 +26771,9 @@ + } + }, + "node_modules/google-gax/node_modules/@grpc/grpc-js": { +- "version": "1.12.2", +- "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", +- "integrity": "sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==", +- "license": "Apache-2.0", ++ "version": "1.12.5", ++ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz", ++ "integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.13", +@@ -26396,11 +26784,11 @@ + } + }, + "node_modules/gopd": { +- "version": "1.0.1", +- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", +- "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", +- "dependencies": { +- "get-intrinsic": "^1.1.3" ++ "version": "1.2.0", ++ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", ++ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", ++ "engines": { ++ "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" +@@ -26447,7 +26835,6 @@ + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", +- "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" +@@ -26523,14 +26910,14 @@ + } + }, + "node_modules/hardhat": { +- "version": "2.22.16", +- "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.22.16.tgz", +- "integrity": "sha512-d52yQZ09u0roL6GlgJSvtknsBtIuj9JrJ/U8VMzr/wue+gO5v2tQayvOX6llerlR57Zw2EOTQjLAt6RpHvjwHA==", ++ "version": "2.22.17", ++ "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.22.17.tgz", ++ "integrity": "sha512-tDlI475ccz4d/dajnADUTRc1OJ3H8fpP9sWhXhBPpYsQOg8JHq5xrDimo53UhWPl7KJmAeDCm1bFG74xvpGRpg==", + "dev": true, + "dependencies": { + "@ethersproject/abi": "^5.1.2", + "@metamask/eth-sig-util": "^4.0.0", +- "@nomicfoundation/edr": "^0.6.4", ++ "@nomicfoundation/edr": "^0.6.5", + "@nomicfoundation/ethereumjs-common": "4.0.4", + "@nomicfoundation/ethereumjs-tx": "5.0.4", + "@nomicfoundation/ethereumjs-util": "9.0.4", +@@ -26833,10 +27220,13 @@ + } + }, + "node_modules/has-bigints": { +- "version": "1.0.2", +- "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", +- "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", ++ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, ++ "engines": { ++ "node": ">= 0.4" ++ }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } +@@ -26861,9 +27251,13 @@ + } + }, + "node_modules/has-proto": { +- "version": "1.0.3", +- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", +- "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", ++ "version": "1.2.0", ++ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", ++ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", ++ "dev": true, ++ "dependencies": { ++ "dunder-proto": "^1.0.0" ++ }, + "engines": { + "node": ">= 0.4" + }, +@@ -26872,9 +27266,9 @@ + } + }, + "node_modules/has-symbols": { +- "version": "1.0.3", +- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", +- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", ++ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, +@@ -26930,6 +27324,16 @@ + "node": ">= 0.4" + } + }, ++ "node_modules/hast-util-parse-selector": { ++ "version": "2.2.5", ++ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", ++ "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", ++ "license": "MIT", ++ "funding": { ++ "type": "opencollective", ++ "url": "https://opencollective.com/unified" ++ } ++ }, + "node_modules/hast-util-to-estree": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz", +@@ -27709,6 +28113,71 @@ + "url": "https://opencollective.com/unified" + } + }, ++ "node_modules/hastscript": { ++ "version": "6.0.0", ++ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", ++ "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", ++ "license": "MIT", ++ "dependencies": { ++ "@types/hast": "^2.0.0", ++ "comma-separated-tokens": "^1.0.0", ++ "hast-util-parse-selector": "^2.0.0", ++ "property-information": "^5.0.0", ++ "space-separated-tokens": "^1.0.0" ++ }, ++ "funding": { ++ "type": "opencollective", ++ "url": "https://opencollective.com/unified" ++ } ++ }, ++ "node_modules/hastscript/node_modules/@types/hast": { ++ "version": "2.3.10", ++ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", ++ "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", ++ "license": "MIT", ++ "dependencies": { ++ "@types/unist": "^2" ++ } ++ }, ++ "node_modules/hastscript/node_modules/@types/unist": { ++ "version": "2.0.11", ++ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", ++ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", ++ "license": "MIT" ++ }, ++ "node_modules/hastscript/node_modules/comma-separated-tokens": { ++ "version": "1.0.8", ++ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", ++ "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", ++ "license": "MIT", ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/hastscript/node_modules/property-information": { ++ "version": "5.6.0", ++ "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", ++ "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", ++ "license": "MIT", ++ "dependencies": { ++ "xtend": "^4.0.0" ++ }, ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/hastscript/node_modules/space-separated-tokens": { ++ "version": "1.1.5", ++ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", ++ "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", ++ "license": "MIT", ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", +@@ -27777,6 +28246,21 @@ + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", + "license": "MIT" + }, ++ "node_modules/highlight.js": { ++ "version": "10.7.3", ++ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", ++ "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", ++ "license": "BSD-3-Clause", ++ "engines": { ++ "node": "*" ++ } ++ }, ++ "node_modules/highlightjs-vue": { ++ "version": "1.0.0", ++ "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", ++ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", ++ "license": "CC0-1.0" ++ }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", +@@ -27801,6 +28285,20 @@ + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, ++ "node_modules/html-encoding-sniffer": { ++ "version": "4.0.0", ++ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", ++ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "whatwg-encoding": "^3.1.1" ++ }, ++ "engines": { ++ "node": ">=18" ++ } ++ }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", +@@ -28332,14 +28830,14 @@ + } + }, + "node_modules/internal-slot": { +- "version": "1.0.7", +- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", +- "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", ++ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", +- "hasown": "^2.0.0", +- "side-channel": "^1.0.4" ++ "hasown": "^2.0.2", ++ "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" +@@ -28476,13 +28974,14 @@ + } + }, + "node_modules/is-array-buffer": { +- "version": "3.0.4", +- "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", +- "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", ++ "version": "3.0.5", ++ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", ++ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.2", +- "get-intrinsic": "^1.2.1" ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.3", ++ "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" +@@ -28501,7 +29000,6 @@ + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, +- "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, +@@ -28513,12 +29011,15 @@ + } + }, + "node_modules/is-bigint": { +- "version": "1.0.4", +- "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", +- "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", ++ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { +- "has-bigints": "^1.0.1" ++ "has-bigints": "^1.0.2" ++ }, ++ "engines": { ++ "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" +@@ -28536,13 +29037,13 @@ + } + }, + "node_modules/is-boolean-object": { +- "version": "1.1.2", +- "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", +- "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", ++ "version": "1.2.1", ++ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", ++ "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.2", +- "has-tostringtag": "^1.0.0" ++ "call-bound": "^1.0.2", ++ "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" +@@ -28625,11 +29126,13 @@ + } + }, + "node_modules/is-data-view": { +- "version": "1.0.1", +- "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", +- "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", ++ "version": "1.0.2", ++ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", ++ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "dependencies": { ++ "call-bound": "^1.0.2", ++ "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { +@@ -28640,12 +29143,13 @@ + } + }, + "node_modules/is-date-object": { +- "version": "1.0.5", +- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", +- "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", ++ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { +- "has-tostringtag": "^1.0.0" ++ "call-bound": "^1.0.2", ++ "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" +@@ -28703,13 +29207,15 @@ + } + }, + "node_modules/is-finalizationregistry": { +- "version": "1.0.2", +- "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", +- "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", ++ "version": "1.1.1", ++ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", ++ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, +- "license": "MIT", + "dependencies": { +- "call-bind": "^1.0.2" ++ "call-bound": "^1.0.3" ++ }, ++ "engines": { ++ "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" +@@ -28821,7 +29327,6 @@ + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, +- "license": "MIT", + "engines": { + "node": ">= 0.4" + }, +@@ -28846,25 +29351,14 @@ + "url": "https://github.com/sponsors/ljharb" + } + }, +- "node_modules/is-negative-zero": { +- "version": "2.0.3", +- "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", +- "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", +- "dev": true, +- "engines": { +- "node": ">= 0.4" +- }, +- "funding": { +- "url": "https://github.com/sponsors/ljharb" +- } +- }, + "node_modules/is-number-object": { +- "version": "1.0.7", +- "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", +- "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", ++ "version": "1.1.1", ++ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", ++ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { +- "has-tostringtag": "^1.0.0" ++ "call-bound": "^1.0.3", ++ "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" +@@ -28950,6 +29444,20 @@ + "url": "https://github.com/sponsors/sindresorhus" + } + }, ++ "node_modules/is-potential-custom-element-name": { ++ "version": "1.0.1", ++ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", ++ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true ++ }, ++ "node_modules/is-promise": { ++ "version": "4.0.0", ++ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", ++ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", ++ "license": "MIT" ++ }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", +@@ -28959,13 +29467,15 @@ + } + }, + "node_modules/is-regex": { +- "version": "1.1.4", +- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", +- "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", ++ "version": "1.2.1", ++ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", ++ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.2", +- "has-tostringtag": "^1.0.0" ++ "call-bound": "^1.0.2", ++ "gopd": "^1.2.0", ++ "has-tostringtag": "^1.0.2", ++ "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" +@@ -28979,7 +29489,6 @@ + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, +- "license": "MIT", + "engines": { + "node": ">= 0.4" + }, +@@ -28988,12 +29497,12 @@ + } + }, + "node_modules/is-shared-array-buffer": { +- "version": "1.0.3", +- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", +- "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", ++ "version": "1.0.4", ++ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", ++ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.7" ++ "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" +@@ -29034,12 +29543,13 @@ + } + }, + "node_modules/is-string": { +- "version": "1.0.7", +- "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", +- "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", ++ "version": "1.1.1", ++ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", ++ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { +- "has-tostringtag": "^1.0.0" ++ "call-bound": "^1.0.3", ++ "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" +@@ -29049,12 +29559,14 @@ + } + }, + "node_modules/is-symbol": { +- "version": "1.0.4", +- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", +- "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", ++ "version": "1.1.1", ++ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", ++ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { +- "has-symbols": "^1.0.2" ++ "call-bound": "^1.0.2", ++ "has-symbols": "^1.1.0", ++ "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" +@@ -29064,11 +29576,11 @@ + } + }, + "node_modules/is-typed-array": { +- "version": "1.1.13", +- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", +- "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", ++ "version": "1.1.15", ++ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", ++ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dependencies": { +- "which-typed-array": "^1.1.14" ++ "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" +@@ -29100,7 +29612,6 @@ + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, +- "license": "MIT", + "engines": { + "node": ">= 0.4" + }, +@@ -29109,26 +29620,28 @@ + } + }, + "node_modules/is-weakref": { +- "version": "1.0.2", +- "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", +- "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", ++ "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.2" ++ "call-bound": "^1.0.2" ++ }, ++ "engines": { ++ "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { +- "version": "2.0.3", +- "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", +- "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", ++ "version": "2.0.4", ++ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", ++ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, +- "license": "MIT", + "dependencies": { +- "call-bind": "^1.0.7", +- "get-intrinsic": "^1.2.4" ++ "call-bound": "^1.0.3", ++ "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" +@@ -29310,17 +29823,17 @@ + } + }, + "node_modules/iterator.prototype": { +- "version": "1.1.3", +- "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", +- "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", ++ "version": "1.1.4", ++ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", ++ "integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==", + "dev": true, +- "license": "MIT", + "dependencies": { +- "define-properties": "^1.2.1", +- "get-intrinsic": "^1.2.1", +- "has-symbols": "^1.0.3", +- "reflect.getprototypeof": "^1.0.4", +- "set-function-name": "^2.0.1" ++ "define-data-property": "^1.1.4", ++ "es-object-atoms": "^1.0.0", ++ "get-intrinsic": "^1.2.6", ++ "has-symbols": "^1.1.0", ++ "reflect.getprototypeof": "^1.0.8", ++ "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" +@@ -29721,6 +30234,166 @@ + "signal-exit": "^3.0.2" + } + }, ++ "node_modules/jsdom": { ++ "version": "25.0.1", ++ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", ++ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "cssstyle": "^4.1.0", ++ "data-urls": "^5.0.0", ++ "decimal.js": "^10.4.3", ++ "form-data": "^4.0.0", ++ "html-encoding-sniffer": "^4.0.0", ++ "http-proxy-agent": "^7.0.2", ++ "https-proxy-agent": "^7.0.5", ++ "is-potential-custom-element-name": "^1.0.1", ++ "nwsapi": "^2.2.12", ++ "parse5": "^7.1.2", ++ "rrweb-cssom": "^0.7.1", ++ "saxes": "^6.0.0", ++ "symbol-tree": "^3.2.4", ++ "tough-cookie": "^5.0.0", ++ "w3c-xmlserializer": "^5.0.0", ++ "webidl-conversions": "^7.0.0", ++ "whatwg-encoding": "^3.1.1", ++ "whatwg-mimetype": "^4.0.0", ++ "whatwg-url": "^14.0.0", ++ "ws": "^8.18.0", ++ "xml-name-validator": "^5.0.0" ++ }, ++ "engines": { ++ "node": ">=18" ++ }, ++ "peerDependencies": { ++ "canvas": "^2.11.2" ++ }, ++ "peerDependenciesMeta": { ++ "canvas": { ++ "optional": true ++ } ++ } ++ }, ++ "node_modules/jsdom/node_modules/agent-base": { ++ "version": "7.1.3", ++ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", ++ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "engines": { ++ "node": ">= 14" ++ } ++ }, ++ "node_modules/jsdom/node_modules/http-proxy-agent": { ++ "version": "7.0.2", ++ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", ++ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "agent-base": "^7.1.0", ++ "debug": "^4.3.4" ++ }, ++ "engines": { ++ "node": ">= 14" ++ } ++ }, ++ "node_modules/jsdom/node_modules/https-proxy-agent": { ++ "version": "7.0.6", ++ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", ++ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "agent-base": "^7.1.2", ++ "debug": "4" ++ }, ++ "engines": { ++ "node": ">= 14" ++ } ++ }, ++ "node_modules/jsdom/node_modules/tough-cookie": { ++ "version": "5.0.0", ++ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", ++ "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", ++ "license": "BSD-3-Clause", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "tldts": "^6.1.32" ++ }, ++ "engines": { ++ "node": ">=16" ++ } ++ }, ++ "node_modules/jsdom/node_modules/tr46": { ++ "version": "5.0.0", ++ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", ++ "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "punycode": "^2.3.1" ++ }, ++ "engines": { ++ "node": ">=18" ++ } ++ }, ++ "node_modules/jsdom/node_modules/webidl-conversions": { ++ "version": "7.0.0", ++ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", ++ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", ++ "license": "BSD-2-Clause", ++ "optional": true, ++ "peer": true, ++ "engines": { ++ "node": ">=12" ++ } ++ }, ++ "node_modules/jsdom/node_modules/whatwg-url": { ++ "version": "14.1.0", ++ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", ++ "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "tr46": "^5.0.0", ++ "webidl-conversions": "^7.0.0" ++ }, ++ "engines": { ++ "node": ">=18" ++ } ++ }, ++ "node_modules/jsdom/node_modules/ws": { ++ "version": "8.18.0", ++ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", ++ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "engines": { ++ "node": ">=10.0.0" ++ }, ++ "peerDependencies": { ++ "bufferutil": "^4.0.1", ++ "utf-8-validate": ">=5.0.2" ++ }, ++ "peerDependenciesMeta": { ++ "bufferutil": { ++ "optional": true ++ }, ++ "utf-8-validate": { ++ "optional": true ++ } ++ } ++ }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", +@@ -29737,7 +30410,6 @@ + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", +- "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } +@@ -29968,7 +30640,6 @@ + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", +- "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", +@@ -30021,7 +30692,6 @@ + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", +- "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" +@@ -30093,9 +30763,9 @@ + } + }, + "node_modules/knip": { +- "version": "5.39.0", +- "resolved": "https://registry.npmjs.org/knip/-/knip-5.39.0.tgz", +- "integrity": "sha512-GL6tGe5SCyLm4JtKFlHq5TOm7jbzzrXyk/xemSmWgdehN89ZKdFQkpJXL/lOTrQV6jnqgx9LlFMN1UnICx3FYQ==", ++ "version": "5.41.1", ++ "resolved": "https://registry.npmjs.org/knip/-/knip-5.41.1.tgz", ++ "integrity": "sha512-yNpCCe2REU7U3VRvMASnXSEtfEC2HmOoDW9Vp9teQ9FktJYnuagvSZD3xWq8Ru7sPABkmvbC5TVWuMzIaeADNA==", + "dev": true, + "funding": [ + { +@@ -30111,7 +30781,6 @@ + "url": "https://polar.sh/webpro-nl" + } + ], +- "license": "ISC", + "dependencies": { + "@nodelib/fs.walk": "1.2.8", + "@snyk/github-codeowners": "1.1.0", +@@ -30214,10 +30883,9 @@ + } + }, + "node_modules/libphonenumber-js": { +- "version": "1.11.14", +- "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.14.tgz", +- "integrity": "sha512-sexvAfwcW1Lqws4zFp8heAtAEXbEDnvkYCEGzvOoMgZR7JhXo/IkE9MkkGACgBed5fWqh3ShBGnJBdDnU9N8EQ==", +- "license": "MIT" ++ "version": "1.11.17", ++ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.17.tgz", ++ "integrity": "sha512-Jr6v8thd5qRlOlc6CslSTzGzzQW03uiscab7KHQZX1Dfo4R6n6FDhZ0Hri6/X7edLIDv9gl4VMZXhxTjLnl0VQ==" + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", +@@ -30614,6 +31282,33 @@ + "url": "https://github.com/sponsors/sindresorhus" + } + }, ++ "node_modules/lowlight": { ++ "version": "1.20.0", ++ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", ++ "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", ++ "license": "MIT", ++ "dependencies": { ++ "fault": "^1.0.0", ++ "highlight.js": "~10.7.0" ++ }, ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/lowlight/node_modules/fault": { ++ "version": "1.0.4", ++ "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", ++ "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", ++ "license": "MIT", ++ "dependencies": { ++ "format": "^0.2.0" ++ }, ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, + "node_modules/lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", +@@ -30739,6 +31434,14 @@ + "license": "Apache-2.0", + "peer": true + }, ++ "node_modules/math-intrinsics": { ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", ++ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", ++ "engines": { ++ "node": ">= 0.4" ++ } ++ }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", +@@ -35170,6 +35873,20 @@ + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==" + }, ++ "node_modules/nwsapi": { ++ "version": "2.2.16", ++ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", ++ "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true ++ }, ++ "node_modules/oauth": { ++ "version": "0.10.0", ++ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", ++ "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==", ++ "license": "MIT" ++ }, + "node_modules/ob1": { + "version": "0.81.0", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.81.0.tgz", +@@ -35248,9 +35965,12 @@ + } + }, + "node_modules/object-inspect": { +- "version": "1.13.1", +- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", +- "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", ++ "version": "1.13.3", ++ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", ++ "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", ++ "engines": { ++ "node": ">= 0.4" ++ }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } +@@ -35291,14 +36011,16 @@ + } + }, + "node_modules/object.assign": { +- "version": "4.1.5", +- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", +- "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", ++ "version": "4.1.7", ++ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", ++ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.5", ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.3", + "define-properties": "^1.2.1", +- "has-symbols": "^1.0.3", ++ "es-object-atoms": "^1.0.0", ++ "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { +@@ -35355,12 +36077,13 @@ + } + }, + "node_modules/object.values": { +- "version": "1.2.0", +- "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", +- "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", ++ "version": "1.2.1", ++ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", ++ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.7", ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, +@@ -35617,6 +36340,23 @@ + "dev": true, + "license": "MIT" + }, ++ "node_modules/own-keys": { ++ "version": "1.0.1", ++ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", ++ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", ++ "dev": true, ++ "dependencies": { ++ "get-intrinsic": "^1.2.6", ++ "object-keys": "^1.1.1", ++ "safe-push-apply": "^1.0.0" ++ }, ++ "engines": { ++ "node": ">= 0.4" ++ }, ++ "funding": { ++ "url": "https://github.com/sponsors/ljharb" ++ } ++ }, + "node_modules/ox": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.1.2.tgz", +@@ -35877,6 +36617,20 @@ + "url": "https://github.com/sponsors/sindresorhus" + } + }, ++ "node_modules/parse5": { ++ "version": "7.2.1", ++ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", ++ "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "entities": "^4.5.0" ++ }, ++ "funding": { ++ "url": "https://github.com/inikulin/parse5?sponsor=1" ++ } ++ }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", +@@ -35885,6 +36639,52 @@ + "node": ">= 0.8" + } + }, ++ "node_modules/passport": { ++ "version": "0.7.0", ++ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", ++ "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", ++ "license": "MIT", ++ "dependencies": { ++ "passport-strategy": "1.x.x", ++ "pause": "0.0.1", ++ "utils-merge": "^1.0.1" ++ }, ++ "engines": { ++ "node": ">= 0.4.0" ++ }, ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/jaredhanson" ++ } ++ }, ++ "node_modules/passport-oauth2": { ++ "version": "1.8.0", ++ "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", ++ "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", ++ "license": "MIT", ++ "dependencies": { ++ "base64url": "3.x.x", ++ "oauth": "0.10.x", ++ "passport-strategy": "1.x.x", ++ "uid2": "0.0.x", ++ "utils-merge": "1.x.x" ++ }, ++ "engines": { ++ "node": ">= 0.4.0" ++ }, ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/jaredhanson" ++ } ++ }, ++ "node_modules/passport-strategy": { ++ "version": "1.0.0", ++ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", ++ "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", ++ "engines": { ++ "node": ">= 0.4.0" ++ } ++ }, + "node_modules/patch-package": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", +@@ -36148,6 +36949,11 @@ + "node": "*" + } + }, ++ "node_modules/pause": { ++ "version": "0.0.1", ++ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", ++ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" ++ }, + "node_modules/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", +@@ -36279,6 +37085,8 @@ + "resolved": "https://registry.npmjs.org/permissionless/-/permissionless-0.2.15.tgz", + "integrity": "sha512-S5+FP9e6xY7AEC+9w4mJ66826Cn7gHqHxKMFopevM2+ZPTpYpvYnB4APLljNsCfuIqMVNutTbgcuLZTasNvMEQ==", + "license": "MIT", ++ "optional": true, ++ "peer": true, + "peerDependencies": { + "viem": "^2.21.22" + } +@@ -36903,11 +37711,10 @@ + } + }, + "node_modules/prettier": { +- "version": "3.4.1", +- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", +- "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", ++ "version": "3.4.2", ++ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", ++ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, +- "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, +@@ -36981,12 +37788,12 @@ + } + }, + "node_modules/prisma": { +- "version": "6.1.0", +- "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.1.0.tgz", +- "integrity": "sha512-aFI3Yi+ApUxkwCJJwyQSwpyzUX7YX3ihzuHNHOyv4GJg3X5tQsmRaJEnZ+ZyfHpMtnyahhmXVfbTZ+lS8ZtfKw==", ++ "version": "6.2.1", ++ "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.2.1.tgz", ++ "integrity": "sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA==", + "hasInstallScript": true, + "dependencies": { +- "@prisma/engines": "6.1.0" ++ "@prisma/engines": "6.2.1" + }, + "bin": { + "prisma": "build/index.js" +@@ -36998,6 +37805,15 @@ + "fsevents": "2.3.3" + } + }, ++ "node_modules/prismjs": { ++ "version": "1.29.0", ++ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", ++ "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", ++ "license": "MIT", ++ "engines": { ++ "node": ">=6" ++ } ++ }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", +@@ -37145,7 +37961,6 @@ + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", +- "license": "Apache-2.0", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" +@@ -37601,6 +38416,15 @@ + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, ++ "node_modules/random-bytes": { ++ "version": "1.0.0", ++ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", ++ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", ++ "license": "MIT", ++ "engines": { ++ "node": ">= 0.8" ++ } ++ }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", +@@ -37677,14 +38501,14 @@ + } + }, + "node_modules/rc-cascader": { +- "version": "3.30.0", +- "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.30.0.tgz", +- "integrity": "sha512-rrzSbk1Bdqbu+pDwiLCLHu72+lwX9BZ28+JKzoi0DWZ4N29QYFeip8Gctl33QVd2Xg3Rf14D3yAOG76ElJw16w==", ++ "version": "3.33.0", ++ "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.33.0.tgz", ++ "integrity": "sha512-JvZrMbKBXIbEDmpIORxqvedY/bck6hGbs3hxdWT8eS9wSQ1P7//lGxbyKjOSyQiVBbgzNWriSe6HoMcZO/+0rQ==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", +- "rc-tree": "~5.10.1", ++ "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { +@@ -37693,9 +38517,9 @@ + } + }, + "node_modules/rc-checkbox": { +- "version": "3.3.0", +- "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.3.0.tgz", +- "integrity": "sha512-Ih3ZaAcoAiFKJjifzwsGiT/f/quIkxJoklW4yKGho14Olulwn8gN7hOBve0/WGDg5o/l/5mL0w7ff7/YGvefVw==", ++ "version": "3.5.0", ++ "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", ++ "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", +@@ -37803,9 +38627,9 @@ + } + }, + "node_modules/rc-input": { +- "version": "1.6.4", +- "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.6.4.tgz", +- "integrity": "sha512-lBZhfRD4NSAUW0zOKLUeI6GJuXkxeZYi0hr8VcJgJpyTNOvHw1ysrKWAHcEOAAHj7guxgmWYSi6xWrEdfrSAsA==", ++ "version": "1.7.2", ++ "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.7.2.tgz", ++ "integrity": "sha512-g3nYONnl4edWj2FfVoxsU3Ec4XTE+Hb39Kfh2MFxMZjp/0gGyPUgy/v7ZhS27ZxUFNkuIDYXm9PJsLyJbtg86A==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", +@@ -37817,14 +38641,14 @@ + } + }, + "node_modules/rc-input-number": { +- "version": "9.3.0", +- "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.3.0.tgz", +- "integrity": "sha512-JQ363ywqRyxwgVxpg2z2kja3CehTpYdqR7emJ/6yJjRdbvo+RvfE83fcpBCIJRq3zLp8SakmEXq60qzWyZ7Usw==", ++ "version": "9.4.0", ++ "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.4.0.tgz", ++ "integrity": "sha512-Tiy4DcXcFXAf9wDhN8aUAyMeCLHJUHA/VA/t7Hj8ZEx5ETvxG7MArDOSE6psbiSCo+vJPm4E3fGN710ITVn6GA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", +- "rc-input": "~1.6.0", ++ "rc-input": "~1.7.1", + "rc-util": "^5.40.1" + }, + "peerDependencies": { +@@ -37833,16 +38657,16 @@ + } + }, + "node_modules/rc-mentions": { +- "version": "2.17.0", +- "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.17.0.tgz", +- "integrity": "sha512-sfHy+qLvc+p8jx8GUsujZWXDOIlIimp6YQz7N5ONQ6bHsa2kyG+BLa5k2wuxgebBbH97is33wxiyq5UkiXRpHA==", ++ "version": "2.19.1", ++ "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.19.1.tgz", ++ "integrity": "sha512-KK3bAc/bPFI993J3necmaMXD2reZTzytZdlTvkeBbp50IGH1BDPDvxLdHDUrpQx2b2TGaVJsn+86BvYa03kGqA==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", +- "rc-input": "~1.6.0", ++ "rc-input": "~1.7.1", + "rc-menu": "~9.16.0", +- "rc-textarea": "~1.8.0", ++ "rc-textarea": "~1.9.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { +@@ -37868,9 +38692,9 @@ + } + }, + "node_modules/rc-motion": { +- "version": "2.9.4", +- "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.4.tgz", +- "integrity": "sha512-TAPUUufDqhPO669qJobI0d9U0XZ/VPNQyZTivUxxzU1EyuPe3PtHSx7Kb902KuzQgu7sS18z8GguaxZEALV/ww==", ++ "version": "2.9.5", ++ "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", ++ "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", +@@ -37929,9 +38753,9 @@ + } + }, + "node_modules/rc-picker": { +- "version": "4.8.3", +- "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.8.3.tgz", +- "integrity": "sha512-hJ45qoEs4mfxXPAJdp1n3sKwADul874Cd0/HwnsEOE60H+tgiJUGgbOD62As3EG/rFVNS5AWRfBCDJJfmRqOVQ==", ++ "version": "4.9.2", ++ "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.9.2.tgz", ++ "integrity": "sha512-SLW4PRudODOomipKI0dvykxW4P8LOqtMr17MOaLU6NQJhkh9SZeh44a/8BMxwv5T6e3kiIeYc9k5jFg2Mv35Pg==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", +@@ -37998,9 +38822,9 @@ + } + }, + "node_modules/rc-resize-observer": { +- "version": "1.4.1", +- "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.1.tgz", +- "integrity": "sha512-JbAeFDsaaZRPwaTlXnCqgeO9c6E7qoaE/hxsub08cdnnPn6767c/j9+r/TifUdfvwXtdcfHygKbZ7ecM/PXo/Q==", ++ "version": "1.4.3", ++ "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", ++ "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", +@@ -38013,9 +38837,9 @@ + } + }, + "node_modules/rc-segmented": { +- "version": "2.5.0", +- "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.5.0.tgz", +- "integrity": "sha512-B28Fe3J9iUFOhFJET3RoXAPFJ2u47QvLSYcZWC4tFYNGPEjug5LAxEasZlA/PpAxhdOPqGWsGbSj7ftneukJnw==", ++ "version": "2.7.0", ++ "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz", ++ "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", +@@ -38028,9 +38852,9 @@ + } + }, + "node_modules/rc-select": { +- "version": "14.16.4", +- "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.4.tgz", +- "integrity": "sha512-jP6qf7+vjnxGvPpfPWbGYfFlSl3h8L2XcD4O7g2GYXmEeBC0mw+nPD7i++OOE8v3YGqP8xtYjRKAWCMLfjgxlw==", ++ "version": "14.16.5", ++ "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.5.tgz", ++ "integrity": "sha512-cRls713egTcitJ7WUXhHEf22h3U1OMC8nbw9+HN4Fniew8Xo3avgEDvIeGRwhbiyPNbQR23AwP+tt6KWUcB4IA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", +@@ -38049,9 +38873,9 @@ + } + }, + "node_modules/rc-slider": { +- "version": "11.1.7", +- "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.7.tgz", +- "integrity": "sha512-ytYbZei81TX7otdC0QvoYD72XSlxvTihNth5OeZ6PMXyEDq/vHdWFulQmfDGyXK1NwKwSlKgpvINOa88uT5g2A==", ++ "version": "11.1.8", ++ "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.8.tgz", ++ "integrity": "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", +@@ -38097,15 +38921,15 @@ + } + }, + "node_modules/rc-table": { +- "version": "7.49.0", +- "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.49.0.tgz", +- "integrity": "sha512-/FoPLX94muAQOxVpi1jhnpKjOIqUbT81eELQPAzSXOke4ky4oCWYUXOcVpL31ZCO90xScwVSXRd7coqtgtB1Ng==", ++ "version": "7.50.2", ++ "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.2.tgz", ++ "integrity": "sha512-+nJbzxzstBriLb5sr9U7Vjs7+4dO8cWlouQbMwBVYghk2vr508bBdkHJeP/z9HVjAIKmAgMQKxmtbgDd3gc5wA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", +- "rc-util": "^5.41.0", ++ "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { +@@ -38117,9 +38941,9 @@ + } + }, + "node_modules/rc-tabs": { +- "version": "15.4.0", +- "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.4.0.tgz", +- "integrity": "sha512-llKuyiAVqmXm2z7OrmhX5cNb2ueZaL8ZyA2P4R+6/72NYYcbEgOXibwHiQCFY2RiN3swXl53SIABi2CumUS02g==", ++ "version": "15.5.0", ++ "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.0.tgz", ++ "integrity": "sha512-NrDcTaUJLh9UuDdMBkjKTn97U9iXG44s9D03V5NHkhEDWO5/nC6PwC3RhkCWFMKB9hh+ryqgZ+TIr1b9Jd/hnQ==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", +@@ -38138,13 +38962,13 @@ + } + }, + "node_modules/rc-textarea": { +- "version": "1.8.2", +- "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.8.2.tgz", +- "integrity": "sha512-UFAezAqltyR00a8Lf0IPAyTd29Jj9ee8wt8DqXyDMal7r/Cg/nDt3e1OOv3Th4W6mKaZijjgwuPXhAfVNTN8sw==", ++ "version": "1.9.0", ++ "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.9.0.tgz", ++ "integrity": "sha512-dQW/Bc/MriPBTugj2Kx9PMS5eXCCGn2cxoIaichjbNvOiARlaHdI99j4DTxLl/V8+PIfW06uFy7kjfUIDDKyxQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", +- "rc-input": "~1.6.0", ++ "rc-input": "~1.7.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, +@@ -38154,9 +38978,9 @@ + } + }, + "node_modules/rc-tooltip": { +- "version": "6.2.1", +- "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.2.1.tgz", +- "integrity": "sha512-rws0duD/3sHHsD905Nex7FvoUGy2UBQRhTkKxeEvr2FB+r21HsOxcDJI0TzyO8NHhnAA8ILr8pfbSBg5Jj5KBg==", ++ "version": "6.3.2", ++ "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.3.2.tgz", ++ "integrity": "sha512-oA4HZIiZJbUQ5ojigM0y4XtWxaH/aQlJSzknjICRWNpqyemy1sL3X3iEQV2eSPBWEq+bqU3+aSs81z+28j9luA==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", +@@ -38168,9 +38992,9 @@ + } + }, + "node_modules/rc-tree": { +- "version": "5.10.1", +- "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.10.1.tgz", +- "integrity": "sha512-FPXb3tT/u39mgjr6JNlHaUTYfHkVGW56XaGDahDpEFLGsnPxGcVLNTjcqoQb/GNbSCycl7tD7EvIymwOTP0+Yw==", ++ "version": "5.13.0", ++ "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.0.tgz", ++ "integrity": "sha512-2+lFvoVRnvHQ1trlpXMOWtF8BUgF+3TiipG72uOfhpL5CUdXCk931kvDdUkTL/IZVtNEDQKwEEmJbAYJSA5NnA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", +@@ -38187,14 +39011,14 @@ + } + }, + "node_modules/rc-tree-select": { +- "version": "5.24.5", +- "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.24.5.tgz", +- "integrity": "sha512-PnyR8LZJWaiEFw0SHRqo4MNQWyyZsyMs8eNmo68uXZWjxc7QqeWcjPPoONN0rc90c3HZqGF9z+Roz+GLzY5GXA==", ++ "version": "5.27.0", ++ "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", ++ "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", +- "rc-tree": "~5.10.1", ++ "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { +@@ -38217,9 +39041,9 @@ + } + }, + "node_modules/rc-util": { +- "version": "5.44.2", +- "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.2.tgz", +- "integrity": "sha512-uGSk3hpPBLa3/0QAcKhCjgl4SFnhQCJDLvvpoLdbR6KgDuXrujG+dQaUeUvBJr2ZWak1O/9n+cYbJiWmmk95EQ==", ++ "version": "5.44.3", ++ "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.3.tgz", ++ "integrity": "sha512-q6KCcOFk3rv/zD3MckhJteZxb0VjAIFuf622B7ElK4vfrZdAzs16XR5p3VTdy3+U5jfJU5ACz4QnhLSuAGe5dA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" +@@ -38235,9 +39059,9 @@ + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/rc-virtual-list": { +- "version": "3.15.0", +- "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.15.0.tgz", +- "integrity": "sha512-dF2YQztqrU3ijAeWOqscTshCEr7vpimzSqAVjO1AyAmaqcHulaXpnGR0ptK5PXfxTUy48VkJOiglMIxlkYGs0w==", ++ "version": "3.17.0", ++ "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.17.0.tgz", ++ "integrity": "sha512-h0jPHWt8/Ots9eiGVSGQTxwrSuQ3kxqL/ERKubv8zzIMICGQaDDWm/JoUa31MdQUC7PKDMiy5KDLkNfHcWo+iQ==", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", +@@ -38718,6 +39542,23 @@ + } + } + }, ++ "node_modules/react-syntax-highlighter": { ++ "version": "15.6.1", ++ "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", ++ "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", ++ "license": "MIT", ++ "dependencies": { ++ "@babel/runtime": "^7.3.1", ++ "highlight.js": "^10.4.1", ++ "highlightjs-vue": "^1.0.0", ++ "lowlight": "^1.17.0", ++ "prismjs": "^1.27.0", ++ "refractor": "^3.6.0" ++ }, ++ "peerDependencies": { ++ "react": ">= 0.14.0" ++ } ++ }, + "node_modules/react-use-intercom": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/react-use-intercom/-/react-use-intercom-5.4.1.tgz", +@@ -39021,19 +39862,19 @@ + } + }, + "node_modules/reflect.getprototypeof": { +- "version": "1.0.6", +- "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", +- "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", ++ "version": "1.0.9", ++ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", ++ "integrity": "sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==", + "dev": true, +- "license": "MIT", + "dependencies": { +- "call-bind": "^1.0.7", ++ "call-bind": "^1.0.8", + "define-properties": "^1.2.1", +- "es-abstract": "^1.23.1", ++ "dunder-proto": "^1.0.1", ++ "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", +- "get-intrinsic": "^1.2.4", +- "globalthis": "^1.0.3", +- "which-builtin-type": "^1.1.3" ++ "get-intrinsic": "^1.2.6", ++ "gopd": "^1.2.0", ++ "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" +@@ -39042,6 +39883,122 @@ + "url": "https://github.com/sponsors/ljharb" + } + }, ++ "node_modules/refractor": { ++ "version": "3.6.0", ++ "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", ++ "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", ++ "license": "MIT", ++ "dependencies": { ++ "hastscript": "^6.0.0", ++ "parse-entities": "^2.0.0", ++ "prismjs": "~1.27.0" ++ }, ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/refractor/node_modules/character-entities": { ++ "version": "1.2.4", ++ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", ++ "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", ++ "license": "MIT", ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/refractor/node_modules/character-entities-legacy": { ++ "version": "1.1.4", ++ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", ++ "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", ++ "license": "MIT", ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/refractor/node_modules/character-reference-invalid": { ++ "version": "1.1.4", ++ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", ++ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", ++ "license": "MIT", ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/refractor/node_modules/is-alphabetical": { ++ "version": "1.0.4", ++ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", ++ "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", ++ "license": "MIT", ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/refractor/node_modules/is-alphanumerical": { ++ "version": "1.0.4", ++ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", ++ "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", ++ "license": "MIT", ++ "dependencies": { ++ "is-alphabetical": "^1.0.0", ++ "is-decimal": "^1.0.0" ++ }, ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/refractor/node_modules/is-decimal": { ++ "version": "1.0.4", ++ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", ++ "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", ++ "license": "MIT", ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/refractor/node_modules/is-hexadecimal": { ++ "version": "1.0.4", ++ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", ++ "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", ++ "license": "MIT", ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/refractor/node_modules/parse-entities": { ++ "version": "2.0.0", ++ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", ++ "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", ++ "license": "MIT", ++ "dependencies": { ++ "character-entities": "^1.0.0", ++ "character-entities-legacy": "^1.0.0", ++ "character-reference-invalid": "^1.0.0", ++ "is-alphanumerical": "^1.0.0", ++ "is-decimal": "^1.0.0", ++ "is-hexadecimal": "^1.0.0" ++ }, ++ "funding": { ++ "type": "github", ++ "url": "https://github.com/sponsors/wooorm" ++ } ++ }, ++ "node_modules/refractor/node_modules/prismjs": { ++ "version": "1.27.0", ++ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", ++ "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", ++ "license": "MIT", ++ "engines": { ++ "node": ">=6" ++ } ++ }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", +@@ -39082,15 +40039,15 @@ + } + }, + "node_modules/regexp.prototype.flags": { +- "version": "1.5.2", +- "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", +- "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", ++ "version": "1.5.3", ++ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", ++ "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.6", ++ "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", +- "set-function-name": "^2.0.1" ++ "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" +@@ -39795,6 +40785,14 @@ + "uuid": "dist/bin/uuid" + } + }, ++ "node_modules/rrweb-cssom": { ++ "version": "0.7.1", ++ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", ++ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true ++ }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", +@@ -39865,14 +40863,15 @@ + } + }, + "node_modules/safe-array-concat": { +- "version": "1.1.2", +- "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", +- "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", ++ "version": "1.1.3", ++ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", ++ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.7", +- "get-intrinsic": "^1.2.4", +- "has-symbols": "^1.0.3", ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.2", ++ "get-intrinsic": "^1.2.6", ++ "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { +@@ -39901,15 +40900,31 @@ + } + ] + }, ++ "node_modules/safe-push-apply": { ++ "version": "1.0.0", ++ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", ++ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", ++ "dev": true, ++ "dependencies": { ++ "es-errors": "^1.3.0", ++ "isarray": "^2.0.5" ++ }, ++ "engines": { ++ "node": ">= 0.4" ++ }, ++ "funding": { ++ "url": "https://github.com/sponsors/ljharb" ++ } ++ }, + "node_modules/safe-regex-test": { +- "version": "1.0.3", +- "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", +- "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", ++ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.6", ++ "call-bound": "^1.0.2", + "es-errors": "^1.3.0", +- "is-regex": "^1.1.4" ++ "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" +@@ -39959,6 +40974,20 @@ + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, ++ "node_modules/saxes": { ++ "version": "6.0.0", ++ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", ++ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", ++ "license": "ISC", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "xmlchars": "^2.2.0" ++ }, ++ "engines": { ++ "node": ">=v12.22.7" ++ } ++ }, + "node_modules/sc-istanbul": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/sc-istanbul/-/sc-istanbul-0.4.6.tgz", +@@ -40863,14 +41892,65 @@ + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "node_modules/side-channel": { +- "version": "1.0.6", +- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", +- "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", ++ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { +- "call-bind": "^1.0.7", + "es-errors": "^1.3.0", +- "get-intrinsic": "^1.2.4", +- "object-inspect": "^1.13.1" ++ "object-inspect": "^1.13.3", ++ "side-channel-list": "^1.0.0", ++ "side-channel-map": "^1.0.1", ++ "side-channel-weakmap": "^1.0.2" ++ }, ++ "engines": { ++ "node": ">= 0.4" ++ }, ++ "funding": { ++ "url": "https://github.com/sponsors/ljharb" ++ } ++ }, ++ "node_modules/side-channel-list": { ++ "version": "1.0.0", ++ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", ++ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", ++ "dependencies": { ++ "es-errors": "^1.3.0", ++ "object-inspect": "^1.13.3" ++ }, ++ "engines": { ++ "node": ">= 0.4" ++ }, ++ "funding": { ++ "url": "https://github.com/sponsors/ljharb" ++ } ++ }, ++ "node_modules/side-channel-map": { ++ "version": "1.0.1", ++ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", ++ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", ++ "dependencies": { ++ "call-bound": "^1.0.2", ++ "es-errors": "^1.3.0", ++ "get-intrinsic": "^1.2.5", ++ "object-inspect": "^1.13.3" ++ }, ++ "engines": { ++ "node": ">= 0.4" ++ }, ++ "funding": { ++ "url": "https://github.com/sponsors/ljharb" ++ } ++ }, ++ "node_modules/side-channel-weakmap": { ++ "version": "1.0.2", ++ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", ++ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", ++ "dependencies": { ++ "call-bound": "^1.0.2", ++ "es-errors": "^1.3.0", ++ "get-intrinsic": "^1.2.5", ++ "object-inspect": "^1.13.3", ++ "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" +@@ -41437,6 +42517,12 @@ + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, ++ "node_modules/stable-hash": { ++ "version": "0.0.4", ++ "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", ++ "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", ++ "dev": true ++ }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", +@@ -41689,23 +42775,24 @@ + "license": "MIT" + }, + "node_modules/string.prototype.matchall": { +- "version": "4.0.11", +- "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", +- "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", ++ "version": "4.0.12", ++ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", ++ "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.7", ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.3", + "define-properties": "^1.2.1", +- "es-abstract": "^1.23.2", ++ "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", +- "get-intrinsic": "^1.2.4", +- "gopd": "^1.0.1", +- "has-symbols": "^1.0.3", +- "internal-slot": "^1.0.7", +- "regexp.prototype.flags": "^1.5.2", ++ "get-intrinsic": "^1.2.6", ++ "gopd": "^1.2.0", ++ "has-symbols": "^1.1.0", ++ "internal-slot": "^1.1.0", ++ "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", +- "side-channel": "^1.0.6" ++ "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" +@@ -41725,15 +42812,18 @@ + } + }, + "node_modules/string.prototype.trim": { +- "version": "1.2.9", +- "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", +- "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", ++ "version": "1.2.10", ++ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", ++ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.7", ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.2", ++ "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", +- "es-abstract": "^1.23.0", +- "es-object-atoms": "^1.0.0" ++ "es-abstract": "^1.23.5", ++ "es-object-atoms": "^1.0.0", ++ "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" +@@ -41743,15 +42833,19 @@ + } + }, + "node_modules/string.prototype.trimend": { +- "version": "1.0.8", +- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", +- "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", ++ "version": "1.0.9", ++ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", ++ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.7", ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, ++ "engines": { ++ "node": ">= 0.4" ++ }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } +@@ -42114,6 +43208,14 @@ + "whatwg-fetch": "^3.4.1" + } + }, ++ "node_modules/symbol-tree": { ++ "version": "3.2.4", ++ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", ++ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true ++ }, + "node_modules/sync-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", +@@ -43059,6 +44161,28 @@ + "node": ">=14.0.0" + } + }, ++ "node_modules/tldts": { ++ "version": "6.1.70", ++ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz", ++ "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "tldts-core": "^6.1.70" ++ }, ++ "bin": { ++ "tldts": "bin/cli.js" ++ } ++ }, ++ "node_modules/tldts-core": { ++ "version": "6.1.70", ++ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz", ++ "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true ++ }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", +@@ -43538,30 +44662,30 @@ + } + }, + "node_modules/typed-array-buffer": { +- "version": "1.0.2", +- "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", +- "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", ++ "version": "1.0.3", ++ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", ++ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.7", ++ "call-bound": "^1.0.3", + "es-errors": "^1.3.0", +- "is-typed-array": "^1.1.13" ++ "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { +- "version": "1.0.1", +- "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", +- "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", ++ "version": "1.0.3", ++ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", ++ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.7", ++ "call-bind": "^1.0.8", + "for-each": "^0.3.3", +- "gopd": "^1.0.1", +- "has-proto": "^1.0.3", +- "is-typed-array": "^1.1.13" ++ "gopd": "^1.2.0", ++ "has-proto": "^1.2.0", ++ "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" +@@ -43571,17 +44695,18 @@ + } + }, + "node_modules/typed-array-byte-offset": { +- "version": "1.0.2", +- "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", +- "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", ++ "version": "1.0.4", ++ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", ++ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", +- "call-bind": "^1.0.7", ++ "call-bind": "^1.0.8", + "for-each": "^0.3.3", +- "gopd": "^1.0.1", +- "has-proto": "^1.0.3", +- "is-typed-array": "^1.1.13" ++ "gopd": "^1.2.0", ++ "has-proto": "^1.2.0", ++ "is-typed-array": "^1.1.15", ++ "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" +@@ -43591,17 +44716,17 @@ + } + }, + "node_modules/typed-array-length": { +- "version": "1.0.6", +- "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", +- "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", ++ "version": "1.0.7", ++ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", ++ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", +- "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", +- "possible-typed-array-names": "^1.0.0" ++ "possible-typed-array-names": "^1.0.0", ++ "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" +@@ -43708,6 +44833,24 @@ + "node": ">=0.8.0" + } + }, ++ "node_modules/uid-safe": { ++ "version": "2.1.5", ++ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", ++ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", ++ "license": "MIT", ++ "dependencies": { ++ "random-bytes": "~1.0.0" ++ }, ++ "engines": { ++ "node": ">= 0.8" ++ } ++ }, ++ "node_modules/uid2": { ++ "version": "0.0.4", ++ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", ++ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", ++ "license": "MIT" ++ }, + "node_modules/uint8arrays": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.0.tgz", +@@ -43718,15 +44861,18 @@ + } + }, + "node_modules/unbox-primitive": { +- "version": "1.0.2", +- "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", +- "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", ++ "version": "1.1.0", ++ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", ++ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { +- "call-bind": "^1.0.2", ++ "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", +- "has-symbols": "^1.0.3", +- "which-boxed-primitive": "^1.0.2" ++ "has-symbols": "^1.1.0", ++ "which-boxed-primitive": "^1.1.1" ++ }, ++ "engines": { ++ "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" +@@ -44466,16 +45612,15 @@ + } + }, + "node_modules/vaul": { +- "version": "1.1.1", +- "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.1.tgz", +- "integrity": "sha512-+ejzF6ffQKPcfgS7uOrGn017g39F8SO4yLPXbBhpC7a0H+oPqPna8f1BUfXaz8eU4+pxbQcmjxW+jWBSbxjaFg==", +- "license": "MIT", ++ "version": "1.1.2", ++ "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", ++ "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { +- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0", +- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0" ++ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", ++ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/vfile": { +@@ -44726,9 +45871,9 @@ + } + }, + "node_modules/vite-tsconfig-paths": { +- "version": "5.1.3", +- "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.3.tgz", +- "integrity": "sha512-0bz+PDlLpGfP2CigeSKL9NFTF1KtXkeHGZSSaGQSuPZH77GhoiQaA8IjYgOaynSuwlDTolSUEU0ErVvju3NURg==", ++ "version": "5.1.4", ++ "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", ++ "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", +@@ -45367,6 +46512,20 @@ + "dev": true, + "license": "MIT" + }, ++ "node_modules/w3c-xmlserializer": { ++ "version": "5.0.0", ++ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", ++ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "xml-name-validator": "^5.0.0" ++ }, ++ "engines": { ++ "node": ">=18" ++ } ++ }, + "node_modules/wagmi": { + "version": "2.12.30", + "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.12.30.tgz", +@@ -45684,15 +46843,15 @@ + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/webpack": { +- "version": "5.96.1", +- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", +- "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", ++ "version": "5.97.1", ++ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", ++ "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", +- "@webassemblyjs/ast": "^1.12.1", +- "@webassemblyjs/wasm-edit": "^1.12.1", +- "@webassemblyjs/wasm-parser": "^1.12.1", ++ "@webassemblyjs/ast": "^1.14.1", ++ "@webassemblyjs/wasm-edit": "^1.14.1", ++ "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", +@@ -45958,12 +47117,37 @@ + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, ++ "node_modules/whatwg-encoding": { ++ "version": "3.1.1", ++ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", ++ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "dependencies": { ++ "iconv-lite": "0.6.3" ++ }, ++ "engines": { ++ "node": ">=18" ++ } ++ }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, ++ "node_modules/whatwg-mimetype": { ++ "version": "4.0.0", ++ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", ++ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true, ++ "engines": { ++ "node": ">=18" ++ } ++ }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", +@@ -45988,40 +47172,43 @@ + } + }, + "node_modules/which-boxed-primitive": { +- "version": "1.0.2", +- "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", +- "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", ++ "version": "1.1.1", ++ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", ++ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { +- "is-bigint": "^1.0.1", +- "is-boolean-object": "^1.1.0", +- "is-number-object": "^1.0.4", +- "is-string": "^1.0.5", +- "is-symbol": "^1.0.3" ++ "is-bigint": "^1.1.0", ++ "is-boolean-object": "^1.2.1", ++ "is-number-object": "^1.1.1", ++ "is-string": "^1.1.1", ++ "is-symbol": "^1.1.1" ++ }, ++ "engines": { ++ "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { +- "version": "1.1.4", +- "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", +- "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", ++ "version": "1.2.1", ++ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", ++ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, +- "license": "MIT", + "dependencies": { ++ "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", +- "is-date-object": "^1.0.5", +- "is-finalizationregistry": "^1.0.2", ++ "is-date-object": "^1.1.0", ++ "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", +- "is-regex": "^1.1.4", ++ "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", +- "which-boxed-primitive": "^1.0.2", ++ "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", +- "which-typed-array": "^1.1.15" ++ "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" +@@ -46035,7 +47222,6 @@ + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, +- "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", +@@ -46055,14 +47241,15 @@ + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/which-typed-array": { +- "version": "1.1.15", +- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", +- "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", ++ "version": "1.1.18", ++ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", ++ "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "dependencies": { + "available-typed-arrays": "^1.0.7", +- "call-bind": "^1.0.7", ++ "call-bind": "^1.0.8", ++ "call-bound": "^1.0.3", + "for-each": "^0.3.3", +- "gopd": "^1.0.1", ++ "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { +@@ -46302,6 +47489,25 @@ + } + } + }, ++ "node_modules/xml-name-validator": { ++ "version": "5.0.0", ++ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", ++ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", ++ "license": "Apache-2.0", ++ "optional": true, ++ "peer": true, ++ "engines": { ++ "node": ">=18" ++ } ++ }, ++ "node_modules/xmlchars": { ++ "version": "2.2.0", ++ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", ++ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", ++ "license": "MIT", ++ "optional": true, ++ "peer": true ++ }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", +@@ -46467,9 +47673,9 @@ + "license": "MIT" + }, + "node_modules/zod": { +- "version": "3.23.8", +- "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", +- "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", ++ "version": "3.24.1", ++ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", ++ "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" +@@ -46538,7 +47744,7 @@ + "version": "1.0.0", + "license": "UNLICENSED", + "dependencies": { +- "@amplitude/analytics-browser": "^2.11.10", ++ "@amplitude/analytics-browser": "^2.11.11", + "@amplitude/analytics-node": "^1.3.6", + "@amplitude/plugin-session-replay-browser": "^1.12.0" + }, +@@ -46546,11 +47752,6 @@ + "@amplitude/analytics-types": "^2.8.0" + } + }, +- "packages/attestation": { +- "name": "@ethos/attestation", +- "version": "1.0.0", +- "license": "UNLICENSED" +- }, + "packages/blockchain-manager": { + "name": "@ethos/blockchain-manager", + "version": "1.0.0", +@@ -46594,13 +47795,13 @@ + "@ant-design/icons": "^5.5.2", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", +- "antd": "^5.22.5", ++ "antd": "^5.23.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "peerDependencies": { + "@ant-design/icons": "^5.5.2", +- "antd": "^5.22.5", ++ "antd": "^5.23.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + } +@@ -46639,10 +47840,10 @@ + "@nomicfoundation/hardhat-verify": "^2.0.12", + "@openzeppelin/contracts": "^5.1.0", + "@openzeppelin/contracts-upgradeable": "^5.1.0", +- "@openzeppelin/hardhat-upgrades": "^3.6.0", ++ "@openzeppelin/hardhat-upgrades": "^3.8.0", + "@prb/math": "^4.1.0", + "dotenv": "^16.4.5", +- "hardhat": "^2.22.16", ++ "hardhat": "^2.22.17", + "hardhat-gas-reporter": "^1.0.10", + "solhint": "^5.0.3", + "solidity-coverage": "^0.8.14", +@@ -46659,7 +47860,8 @@ + "dependencies": { + "@ethos/blockchain-manager": "^1.0.0", + "@ethos/score": "^1.0.0", +- "viem": "^2.21.51" ++ "viem": "^2.21.51", ++ "zod": "^3.24.1" + } + }, + "packages/echo-client": { +@@ -46715,7 +47917,6 @@ + "name": "@ethos/echo", + "version": "0.0.1", + "dependencies": { +- "@ethos/attestation": "^1.0.0", + "@ethos/blockchain-manager": "^1.0.0", + "@ethos/config": "^1.0.0", + "@ethos/contracts": "^1.0.0", +@@ -46724,20 +47925,24 @@ + "@ethos/helpers": "^1.0.0", + "@ethos/logger": "^1.1.0", + "@ethos/score": "^1.0.0", +- "@prisma/client": "^6.1.0", +- "@privy-io/server-auth": "^1.16.5", ++ "@prisma/client": "^6.2.1", ++ "@privy-io/server-auth": "^1.17.1", + "@sentry/node": "^8.38.0", + "@sentry/profiling-node": "^8.30.0", ++ "@superfaceai/passport-twitter-oauth2": "^1.2.4", + "@the-convocation/twitter-scraper": "^0.14.1", + "amqp-connection-manager": "^4.1.14", + "amqplib": "^0.10.5", + "compression": "^1.7.5", ++ "connect-redis": "^8.0.1", ++ "cookie": "^1.0.2", + "cors": "^2.8.5", + "cron": "^3.2.1", + "date-fns": "^4.1.0", + "ethers": "^6.13.4", +- "express": "^4.21.1", +- "firebase-admin": "^12.7.0", ++ "express": "^5.0.1", ++ "express-session": "^1.18.1", ++ "firebase-admin": "^13.0.2", + "helmet": "^8.0.0", + "ioredis": "^5.4.1", + "jwt-decode": "^4.0.0", +@@ -46745,6 +47950,7 @@ + "lru-cache": "^11.0.1", + "on-finished": "^2.4.1", + "parse-duration": "^1.1.0", ++ "passport": "^0.7.0", + "pino-sentry-transport": "^1.3.0", + "prom-client": "^15.1.3", + "redlock2": "^5.0.0-beta.3", +@@ -46754,17 +47960,108 @@ + "zod": "^3.23.8" + }, + "devDependencies": { +- "@types/amqplib": "^0.10.5", ++ "@types/amqplib": "^0.10.6", + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", ++ "@types/express-session": "^1.18.1", + "@types/on-finished": "^2.3.4", ++ "@types/passport": "^1.0.17", ++ "@types/passport-oauth2": "^1.4.17", + "pino": "^9.5.0", +- "prisma": "^6.1.0", ++ "prisma": "^6.2.1", + "tsx": "^4.19.2", + "type-fest": "^4.26.1" + } + }, ++ "services/echo/node_modules/accepts": { ++ "version": "2.0.0", ++ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", ++ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", ++ "license": "MIT", ++ "dependencies": { ++ "mime-types": "^3.0.0", ++ "negotiator": "^1.0.0" ++ }, ++ "engines": { ++ "node": ">= 0.6" ++ } ++ }, ++ "services/echo/node_modules/body-parser": { ++ "version": "2.0.2", ++ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.2.tgz", ++ "integrity": "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ==", ++ "license": "MIT", ++ "dependencies": { ++ "bytes": "3.1.2", ++ "content-type": "~1.0.5", ++ "debug": "3.1.0", ++ "destroy": "1.2.0", ++ "http-errors": "2.0.0", ++ "iconv-lite": "0.5.2", ++ "on-finished": "2.4.1", ++ "qs": "6.13.0", ++ "raw-body": "^3.0.0", ++ "type-is": "~1.6.18" ++ }, ++ "engines": { ++ "node": ">=18" ++ } ++ }, ++ "services/echo/node_modules/body-parser/node_modules/debug": { ++ "version": "3.1.0", ++ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", ++ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", ++ "license": "MIT", ++ "dependencies": { ++ "ms": "2.0.0" ++ } ++ }, ++ "services/echo/node_modules/body-parser/node_modules/ms": { ++ "version": "2.0.0", ++ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", ++ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", ++ "license": "MIT" ++ }, ++ "services/echo/node_modules/bytes": { ++ "version": "3.1.2", ++ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", ++ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", ++ "license": "MIT", ++ "engines": { ++ "node": ">= 0.8" ++ } ++ }, ++ "services/echo/node_modules/content-disposition": { ++ "version": "1.0.0", ++ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", ++ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", ++ "license": "MIT", ++ "dependencies": { ++ "safe-buffer": "5.2.1" ++ }, ++ "engines": { ++ "node": ">= 0.6" ++ } ++ }, ++ "services/echo/node_modules/cookie": { ++ "version": "1.0.2", ++ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", ++ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", ++ "license": "MIT", ++ "engines": { ++ "node": ">=18" ++ } ++ }, ++ "services/echo/node_modules/cookie-signature": { ++ "version": "1.2.2", ++ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", ++ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", ++ "license": "MIT", ++ "engines": { ++ "node": ">=6.6.0" ++ } ++ }, + "services/echo/node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", +@@ -46803,24 +48416,25 @@ + "@nivo/core": "^0.88.0", + "@nivo/line": "^0.88.0", + "@nivo/scales": "^0.88.0", +- "@privy-io/react-auth": "^1.93.0", +- "@privy-io/server-auth": "^1.16.6", +- "@privy-io/wagmi": "^0.2.12", ++ "@privy-io/react-auth": "^1.98.4", ++ "@privy-io/server-auth": "^1.17.1", ++ "@privy-io/wagmi": "^0.2.13", + "@radix-ui/react-focus-scope": "^1.1.0", + "@remix-run/node": "^2.15.2", + "@remix-run/react": "^2.15.2", + "@remix-run/serve": "^2.15.2", + "@resvg/resvg-js": "^2.6.2", +- "@tanstack/react-query": "^5.62.3", +- "antd": "^5.22.5", ++ "@tanstack/react-query": "^5.64.1", ++ "antd": "^5.23.1", + "clsx": "^2.1.1", + "cookie": "^1.0.2", + "d3": "^7.9.0", + "ethers": "^6.13.4", + "ethers-decode-error": "^2.1.3", + "isbot": "^5.1.17", ++ "lottie-react": "^2.4.0", + "motion": "^11.12.0", +- "prisma": "^6.1.0", ++ "prisma": "^6.2.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hotkeys-hook": "^4.6.1", +@@ -46830,7 +48444,7 @@ + "tailwindcss": "^3.4.15", + "tailwindcss-safe-area": "^0.6.0", + "ua-parser-js": "^2.0.0", +- "vaul": "^1.1.1", ++ "vaul": "^1.1.2", + "viem": "^2.21.51", + "wagmi": "^2.12.30", + "zod": "^3.23.8" +@@ -46847,7 +48461,7 @@ + "vite": "^5.4.11", + "vite-plugin-cjs-interop": "^2.1.6", + "vite-plugin-node-polyfills": "^0.22.0", +- "vite-tsconfig-paths": "^5.1.3" ++ "vite-tsconfig-paths": "^5.1.4" + } + }, + "services/emporos/node_modules/cookie": { +@@ -46963,7 +48577,6 @@ + "@ant-design/nextjs-registry": "^1.0.2", + "@emotion/react": "^11.13.5", + "@ethos/analytics": "^1.0.0", +- "@ethos/attestation": "^1.0.0", + "@ethos/blockchain-manager": "^1.0.0", + "@ethos/common-ui": "^1.0.0", + "@ethos/config": "^1.0.0", +@@ -46976,16 +48589,17 @@ + "@nivo/core": "^0.88.0", + "@nivo/line": "^0.88.0", + "@nivo/pie": "^0.88.0", +- "@privy-io/react-auth": "^1.92.6", +- "@privy-io/wagmi": "^0.2.12", ++ "@privy-io/react-auth": "^1.98.4", ++ "@privy-io/wagmi": "^0.2.13", + "@react-aria/overlays": "^3.23.4", + "@sentry/nextjs": "^8.38.0", + "@statsig/react-bindings": "^3.7.0", +- "@tanstack/react-query": "^5.62.3", +- "@tanstack/react-query-devtools": "^5.62.3", +- "@tanstack/react-query-persist-client": "^5.62.3", +- "antd": "^5.22.5", ++ "@tanstack/react-query": "^5.64.1", ++ "@tanstack/react-query-devtools": "^5.64.1", ++ "@tanstack/react-query-persist-client": "^5.64.1", ++ "antd": "^5.23.1", + "async-retry": "^1.3.3", ++ "cookies-next": "^5.0.2", + "d3": "^7.9.0", + "ethereum-blockies-base64": "^1.0.2", + "ethers": "^6.13.4", +@@ -47007,6 +48621,7 @@ + "react-infinite-scroll-component": "^6.1.0", + "react-intersection-observer": "^9.13.1", + "react-markdown": "^9.0.1", ++ "react-syntax-highlighter": "^15.6.1", + "react-use-intercom": "^5.4.1", + "serialize-error": "^11.0.3", + "ua-parser-js": "^2.0.0", +@@ -47021,6 +48636,7 @@ + "@types/lodash-es": "^4.17.12", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", ++ "@types/react-syntax-highlighter": "^15.5.13", + "@types/webpack-env": "^1.18.5", + "type-fest": "^4.26.1" + } +@@ -47120,7 +48736,7 @@ + "copy-webpack-plugin": "^12.0.2", + "cross-env": "^7.0.3", + "typescript": "5.7.2", +- "webpack": "5.96.1", ++ "webpack": "5.97.1", + "webpack-cli": "5.1.4", + "webpack-glob-entries": "^1.0.1" + } diff --git a/ethos/package.json b/ethos/package.json index c4191b3..8ef12f6 100644 --- a/ethos/package.json +++ b/ethos/package.json @@ -46,37 +46,37 @@ "author": "Ethos Network Inc.", "license": "UNLICENSED", "devDependencies": { - "@dotenvx/dotenvx": "^1.26.1", - "@next/eslint-plugin-next": "^15.1.2", - "@stylistic/eslint-plugin-js": "^2.11.0", - "@tanstack/eslint-plugin-query": "^5.62.1", + "@dotenvx/dotenvx": "^1.32.0", + "@next/eslint-plugin-next": "^15.1.3", + "@stylistic/eslint-plugin-js": "^2.12.1", + "@tanstack/eslint-plugin-query": "^5.62.16", "@types/inquirer": "^9.0.7", - "@types/node": "^22.10.1", + "@types/node": "^22.10.2", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "@vitest/coverage-v8": "^2.1.8", - "concurrently": "^9.1.0", + "concurrently": "^9.1.1", "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-love": "^44.0.0", "eslint-config-prettier": "^9.1.0", - "eslint-import-resolver-typescript": "^3.6.3", + "eslint-import-resolver-typescript": "^3.7.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-n": "^16.6.2", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^6.6.0", - "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react": "^7.37.3", + "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-unicorn": "^56.0.1", "eslint-plugin-vitest": "^0.4.1", "glob": "^11.0.0", "husky": "^9.1.7", "inquirer": "^8.2.6", - "knip": "^5.39.0", + "knip": "^5.41.1", "open-cli": "^8.0.0", "patch-package": "^8.0.0", "picocolors": "^1.1.1", - "prettier": "3.4.1", + "prettier": "3.4.2", "serve": "^14.2.4", "ts-node": "^10.9.2", "tsx": "^4.19.1", diff --git a/ethos/package.json.rej b/ethos/package.json.rej new file mode 100644 index 0000000..4e2f630 --- /dev/null +++ b/ethos/package.json.rej @@ -0,0 +1,50 @@ +diff a/ethos/package.json b/ethos/package.json (rejected hunks) +@@ -46,37 +46,37 @@ + "author": "Ethos Network Inc.", + "license": "UNLICENSED", + "devDependencies": { +- "@dotenvx/dotenvx": "^1.26.1", +- "@next/eslint-plugin-next": "^15.1.2", +- "@stylistic/eslint-plugin-js": "^2.11.0", +- "@tanstack/eslint-plugin-query": "^5.62.1", ++ "@dotenvx/dotenvx": "^1.32.0", ++ "@next/eslint-plugin-next": "^15.1.3", ++ "@stylistic/eslint-plugin-js": "^2.12.1", ++ "@tanstack/eslint-plugin-query": "^5.62.16", + "@types/inquirer": "^9.0.7", +- "@types/node": "^22.10.1", ++ "@types/node": "^22.10.2", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@vitest/coverage-v8": "^2.1.8", +- "concurrently": "^9.1.0", ++ "concurrently": "^9.1.1", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-config-love": "^44.0.0", + "eslint-config-prettier": "^9.1.0", +- "eslint-import-resolver-typescript": "^3.6.3", ++ "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-promise": "^6.6.0", +- "eslint-plugin-react": "^7.37.2", +- "eslint-plugin-react-hooks": "^5.0.0", ++ "eslint-plugin-react": "^7.37.3", ++ "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-vitest": "^0.4.1", + "glob": "^11.0.0", + "husky": "^9.1.7", + "inquirer": "^8.2.6", +- "knip": "^5.39.0", ++ "knip": "^5.41.1", + "open-cli": "^8.0.0", + "patch-package": "^8.0.0", + "picocolors": "^1.1.1", +- "prettier": "3.4.1", ++ "prettier": "3.4.2", + "serve": "^14.2.4", + "ts-node": "^10.9.2", + "tsx": "^4.19.1", diff --git a/ethos/packages/analytics/package.json b/ethos/packages/analytics/package.json new file mode 100644 index 0000000..cdc3cd7 --- /dev/null +++ b/ethos/packages/analytics/package.json @@ -0,0 +1,21 @@ +{ + "name": "@ethos/analytics", + "version": "1.0.0", + "description": "Wrapper for Amplitude analytics", + "main": "dist/index.js", + "type": "module", + "author": "Ethos Network Inc.", + "license": "UNLICENSED", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + }, + "dependencies": { + "@amplitude/analytics-browser": "^2.11.11", + "@amplitude/analytics-node": "^1.3.6", + "@amplitude/plugin-session-replay-browser": "^1.12.0" + }, + "devDependencies": { + "@amplitude/analytics-types": "^2.8.0" + } +} \ No newline at end of file diff --git a/ethos/packages/analytics/src/analytics.type.ts b/ethos/packages/analytics/src/analytics.type.ts new file mode 100644 index 0000000..eaf4613 --- /dev/null +++ b/ethos/packages/analytics/src/analytics.type.ts @@ -0,0 +1 @@ +export type Attributes = Record; diff --git a/ethos/packages/analytics/src/browser-analytics.ts b/ethos/packages/analytics/src/browser-analytics.ts new file mode 100644 index 0000000..f0d98d2 --- /dev/null +++ b/ethos/packages/analytics/src/browser-analytics.ts @@ -0,0 +1,102 @@ +import * as amplitude from '@amplitude/analytics-browser'; +import { type BrowserOptions } from '@amplitude/analytics-types'; +import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'; +import { type Attributes } from './analytics.type.js'; + +type SessionReplayOptions = Parameters[0]; + +export class BrowserAnalyticsClient { + private globalEventProperties: Attributes = {}; + + constructor( + apiKey: string, + options?: { + amplitudeOptions?: BrowserOptions; + enableSessionReplay?: boolean; + sessionReplayOptions?: SessionReplayOptions; + }, + ) { + if (options?.enableSessionReplay) { + const sessionReplayTracking = sessionReplayPlugin(options.sessionReplayOptions); + + amplitude.add(sessionReplayTracking); + } + + amplitude.init(apiKey, { + useBatch: true, + ...options?.amplitudeOptions, + }); + } + + setGlobalEventProperties(properties: Attributes): void { + this.globalEventProperties = properties; + } + + setUserId(userId: string): void { + amplitude.setUserId(userId); + } + + setUserProperties(properties: Attributes): void { + const identity = new amplitude.Identify(); + + for (const [key, value] of Object.entries(properties)) { + identity.set(key, value); + } + + amplitude.identify(identity); + } + + /** + * Sends an event about screen being viewed. It can be used to track page views + * or views of specific components like modal, banner, etc. + */ + sendScreenEvent(screenName: string, attributes?: Attributes): void { + const eventProperties = { + ...this.globalEventProperties, + ...attributes, + }; + + amplitude.track(`viewed ${screenName}`, eventProperties); + } + + /** + * Sends an event about the result of user interaction with the app. For + * example, user signed in, file uploaded, form submitted, etc. + * @param actionSubject E.g., 'user', 'file', 'form' + * @param action E.g., 'signedIn', 'uploaded', 'submitted' + */ + sendTrackEvent( + actionSubject: string, + action: string, + attributes?: Record, + ): void { + const eventProperties = { + ...this.globalEventProperties, + ...attributes, + }; + + amplitude.track(`${actionSubject} ${action}`, eventProperties); + } + + /** + * Sends an event about user interaction with the UI. For example, button click, + * link click, dropdown opened, etc. + * @param actionSubject E.g., 'button', 'link', 'dropdown' + * @param action E.g., 'click', 'hover', 'open' + * @param actionSubjectId Identifier of the UI element that was interacted with + */ + sendUIEvent( + actionSubject: string, + action: string, + actionSubjectId: string, + attributes?: Record, + ): void { + const eventProperties = { + ...this.globalEventProperties, + ...attributes, + actionSubjectId, + }; + + amplitude.track(`${actionSubject} ${action}`, eventProperties); + } +} diff --git a/ethos/packages/analytics/src/index.ts b/ethos/packages/analytics/src/index.ts new file mode 100644 index 0000000..0dffc59 --- /dev/null +++ b/ethos/packages/analytics/src/index.ts @@ -0,0 +1,4 @@ +export { BrowserAnalyticsClient } from './browser-analytics.js'; +export { ServerAnalyticsClient } from './server-analytics.js'; + +export { type NodeOptions, LogLevel } from '@amplitude/analytics-types'; diff --git a/ethos/packages/analytics/src/server-analytics.ts b/ethos/packages/analytics/src/server-analytics.ts new file mode 100644 index 0000000..867663f --- /dev/null +++ b/ethos/packages/analytics/src/server-analytics.ts @@ -0,0 +1,30 @@ +import { init, track } from '@amplitude/analytics-node'; +import { type NodeOptions } from '@amplitude/analytics-types'; +import { type Attributes } from './analytics.type.js'; + +export class ServerAnalyticsClient { + private globalEventProperties: Attributes = {}; + + constructor(apiKey: string, options?: NodeOptions) { + init(apiKey, { + useBatch: true, + serverUrl: 'https://api2.amplitude.com/batch', + ...options, + }); + } + + setGlobalEventProperties(properties: Attributes): void { + this.globalEventProperties = properties; + } + + sendTrackEvent(event: string, properties?: Record): void { + track( + event, + { + ...this.globalEventProperties, + ...properties, + }, + { user_id: 'server' }, + ); + } +} diff --git a/ethos/packages/analytics/tsconfig.json b/ethos/packages/analytics/tsconfig.json new file mode 100644 index 0000000..cc8c840 --- /dev/null +++ b/ethos/packages/analytics/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + } +} diff --git a/ethos/packages/blockchain-manager/package.json b/ethos/packages/blockchain-manager/package.json new file mode 100644 index 0000000..f0a0b1e --- /dev/null +++ b/ethos/packages/blockchain-manager/package.json @@ -0,0 +1,24 @@ +{ + "name": "@ethos/blockchain-manager", + "version": "1.0.0", + "description": "Encapsulates the blockchain interaction logic with Ethos contracts", + "main": "dist/index.js", + "type": "module", + "author": "Ethos Network Inc.", + "license": "UNLICENSED", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + }, + "dependencies": { + "@ethos/contracts": "^1.0.0", + "@ethos/env": "^1.0.0", + "@ethos/helpers": "^1.0.0", + "async-retry": "^1.3.3", + "ethers": "^6.13.4", + "viem": "^2.21.51" + }, + "devDependencies": { + "@types/async-retry": "^1.4.9" + } +} diff --git a/ethos/packages/blockchain-manager/src/BlockchainManager.ts b/ethos/packages/blockchain-manager/src/BlockchainManager.ts new file mode 100644 index 0000000..b7ee5ec --- /dev/null +++ b/ethos/packages/blockchain-manager/src/BlockchainManager.ts @@ -0,0 +1,395 @@ +import { + type ContractLookup, + contracts, + type Contract, + getContractsForEnvironment, +} from '@ethos/contracts'; +import { type EthosEnvironment } from '@ethos/env'; +import { + type BytesLike, + type ErrorDescription, + type Interface, + Wallet, + isCallException, + type Listener, + type TransactionReceipt, + type Provider, +} from 'ethers'; +import { isAddress, type Address } from 'viem'; +import { ContractAddressManager } from './contracts/ContractAddressManager.js'; +import { EthosAttestation } from './contracts/EthosAttestation.js'; +import { EthosDiscussion } from './contracts/EthosDiscussion.js'; +import { EthosProfile } from './contracts/EthosProfile.js'; +import { EthosReview } from './contracts/EthosReview.js'; +import { EthosVote } from './contracts/EthosVote.js'; +import { EthosVouch } from './contracts/EthosVouch.js'; +import { ReputationMarket } from './contracts/ReputationMarket.js'; +import { + type ContractRunnerConfig, + formatMessageToSign, + getContractRunner, + getLogs, + registerListener, +} from './contracts/utils.js'; +import { + type CancelListener, + type AttestationService, + type ProfileId, + BlockchainError, +} from './types.js'; + +export class BlockchainManager { + provider: Provider | null; + ethosAttestation: EthosAttestation; + ethosDiscussion: EthosDiscussion; + ethosProfile: EthosProfile; + ethosReview: EthosReview; + ethosVote: EthosVote; + ethosVouch: EthosVouch; + reputationMarket: ReputationMarket; + contractLookup: ContractLookup; + contractAddressManager: ContractAddressManager; + private readonly contractAddressLookup: Record; + + constructor(environment: EthosEnvironment, config: ContractRunnerConfig = {}) { + const runner = getContractRunner(config); + + this.provider = runner.provider; + + this.contractLookup = getContractsForEnvironment(environment); + + this.ethosAttestation = new EthosAttestation(runner, this.contractLookup); + this.ethosDiscussion = new EthosDiscussion(runner, this.contractLookup); + this.ethosProfile = new EthosProfile(runner, this.contractLookup); + this.ethosReview = new EthosReview(runner, this.contractLookup); + this.ethosVote = new EthosVote(runner, this.contractLookup); + this.ethosVouch = new EthosVouch(runner, this.contractLookup); + this.reputationMarket = new ReputationMarket(runner, this.contractLookup); + this.contractAddressManager = new ContractAddressManager(runner, this.contractLookup); + this.contractAddressLookup = {}; + + for (const name of contracts) { + this.contractAddressLookup[this.contractLookup[name].address] = name; + } + } + + /** + * Retrieves the current address of a specified contract. + * @param contract The contract name to look up. + * @returns The address of the specified contract. + */ + getContractAddress(contract: Contract): Address { + return this.contractLookup[contract].address; + } + + /** + * Retrieves the name of a contract given its address. + * @param contractAddress The address of the contract to look up. + * @returns The name of the contract associated with the given address. + */ + getContractName(contractAddress: Address): Contract { + return this.contractAddressLookup[contractAddress]; + } + + /** + * EVENT HANDLING METHODS + */ + + /** + * Sets up event listeners for all Ethos contracts and calls the provided callback when events occur. + * + * This includes a manually curated list of relevant events for each contract. + * + * @param callback - A function to be called when any Ethos contract event is emitted. + * The callback will receive all arguments passed by the event. + * The last argument is always the standard event log. + */ + async onEthosEvent(callback: Listener): Promise<{ + ethosAttestation: CancelListener; + ethosProfile: CancelListener; + ethosDiscussion: CancelListener; + ethosReview: CancelListener; + ethosVote: CancelListener; + ethosVouch: CancelListener; + ethosReputationMarket: CancelListener; + }> { + const ae = this.ethosAttestation.contract.filters; + const attestationEvents = [ + ae.AttestationCreated, + ae.AttestationArchived, + ae.AttestationClaimed, + ae.AttestationRestored, + ]; + const de = this.ethosDiscussion.contract.filters; + const discussionEvents = [de.ReplyAdded, de.ReplyEdited]; + const pe = this.ethosProfile.contract.filters; + const profileEvents = [ + pe.AddressClaim, + pe.InvitesAdded, + pe.ProfileArchived, + pe.ProfileCreated, + pe.ProfileRestored, + pe.Uninvited, + pe.UserInvited, + ]; + const re = this.ethosReview.contract.filters; + const reviewEvents = [re.ReviewCreated, re.ReviewArchived, re.ReviewRestored, re.ReviewEdited]; + const voe = this.ethosVote.contract.filters; + const voteEvents = [voe.VoteChanged, voe.Voted]; + const ve = this.ethosVouch.contract.filters; + const vouchEvents = [ve.MarkedUnhealthy, ve.Unvouched, ve.Vouched]; + const me = this.reputationMarket.contract.filters; + const marketEvents = [me.MarketCreated, me.MarketUpdated, me.VotesBought, me.VotesSold]; + + return { + ethosAttestation: await registerListener( + this.ethosAttestation.contract, + attestationEvents, + callback, + ), + ethosProfile: await registerListener(this.ethosProfile.contract, profileEvents, callback), + ethosDiscussion: await registerListener( + this.ethosDiscussion.contract, + discussionEvents, + callback, + ), + ethosReview: await registerListener(this.ethosReview.contract, reviewEvents, callback), + ethosVote: await registerListener(this.ethosVote.contract, voteEvents, callback), + ethosVouch: await registerListener(this.ethosVouch.contract, vouchEvents, callback), + ethosReputationMarket: await registerListener( + this.reputationMarket.contract, + marketEvents, + callback, + ), + }; + } + + /** + * Get all EthosProfile events. + */ + async getProfileEvents(fromBlock: number, toBlock?: number): ReturnType { + return await getLogs(this.ethosProfile, fromBlock, toBlock); + } + + /** + * Get all EthosReview events. + */ + async getReviewEvents(fromBlock: number, toBlock?: number): ReturnType { + return await getLogs(this.ethosReview, fromBlock, toBlock); + } + + /** + * Get all EthosVouch events. + */ + async getVouchEvents(fromBlock: number, toBlock?: number): ReturnType { + return await getLogs(this.ethosVouch, fromBlock, toBlock); + } + + /** + * Get all EthosDiscussion events. + */ + async getDiscussionEvents(fromBlock: number, toBlock?: number): ReturnType { + return await getLogs(this.ethosDiscussion, fromBlock, toBlock); + } + + /** + * Get all EthosVote events. + */ + async getVoteEvents(fromBlock: number, toBlock?: number): ReturnType { + return await getLogs(this.ethosVote, fromBlock, toBlock); + } + + /** + * Get all EthosAttestation events. + */ + async getAttestationEvents(fromBlock: number, toBlock?: number): ReturnType { + return await getLogs(this.ethosAttestation, fromBlock, toBlock); + } + + /** + * Get all ReputationMarket events. + */ + async getMarketEvents(fromBlock: number, toBlock?: number): ReturnType { + return await getLogs(this.reputationMarket, fromBlock, toBlock); + } + + /** + * Creates a signature for creating an attestation by signing all parameters. + * @param profileId Profile id. Use max uint for non-existing profile. + * @param account Account address to register. + * @param signerPrivateKey Signer private key. + * @returns Random value and signature. + */ + async createSignatureForRegisterAddress( + profileId: ProfileId, + account: Address, + signerPrivateKey: string, + ): Promise<{ randValue: number; signature: string }> { + // TODO: check if it's secure to use timestamp + const randValue = Date.now(); + + const messageHash = formatMessageToSign( + [ + ['address', account], + ['uint256', BigInt(profileId)], + ['uint256', BigInt(randValue)], + ], + 'solidityPacked', + ); + + const signer = new Wallet(signerPrivateKey); + + const signature = await signer.signMessage(messageHash); + + return { + randValue, + signature, + }; + } + + /** + * ATTESTATION METHODS + */ + + /** + * Creates a signature for creating an attestation by signing all parameters. + * @param profileId Profile id. Use max uint for non-existing profile. + * @param account Account name. + * @param service Service name. E.g., 'x.com'. + * @param evidence Evidence of attestation. + * @param signerPrivateKey Signer private key. + * @returns Random value and signature. + */ + async createSignatureForCreateAttestation( + profileId: ProfileId, + account: string, + service: AttestationService, + evidence: string, + signerPrivateKey: string, + ): Promise<{ randValue: number; signature: string }> { + // TODO: check if it's secure to use timestamp + const randValue = Date.now(); + + const messageHash = formatMessageToSign( + [ + ['uint256', BigInt(profileId)], + ['uint256', BigInt(randValue)], + ['string', account.toLowerCase()], + ['string', service.toLowerCase()], + ['string', evidence], + ], + 'abiEncoded', + ); + + const signer = new Wallet(signerPrivateKey); + + const signature = await signer.signMessage(messageHash); + + return { + randValue, + signature, + }; + } + + /** + * Retrieves a transaction receipt by its hash. + * @param txHash - The transaction hash to look up + * @returns An object containing the transaction receipt, which may be null if the transaction is not found + * @throws {Error} If the provider is not available + * @example + * ```typescript + * const { transaction } = await blockchainManager.getTransactionReceiptByHash("0x123..."); + * if (transaction) { + * console.log("Transaction was mined in block:", transaction.blockNumber); + * } + * ``` + */ + async getTransactionReceiptByHash(txHash: string): Promise<{ + transaction: TransactionReceipt | null; + }> { + if (!this.provider) throw new Error('Provider not available'); + + const tx = await this.provider.getTransactionReceipt(txHash); + + return { + transaction: tx, + }; + } + + /** + * HELPER METHODS + */ + + parseError(error: any): ErrorDescription | null { + if (!isCallException(error) || !error.data) { + return null; + } + + const contractAddress = error.transaction.to; + + if (!contractAddress || !isAddress(contractAddress)) { + return null; + } + + const parserLookup: Record = { + [this.contractLookup.attestation.address]: (data: BytesLike) => + this.ethosAttestation.contract.interface.parseError(data), + [this.contractLookup.discussion.address]: (data: BytesLike) => + this.ethosDiscussion.contract.interface.parseError(data), + [this.contractLookup.profile.address]: (data: BytesLike) => + this.ethosProfile.contract.interface.parseError(data), + [this.contractLookup.review.address]: (data: BytesLike) => + this.ethosReview.contract.interface.parseError(data), + [this.contractLookup.vote.address]: (data: BytesLike) => + this.ethosVote.contract.interface.parseError(data), + [this.contractLookup.vouch.address]: (data: BytesLike) => + this.ethosVouch.contract.interface.parseError(data), + [this.contractLookup.reputationMarket.address]: (data: BytesLike) => + this.reputationMarket.contract.interface.parseError(data), + }; + + if (!parserLookup[contractAddress]) { + return null; + } + + return parserLookup[contractAddress](error.data); + } + + /** + * Wraps a blockchain action in human readable error messages! No more hex strings! + * + * Use this anywhere you're seeing: execution reverted (unknown custom error) + * + * This method executes the provided action and catches any errors that occur. + * If the error is a blockchain-specific error that can be parsed, it wraps it + * in a BlockchainError. Otherwise, it rethrows the original error. + * + * @template T The return type of the action. + * @param action A function that performs a blockchain operation and returns a promise. + * @returns A promise that resolves with the result of the action. + * @throws {BlockchainError} If a blockchain-specific error occurs and can be parsed. + * @throws The original error if it cannot be parsed as a blockchain-specific error. + * + * @example + * ```typescript + * const result = await blockchainManager.wrapChainErr(async () => { + * return await someBlockchainOperation(); + * }); + * ``` + */ + async wrapChainErr(action: () => Promise): Promise { + try { + const result = await action(); + + return result; + } catch (error) { + const parsedError = this.parseError(error); + + if (parsedError) { + throw new BlockchainError(parsedError); + } + + throw error; + } + } +} diff --git a/ethos/packages/blockchain-manager/src/contracts/ContractAddressManager.ts b/ethos/packages/blockchain-manager/src/contracts/ContractAddressManager.ts new file mode 100644 index 0000000..2965306 --- /dev/null +++ b/ethos/packages/blockchain-manager/src/contracts/ContractAddressManager.ts @@ -0,0 +1,48 @@ +import { type ContractLookup, TypeChain } from '@ethos/contracts'; +import { type ContractRunner, type ContractTransactionResponse } from 'ethers'; +import { getAddress, type Address } from 'viem'; + +export class ContractAddressManager { + public readonly address: Address; + public readonly contractRunner: ContractRunner; + public readonly contract: TypeChain.ContractAddressManagerAbi; + + constructor(runner: ContractRunner, contractLookup: ContractLookup) { + this.address = contractLookup.contractAddressManager.address; + this.contractRunner = runner; + this.contract = TypeChain.ContractAddressManagerAbi__factory.connect(this.address, runner); + } + + /** + * Updates contract addresses for given names. + * @param contractAddresses Array of contract addresses. + * @param names Array of contract names. + * @returns Transaction response. + */ + async updateContractAddressesForNames( + contractAddresses: Address[], + names: string[], + ): Promise { + return await this.contract.updateContractAddressesForNames(contractAddresses, names); + } + + /** + * Returns contract address for a given name. + * @param name Name of the contract. + * @returns Contract address. + */ + async getContractAddressForName(name: string): Promise
{ + const address = await this.contract.getContractAddressForName(name); + + return getAddress(address); + } + + /** + * Checks if the given address is an Ethos contract. + * @param targetAddress Address to check. + * @returns Boolean indicating if the address is an Ethos contract. + */ + async checkIsEthosContract(targetAddress: Address): Promise { + return await this.contract.checkIsEthosContract(targetAddress); + } +} diff --git a/ethos/packages/blockchain-manager/src/contracts/EthosAttestation.test.ts b/ethos/packages/blockchain-manager/src/contracts/EthosAttestation.test.ts new file mode 100644 index 0000000..d9bc0e3 --- /dev/null +++ b/ethos/packages/blockchain-manager/src/contracts/EthosAttestation.test.ts @@ -0,0 +1,32 @@ +import { hashServiceAndAccount } from './utils.js'; + +describe('hashServiceAndAccount', () => { + it('should return the same hash as the blockchain manager getServiceAndAccountHash function', () => { + const combinations = [ + { + service: 'service', + account: 'account', + hash: '0x0423512e07aff27ea2f78564b40f9d7a52209e156a44a01cb48d4d55cf3f0445', + }, + { + service: 'x.com', + account: 'benwalther256', + hash: '0xf62354dc3386209390cbd17034bc141ecf753e4116fc9d5a4b6f80bbb407fac4', + }, + { + service: 'x.com', + account: ' ', + hash: '0xb7846bcd824253c55de64c0102495685c17dc835768c55e47b5248dcf07a2300', + }, + { + service: 'anything else', + account: 'arbitrary string', + hash: '0x024fa0fed9e57db3fec0dac633d6cc0cb896bcf4476ffcc6bab2b93e1a9dd64a', + }, + ]; + + for (const { service, account, hash: expectedHash } of combinations) { + expect(hashServiceAndAccount(service, account)).toBe(expectedHash); + } + }); +}); diff --git a/ethos/packages/blockchain-manager/src/contracts/EthosAttestation.ts b/ethos/packages/blockchain-manager/src/contracts/EthosAttestation.ts new file mode 100644 index 0000000..8738b66 --- /dev/null +++ b/ethos/packages/blockchain-manager/src/contracts/EthosAttestation.ts @@ -0,0 +1,107 @@ +import { type ContractLookup, TypeChain } from '@ethos/contracts'; +import { type ContractRunner, type ContractTransactionResponse, toNumber } from 'ethers'; +import { type Address } from 'viem'; +import { type Attestation, type AttestationService } from '../types.js'; +import { hashServiceAndAccount } from './utils.js'; + +export class EthosAttestation { + public readonly address: Address; + public readonly contractRunner: ContractRunner; + public readonly contract: TypeChain.AttestationAbi; + + constructor(runner: ContractRunner, contractLookup: ContractLookup) { + this.address = contractLookup.attestation.address; + this.contractRunner = runner; + this.contract = TypeChain.AttestationAbi__factory.connect(this.address, runner); + } + + /** + * Creates a new attestation and links it to the current sender's profile. + * @param profileId Profile id. Use max uint for non-existing profile. + * @param randValue Random value. + * @param attestationDetails Attestation details with service name and account. + * @param evidence Evidence of attestation. + * @param signature Signature of the attestation. + * @returns Transaction response. + */ + async createAttestation( + profileId: number, + randValue: number, + attestationDetails: { account: string; service: AttestationService }, + evidence: string, + signature: string, + ): Promise { + return await this.contract.createAttestation( + profileId, + randValue, + attestationDetails, + evidence, + signature, + ); + } + + /** + * Archives attestation. + * @param attestationHash Hash of the attestation. + * @returns Transaction response. + */ + async archiveAttestation( + service: AttestationService, + account: string, + ): Promise { + const attestationHash = hashServiceAndAccount(service, account); + + return await this.contract.archiveAttestation(attestationHash); + } + + /** + * @dev Restores attestation. + * @param attestationHash Hash of the attestation. + */ + async restoreAttestation( + service: AttestationService, + account: string, + ): Promise { + const attestationHash = hashServiceAndAccount(service, account); + + return await this.contract.restoreAttestation(attestationHash); + } + + /** + * Gets attestation details by hash. + * @param hash Attestation hash. + * @returns Attestation. + */ + async attestationByHash(hash: string): Promise { + const { attestationId, archived, profileId, createdAt, account, service } = + await this.contract.attestationByHash(hash); + + return { + id: toNumber(attestationId), + hash, + archived, + profileId: toNumber(profileId), + createdAt: toNumber(createdAt), + account: account.toLowerCase(), + service: service.toLowerCase() as AttestationService, + }; + } + + /** + * Gets attestation hashes by profile id. + * @param profileId Profile id. + * @returns Array of attestation hashes. + */ + async getAttestationHashesByProfileId(profileId: number): Promise { + return await this.contract.getAttestationHashesByProfileId(profileId); + } + + /** + * Checks whether the attestation exists for provided hash. + * @param hash The hash of the attestation. + * @returns Whether the attestation exists. + */ + async attestationExistsForHash(hash: string): Promise { + return await this.contract.attestationExistsForHash(hash); + } +} diff --git a/ethos/packages/blockchain-manager/src/contracts/EthosDiscussion.ts b/ethos/packages/blockchain-manager/src/contracts/EthosDiscussion.ts new file mode 100644 index 0000000..707d456 --- /dev/null +++ b/ethos/packages/blockchain-manager/src/contracts/EthosDiscussion.ts @@ -0,0 +1,91 @@ +import { type ContractLookup, TypeChain, type TargetContract } from '@ethos/contracts'; +import { type ContractRunner } from 'ethers'; +import { type Address } from 'viem'; + +export class EthosDiscussion { + public readonly address: Address; + public readonly contractRunner: ContractRunner; + public readonly contract: TypeChain.DiscussionAbi; + private readonly contractLookup: ContractLookup; + + constructor(runner: ContractRunner, contractLookup: ContractLookup) { + this.address = contractLookup.discussion.address; + this.contractRunner = runner; + this.contract = TypeChain.DiscussionAbi__factory.connect(this.address, runner); + this.contractLookup = contractLookup; + } + + /** + * Adds a reply to a target contract with a target id. + */ + async addReply( + targetContract: TargetContract, + targetId: number, + content: string, + ): ReturnType { + return await this.contract.addReply( + this.contractLookup[targetContract].address, + targetId, + content, + '', + ); + } + + /** + * Gets replies by ID. + */ + async repliesById(replyIds: bigint[]): ReturnType { + return await this.contract.repliesById(replyIds); + } + + /** + * Edits an existing reply. + */ + async editReply( + replyId: number, + content: string, + metadata: string, + ): ReturnType { + return await this.contract.editReply(replyId, content, metadata); + } + + /** + * Gets replies by author within a specified range. + */ + async repliesByAuthorInRange( + author: number, + fromIdx: number, + maxLength: number, + ): ReturnType { + return await this.contract.repliesByAuthorInRange(author, fromIdx, maxLength); + } + + /** + * Gets direct replies for a target within a specified range. + */ + async directRepliesInRange( + targetContract: Address, + parentId: number, + fromIdx: number, + maxLength: number, + ): ReturnType { + return await this.contract.directRepliesInRange(targetContract, parentId, fromIdx, maxLength); + } + + /** + * Gets the number of direct replies for a given target. + */ + async directReplyCount( + targetContract: Address, + targetId: number, + ): ReturnType { + return await this.contract.directReplyCount(targetContract, targetId); + } + + /** + * Gets the total number of replies. + */ + async getReplyCount(): Promise { + return await this.contract.replyCount(); + } +} diff --git a/ethos/packages/blockchain-manager/src/contracts/EthosProfile.ts b/ethos/packages/blockchain-manager/src/contracts/EthosProfile.ts new file mode 100644 index 0000000..86eebf4 --- /dev/null +++ b/ethos/packages/blockchain-manager/src/contracts/EthosProfile.ts @@ -0,0 +1,269 @@ +import { type ContractLookup, TypeChain } from '@ethos/contracts'; +import { isValidAddress } from '@ethos/helpers'; +import { toNumber, type ContractRunner, type ContractTransactionResponse } from 'ethers'; +import { isAddressEqual, zeroAddress, type Address, isAddress, getAddress } from 'viem'; +import { type Profile, type ProfileId } from '../types.js'; + +export class EthosProfile { + public readonly address: Address; + public readonly contractRunner: ContractRunner; + public readonly contract: TypeChain.ProfileAbi; + + constructor(runner: ContractRunner, contractLookup: ContractLookup) { + this.address = contractLookup.profile.address; + this.contractRunner = runner; + this.contract = TypeChain.ProfileAbi__factory.connect(this.address, runner); + } + + /** + * Invites an address to create an Ethos profile. + * @param address Address to invite. + * @returns Transaction response. + */ + async inviteAddress(address: Address): Promise { + return await this.contract.inviteAddress(address); + } + + /** + * Adds invites to a profile. + * @param user Address of the profile to add invites to. + * @param amount Quantity of invites to add to the profile. + * @returns Transaction response. + */ + async addInvites(user: Address, amount: number): Promise { + return await this.contract.addInvites(user, amount); + } + + /** + * Revokes invitation of an address to create an Ethos profile. + * @param address Address to invite. + * @returns Transaction response. + */ + async uninviteUser(address: Address): Promise { + return await this.contract.uninviteUser(address); + } + + /** + * Creates a new Ethos profile for the sender. + * @param inviterId profileID of the account that is inviting the new user + * @returns Transaction response. + */ + async createProfile(inviterId: ProfileId): Promise { + return await this.contract.createProfile(inviterId); + } + + /** + * Archives an ethos profile. + * @returns Transaction response. + */ + async archiveProfile(): Promise { + return await this.contract.archiveProfile(); + } + + /** + * Restores an ethos profile. + * @returns Transaction response. + */ + async restoreProfile(): Promise { + return await this.contract.restoreProfile(); + } + + /** + * Registers an address to an Ethos profile. + * @returns Transaction response. + */ + async registerAddress( + address: Address, + id: ProfileId, + randValue: number, + signature: string, + ): Promise { + return await this.contract.registerAddress(address, id, randValue, signature); + } + + /** + * Deletes an address from an Ethos profile. + * @param index Index of the address to delete. + * @returns Transaction response. + */ + async deleteAddressAtIndex(index: number): Promise { + return await this.contract.deleteAddressAtIndex(index, false); + } + + /** + * Deletes an address from an Ethos profile. + * @param address Address to delete. + * @param markAsCompromised Whether to mark the address as compromised. + * @returns Transaction response. + */ + async deleteAddress( + address: Address, + markAsCompromised: boolean, + ): Promise { + return await this.contract.deleteAddress(address, markAsCompromised); + } + + /** + * Retrieve a profile from smart contract by id + * @param id Profile id. + * @returns Basic profile information, minus address information (see addressesForProfile) + */ + async profile(id: ProfileId): Promise | null> { + if (id < 0) return null; + const { archived, profileId, createdAt, inviteInfo } = await this.contract.getProfile(id); + + return { + archived, + id: toNumber(profileId), + createdAt: toNumber(createdAt), + inviteInfo: { + sent: inviteInfo.sent.map((address) => getAddress(address)), + acceptedIds: inviteInfo.acceptedIds.map(toNumber), + available: toNumber(inviteInfo.available), + invitedBy: toNumber(inviteInfo.invitedBy), + }, + }; + } + + /** + * @param id Profile id. + * @returns Addresses for profile. + */ + async addressesForProfile(id: ProfileId): Promise { + if (id < 0) return []; + const addresses = await this.contract.addressesForProfile(id); + + return addresses.map((address) => getAddress(address)); + } + + /** + * Gets verified profile id for address. + * @param address Address to be checked. + * @returns Profile id. + */ + async verifiedProfileIdForAddress(address: Address): Promise { + if (!isAddress(address)) return 0; + if (isAddressEqual(address, zeroAddress)) return 0; + const id = await this.contract.verifiedProfileIdForAddress(address); + + return toNumber(id); + } + + /** + * @param id Profile id. + * @returns Basic profile information and associated addresses. + */ + async getProfile(id: number): Promise { + if (id < 0) return null; + const [profileData, addresses] = await Promise.all([ + this.profile(id), + this.addressesForProfile(id), + ]); + + if (!profileData) return null; + + const { archived, createdAt, inviteInfo } = profileData; + + const profile: Profile = { + archived, + id: toNumber(id), + addresses, + primaryAddress: addresses[0], + createdAt, + inviteInfo, + }; + + return profile; + } + + /** + * @returns Basic profile information and associated addresses. + */ + async getProfileByAddress(address: Address): Promise { + if (!isAddress(address)) return null; + if (isAddressEqual(address, zeroAddress)) return null; + try { + const status = await this.getProfileStatusByAddress(address); + + if (status.profileId === 0) { + return null; + } + + return await this.getProfile(status.profileId); + } catch (error) { + return null; + } + } + + /** + * Retrieves the unix timestamp for the time an invitation was sent + * @param sender - The ProfileId of the account that sent the invitation. + * @param recipient - The address where the invitation was sent. + * @returns The unix timestamp of when the invitation was sent, or 0 if no such invitation exists. + */ + async getInvitationSentTime(sender: ProfileId, recipient: Address): Promise { + if (sender < 0) return 0; + if (!isValidAddress(recipient)) return 0; + const sentAt = await this.contract.sentAt(sender, recipient); + + return toNumber(sentAt); + } + + /** + * Invites multiple addresses to create Ethos profiles in a single transaction. + * @param addresses Array of addresses to invite. + * @returns Transaction response. + */ + async bulkInviteAddresses(addresses: Address[]): Promise { + return await this.contract.bulkInviteAddresses(addresses); + } + + /** + * Gets the total number of profiles created. + * @returns The total number of profiles. + */ + async getProfileCount(): Promise { + const count = await this.contract.profileCount(); + + return toNumber(count); + } + + /** + * Returns array of addresses that have pending invites for given profileId + * @param profileId Profile id. + * @returns Array of addresses with pending invites. + */ + async sentInvitationsForProfile(profileId: ProfileId): Promise { + if (profileId <= 0) return []; + const sentInvitations = await this.contract.sentInvitationsForProfile(profileId); + + return sentInvitations.map((address) => getAddress(address)); + } + + /** + * Gets the profile status by ID. + * @param profileId The ID of the profile. + * @returns An object containing verified, archived, and mock status. + */ + async getProfileStatusById( + profileId: ProfileId, + ): Promise<{ verified: boolean; archived: boolean; mock: boolean }> { + const [verified, archived, mock] = await this.contract.profileStatusById(profileId); + + return { verified, archived, mock }; + } + + /** + * Gets the profile status by address. + * @param address The address to check. + * @returns An object containing verified, archived, mock status, and profileId. + */ + async getProfileStatusByAddress( + address: Address, + ): Promise<{ verified: boolean; archived: boolean; mock: boolean; profileId: number }> { + const [verified, archived, mock, profileId] = + await this.contract.profileStatusByAddress(address); + + return { verified, archived, mock, profileId: toNumber(profileId) }; + } +} diff --git a/ethos/packages/blockchain-manager/src/contracts/EthosReview.ts b/ethos/packages/blockchain-manager/src/contracts/EthosReview.ts new file mode 100644 index 0000000..d6bd6a9 --- /dev/null +++ b/ethos/packages/blockchain-manager/src/contracts/EthosReview.ts @@ -0,0 +1,156 @@ +import { type ContractLookup, TypeChain } from '@ethos/contracts'; +import { isValidAddress } from '@ethos/helpers'; +import { toNumber, type ContractRunner, type ContractTransactionResponse } from 'ethers'; +import { type Address, isAddress, zeroAddress } from 'viem'; +import { + Score, + ScoreByValue, + type Review, + type ScoreType, + type ReviewTarget, + getScoreValue, +} from '../types.js'; + +type ReviewRaw = Awaited>; + +export class EthosReview { + public readonly address: Address; + public readonly contractRunner: ContractRunner; + public readonly contract: TypeChain.ReviewAbi; + + constructor(runner: ContractRunner, contractLookup: ContractLookup) { + this.address = contractLookup.review.address; + this.contractRunner = runner; + this.contract = TypeChain.ReviewAbi__factory.connect(this.address, runner); + } + + /** + * Adds a review. + * @returns Transaction response. + */ + async addReview( + scoreType: ScoreType, + subject: ReviewTarget, + comment: string, + metadata: string, + ): Promise { + const score = Score[scoreType]; + + const address = 'address' in subject ? subject.address : zeroAddress; + const attestation = 'service' in subject ? subject : { service: '', account: '' }; + const paymentToken = zeroAddress; + + return await this.contract.addReview( + score, + address, + paymentToken, + comment, + metadata, + attestation, + ); + } + + /** + * Edits an existing review. May only be called by the original author of the review. + */ + async editReview( + id: number, + comment: string, + metadata: string, + ): Promise { + return await this.contract.editReview(id, comment, metadata); + } + + /** + * Archives a review. + * @returns Transaction response. + */ + async archiveReview(id: number): Promise { + return await this.contract.archiveReview(id); + } + + /** + * Restores an archived review. + * @param id The ID of the review to restore. + * @returns Transaction response. + */ + async restoreReview(id: number): Promise { + return await this.contract.restoreReview(id); + } + + /** + * Get review details. + */ + async getReview(id: number): Promise { + const rawReview = await this.contract.reviews(id); + + return this.formatRawReview(rawReview); + } + + private formatRawReview(rawReview: ReviewRaw): Review | null { + const { + archived, + score, + author, + subject, + reviewId, + createdAt, + comment, + metadata, + attestationDetails: { account, service }, + } = rawReview; + + if (!isValidAddress(author)) { + return null; + } + + return { + // TODO: figure out how to get review id when we request reviews by subject/author + id: toNumber(reviewId), + archived: Boolean(archived), + score: ScoreByValue[getScoreValue(toNumber(score))], + author: isAddress(author) ? author : zeroAddress, + subject: isAddress(subject) ? subject : zeroAddress, + createdAt: toNumber(createdAt), + comment, + metadata, + attestationDetails: { + account: account.toLowerCase(), + service: service.toLowerCase(), + }, + }; + } + + /** + * Returns the number of reviews. Also, it's the same as the most recent review id. + */ + async reviewCount(): Promise { + const reviewCount = await this.contract.reviewCount(); + + return toNumber(reviewCount); + } + + /** + * Sets review price for a specific payment token. + * @param allowed Whether the token is allowed. + * @param paymentToken Payment token address. + * @param price Review price. + * @returns Transaction response. + */ + async setReviewPrice( + allowed: boolean, + paymentToken: Address, + price: bigint, + ): Promise { + return await this.contract.setReviewPrice(allowed, paymentToken, price); + } + + /** + * Withdraws funds from the contract. + * @param paymentToken Payment token address. + * @returns Transaction response. + */ + async withdrawFunds(paymentToken: Address): Promise { + return await this.contract.withdrawFunds(paymentToken); + } +} diff --git a/ethos/packages/blockchain-manager/src/contracts/EthosVote.ts b/ethos/packages/blockchain-manager/src/contracts/EthosVote.ts new file mode 100644 index 0000000..3446d04 --- /dev/null +++ b/ethos/packages/blockchain-manager/src/contracts/EthosVote.ts @@ -0,0 +1,104 @@ +import { type ContractLookup, TypeChain, type TargetContract } from '@ethos/contracts'; +import { type ContractRunner, type ContractTransactionResponse } from 'ethers'; +import { type Address } from 'viem'; + +export class EthosVote { + public readonly address: Address; + public readonly contractRunner: ContractRunner; + public readonly contract: TypeChain.VoteAbi; + private readonly contractLookup: ContractLookup; + + constructor(runner: ContractRunner, contractLookup: ContractLookup) { + this.address = contractLookup.vote.address; + this.contractRunner = runner; + this.contract = TypeChain.VoteAbi__factory.connect(this.address, runner); + this.contractLookup = contractLookup; + } + + /** + * Votes for a target contract with a target id. + */ + async voteFor( + targetContract: TargetContract, + targetId: number, + isUpvote: boolean, + ): Promise { + return await this.contract.voteFor( + this.contractLookup[targetContract].address, + targetId, + isUpvote, + ); + } + + /** + * Gets vote by id. + */ + async getVoteById(id: number): ReturnType { + return await this.contract.votes(id); + } + + /** + * Gets the total number of votes. + */ + async getVoteCount(): Promise { + return await this.contract.voteCount(); + } + + /** + * Return the vote id for a specific voter, target contract, and target id. + * Target is typically the review contract and review id, or vouch contract and vouch id, etc. + */ + async getVoteIndexFor( + voter: number, + targetContract: TargetContract, + targetId: number, + ): Promise { + return await this.contract.voteIndexFor( + voter, + this.contractLookup[targetContract].address, + targetId, + ); + } + + /** + * Checks if a voter has voted for a target contract and target id. + */ + async hasVotedFor( + voter: number, + targetContract: TargetContract, + targetId: number, + ): Promise { + return await this.contract.hasVotedFor( + voter, + this.contractLookup[targetContract].address, + targetId, + ); + } + + /** + * Gets the vote counts (upvotes and downvotes) for a target contract and target id. + */ + async getVotesCountFor( + targetContract: TargetContract, + targetId: number, + ): Promise<[bigint, bigint]> { + return await this.contract.votesCountFor(this.contractLookup[targetContract].address, targetId); + } + + /** + * Gets votes in range for a target contract and target id. + */ + async getVotesInRangeFor( + targetContract: TargetContract, + targetId: number, + fromIdx: number, + maxLength: number, + ): ReturnType { + return await this.contract.votesInRangeFor( + this.contractLookup[targetContract].address, + targetId, + fromIdx, + maxLength, + ); + } +} diff --git a/ethos/packages/blockchain-manager/src/contracts/EthosVouch.ts b/ethos/packages/blockchain-manager/src/contracts/EthosVouch.ts new file mode 100644 index 0000000..e23ae1a --- /dev/null +++ b/ethos/packages/blockchain-manager/src/contracts/EthosVouch.ts @@ -0,0 +1,215 @@ +import { type ContractLookup, TypeChain } from '@ethos/contracts'; +import { type ContractRunner, type ContractTransactionResponse, toNumber } from 'ethers'; +import { getAddress, type Hex, parseUnits, type Address } from 'viem'; +import { type AttestationTarget, type Fees, type Vouch } from '../types.js'; +import { hashServiceAndAccount } from './utils.js'; + +type VouchRaw = Awaited>; + +export class EthosVouch { + public readonly address: Address; + public readonly contractRunner: ContractRunner; + public readonly contract: TypeChain.VouchAbi; + + constructor(runner: ContractRunner, contractLookup: ContractLookup) { + this.address = contractLookup.vouch.address; + this.contractRunner = runner; + this.contract = TypeChain.VouchAbi__factory.connect(this.address, runner); + } + + /** + * Returns the number of vouches. Also, it's the same as the most recent vouch id. + */ + async vouchCount(): Promise { + const vouchCount = await this.contract.vouchCount(); + + return toNumber(vouchCount); + } + + /** + * Get vouch details. + */ + async getVouch(id: number): Promise { + const rawVouch = await this.contract.vouches(id); + + return this.formatRawVouch(rawVouch); + } + + /** + * Vouches for profile Id. + * @param subjectProfileId Vouchee profile Id. + * @param paymentAmount Payment amount. Must be equal to msg.value for native token. + * @param comment Vouch title + * @param metadata Optional metadata. + */ + async vouchByProfileId( + subjectProfileId: number, + paymentAmount: string, + comment: string, + metadata?: string, + ): Promise { + const value = parseUnits(paymentAmount, 18); + + return await this.contract.vouchByProfileId(subjectProfileId, comment, metadata ?? '', { + value, + }); + } + + /** + * Vouches for address, allowing vouches for addresses that have not yet joined Ethos, or + * redirecting to a vouch by profileId if the address is registered. + * @param voucheeAddress Vouchee address. + * @param paymentAmount Payment amount. Must be equal to msg.value for native token. + * @param comment Vouch title + * @param metadata Optional metadata. + */ + async vouchByAddress( + subjectAddress: Address, + paymentAmount: string, + comment: string, + metadata?: string, + ): Promise { + const value = parseUnits(paymentAmount, 18); + + return await this.contract.vouchByAddress(subjectAddress, comment, metadata ?? '', { + value, + }); + } + + /** + * Vouches by attestation hash, allowing vouches for social accounts that have not yet joined Ethos, or + * redirecting to a vouch by profileId if the attestation hash is registered. + * @param attestationHash The attestation hash identifying the subject to vouch for + * @param paymentAmount Payment amount. Must be equal to msg.value for native token. + * @param comment Vouch title + * @param metadata Optional metadata. + */ + async vouchByAttestationHash( + attestationHash: Hex, + paymentAmount: string, + comment: string, + metadata?: string, + ): Promise { + const value = parseUnits(paymentAmount, 18); + + return await this.contract.vouchByAttestation(attestationHash, comment, metadata ?? '', { + value, + }); + } + + /** + * Vouches by attestation service and account, allowing vouches for social accounts that have not yet joined Ethos, or + * redirecting to a vouch by profileId if the attestation is registered. + * @param service - The attestation service (e.g., 'x.com', 'github.com') + * @param account - The account identifier for the attestation service (user id, username) + * @param paymentAmount - Payment amount in ETH. Must be equal to msg.value for native token + * @param comment - Vouch title + * @param metadata - Optional metadata to include with the vouch + * @returns A promise that resolves to the contract transaction response + */ + async vouchByAttestation( + attestation: AttestationTarget, + paymentAmount: string, + comment: string, + metadata?: string, + ): Promise { + const value = parseUnits(paymentAmount, 18); + const attestationHash = hashServiceAndAccount(attestation.service, attestation.account); + + return await this.contract.vouchByAttestation(attestationHash, comment, metadata ?? '', { + value, + }); + } + + /** + * Unvouches vouch. + */ + async unvouch(vouchId: number): Promise { + return await this.contract.unvouch(vouchId); + } + + /** + * Unvouches vouch. + */ + async unvouchUnhealthy(vouchId: number): Promise { + return await this.contract.unvouchUnhealthy(vouchId); + } + + /** + * Returns all fee configurations from the contract + */ + async getAllFees(): Promise { + const [ + entryProtocolFeeBasisPoints, + exitFeeBasisPoints, + entryDonationFeeBasisPoints, + entryVouchersPoolFeeBasisPoints, + ] = await Promise.all([ + this.contract.entryProtocolFeeBasisPoints(), + this.contract.exitFeeBasisPoints(), + this.contract.entryDonationFeeBasisPoints(), + this.contract.entryVouchersPoolFeeBasisPoints(), + ]); + + return { + entryProtocolFeeBasisPoints, + exitFeeBasisPoints, + entryDonationFeeBasisPoints, + entryVouchersPoolFeeBasisPoints, + }; + } + + /** + * Gets the rewards balance for a profile ID + * @param profileId The profile ID to check rewards balance for + * @returns The rewards balance + */ + async getRewardsBalance(profileId: number): Promise<{ balance: string }> { + const balance = await this.contract.rewardsByProfileId(profileId); + + return { + balance: balance.toString(), + }; + } + + /** + * Withdraws all rewards + * @returns The transaction response + */ + async claimRewards(): Promise { + // The contract only supports claiming all rewards at once via claimRewards() + return await this.contract.claimRewards(); + } + + private formatRawVouch(vouch: VouchRaw): Vouch | null { + const { + archived, + unhealthy, + authorProfileId, + authorAddress, + vouchId, + subjectProfileId, + balance, + comment, + metadata, + activityCheckpoints: { vouchedAt, unvouchedAt, unhealthyAt }, + } = vouch; + + return { + id: toNumber(vouchId), + archived: Boolean(archived), + unhealthy: Boolean(unhealthy), + authorProfileId: toNumber(authorProfileId), + authorAddress: getAddress(authorAddress), + subjectProfileId: toNumber(subjectProfileId), + balance, + comment, + metadata, + activityCheckpoints: { + vouchedAt: toNumber(vouchedAt), + unvouchedAt: toNumber(unvouchedAt), + unhealthyAt: toNumber(unhealthyAt), + }, + }; + } +} diff --git a/ethos/packages/blockchain-manager/src/contracts/ReputationMarket.ts b/ethos/packages/blockchain-manager/src/contracts/ReputationMarket.ts new file mode 100644 index 0000000..4bdc8db --- /dev/null +++ b/ethos/packages/blockchain-manager/src/contracts/ReputationMarket.ts @@ -0,0 +1,309 @@ +import { type ContractLookup, type TypeChainCommon, TypeChain } from '@ethos/contracts'; + +import { + type ErrorDescription, + isCallException, + type ContractRunner, + type ContractTransactionResponse, +} from 'ethers'; +import { getAddress, type Address } from 'viem'; +import { type ProfileId } from '../types.js'; + +export class ReputationMarketError extends Error { + reason: 'INSUFFICIENT_VOTES' | 'INSUFFICIENT_FUNDS' | 'SLIPPAGE_LIMIT_EXCEEDED' | 'UNKNOWN'; + + constructor(reason: ReputationMarketError['reason'], message: string, error?: ErrorDescription) { + super(message, { cause: error }); + this.reason = reason; + } + + static fromErrorDescription(error: ErrorDescription): ReputationMarketError { + switch (error.name) { + case 'InsufficientVotesOwned': + return new ReputationMarketError('INSUFFICIENT_VOTES', 'Insufficient votes owned', error); + case 'InsufficientFunds': + return new ReputationMarketError('INSUFFICIENT_FUNDS', 'Insufficient funds', error); + case 'SlippageLimitExceeded': + return new ReputationMarketError( + 'SLIPPAGE_LIMIT_EXCEEDED', + 'Slippage limit exceeded', + error, + ); + default: { + return new ReputationMarketError('UNKNOWN', 'Unexpected error', error); + } + } + } +} + +type Market = { + profileId: ProfileId; + trustVotes: bigint; + distrustVotes: bigint; + basePrice: bigint; + liquidityParameter: bigint; +}; + +export class ReputationMarket { + public readonly address: Address; + public readonly contractRunner: ContractRunner; + public readonly contract: TypeChain.ReputationMarketAbi; + + constructor(runner: ContractRunner, contractLookup: ContractLookup) { + this.address = contractLookup.reputationMarket.address; + this.contractRunner = runner; + this.contract = TypeChain.ReputationMarketAbi__factory.connect(this.address, runner); + } + + async getVotePrice(profileId: number, isPositive: boolean): Promise { + return await this.contract.getVotePrice(profileId, isPositive); + } + + async getUserVotes( + user: Address, + profileId: number, + ): Promise<{ trustVotes: bigint; distrustVotes: bigint }> { + return await this.contract.getUserVotes(user, profileId); + } + + async createMarket(value: bigint): Promise { + return await this.contract.createMarket({ value }); + } + + async calculateVotesToBuy( + profileId: number, + voteType: 'trust' | 'distrust', + buyAmountWei: bigint, + slippagePercentage: number, + buyFeePercentage: number, + ): Promise<{ minVotesToBuy: number; maxVotesToBuy: number }> { + const market = await this.getMarket(profileId); + + const buyAmountMinusFees = + buyAmountWei - (buyAmountWei * BigInt(buyFeePercentage * 100)) / BigInt(100); + + const M = Number(buyAmountMinusFees) / Number(market.basePrice); + const b = Number(market.liquidityParameter); + const Y = Number(market.trustVotes); + const N = Number(market.distrustVotes); + + const expY = Math.exp(Y / b); + const expN = Math.exp(N / b); + const expM = Math.exp(M / b); + + let X; + + if (voteType === 'trust') { + X = b * (Math.log(expM * (expY + expN) - expN) - Y / b); + } else { + X = b * (Math.log(expM * (expY + expN) - expY) - N / b); + } + + let maxVotesToBuy: number; + + if (isNaN(X) || !isFinite(X)) { + maxVotesToBuy = 0; + } else { + const roundedX = Math.ceil((X * 100) / 100); + maxVotesToBuy = roundedX; + } + + const minVotesToBuy = Math.floor(maxVotesToBuy * (1 - slippagePercentage)); + + return { + minVotesToBuy, + maxVotesToBuy, + }; + } + + async createMarketWithConfigAdmin( + marketOwner: Address, + marketConfigIndex: number, + funds: bigint, + ): Promise { + return await this.contract.createMarketWithConfigAdmin(marketOwner, marketConfigIndex, { + value: funds, + }); + } + + async getMarketConfigs(): Promise< + Array<{ configIndex: number; liquidity: bigint; creationCost: bigint; basePrice: bigint }> + > { + const marketConfigCount = await this.contract.getMarketConfigCount(); + + const configs = await Promise.all( + Array.from({ length: Number(marketConfigCount) }, async (_, i) => { + const config = await this.contract.marketConfigs(i); + + return { + configIndex: i, + liquidity: config.liquidity, + creationCost: config.creationCost, + basePrice: config.basePrice, + }; + }), + ); + + return configs; + } + + async getBuyFeePercentage(): Promise { + const [entryProtocolBasisPoints, donationBasisPoints] = await Promise.all([ + this.contract.entryProtocolFeeBasisPoints(), + this.contract.donationBasisPoints(), + ]); + + return (Number(entryProtocolBasisPoints) + Number(donationBasisPoints)) / 10000; + } + + async buyVotes( + profileId: number, + buyAmountWei: bigint, + isPositive: boolean, + maxVotesToBuy: number, + minVotesToBuy: number, + ): Promise { + return await this.wrapErrors(this.contract.buyVotes, [ + profileId, + isPositive, + maxVotesToBuy, + minVotesToBuy, + { + value: buyAmountWei, + }, + ]); + } + + async sellVotes( + profileId: number, + isPositive: boolean, + amount: number, + minimumVotePrice: bigint, + ): Promise { + return await this.wrapErrors(this.contract.sellVotes, [ + profileId, + isPositive, + amount, + minimumVotePrice, + ]); + } + + async getMarket(profileId: number): Promise { + const market = await this.contract.getMarket(profileId); + + return { + profileId: Number(market.profileId), + trustVotes: market.trustVotes, + distrustVotes: market.distrustVotes, + basePrice: market.basePrice, + liquidityParameter: market.liquidityParameter, + }; + } + + async simulateSell( + profileId: number, + isPositive: boolean, + amount: number, + address: Address, + ): Promise<{ + proceedsBeforeFees: bigint; + protocolFee: bigint; + proceedsAfterFees: bigint; + newVotePrice: bigint; + }> { + try { + return await this.contract.simulateSell(profileId, isPositive, amount, { from: address }); + } catch (error) { + const marketError = this.tryParseError(error); + throw marketError; + } + } + + async simulateBuy( + profileId: number, + isPositive: boolean, + votesToBuy: number, + ): Promise<{ + purchaseCostBeforeFees: bigint; + protocolFee: bigint; + donation: bigint; + totalCostIncludingFees: bigint; + newVotePrice: bigint; + }> { + try { + return await this.contract.simulateBuy(profileId, isPositive, BigInt(votesToBuy)); + } catch (error) { + const marketError = this.tryParseError(error); + throw marketError; + } + } + + async getParticipants(profileId: number, index: number): Promise
{ + const participant = await this.contract.participants(profileId, index); + + return getAddress(participant); + } + + async isParticipant(profileId: number, address: Address): Promise { + return await this.contract.isParticipant(profileId, address); + } + + async getParticipantCount(profileId: number): Promise { + return await this.contract.getParticipantCount(profileId); + } + + async setIsProfileAllowedToCreateMarket( + profileId: number, + isAllowed: boolean, + ): Promise { + return await this.contract.setUserAllowedToCreateMarket(profileId, isAllowed); + } + + async getIsProfileAllowedToCreateMarket(profileId: number): Promise { + return await this.contract.isAllowedToCreateMarket(profileId); + } + + private async wrapErrors< + A extends any[] = any[], + R = any, + S extends TypeChainCommon.StateMutability = 'payable', + >( + method: TypeChainCommon.TypedContractMethod, + args: TypeChainCommon.ContractMethodArgs, + ): Promise< + S extends 'view' + ? Promise> + : Promise + > { + try { + return await method(...args); + } catch (error: any) { + if (error.action === 'estimateGas') { + try { + // If it's an estimateGas error, it's likely that the custom revert data is not included. + // So we need to perform a static call to ensure the custom revert data is included. + await method.staticCall(...args); + // The static call is expected to throw at this point. + throw error; + } catch (err) { + throw this.tryParseError(err); + } + } + throw this.tryParseError(error); + } + } + + private tryParseError(error: any): Error { + if (!isCallException(error) || !error.data) { + return error; + } + + const parsedError = this.contract.interface.parseError(error.data); + + if (!parsedError) { + return new Error('Unexpected error', { cause: error }); + } + + return ReputationMarketError.fromErrorDescription(parsedError); + } +} diff --git a/ethos/packages/blockchain-manager/src/contracts/utils.ts b/ethos/packages/blockchain-manager/src/contracts/utils.ts new file mode 100644 index 0000000..c9f81ce --- /dev/null +++ b/ethos/packages/blockchain-manager/src/contracts/utils.ts @@ -0,0 +1,260 @@ +import { type UnionOptional } from '@ethos/helpers'; +import retry, { type Options } from 'async-retry'; +import { + type ContractRunner, + type Eip1193Provider, + type JsonRpcApiProvider, + type Provider, + type Signer, + BrowserProvider, + JsonRpcProvider, + Wallet, + getDefaultProvider, + type AbstractProvider, + type BaseContract, + type Log, + type FetchRequest, + type Networkish, + type JsonRpcApiProviderOptions, + type ContractEventName, + type Listener, +} from 'ethers'; +import { + encodeAbiParameters, + type Hex, + keccak256, + type Address, + hexToBytes, + encodePacked, +} from 'viem'; +import { isAlchemyRateLimitError } from '../providers.js'; +import { type CancelListener } from '../types.js'; + +const RATE_LIMIT_RETRIES = 5; + +declare global { + // eslint-disable-next-line no-var + var ethereum: Eip1193Provider; + // eslint-disable-next-line no-var + var window: Window & typeof globalThis; +} + +type Param = ['address', Address] | ['string', string] | ['uint256', bigint]; + +type AlchemyRunnerConfig = { + alchemyConnectionURL?: string; + walletPrivateKey?: string; + polling?: boolean; +}; + +type ExplicitProviderConfig = { + provider?: AbstractProvider; + signer?: Signer; +}; + +export type ContractRunnerConfig = AlchemyRunnerConfig | ExplicitProviderConfig; + +export function getContractRunner({ + alchemyConnectionURL, + provider: providerOverride, + signer, + walletPrivateKey, + polling, +}: UnionOptional = {}): ContractRunner { + const isBrowser = Boolean(globalThis.window); + let provider: AbstractProvider | undefined; + + try { + if (providerOverride) { + provider = providerOverride; + } else if (isBrowser && globalThis.ethereum) { + provider = new BrowserProvider(globalThis.ethereum); + } else if (alchemyConnectionURL) { + provider = new JsonRpcApiProviderWithRetry({ retries: 5 }, alchemyConnectionURL, undefined, { + polling, + }); + } + } catch (err) { + console.error('Error creating contract runner', { + err, + isBrowser, + hasBrowserProvider: Boolean(globalThis.ethereum), + }); + } finally { + if (!provider) { + // TODO: find a better fallback like loading only from a server cache w/out any blockchain support + provider = getDefaultProvider(); + } + } + + if (walletPrivateKey) { + return new Wallet(walletPrivateKey, provider); + } + + return new AsyncContractRunner(provider, signer); +} + +class AsyncContractRunner implements ContractRunner { + private _signer?: Signer; + public provider: Provider; + + constructor(underlying: AbstractProvider, signer?: Signer) { + this.provider = underlying; + + if (signer) { + this._signer = signer; + } + } + + protected async signer(): Promise { + if (!this._signer) { + this._signer = await (this.provider as JsonRpcApiProvider).getSigner(); + } + + return this._signer; + } + + async sendTransaction( + ...args: Parameters + ): ReturnType { + const signer = await this.signer(); + + return await signer.sendTransaction(...args); + } +} + +class JsonRpcApiProviderWithRetry extends JsonRpcProvider { + retryOptions: Options; + + constructor( + retryOptions: Options, + url?: string | FetchRequest, + network?: Networkish, + options?: JsonRpcApiProviderOptions, + ) { + super(url, network, options); + this.retryOptions = retryOptions; + } + + override async send( + ...args: Parameters + ): ReturnType { + return await retry(async (bail) => { + return await super.send(...args).catch((err: any) => { + if (err.error?.code !== 429) { + bail(err as Error); + + return; + } + + throw err; + }); + }, this.retryOptions); + } +} + +export function formatMessageToSign( + parameters: Param[], + type: 'solidityPacked' | 'abiEncoded', +): Uint8Array { + const types = parameters.map(([type]) => type); + const values = parameters.map(([, value]) => value); + + switch (type) { + case 'solidityPacked': { + const hash = keccak256(encodePacked(types, values)); + + return hexToBytes(hash); + } + case 'abiEncoded': { + const encoded = encodeAbiParameters( + types.map((type) => ({ type })), + values, + ); + const hash = keccak256(encoded); + + return hexToBytes(hash); + } + default: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Invalid type ${type}`); + } + } +} + +type EthosBaseContract = { + address: Address; + contractRunner: ContractRunner; + contract: BaseContract; +}; + +export async function getLogs( + ethosContract: EthosBaseContract, + fromBlock: number, + toBlock?: number, +): Promise { + if (ethosContract.contractRunner.provider === null) { + throw Error('contractRunner.provider cannot be null'); + } + let lastError: unknown; + + for (let attempt = 0; attempt < RATE_LIMIT_RETRIES; attempt++) { + try { + const events = await ethosContract.contractRunner.provider.getLogs({ + fromBlock, + toBlock, + address: ethosContract.address, + }); + + return events; + } catch (err) { + lastError = err; + + if (isAlchemyRateLimitError(err)) { + // rate limit is per second, so sleep for 1.5 + await new Promise((resolve) => setTimeout(resolve, 1500)); + continue; + } + + throw err; + } + } + + throw lastError; +} + +/** + * Registers event listeners for specified events on a contract. + * + * @param contract - The BaseContract instance to register listeners on. + * @param events - An array of TypedContractEvent objects representing the events to listen for. + * @param callback - A function to be called when any of the specified events are emitted. + * @returns A function that, when called, will remove all registered listeners. + */ +export async function registerListener( + contract: BaseContract, + events: ContractEventName[], + callback: Listener, +): Promise { + // Add listeners for each event + await Promise.all(events.map(async (event) => await contract.addListener(event, callback))); + + // Return a function that removes all registered listeners + return async (): Promise => { + await Promise.all(events.map(async (event) => await contract.removeListener(event, callback))); + }; +} + +/** + * Convenience function that hashes the service and account. + * Used to match smart contract generated attestation hashes. + * (compare to getServiceAndAccountHash contract method) + * @param service Service name. E.g., 'x.com'. + * @param account Account id. E.g., '115151312311'. + * @returns attestationHash string + */ +export function hashServiceAndAccount(service: string, account: string): Hex { + const encoded = encodeAbiParameters([{ type: 'string' }, { type: 'string' }], [service, account]); + + return keccak256(encoded); +} diff --git a/ethos/packages/blockchain-manager/src/index.ts b/ethos/packages/blockchain-manager/src/index.ts new file mode 100644 index 0000000..75b3774 --- /dev/null +++ b/ethos/packages/blockchain-manager/src/index.ts @@ -0,0 +1,30 @@ +export { BlockchainManager } from './BlockchainManager.js'; + +export type { + Attestation, + AttestationService, + AttestationTarget, + Balance, + CancelListener, + EmptyAttestationTarget, + Profile, + InviteInfo, + ProfileId, + Reply, + Review, + ReviewTarget, + ScoreType, + ScoreValue, + Vouch, + VouchFunds, + Vote, + Fees, +} from './types.js'; +export { isAttestationService } from './types.js'; +export { getScoreValue } from './types.js'; + +export { ReputationMarketError } from './contracts/ReputationMarket.js'; + +export { NegativeReview, NeutralReview, PositiveReview, Score, ScoreByValue } from './types.js'; +export { isAlchemyRateLimitError } from './providers.js'; +export { hashServiceAndAccount } from './contracts/utils.js'; diff --git a/ethos/packages/blockchain-manager/src/providers.ts b/ethos/packages/blockchain-manager/src/providers.ts new file mode 100644 index 0000000..d272a83 --- /dev/null +++ b/ethos/packages/blockchain-manager/src/providers.ts @@ -0,0 +1,21 @@ +export function isAlchemyRateLimitError(err: unknown): err is AlchemyRateLimitError { + if (typeof err !== 'object' || err === null) return false; + + const errorObj = err as { info?: { error?: { code?: number; message?: string } } }; + const errorInfo = errorObj.info?.error; + + return ( + errorInfo?.code === 429 && + typeof errorInfo.message === 'string' && + errorInfo.message.includes('exceeded its compute units per second capacity') + ); +} + +type AlchemyRateLimitError = { + info: { + error: { + code: 429; + message: string; + }; + }; +}; diff --git a/ethos/packages/blockchain-manager/src/types.ts b/ethos/packages/blockchain-manager/src/types.ts new file mode 100644 index 0000000..f4e7e81 --- /dev/null +++ b/ethos/packages/blockchain-manager/src/types.ts @@ -0,0 +1,203 @@ +import { type ErrorDescription } from 'ethers'; +import { type Address } from 'viem'; + +export type ProfileId = number; + +export type Profile = { + id: ProfileId; + archived: boolean; + createdAt: number; + addresses: Address[]; + primaryAddress: Address; + inviteInfo: InviteInfo; +}; + +export type InviteInfo = { + sent: Address[]; + acceptedIds: number[]; + available: number; + invitedBy: ProfileId; +}; + +export type AttestationService = 'x.com'; + +export function isAttestationService(service: string): service is AttestationService { + return service === 'x.com'; +} + +export type Attestation = { + id: number; + hash: string; + archived: boolean; + profileId: ProfileId; + account: string; + service: AttestationService; + createdAt: number; +}; + +export type AttestationTarget = { + service: AttestationService; + account: string; +}; + +export type EmptyAttestationTarget = { + service: ''; + account: ''; +}; + +export const Score = { + negative: 0, + neutral: 1, + positive: 2, +} as const; + +export type ScoreType = keyof typeof Score; +export type ScoreValue = (typeof Score)[ScoreType]; + +export const NegativeReview = 'negative' as const satisfies ScoreType; +export const NeutralReview = 'neutral' as const satisfies ScoreType; +export const PositiveReview = 'positive' as const satisfies ScoreType; + +export const ScoreByValue = { + 0: NegativeReview, + 1: NeutralReview, + 2: PositiveReview, +} as const satisfies Record; + +function isScoreValue(score: number): score is ScoreValue { + return Object.values(Score).some((s) => s === score); +} + +export function getScoreValue(score: number): ScoreValue { + if (!isScoreValue(score)) { + throw new Error('Invalid review score value'); + } + + return score; +} + +export type Review = { + id: number; + author: Address; + subject: Address; + score: ScoreType; + comment: string; + metadata: string; + createdAt: number; + archived: boolean; + attestationDetails?: { + account: string; + service: string; + }; +}; + +export type ReviewTarget = { address: Address } | { service: string; account: string }; + +export const ReviewsBy = { + author: 0, + subject: 1, + attestationHash: 2, +} as const; + +export type ReviewsByType = keyof typeof ReviewsBy; + +export type Vouch = { + id: number; + archived: boolean; + unhealthy: boolean; + authorAddress: Address; + authorProfileId: ProfileId; + subjectProfileId: ProfileId; + balance: bigint; + comment: string; + metadata: string; + activityCheckpoints: { + vouchedAt: number; + unvouchedAt: number; + unhealthyAt: number; + }; +}; + +/** + * Financial accounting for vouch balances at different points in time. + */ +export type VouchFunds = { + /** + * The initial amount transferred before fees. + */ + deposited: bigint; + /** + * The amount vouched after fees. + */ + staked: bigint; + /** + * The current balance. + */ + balance: bigint; + /** + * Amount withdrawn when unvouching (after fees); zero until unvouched. + */ + withdrawn: bigint; +}; + +export type Reply = { + parentIsOriginalComment: boolean; + targetContract: string; + authorProfileId: ProfileId; + id: bigint; + parentId: bigint; + createdAt: number; + content: string; + metadata: string; +}; + +export type Vote = { + isUpvote: boolean; + isArchived: boolean; + voter: ProfileId; + targetContract: Address; + targetId: bigint; + createdAt: number; +}; + +export type CancelListener = () => Promise; + +export type Balance = { + profileId: ProfileId; + token: Address; + balance: string; +}; + +export type Fees = { + entryProtocolFeeBasisPoints: bigint; + exitFeeBasisPoints: bigint; + entryDonationFeeBasisPoints: bigint; + entryVouchersPoolFeeBasisPoints: bigint; +}; + +export const MarketCreationErrorCode = { + PROFILE_NOT_ALLOWED: 0, + PROFILE_MISMATCH: 1, +} as const; + +export class BlockchainError extends Error { + override name: string; + args: Record; + + constructor(parsedError: ErrorDescription) { + const argsMap: Record = {}; + + parsedError.args.forEach((arg, index) => { + if (parsedError.fragment.inputs[index]) { + const inputName = parsedError.fragment.inputs[index].name; + argsMap[inputName] = arg.toString(); + } + }); + const formattedArgs = Object.entries(argsMap) + .map(([key, value]) => `${key}: ${value}`) + .join(', '); + super(`${formattedArgs}`); + this.name = parsedError.name; + this.args = argsMap; + } +} diff --git a/ethos/packages/blockchain-manager/tsconfig.json b/ethos/packages/blockchain-manager/tsconfig.json new file mode 100644 index 0000000..8bc5b63 --- /dev/null +++ b/ethos/packages/blockchain-manager/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": [ + "ES2023", + "DOM" + ], + "outDir": "dist", + } +} diff --git a/ethos/packages/common-ui/package.json b/ethos/packages/common-ui/package.json new file mode 100644 index 0000000..2548e30 --- /dev/null +++ b/ethos/packages/common-ui/package.json @@ -0,0 +1,27 @@ +{ + "name": "@ethos/common-ui", + "version": "1.0.0", + "description": "A library package of common web components and utilities", + "main": "dist/index.js", + "type": "module", + "author": "Ethos Network Inc.", + "license": "UNLICENSED", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + }, + "peerDependencies": { + "@ant-design/icons": "^5.5.2", + "antd": "^5.23.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@ant-design/icons": "^5.5.2", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "antd": "^5.23.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + } +} diff --git a/ethos/packages/common-ui/src/hooks/use-is-mobile.tsx b/ethos/packages/common-ui/src/hooks/use-is-mobile.tsx new file mode 100644 index 0000000..a43212c --- /dev/null +++ b/ethos/packages/common-ui/src/hooks/use-is-mobile.tsx @@ -0,0 +1,26 @@ +'use client'; +import { useEffect, useState } from 'react'; + +export function useIsMobile(maxWidth: number = 767): boolean { + const query = `(max-width: ${maxWidth}px)`; + + const [isMobile, setIsMobile] = useState( + typeof window !== 'undefined' ? window.matchMedia(query).matches : false, + ); + + useEffect(() => { + function handleMediaQueryChange(event: MediaQueryListEvent): void { + setIsMobile(event.matches); + } + + const mediaQuery = window.matchMedia(query); + + mediaQuery.addEventListener('change', handleMediaQueryChange); + + return () => { + mediaQuery.removeEventListener('change', handleMediaQueryChange); + }; + }, [query]); + + return isMobile; +} diff --git a/ethos/packages/common-ui/src/index.tsx b/ethos/packages/common-ui/src/index.tsx new file mode 100644 index 0000000..fa330de --- /dev/null +++ b/ethos/packages/common-ui/src/index.tsx @@ -0,0 +1,15 @@ +export { + getDarkTheme, + getLightTheme, + themes, + type EthosTheme, + baseDarkTheme, + baseLightTheme, + tokenCssVarsBase, + type TokenCssVarsBaseKey, +} from './theme/themes.js'; +export { Logo, LogoFull } from './logo/index.js'; +export { LogoInvertedSvg, LogoSvg, LogoFullSvg } from './logo/logos.js'; +export { useDebouncedValue } from './utils/debounce.js'; +export { useCopyToClipboard } from './utils/clipboard.js'; +export { useIsMobile } from './hooks/use-is-mobile.js'; diff --git a/ethos/packages/common-ui/src/logo/index.tsx b/ethos/packages/common-ui/src/logo/index.tsx new file mode 100644 index 0000000..57a553e --- /dev/null +++ b/ethos/packages/common-ui/src/logo/index.tsx @@ -0,0 +1,16 @@ +'use client'; +import Icon from '@ant-design/icons'; +import { type CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon'; +import { LogoSvg, LogoFullSvg, LogoInvertedSvg } from './logos.js'; + +export function Logo(props: Partial) { + return ; +} + +export function LogoFull(props: Partial) { + return ; +} + +export function LogoInverted(props: Partial) { + return ; +} diff --git a/ethos/packages/common-ui/src/logo/logos.tsx b/ethos/packages/common-ui/src/logo/logos.tsx new file mode 100644 index 0000000..4fe947e --- /dev/null +++ b/ethos/packages/common-ui/src/logo/logos.tsx @@ -0,0 +1,55 @@ +export function LogoSvg() { + return ( + + + + ); +} + +export function LogoFullSvg() { + return ( + + + + + + ); +} + +export function LogoInvertedSvg({ fill = '#CBCBC2' }: { fill?: string }) { + return ( + + + + + + ); +} diff --git a/ethos/packages/common-ui/src/theme/themes.ts b/ethos/packages/common-ui/src/theme/themes.ts new file mode 100644 index 0000000..be641b8 --- /dev/null +++ b/ethos/packages/common-ui/src/theme/themes.ts @@ -0,0 +1,483 @@ +import { theme, type ThemeConfig } from 'antd'; + +export const themes = ['light', 'dark'] as const; + +export type EthosTheme = (typeof themes)[number]; + +export const baseDarkTheme = { + cssVar: true, + hashed: false, + algorithm: typeof window === 'undefined' ? [] : [theme.defaultAlgorithm], + token: { + colorPrimary: '#2E7BC3', + colorPrimaryBgHover: '#1F21B61A', + colorPrimaryBg: '#1F21B61A', + colorInfo: '#1b69b1', + colorTextBase: '#EFEEE0', + colorText: '#EFEEE0D9', + colorTextSecondary: '#FFFFFFA6', + colorTextTertiary: '#FFFFFF73', + colorBgBase: '#232320', + colorBgLayout: '#232320', + colorBorder: '#9E9C8D00', + colorBorderSecondary: '#B3B2A500', + colorWarningBg: '#222830', + colorWarningBorder: '#FFE58F00', + colorWarningTextActive: '#D48806', + colorBgElevated: '#333330', + colorErrorBg: '#B72B3826', + colorErrorBgHover: '#422D2B', + colorSuccessBgHover: '#29392A', + colorErrorBorder: '#FFCCC700', + colorPrimaryBorder: '#8F97DB00', + colorSuccessBorder: '#B7EB8F00', + colorBorderBg: '#1F212640', + colorError: '#b72b38', + colorWarning: '#C29010', + wireframe: false, + colorSuccess: '#127f31', + colorSuccessBg: '#29392A', + colorInfoBg: '#222830', + colorInfoBorder: '#162e4300', + colorLink: '#8D8D85', + colorLinkHover: '#8D8D85BF', + colorBgContainer: '#2d2d2A', + colorBgMask: '#23232040', + lineHeight: 1.25, + padding: 18, + paddingXL: 40, + borderRadiusLG: 8, + }, + components: { + Button: { + defaultActiveColor: '#F9F9F9', + defaultHoverColor: '#F1F1F1', + groupBorderColor: '#4096FF00', + primaryShadow: 'none', + defaultShadow: 'none', + dangerShadow: 'none', + defaultBorderColor: '#D9D9D900', + colorTextLightSolid: '#232320', + dangerColor: '#ffffff', + }, + Avatar: { + containerSizeSM: 20, + containerSize: 40, + containerSizeLG: 64, + }, + Anchor: { + fontSize: 12, + fontSizeLG: 14, + }, + Dropdown: { + paddingBlock: 11, + fontSizeIcon: 14, + }, + Layout: { + headerBg: '#232320', + lightSiderBg: '#2D2D29', + lightTriggerBg: '#2D2D29', + lightTriggerColor: '#2D2D29', + bodyBg: '#232320', + footerBg: '#2D2D29', + colorText: '#1F2126', + headerColor: '#C1C0B6', + }, + Mentions: { + colorBgContainer: '#232320', + colorErrorBg: '#232320', + colorWarningBg: '#232320', + }, + Menu: { + itemBg: '#232320', + darkItemBg: '#C1C0B6', + itemHoverColor: '#2E7BC3BF', + horizontalItemSelectedColor: '#2E7BC3', + activeBarHeight: 0, + motionDurationMid: '0s', + motionDurationSlow: '0s', + }, + Modal: { + colorBgMask: '#00000073', + }, + Typography: { + titleMarginBottom: 0, + titleMarginTop: 0, + fontSize: 12, + fontSizeLG: 14, + colorTextDescription: '#FFFFFFA6', + lineHeightHeading5: 1, + }, + Select: { + optionSelectedBg: '#12121212', + colorBgContainer: '#2d2d2A', + }, + Statistic: { + padding: 24, + }, + Alert: { + colorErrorBg: '#383832', + colorWarningBg: '#383832', + colorSuccessBg: '#383832', + colorInfoBg: '#383832', + fontSize: 12, + }, + Table: { + bodySortBg: `#1F21260A`, + headerSortActiveBg: '#1F212626', + headerSplitColor: '#232320A0', + headerBg: '#23232030', + colorLink: '#1B69B1', + colorLinkHover: '#2373BE', + colorLinkActive: '#2E89DE', + colorPrimary: '#1B69B1', + borderColor: '#232320A0', + cellPaddingBlock: 16, + }, + Input: { + colorBgContainer: '#2D2D29', + colorTextPlaceholder: '#C1C0B6A6', + }, + InputNumber: { + colorBgContainer: '#232320', + }, + DatePicker: { + colorBgContainer: '#232320', + }, + Cascader: { + colorBgContainer: '#232320', + }, + Pagination: { + colorBgContainer: '#232320', + }, + Tooltip: { + colorTextLightSolid: '#FFFFFF', + colorBgSpotlight: '#424242', + fontSize: 12, + lineHeight: 1.75, + }, + Notification: { + colorBgElevated: '#333330', + }, + Checkbox: { + colorBgContainer: '#333330', + }, + Card: { + paddingLG: 18, + }, + }, +} as const satisfies ThemeConfig; + +export const baseLightTheme = { + cssVar: true, + hashed: false, + algorithm: typeof window === 'undefined' ? [] : [theme.defaultAlgorithm], + token: { + colorPrimary: '#1F21B6', + colorPrimaryBgHover: '#1F21B614', + colorInfo: '#1f21b6', + colorTextBase: '#1F2126', + colorText: '#1F2126E0', + colorTextSecondary: '#1F2126A6', + colorTextTertiary: '#1F212673', + colorLink: '#343539', + colorLinkHover: '#343539BF', + colorBgBase: '#C1C0B6', + colorBgContainer: '#CBCBC2', + colorBorder: '#9E9C8D00', + colorBorderSecondary: '#B3B2A500', + colorPrimaryBg: '#1F21B61A', + colorBgLayout: '#C1C0B6', + colorWarningBg: '#D5D4CD', + colorWarningBorder: '#FFE58F00', + colorWarningTextActive: '#D48806', + colorBgElevated: '#D5D4CD', + colorErrorBg: '#CF132226', + colorErrorBorder: '#FFCCC700', + colorPrimaryBorder: '#8F97DB00', + colorSuccessBg: '#AFC0AC', + colorSuccessBorder: '#B7EB8F00', + colorBorderBg: '#1F212640', + colorSuccessBgHover: '#AFC0AC', + colorErrorBgHover: '#CCAFAA', + colorSuccess: '#127f31', + colorError: '#b72b38', + colorWarning: '#cc9a1a', + colorBgMask: '#23232040', + wireframe: false, + lineHeight: 1.25, + padding: 18, + paddingXL: 40, + borderRadiusLG: 8, + }, + components: { + Button: { + defaultBg: '#C1C0B6', + defaultBorderColor: '#D9D9D900', + defaultActiveBorderColor: '#0958D900', + defaultColor: '#010101', + defaultActiveColor: '#494949', + defaultHoverColor: '#414141', + defaultHoverBorderColor: '#414141', + groupBorderColor: '#4096FF00', + primaryShadow: 'none', + defaultShadow: 'none', + dangerShadow: 'none', + colorTextLightSolid: '#C1C0B6', + dangerColor: '#ffffff', + }, + Avatar: { + containerSizeSM: 20, + containerSize: 40, + containerSizeLG: 64, + }, + Anchor: { + fontSize: 12, + fontSizeLG: 14, + }, + Dropdown: { + paddingBlock: 11, + fontSizeIcon: 14, + }, + Layout: { + headerBg: '#C1C0B6', + lightSiderBg: '#C1C0B6', + lightTriggerBg: '#C1C0B6', + lightTriggerColor: '#C1C0B6', + footerBg: '#C1C0B6', + colorText: '#1F2126', + headerColor: '#C1C0B6', + algorithm: true, + bodyBg: '#C1C0B6', + }, + Mentions: { + colorBgContainer: '#C1C0B6', + }, + Modal: { + colorBgMask: '#00000073', + }, + InputNumber: { + colorBgContainer: '#C1C0B6', + }, + Input: { + colorBgContainer: '#cbcbc2', // lower case fixed the issue for search 🤷‍♂️, maybe value case change triggered update?? + colorTextPlaceholder: '#1F212673', + }, + Select: { + colorBgContainer: '#cbcbc2', + }, + Statistic: { + colorTextDescription: '#1F2126A6', + colorTextHeading: '#1F2126E0', + padding: 24, + }, + Table: { + headerSplitColor: '#1F21260F', + headerBg: '#1F212605', + borderColor: '#1F21260F', + cellPaddingBlock: 16, + }, + Menu: { + itemBg: '#C1C0B6', + itemHoverColor: '#1F21B6BF', + itemSelectedColor: '#1F21B6', + activeBarHeight: 0, + motionDurationMid: '0s', + motionDurationSlow: '0s', + }, + Typography: { + titleMarginBottom: 0, + titleMarginTop: 0, + fontSize: 12, + fontSizeLG: 14, + colorTextDescription: '#1F2126A6', + lineHeightHeading5: 1, + }, + Alert: { + colorInfoBg: '#D5D4CD', + colorInfoBorder: '#8F97DB00', + fontSize: 12, + }, + Pagination: { + colorBgContainer: '#C1C0B6', + }, + DatePicker: { + colorBgContainer: '#C1C0B6', + }, + Tooltip: { + colorBgSpotlight: '#000000', + fontSize: 12, + lineHeight: 1.75, + colorTextLightSolid: '#C1C0B6', + }, + Notification: { + colorBgElevated: '#D5D4CD', + }, + Checkbox: { + colorBgContainer: '#C1C0B6', + }, + Card: { + paddingLG: 18, + }, + }, +} as const satisfies ThemeConfig; + +type EthosDarkTheme = Omit & { + token: Omit<(typeof baseDarkTheme)['token'], 'fontFamily'> & { + fontFamily: string; + }; +}; + +type EthosLightTheme = Omit & { + token: Omit<(typeof baseLightTheme)['token'], 'fontFamily'> & { + fontFamily: string; + }; +}; + +// Each application has its own fontFamily, so use this for the theme in the Ant context, +// but reference the base themes when directly referencing the theme object. +export function getDarkTheme(fontFamily: string): EthosDarkTheme { + return { + ...baseDarkTheme, + token: { + ...baseDarkTheme.token, + fontFamily, + }, + }; +} + +// Each application has its own fontFamily, so use this for the theme in the Ant context, +// but reference the base themes when directly referencing the theme object. +export function getLightTheme(fontFamily: string): EthosLightTheme { + return { + ...baseLightTheme, + token: { + ...baseLightTheme.token, + fontFamily, + }, + }; +} + +export const tokenCssVarsBase = { + // TEXT + buttonDangerColor: 'var(--ant-button-danger-color)', + colorTextBase: 'var(--ant-color-text-base)', + colorText: 'var(--ant-color-text)', + colorTextLightSolid: 'var(--ant-color-text-light-solid)', + colorIconHover: 'var(--ant-color-icon-hover)', + colorPrimary: 'var(--ant-color-primary)', + colorPrimaryHover: 'var(--ant-color-primary-hover)', + colorPrimaryBgHover: 'var(--ant-color-primary-bg-hover)', + colorPrimaryText: 'var(--ant-color-primary-text)', + colorPrimaryTextActive: 'var(--ant-color-primary-text-active)', + colorTextSecondary: 'var(--ant-color-text-secondary)', + colorFillSecondary: 'var(--ant-color-fill-secondary)', + colorTextDescription: 'var(--ant-color-text-description)', + colorLink: 'var(--ant-color-link)', + colorLinkHover: 'var(--ant-color-link-hover)', + colorInfo: 'var(--ant-color-info)', + colorFill: 'var(--ant-color-fill)', + colorTextTertiary: 'var(--ant-color-text-tertiary)', + colorTextQuaternary: 'var(--ant-color-text-quaternary)', + colorSuccess: 'var(--ant-color-success)', + colorSuccessHover: 'var(--ant-color-success-hover)', + colorSuccessTextHover: 'var(--ant-color-success-text-hover)', + colorSuccessBorderHover: 'var(--ant-color-success-border-hover)', + colorSuccessBorder: 'var(--ant-color-success-border)', + colorSuccessBg: 'var(--ant-color-success-bg)', + colorSuccessBgHover: 'var(--ant-color-success-bg-hover)', + colorSuccessActive: 'var(--ant-color-success-active)', + colorError: 'var(--ant-color-error)', + colorErrorActive: 'var(--ant-color-error-active)', + colorErrorHover: 'var(--ant-color-error-hover)', + colorErrorBgHover: 'var(--ant-color-error-bg-hover)', + colorErrorTextActive: 'var(--ant-color-error-text-active)', + colorWarning: 'var(--ant-color-warning)', + colorWarningTextActive: 'var(--ant-color-warning-text-active)', + colorTextDisabled: 'var(--ant-color-text-disabled)', + colorWhite: 'var(--ant-color-white)', + colorFillAlter: 'var(--ant-color-fill-alter)', + colorBorder: 'var(--ant-color-border)', + colorBorderSecondary: 'var(--ant-color-border-secondary)', + colorBorderBg: 'var(--ant-color-border-bg)', + + cyan7: 'var(--ant-cyan-7)', + magenta7: 'var(--ant-magenta-7)', + orange6: 'var(--ant-orange-6)', + orange7: 'var(--ant-orange-7)', + orange8: 'var(--ant-orange-8)', + + // BG + colorBgBase: 'var(--ant-color-bg-base)', + colorBgLayout: 'var(--ant-color-bg-layout)', + colorBgElevated: 'var(--ant-color-bg-elevated)', + colorBgContainer: 'var(--ant-color-bg-container)', + colorBgMask: 'var(--ant-color-bg-mask)', + colorBgSpotlight: 'var(--ant-color-bg-spotlight)', + colorLayoutHeaderBg: 'var(--ant-layout-header-bg)', + colorBgSolid: 'var(--ant-color-bg-solid)', + colorBgSolidActive: 'var(--ant-color-bg-solid-active)', + colorBgSolidHover: 'var(--ant-color-bg-solid-hover)', + colorBgTextActive: 'var(--ant-color-bg-text-active)', + + // Box-shadow + boxShadow: 'var(--ant-box-shadow)', + boxShadowSecondary: 'var(--ant-box-shadow-secondary)', + boxShadowTertiary: 'var(--ant-box-shadow-tertiary)', + boxShadowCard: 'var(--ant-box-shadow-card)', + + // Awards + colorAwardGold: '#C78007', + colorAwardSilver: '#646464', + colorAwardBronze: '#BE5521', + + // Font-size + fontSize: 'var(--ant-font-size)', + fontSizeSM: 'var(--ant-font-size-sm)', + fontSizeLG: 'var(--ant-font-size-lg)', + fontSizeXL: 'var(--ant-font-size-xl)', + fontSizeHeading1: 'var(--ant-font-size-heading-1)', + fontSizeHeading2: 'var(--ant-font-size-heading-2)', + fontSizeHeading3: 'var(--ant-font-size-heading-3)', + fontSizeHeading4: 'var(--ant-font-size-heading-4)', + fontSizeHeading5: 'var(--ant-font-size-heading-5)', + + // Line-height + lineHeight: 'var(--ant-line-height)', + lineHeightSM: 'var(--ant-line-height-sm)', + lineHeightLG: 'var(--ant-line-height-lg)', + lineHeightHeading1: 'var(--ant-line-height-heading-1)', + lineHeightHeading2: 'var(--ant-line-height-heading-2)', + lineHeightHeading3: 'var(--ant-line-height-heading-3)', + lineHeightHeading4: 'var(--ant-line-height-heading-4)', + lineHeightHeading5: 'var(--ant-line-height-heading-5)', + + // Border-radius + borderRadiusXS: 'var(--ant-border-radius-xs)', + borderRadiusSM: 'var(--ant-border-radius-sm)', + borderRadius: 'var(--ant-border-radius)', + borderRadiusLG: 'var(--ant-border-radius-lg)', + + fullHeight: 'var(--full-height, 100vh)', + fullWidth: 'var(--full-width, 100vw)', + + // Spacing + marginXXS: 'var(--ant-margin-xxs)', + marginXS: 'var(--ant-margin-xs)', + marginSM: 'var(--ant-margin-sm)', + margin: 'var(--ant-margin)', + marginMD: 'var(--ant-margin-md)', + marginLG: 'var(--ant-margin-lg)', + marginXL: 'var(--ant-margin-xl)', + marginXXL: 'var(--ant-margin-xxl)', + + paddingXXS: 'var(--ant-padding-xxs)', + paddingXS: 'var(--ant-padding-xs)', + paddingSM: 'var(--ant-padding-sm)', + padding: 'var(--ant-padding)', + paddingMD: 'var(--ant-padding-md)', + paddingLG: 'var(--ant-padding-lg)', + paddingXL: 'var(--ant-padding-xl)', +} as const; + +export type TokenCssVarsBaseKey = keyof typeof tokenCssVarsBase; diff --git a/ethos/packages/common-ui/src/utils/clipboard.ts b/ethos/packages/common-ui/src/utils/clipboard.ts new file mode 100644 index 0000000..f4f4f62 --- /dev/null +++ b/ethos/packages/common-ui/src/utils/clipboard.ts @@ -0,0 +1,29 @@ +import { App } from 'antd'; +import { type NotificationInstance } from 'antd/es/notification/interface'; + +export function useCopyToClipboard() { + const { notification } = App.useApp(); + + return async (text: string, successMessage?: string) => { + await copyToClipboard(notification, text, successMessage); + }; +} + +async function copyToClipboard( + notification: NotificationInstance, + text: string, + successMessage?: string, +) { + try { + await navigator.clipboard.writeText(text); + + if (successMessage) { + notification.success({ + message: successMessage, + key: 'copy-link-success', + }); + } + } catch (err) { + console.error(err); + } +} diff --git a/ethos/packages/common-ui/src/utils/debounce.ts b/ethos/packages/common-ui/src/utils/debounce.ts new file mode 100644 index 0000000..eeed45d --- /dev/null +++ b/ethos/packages/common-ui/src/utils/debounce.ts @@ -0,0 +1,32 @@ +'use client'; +import { useEffect, useRef, useState } from 'react'; + +export function useDebouncedValue(value: T, delay: number, immediate = false) { + const [debouncedValue, setDebouncedValue] = useState(value); + const timeoutRef = useRef(null); + const firstUpdate = useRef(true); // To track the first call + + useEffect(() => { + if (value === debouncedValue) return; + + if (immediate && firstUpdate.current) { + // If it's the first call and immediate is true, set the value immediately + setDebouncedValue(value); + firstUpdate.current = false; // Mark the first update as completed + } else { + // Set up the debounce for subsequent updates + timeoutRef.current = setTimeout(() => { + setDebouncedValue(value); + }, delay); + } + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value, delay, immediate]); + + return debouncedValue; +} diff --git a/ethos/packages/common-ui/tsconfig.json b/ethos/packages/common-ui/tsconfig.json new file mode 100644 index 0000000..f480fa5 --- /dev/null +++ b/ethos/packages/common-ui/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": [ + "DOM", + "DOM.Iterable", + "ES2022" + ], + "outDir": "dist", + "jsx": "react-jsx", + "module": "esnext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true, + "allowJs": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/ethos/packages/contracts/contracts/EthosVouch.sol b/ethos/packages/contracts/contracts/EthosVouch.sol index 6fc8bdf..dfe3f22 100644 --- a/ethos/packages/contracts/contracts/EthosVouch.sol +++ b/ethos/packages/contracts/contracts/EthosVouch.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; - import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ITargetStatus } from "./interfaces/ITargetStatus.sol"; import { IEthosProfile } from "./interfaces/IEthosProfile.sol"; @@ -181,7 +180,23 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy * @notice Maps profile IDs to their rewards balance in ETH * @dev Balances are Eth only; no ERC20 support. Maps Ethos profile IDs to their rewards balances. */ - mapping(uint256 => uint256) public rewards; + mapping(uint256 => uint256) public rewardsByProfileId; + /** + * @notice Maps attestation hashes to their rewards balance + * Only used when vouching for mock profiles + */ + mapping(bytes32 => uint256) public rewardsByAttestationHash; + /** + * @notice Maps address to their rewards balance + * Only used when vouching for mock profiles + */ + mapping(address => uint256) public rewardsByAddress; + + /* + * @notice Maps author profile IDs to their frozen status, preventing withdrawals before slashing + * @dev authorProfileId => isFrozen + */ + mapping(uint256 => bool) public frozenAuthors; // Add storage gap as the last storage variable // This allows us to add new storage variables in future upgrades @@ -195,7 +210,6 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy error VouchNotFound(uint256 vouchId); error NotAuthorForVouch(uint256 vouchId, uint256 user); error WrongSubjectProfileIdForVouch(uint256 vouchId, uint256 subjectProfileId); - error WithdrawalFailed(bytes data, string message); error CannotMarkVouchAsUnhealthy(uint256 vouchId); error AlreadyUnvouched(uint256 vouchId); error InvalidFeeMultiplier(uint256 newFee); @@ -209,12 +223,16 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy error InvalidSlashPercentage(); error InvalidFeeProtocolAddress(); error NotSlasher(); + error InvalidAttestationHash(bytes32 attestationHash); + error PendingSlash(uint256 vouchId, uint256 authorProfileId); // --- Events --- event Vouched( uint256 indexed vouchId, uint256 indexed authorProfileId, uint256 indexed subjectProfileId, + address subjectAddress, + bytes32 attestationHash, uint256 amountStaked, uint256 amountDeposited ); @@ -223,6 +241,8 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy uint256 indexed vouchId, uint256 indexed authorProfileId, uint256 indexed subjectProfileId, + address subjectAddress, + bytes32 attestationHash, uint256 amountStaked, uint256 amountDeposited ); @@ -247,6 +267,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy event DepositedToRewards(uint256 indexed recipientProfileId, uint256 amount); event WithdrawnFromRewards(uint256 indexed accountProfileId, uint256 amount); event Slashed(uint256 indexed authorProfileId, uint256 slashBasisPoints, uint256 totalSlashed); + event Frozen(uint256 indexed authorProfileId, bool isFrozen); /** * @notice Modifier to restrict access to slasher contract only @@ -310,40 +331,101 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy // --- Vouch Functions --- /** - * @dev Vouches for address. - * @param subjectAddress Vouchee address. - * @param comment Comment. - * @param metadata Metadata. + * @notice Vouches by address, allowing vouches for addresses that have not yet joined Ethos (mock profiles) + * @param subjectAddress The address to vouch for + * @param comment Optional text comment about the vouch + * @param metadata Additional structured metadata about the vouch */ function vouchByAddress( address subjectAddress, string calldata comment, string calldata metadata - ) public payable onlyNonZeroAddress(subjectAddress) whenNotPaused { - IEthosProfile profile = IEthosProfile( - contractAddressManager.getContractAddressForName(ETHOS_PROFILE) + ) public payable onlyNonZeroAddress(subjectAddress) whenNotPaused nonReentrant { + (bool verified, bool archived, bool mock, uint256 subjectProfileId) = _ethosProfile() + .profileStatusByAddress(subjectAddress); + + // you may not vouch for archived profiles + if (archived) revert InvalidEthosProfileForVouch(subjectProfileId); + // if verified, use the standard vouchByProfileId + if (verified) { + _vouchByProfileIdCommon(subjectProfileId, comment, metadata); + return; + } + // use vouchByAddress or vouchByAttestationHash for mock profiles + if (!mock) revert InvalidEthosProfileForVouch(subjectProfileId); + + _vouchCommon(subjectProfileId, comment, metadata, bytes32(0), subjectAddress); + } + + /** + * @notice Vouches by attestation hash, allowing vouches for social accounts that have not yet joined Ethos (mock profiles) + * @param attestationHash The attestation hash identifying the subject to vouch for + * @param comment Optional text comment about the vouch + * @param metadata Additional structured metadata about the vouch + */ + function vouchByAttestation( + bytes32 attestationHash, + string calldata comment, + string calldata metadata + ) public payable whenNotPaused nonReentrant { + uint256 attestationProfileId = _ethosProfile().profileIdByAttestation(attestationHash); + // submitted hash must match the mock profile attestation hash + (bool verified, bool archived, bool mock) = _ethosProfile().profileStatusById( + attestationProfileId ); - profile.verifiedProfileIdForAddress(msg.sender); - uint256 profileId = profile.profileIdByAddress(subjectAddress); + // you may not vouch for archived profiles + if (archived) revert InvalidEthosProfileForVouch(attestationProfileId); + // this function is only for mock profiles + if (verified) { + _vouchByProfileIdCommon(attestationProfileId, comment, metadata); + return; + } + // use vouchByAddress or vouchByAttestationHash for verified profiles + if (!mock) revert InvalidEthosProfileForVouch(attestationProfileId); - vouchByProfileId(profileId, comment, metadata); + _vouchCommon(attestationProfileId, comment, metadata, attestationHash, address(0)); } /** - * @dev Vouches for profile Id. - * @param subjectProfileId Subject profile Id. - * @param comment Comment. - * @param metadata Metadata. + * @notice Vouches by profile ID, allowing vouches for existing Ethos profiles + * @param subjectProfileId The profile ID to vouch for + * @param comment Optional text comment about the vouch + * @param metadata Additional structured metadata about the vouch */ function vouchByProfileId( uint256 subjectProfileId, string calldata comment, string calldata metadata - ) public payable whenNotPaused nonReentrant { - uint256 authorProfileId = IEthosProfile( - contractAddressManager.getContractAddressForName(ETHOS_PROFILE) - ).verifiedProfileIdForAddress(msg.sender); + ) external payable whenNotPaused nonReentrant { + _vouchByProfileIdCommon(subjectProfileId, comment, metadata); + } + + function _vouchByProfileIdCommon( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata + ) internal { + (bool verified, bool archived, bool mock) = _ethosProfile().profileStatusById(subjectProfileId); + + // this function is only for verified profiles + if (!verified) revert InvalidEthosProfileForVouch(subjectProfileId); + // you may not vouch for archived profiles + if (archived) revert InvalidEthosProfileForVouch(subjectProfileId); + // use vouchByAddress or vouchByAttestationHash for mock profiles + if (mock) revert InvalidEthosProfileForVouch(subjectProfileId); + + _vouchCommon(subjectProfileId, comment, metadata, bytes32(0), address(0)); + } + + function _vouchCommon( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata, + bytes32 attestationHash, + address subjectAddress + ) internal { + uint256 authorProfileId = _ethosProfile().verifiedProfileIdForAddress(msg.sender); // pls no vouch for yourself if (authorProfileId == subjectProfileId) { @@ -359,19 +441,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy } // validate subject profile - if (subjectProfileId == 0) { - revert InvalidEthosProfileForVouch(subjectProfileId); - } - (bool verified, bool archived, bool mock) = IEthosProfile( - contractAddressManager.getContractAddressForName(ETHOS_PROFILE) - ).profileStatusById(subjectProfileId); - - // you may not vouch for archived profiles - // however, you may vouch for verified AND mock profiles - // we allow vouching for mock profiles in case they are later verified - if (archived || (!mock && !verified)) { - revert InvalidEthosProfileForVouch(subjectProfileId); - } + if (subjectProfileId == 0) revert InvalidEthosProfileForVouch(subjectProfileId); // one vouch per profile per author _vouchShouldNotExistFor(authorProfileId, subjectProfileId); @@ -389,7 +459,13 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy revert MinimumVouchAmount(configuredMinimumVouchAmount); } - (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId, authorProfileId); + (uint256 toDeposit, ) = applyEntryFees( + msg.value, + subjectProfileId, + authorProfileId, + attestationHash, + subjectAddress + ); // store vouch details uint256 count = vouchCount; @@ -418,49 +494,95 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy }) }); - emit Vouched(count, authorProfileId, subjectProfileId, msg.value, toDeposit); + emit Vouched( + count, + authorProfileId, + subjectProfileId, + subjectAddress, + attestationHash, + msg.value, + toDeposit + ); vouchCount++; } /** - * @notice Increases the amount staked for an existing vouch - + * @notice Increases the staked amount for an existing vouch * @param vouchId The ID of the vouch to increase - - * @custom:throws {NotAuthorForVouch} If the caller is not the author of the vouch - * @custom:throws {AlreadyUnvouched} If the vouch has already been unvouched - * @custom:emits VouchIncreased + * @param attestationHash Optional mock profile attestation hash (use bytes32(0) if not needed) + * @param subjectAddress Optional mock profile address (use address(0) if not needed) + * @dev For verified profiles, both attestationHash and subjectAddress should be empty + * @dev For mock profiles, either attestationHash OR subjectAddress must match the original vouch */ - function increaseVouch(uint256 vouchId) public payable nonReentrant { + function increaseVouch( + uint256 vouchId, + bytes32 attestationHash, + address subjectAddress + ) public payable nonReentrant { // vouch increases much also meet the minimum vouch amount if (msg.value < configuredMinimumVouchAmount) { revert MinimumVouchAmount(configuredMinimumVouchAmount); } // get the profile id of the author - uint256 authorProfileId = IEthosProfile( - contractAddressManager.getContractAddressForName(ETHOS_PROFILE) - ).verifiedProfileIdForAddress(msg.sender); + uint256 authorProfileId = _ethosProfile().verifiedProfileIdForAddress(msg.sender); _vouchShouldBelongToAuthor(vouchId, authorProfileId); // make sure this vouch is active; not unvouched _vouchShouldBePossibleUnvouch(vouchId); uint256 subjectProfileId = vouches[vouchId].subjectProfileId; - (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId, authorProfileId); + + // If the subject profile is a mock profile, require a subject address or attestation hash + (, , bool mock) = _ethosProfile().profileStatusById(subjectProfileId); + if (mock && (subjectAddress == address(0) && attestationHash == bytes32(0))) + revert InvalidEthosProfileForVouch(subjectProfileId); + + // Validate that either attestationHash or subjectAddress matches the vouch + if (attestationHash != bytes32(0)) { + uint256 attestationProfileId = _ethosProfile().profileIdByAttestation(attestationHash); + if (attestationProfileId != subjectProfileId) + revert InvalidEthosProfileForVouch(attestationProfileId); + } + + if (subjectAddress != address(0)) { + uint256 addressProfileId = _ethosProfile().profileIdByAddress(subjectAddress); + if (addressProfileId != subjectProfileId) + revert InvalidEthosProfileForVouch(addressProfileId); + } + + (uint256 toDeposit, ) = applyEntryFees( + msg.value, + subjectProfileId, + authorProfileId, + attestationHash, + subjectAddress + ); vouches[vouchId].balance += toDeposit; - emit VouchIncreased(vouchId, authorProfileId, subjectProfileId, msg.value, toDeposit); + emit VouchIncreased( + vouchId, + authorProfileId, + subjectProfileId, + subjectAddress, + attestationHash, + msg.value, + toDeposit + ); } // --- Unvouch Functions --- /** - * @dev Unvouches vouch. - * @param vouchId Vouch Id. + * @notice Withdraws a vouch, returning staked funds to the author minus exit fees + * @param vouchId The ID of the vouch to withdraw */ function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { Vouch storage v = vouches[vouchId]; _vouchShouldExist(vouchId); _vouchShouldBePossibleUnvouch(vouchId); + + // Prevent withdrawals if the author's profile is frozen + if (frozenAuthors[v.authorProfileId]) revert PendingSlash(vouchId, v.authorProfileId); + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch // however, we don't care about the status of the address's profile; funds are always attached // to an address, not a profile @@ -475,7 +597,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy _removeVouchFromArrays(v); // apply fees and determine how much is left to send back to the author - (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId, v.authorProfileId); + (uint256 toWithdraw, ) = applyExitFees(v.balance); // set the balance to 0 and save back to storage v.balance = 0; // send the funds to the author @@ -503,9 +625,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy */ function markUnhealthy(uint256 vouchId) public whenNotPaused { Vouch storage v = vouches[vouchId]; - uint256 profileId = IEthosProfile( - contractAddressManager.getContractAddressForName(ETHOS_PROFILE) - ).verifiedProfileIdForAddress(msg.sender); + uint256 profileId = _ethosProfile().verifiedProfileIdForAddress(msg.sender); _vouchShouldExist(vouchId); _vouchShouldBePossibleUnhealthy(vouchId); @@ -519,6 +639,27 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy // --- Slash Functions --- + /** + * @notice Freezes all vouches from a specific author, preventing withdrawals until unfrozen or slashed + * @dev Only callable by the authorized slasher contract + * @param authorProfileId The profile ID whose vouches will be frozen + * This is a protective measure to prevent withdrawal of funds during investigation before the slashing process is complete + */ + function freeze(uint256 authorProfileId) external onlySlasher whenNotPaused nonReentrant { + frozenAuthors[authorProfileId] = true; + emit Frozen(authorProfileId, true); + } + + /** + * @notice Unfreezes an author's vouches, allowing withdrawals again + * @dev Only callable by the authorized slasher contract + * @param authorProfileId The profile ID whose vouches will be unfrozen + */ + function unfreeze(uint256 authorProfileId) external onlySlasher whenNotPaused nonReentrant { + frozenAuthors[authorProfileId] = false; + emit Frozen(authorProfileId, false); + } + /** * @notice Reduces all vouch balances for a given author by a percentage * @param authorProfileId The profile ID whose vouches will be slashed @@ -672,48 +813,85 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy // --- Reward Functions --- + /** + * @notice Claims accumulated rewards for a verified Ethos profile + * @dev Combines rewards from both profile ID and address-based rewards + */ function claimRewards() external whenNotPaused nonReentrant { - (bool verified, , bool mock, uint256 callerProfileId) = IEthosProfile( - contractAddressManager.getContractAddressForName(ETHOS_PROFILE) - ).profileStatusByAddress(msg.sender); + (bool verified, , bool mock, uint256 callerProfileId) = _ethosProfile().profileStatusByAddress( + msg.sender + ); // Only check that this is a real profile (not mock) and was verified at some point - if (!verified || mock) { - revert ProfileNotFoundForAddress(msg.sender); - } + if (!verified || mock) revert ProfileNotFoundForAddress(msg.sender); + + // claim rewards by profile id + uint256 amount = rewardsByProfileId[callerProfileId]; + + // claim rewards by address + uint256 addressRewards = rewardsByAddress[msg.sender]; + amount += addressRewards; - uint256 amount = rewards[callerProfileId]; if (amount == 0) revert InsufficientRewardsBalance(); - rewards[callerProfileId] = 0; + rewardsByProfileId[callerProfileId] = 0; + rewardsByAddress[msg.sender] = 0; (bool success, ) = msg.sender.call{ value: amount }(""); if (!success) revert FeeTransferFailed("Rewards claim failed"); emit WithdrawnFromRewards(callerProfileId, amount); } - function _depositRewards(uint256 amount, uint256 recipientProfileId) internal { - rewards[recipientProfileId] += amount; - emit DepositedToRewards(recipientProfileId, amount); - } - /** - * @notice Distributes rewards to previous vouchers proportionally based on their current balance, - * excluding the current transaction's author - * @param amount The amount to distribute as rewards - * @param subjectProfileId The profile ID whose vouchers will receive rewards - * @param excludeAuthorId The profile ID to exclude from reward distribution + * @notice Claims accumulated rewards for a profile using their attestation hash + * @dev Only for verified profiles that registered the attestation hash + * @param attestationHash The attestation hash used to identify the rewards */ - function _rewardPreviousVouchers( + function claimRewardsByAttestation(bytes32 attestationHash) external whenNotPaused nonReentrant { + (bool verified, , bool mock, uint256 callerProfileId) = _ethosProfile().profileStatusByAddress( + msg.sender + ); + if (!verified) revert ProfileNotFoundForAddress(msg.sender); + if (mock) revert ProfileNotFoundForAddress(msg.sender); + + // ensure this hash belongs to the caller + if (_ethosProfile().profileIdByAttestation(attestationHash) != callerProfileId) + revert InvalidAttestationHash(attestationHash); + + uint256 amount = rewardsByAttestationHash[attestationHash]; + if (amount == 0) revert InsufficientRewardsBalance(); + + rewardsByAttestationHash[attestationHash] = 0; + (bool success, ) = msg.sender.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Rewards claim failed"); + + emit WithdrawnFromRewards(callerProfileId, amount); + } + + function _depositRewards( uint256 amount, + uint256 recipientProfileId, + bytes32 attestationHash, + address subjectAddress + ) internal { + if (attestationHash != bytes32(0)) { + rewardsByAttestationHash[attestationHash] += amount; + } else if (subjectAddress != address(0)) { + rewardsByAddress[subjectAddress] += amount; + } else { + rewardsByProfileId[recipientProfileId] += amount; + } + emit DepositedToRewards(recipientProfileId, amount); + } + + function _previousVouchersBalance( uint256 subjectProfileId, uint256 excludeAuthorId - ) internal returns (uint256 amountDistributed) { + ) internal view returns (uint256 totalBalance) { uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; uint256 totalVouches = vouchIds.length; // Calculate total balance of all active vouches except author's - uint256 totalBalance; for (uint256 i = 0; i < totalVouches; i++) { Vouch storage vouch = vouches[vouchIds[i]]; // Only include active (not archived) vouches from other authors @@ -721,19 +899,33 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy totalBalance += vouch.balance; } } + } - // If there are no eligible vouchers, return 0 - if (totalBalance == 0) { - return 0; - } - + function _rewardPreviousVouchers( + uint256 amountToDistribute, + uint256 previousVouchersBalance, + uint256 subjectProfileId, + uint256 excludeAuthorId, + bytes32 attestationHash, + address subjectAddress + ) internal { + uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; + uint256 totalVouches = vouchIds.length; // Distribute rewards proportionally - uint256 remainingRewards = amount; + uint256 remainingRewards = amountToDistribute; for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { Vouch storage vouch = vouches[vouchIds[i]]; if (!vouch.archived && vouch.authorProfileId != excludeAuthorId) { // Calculate this vouch's share of the rewards - uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); + uint256 reward = amountToDistribute.mulDiv( + vouch.balance, + previousVouchersBalance, + Math.Rounding.Floor + ); + // Avoid rewarding the author more than the amount they vouched + // this prevents vouchers from vouching everyone for dust, just to try to get the total real vouch rewards + reward = Math.min(reward, vouch.balance); + if (reward > 0) { vouch.balance += reward; remainingRewards -= reward; @@ -741,12 +933,10 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy } } - // Send any dust (remaining rewards due to rounding) to the subject reward escrow + // Send any remaining rewards to the subject reward escrow if (remainingRewards > 0) { - _depositRewards(remainingRewards, subjectProfileId); + _depositRewards(remainingRewards, subjectProfileId, attestationHash, subjectAddress); } - - return amount; } // --- View Functions --- @@ -798,9 +988,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy uint256 author, address subjectAddress ) external view returns (Vouch memory) { - address ethosProfile = contractAddressManager.getContractAddressForName(ETHOS_PROFILE); - - uint256 profileId = IEthosProfile(ethosProfile).verifiedProfileIdForAddress(subjectAddress); + uint256 profileId = _ethosProfile().verifiedProfileIdForAddress(subjectAddress); return verifiedVouchByAuthorForSubjectProfileId(author, profileId); } @@ -929,85 +1117,69 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy } } - /** - * @notice Applies and distributes protocol, donation, and vouchers pool fees - * @param amount The total amount to apply fees to - * @param isEntry Whether this is an entry transaction (true) or exit/withdrawal (false) - * @param subjectProfileId The profile ID receiving donation/rewards - * @param authorProfileId The profile ID of the vouch author - * @return toDeposit The amount remaining after fees are deducted - * @return totalFees The total amount of fees collected - */ - function applyFees( + function applyEntryFees( uint256 amount, - bool isEntry, uint256 subjectProfileId, - uint256 authorProfileId + uint256 authorProfileId, + bytes32 attestationHash, + address subjectAddress ) internal returns (uint256 toDeposit, uint256 totalFees) { - if (isEntry) { - // Sum all entry fees to calculate a max fee to be split across each fee type - uint256 totalFeesBasisPoints = entryProtocolFeeBasisPoints + - entryDonationFeeBasisPoints + - entryVouchersPoolFeeBasisPoints; - - if (totalFeesBasisPoints == 0) return (amount, 0); - - // Calculate total fees using combined basis points - // Formula: totalFees = amount - (amount * BASIS_POINT_SCALE / (BASIS_POINT_SCALE + totalBasisPoints)) - totalFees = - amount - - ( - amount.mulDiv( - BASIS_POINT_SCALE, - (BASIS_POINT_SCALE + totalFeesBasisPoints), - Math.Rounding.Floor - ) - ); + uint256 previousVouchersBalance = _previousVouchersBalance(subjectProfileId, authorProfileId); + // Sum all entry fees to calculate a max fee to be split across each fee type + uint256 totalFeesBasisPoints = entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints; + if (previousVouchersBalance > 0) totalFeesBasisPoints += entryVouchersPoolFeeBasisPoints; + if (totalFeesBasisPoints == 0) return (amount, 0); + + // Calculate deposit amount + toDeposit = amount.mulDiv( + BASIS_POINT_SCALE, + (BASIS_POINT_SCALE + totalFeesBasisPoints), + Math.Rounding.Floor + ); - // Distribute total fees proportionally to each fee type based on their relative basis points - // Protocol fee share = (protocolFeeBasisPoints / totalBasisPoints) * totalFees - uint256 protocolFee = totalFees.mulDiv( - entryProtocolFeeBasisPoints, - totalFeesBasisPoints, - Math.Rounding.Floor - ); + // Calculate total fees as the remainder + totalFees = amount - toDeposit; - // Donation fee share = (donationFeeBasisPoints / totalBasisPoints) * totalFees - uint256 donationFee = totalFees.mulDiv( - entryDonationFeeBasisPoints, - totalFeesBasisPoints, - Math.Rounding.Floor + // Distribute total fees proportionally + uint256 protocolFee = totalFees.mulDiv( + entryProtocolFeeBasisPoints, + totalFeesBasisPoints, + Math.Rounding.Floor + ); + uint256 donationFee = totalFees.mulDiv( + entryDonationFeeBasisPoints, + totalFeesBasisPoints, + Math.Rounding.Floor + ); + uint256 vouchersPoolFee = totalFees - protocolFee - donationFee; + + // Distribute each fee portion to its respective destination + if (protocolFee > 0) _depositProtocolFee(protocolFee); + + if (donationFee > 0) + _depositRewards(donationFee, subjectProfileId, attestationHash, subjectAddress); + + if (vouchersPoolFee > 0) + _rewardPreviousVouchers( + vouchersPoolFee, + previousVouchersBalance, + subjectProfileId, + authorProfileId, + attestationHash, + subjectAddress ); - // Assign remaining fees to vouchers pool to handle rounding precision - uint256 vouchersPoolFee = totalFees - protocolFee - donationFee; - - // Distribute each fee portion to its respective destination - if (protocolFee > 0) { - _depositProtocolFee(protocolFee); - } - if (donationFee > 0) { - _depositRewards(donationFee, subjectProfileId); - } - if (vouchersPoolFee > 0) { - vouchersPoolFee = _rewardPreviousVouchers( - vouchersPoolFee, - subjectProfileId, - authorProfileId - ); - } - - toDeposit = amount - totalFees; - } else { - // Exit transactions only have a single protocol fee - totalFees = calcFee(amount, exitFeeBasisPoints); - if (totalFees > 0) { - _depositProtocolFee(totalFees); - } - toDeposit = amount - totalFees; - } + // vouchersPoolFee is dynamic and may be less than initially allocated + // so we need to recalculate totalFees to ensure the correct amount is deposited + totalFees = protocolFee + donationFee + vouchersPoolFee; + toDeposit = amount - totalFees; + } - return (toDeposit, totalFees); + function applyExitFees(uint256 amount) internal returns (uint256 toDeposit, uint256 totalFees) { + // Exit transactions only have a single protocol fee + totalFees = calcFee(amount, exitFeeBasisPoints); + if (totalFees > 0) _depositProtocolFee(totalFees); + toDeposit = amount - totalFees; } /** @@ -1083,4 +1255,8 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy // the author->subject mapping is only for active vouches; remove it delete vouchIdByAuthorForSubjectProfileId[v.authorProfileId][v.subjectProfileId]; } + + function _ethosProfile() internal view returns (IEthosProfile) { + return IEthosProfile(contractAddressManager.getContractAddressForName(ETHOS_PROFILE)); + } } diff --git a/ethos/packages/contracts/contracts/EthosVouch.sol.rej b/ethos/packages/contracts/contracts/EthosVouch.sol.rej new file mode 100644 index 0000000..e2ee644 --- /dev/null +++ b/ethos/packages/contracts/contracts/EthosVouch.sol.rej @@ -0,0 +1,714 @@ +diff a/ethos/packages/contracts/contracts/EthosVouch.sol b/ethos/packages/contracts/contracts/EthosVouch.sol (rejected hunks) +@@ -1,6 +1,5 @@ + // SPDX-License-Identifier: MIT + pragma solidity 0.8.26; +- + import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + import { ITargetStatus } from "./interfaces/ITargetStatus.sol"; + import { IEthosProfile } from "./interfaces/IEthosProfile.sol"; +@@ -181,7 +180,23 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + * @notice Maps profile IDs to their rewards balance in ETH + * @dev Balances are Eth only; no ERC20 support. Maps Ethos profile IDs to their rewards balances. + */ +- mapping(uint256 => uint256) public rewards; ++ mapping(uint256 => uint256) public rewardsByProfileId; ++ /** ++ * @notice Maps attestation hashes to their rewards balance ++ * Only used when vouching for mock profiles ++ */ ++ mapping(bytes32 => uint256) public rewardsByAttestationHash; ++ /** ++ * @notice Maps address to their rewards balance ++ * Only used when vouching for mock profiles ++ */ ++ mapping(address => uint256) public rewardsByAddress; ++ ++ /* ++ * @notice Maps author profile IDs to their frozen status, preventing withdrawals before slashing ++ * @dev authorProfileId => isFrozen ++ */ ++ mapping(uint256 => bool) public frozenAuthors; + + // Add storage gap as the last storage variable + // This allows us to add new storage variables in future upgrades +@@ -195,7 +210,6 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + error VouchNotFound(uint256 vouchId); + error NotAuthorForVouch(uint256 vouchId, uint256 user); + error WrongSubjectProfileIdForVouch(uint256 vouchId, uint256 subjectProfileId); +- error WithdrawalFailed(bytes data, string message); + error CannotMarkVouchAsUnhealthy(uint256 vouchId); + error AlreadyUnvouched(uint256 vouchId); + error InvalidFeeMultiplier(uint256 newFee); +@@ -209,12 +223,16 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + error InvalidSlashPercentage(); + error InvalidFeeProtocolAddress(); + error NotSlasher(); ++ error InvalidAttestationHash(bytes32 attestationHash); ++ error PendingSlash(uint256 vouchId, uint256 authorProfileId); + + // --- Events --- + event Vouched( + uint256 indexed vouchId, + uint256 indexed authorProfileId, + uint256 indexed subjectProfileId, ++ address subjectAddress, ++ bytes32 attestationHash, + uint256 amountStaked, + uint256 amountDeposited + ); +@@ -223,6 +241,8 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + uint256 indexed vouchId, + uint256 indexed authorProfileId, + uint256 indexed subjectProfileId, ++ address subjectAddress, ++ bytes32 attestationHash, + uint256 amountStaked, + uint256 amountDeposited + ); +@@ -247,6 +267,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + event DepositedToRewards(uint256 indexed recipientProfileId, uint256 amount); + event WithdrawnFromRewards(uint256 indexed accountProfileId, uint256 amount); + event Slashed(uint256 indexed authorProfileId, uint256 slashBasisPoints, uint256 totalSlashed); ++ event Frozen(uint256 indexed authorProfileId, bool isFrozen); + + /** + * @notice Modifier to restrict access to slasher contract only +@@ -310,40 +331,101 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + // --- Vouch Functions --- + + /** +- * @dev Vouches for address. +- * @param subjectAddress Vouchee address. +- * @param comment Comment. +- * @param metadata Metadata. ++ * @notice Vouches by address, allowing vouches for addresses that have not yet joined Ethos (mock profiles) ++ * @param subjectAddress The address to vouch for ++ * @param comment Optional text comment about the vouch ++ * @param metadata Additional structured metadata about the vouch + */ + function vouchByAddress( + address subjectAddress, + string calldata comment, + string calldata metadata +- ) public payable onlyNonZeroAddress(subjectAddress) whenNotPaused { +- IEthosProfile profile = IEthosProfile( +- contractAddressManager.getContractAddressForName(ETHOS_PROFILE) ++ ) public payable onlyNonZeroAddress(subjectAddress) whenNotPaused nonReentrant { ++ (bool verified, bool archived, bool mock, uint256 subjectProfileId) = _ethosProfile() ++ .profileStatusByAddress(subjectAddress); ++ ++ // you may not vouch for archived profiles ++ if (archived) revert InvalidEthosProfileForVouch(subjectProfileId); ++ // if verified, use the standard vouchByProfileId ++ if (verified) { ++ _vouchByProfileIdCommon(subjectProfileId, comment, metadata); ++ return; ++ } ++ // use vouchByAddress or vouchByAttestationHash for mock profiles ++ if (!mock) revert InvalidEthosProfileForVouch(subjectProfileId); ++ ++ _vouchCommon(subjectProfileId, comment, metadata, bytes32(0), subjectAddress); ++ } ++ ++ /** ++ * @notice Vouches by attestation hash, allowing vouches for social accounts that have not yet joined Ethos (mock profiles) ++ * @param attestationHash The attestation hash identifying the subject to vouch for ++ * @param comment Optional text comment about the vouch ++ * @param metadata Additional structured metadata about the vouch ++ */ ++ function vouchByAttestation( ++ bytes32 attestationHash, ++ string calldata comment, ++ string calldata metadata ++ ) public payable whenNotPaused nonReentrant { ++ uint256 attestationProfileId = _ethosProfile().profileIdByAttestation(attestationHash); ++ // submitted hash must match the mock profile attestation hash ++ (bool verified, bool archived, bool mock) = _ethosProfile().profileStatusById( ++ attestationProfileId + ); +- profile.verifiedProfileIdForAddress(msg.sender); + +- uint256 profileId = profile.profileIdByAddress(subjectAddress); ++ // you may not vouch for archived profiles ++ if (archived) revert InvalidEthosProfileForVouch(attestationProfileId); ++ // this function is only for mock profiles ++ if (verified) { ++ _vouchByProfileIdCommon(attestationProfileId, comment, metadata); ++ return; ++ } ++ // use vouchByAddress or vouchByAttestationHash for verified profiles ++ if (!mock) revert InvalidEthosProfileForVouch(attestationProfileId); + +- vouchByProfileId(profileId, comment, metadata); ++ _vouchCommon(attestationProfileId, comment, metadata, attestationHash, address(0)); + } + + /** +- * @dev Vouches for profile Id. +- * @param subjectProfileId Subject profile Id. +- * @param comment Comment. +- * @param metadata Metadata. ++ * @notice Vouches by profile ID, allowing vouches for existing Ethos profiles ++ * @param subjectProfileId The profile ID to vouch for ++ * @param comment Optional text comment about the vouch ++ * @param metadata Additional structured metadata about the vouch + */ + function vouchByProfileId( + uint256 subjectProfileId, + string calldata comment, + string calldata metadata +- ) public payable whenNotPaused nonReentrant { +- uint256 authorProfileId = IEthosProfile( +- contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +- ).verifiedProfileIdForAddress(msg.sender); ++ ) external payable whenNotPaused nonReentrant { ++ _vouchByProfileIdCommon(subjectProfileId, comment, metadata); ++ } ++ ++ function _vouchByProfileIdCommon( ++ uint256 subjectProfileId, ++ string calldata comment, ++ string calldata metadata ++ ) internal { ++ (bool verified, bool archived, bool mock) = _ethosProfile().profileStatusById(subjectProfileId); ++ ++ // this function is only for verified profiles ++ if (!verified) revert InvalidEthosProfileForVouch(subjectProfileId); ++ // you may not vouch for archived profiles ++ if (archived) revert InvalidEthosProfileForVouch(subjectProfileId); ++ // use vouchByAddress or vouchByAttestationHash for mock profiles ++ if (mock) revert InvalidEthosProfileForVouch(subjectProfileId); ++ ++ _vouchCommon(subjectProfileId, comment, metadata, bytes32(0), address(0)); ++ } ++ ++ function _vouchCommon( ++ uint256 subjectProfileId, ++ string calldata comment, ++ string calldata metadata, ++ bytes32 attestationHash, ++ address subjectAddress ++ ) internal { ++ uint256 authorProfileId = _ethosProfile().verifiedProfileIdForAddress(msg.sender); + + // pls no vouch for yourself + if (authorProfileId == subjectProfileId) { +@@ -359,19 +441,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + } + + // validate subject profile +- if (subjectProfileId == 0) { +- revert InvalidEthosProfileForVouch(subjectProfileId); +- } +- (bool verified, bool archived, bool mock) = IEthosProfile( +- contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +- ).profileStatusById(subjectProfileId); +- +- // you may not vouch for archived profiles +- // however, you may vouch for verified AND mock profiles +- // we allow vouching for mock profiles in case they are later verified +- if (archived || (!mock && !verified)) { +- revert InvalidEthosProfileForVouch(subjectProfileId); +- } ++ if (subjectProfileId == 0) revert InvalidEthosProfileForVouch(subjectProfileId); + + // one vouch per profile per author + _vouchShouldNotExistFor(authorProfileId, subjectProfileId); +@@ -389,7 +459,13 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + +- (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId, authorProfileId); ++ (uint256 toDeposit, ) = applyEntryFees( ++ msg.value, ++ subjectProfileId, ++ authorProfileId, ++ attestationHash, ++ subjectAddress ++ ); + + // store vouch details + uint256 count = vouchCount; +@@ -418,49 +494,95 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + }) + }); + +- emit Vouched(count, authorProfileId, subjectProfileId, msg.value, toDeposit); ++ emit Vouched( ++ count, ++ authorProfileId, ++ subjectProfileId, ++ subjectAddress, ++ attestationHash, ++ msg.value, ++ toDeposit ++ ); + vouchCount++; + } + + /** +- * @notice Increases the amount staked for an existing vouch +- ++ * @notice Increases the staked amount for an existing vouch + * @param vouchId The ID of the vouch to increase +- +- * @custom:throws {NotAuthorForVouch} If the caller is not the author of the vouch +- * @custom:throws {AlreadyUnvouched} If the vouch has already been unvouched +- * @custom:emits VouchIncreased ++ * @param attestationHash Optional mock profile attestation hash (use bytes32(0) if not needed) ++ * @param subjectAddress Optional mock profile address (use address(0) if not needed) ++ * @dev For verified profiles, both attestationHash and subjectAddress should be empty ++ * @dev For mock profiles, either attestationHash OR subjectAddress must match the original vouch + */ +- function increaseVouch(uint256 vouchId) public payable nonReentrant { ++ function increaseVouch( ++ uint256 vouchId, ++ bytes32 attestationHash, ++ address subjectAddress ++ ) public payable nonReentrant { + // vouch increases much also meet the minimum vouch amount + if (msg.value < configuredMinimumVouchAmount) { + revert MinimumVouchAmount(configuredMinimumVouchAmount); + } + // get the profile id of the author +- uint256 authorProfileId = IEthosProfile( +- contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +- ).verifiedProfileIdForAddress(msg.sender); ++ uint256 authorProfileId = _ethosProfile().verifiedProfileIdForAddress(msg.sender); + _vouchShouldBelongToAuthor(vouchId, authorProfileId); + // make sure this vouch is active; not unvouched + _vouchShouldBePossibleUnvouch(vouchId); + + uint256 subjectProfileId = vouches[vouchId].subjectProfileId; +- (uint256 toDeposit, ) = applyFees(msg.value, true, subjectProfileId, authorProfileId); ++ ++ // If the subject profile is a mock profile, require a subject address or attestation hash ++ (, , bool mock) = _ethosProfile().profileStatusById(subjectProfileId); ++ if (mock && (subjectAddress == address(0) && attestationHash == bytes32(0))) ++ revert InvalidEthosProfileForVouch(subjectProfileId); ++ ++ // Validate that either attestationHash or subjectAddress matches the vouch ++ if (attestationHash != bytes32(0)) { ++ uint256 attestationProfileId = _ethosProfile().profileIdByAttestation(attestationHash); ++ if (attestationProfileId != subjectProfileId) ++ revert InvalidEthosProfileForVouch(attestationProfileId); ++ } ++ ++ if (subjectAddress != address(0)) { ++ uint256 addressProfileId = _ethosProfile().profileIdByAddress(subjectAddress); ++ if (addressProfileId != subjectProfileId) ++ revert InvalidEthosProfileForVouch(addressProfileId); ++ } ++ ++ (uint256 toDeposit, ) = applyEntryFees( ++ msg.value, ++ subjectProfileId, ++ authorProfileId, ++ attestationHash, ++ subjectAddress ++ ); + vouches[vouchId].balance += toDeposit; + +- emit VouchIncreased(vouchId, authorProfileId, subjectProfileId, msg.value, toDeposit); ++ emit VouchIncreased( ++ vouchId, ++ authorProfileId, ++ subjectProfileId, ++ subjectAddress, ++ attestationHash, ++ msg.value, ++ toDeposit ++ ); + } + + // --- Unvouch Functions --- + + /** +- * @dev Unvouches vouch. +- * @param vouchId Vouch Id. ++ * @notice Withdraws a vouch, returning staked funds to the author minus exit fees ++ * @param vouchId The ID of the vouch to withdraw + */ + function unvouch(uint256 vouchId) public whenNotPaused nonReentrant { + Vouch storage v = vouches[vouchId]; + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnvouch(vouchId); ++ ++ // Prevent withdrawals if the author's profile is frozen ++ if (frozenAuthors[v.authorProfileId]) revert PendingSlash(vouchId, v.authorProfileId); ++ + // because it's $$$, you can only withdraw/unvouch to the same address you used to vouch + // however, we don't care about the status of the address's profile; funds are always attached + // to an address, not a profile +@@ -475,7 +597,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + _removeVouchFromArrays(v); + + // apply fees and determine how much is left to send back to the author +- (uint256 toWithdraw, ) = applyFees(v.balance, false, v.subjectProfileId, v.authorProfileId); ++ (uint256 toWithdraw, ) = applyExitFees(v.balance); + // set the balance to 0 and save back to storage + v.balance = 0; + // send the funds to the author +@@ -503,9 +625,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + */ + function markUnhealthy(uint256 vouchId) public whenNotPaused { + Vouch storage v = vouches[vouchId]; +- uint256 profileId = IEthosProfile( +- contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +- ).verifiedProfileIdForAddress(msg.sender); ++ uint256 profileId = _ethosProfile().verifiedProfileIdForAddress(msg.sender); + + _vouchShouldExist(vouchId); + _vouchShouldBePossibleUnhealthy(vouchId); +@@ -519,6 +639,27 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + + // --- Slash Functions --- + ++ /** ++ * @notice Freezes all vouches from a specific author, preventing withdrawals until unfrozen or slashed ++ * @dev Only callable by the authorized slasher contract ++ * @param authorProfileId The profile ID whose vouches will be frozen ++ * This is a protective measure to prevent withdrawal of funds during investigation before the slashing process is complete ++ */ ++ function freeze(uint256 authorProfileId) external onlySlasher whenNotPaused nonReentrant { ++ frozenAuthors[authorProfileId] = true; ++ emit Frozen(authorProfileId, true); ++ } ++ ++ /** ++ * @notice Unfreezes an author's vouches, allowing withdrawals again ++ * @dev Only callable by the authorized slasher contract ++ * @param authorProfileId The profile ID whose vouches will be unfrozen ++ */ ++ function unfreeze(uint256 authorProfileId) external onlySlasher whenNotPaused nonReentrant { ++ frozenAuthors[authorProfileId] = false; ++ emit Frozen(authorProfileId, false); ++ } ++ + /** + * @notice Reduces all vouch balances for a given author by a percentage + * @param authorProfileId The profile ID whose vouches will be slashed +@@ -672,48 +813,85 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + + // --- Reward Functions --- + ++ /** ++ * @notice Claims accumulated rewards for a verified Ethos profile ++ * @dev Combines rewards from both profile ID and address-based rewards ++ */ + function claimRewards() external whenNotPaused nonReentrant { +- (bool verified, , bool mock, uint256 callerProfileId) = IEthosProfile( +- contractAddressManager.getContractAddressForName(ETHOS_PROFILE) +- ).profileStatusByAddress(msg.sender); ++ (bool verified, , bool mock, uint256 callerProfileId) = _ethosProfile().profileStatusByAddress( ++ msg.sender ++ ); + + // Only check that this is a real profile (not mock) and was verified at some point +- if (!verified || mock) { +- revert ProfileNotFoundForAddress(msg.sender); +- } ++ if (!verified || mock) revert ProfileNotFoundForAddress(msg.sender); ++ ++ // claim rewards by profile id ++ uint256 amount = rewardsByProfileId[callerProfileId]; ++ ++ // claim rewards by address ++ uint256 addressRewards = rewardsByAddress[msg.sender]; ++ amount += addressRewards; + +- uint256 amount = rewards[callerProfileId]; + if (amount == 0) revert InsufficientRewardsBalance(); + +- rewards[callerProfileId] = 0; ++ rewardsByProfileId[callerProfileId] = 0; ++ rewardsByAddress[msg.sender] = 0; + (bool success, ) = msg.sender.call{ value: amount }(""); + if (!success) revert FeeTransferFailed("Rewards claim failed"); + + emit WithdrawnFromRewards(callerProfileId, amount); + } + +- function _depositRewards(uint256 amount, uint256 recipientProfileId) internal { +- rewards[recipientProfileId] += amount; +- emit DepositedToRewards(recipientProfileId, amount); +- } +- + /** +- * @notice Distributes rewards to previous vouchers proportionally based on their current balance, +- * excluding the current transaction's author +- * @param amount The amount to distribute as rewards +- * @param subjectProfileId The profile ID whose vouchers will receive rewards +- * @param excludeAuthorId The profile ID to exclude from reward distribution ++ * @notice Claims accumulated rewards for a profile using their attestation hash ++ * @dev Only for verified profiles that registered the attestation hash ++ * @param attestationHash The attestation hash used to identify the rewards + */ +- function _rewardPreviousVouchers( ++ function claimRewardsByAttestation(bytes32 attestationHash) external whenNotPaused nonReentrant { ++ (bool verified, , bool mock, uint256 callerProfileId) = _ethosProfile().profileStatusByAddress( ++ msg.sender ++ ); ++ if (!verified) revert ProfileNotFoundForAddress(msg.sender); ++ if (mock) revert ProfileNotFoundForAddress(msg.sender); ++ ++ // ensure this hash belongs to the caller ++ if (_ethosProfile().profileIdByAttestation(attestationHash) != callerProfileId) ++ revert InvalidAttestationHash(attestationHash); ++ ++ uint256 amount = rewardsByAttestationHash[attestationHash]; ++ if (amount == 0) revert InsufficientRewardsBalance(); ++ ++ rewardsByAttestationHash[attestationHash] = 0; ++ (bool success, ) = msg.sender.call{ value: amount }(""); ++ if (!success) revert FeeTransferFailed("Rewards claim failed"); ++ ++ emit WithdrawnFromRewards(callerProfileId, amount); ++ } ++ ++ function _depositRewards( + uint256 amount, ++ uint256 recipientProfileId, ++ bytes32 attestationHash, ++ address subjectAddress ++ ) internal { ++ if (attestationHash != bytes32(0)) { ++ rewardsByAttestationHash[attestationHash] += amount; ++ } else if (subjectAddress != address(0)) { ++ rewardsByAddress[subjectAddress] += amount; ++ } else { ++ rewardsByProfileId[recipientProfileId] += amount; ++ } ++ emit DepositedToRewards(recipientProfileId, amount); ++ } ++ ++ function _previousVouchersBalance( + uint256 subjectProfileId, + uint256 excludeAuthorId +- ) internal returns (uint256 amountDistributed) { ++ ) internal view returns (uint256 totalBalance) { + uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; + uint256 totalVouches = vouchIds.length; + + // Calculate total balance of all active vouches except author's +- uint256 totalBalance; + for (uint256 i = 0; i < totalVouches; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + // Only include active (not archived) vouches from other authors +@@ -721,19 +899,33 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + totalBalance += vouch.balance; + } + } ++ } + +- // If there are no eligible vouchers, return 0 +- if (totalBalance == 0) { +- return 0; +- } +- ++ function _rewardPreviousVouchers( ++ uint256 amountToDistribute, ++ uint256 previousVouchersBalance, ++ uint256 subjectProfileId, ++ uint256 excludeAuthorId, ++ bytes32 attestationHash, ++ address subjectAddress ++ ) internal { ++ uint256[] storage vouchIds = vouchIdsForSubjectProfileId[subjectProfileId]; ++ uint256 totalVouches = vouchIds.length; + // Distribute rewards proportionally +- uint256 remainingRewards = amount; ++ uint256 remainingRewards = amountToDistribute; + for (uint256 i = 0; i < totalVouches && remainingRewards > 0; i++) { + Vouch storage vouch = vouches[vouchIds[i]]; + if (!vouch.archived && vouch.authorProfileId != excludeAuthorId) { + // Calculate this vouch's share of the rewards +- uint256 reward = amount.mulDiv(vouch.balance, totalBalance, Math.Rounding.Floor); ++ uint256 reward = amountToDistribute.mulDiv( ++ vouch.balance, ++ previousVouchersBalance, ++ Math.Rounding.Floor ++ ); ++ // Avoid rewarding the author more than the amount they vouched ++ // this prevents vouchers from vouching everyone for dust, just to try to get the total real vouch rewards ++ reward = Math.min(reward, vouch.balance); ++ + if (reward > 0) { + vouch.balance += reward; + remainingRewards -= reward; +@@ -741,12 +933,10 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + } + } + +- // Send any dust (remaining rewards due to rounding) to the subject reward escrow ++ // Send any remaining rewards to the subject reward escrow + if (remainingRewards > 0) { +- _depositRewards(remainingRewards, subjectProfileId); ++ _depositRewards(remainingRewards, subjectProfileId, attestationHash, subjectAddress); + } +- +- return amount; + } + + // --- View Functions --- +@@ -798,9 +988,7 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + uint256 author, + address subjectAddress + ) external view returns (Vouch memory) { +- address ethosProfile = contractAddressManager.getContractAddressForName(ETHOS_PROFILE); +- +- uint256 profileId = IEthosProfile(ethosProfile).verifiedProfileIdForAddress(subjectAddress); ++ uint256 profileId = _ethosProfile().verifiedProfileIdForAddress(subjectAddress); + + return verifiedVouchByAuthorForSubjectProfileId(author, profileId); + } +@@ -929,85 +1117,69 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + } + } + +- /** +- * @notice Applies and distributes protocol, donation, and vouchers pool fees +- * @param amount The total amount to apply fees to +- * @param isEntry Whether this is an entry transaction (true) or exit/withdrawal (false) +- * @param subjectProfileId The profile ID receiving donation/rewards +- * @param authorProfileId The profile ID of the vouch author +- * @return toDeposit The amount remaining after fees are deducted +- * @return totalFees The total amount of fees collected +- */ +- function applyFees( ++ function applyEntryFees( + uint256 amount, +- bool isEntry, + uint256 subjectProfileId, +- uint256 authorProfileId ++ uint256 authorProfileId, ++ bytes32 attestationHash, ++ address subjectAddress + ) internal returns (uint256 toDeposit, uint256 totalFees) { +- if (isEntry) { +- // Sum all entry fees to calculate a max fee to be split across each fee type +- uint256 totalFeesBasisPoints = entryProtocolFeeBasisPoints + +- entryDonationFeeBasisPoints + +- entryVouchersPoolFeeBasisPoints; +- +- if (totalFeesBasisPoints == 0) return (amount, 0); +- +- // Calculate total fees using combined basis points +- // Formula: totalFees = amount - (amount * BASIS_POINT_SCALE / (BASIS_POINT_SCALE + totalBasisPoints)) +- totalFees = +- amount - +- ( +- amount.mulDiv( +- BASIS_POINT_SCALE, +- (BASIS_POINT_SCALE + totalFeesBasisPoints), +- Math.Rounding.Floor +- ) +- ); ++ uint256 previousVouchersBalance = _previousVouchersBalance(subjectProfileId, authorProfileId); ++ // Sum all entry fees to calculate a max fee to be split across each fee type ++ uint256 totalFeesBasisPoints = entryProtocolFeeBasisPoints + entryDonationFeeBasisPoints; ++ if (previousVouchersBalance > 0) totalFeesBasisPoints += entryVouchersPoolFeeBasisPoints; ++ if (totalFeesBasisPoints == 0) return (amount, 0); ++ ++ // Calculate deposit amount ++ toDeposit = amount.mulDiv( ++ BASIS_POINT_SCALE, ++ (BASIS_POINT_SCALE + totalFeesBasisPoints), ++ Math.Rounding.Floor ++ ); + +- // Distribute total fees proportionally to each fee type based on their relative basis points +- // Protocol fee share = (protocolFeeBasisPoints / totalBasisPoints) * totalFees +- uint256 protocolFee = totalFees.mulDiv( +- entryProtocolFeeBasisPoints, +- totalFeesBasisPoints, +- Math.Rounding.Floor +- ); ++ // Calculate total fees as the remainder ++ totalFees = amount - toDeposit; + +- // Donation fee share = (donationFeeBasisPoints / totalBasisPoints) * totalFees +- uint256 donationFee = totalFees.mulDiv( +- entryDonationFeeBasisPoints, +- totalFeesBasisPoints, +- Math.Rounding.Floor ++ // Distribute total fees proportionally ++ uint256 protocolFee = totalFees.mulDiv( ++ entryProtocolFeeBasisPoints, ++ totalFeesBasisPoints, ++ Math.Rounding.Floor ++ ); ++ uint256 donationFee = totalFees.mulDiv( ++ entryDonationFeeBasisPoints, ++ totalFeesBasisPoints, ++ Math.Rounding.Floor ++ ); ++ uint256 vouchersPoolFee = totalFees - protocolFee - donationFee; ++ ++ // Distribute each fee portion to its respective destination ++ if (protocolFee > 0) _depositProtocolFee(protocolFee); ++ ++ if (donationFee > 0) ++ _depositRewards(donationFee, subjectProfileId, attestationHash, subjectAddress); ++ ++ if (vouchersPoolFee > 0) ++ _rewardPreviousVouchers( ++ vouchersPoolFee, ++ previousVouchersBalance, ++ subjectProfileId, ++ authorProfileId, ++ attestationHash, ++ subjectAddress + ); + +- // Assign remaining fees to vouchers pool to handle rounding precision +- uint256 vouchersPoolFee = totalFees - protocolFee - donationFee; +- +- // Distribute each fee portion to its respective destination +- if (protocolFee > 0) { +- _depositProtocolFee(protocolFee); +- } +- if (donationFee > 0) { +- _depositRewards(donationFee, subjectProfileId); +- } +- if (vouchersPoolFee > 0) { +- vouchersPoolFee = _rewardPreviousVouchers( +- vouchersPoolFee, +- subjectProfileId, +- authorProfileId +- ); +- } +- +- toDeposit = amount - totalFees; +- } else { +- // Exit transactions only have a single protocol fee +- totalFees = calcFee(amount, exitFeeBasisPoints); +- if (totalFees > 0) { +- _depositProtocolFee(totalFees); +- } +- toDeposit = amount - totalFees; +- } ++ // vouchersPoolFee is dynamic and may be less than initially allocated ++ // so we need to recalculate totalFees to ensure the correct amount is deposited ++ totalFees = protocolFee + donationFee + vouchersPoolFee; ++ toDeposit = amount - totalFees; ++ } + +- return (toDeposit, totalFees); ++ function applyExitFees(uint256 amount) internal returns (uint256 toDeposit, uint256 totalFees) { ++ // Exit transactions only have a single protocol fee ++ totalFees = calcFee(amount, exitFeeBasisPoints); ++ if (totalFees > 0) _depositProtocolFee(totalFees); ++ toDeposit = amount - totalFees; + } + + /** +@@ -1083,4 +1255,8 @@ contract EthosVouch is AccessControl, UUPSUpgradeable, ITargetStatus, Reentrancy + // the author->subject mapping is only for active vouches; remove it + delete vouchIdByAuthorForSubjectProfileId[v.authorProfileId][v.subjectProfileId]; + } ++ ++ function _ethosProfile() internal view returns (IEthosProfile) { ++ return IEthosProfile(contractAddressManager.getContractAddressForName(ETHOS_PROFILE)); ++ } + } diff --git a/ethos/packages/contracts/contracts/ReputationMarket.sol b/ethos/packages/contracts/contracts/ReputationMarket.sol index 8590a90..bcbd58d 100644 --- a/ethos/packages/contracts/contracts/ReputationMarket.sol +++ b/ethos/packages/contracts/contracts/ReputationMarket.sol @@ -4,7 +4,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { AccessControl } from "./utils/AccessControl.sol"; import { ETHOS_PROFILE } from "./utils/Constants.sol"; import { IEthosProfile } from "./interfaces/IEthosProfile.sol"; -import { InsufficientLiquidity, InactiveMarket, InsufficientFunds, FeeTransferFailed, InsufficientVotesOwned, InsufficientVotesToSell, InvalidProfileId, MarketAlreadyExists, MarketCreationErrorCode, MarketCreationUnauthorized, MarketDoesNotExist, SellSlippageLimitExceeded, InvalidMarketConfigOption, UnauthorizedGraduation, UnauthorizedWithdrawal, MarketNotGraduated, ZeroAddressNotAllowed } from "./errors/ReputationMarketErrors.sol"; +import { InsufficientLiquidity, InactiveMarket, InsufficientFunds, FeeTransferFailed, InsufficientVotesOwned, InsufficientVotesToSell, InvalidProfileId, MarketAlreadyExists, MarketCreationErrorCode, MarketCreationUnauthorized, MarketDoesNotExist, SellSlippageLimitExceeded, InvalidMarketConfigOption, UnauthorizedGraduation, UnauthorizedWithdrawal, MarketNotGraduated, ZeroAddressNotAllowed, BuySlippageLimitExceeded } from "./errors/ReputationMarketErrors.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { LMSR } from "./utils/LMSR.sol"; @@ -444,10 +444,7 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard, IT uint256 minVotesToBuy ) public payable whenNotPaused activeMarket(profileId) nonReentrant { _checkMarketExists(profileId); - // preliminary check to ensure this is enough money to buy the minimum requested votes. - (, , , uint256 total) = _calculateBuy(markets[profileId], isPositive, minVotesToBuy); - if (total > msg.value) revert InsufficientFunds(); - + // check if we can afford the max number of votes to buy ( uint256 purchaseCostBeforeFees, uint256 protocolFee, @@ -455,15 +452,23 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard, IT uint256 totalCostIncludingFees ) = _calculateBuy(markets[profileId], isPositive, maxVotesToBuy); uint256 currentVotesToBuy = maxVotesToBuy; - // if the cost is greater than the maximum votes to buy, - // decrement vote count and recalculate until we identify the max number of votes they can afford - while (totalCostIncludingFees > msg.value) { - currentVotesToBuy--; - (purchaseCostBeforeFees, protocolFee, donation, totalCostIncludingFees) = _calculateBuy( - markets[profileId], - isPositive, - currentVotesToBuy - ); + + if (totalCostIncludingFees > msg.value) { + // if there's no slippage allowed (via minVotesToBuy), don't bother calculating further options, just revert + if (maxVotesToBuy == minVotesToBuy) revert InsufficientFunds(); + // if the cost is greater than the maximum votes to buy, + // check if we can afford the minimum requested votes + (, , , uint256 total) = _calculateBuy(markets[profileId], isPositive, minVotesToBuy); + if (total > msg.value) revert BuySlippageLimitExceeded(total, msg.value); + // if we can afford the minimum requested votes, but not the max, + // find the number of votes they can afford + ( + currentVotesToBuy, + purchaseCostBeforeFees, + protocolFee, + donation, + totalCostIncludingFees + ) = _searchAffordableBuy(markets[profileId], isPositive, maxVotesToBuy); } // Update market state @@ -496,6 +501,54 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard, IT _emitMarketUpdate(profileId); } + function _searchAffordableBuy( + Market memory market, + bool isPositive, + uint256 maxVotesToBuy + ) + private + view + returns ( + uint256 currentVotesToBuy, + uint256 purchaseCostBeforeFees, + uint256 protocolFee, + uint256 donation, + uint256 totalCostIncludingFees + ) + { + uint256 low = 0; + uint256 high = maxVotesToBuy; + + while (low <= high) { + uint256 mid = (low + high) / 2; + + (purchaseCostBeforeFees, protocolFee, donation, totalCostIncludingFees) = _calculateBuy( + market, + isPositive, + mid + ); + + if (totalCostIncludingFees == msg.value) { + // Exact match found + return (mid, purchaseCostBeforeFees, protocolFee, donation, totalCostIncludingFees); + } else if (totalCostIncludingFees < msg.value) { + // Can afford this amount, try higher + currentVotesToBuy = mid; // Store last known good amount + low = mid + 1; + } else { + // Too expensive, try lower + high = mid - 1; + } + } + + // If we found an affordable amount, return its costs + (purchaseCostBeforeFees, protocolFee, donation, totalCostIncludingFees) = _calculateBuy( + market, + isPositive, + currentVotesToBuy + ); + } + /** * @dev Previews the cost and fees for buying votes (without executing the trade) * @param market The market state to calculate costs for @@ -1054,7 +1107,7 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard, IT cost = positiveCostRatio.mulDiv( market.basePrice, 1e18, - isPositive ? Math.Rounding.Floor : Math.Rounding.Ceil + !isBuy ? Math.Rounding.Floor : Math.Rounding.Ceil ); } diff --git a/ethos/packages/contracts/contracts/ReputationMarket.sol.rej b/ethos/packages/contracts/contracts/ReputationMarket.sol.rej new file mode 100644 index 0000000..bd2b70a --- /dev/null +++ b/ethos/packages/contracts/contracts/ReputationMarket.sol.rej @@ -0,0 +1,120 @@ +diff a/ethos/packages/contracts/contracts/ReputationMarket.sol b/ethos/packages/contracts/contracts/ReputationMarket.sol (rejected hunks) +@@ -4,7 +4,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils + import { AccessControl } from "./utils/AccessControl.sol"; + import { ETHOS_PROFILE } from "./utils/Constants.sol"; + import { IEthosProfile } from "./interfaces/IEthosProfile.sol"; +-import { InsufficientLiquidity, InactiveMarket, InsufficientFunds, FeeTransferFailed, InsufficientVotesOwned, InsufficientVotesToSell, InvalidProfileId, MarketAlreadyExists, MarketCreationErrorCode, MarketCreationUnauthorized, MarketDoesNotExist, SellSlippageLimitExceeded, InvalidMarketConfigOption, UnauthorizedGraduation, UnauthorizedWithdrawal, MarketNotGraduated, ZeroAddressNotAllowed } from "./errors/ReputationMarketErrors.sol"; ++import { InsufficientLiquidity, InactiveMarket, InsufficientFunds, FeeTransferFailed, InsufficientVotesOwned, InsufficientVotesToSell, InvalidProfileId, MarketAlreadyExists, MarketCreationErrorCode, MarketCreationUnauthorized, MarketDoesNotExist, SellSlippageLimitExceeded, InvalidMarketConfigOption, UnauthorizedGraduation, UnauthorizedWithdrawal, MarketNotGraduated, ZeroAddressNotAllowed, BuySlippageLimitExceeded } from "./errors/ReputationMarketErrors.sol"; + import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + import { LMSR } from "./utils/LMSR.sol"; +@@ -444,10 +444,7 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard, IT + uint256 minVotesToBuy + ) public payable whenNotPaused activeMarket(profileId) nonReentrant { + _checkMarketExists(profileId); +- // preliminary check to ensure this is enough money to buy the minimum requested votes. +- (, , , uint256 total) = _calculateBuy(markets[profileId], isPositive, minVotesToBuy); +- if (total > msg.value) revert InsufficientFunds(); +- ++ // check if we can afford the max number of votes to buy + ( + uint256 purchaseCostBeforeFees, + uint256 protocolFee, +@@ -455,15 +452,23 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard, IT + uint256 totalCostIncludingFees + ) = _calculateBuy(markets[profileId], isPositive, maxVotesToBuy); + uint256 currentVotesToBuy = maxVotesToBuy; +- // if the cost is greater than the maximum votes to buy, +- // decrement vote count and recalculate until we identify the max number of votes they can afford +- while (totalCostIncludingFees > msg.value) { +- currentVotesToBuy--; +- (purchaseCostBeforeFees, protocolFee, donation, totalCostIncludingFees) = _calculateBuy( +- markets[profileId], +- isPositive, +- currentVotesToBuy +- ); ++ ++ if (totalCostIncludingFees > msg.value) { ++ // if there's no slippage allowed (via minVotesToBuy), don't bother calculating further options, just revert ++ if (maxVotesToBuy == minVotesToBuy) revert InsufficientFunds(); ++ // if the cost is greater than the maximum votes to buy, ++ // check if we can afford the minimum requested votes ++ (, , , uint256 total) = _calculateBuy(markets[profileId], isPositive, minVotesToBuy); ++ if (total > msg.value) revert BuySlippageLimitExceeded(total, msg.value); ++ // if we can afford the minimum requested votes, but not the max, ++ // find the number of votes they can afford ++ ( ++ currentVotesToBuy, ++ purchaseCostBeforeFees, ++ protocolFee, ++ donation, ++ totalCostIncludingFees ++ ) = _searchAffordableBuy(markets[profileId], isPositive, minVotesToBuy, maxVotesToBuy); + } + + // Update market state +@@ -496,6 +501,55 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard, IT + _emitMarketUpdate(profileId); + } + ++ function _searchAffordableBuy( ++ Market memory market, ++ bool isPositive, ++ uint256 minVotesToBuy, ++ uint256 maxVotesToBuy ++ ) ++ private ++ view ++ returns ( ++ uint256 currentVotesToBuy, ++ uint256 purchaseCostBeforeFees, ++ uint256 protocolFee, ++ uint256 donation, ++ uint256 totalCostIncludingFees ++ ) ++ { ++ uint256 low = minVotesToBuy; ++ uint256 high = maxVotesToBuy; ++ ++ while (low <= high) { ++ uint256 mid = (low + high) / 2; ++ ++ (purchaseCostBeforeFees, protocolFee, donation, totalCostIncludingFees) = _calculateBuy( ++ market, ++ isPositive, ++ mid ++ ); ++ ++ if (totalCostIncludingFees == msg.value) { ++ // Exact match found ++ return (mid, purchaseCostBeforeFees, protocolFee, donation, totalCostIncludingFees); ++ } else if (totalCostIncludingFees < msg.value) { ++ // Can afford this amount, try higher ++ currentVotesToBuy = mid; // Store last known good amount ++ low = mid + 1; ++ } else { ++ // Too expensive, try lower ++ high = mid - 1; ++ } ++ } ++ ++ // If we found an affordable amount, return its costs ++ (purchaseCostBeforeFees, protocolFee, donation, totalCostIncludingFees) = _calculateBuy( ++ market, ++ isPositive, ++ currentVotesToBuy ++ ); ++ } ++ + /** + * @dev Previews the cost and fees for buying votes (without executing the trade) + * @param market The market state to calculate costs for +@@ -1054,7 +1108,7 @@ contract ReputationMarket is AccessControl, UUPSUpgradeable, ReentrancyGuard, IT + cost = positiveCostRatio.mulDiv( + market.basePrice, + 1e18, +- isPositive ? Math.Rounding.Floor : Math.Rounding.Ceil ++ !isBuy ? Math.Rounding.Floor : Math.Rounding.Ceil + ); + } + diff --git a/ethos/packages/contracts/contracts/errors/ReputationMarketErrors.sol b/ethos/packages/contracts/contracts/errors/ReputationMarketErrors.sol index 16b502e..561d6ab 100644 --- a/ethos/packages/contracts/contracts/errors/ReputationMarketErrors.sol +++ b/ethos/packages/contracts/contracts/errors/ReputationMarketErrors.sol @@ -26,3 +26,4 @@ error MarketNotGraduated(); error NoFundsToWithdraw(); error ZeroAddressNotAllowed(); error SellSlippageLimitExceeded(uint256 minimumPricePerVote, uint256 actualPricePerVote); +error BuySlippageLimitExceeded(uint256 requiredAmount, uint256 providedAmount); diff --git a/ethos/packages/contracts/contracts/errors/ReputationMarketErrors.sol.rej b/ethos/packages/contracts/contracts/errors/ReputationMarketErrors.sol.rej new file mode 100644 index 0000000..e354319 --- /dev/null +++ b/ethos/packages/contracts/contracts/errors/ReputationMarketErrors.sol.rej @@ -0,0 +1,6 @@ +diff a/ethos/packages/contracts/contracts/errors/ReputationMarketErrors.sol b/ethos/packages/contracts/contracts/errors/ReputationMarketErrors.sol (rejected hunks) +@@ -26,3 +26,4 @@ error MarketNotGraduated(); + error NoFundsToWithdraw(); + error ZeroAddressNotAllowed(); + error SellSlippageLimitExceeded(uint256 minimumPricePerVote, uint256 actualPricePerVote); ++error BuySlippageLimitExceeded(uint256 requiredAmount, uint256 providedAmount); diff --git a/ethos/packages/contracts/deploy.ts b/ethos/packages/contracts/deploy.ts index 7b44573..29bf184 100644 --- a/ethos/packages/contracts/deploy.ts +++ b/ethos/packages/contracts/deploy.ts @@ -184,15 +184,34 @@ async function main(): Promise { 'BASESCAN_TESTNET_API_KEY', ]; break; - case 'prod': + case 'prod': { requiredEnvVars = [ 'ALCHEMY_MAINNET_API_URL', - 'OWNER_MAINNET_PRIVATE_KEY', + 'ALCHEMY_MAINNET_API_KEY', 'BASESCAN_MAINNET_API_KEY', + 'OWNER_MAINNET_PRIVATE_KEY', + 'ADMIN_MAINNET_PRIVATE_KEY', + 'ADMIN_MAINNET_ADDRESS', + 'ADMIN_MAINNET_PRIVATE_KEY', + 'SIGNER_MAINNET_ADDRESS', ]; - console.error('We do not yet support deploying to prod/mainnet'); - process.exit(1); + const { confirmProd } = await inquirer.prompt<{ confirmProd: boolean }>({ + type: 'confirm', + name: 'confirmProd', + message: + '\n🚨 WARNING: PRODUCTION DEPLOYMENT 🚨\n' + + '💰 This will deploy to MAINNET with REAL MONEY 💰\n' + + '⚠️ Are you absolutely sure you want to continue? ⚠️\n' + + '🔐 This action cannot be undone! 🔐', + default: false, + }); + + if (!confirmProd) { + console.log('🛑 Production deployment cancelled'); + process.exit(1); + } break; + } default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions console.error(`⚠️ Unsupported environment: ${args.environment}`); @@ -218,6 +237,7 @@ async function main(): Promise { const updateManagement = await offerToUpdateManagement(); if (updateManagement) await updateAddressManager(args.contract, args.environment); + console.log(`💫 deployed ${args.contract} contract`); } async function verify(contract: Contract, environment: EthosEnvironment): Promise { diff --git a/ethos/packages/contracts/deploy.ts.rej b/ethos/packages/contracts/deploy.ts.rej new file mode 100644 index 0000000..99afd02 --- /dev/null +++ b/ethos/packages/contracts/deploy.ts.rej @@ -0,0 +1,48 @@ +diff a/ethos/packages/contracts/deploy.ts b/ethos/packages/contracts/deploy.ts (rejected hunks) +@@ -184,15 +184,34 @@ async function main(): Promise { + 'BASESCAN_TESTNET_API_KEY', + ]; + break; +- case 'prod': ++ case 'prod': { + requiredEnvVars = [ + 'ALCHEMY_MAINNET_API_URL', +- 'OWNER_MAINNET_PRIVATE_KEY', ++ 'ALCHEMY_MAINNET_API_KEY', + 'BASESCAN_MAINNET_API_KEY', ++ 'OWNER_MAINNET_PRIVATE_KEY', ++ 'ADMIN_MAINNET_PRIVATE_KEY', ++ 'ADMIN_MAINNET_ADDRESS', ++ 'ADMIN_MAINNET_PRIVATE_KEY', ++ 'SIGNER_MAINNET_ADDRESS', + ]; +- console.error('We do not yet support deploying to prod/mainnet'); +- process.exit(1); ++ const { confirmProd } = await inquirer.prompt<{ confirmProd: boolean }>({ ++ type: 'confirm', ++ name: 'confirmProd', ++ message: ++ '\n🚨 WARNING: PRODUCTION DEPLOYMENT 🚨\n' + ++ '💰 This will deploy to MAINNET with REAL MONEY 💰\n' + ++ '⚠️ Are you absolutely sure you want to continue? ⚠️\n' + ++ '🔐 This action cannot be undone! 🔐', ++ default: false, ++ }); ++ ++ if (!confirmProd) { ++ console.log('🛑 Production deployment cancelled'); ++ process.exit(1); ++ } + break; ++ } + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + console.error(`⚠️ Unsupported environment: ${args.environment}`); +@@ -218,6 +237,7 @@ async function main(): Promise { + const updateManagement = await offerToUpdateManagement(); + + if (updateManagement) await updateAddressManager(args.contract, args.environment); ++ console.log(`💫 deployed ${args.contract} contract`); + } + + async function verify(contract: Contract, environment: EthosEnvironment): Promise { diff --git a/ethos/packages/contracts/package.json b/ethos/packages/contracts/package.json index 3b94f76..18a4b19 100644 --- a/ethos/packages/contracts/package.json +++ b/ethos/packages/contracts/package.json @@ -24,10 +24,10 @@ "@nomicfoundation/hardhat-verify": "^2.0.12", "@openzeppelin/contracts": "^5.1.0", "@openzeppelin/contracts-upgradeable": "^5.1.0", - "@openzeppelin/hardhat-upgrades": "^3.6.0", + "@openzeppelin/hardhat-upgrades": "^3.8.0", "@prb/math": "^4.1.0", "dotenv": "^16.4.5", - "hardhat": "^2.22.16", + "hardhat": "^2.22.17", "hardhat-gas-reporter": "^1.0.10", "solhint": "^5.0.3", "solidity-coverage": "^0.8.14", diff --git a/ethos/packages/contracts/package.json.rej b/ethos/packages/contracts/package.json.rej new file mode 100644 index 0000000..8b84593 --- /dev/null +++ b/ethos/packages/contracts/package.json.rej @@ -0,0 +1,14 @@ +diff a/ethos/packages/contracts/package.json b/ethos/packages/contracts/package.json (rejected hunks) +@@ -24,10 +24,10 @@ + "@nomicfoundation/hardhat-verify": "^2.0.12", + "@openzeppelin/contracts": "^5.1.0", + "@openzeppelin/contracts-upgradeable": "^5.1.0", +- "@openzeppelin/hardhat-upgrades": "^3.6.0", ++ "@openzeppelin/hardhat-upgrades": "^3.8.0", + "@prb/math": "^4.1.0", + "dotenv": "^16.4.5", +- "hardhat": "^2.22.16", ++ "hardhat": "^2.22.17", + "hardhat-gas-reporter": "^1.0.10", + "solhint": "^5.0.3", + "solidity-coverage": "^0.8.14", diff --git a/ethos/packages/contracts/scripts/deploy-contract.ts b/ethos/packages/contracts/scripts/deploy-contract.ts index b32d99a..cc86741 100644 --- a/ethos/packages/contracts/scripts/deploy-contract.ts +++ b/ethos/packages/contracts/scripts/deploy-contract.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { readFileSync, writeFileSync } from 'node:fs'; +import { readFileSync } from 'node:fs'; import { ETHOS_ENVIRONMENTS, type EthosEnvironment } from '@ethos/env'; import { getLogger } from '@ethos/logger'; import hre from 'hardhat'; @@ -16,6 +16,7 @@ import { getContractKeyByEnvironment, } from '../src/index.js'; import { + writeMetadataFile, getAdminAccount, getSignerAccount, placeholderContractMetadata, @@ -88,6 +89,7 @@ const contractsConfigMap: ContractConfig = { interactionControl: { name: contractLookup.interactionControl.name, isUpgradeable: contractLookup.interactionControl.isUpgradeable, + // ADMIN SHOULD BE ABLE TO PAUSE CONTRACTS, WHICH MAKES IT THE OWNER OF INTERACTION CONTROL getArguments: () => [adminAccount, contractLookup.contractAddressManager.address], }, profile: { @@ -218,8 +220,19 @@ async function main(): Promise { console.log('📚 Deploying library...', libraryKey); const libraryContractName = libraryKey.split(':')[1]; const libraryContract = await ethers.deployContract(libraryContractName); - await libraryContract.waitForDeployment(); - libraryAddresses[libraryKey] = await libraryContract.getAddress(); + + // Wait for transaction to be mined with explicit confirmation count + const receipt = await libraryContract.deploymentTransaction()?.wait(1); + + if (!receipt) { + throw new Error(`Failed to deploy library ${libraryKey}`); + } + + // Get address after confirmed deployment + const libraryAddress = await libraryContract.getAddress(); + libraryAddresses[libraryKey] = libraryAddress; + + console.log(`📚 Library ${libraryKey} deployed to ${libraryAddress}`); } console.log('🎓 Deploying contract with libraries...'); @@ -229,13 +242,21 @@ async function main(): Promise { } else { contractObj = await ethers.getContractFactory(contractConfig.name); } - const contract = await contractObj.deploy(); - await contract.waitForDeployment(); - - const address = - typeof contract.target === 'string' ? contract.target : await contract.getAddress(); + let address: Address; + + if (!contractConfig.isUpgradeable) { + const contract = await contractObj.deploy(...args); + await contract.waitForDeployment(); + address = getAddress( + typeof contract.target === 'string' ? contract.target : await contract.getAddress(), + ); + } else { + const contract = await contractObj.deploy(); + await contract.waitForDeployment(); + address = getAddress( + typeof contract.target === 'string' ? contract.target : await contract.getAddress(), + ); - if (contractConfig.isUpgradeable) { if (shouldRedeployProxy) { console.log(`${proxyAddress ? '♻️ Redeploying' : '🚢 Deploying'} proxy contract...`); @@ -279,7 +300,7 @@ async function main(): Promise { }; // Save the contract metadata to a JSON file - writeFileSync(metadataFilePath, `${JSON.stringify(metadata, null, 2)}\n`); + writeMetadataFile(metadataFilePath, metadata); // eslint-disable-next-line no-console console.log(`✅ ${pc.blue(contractConfig.name)} deployed to ${pc.yellow(address)}`); diff --git a/ethos/packages/contracts/scripts/deploy-contract.ts.rej b/ethos/packages/contracts/scripts/deploy-contract.ts.rej new file mode 100644 index 0000000..103c018 --- /dev/null +++ b/ethos/packages/contracts/scripts/deploy-contract.ts.rej @@ -0,0 +1,83 @@ +diff a/ethos/packages/contracts/scripts/deploy-contract.ts b/ethos/packages/contracts/scripts/deploy-contract.ts (rejected hunks) +@@ -1,5 +1,5 @@ + /* eslint-disable no-console */ +-import { readFileSync, writeFileSync } from 'node:fs'; ++import { readFileSync } from 'node:fs'; + import { ETHOS_ENVIRONMENTS, type EthosEnvironment } from '@ethos/env'; + import { getLogger } from '@ethos/logger'; + import hre from 'hardhat'; +@@ -16,6 +16,7 @@ import { + getContractKeyByEnvironment, + } from '../src/index.js'; + import { ++ writeMetadataFile, + getAdminAccount, + getSignerAccount, + placeholderContractMetadata, +@@ -88,6 +89,7 @@ const contractsConfigMap: ContractConfig = { + interactionControl: { + name: contractLookup.interactionControl.name, + isUpgradeable: contractLookup.interactionControl.isUpgradeable, ++ // ADMIN SHOULD BE ABLE TO PAUSE CONTRACTS, WHICH MAKES IT THE OWNER OF INTERACTION CONTROL + getArguments: () => [adminAccount, contractLookup.contractAddressManager.address], + }, + profile: { +@@ -218,8 +220,19 @@ async function main(): Promise { + console.log('📚 Deploying library...', libraryKey); + const libraryContractName = libraryKey.split(':')[1]; + const libraryContract = await ethers.deployContract(libraryContractName); +- await libraryContract.waitForDeployment(); +- libraryAddresses[libraryKey] = await libraryContract.getAddress(); ++ ++ // Wait for transaction to be mined with explicit confirmation count ++ const receipt = await libraryContract.deploymentTransaction()?.wait(1); ++ ++ if (!receipt) { ++ throw new Error(`Failed to deploy library ${libraryKey}`); ++ } ++ ++ // Get address after confirmed deployment ++ const libraryAddress = await libraryContract.getAddress(); ++ libraryAddresses[libraryKey] = libraryAddress; ++ ++ console.log(`📚 Library ${libraryKey} deployed to ${libraryAddress}`); + } + + console.log('🎓 Deploying contract with libraries...'); +@@ -229,13 +242,21 @@ async function main(): Promise { + } else { + contractObj = await ethers.getContractFactory(contractConfig.name); + } +- const contract = await contractObj.deploy(); +- await contract.waitForDeployment(); +- +- const address = +- typeof contract.target === 'string' ? contract.target : await contract.getAddress(); ++ let address: Address; ++ ++ if (!contractConfig.isUpgradeable) { ++ const contract = await contractObj.deploy(...args); ++ await contract.waitForDeployment(); ++ address = getAddress( ++ typeof contract.target === 'string' ? contract.target : await contract.getAddress(), ++ ); ++ } else { ++ const contract = await contractObj.deploy(); ++ await contract.waitForDeployment(); ++ address = getAddress( ++ typeof contract.target === 'string' ? contract.target : await contract.getAddress(), ++ ); + +- if (contractConfig.isUpgradeable) { + if (shouldRedeployProxy) { + console.log(`${proxyAddress ? '♻️ Redeploying' : '🚢 Deploying'} proxy contract...`); + +@@ -279,7 +300,7 @@ async function main(): Promise { + }; + + // Save the contract metadata to a JSON file +- writeFileSync(metadataFilePath, `${JSON.stringify(metadata, null, 2)}\n`); ++ writeMetadataFile(metadataFilePath, metadata); + + // eslint-disable-next-line no-console + console.log(`✅ ${pc.blue(contractConfig.name)} deployed to ${pc.yellow(address)}`); diff --git a/ethos/packages/contracts/scripts/utils.ts b/ethos/packages/contracts/scripts/utils.ts index 16f07aa..abcd9d1 100644 --- a/ethos/packages/contracts/scripts/utils.ts +++ b/ethos/packages/contracts/scripts/utils.ts @@ -124,3 +124,8 @@ export async function writeContractABI( // Auto-fix the linting issues await ESLint.outputFixes(results); } + +export function writeMetadataFile(metadataFilePath: string, metadata: Record): void { + const contents = JSON.stringify(metadata, null, 2) + '\n'; + writeFileSync(metadataFilePath, contents); +} diff --git a/ethos/packages/contracts/scripts/utils.ts.rej b/ethos/packages/contracts/scripts/utils.ts.rej new file mode 100644 index 0000000..0bf8ca9 --- /dev/null +++ b/ethos/packages/contracts/scripts/utils.ts.rej @@ -0,0 +1,10 @@ +diff a/ethos/packages/contracts/scripts/utils.ts b/ethos/packages/contracts/scripts/utils.ts (rejected hunks) +@@ -124,3 +124,8 @@ export async function writeContractABI( + // Auto-fix the linting issues + await ESLint.outputFixes(results); + } ++ ++export function writeMetadataFile(metadataFilePath: string, metadata: Record): void { ++ const contents = JSON.stringify(metadata, null, 2) + '\n'; ++ writeFileSync(metadataFilePath, contents); ++} diff --git a/ethos/packages/contracts/src/attestation.json b/ethos/packages/contracts/src/attestation.json index d8a9acd..231e7ee 100644 --- a/ethos/packages/contracts/src/attestation.json +++ b/ethos/packages/contracts/src/attestation.json @@ -1,6 +1,7 @@ { "dev": { "address": "0xA5fD183986523cecf9EA21e0Ec96d1FcBf7d1e18", + "proxyAddress": "0x8a9D5690436CD339ef70044034de514F030A5847", "args": [ "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", @@ -8,7 +9,6 @@ "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" ], - "proxyAddress": "0x8a9D5690436CD339ef70044034de514F030A5847", "proxyArgs": [ "0xA5fD183986523cecf9EA21e0Ec96d1FcBf7d1e18", "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" @@ -30,9 +30,18 @@ ] }, "prod": { - "address": "0x0000000000000000000000000000000000000000", - "proxyAddress": "0x0000000000000000000000000000000000000000", - "args": [], - "proxyArgs": [] + "address": "0x4931536Fc088037f913521DaF0441786f22128C6", + "args": [ + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", + "0xe6F811d8aEd56D68D2b8658e01519069470b7436" + ], + "proxyAddress": "0x18e76564ecAaBDE4665b753197dF7e514973AADE", + "proxyArgs": [ + "0x4931536Fc088037f913521DaF0441786f22128C6", + "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" + ] } } diff --git a/ethos/packages/contracts/src/attestation.json.rej b/ethos/packages/contracts/src/attestation.json.rej new file mode 100644 index 0000000..3aee3ef --- /dev/null +++ b/ethos/packages/contracts/src/attestation.json.rej @@ -0,0 +1,40 @@ +diff a/ethos/packages/contracts/src/attestation.json b/ethos/packages/contracts/src/attestation.json (rejected hunks) +@@ -1,6 +1,7 @@ + { + "dev": { + "address": "0xA5fD183986523cecf9EA21e0Ec96d1FcBf7d1e18", ++ "proxyAddress": "0x8a9D5690436CD339ef70044034de514F030A5847", + "args": [ + "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", + "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", +@@ -8,7 +9,6 @@ + "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", + "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" + ], +- "proxyAddress": "0x8a9D5690436CD339ef70044034de514F030A5847", + "proxyArgs": [ + "0xA5fD183986523cecf9EA21e0Ec96d1FcBf7d1e18", + "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" +@@ -30,9 +30,18 @@ + ] + }, + "prod": { +- "address": "0x0000000000000000000000000000000000000000", +- "proxyAddress": "0x0000000000000000000000000000000000000000", +- "args": [], +- "proxyArgs": [] ++ "address": "0x4931536Fc088037f913521DaF0441786f22128C6", ++ "args": [ ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", ++ "0xe6F811d8aEd56D68D2b8658e01519069470b7436" ++ ], ++ "proxyAddress": "0x18e76564ecAaBDE4665b753197dF7e514973AADE", ++ "proxyArgs": [ ++ "0x4931536Fc088037f913521DaF0441786f22128C6", ++ "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" ++ ] + } + } diff --git a/ethos/packages/contracts/src/contractAddressManager.json b/ethos/packages/contracts/src/contractAddressManager.json index bfbbeee..8bfa812 100644 --- a/ethos/packages/contracts/src/contractAddressManager.json +++ b/ethos/packages/contracts/src/contractAddressManager.json @@ -8,7 +8,7 @@ "args": [] }, "prod": { - "address": "0x0000000000000000000000000000000000000000", + "address": "0xe6F811d8aEd56D68D2b8658e01519069470b7436", "args": [] } } diff --git a/ethos/packages/contracts/src/contractAddressManager.json.rej b/ethos/packages/contracts/src/contractAddressManager.json.rej new file mode 100644 index 0000000..99c890d --- /dev/null +++ b/ethos/packages/contracts/src/contractAddressManager.json.rej @@ -0,0 +1,10 @@ +diff a/ethos/packages/contracts/src/contractAddressManager.json b/ethos/packages/contracts/src/contractAddressManager.json (rejected hunks) +@@ -8,7 +8,7 @@ + "args": [] + }, + "prod": { +- "address": "0x0000000000000000000000000000000000000000", ++ "address": "0xe6F811d8aEd56D68D2b8658e01519069470b7436", + "args": [] + } + } diff --git a/ethos/packages/contracts/src/discussion.json b/ethos/packages/contracts/src/discussion.json index 9ae6e03..ede6d42 100644 --- a/ethos/packages/contracts/src/discussion.json +++ b/ethos/packages/contracts/src/discussion.json @@ -1,6 +1,7 @@ { "dev": { "address": "0x90b3972cD88ae14cD16E64251691d41200275505", + "proxyAddress": "0x8B0C4BcA8ED9b26C077A9a3fDc1dF7Ef0f628583", "args": [ "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", @@ -8,7 +9,6 @@ "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" ], - "proxyAddress": "0x8B0C4BcA8ED9b26C077A9a3fDc1dF7Ef0f628583", "proxyArgs": [ "0x90b3972cD88ae14cD16E64251691d41200275505", "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" @@ -30,10 +30,19 @@ ] }, "prod": { - "address": "0x0000000000000000000000000000000000000000", - "proxyAddress": "0x0000000000000000000000000000000000000000", - "args": [], - "proxyArgs": [] + "address": "0x24935729Bbff8d4C6210D769eB058A1733662f42", + "args": [ + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", + "0xe6F811d8aEd56D68D2b8658e01519069470b7436" + ], + "proxyAddress": "0x7FdAC5E0E137b9299107f4e4f77c315c477e74C9", + "proxyArgs": [ + "0x24935729Bbff8d4C6210D769eB058A1733662f42", + "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" + ] }, "local": { "address": "0xF39b37D42ba10C86AF20263DF25c4F245946A13A", diff --git a/ethos/packages/contracts/src/discussion.json.rej b/ethos/packages/contracts/src/discussion.json.rej new file mode 100644 index 0000000..27ceabd --- /dev/null +++ b/ethos/packages/contracts/src/discussion.json.rej @@ -0,0 +1,41 @@ +diff a/ethos/packages/contracts/src/discussion.json b/ethos/packages/contracts/src/discussion.json (rejected hunks) +@@ -1,6 +1,7 @@ + { + "dev": { + "address": "0x90b3972cD88ae14cD16E64251691d41200275505", ++ "proxyAddress": "0x8B0C4BcA8ED9b26C077A9a3fDc1dF7Ef0f628583", + "args": [ + "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", + "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", +@@ -8,7 +9,6 @@ + "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", + "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" + ], +- "proxyAddress": "0x8B0C4BcA8ED9b26C077A9a3fDc1dF7Ef0f628583", + "proxyArgs": [ + "0x90b3972cD88ae14cD16E64251691d41200275505", + "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" +@@ -30,10 +30,19 @@ + ] + }, + "prod": { +- "address": "0x0000000000000000000000000000000000000000", +- "proxyAddress": "0x0000000000000000000000000000000000000000", +- "args": [], +- "proxyArgs": [] ++ "address": "0x24935729Bbff8d4C6210D769eB058A1733662f42", ++ "args": [ ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", ++ "0xe6F811d8aEd56D68D2b8658e01519069470b7436" ++ ], ++ "proxyAddress": "0x7FdAC5E0E137b9299107f4e4f77c315c477e74C9", ++ "proxyArgs": [ ++ "0x24935729Bbff8d4C6210D769eB058A1733662f42", ++ "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" ++ ] + }, + "local": { + "address": "0xF39b37D42ba10C86AF20263DF25c4F245946A13A", diff --git a/ethos/packages/contracts/src/interactionControl.json b/ethos/packages/contracts/src/interactionControl.json index 1d1ee06..a7f79d1 100644 --- a/ethos/packages/contracts/src/interactionControl.json +++ b/ethos/packages/contracts/src/interactionControl.json @@ -14,7 +14,10 @@ ] }, "prod": { - "address": "0x0000000000000000000000000000000000000000", - "args": [] + "address": "0x4FA6432Cd0e2F2B4310C27d80c9726EA41C38b61", + "args": [ + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0xe6F811d8aEd56D68D2b8658e01519069470b7436" + ] } } diff --git a/ethos/packages/contracts/src/interactionControl.json.rej b/ethos/packages/contracts/src/interactionControl.json.rej new file mode 100644 index 0000000..cb57b96 --- /dev/null +++ b/ethos/packages/contracts/src/interactionControl.json.rej @@ -0,0 +1,14 @@ +diff a/ethos/packages/contracts/src/interactionControl.json b/ethos/packages/contracts/src/interactionControl.json (rejected hunks) +@@ -14,7 +14,10 @@ + ] + }, + "prod": { +- "address": "0x0000000000000000000000000000000000000000", +- "args": [] ++ "address": "0x4FA6432Cd0e2F2B4310C27d80c9726EA41C38b61", ++ "args": [ ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0xe6F811d8aEd56D68D2b8658e01519069470b7436" ++ ] + } + } diff --git a/ethos/packages/contracts/src/profile-abi.json b/ethos/packages/contracts/src/profile-abi.json index a63bc52..542c974 100644 --- a/ethos/packages/contracts/src/profile-abi.json +++ b/ethos/packages/contracts/src/profile-abi.json @@ -115,6 +115,11 @@ "name": "FailedCall", "type": "error" }, + { + "inputs": [], + "name": "IndexOutOfBounds", + "type": "error" + }, { "inputs": [ { diff --git a/ethos/packages/contracts/src/profile-abi.json.rej b/ethos/packages/contracts/src/profile-abi.json.rej new file mode 100644 index 0000000..0fb166b --- /dev/null +++ b/ethos/packages/contracts/src/profile-abi.json.rej @@ -0,0 +1,13 @@ +diff a/ethos/packages/contracts/src/profile-abi.json b/ethos/packages/contracts/src/profile-abi.json (rejected hunks) +@@ -115,6 +115,11 @@ + "name": "FailedCall", + "type": "error" + }, ++ { ++ "inputs": [], ++ "name": "IndexOutOfBounds", ++ "type": "error" ++ }, + { + "inputs": [ + { diff --git a/ethos/packages/contracts/src/profile-abi.ts b/ethos/packages/contracts/src/profile-abi.ts index bfc2a8b..42f5feb 100644 --- a/ethos/packages/contracts/src/profile-abi.ts +++ b/ethos/packages/contracts/src/profile-abi.ts @@ -50,6 +50,7 @@ export const profileAbi = [ { inputs: [], name: 'EnforcedPause', type: 'error' }, { inputs: [], name: 'ExpectedPause', type: 'error' }, { inputs: [], name: 'FailedCall', type: 'error' }, + { inputs: [], name: 'IndexOutOfBounds', type: 'error' }, { inputs: [{ internalType: 'uint256', name: 'profileId', type: 'uint256' }], name: 'InsufficientInvites', diff --git a/ethos/packages/contracts/src/profile-abi.ts.rej b/ethos/packages/contracts/src/profile-abi.ts.rej new file mode 100644 index 0000000..ad437dd --- /dev/null +++ b/ethos/packages/contracts/src/profile-abi.ts.rej @@ -0,0 +1,9 @@ +diff a/ethos/packages/contracts/src/profile-abi.ts b/ethos/packages/contracts/src/profile-abi.ts (rejected hunks) +@@ -50,6 +50,7 @@ export const profileAbi = [ + { inputs: [], name: 'EnforcedPause', type: 'error' }, + { inputs: [], name: 'ExpectedPause', type: 'error' }, + { inputs: [], name: 'FailedCall', type: 'error' }, ++ { inputs: [], name: 'IndexOutOfBounds', type: 'error' }, + { + inputs: [{ internalType: 'uint256', name: 'profileId', type: 'uint256' }], + name: 'InsufficientInvites', diff --git a/ethos/packages/contracts/src/profile.json b/ethos/packages/contracts/src/profile.json index a96a5d9..cc175d2 100644 --- a/ethos/packages/contracts/src/profile.json +++ b/ethos/packages/contracts/src/profile.json @@ -1,6 +1,7 @@ { "dev": { "address": "0xb0462659a44cdcceea697a0794a9A4A88F20D884", + "proxyAddress": "0xde09b9dF112F01b4ecb71bD3b2D4eA622c8C21f2", "args": [ "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", @@ -8,7 +9,6 @@ "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" ], - "proxyAddress": "0xde09b9dF112F01b4ecb71bD3b2D4eA622c8C21f2", "proxyArgs": [ "0xb0462659a44cdcceea697a0794a9A4A88F20D884", "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" @@ -30,9 +30,18 @@ ] }, "prod": { - "address": "0x0000000000000000000000000000000000000000", - "proxyAddress": "0x0000000000000000000000000000000000000000", - "args": [], - "proxyArgs": [] + "address": "0xD31D6486A4CcfC171B7e498fE69fF51FEDA1E3Aa", + "args": [ + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", + "0xe6F811d8aEd56D68D2b8658e01519069470b7436" + ], + "proxyAddress": "0x1d073A9CB370dD94496F921C17B591CAC8126C6F", + "proxyArgs": [ + "0xD31D6486A4CcfC171B7e498fE69fF51FEDA1E3Aa", + "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" + ] } } diff --git a/ethos/packages/contracts/src/profile.json.rej b/ethos/packages/contracts/src/profile.json.rej new file mode 100644 index 0000000..29b7c1d --- /dev/null +++ b/ethos/packages/contracts/src/profile.json.rej @@ -0,0 +1,40 @@ +diff a/ethos/packages/contracts/src/profile.json b/ethos/packages/contracts/src/profile.json (rejected hunks) +@@ -1,6 +1,7 @@ + { + "dev": { + "address": "0xb0462659a44cdcceea697a0794a9A4A88F20D884", ++ "proxyAddress": "0xde09b9dF112F01b4ecb71bD3b2D4eA622c8C21f2", + "args": [ + "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", + "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", +@@ -8,7 +9,6 @@ + "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", + "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" + ], +- "proxyAddress": "0xde09b9dF112F01b4ecb71bD3b2D4eA622c8C21f2", + "proxyArgs": [ + "0xb0462659a44cdcceea697a0794a9A4A88F20D884", + "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" +@@ -30,9 +30,18 @@ + ] + }, + "prod": { +- "address": "0x0000000000000000000000000000000000000000", +- "proxyAddress": "0x0000000000000000000000000000000000000000", +- "args": [], +- "proxyArgs": [] ++ "address": "0xD31D6486A4CcfC171B7e498fE69fF51FEDA1E3Aa", ++ "args": [ ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", ++ "0xe6F811d8aEd56D68D2b8658e01519069470b7436" ++ ], ++ "proxyAddress": "0x1d073A9CB370dD94496F921C17B591CAC8126C6F", ++ "proxyArgs": [ ++ "0xD31D6486A4CcfC171B7e498fE69fF51FEDA1E3Aa", ++ "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" ++ ] + } + } diff --git a/ethos/packages/contracts/src/reputationMarket.json b/ethos/packages/contracts/src/reputationMarket.json index 8d937b9..9186020 100644 --- a/ethos/packages/contracts/src/reputationMarket.json +++ b/ethos/packages/contracts/src/reputationMarket.json @@ -1,6 +1,7 @@ { "dev": { "address": "0x2E44F49149fC23fE07C35F9B0F163759c905c6bf", + "proxyAddress": "0xc120cF967ea59D4bca4894365CcFE5752ccb1FC4", "args": [ "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", @@ -8,7 +9,6 @@ "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" ], - "proxyAddress": "0xc120cF967ea59D4bca4894365CcFE5752ccb1FC4", "proxyArgs": [ "0x2E44F49149fC23fE07C35F9B0F163759c905c6bf", "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" @@ -30,9 +30,18 @@ ] }, "prod": { - "address": "0x0000000000000000000000000000000000000000", - "proxyAddress": "0x0000000000000000000000000000000000000000", - "args": [], - "proxyArgs": [] + "address": "0x7C7AD8d1A255E1aeF9d8f77ee13F107D49Cdfbe0", + "args": [ + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", + "0xe6F811d8aEd56D68D2b8658e01519069470b7436" + ], + "proxyAddress": "0x78C322314013B5658536929E823BE260f9c536a1", + "proxyArgs": [ + "0x7C7AD8d1A255E1aeF9d8f77ee13F107D49Cdfbe0", + "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" + ] } } diff --git a/ethos/packages/contracts/src/reputationMarket.json.rej b/ethos/packages/contracts/src/reputationMarket.json.rej new file mode 100644 index 0000000..316c7a3 --- /dev/null +++ b/ethos/packages/contracts/src/reputationMarket.json.rej @@ -0,0 +1,40 @@ +diff a/ethos/packages/contracts/src/reputationMarket.json b/ethos/packages/contracts/src/reputationMarket.json (rejected hunks) +@@ -1,6 +1,7 @@ + { + "dev": { + "address": "0x2E44F49149fC23fE07C35F9B0F163759c905c6bf", ++ "proxyAddress": "0xc120cF967ea59D4bca4894365CcFE5752ccb1FC4", + "args": [ + "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", + "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", +@@ -8,7 +9,6 @@ + "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", + "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" + ], +- "proxyAddress": "0xc120cF967ea59D4bca4894365CcFE5752ccb1FC4", + "proxyArgs": [ + "0x2E44F49149fC23fE07C35F9B0F163759c905c6bf", + "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" +@@ -30,9 +30,18 @@ + ] + }, + "prod": { +- "address": "0x0000000000000000000000000000000000000000", +- "proxyAddress": "0x0000000000000000000000000000000000000000", +- "args": [], +- "proxyArgs": [] ++ "address": "0x7C7AD8d1A255E1aeF9d8f77ee13F107D49Cdfbe0", ++ "args": [ ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", ++ "0xe6F811d8aEd56D68D2b8658e01519069470b7436" ++ ], ++ "proxyAddress": "0x78C322314013B5658536929E823BE260f9c536a1", ++ "proxyArgs": [ ++ "0x7C7AD8d1A255E1aeF9d8f77ee13F107D49Cdfbe0", ++ "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" ++ ] + } + } diff --git a/ethos/packages/contracts/src/review.json b/ethos/packages/contracts/src/review.json index 1f8dac9..c0c3de7 100644 --- a/ethos/packages/contracts/src/review.json +++ b/ethos/packages/contracts/src/review.json @@ -1,6 +1,7 @@ { "dev": { "address": "0x342f281138a6b09544AB75C09afA6014eb24B277", + "proxyAddress": "0x8BD2b0a5796C5a334Cc21E75b584Be8760e29c14", "args": [ "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", @@ -8,7 +9,6 @@ "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" ], - "proxyAddress": "0x8BD2b0a5796C5a334Cc21E75b584Be8760e29c14", "proxyArgs": [ "0x342f281138a6b09544AB75C09afA6014eb24B277", "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" @@ -30,9 +30,18 @@ ] }, "prod": { - "address": "0x0000000000000000000000000000000000000000", - "proxyAddress": "0x0000000000000000000000000000000000000000", - "args": [], - "proxyArgs": [] + "address": "0x6af8b0FC6998cC4801b9979503dF557035E0AF03", + "args": [ + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", + "0xe6F811d8aEd56D68D2b8658e01519069470b7436" + ], + "proxyAddress": "0x3Dc2154b35E737DAde925893E23068FbB3BB31bD", + "proxyArgs": [ + "0x6af8b0FC6998cC4801b9979503dF557035E0AF03", + "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" + ] } } diff --git a/ethos/packages/contracts/src/review.json.rej b/ethos/packages/contracts/src/review.json.rej new file mode 100644 index 0000000..48c5fab --- /dev/null +++ b/ethos/packages/contracts/src/review.json.rej @@ -0,0 +1,40 @@ +diff a/ethos/packages/contracts/src/review.json b/ethos/packages/contracts/src/review.json (rejected hunks) +@@ -1,6 +1,7 @@ + { + "dev": { + "address": "0x342f281138a6b09544AB75C09afA6014eb24B277", ++ "proxyAddress": "0x8BD2b0a5796C5a334Cc21E75b584Be8760e29c14", + "args": [ + "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", + "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", +@@ -8,7 +9,6 @@ + "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", + "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" + ], +- "proxyAddress": "0x8BD2b0a5796C5a334Cc21E75b584Be8760e29c14", + "proxyArgs": [ + "0x342f281138a6b09544AB75C09afA6014eb24B277", + "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" +@@ -30,9 +30,18 @@ + ] + }, + "prod": { +- "address": "0x0000000000000000000000000000000000000000", +- "proxyAddress": "0x0000000000000000000000000000000000000000", +- "args": [], +- "proxyArgs": [] ++ "address": "0x6af8b0FC6998cC4801b9979503dF557035E0AF03", ++ "args": [ ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", ++ "0xe6F811d8aEd56D68D2b8658e01519069470b7436" ++ ], ++ "proxyAddress": "0x3Dc2154b35E737DAde925893E23068FbB3BB31bD", ++ "proxyArgs": [ ++ "0x6af8b0FC6998cC4801b9979503dF557035E0AF03", ++ "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" ++ ] + } + } diff --git a/ethos/packages/contracts/src/signatureVerifier.json b/ethos/packages/contracts/src/signatureVerifier.json index c8d479f..03e1b8d 100644 --- a/ethos/packages/contracts/src/signatureVerifier.json +++ b/ethos/packages/contracts/src/signatureVerifier.json @@ -8,7 +8,7 @@ "args": [] }, "prod": { - "address": "0x0000000000000000000000000000000000000000", + "address": "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", "args": [] } } diff --git a/ethos/packages/contracts/src/signatureVerifier.json.rej b/ethos/packages/contracts/src/signatureVerifier.json.rej new file mode 100644 index 0000000..54e0ffb --- /dev/null +++ b/ethos/packages/contracts/src/signatureVerifier.json.rej @@ -0,0 +1,10 @@ +diff a/ethos/packages/contracts/src/signatureVerifier.json b/ethos/packages/contracts/src/signatureVerifier.json (rejected hunks) +@@ -8,7 +8,7 @@ + "args": [] + }, + "prod": { +- "address": "0x0000000000000000000000000000000000000000", ++ "address": "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", + "args": [] + } + } diff --git a/ethos/packages/contracts/src/vote.json b/ethos/packages/contracts/src/vote.json index 2763c9e..eceefdb 100644 --- a/ethos/packages/contracts/src/vote.json +++ b/ethos/packages/contracts/src/vote.json @@ -1,6 +1,7 @@ { "dev": { "address": "0x93a9e4AeA25f0D0DBCD7d50b4886BdA1e4d2ba71", + "proxyAddress": "0x1354994b848A33f8Ce5A17f1ad6C150e0D37A25C", "args": [ "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", @@ -8,7 +9,6 @@ "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" ], - "proxyAddress": "0x1354994b848A33f8Ce5A17f1ad6C150e0D37A25C", "proxyArgs": [ "0x93a9e4AeA25f0D0DBCD7d50b4886BdA1e4d2ba71", "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" @@ -30,9 +30,18 @@ ] }, "prod": { - "address": "0x0000000000000000000000000000000000000000", - "proxyAddress": "0x0000000000000000000000000000000000000000", - "args": [], - "proxyArgs": [] + "address": "0x405f85B6670f205252f14064c70B9A980a2c8429", + "args": [ + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", + "0xe6F811d8aEd56D68D2b8658e01519069470b7436" + ], + "proxyAddress": "0x6f75BFb43A28410c03Aa2aF4dC0196fA8049D136", + "proxyArgs": [ + "0x405f85B6670f205252f14064c70B9A980a2c8429", + "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" + ] } } diff --git a/ethos/packages/contracts/src/vote.json.rej b/ethos/packages/contracts/src/vote.json.rej new file mode 100644 index 0000000..d951734 --- /dev/null +++ b/ethos/packages/contracts/src/vote.json.rej @@ -0,0 +1,40 @@ +diff a/ethos/packages/contracts/src/vote.json b/ethos/packages/contracts/src/vote.json (rejected hunks) +@@ -1,6 +1,7 @@ + { + "dev": { + "address": "0x93a9e4AeA25f0D0DBCD7d50b4886BdA1e4d2ba71", ++ "proxyAddress": "0x1354994b848A33f8Ce5A17f1ad6C150e0D37A25C", + "args": [ + "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", + "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", +@@ -8,7 +9,6 @@ + "0x683FE9E5636AC427Ce06Cb3D3BcF8b3492f4bAde", + "0x43DF3FF5c201F4B7CE48B753990C8f08E5103F40" + ], +- "proxyAddress": "0x1354994b848A33f8Ce5A17f1ad6C150e0D37A25C", + "proxyArgs": [ + "0x93a9e4AeA25f0D0DBCD7d50b4886BdA1e4d2ba71", + "0x1459457a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f40" +@@ -30,9 +30,18 @@ + ] + }, + "prod": { +- "address": "0x0000000000000000000000000000000000000000", +- "proxyAddress": "0x0000000000000000000000000000000000000000", +- "args": [], +- "proxyArgs": [] ++ "address": "0x405f85B6670f205252f14064c70B9A980a2c8429", ++ "args": [ ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", ++ "0xe6F811d8aEd56D68D2b8658e01519069470b7436" ++ ], ++ "proxyAddress": "0x6f75BFb43A28410c03Aa2aF4dC0196fA8049D136", ++ "proxyArgs": [ ++ "0x405f85B6670f205252f14064c70B9A980a2c8429", ++ "0x1459457a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b7436" ++ ] + } + } diff --git a/ethos/packages/contracts/src/vouch-abi.json b/ethos/packages/contracts/src/vouch-abi.json index c43ddb8..7dbf1c7 100644 --- a/ethos/packages/contracts/src/vouch-abi.json +++ b/ethos/packages/contracts/src/vouch-abi.json @@ -111,11 +111,6 @@ "name": "ERC1967NonPayable", "type": "error" }, - { - "inputs": [], - "name": "ETHTransferFailed", - "type": "error" - }, { "inputs": [], "name": "EnforcedPause", @@ -168,6 +163,28 @@ "name": "InsufficientRewardsBalance", "type": "error" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "attestationHash", + "type": "bytes32" + } + ], + "name": "InvalidAttestationHash", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "attestationHash", + "type": "bytes32" + } + ], + "name": "InvalidAttestationHash", + "type": "error" + }, { "inputs": [ { @@ -263,6 +280,38 @@ "name": "NotSlasher", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "vouchId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "authorProfileId", + "type": "uint256" + } + ], + "name": "PendingSlash", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "vouchId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "authorProfileId", + "type": "uint256" + } + ], + "name": "PendingSlash", + "type": "error" + }, { "inputs": [ { @@ -327,22 +376,6 @@ "name": "VouchNotFound", "type": "error" }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "string", - "name": "message", - "type": "string" - } - ], - "name": "WithdrawalFailed", - "type": "error" - }, { "inputs": [ { @@ -435,6 +468,44 @@ "name": "ExitFeeBasisPointsUpdated", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "authorProfileId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isFrozen", + "type": "bool" + } + ], + "name": "Frozen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "authorProfileId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isFrozen", + "type": "bool" + } + ], + "name": "Frozen", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -633,6 +704,18 @@ "name": "subjectProfileId", "type": "uint256" }, + { + "indexed": false, + "internalType": "address", + "name": "subjectAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "attestationHash", + "type": "bytes32" + }, { "indexed": false, "internalType": "uint256", @@ -677,6 +760,18 @@ "name": "subjectProfileId", "type": "uint256" }, + { + "indexed": false, + "internalType": "address", + "name": "subjectAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "attestationHash", + "type": "bytes32" + }, { "indexed": false, "internalType": "uint256", @@ -714,6 +809,18 @@ "name": "subjectProfileId", "type": "uint256" }, + { + "indexed": false, + "internalType": "address", + "name": "subjectAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "attestationHash", + "type": "bytes32" + }, { "indexed": false, "internalType": "uint256", @@ -840,6 +947,38 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "authorProfileId", + "type": "uint256" + } + ], + "name": "freeze", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "frozenAuthors", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -860,6 +999,32 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "attestationHash", + "type": "bytes32" + } + ], + "name": "claimRewardsByAttestation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "attestationHash", + "type": "bytes32" + } + ], + "name": "claimRewardsByAttestation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "configuredMinimumVouchAmount", @@ -951,6 +1116,38 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "authorProfileId", + "type": "uint256" + } + ], + "name": "freeze", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "frozenAuthors", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1080,6 +1277,16 @@ "internalType": "uint256", "name": "vouchId", "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "attestationHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "subjectAddress", + "type": "address" } ], "name": "increaseVouch", @@ -1145,6 +1352,44 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "rewardsByAddress", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "rewardsByAttestationHash", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1268,13 +1513,51 @@ }, { "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "rewardsByAddress", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "rewardsByAttestationHash", + "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], - "name": "rewards", + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "rewardsByProfileId", "outputs": [ { "internalType": "uint256", @@ -1462,6 +1745,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "authorProfileId", + "type": "uint256" + } + ], + "name": "unfreeze", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "unhealthyResponsePeriod", @@ -1604,6 +1900,29 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "attestationHash", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "comment", + "type": "string" + }, + { + "internalType": "string", + "name": "metadata", + "type": "string" + } + ], + "name": "vouchByAttestation", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { @@ -1813,6 +2132,29 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "attestationHash", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "comment", + "type": "string" + }, + { + "internalType": "string", + "name": "metadata", + "type": "string" + } + ], + "name": "vouchByAttestation", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { diff --git a/ethos/packages/contracts/src/vouch-abi.json.rej b/ethos/packages/contracts/src/vouch-abi.json.rej new file mode 100644 index 0000000..2585b41 --- /dev/null +++ b/ethos/packages/contracts/src/vouch-abi.json.rej @@ -0,0 +1,101 @@ +diff a/ethos/packages/contracts/src/vouch-abi.json b/ethos/packages/contracts/src/vouch-abi.json (rejected hunks) +@@ -111,11 +111,6 @@ + "name": "ERC1967NonPayable", + "type": "error" + }, +- { +- "inputs": [], +- "name": "ETHTransferFailed", +- "type": "error" +- }, + { + "inputs": [], + "name": "EnforcedPause", +@@ -327,22 +349,6 @@ + "name": "VouchNotFound", + "type": "error" + }, +- { +- "inputs": [ +- { +- "internalType": "bytes", +- "name": "data", +- "type": "bytes" +- }, +- { +- "internalType": "string", +- "name": "message", +- "type": "string" +- } +- ], +- "name": "WithdrawalFailed", +- "type": "error" +- }, + { + "inputs": [ + { +@@ -714,6 +751,18 @@ + "name": "subjectProfileId", + "type": "uint256" + }, ++ { ++ "indexed": false, ++ "internalType": "address", ++ "name": "subjectAddress", ++ "type": "address" ++ }, ++ { ++ "indexed": false, ++ "internalType": "bytes32", ++ "name": "attestationHash", ++ "type": "bytes32" ++ }, + { + "indexed": false, + "internalType": "uint256", +@@ -1080,6 +1174,16 @@ + "internalType": "uint256", + "name": "vouchId", + "type": "uint256" ++ }, ++ { ++ "internalType": "bytes32", ++ "name": "attestationHash", ++ "type": "bytes32" ++ }, ++ { ++ "internalType": "address", ++ "name": "subjectAddress", ++ "type": "address" + } + ], + "name": "increaseVouch", +@@ -1274,7 +1416,7 @@ + "type": "uint256" + } + ], +- "name": "rewards", ++ "name": "rewardsByProfileId", + "outputs": [ + { + "internalType": "uint256", +@@ -1462,6 +1604,19 @@ + "stateMutability": "view", + "type": "function" + }, ++ { ++ "inputs": [ ++ { ++ "internalType": "uint256", ++ "name": "authorProfileId", ++ "type": "uint256" ++ } ++ ], ++ "name": "unfreeze", ++ "outputs": [], ++ "stateMutability": "nonpayable", ++ "type": "function" ++ }, + { + "inputs": [], + "name": "unhealthyResponsePeriod", diff --git a/ethos/packages/contracts/src/vouch-abi.ts b/ethos/packages/contracts/src/vouch-abi.ts index d173cda..72d7b7f 100644 --- a/ethos/packages/contracts/src/vouch-abi.ts +++ b/ethos/packages/contracts/src/vouch-abi.ts @@ -52,7 +52,6 @@ export const vouchAbi = [ type: 'error', }, { inputs: [], name: 'ERC1967NonPayable', type: 'error' }, - { inputs: [], name: 'ETHTransferFailed', type: 'error' }, { inputs: [], name: 'EnforcedPause', type: 'error' }, { inputs: [], name: 'ExpectedPause', type: 'error' }, { inputs: [], name: 'FailedCall', type: 'error' }, @@ -71,6 +70,11 @@ export const vouchAbi = [ }, { inputs: [], name: 'InsufficientProtocolFeeBalance', type: 'error' }, { inputs: [], name: 'InsufficientRewardsBalance', type: 'error' }, + { + inputs: [{ internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }], + name: 'InvalidAttestationHash', + type: 'error', + }, { inputs: [{ internalType: 'uint256', name: 'ethosProfileId', type: 'uint256' }], name: 'InvalidEthosProfileForVouch', @@ -108,6 +112,14 @@ export const vouchAbi = [ }, { inputs: [], name: 'NotInitializing', type: 'error' }, { inputs: [], name: 'NotSlasher', type: 'error' }, + { + inputs: [ + { internalType: 'uint256', name: 'vouchId', type: 'uint256' }, + { internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }, + ], + name: 'PendingSlash', + type: 'error', + }, { inputs: [{ internalType: 'address', name: 'userAddress', type: 'address' }], name: 'ProfileNotFoundForAddress', @@ -134,14 +146,6 @@ export const vouchAbi = [ name: 'VouchNotFound', type: 'error', }, - { - inputs: [ - { internalType: 'bytes', name: 'data', type: 'bytes' }, - { internalType: 'string', name: 'message', type: 'string' }, - ], - name: 'WithdrawalFailed', - type: 'error', - }, { inputs: [ { internalType: 'uint256', name: 'vouchId', type: 'uint256' }, @@ -207,6 +211,15 @@ export const vouchAbi = [ name: 'ExitFeeBasisPointsUpdated', type: 'event', }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }, + { indexed: false, internalType: 'bool', name: 'isFrozen', type: 'bool' }, + ], + name: 'Frozen', + type: 'event', + }, { anonymous: false, inputs: [{ indexed: false, internalType: 'uint64', name: 'version', type: 'uint64' }], @@ -306,6 +319,8 @@ export const vouchAbi = [ { indexed: true, internalType: 'uint256', name: 'vouchId', type: 'uint256' }, { indexed: true, internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }, { indexed: true, internalType: 'uint256', name: 'subjectProfileId', type: 'uint256' }, + { indexed: false, internalType: 'address', name: 'subjectAddress', type: 'address' }, + { indexed: false, internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }, { indexed: false, internalType: 'uint256', name: 'amountStaked', type: 'uint256' }, { indexed: false, internalType: 'uint256', name: 'amountDeposited', type: 'uint256' }, ], @@ -318,6 +333,8 @@ export const vouchAbi = [ { indexed: true, internalType: 'uint256', name: 'vouchId', type: 'uint256' }, { indexed: true, internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }, { indexed: true, internalType: 'uint256', name: 'subjectProfileId', type: 'uint256' }, + { indexed: false, internalType: 'address', name: 'subjectAddress', type: 'address' }, + { indexed: false, internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }, { indexed: false, internalType: 'uint256', name: 'amountStaked', type: 'uint256' }, { indexed: false, internalType: 'uint256', name: 'amountDeposited', type: 'uint256' }, ], @@ -396,6 +413,20 @@ export const vouchAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [{ internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }], + name: 'claimRewardsByAttestation', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }], + name: 'claimRewardsByAttestation', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [], name: 'configuredMinimumVouchAmount', @@ -445,6 +476,34 @@ export const vouchAbi = [ stateMutability: 'view', type: 'function', }, + { + inputs: [{ internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }], + name: 'freeze', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'frozenAuthors', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }], + name: 'freeze', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'frozenAuthors', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, { inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }], name: 'getRoleAdmin', @@ -497,7 +556,11 @@ export const vouchAbi = [ type: 'function', }, { - inputs: [{ internalType: 'uint256', name: 'vouchId', type: 'uint256' }], + inputs: [ + { internalType: 'uint256', name: 'vouchId', type: 'uint256' }, + { internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }, + { internalType: 'address', name: 'subjectAddress', type: 'address' }, + ], name: 'increaseVouch', outputs: [], stateMutability: 'payable', @@ -584,9 +647,23 @@ export const vouchAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'rewardsByAddress', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + name: 'rewardsByAttestationHash', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, { inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - name: 'rewards', + name: 'rewardsByProfileId', outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], stateMutability: 'view', type: 'function', @@ -676,6 +753,13 @@ export const vouchAbi = [ stateMutability: 'view', type: 'function', }, + { + inputs: [{ internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }], + name: 'unfreeze', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [], name: 'unhealthyResponsePeriod', @@ -835,6 +919,28 @@ export const vouchAbi = [ stateMutability: 'payable', type: 'function', }, + { + inputs: [ + { internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }, + { internalType: 'string', name: 'comment', type: 'string' }, + { internalType: 'string', name: 'metadata', type: 'string' }, + ], + name: 'vouchByAttestation', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }, + { internalType: 'string', name: 'comment', type: 'string' }, + { internalType: 'string', name: 'metadata', type: 'string' }, + ], + name: 'vouchByAttestation', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, { inputs: [ { internalType: 'uint256', name: 'subjectProfileId', type: 'uint256' }, diff --git a/ethos/packages/contracts/src/vouch-abi.ts.rej b/ethos/packages/contracts/src/vouch-abi.ts.rej new file mode 100644 index 0000000..6e8aaee --- /dev/null +++ b/ethos/packages/contracts/src/vouch-abi.ts.rej @@ -0,0 +1,137 @@ +diff a/ethos/packages/contracts/src/vouch-abi.ts b/ethos/packages/contracts/src/vouch-abi.ts (rejected hunks) +@@ -52,7 +52,6 @@ export const vouchAbi = [ + type: 'error', + }, + { inputs: [], name: 'ERC1967NonPayable', type: 'error' }, +- { inputs: [], name: 'ETHTransferFailed', type: 'error' }, + { inputs: [], name: 'EnforcedPause', type: 'error' }, + { inputs: [], name: 'ExpectedPause', type: 'error' }, + { inputs: [], name: 'FailedCall', type: 'error' }, +@@ -71,6 +70,11 @@ export const vouchAbi = [ + }, + { inputs: [], name: 'InsufficientProtocolFeeBalance', type: 'error' }, + { inputs: [], name: 'InsufficientRewardsBalance', type: 'error' }, ++ { ++ inputs: [{ internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }], ++ name: 'InvalidAttestationHash', ++ type: 'error', ++ }, + { + inputs: [{ internalType: 'uint256', name: 'ethosProfileId', type: 'uint256' }], + name: 'InvalidEthosProfileForVouch', +@@ -108,6 +112,14 @@ export const vouchAbi = [ + }, + { inputs: [], name: 'NotInitializing', type: 'error' }, + { inputs: [], name: 'NotSlasher', type: 'error' }, ++ { ++ inputs: [ ++ { internalType: 'uint256', name: 'vouchId', type: 'uint256' }, ++ { internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }, ++ ], ++ name: 'PendingSlash', ++ type: 'error', ++ }, + { + inputs: [{ internalType: 'address', name: 'userAddress', type: 'address' }], + name: 'ProfileNotFoundForAddress', +@@ -134,14 +146,6 @@ export const vouchAbi = [ + name: 'VouchNotFound', + type: 'error', + }, +- { +- inputs: [ +- { internalType: 'bytes', name: 'data', type: 'bytes' }, +- { internalType: 'string', name: 'message', type: 'string' }, +- ], +- name: 'WithdrawalFailed', +- type: 'error', +- }, + { + inputs: [ + { internalType: 'uint256', name: 'vouchId', type: 'uint256' }, +@@ -207,6 +211,15 @@ export const vouchAbi = [ + name: 'ExitFeeBasisPointsUpdated', + type: 'event', + }, ++ { ++ anonymous: false, ++ inputs: [ ++ { indexed: true, internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }, ++ { indexed: false, internalType: 'bool', name: 'isFrozen', type: 'bool' }, ++ ], ++ name: 'Frozen', ++ type: 'event', ++ }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'uint64', name: 'version', type: 'uint64' }], +@@ -306,6 +319,8 @@ export const vouchAbi = [ + { indexed: true, internalType: 'uint256', name: 'vouchId', type: 'uint256' }, + { indexed: true, internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }, + { indexed: true, internalType: 'uint256', name: 'subjectProfileId', type: 'uint256' }, ++ { indexed: false, internalType: 'address', name: 'subjectAddress', type: 'address' }, ++ { indexed: false, internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }, + { indexed: false, internalType: 'uint256', name: 'amountStaked', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'amountDeposited', type: 'uint256' }, + ], +@@ -318,6 +333,8 @@ export const vouchAbi = [ + { indexed: true, internalType: 'uint256', name: 'vouchId', type: 'uint256' }, + { indexed: true, internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }, + { indexed: true, internalType: 'uint256', name: 'subjectProfileId', type: 'uint256' }, ++ { indexed: false, internalType: 'address', name: 'subjectAddress', type: 'address' }, ++ { indexed: false, internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }, + { indexed: false, internalType: 'uint256', name: 'amountStaked', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'amountDeposited', type: 'uint256' }, + ], +@@ -497,7 +535,11 @@ export const vouchAbi = [ + type: 'function', + }, + { +- inputs: [{ internalType: 'uint256', name: 'vouchId', type: 'uint256' }], ++ inputs: [ ++ { internalType: 'uint256', name: 'vouchId', type: 'uint256' }, ++ { internalType: 'bytes32', name: 'attestationHash', type: 'bytes32' }, ++ { internalType: 'address', name: 'subjectAddress', type: 'address' }, ++ ], + name: 'increaseVouch', + outputs: [], + stateMutability: 'payable', +@@ -584,9 +626,23 @@ export const vouchAbi = [ + stateMutability: 'nonpayable', + type: 'function', + }, ++ { ++ inputs: [{ internalType: 'address', name: '', type: 'address' }], ++ name: 'rewardsByAddress', ++ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], ++ stateMutability: 'view', ++ type: 'function', ++ }, ++ { ++ inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], ++ name: 'rewardsByAttestationHash', ++ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], ++ stateMutability: 'view', ++ type: 'function', ++ }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], +- name: 'rewards', ++ name: 'rewardsByProfileId', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', +@@ -676,6 +732,13 @@ export const vouchAbi = [ + stateMutability: 'view', + type: 'function', + }, ++ { ++ inputs: [{ internalType: 'uint256', name: 'authorProfileId', type: 'uint256' }], ++ name: 'unfreeze', ++ outputs: [], ++ stateMutability: 'nonpayable', ++ type: 'function', ++ }, + { + inputs: [], + name: 'unhealthyResponsePeriod', diff --git a/ethos/packages/contracts/src/vouch.json b/ethos/packages/contracts/src/vouch.json index 85e173f..8e36e66 100644 --- a/ethos/packages/contracts/src/vouch.json +++ b/ethos/packages/contracts/src/vouch.json @@ -1,6 +1,6 @@ { "dev": { - "address": "0x71B97034776E4c8E81C76e23B92aed0A68198a42", + "address": "0x261c356414b6aF52677F82963252Ac98Af66840A", "args": [ "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", @@ -13,9 +13,9 @@ "0", "0" ], - "proxyAddress": "0xb434Ede53dF1119Ac9F749F0bCE9551f10DAC81a", + "proxyAddress": "0x332067EBDf53Ab0C3284FCcB836d3E020F7A7191", "proxyArgs": [ - "0x71B97034776E4c8E81C76e23B92aed0A68198a42", + "0x261c356414b6aF52677F82963252Ac98Af66840A", "0x3986de6a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f400000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ] }, @@ -36,9 +36,23 @@ ] }, "prod": { - "address": "0x0000000000000000000000000000000000000000", - "proxyAddress": "0x0000000000000000000000000000000000000000", - "args": [], - "proxyArgs": [] + "address": "0x5a914935c56A9F62417726064A9A9aeD5d7aE9c3", + "proxyAddress": "0x879989C287e2a98A31f04F94eEa408f3e1b99d18", + "args": [ + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", + "0xe6F811d8aEd56D68D2b8658e01519069470b7436", + "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", + "0", + "0", + "0", + "0" + ], + "proxyArgs": [ + "0x5a914935c56A9F62417726064A9A9aeD5d7aE9c3", + "0x3986de6a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b74360000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ] } } diff --git a/ethos/packages/contracts/src/vouch.json.rej b/ethos/packages/contracts/src/vouch.json.rej new file mode 100644 index 0000000..43cd0e1 --- /dev/null +++ b/ethos/packages/contracts/src/vouch.json.rej @@ -0,0 +1,49 @@ +diff a/ethos/packages/contracts/src/vouch.json b/ethos/packages/contracts/src/vouch.json (rejected hunks) +@@ -1,6 +1,6 @@ + { + "dev": { +- "address": "0x71B97034776E4c8E81C76e23B92aed0A68198a42", ++ "address": "0x261c356414b6aF52677F82963252Ac98Af66840A", + "args": [ + "0x7568033fa1C69BB90bCD8e28432E243Ffb1C65b4", + "0x61eA43eB8d1bDA72646F141bD3af08bd0f0d362d", +@@ -13,9 +13,9 @@ + "0", + "0" + ], +- "proxyAddress": "0xb434Ede53dF1119Ac9F749F0bCE9551f10DAC81a", ++ "proxyAddress": "0x332067EBDf53Ab0C3284FCcB836d3E020F7A7191", + "proxyArgs": [ +- "0x71B97034776E4c8E81C76e23B92aed0A68198a42", ++ "0x261c356414b6aF52677F82963252Ac98Af66840A", + "0x3986de6a0000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b400000000000000000000000061ea43eb8d1bda72646f141bd3af08bd0f0d362d000000000000000000000000fee5e1fd4a32012b54d479af03a52616d9223a95000000000000000000000000683fe9e5636ac427ce06cb3d3bcf8b3492f4bade00000000000000000000000043df3ff5c201f4b7ce48b753990c8f08e5103f400000000000000000000000007568033fa1c69bb90bcd8e28432e243ffb1c65b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ] + }, +@@ -36,9 +36,23 @@ + ] + }, + "prod": { +- "address": "0x0000000000000000000000000000000000000000", +- "proxyAddress": "0x0000000000000000000000000000000000000000", +- "args": [], +- "proxyArgs": [] ++ "address": "0x5a914935c56A9F62417726064A9A9aeD5d7aE9c3", ++ "proxyAddress": "0x879989C287e2a98A31f04F94eEa408f3e1b99d18", ++ "args": [ ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0x0992bb163B8F5615258defe98fB8Bd63327b5B10", ++ "0xe6F811d8aEd56D68D2b8658e01519069470b7436", ++ "0x7D1D7483be9DF982D680eFe16C13Cdc650Cd3034", ++ "0", ++ "0", ++ "0", ++ "0" ++ ], ++ "proxyArgs": [ ++ "0x5a914935c56A9F62417726064A9A9aeD5d7aE9c3", ++ "0x3986de6a0000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000992bb163b8f5615258defe98fb8bd63327b5b10000000000000000000000000e6f811d8aed56d68d2b8658e01519069470b74360000000000000000000000007d1d7483be9df982d680efe16c13cdc650cd30340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ++ ] + } + } diff --git a/ethos/packages/contracts/test/reputationMarket/rep.market.test.ts b/ethos/packages/contracts/test/reputationMarket/rep.market.test.ts index 5198b31..c809477 100644 --- a/ethos/packages/contracts/test/reputationMarket/rep.market.test.ts +++ b/ethos/packages/contracts/test/reputationMarket/rep.market.test.ts @@ -424,6 +424,46 @@ describe('ReputationMarket', () => { ).to.not.be.reverted; }); }); + + it('should revert with BuySlippageLimitExceeded when insufficient funds for minimum votes', async () => { + // Try to buy votes with insufficient funds + const buyAmount = DEFAULT.buyAmount / 2n; // Insufficient funds + const votesToBuy = 100n; + const minVotesToBuy = 90n; // 90% of requested votes + + // Calculate actual cost for these votes + const { totalCostIncludingFees } = await reputationMarket.simulateBuy( + DEFAULT.profileId, + DEFAULT.isPositive, + minVotesToBuy, + ); + + // Attempt purchase with insufficient funds + await expect( + userA.buyVotes({ + buyAmount, + votesToBuy, + minVotesToBuy, + }), + ) + .to.be.revertedWithCustomError(reputationMarket, 'BuySlippageLimitExceeded') + .withArgs(totalCostIncludingFees, buyAmount); + }); + + it('should revert with InsufficientFunds when max equals min votes and insufficient funds', async () => { + // Try to buy votes with insufficient funds + const buyAmount = DEFAULT.buyAmount / 2n; // Insufficient funds + const votesToBuy = 100n; + + // Attempt purchase with insufficient funds and equal max/min votes + await expect( + userA.buyVotes({ + buyAmount, + votesToBuy, + minVotesToBuy: votesToBuy, // Set min equal to max + }), + ).to.be.revertedWithCustomError(reputationMarket, 'InsufficientFunds'); + }); }); describe('Simulations', () => { diff --git a/ethos/packages/contracts/test/reputationMarket/rep.market.test.ts.rej b/ethos/packages/contracts/test/reputationMarket/rep.market.test.ts.rej new file mode 100644 index 0000000..9612263 --- /dev/null +++ b/ethos/packages/contracts/test/reputationMarket/rep.market.test.ts.rej @@ -0,0 +1,48 @@ +diff a/ethos/packages/contracts/test/reputationMarket/rep.market.test.ts b/ethos/packages/contracts/test/reputationMarket/rep.market.test.ts (rejected hunks) +@@ -424,6 +424,46 @@ describe('ReputationMarket', () => { + ).to.not.be.reverted; + }); + }); ++ ++ it('should revert with BuySlippageLimitExceeded when insufficient funds for minimum votes', async () => { ++ // Try to buy votes with insufficient funds ++ const buyAmount = DEFAULT.buyAmount / 2n; // Insufficient funds ++ const votesToBuy = 100n; ++ const minVotesToBuy = 90n; // 90% of requested votes ++ ++ // Calculate actual cost for these votes ++ const { totalCostIncludingFees } = await reputationMarket.simulateBuy( ++ DEFAULT.profileId, ++ DEFAULT.isPositive, ++ minVotesToBuy, ++ ); ++ ++ // Attempt purchase with insufficient funds ++ await expect( ++ userA.buyVotes({ ++ buyAmount, ++ votesToBuy, ++ minVotesToBuy, ++ }), ++ ) ++ .to.be.revertedWithCustomError(reputationMarket, 'BuySlippageLimitExceeded') ++ .withArgs(totalCostIncludingFees, buyAmount); ++ }); ++ ++ it('should revert with InsufficientFunds when max equals min votes and insufficient funds', async () => { ++ // Try to buy votes with insufficient funds ++ const buyAmount = DEFAULT.buyAmount / 2n; // Insufficient funds ++ const votesToBuy = 100n; ++ ++ // Attempt purchase with insufficient funds and equal max/min votes ++ await expect( ++ userA.buyVotes({ ++ buyAmount, ++ votesToBuy, ++ minVotesToBuy: votesToBuy, // Set min equal to max ++ }), ++ ).to.be.revertedWithCustomError(reputationMarket, 'InsufficientFunds'); ++ }); + }); + + describe('Simulations', () => { diff --git a/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts b/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts index 84a70a6..f2c49d9 100644 --- a/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts +++ b/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts @@ -11,6 +11,7 @@ const { ethers } = hre; describe('ReputationMarket Base Price Tests', () => { let deployer: EthosDeployer; let userA: MarketUser; + let ethosUserA: EthosUser; let reputationMarket: ReputationMarket; const DEFAULT_PRICE = 0.01; @@ -20,7 +21,7 @@ describe('ReputationMarket Base Price Tests', () => { if (!deployer.reputationMarket.contract) { throw new Error('ReputationMarket contract not found'); } - const ethosUserA = await deployer.createUser(); + ethosUserA = await deployer.createUser(); await ethosUserA.setBalance('5000000000'); userA = new MarketUser(ethosUserA.signer); @@ -209,5 +210,160 @@ describe('ReputationMarket Base Price Tests', () => { 'Price is too high', ); // less than 1% of base price }); + + it('should sell the last distrust vote', async () => { + const range = 100n; + const initialContractBalance = await ethers.provider.getBalance(reputationMarket.target); + const oneEth = ethers.parseEther('1'); + const oddBasePrice = ethers.parseEther('0.16325'); + + // Create market setup + const userC = await deployer.createUser(); + await userC.setBalance((range * oneEth).toString()); + const marketC = new MarketUser(userC.signer); + + await reputationMarket + .connect(deployer.ADMIN) + .addMarketConfig(DEFAULT.liquidity, oddBasePrice, 0n); + + const configCount = await reputationMarket.getMarketConfigCount(); + + await reputationMarket + .connect(deployer.ADMIN) + .createMarketWithConfigAdmin(userC.signer.address, configCount - 1n, { + value: 0n, + }); + + // Check initial market funds + expect(await reputationMarket.marketFunds(userC.profileId)).to.equal(0n); + + // Buy votes and check funds increased + await marketC.buyVotes({ + votesToBuy: range, + buyAmount: oneEth * range, + isPositive: false, + profileId: userC.profileId, + }); + + const afterBuyFunds = await reputationMarket.marketFunds(userC.profileId); + expect(afterBuyFunds).to.be.gt(0n, 'Market funds should increase after buying'); + + // Sell votes in batches and track funds + const beforeFirstSellFunds = await reputationMarket.marketFunds(userC.profileId); + await marketC.sellVotes({ sellVotes: 9n, isPositive: false, profileId: userC.profileId }); + const afterFirstSellFunds = await reputationMarket.marketFunds(userC.profileId); + expect(afterFirstSellFunds).to.be.lt( + beforeFirstSellFunds, + 'Funds should decrease after first sell', + ); + + const beforeSecondSellFunds = await reputationMarket.marketFunds(userC.profileId); + await marketC.sellVotes({ sellVotes: 11n, isPositive: false, profileId: userC.profileId }); + const afterSecondSellFunds = await reputationMarket.marketFunds(userC.profileId); + expect(afterSecondSellFunds).to.be.lt( + beforeSecondSellFunds, + 'Funds should decrease after second sell', + ); + + await marketC.sellVotes({ sellVotes: 80n, isPositive: false, profileId: userC.profileId }); + const afterFinalSellFunds = await reputationMarket.marketFunds(userC.profileId); + expect(afterFinalSellFunds).to.be.lte( + 2n, + 'Market funds should be dust after selling all votes', + ); + + // Buy two distrust votes + await marketC.buyVotes({ + votesToBuy: 2n, + buyAmount: oneEth * 2n, + isPositive: false, + profileId: userC.profileId, + }); + + const afterTwoVotesBuyFunds = await reputationMarket.marketFunds(userC.profileId); + expect(afterTwoVotesBuyFunds).to.be.gt(0n, 'Market funds should increase after buying'); + + // Sell first vote + await marketC.sellVotes({ + sellVotes: 1n, + isPositive: false, + profileId: userC.profileId, + }); + const afterFirstVoteSellFunds = await reputationMarket.marketFunds(userC.profileId); + expect(afterFirstVoteSellFunds).to.be.lt( + afterTwoVotesBuyFunds, + 'Funds should decrease after selling first vote', + ); + + // Sell second vote + await marketC.sellVotes({ + sellVotes: 1n, + isPositive: false, + profileId: userC.profileId, + }); + const afterSecondVoteSellFunds = await reputationMarket.marketFunds(userC.profileId); + expect(afterSecondVoteSellFunds).to.be.lte( + 2n, + 'Market funds should be dust after selling all votes', + ); + + // Verify final contract balance still matches initial + const finalContractBalance = await ethers.provider.getBalance(reputationMarket.target); + expect(finalContractBalance).to.be.closeTo(initialContractBalance, 2n); + }); + }); + + it('should allow maximum spread between votesToBuy and minVotesToBuy', async () => { + const basePrice = ethers.parseEther(DEFAULT_PRICE.toString()); + const votesToBuy = 10000n; + await ethosUserA.setBalance((votesToBuy * basePrice * 2n).toString()); + + // Create market first + await reputationMarket.connect(deployer.ADMIN).createMarketWithConfigAdmin( + userA.signer.address, + 0n, // use default config (index 0) + { value: DEFAULT.creationCost }, + ); + + // Now buy votes to move the market until trust price is less than 0.001 eth + await userA.buyVotes({ + votesToBuy, + isPositive: false, + buyAmount: votesToBuy * basePrice * 2n, + }); + const trustPrice = await reputationMarket.getVotePrice(DEFAULT.profileId, true); + const expectedPrice = basePrice / 10n; + expect(trustPrice).to.be.lt(expectedPrice); + + // user wants to buy the maximum number of votes + const maxVotesToBuy = 133000n - 1n; // this is the max safe number of votes to buy given LMSR liquidity constraints + const minVotesToBuy = 1n; + + // Simulate the buy to get the actual cost + const simulation = await reputationMarket.simulateBuy( + DEFAULT.profileId, + true, // buying trust votes + minVotesToBuy, + ); + + // user cannot buy more than the max number of votes + await expect( + userA.buyVotes({ + votesToBuy: maxVotesToBuy + 1n, + minVotesToBuy, + buyAmount: simulation.totalCostIncludingFees, // Use simulated cost + }), + ).to.be.revertedWithCustomError(deployer.lmsrLibrary.contract, 'VotesExceedSafeLimit'); + + // user can request the maximum number of votes, but end up buying just one + const result = await userA.buyVotes({ + votesToBuy: maxVotesToBuy, + minVotesToBuy, + buyAmount: simulation.totalCostIncludingFees, // Use simulated cost + }); + + // user has one vote + expect(result.trustVotes).to.equal(1n); + // console.log(`User used ${result.gas} gas`); }); }); diff --git a/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts.rej b/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts.rej new file mode 100644 index 0000000..ec9c0e0 --- /dev/null +++ b/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts.rej @@ -0,0 +1,179 @@ +diff a/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts b/ethos/packages/contracts/test/reputationMarket/rep.price.test.ts (rejected hunks) +@@ -11,6 +11,7 @@ const { ethers } = hre; + describe('ReputationMarket Base Price Tests', () => { + let deployer: EthosDeployer; + let userA: MarketUser; ++ let ethosUserA: EthosUser; + let reputationMarket: ReputationMarket; + const DEFAULT_PRICE = 0.01; + +@@ -20,7 +21,7 @@ describe('ReputationMarket Base Price Tests', () => { + if (!deployer.reputationMarket.contract) { + throw new Error('ReputationMarket contract not found'); + } +- const ethosUserA = await deployer.createUser(); ++ ethosUserA = await deployer.createUser(); + await ethosUserA.setBalance('5000000000'); + + userA = new MarketUser(ethosUserA.signer); +@@ -209,5 +210,160 @@ describe('ReputationMarket Base Price Tests', () => { + 'Price is too high', + ); // less than 1% of base price + }); ++ ++ it('should sell the last distrust vote', async () => { ++ const range = 100n; ++ const initialContractBalance = await ethers.provider.getBalance(reputationMarket.target); ++ const oneEth = ethers.parseEther('1'); ++ const oddBasePrice = ethers.parseEther('0.16325'); ++ ++ // Create market setup ++ const userC = await deployer.createUser(); ++ await userC.setBalance((range * oneEth).toString()); ++ const marketC = new MarketUser(userC.signer); ++ ++ await reputationMarket ++ .connect(deployer.ADMIN) ++ .addMarketConfig(DEFAULT.liquidity, oddBasePrice, 0n); ++ ++ const configCount = await reputationMarket.getMarketConfigCount(); ++ ++ await reputationMarket ++ .connect(deployer.ADMIN) ++ .createMarketWithConfigAdmin(userC.signer.address, configCount - 1n, { ++ value: 0n, ++ }); ++ ++ // Check initial market funds ++ expect(await reputationMarket.marketFunds(userC.profileId)).to.equal(0n); ++ ++ // Buy votes and check funds increased ++ await marketC.buyVotes({ ++ votesToBuy: range, ++ buyAmount: oneEth * range, ++ isPositive: false, ++ profileId: userC.profileId, ++ }); ++ ++ const afterBuyFunds = await reputationMarket.marketFunds(userC.profileId); ++ expect(afterBuyFunds).to.be.gt(0n, 'Market funds should increase after buying'); ++ ++ // Sell votes in batches and track funds ++ const beforeFirstSellFunds = await reputationMarket.marketFunds(userC.profileId); ++ await marketC.sellVotes({ sellVotes: 9n, isPositive: false, profileId: userC.profileId }); ++ const afterFirstSellFunds = await reputationMarket.marketFunds(userC.profileId); ++ expect(afterFirstSellFunds).to.be.lt( ++ beforeFirstSellFunds, ++ 'Funds should decrease after first sell', ++ ); ++ ++ const beforeSecondSellFunds = await reputationMarket.marketFunds(userC.profileId); ++ await marketC.sellVotes({ sellVotes: 11n, isPositive: false, profileId: userC.profileId }); ++ const afterSecondSellFunds = await reputationMarket.marketFunds(userC.profileId); ++ expect(afterSecondSellFunds).to.be.lt( ++ beforeSecondSellFunds, ++ 'Funds should decrease after second sell', ++ ); ++ ++ await marketC.sellVotes({ sellVotes: 80n, isPositive: false, profileId: userC.profileId }); ++ const afterFinalSellFunds = await reputationMarket.marketFunds(userC.profileId); ++ expect(afterFinalSellFunds).to.be.lte( ++ 2n, ++ 'Market funds should be dust after selling all votes', ++ ); ++ ++ // Buy two distrust votes ++ await marketC.buyVotes({ ++ votesToBuy: 2n, ++ buyAmount: oneEth * 2n, ++ isPositive: false, ++ profileId: userC.profileId, ++ }); ++ ++ const afterTwoVotesBuyFunds = await reputationMarket.marketFunds(userC.profileId); ++ expect(afterTwoVotesBuyFunds).to.be.gt(0n, 'Market funds should increase after buying'); ++ ++ // Sell first vote ++ await marketC.sellVotes({ ++ sellVotes: 1n, ++ isPositive: false, ++ profileId: userC.profileId, ++ }); ++ const afterFirstVoteSellFunds = await reputationMarket.marketFunds(userC.profileId); ++ expect(afterFirstVoteSellFunds).to.be.lt( ++ afterTwoVotesBuyFunds, ++ 'Funds should decrease after selling first vote', ++ ); ++ ++ // Sell second vote ++ await marketC.sellVotes({ ++ sellVotes: 1n, ++ isPositive: false, ++ profileId: userC.profileId, ++ }); ++ const afterSecondVoteSellFunds = await reputationMarket.marketFunds(userC.profileId); ++ expect(afterSecondVoteSellFunds).to.be.lte( ++ 2n, ++ 'Market funds should be dust after selling all votes', ++ ); ++ ++ // Verify final contract balance still matches initial ++ const finalContractBalance = await ethers.provider.getBalance(reputationMarket.target); ++ expect(finalContractBalance).to.be.closeTo(initialContractBalance, 2n); ++ }); ++ }); ++ ++ it('should allow maximum spread between votesToBuy and minVotesToBuy', async () => { ++ const basePrice = ethers.parseEther(DEFAULT_PRICE.toString()); ++ const votesToBuy = 10000n; ++ await ethosUserA.setBalance((votesToBuy * basePrice * 2n).toString()); ++ ++ // Create market first ++ await reputationMarket.connect(deployer.ADMIN).createMarketWithConfigAdmin( ++ userA.signer.address, ++ 0n, // use default config (index 0) ++ { value: DEFAULT.creationCost }, ++ ); ++ ++ // Now buy votes to move the market until trust price is less than 0.001 eth ++ await userA.buyVotes({ ++ votesToBuy, ++ isPositive: false, ++ buyAmount: votesToBuy * basePrice * 2n, ++ }); ++ const trustPrice = await reputationMarket.getVotePrice(DEFAULT.profileId, true); ++ const expectedPrice = basePrice / 10n; ++ expect(trustPrice).to.be.lt(expectedPrice); ++ ++ // user wants to buy the maximum number of votes ++ const maxVotesToBuy = 133000n - 1n; // this is the max safe number of votes to buy given LMSR liquidity constraints ++ const minVotesToBuy = 1n; ++ ++ // Simulate the buy to get the actual cost ++ const simulation = await reputationMarket.simulateBuy( ++ DEFAULT.profileId, ++ true, // buying trust votes ++ minVotesToBuy, ++ ); ++ ++ // user cannot buy more than the max number of votes ++ await expect( ++ userA.buyVotes({ ++ votesToBuy: maxVotesToBuy + 1n, ++ minVotesToBuy, ++ buyAmount: simulation.totalCostIncludingFees, // Use simulated cost ++ }), ++ ).to.be.revertedWithCustomError(deployer.lmsrLibrary.contract, 'VotesExceedSafeLimit'); ++ ++ // user can request the maximum number of votes, but end up buying just one ++ const result = await userA.buyVotes({ ++ votesToBuy: maxVotesToBuy, ++ minVotesToBuy, ++ buyAmount: simulation.totalCostIncludingFees, // Use simulated cost ++ }); ++ ++ // user has one vote ++ expect(result.trustVotes).to.equal(1n); ++ // console.log(`User used ${result.gas} gas`); + }); + }); diff --git a/ethos/packages/contracts/test/utils/common.ts b/ethos/packages/contracts/test/utils/common.ts index 8e3e4d0..bd44438 100644 --- a/ethos/packages/contracts/test/utils/common.ts +++ b/ethos/packages/contracts/test/utils/common.ts @@ -130,6 +130,15 @@ export function calcFeeDistribution( // Calculate total basis points for all fees const totalBasisPoints = feeBasisPoints.entry + feeBasisPoints.donation + feeBasisPoints.vouchIncentives; + + // If total basis points is 0, return 0 fees; don't divide by 0 + if (totalBasisPoints === 0n) { + return { + totalFees: 0n, + deposit: amount, + shares: { protocol: 0n, donation: 0n, vouchersPool: 0n }, + }; + } // Calculate total fees using combined basis points const totalFees = amount - (amount * 10000n) / (10000n + totalBasisPoints); diff --git a/ethos/packages/contracts/test/utils/common.ts.rej b/ethos/packages/contracts/test/utils/common.ts.rej new file mode 100644 index 0000000..486d3cd --- /dev/null +++ b/ethos/packages/contracts/test/utils/common.ts.rej @@ -0,0 +1,17 @@ +diff a/ethos/packages/contracts/test/utils/common.ts b/ethos/packages/contracts/test/utils/common.ts (rejected hunks) +@@ -130,6 +130,15 @@ export function calcFeeDistribution( + // Calculate total basis points for all fees + const totalBasisPoints = + feeBasisPoints.entry + feeBasisPoints.donation + feeBasisPoints.vouchIncentives; ++ ++ // If total basis points is 0, return 0 fees; don't divide by 0 ++ if (totalBasisPoints === 0n) { ++ return { ++ totalFees: 0n, ++ deposit: amount, ++ shares: { protocol: 0n, donation: 0n, vouchersPool: 0n }, ++ }; ++ } + // Calculate total fees using combined basis points + const totalFees = amount - (amount * 10000n) / (10000n + totalBasisPoints); + diff --git a/ethos/packages/contracts/test/utils/defaults.ts b/ethos/packages/contracts/test/utils/defaults.ts index e532d5b..8af61ae 100644 --- a/ethos/packages/contracts/test/utils/defaults.ts +++ b/ethos/packages/contracts/test/utils/defaults.ts @@ -17,12 +17,18 @@ export const DEFAULT = { ACCOUNT_NAME_EXAMPLE: 'example', ATTESTATION_EVIDENCE_0: 'ATTESTATION_EVIDENCE_0', ATTESTATION_EVIDENCE_1: 'ATTESTATION_EVIDENCE_1', + ATTESTATION_EVIDENCE: JSON.stringify({ + source: 'privy', + type: 'OAuth2', + id: 'privyLogin.id', + approver: 'ethos.network', + }), ATTESTATION_HASH: '0x0000000000000000000000000000000000000000000000000000000000000000', PAYMENT_TOKEN: zeroAddress, PAYMENT_AMOUNT: ethers.parseEther('0.1'), PROVIDER: ethers.provider, EMPTY_BYTES: '0x' + '0'.repeat(64), -}; +} as const; export type VouchParams = { paymentAmount?: bigint; diff --git a/ethos/packages/contracts/test/utils/defaults.ts.rej b/ethos/packages/contracts/test/utils/defaults.ts.rej new file mode 100644 index 0000000..e6c7fa1 --- /dev/null +++ b/ethos/packages/contracts/test/utils/defaults.ts.rej @@ -0,0 +1,21 @@ +diff a/ethos/packages/contracts/test/utils/defaults.ts b/ethos/packages/contracts/test/utils/defaults.ts (rejected hunks) +@@ -17,12 +17,18 @@ export const DEFAULT = { + ACCOUNT_NAME_EXAMPLE: 'example', + ATTESTATION_EVIDENCE_0: 'ATTESTATION_EVIDENCE_0', + ATTESTATION_EVIDENCE_1: 'ATTESTATION_EVIDENCE_1', ++ ATTESTATION_EVIDENCE: JSON.stringify({ ++ source: 'privy', ++ type: 'OAuth2', ++ id: 'privyLogin.id', ++ approver: 'ethos.network', ++ }), + ATTESTATION_HASH: '0x0000000000000000000000000000000000000000000000000000000000000000', + PAYMENT_TOKEN: zeroAddress, + PAYMENT_AMOUNT: ethers.parseEther('0.1'), + PROVIDER: ethers.provider, + EMPTY_BYTES: '0x' + '0'.repeat(64), +-}; ++} as const; + + export type VouchParams = { + paymentAmount?: bigint; diff --git a/ethos/packages/contracts/test/utils/ethosUser.ts b/ethos/packages/contracts/test/utils/ethosUser.ts index 765286f..8dc49d9 100644 --- a/ethos/packages/contracts/test/utils/ethosUser.ts +++ b/ethos/packages/contracts/test/utils/ethosUser.ts @@ -2,7 +2,7 @@ import { type HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signer import { time } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; import { type ContractTransactionResponse } from 'ethers'; import hre from 'hardhat'; -import { zeroAddress } from 'viem'; +import { zeroAddress, zeroHash } from 'viem'; import { type IEthosProfile } from '../../typechain-types/index.js'; import { common } from './common.js'; import { @@ -98,7 +98,7 @@ export class EthosUser { } public async getRewardsBalance(): Promise { - return await this.deployer.ethosVouch.contract?.rewards(this.profileId); + return await this.deployer.ethosVouch.contract?.rewardsByProfileId(this.profileId); } public async grantInvites(amount: number): Promise { @@ -139,12 +139,55 @@ export class EthosUser { .registerAddress(address, this.profileId, randValue, signature); } + public async deleteAddress( + address: string, + markAsCompromised = false, + ): Promise { + return await this.deployer.ethosProfile.contract + ?.connect(this.signer) + .deleteAddress(address, markAsCompromised); + } + + public async attest( + params: { + service?: string; + account?: string; + evidence?: string; + } = {}, + ): Promise { + const randValue = Math.floor(Math.random() * 1000000); + const evidence = params.evidence ?? DEFAULT.ATTESTATION_EVIDENCE; + const attestationDetails = { + service: params.service ?? DEFAULT.SERVICE_X, + account: params.account ?? DEFAULT.ACCOUNT_NAME_EXAMPLE, + }; + + const signature = await common.signatureForCreateAttestation( + this.profileId.toString(), + randValue.toString(), + attestationDetails.account, + attestationDetails.service, + evidence, + this.deployer.EXPECTED_SIGNER, + ); + + await this.deployer.ethosAttestation.contract + ?.connect(this.signer) + .createAttestation(this.profileId, randValue, attestationDetails, evidence, signature); + + return await this.getAttestationHash(attestationDetails.service, attestationDetails.account); + } + + public async getAttestationHash(service: string, account: string): Promise { + return await this.deployer.ethosAttestation.contract.getServiceAndAccountHash(service, account); + } + public async increaseVouch( vouchId: bigint, params: { paymentAmount: bigint } = { paymentAmount: VOUCH_PARAMS.paymentAmount }, ): Promise { return await this.deployer.ethosVouch.contract ?.connect(this.signer) - .increaseVouch(vouchId, { value: params.paymentAmount }); + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: params.paymentAmount }); } } diff --git a/ethos/packages/contracts/test/utils/ethosUser.ts.rej b/ethos/packages/contracts/test/utils/ethosUser.ts.rej new file mode 100644 index 0000000..7f8adaf --- /dev/null +++ b/ethos/packages/contracts/test/utils/ethosUser.ts.rej @@ -0,0 +1,76 @@ +diff a/ethos/packages/contracts/test/utils/ethosUser.ts b/ethos/packages/contracts/test/utils/ethosUser.ts (rejected hunks) +@@ -2,7 +2,7 @@ import { type HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signer + import { time } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; + import { type ContractTransactionResponse } from 'ethers'; + import hre from 'hardhat'; +-import { zeroAddress } from 'viem'; ++import { zeroAddress, zeroHash } from 'viem'; + import { type IEthosProfile } from '../../typechain-types/index.js'; + import { common } from './common.js'; + import { +@@ -98,7 +98,7 @@ export class EthosUser { + } + + public async getRewardsBalance(): Promise { +- return await this.deployer.ethosVouch.contract?.rewards(this.profileId); ++ return await this.deployer.ethosVouch.contract?.rewardsByProfileId(this.profileId); + } + + public async grantInvites(amount: number): Promise { +@@ -139,12 +139,55 @@ export class EthosUser { + .registerAddress(address, this.profileId, randValue, signature); + } + ++ public async deleteAddress( ++ address: string, ++ markAsCompromised = false, ++ ): Promise { ++ return await this.deployer.ethosProfile.contract ++ ?.connect(this.signer) ++ .deleteAddress(address, markAsCompromised); ++ } ++ ++ public async attest( ++ params: { ++ service?: string; ++ account?: string; ++ evidence?: string; ++ } = {}, ++ ): Promise { ++ const randValue = Math.floor(Math.random() * 1000000); ++ const evidence = params.evidence ?? DEFAULT.ATTESTATION_EVIDENCE; ++ const attestationDetails = { ++ service: params.service ?? DEFAULT.SERVICE_X, ++ account: params.account ?? DEFAULT.ACCOUNT_NAME_EXAMPLE, ++ }; ++ ++ const signature = await common.signatureForCreateAttestation( ++ this.profileId.toString(), ++ randValue.toString(), ++ attestationDetails.account, ++ attestationDetails.service, ++ evidence, ++ this.deployer.EXPECTED_SIGNER, ++ ); ++ ++ await this.deployer.ethosAttestation.contract ++ ?.connect(this.signer) ++ .createAttestation(this.profileId, randValue, attestationDetails, evidence, signature); ++ ++ return await this.getAttestationHash(attestationDetails.service, attestationDetails.account); ++ } ++ ++ public async getAttestationHash(service: string, account: string): Promise { ++ return await this.deployer.ethosAttestation.contract.getServiceAndAccountHash(service, account); ++ } ++ + public async increaseVouch( + vouchId: bigint, + params: { paymentAmount: bigint } = { paymentAmount: VOUCH_PARAMS.paymentAmount }, + ): Promise { + return await this.deployer.ethosVouch.contract + ?.connect(this.signer) +- .increaseVouch(vouchId, { value: params.paymentAmount }); ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: params.paymentAmount }); + } + } diff --git a/ethos/packages/contracts/test/vouch/vouch.address.test.ts b/ethos/packages/contracts/test/vouch/vouch.address.test.ts index 8034a32..4358a96 100644 --- a/ethos/packages/contracts/test/vouch/vouch.address.test.ts +++ b/ethos/packages/contracts/test/vouch/vouch.address.test.ts @@ -46,7 +46,11 @@ describe('EthosVouch Vouching by Address', () => { }); it('should successfully vouch for a valid profile address', async () => { - await userA.vouch(userB); + await ethosVouch + .connect(userA.signer) + .vouchByAddress(userB.signer.address, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); // Verify the vouch was created const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( @@ -55,6 +59,8 @@ describe('EthosVouch Vouching by Address', () => { ); expect(vouch.authorProfileId).to.equal(userA.profileId); expect(vouch.subjectProfileId).to.equal(userB.profileId); + expect(vouch.comment).to.equal(DEFAULT.COMMENT); + expect(vouch.metadata).to.equal(DEFAULT.METADATA); }); it('should create a vouch using vouchByAddress', async () => { @@ -97,6 +103,7 @@ describe('EthosVouch Profile Status Tests', () => { let mockProfileByAddress: EthosUser; let mockProfileByAttestation: EthosUser; let archivedProfileUser: EthosUser; + let mockAttestationHash: string; beforeEach(async () => { deployer = await loadFixture(createDeployer); @@ -109,7 +116,7 @@ describe('EthosVouch Profile Status Tests', () => { attestationDetails: { account: DEFAULT.ACCOUNT_NAME_EXAMPLE, service: DEFAULT.SERVICE_X }, }); // get the profile id of the mock profile by attestation - const mockAttestationHash = await common.attestationHash( + mockAttestationHash = await common.attestationHash( DEFAULT.ACCOUNT_NAME_EXAMPLE, DEFAULT.SERVICE_X, ); @@ -140,7 +147,11 @@ describe('EthosVouch Profile Status Tests', () => { }); it('should allow vouching for a mock profile created via address review', async () => { - await userA.vouch(mockProfileByAddress); + await ethosVouch + .connect(userA.signer) + .vouchByAddress(mockProfileByAddress.signer.address, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( userA.profileId, @@ -167,8 +178,11 @@ describe('EthosVouch Profile Status Tests', () => { it('should handle profile transitions from mock to verified for mocks created via address review', async () => { // First vouch for mock profile - await userA.vouch(mockProfileByAddress); - + await ethosVouch + .connect(userA.signer) + .vouchByAddress(mockProfileByAddress.signer.address, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); // Verify the mock profile by creating a real profile await deployer.ethosProfile.contract ?.connect(deployer.OWNER) @@ -191,7 +205,11 @@ describe('EthosVouch Profile Status Tests', () => { }); it('should allow vouching for a mock profile created via attestation review', async () => { - await userA.vouch(mockProfileByAttestation); + await ethosVouch + .connect(userA.signer) + .vouchByAttestation(mockAttestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( userA.profileId, @@ -203,7 +221,11 @@ describe('EthosVouch Profile Status Tests', () => { it('should handle profile transitions from mock to verified for mocks created via attestation review', async () => { // First vouch for mock profile - await userA.vouch(mockProfileByAttestation); + await ethosVouch + .connect(userA.signer) + .vouchByAttestation(mockAttestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); // Verify the mock profile by creating a real profile await deployer.ethosProfile.contract @@ -211,10 +233,10 @@ describe('EthosVouch Profile Status Tests', () => { .inviteAddress(mockProfileByAttestation.signer.address); await deployer.ethosProfile.contract?.connect(mockProfileByAttestation.signer).createProfile(1); - // Because the original vouch is still there, another vouch should fail + // Because the user now has a new profileId, vouching for the old profileId should fail await expect(userA.vouch(mockProfileByAttestation)).to.be.revertedWithCustomError( deployer.ethosVouch.contract, - 'AlreadyVouched', + 'InvalidEthosProfileForVouch', ); const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( diff --git a/ethos/packages/contracts/test/vouch/vouch.address.test.ts.rej b/ethos/packages/contracts/test/vouch/vouch.address.test.ts.rej new file mode 100644 index 0000000..7d487e0 --- /dev/null +++ b/ethos/packages/contracts/test/vouch/vouch.address.test.ts.rej @@ -0,0 +1,106 @@ +diff a/ethos/packages/contracts/test/vouch/vouch.address.test.ts b/ethos/packages/contracts/test/vouch/vouch.address.test.ts (rejected hunks) +@@ -46,7 +46,11 @@ describe('EthosVouch Vouching by Address', () => { + }); + + it('should successfully vouch for a valid profile address', async () => { +- await userA.vouch(userB); ++ await ethosVouch ++ .connect(userA.signer) ++ .vouchByAddress(userB.signer.address, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); + + // Verify the vouch was created + const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( +@@ -55,6 +59,8 @@ describe('EthosVouch Vouching by Address', () => { + ); + expect(vouch.authorProfileId).to.equal(userA.profileId); + expect(vouch.subjectProfileId).to.equal(userB.profileId); ++ expect(vouch.comment).to.equal(DEFAULT.COMMENT); ++ expect(vouch.metadata).to.equal(DEFAULT.METADATA); + }); + + it('should create a vouch using vouchByAddress', async () => { +@@ -97,6 +103,7 @@ describe('EthosVouch Profile Status Tests', () => { + let mockProfileByAddress: EthosUser; + let mockProfileByAttestation: EthosUser; + let archivedProfileUser: EthosUser; ++ let mockAttestationHash: string; + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); +@@ -109,7 +116,7 @@ describe('EthosVouch Profile Status Tests', () => { + attestationDetails: { account: DEFAULT.ACCOUNT_NAME_EXAMPLE, service: DEFAULT.SERVICE_X }, + }); + // get the profile id of the mock profile by attestation +- const mockAttestationHash = await common.attestationHash( ++ mockAttestationHash = await common.attestationHash( + DEFAULT.ACCOUNT_NAME_EXAMPLE, + DEFAULT.SERVICE_X, + ); +@@ -140,7 +147,11 @@ describe('EthosVouch Profile Status Tests', () => { + }); + + it('should allow vouching for a mock profile created via address review', async () => { +- await userA.vouch(mockProfileByAddress); ++ await ethosVouch ++ .connect(userA.signer) ++ .vouchByAddress(mockProfileByAddress.signer.address, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); + + const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( + userA.profileId, +@@ -167,8 +178,11 @@ describe('EthosVouch Profile Status Tests', () => { + + it('should handle profile transitions from mock to verified for mocks created via address review', async () => { + // First vouch for mock profile +- await userA.vouch(mockProfileByAddress); +- ++ await ethosVouch ++ .connect(userA.signer) ++ .vouchByAddress(mockProfileByAddress.signer.address, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); + // Verify the mock profile by creating a real profile + await deployer.ethosProfile.contract + ?.connect(deployer.OWNER) +@@ -191,7 +205,11 @@ describe('EthosVouch Profile Status Tests', () => { + }); + + it('should allow vouching for a mock profile created via attestation review', async () => { +- await userA.vouch(mockProfileByAttestation); ++ await ethosVouch ++ .connect(userA.signer) ++ .vouchByAttestation(mockAttestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); + + const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( + userA.profileId, +@@ -203,7 +221,11 @@ describe('EthosVouch Profile Status Tests', () => { + + it('should handle profile transitions from mock to verified for mocks created via attestation review', async () => { + // First vouch for mock profile +- await userA.vouch(mockProfileByAttestation); ++ await ethosVouch ++ .connect(userA.signer) ++ .vouchByAttestation(mockAttestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); + + // Verify the mock profile by creating a real profile + await deployer.ethosProfile.contract +@@ -211,10 +233,10 @@ describe('EthosVouch Profile Status Tests', () => { + .inviteAddress(mockProfileByAttestation.signer.address); + await deployer.ethosProfile.contract?.connect(mockProfileByAttestation.signer).createProfile(1); + +- // Because the original vouch is still there, another vouch should fail ++ // Because the user now has a new profileId, vouching for the old profileId should fail + await expect(userA.vouch(mockProfileByAttestation)).to.be.revertedWithCustomError( + deployer.ethosVouch.contract, +- 'AlreadyVouched', ++ 'InvalidEthosProfileForVouch', + ); + + const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( diff --git a/ethos/packages/contracts/test/vouch/vouch.fees.test.ts b/ethos/packages/contracts/test/vouch/vouch.fees.test.ts index 43530a3..9f83061 100644 --- a/ethos/packages/contracts/test/vouch/vouch.fees.test.ts +++ b/ethos/packages/contracts/test/vouch/vouch.fees.test.ts @@ -44,7 +44,7 @@ async function setupFees(deployer: EthosDeployer): Promise { ); } -describe('Vault Fees', () => { +describe('Vouch Fees', () => { let deployer: EthosDeployer; let userA: EthosUser; let userB: EthosUser; @@ -105,13 +105,12 @@ describe('Vault Fees', () => { const { deposit: initialDeposit } = calcFeeDistribution(paymentAmount, { entry: entryFee, donation: donationFee, - vouchIncentives, + vouchIncentives: 0n, }); const { vouchId } = await userA.vouch(userB); const balance = await userA.getVouchBalance(vouchId); - expect(balance).to.be.closeTo(initialDeposit, 1n); - + expect(balance).to.be.closeTo(initialDeposit, 2n); const balanceBeforeUnvouch = await userA.getBalance(); const unvouchTx = await userA.unvouch(vouchId); const receipt = await unvouchTx.wait(); @@ -400,7 +399,6 @@ describe('Vault Fees', () => { const protocolFee = 400n; // 4% const donationFee = 300n; // 3% const vouchersPoolFee = 200n; // 2% - const totalFeeBasisPoints = protocolFee + donationFee + vouchersPoolFee; await Promise.all([ deployer.ethosVouch.contract @@ -422,12 +420,12 @@ describe('Vault Fees', () => { await userB.setBalance(ethers.parseEther('1000.0').toString()); // Calculate total fees first - const totalFees = - vouchAmount - (vouchAmount * BASIS_POINTS) / (BASIS_POINTS + totalFeeBasisPoints); + const firstVouchFeeBasisPoints = donationFee + protocolFee; + const firstVouchFees = + vouchAmount - (vouchAmount * BASIS_POINTS) / (BASIS_POINTS + firstVouchFeeBasisPoints); // Calculate individual fee shares - const donationShare = (totalFees * donationFee) / totalFeeBasisPoints; - const vouchersPoolShare = (totalFees * vouchersPoolFee) / totalFeeBasisPoints; + const firstVouchDonationShare = (firstVouchFees * donationFee) / firstVouchFeeBasisPoints; // Track initial rewards const initialUserBRewards = await userB.getRewardsBalance(); @@ -440,24 +438,234 @@ describe('Vault Fees', () => { const rewardsIncrease = afterFirstVouchRewards - initialUserBRewards; // For first vouch, only donation fee goes to rewards - expect(rewardsIncrease).to.equal(donationShare); + expect(rewardsIncrease).to.be.closeTo(firstVouchDonationShare, 1n); // Second vouch const userC = await deployer.createUser(); await userC.setBalance(ethers.parseEther('1000.0').toString()); + const secondVouchBasisPoints = donationFee + protocolFee + vouchersPoolFee; + const secondVouchFees = + vouchAmount - (vouchAmount * BASIS_POINTS) / (BASIS_POINTS + secondVouchBasisPoints); + const secondVouchVouchersPoolShare = + (secondVouchFees * vouchersPoolFee) / secondVouchBasisPoints; // Track first voucher's balance before second vouch const beforeSecondVouchBalance = await userA.getVouchBalance(firstVouchId); // Make second vouch - await userC.vouch(userB, { paymentAmount: vouchAmount }); + const { vouchId: secondVouchId } = await userC.vouch(userB, { paymentAmount: vouchAmount }); + const secondVouchBalance = await userA.getVouchBalance(secondVouchId); // Check first voucher's balance after second vouch const afterSecondVouchBalance = await userA.getVouchBalance(firstVouchId); const voucherBalanceIncrease = afterSecondVouchBalance - beforeSecondVouchBalance; + // Third vouch + const userD = await deployer.createUser(); + await userD.setBalance(ethers.parseEther('1000.0').toString()); + const { vouchId: thirdVouchId } = await userD.vouch(userB, { paymentAmount: vouchAmount }); + const thirdVouchBalance = await userA.getVouchBalance(thirdVouchId); + // First voucher should receive the vouchers pool fee from second vouch (allow 1 wei difference) - expect(voucherBalanceIncrease).to.be.closeTo(vouchersPoolShare, 1n); + expect(voucherBalanceIncrease).to.be.closeTo(secondVouchVouchersPoolShare, 1n); + // First vouch balance should be greater than second vouch balance + expect(beforeSecondVouchBalance).to.gt(secondVouchBalance); + // All the consequent vouches should have the same balance + expect(secondVouchBalance).to.equal(thirdVouchBalance); + }); + + it('should limit voucher rewards to their vouch amount when they are the only voucher', async () => { + // Set up a scenario where vouchers pool fee is high + const vouchersPoolFee = 1000n; // 10% - maximum allowed fee + await deployer.ethosVouch.contract + .connect(deployer.ADMIN) + .setEntryVouchersPoolFeeBasisPoints(vouchersPoolFee); + + // First vouch - small amount + const smallVouchAmount = paymentAmount; // Use default payment amount + await userA.setBalance((paymentAmount * 10n).toString()); + const { vouchId: firstVouchId } = await userA.vouch(userB, { paymentAmount: smallVouchAmount }); + + // Second vouch - large amount (100x first vouch) + const largeVouchAmount = paymentAmount * 1000n; + const userC = await deployer.createUser(); + await userC.setBalance((largeVouchAmount * 2n).toString()); + + // Get userA's vouch balance before second vouch + const balanceBeforeSecondVouch = await userA.getVouchBalance(firstVouchId); + + // Make second vouch + await userC.vouch(userB, { paymentAmount: largeVouchAmount }); + + // Get userA's vouch balance after second vouch + const balanceAfterSecondVouch = await userA.getVouchBalance(firstVouchId); + const rewardAmount = balanceAfterSecondVouch - balanceBeforeSecondVouch; + + // The reward should not exceed the original vouch amount + expect(rewardAmount).to.be.lte(smallVouchAmount); + }); + + it('should limit voucher rewards when there are multiple eligible vouchers', async () => { + // Set up a scenario where vouchers pool fee is high + const vouchersPoolFee = 1000n; // 10% - maximum allowed fee + await deployer.ethosVouch.contract + .connect(deployer.ADMIN) + .setEntryVouchersPoolFeeBasisPoints(vouchersPoolFee); + + // Third vouch - large amount (1000x initial vouch) + const largeVouchAmount = paymentAmount * 1000n; + const userD = await deployer.createUser(); + + // First two vouches - equal amounts + const initialVouchAmount = paymentAmount; + await Promise.all([ + userA.setBalance((paymentAmount * 20n).toString()), + userB.setBalance((paymentAmount * 20n).toString()), + userD.setBalance((largeVouchAmount * 20n).toString()), + ]); + + const userC = await deployer.createUser(); + const { vouchId: firstVouchId } = await userA.vouch(userC, { + paymentAmount: initialVouchAmount, + }); + const { vouchId: secondVouchId } = await userB.vouch(userC, { + paymentAmount: initialVouchAmount, + }); + + // Get balances before third vouch + const [firstVoucherInitialBalance, secondVoucherInitialBalance] = await Promise.all([ + userA.getVouchBalance(firstVouchId), + userB.getVouchBalance(secondVouchId), + ]); + + // Make third vouch + await userD.vouch(userC, { paymentAmount: largeVouchAmount }); + + // Get balances after third vouch + const [firstVoucherFinalBalance, secondVoucherFinalBalance] = await Promise.all([ + userA.getVouchBalance(firstVouchId), + userB.getVouchBalance(secondVouchId), + ]); + + // Calculate rewards for each voucher explicitly + const rewardsForFirstVoucher = firstVoucherFinalBalance - firstVoucherInitialBalance; + const rewardsForSecondVoucher = secondVoucherFinalBalance - secondVoucherInitialBalance; + // The rewards don't exceed the original vouch amount since there are multiple vouchers + expect(rewardsForFirstVoucher).to.be.lte( + firstVoucherInitialBalance, + 'Rewards for first voucher should be less than or equal to the vouch amount', + ); + expect(rewardsForSecondVoucher).to.be.lte( + firstVoucherInitialBalance, + 'Rewards for second voucher should be less than or equal to the vouch amount', + ); + }); + + it('should not deduct vouchers pool fee when there are no previous vouchers', async () => { + // Set up fees - only vouchers pool fee + const vouchersPoolFee = 500n; // 5% + await deployer.ethosVouch.contract + .connect(deployer.ADMIN) + .setEntryVouchersPoolFeeBasisPoints(vouchersPoolFee); + + // Clear other fees to isolate the issue + await Promise.all([ + deployer.ethosVouch.contract.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(0n), + deployer.ethosVouch.contract.connect(deployer.ADMIN).setEntryDonationFeeBasisPoints(0n), + ]); + + const vouchAmount = ethers.parseEther('1.0'); + // Convert to string without decimal points + await userA.setBalance(vouchAmount.toString()); + + // First vouch - should not deduct vouchers pool fee since there are no previous vouchers + const initialBalance = await userA.getBalance(); + const vouchTx = await userA.deployer.ethosVouch.contract + .connect(userA.signer) + .vouchByProfileId(userB.profileId, DEFAULT.COMMENT, DEFAULT.METADATA, { value: vouchAmount }); + const receipt = await vouchTx.wait(); + + if (!receipt) { + expect.fail('Transaction receipt not found'); + } + + // Get transaction cost + const gasCost = receipt.gasUsed * receipt.gasPrice; + + // Get the vouch details + const vouch = await userA.deployer.ethosVouch.contract.verifiedVouchByAuthorForSubjectProfileId( + userA.profileId, + userB.profileId, + ); + + // Check vouch balance + const vouchBalance = await userA.getVouchBalance(vouch.vouchId); + + // Expected: full amount minus gas (no fees since there were no previous vouchers) + expect(vouchBalance).to.equal(vouchAmount, 'Vouch balance should be the full amount'); + + // Verify user's ETH balance reflects this + const finalBalance = await userA.getBalance(); + const expectedFinalBalance = initialBalance - vouchAmount - gasCost; + expect(finalBalance).to.equal(expectedFinalBalance, 'User balance should reflect the vouch'); + }); + + it('should correctly calculate all fees for initial vouch of 1 ETH times total fee percentage', async () => { + // Set up all fees + await setupFees(deployer); + + // Calculate total amount needed using basis points (10000 = 100%) + const BASIS_POINTS = 10000n; + const totalFeeBasisPoints = entryFee + donationFee; // omit vouchIncentives because there's no previous vouchers + const oneEth = ethers.parseEther('1.0'); + const vouchAmount = (oneEth * (BASIS_POINTS + totalFeeBasisPoints)) / BASIS_POINTS; + + await userA.setBalance((vouchAmount * 2n).toString()); + const protocolBalance = await ethers.provider.getBalance( + await deployer.ethosVouch.contract.protocolFeeAddress(), + ); + + // Calculate expected fee distribution + const { deposit: expectedDeposit, shares } = calcFeeDistribution(vouchAmount, { + entry: entryFee, + donation: donationFee, + vouchIncentives: 0n, + }); + expect(expectedDeposit).to.equal(oneEth, 'Expected deposit should be 1 ETH'); + + // Get initial balances + const initialProtocolBalance = await ethers.provider.getBalance( + await deployer.ethosVouch.contract.protocolFeeAddress(), + ); + const initialRecipientRewards = await userB.getRewardsBalance(); + + // Perform vouch + const { vouchId } = await userA.vouch(userB, { paymentAmount: vouchAmount }); + + // Get final balances + const [vouchBalance, recipientRewards, protocolFeeBalance] = await Promise.all([ + userA.getVouchBalance(vouchId), + userB.getRewardsBalance(), + ethers.provider.getBalance(await deployer.ethosVouch.contract.protocolFeeAddress()), + ]); + + // Calculate actual fees paid + const actualProtocolFee = protocolFeeBalance - initialProtocolBalance; + const actualDonationFee = recipientRewards - initialRecipientRewards; + expect(actualProtocolFee).to.equal(shares.protocol, 'Protocol fee matches calculation'); + expect(actualDonationFee).to.equal(shares.donation, 'Donation fee matches calculation'); + + // Vouch balance should be deposit amount (no vouchers pool fee since it's first vouch) + expect(vouchBalance).to.equal(oneEth, 'Vouch balance should be 1 ETH'); + + // Recipient should receive donation fee + expect(recipientRewards).to.equal(shares.donation, 'Recipient rewards should be donation fee'); + + // Protocol fee address should receive protocol fee + expect(protocolFeeBalance - protocolBalance).to.equal( + shares.protocol, + 'Protocol fee matches calculation', + ); }); async function getTotalFees(): Promise { diff --git a/ethos/packages/contracts/test/vouch/vouch.fees.test.ts.rej b/ethos/packages/contracts/test/vouch/vouch.fees.test.ts.rej new file mode 100644 index 0000000..259bbf5 --- /dev/null +++ b/ethos/packages/contracts/test/vouch/vouch.fees.test.ts.rej @@ -0,0 +1,289 @@ +diff a/ethos/packages/contracts/test/vouch/vouch.fees.test.ts b/ethos/packages/contracts/test/vouch/vouch.fees.test.ts (rejected hunks) +@@ -44,7 +44,7 @@ async function setupFees(deployer: EthosDeployer): Promise { + ); + } + +-describe('Vault Fees', () => { ++describe('Vouch Fees', () => { + let deployer: EthosDeployer; + let userA: EthosUser; + let userB: EthosUser; +@@ -105,13 +105,12 @@ describe('Vault Fees', () => { + const { deposit: initialDeposit } = calcFeeDistribution(paymentAmount, { + entry: entryFee, + donation: donationFee, +- vouchIncentives, ++ vouchIncentives: 0n, + }); + + const { vouchId } = await userA.vouch(userB); + const balance = await userA.getVouchBalance(vouchId); +- expect(balance).to.be.closeTo(initialDeposit, 1n); +- ++ expect(balance).to.be.closeTo(initialDeposit, 2n); + const balanceBeforeUnvouch = await userA.getBalance(); + const unvouchTx = await userA.unvouch(vouchId); + const receipt = await unvouchTx.wait(); +@@ -400,7 +399,6 @@ describe('Vault Fees', () => { + const protocolFee = 400n; // 4% + const donationFee = 300n; // 3% + const vouchersPoolFee = 200n; // 2% +- const totalFeeBasisPoints = protocolFee + donationFee + vouchersPoolFee; + + await Promise.all([ + deployer.ethosVouch.contract +@@ -422,12 +420,12 @@ describe('Vault Fees', () => { + await userB.setBalance(ethers.parseEther('1000.0').toString()); + + // Calculate total fees first +- const totalFees = +- vouchAmount - (vouchAmount * BASIS_POINTS) / (BASIS_POINTS + totalFeeBasisPoints); ++ const firstVouchFeeBasisPoints = donationFee + protocolFee; ++ const firstVouchFees = ++ vouchAmount - (vouchAmount * BASIS_POINTS) / (BASIS_POINTS + firstVouchFeeBasisPoints); + + // Calculate individual fee shares +- const donationShare = (totalFees * donationFee) / totalFeeBasisPoints; +- const vouchersPoolShare = (totalFees * vouchersPoolFee) / totalFeeBasisPoints; ++ const firstVouchDonationShare = (firstVouchFees * donationFee) / firstVouchFeeBasisPoints; + + // Track initial rewards + const initialUserBRewards = await userB.getRewardsBalance(); +@@ -440,24 +438,234 @@ describe('Vault Fees', () => { + const rewardsIncrease = afterFirstVouchRewards - initialUserBRewards; + + // For first vouch, only donation fee goes to rewards +- expect(rewardsIncrease).to.equal(donationShare); ++ expect(rewardsIncrease).to.be.closeTo(firstVouchDonationShare, 1n); + + // Second vouch + const userC = await deployer.createUser(); + await userC.setBalance(ethers.parseEther('1000.0').toString()); ++ const secondVouchBasisPoints = donationFee + protocolFee + vouchersPoolFee; ++ const secondVouchFees = ++ vouchAmount - (vouchAmount * BASIS_POINTS) / (BASIS_POINTS + secondVouchBasisPoints); ++ const secondVouchVouchersPoolShare = ++ (secondVouchFees * vouchersPoolFee) / secondVouchBasisPoints; + + // Track first voucher's balance before second vouch + const beforeSecondVouchBalance = await userA.getVouchBalance(firstVouchId); + + // Make second vouch +- await userC.vouch(userB, { paymentAmount: vouchAmount }); ++ const { vouchId: secondVouchId } = await userC.vouch(userB, { paymentAmount: vouchAmount }); ++ const secondVouchBalance = await userA.getVouchBalance(secondVouchId); + + // Check first voucher's balance after second vouch + const afterSecondVouchBalance = await userA.getVouchBalance(firstVouchId); + const voucherBalanceIncrease = afterSecondVouchBalance - beforeSecondVouchBalance; + ++ // Third vouch ++ const userD = await deployer.createUser(); ++ await userD.setBalance(ethers.parseEther('1000.0').toString()); ++ const { vouchId: thirdVouchId } = await userD.vouch(userB, { paymentAmount: vouchAmount }); ++ const thirdVouchBalance = await userA.getVouchBalance(thirdVouchId); ++ + // First voucher should receive the vouchers pool fee from second vouch (allow 1 wei difference) +- expect(voucherBalanceIncrease).to.be.closeTo(vouchersPoolShare, 1n); ++ expect(voucherBalanceIncrease).to.be.closeTo(secondVouchVouchersPoolShare, 1n); ++ // First vouch balance should be greater than second vouch balance ++ expect(beforeSecondVouchBalance).to.gt(secondVouchBalance); ++ // All the consequent vouches should have the same balance ++ expect(secondVouchBalance).to.equal(thirdVouchBalance); ++ }); ++ ++ it('should limit voucher rewards to their vouch amount when they are the only voucher', async () => { ++ // Set up a scenario where vouchers pool fee is high ++ const vouchersPoolFee = 1000n; // 10% - maximum allowed fee ++ await deployer.ethosVouch.contract ++ .connect(deployer.ADMIN) ++ .setEntryVouchersPoolFeeBasisPoints(vouchersPoolFee); ++ ++ // First vouch - small amount ++ const smallVouchAmount = paymentAmount; // Use default payment amount ++ await userA.setBalance((paymentAmount * 10n).toString()); ++ const { vouchId: firstVouchId } = await userA.vouch(userB, { paymentAmount: smallVouchAmount }); ++ ++ // Second vouch - large amount (100x first vouch) ++ const largeVouchAmount = paymentAmount * 1000n; ++ const userC = await deployer.createUser(); ++ await userC.setBalance((largeVouchAmount * 2n).toString()); ++ ++ // Get userA's vouch balance before second vouch ++ const balanceBeforeSecondVouch = await userA.getVouchBalance(firstVouchId); ++ ++ // Make second vouch ++ await userC.vouch(userB, { paymentAmount: largeVouchAmount }); ++ ++ // Get userA's vouch balance after second vouch ++ const balanceAfterSecondVouch = await userA.getVouchBalance(firstVouchId); ++ const rewardAmount = balanceAfterSecondVouch - balanceBeforeSecondVouch; ++ ++ // The reward should not exceed the original vouch amount ++ expect(rewardAmount).to.be.lte(smallVouchAmount); ++ }); ++ ++ it('should limit voucher rewards when there are multiple eligible vouchers', async () => { ++ // Set up a scenario where vouchers pool fee is high ++ const vouchersPoolFee = 1000n; // 10% - maximum allowed fee ++ await deployer.ethosVouch.contract ++ .connect(deployer.ADMIN) ++ .setEntryVouchersPoolFeeBasisPoints(vouchersPoolFee); ++ ++ // Third vouch - large amount (1000x initial vouch) ++ const largeVouchAmount = paymentAmount * 1000n; ++ const userD = await deployer.createUser(); ++ ++ // First two vouches - equal amounts ++ const initialVouchAmount = paymentAmount; ++ await Promise.all([ ++ userA.setBalance((paymentAmount * 20n).toString()), ++ userB.setBalance((paymentAmount * 20n).toString()), ++ userD.setBalance((largeVouchAmount * 20n).toString()), ++ ]); ++ ++ const userC = await deployer.createUser(); ++ const { vouchId: firstVouchId } = await userA.vouch(userC, { ++ paymentAmount: initialVouchAmount, ++ }); ++ const { vouchId: secondVouchId } = await userB.vouch(userC, { ++ paymentAmount: initialVouchAmount, ++ }); ++ ++ // Get balances before third vouch ++ const [firstVoucherInitialBalance, secondVoucherInitialBalance] = await Promise.all([ ++ userA.getVouchBalance(firstVouchId), ++ userB.getVouchBalance(secondVouchId), ++ ]); ++ ++ // Make third vouch ++ await userD.vouch(userC, { paymentAmount: largeVouchAmount }); ++ ++ // Get balances after third vouch ++ const [firstVoucherFinalBalance, secondVoucherFinalBalance] = await Promise.all([ ++ userA.getVouchBalance(firstVouchId), ++ userB.getVouchBalance(secondVouchId), ++ ]); ++ ++ // Calculate rewards for each voucher explicitly ++ const rewardsForFirstVoucher = firstVoucherFinalBalance - firstVoucherInitialBalance; ++ const rewardsForSecondVoucher = secondVoucherFinalBalance - secondVoucherInitialBalance; ++ // The rewards don't exceed the original vouch amount since there are multiple vouchers ++ expect(rewardsForFirstVoucher).to.be.lte( ++ firstVoucherInitialBalance, ++ 'Rewards for first voucher should be less than or equal to the vouch amount', ++ ); ++ expect(rewardsForSecondVoucher).to.be.lte( ++ firstVoucherInitialBalance, ++ 'Rewards for second voucher should be less than or equal to the vouch amount', ++ ); ++ }); ++ ++ it('should not deduct vouchers pool fee when there are no previous vouchers', async () => { ++ // Set up fees - only vouchers pool fee ++ const vouchersPoolFee = 500n; // 5% ++ await deployer.ethosVouch.contract ++ .connect(deployer.ADMIN) ++ .setEntryVouchersPoolFeeBasisPoints(vouchersPoolFee); ++ ++ // Clear other fees to isolate the issue ++ await Promise.all([ ++ deployer.ethosVouch.contract.connect(deployer.ADMIN).setEntryProtocolFeeBasisPoints(0n), ++ deployer.ethosVouch.contract.connect(deployer.ADMIN).setEntryDonationFeeBasisPoints(0n), ++ ]); ++ ++ const vouchAmount = ethers.parseEther('1.0'); ++ // Convert to string without decimal points ++ await userA.setBalance(vouchAmount.toString()); ++ ++ // First vouch - should not deduct vouchers pool fee since there are no previous vouchers ++ const initialBalance = await userA.getBalance(); ++ const vouchTx = await userA.deployer.ethosVouch.contract ++ .connect(userA.signer) ++ .vouchByProfileId(userB.profileId, DEFAULT.COMMENT, DEFAULT.METADATA, { value: vouchAmount }); ++ const receipt = await vouchTx.wait(); ++ ++ if (!receipt) { ++ expect.fail('Transaction receipt not found'); ++ } ++ ++ // Get transaction cost ++ const gasCost = receipt.gasUsed * receipt.gasPrice; ++ ++ // Get the vouch details ++ const vouch = await userA.deployer.ethosVouch.contract.verifiedVouchByAuthorForSubjectProfileId( ++ userA.profileId, ++ userB.profileId, ++ ); ++ ++ // Check vouch balance ++ const vouchBalance = await userA.getVouchBalance(vouch.vouchId); ++ ++ // Expected: full amount minus gas (no fees since there were no previous vouchers) ++ expect(vouchBalance).to.equal(vouchAmount, 'Vouch balance should be the full amount'); ++ ++ // Verify user's ETH balance reflects this ++ const finalBalance = await userA.getBalance(); ++ const expectedFinalBalance = initialBalance - vouchAmount - gasCost; ++ expect(finalBalance).to.equal(expectedFinalBalance, 'User balance should reflect the vouch'); ++ }); ++ ++ it('should correctly calculate all fees for initial vouch of 1 ETH times total fee percentage', async () => { ++ // Set up all fees ++ await setupFees(deployer); ++ ++ // Calculate total amount needed using basis points (10000 = 100%) ++ const BASIS_POINTS = 10000n; ++ const totalFeeBasisPoints = entryFee + donationFee; // omit vouchIncentives because there's no previous vouchers ++ const oneEth = ethers.parseEther('1.0'); ++ const vouchAmount = (oneEth * (BASIS_POINTS + totalFeeBasisPoints)) / BASIS_POINTS; ++ ++ await userA.setBalance((vouchAmount * 2n).toString()); ++ const protocolBalance = await ethers.provider.getBalance( ++ await deployer.ethosVouch.contract.protocolFeeAddress(), ++ ); ++ ++ // Calculate expected fee distribution ++ const { deposit: expectedDeposit, shares } = calcFeeDistribution(vouchAmount, { ++ entry: entryFee, ++ donation: donationFee, ++ vouchIncentives: 0n, ++ }); ++ expect(expectedDeposit).to.equal(oneEth, 'Expected deposit should be 1 ETH'); ++ ++ // Get initial balances ++ const initialProtocolBalance = await ethers.provider.getBalance( ++ await deployer.ethosVouch.contract.protocolFeeAddress(), ++ ); ++ const initialRecipientRewards = await userB.getRewardsBalance(); ++ ++ // Perform vouch ++ const { vouchId } = await userA.vouch(userB, { paymentAmount: vouchAmount }); ++ ++ // Get final balances ++ const [vouchBalance, recipientRewards, protocolFeeBalance] = await Promise.all([ ++ userA.getVouchBalance(vouchId), ++ userB.getRewardsBalance(), ++ ethers.provider.getBalance(await deployer.ethosVouch.contract.protocolFeeAddress()), ++ ]); ++ ++ // Calculate actual fees paid ++ const actualProtocolFee = protocolFeeBalance - initialProtocolBalance; ++ const actualDonationFee = recipientRewards - initialRecipientRewards; ++ expect(actualProtocolFee).to.equal(shares.protocol, 'Protocol fee matches calculation'); ++ expect(actualDonationFee).to.equal(shares.donation, 'Donation fee matches calculation'); ++ ++ // Vouch balance should be deposit amount (no vouchers pool fee since it's first vouch) ++ expect(vouchBalance).to.equal(oneEth, 'Vouch balance should be 1 ETH'); ++ ++ // Recipient should receive donation fee ++ expect(recipientRewards).to.equal(shares.donation, 'Recipient rewards should be donation fee'); ++ ++ // Protocol fee address should receive protocol fee ++ expect(protocolFeeBalance - protocolBalance).to.equal( ++ shares.protocol, ++ 'Protocol fee matches calculation', ++ ); + }); + + async function getTotalFees(): Promise { diff --git a/ethos/packages/contracts/test/vouch/vouch.incentives.test.ts b/ethos/packages/contracts/test/vouch/vouch.incentives.test.ts index d4d6689..c582335 100644 --- a/ethos/packages/contracts/test/vouch/vouch.incentives.test.ts +++ b/ethos/packages/contracts/test/vouch/vouch.incentives.test.ts @@ -41,9 +41,8 @@ describe('Vouch Incentives', () => { const { deposit } = calcFeeDistribution(paymentAmount, { entry: 0n, donation: 0n, - vouchIncentives, + vouchIncentives: 0n, }); - expect(balance).to.be.closeTo(deposit, 1n); }); @@ -80,7 +79,7 @@ describe('Vouch Incentives', () => { const { deposit: firstDeposit } = calcFeeDistribution(paymentAmount, { entry: 0n, donation: 0n, - vouchIncentives, + vouchIncentives: 0n, // no vouch incentives for first vouch }); expect(vouch0InitialBalance).to.equal(firstDeposit); @@ -117,7 +116,7 @@ describe('Vouch Incentives', () => { const { deposit: deposit1 } = calcFeeDistribution(amount1, { entry: 0n, donation: 0n, - vouchIncentives, + vouchIncentives: 0n, // no vouch incentives for first vouch }); // Second vouch - fees to first voucher @@ -175,7 +174,7 @@ describe('Vouch Incentives', () => { const { deposit: firstDeposit } = calcFeeDistribution(paymentAmount, { entry: 0n, donation: 0n, - vouchIncentives, + vouchIncentives: 0n, // no vouch incentives for first vouch }); // Verify first vouch deposit @@ -195,7 +194,7 @@ describe('Vouch Incentives', () => { const { deposit: newFirstDeposit } = calcFeeDistribution(paymentAmount, { entry: 0n, donation: 0n, - vouchIncentives, + vouchIncentives: 0n, // no vouch incentives for first vouch }); // Verify new first vouch deposit @@ -212,7 +211,7 @@ describe('Vouch Incentives', () => { const { deposit: nextFirstDeposit } = calcFeeDistribution(paymentAmount, { entry: 0n, donation: 0n, - vouchIncentives, + vouchIncentives: 0n, // no vouch incentives for first vouch }); // Verify the new first vouch deposit @@ -293,7 +292,7 @@ describe('Vouch Incentives', () => { const { deposit: firstDeposit } = calcFeeDistribution(paymentAmount, { entry: 0n, donation: 0n, - vouchIncentives, + vouchIncentives: 0n, // no vouch incentives for first vouch }); // Verify first vouch deposit @@ -368,14 +367,14 @@ describe('Vouch Incentives', () => { const { deposit: initialDeposit } = calcFeeDistribution(initialAmount, { entry: 0n, donation: 0n, - vouchIncentives, + vouchIncentives: 0n, // no vouch incentives for first vouch }); // Calculate increase deposit const { deposit: increaseDeposit } = calcFeeDistribution(increaseAmount, { entry: 0n, donation: 0n, - vouchIncentives, + vouchIncentives: 0n, // no vouch incentives when increasing vouch where you're the only voucher }); // Increase vouch diff --git a/ethos/packages/contracts/test/vouch/vouch.incentives.test.ts.rej b/ethos/packages/contracts/test/vouch/vouch.incentives.test.ts.rej new file mode 100644 index 0000000..a499a15 --- /dev/null +++ b/ethos/packages/contracts/test/vouch/vouch.incentives.test.ts.rej @@ -0,0 +1,83 @@ +diff a/ethos/packages/contracts/test/vouch/vouch.incentives.test.ts b/ethos/packages/contracts/test/vouch/vouch.incentives.test.ts (rejected hunks) +@@ -41,9 +41,8 @@ describe('Vouch Incentives', () => { + const { deposit } = calcFeeDistribution(paymentAmount, { + entry: 0n, + donation: 0n, +- vouchIncentives, ++ vouchIncentives: 0n, + }); +- + expect(balance).to.be.closeTo(deposit, 1n); + }); + +@@ -80,7 +79,7 @@ describe('Vouch Incentives', () => { + const { deposit: firstDeposit } = calcFeeDistribution(paymentAmount, { + entry: 0n, + donation: 0n, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch incentives for first vouch + }); + expect(vouch0InitialBalance).to.equal(firstDeposit); + +@@ -117,7 +116,7 @@ describe('Vouch Incentives', () => { + const { deposit: deposit1 } = calcFeeDistribution(amount1, { + entry: 0n, + donation: 0n, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch incentives for first vouch + }); + + // Second vouch - fees to first voucher +@@ -175,7 +174,7 @@ describe('Vouch Incentives', () => { + const { deposit: firstDeposit } = calcFeeDistribution(paymentAmount, { + entry: 0n, + donation: 0n, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch incentives for first vouch + }); + + // Verify first vouch deposit +@@ -195,7 +194,7 @@ describe('Vouch Incentives', () => { + const { deposit: newFirstDeposit } = calcFeeDistribution(paymentAmount, { + entry: 0n, + donation: 0n, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch incentives for first vouch + }); + + // Verify new first vouch deposit +@@ -212,7 +211,7 @@ describe('Vouch Incentives', () => { + const { deposit: nextFirstDeposit } = calcFeeDistribution(paymentAmount, { + entry: 0n, + donation: 0n, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch incentives for first vouch + }); + + // Verify the new first vouch deposit +@@ -293,7 +292,7 @@ describe('Vouch Incentives', () => { + const { deposit: firstDeposit } = calcFeeDistribution(paymentAmount, { + entry: 0n, + donation: 0n, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch incentives for first vouch + }); + + // Verify first vouch deposit +@@ -368,14 +367,14 @@ describe('Vouch Incentives', () => { + const { deposit: initialDeposit } = calcFeeDistribution(initialAmount, { + entry: 0n, + donation: 0n, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch incentives for first vouch + }); + + // Calculate increase deposit + const { deposit: increaseDeposit } = calcFeeDistribution(increaseAmount, { + entry: 0n, + donation: 0n, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch incentives when increasing vouch where you're the only voucher + }); + + // Increase vouch diff --git a/ethos/packages/contracts/test/vouch/vouch.increase.test.ts b/ethos/packages/contracts/test/vouch/vouch.increase.test.ts index 0eff7e4..c0575b7 100644 --- a/ethos/packages/contracts/test/vouch/vouch.increase.test.ts +++ b/ethos/packages/contracts/test/vouch/vouch.increase.test.ts @@ -1,7 +1,9 @@ import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; import { expect } from 'chai'; import hre from 'hardhat'; +import { zeroHash, zeroAddress } from 'viem'; import { calcFeeDistribution, calculateFee } from '../utils/common.js'; +import { DEFAULT } from '../utils/defaults.js'; import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; import { type EthosUser } from '../utils/ethosUser.js'; @@ -40,18 +42,21 @@ describe('EthosVouch Increasing', () => { it('should successfully increase vouch amount', async () => { const { vouchId, balance } = await userA.vouch(userB, { paymentAmount: initialAmount }); - // Calculate fees and expected deposit - const { deposit } = calcFeeDistribution(increaseAmount, { + // Calculate fees and expected deposit, accounting for vouchers pool fee adjustment + const { deposit: depositWithPool, shares } = calcFeeDistribution(increaseAmount, { entry: entryFee, donation: donationFee, - vouchIncentives, + vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch }); + // When there are no previous vouchers, the vouchers pool fee is returned to the deposit + const deposit = depositWithPool + shares.vouchersPool; await deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(vouchId, { value: increaseAmount }); + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); const finalBalance = await userA.getVouchBalance(vouchId); + expect(finalBalance).to.be.closeTo(balance + deposit, 1n); }); @@ -61,7 +66,7 @@ describe('EthosVouch Increasing', () => { await expect( deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(vouchId, { value: increaseAmount }), + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }), ).to.emit(deployer.ethosVouch.contract, 'VouchIncreased'); }); @@ -75,7 +80,7 @@ describe('EthosVouch Increasing', () => { // Increase vouch await deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(vouchId, { value: increaseAmount }); + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); // Check protocol fee recipient's balance increased by expected amount const finalFeeBalance = await ethers.provider.getBalance(protocolFeeAddress); @@ -83,7 +88,7 @@ describe('EthosVouch Increasing', () => { const { shares } = calcFeeDistribution(increaseAmount, { entry: entryFee, donation: donationFee, - vouchIncentives, + vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch }); expect(finalFeeBalance - initialFeeBalance).to.equal(shares.protocol); @@ -98,7 +103,7 @@ describe('EthosVouch Increasing', () => { // Increase vouch await deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(vouchId, { value: increaseAmount }); + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); // Check userB's rewards balance increased by expected amount const finalRewardsBalance = await userB.getRewardsBalance(); @@ -106,10 +111,10 @@ describe('EthosVouch Increasing', () => { const { shares } = calcFeeDistribution(increaseAmount, { entry: entryFee, donation: donationFee, - vouchIncentives, + vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch }); - expect(finalRewardsBalance - initialRewardsBalance).to.equal(shares.donation); + expect(finalRewardsBalance - initialRewardsBalance).to.be.closeTo(shares.donation, 1n); }); it('should apply vouchers pool fee correctly on increased amount', async () => { @@ -131,7 +136,7 @@ describe('EthosVouch Increasing', () => { // Increase vouch await deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(vouchId, { value: increaseAmount }); + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); const finalBalanceC = await userC.getVouchBalance(vouchIdC); const actualIncrease = finalBalanceC - balanceC; @@ -146,7 +151,7 @@ describe('EthosVouch Increasing', () => { await expect( deployer.ethosVouch.contract .connect(userB.signer) - .increaseVouch(vouchId, { value: increaseAmount }), + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }), ) .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'NotAuthorForVouch') .withArgs(vouchId, userB.profileId); @@ -159,7 +164,7 @@ describe('EthosVouch Increasing', () => { await expect( deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(vouchId, { value: increaseAmount }), + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }), ) .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'AlreadyUnvouched') .withArgs(vouchId); @@ -171,7 +176,7 @@ describe('EthosVouch Increasing', () => { await expect( deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(nonExistentVouchId, { value: increaseAmount }), + .increaseVouch(nonExistentVouchId, zeroHash, zeroAddress, { value: increaseAmount }), ) .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'NotAuthorForVouch') .withArgs(nonExistentVouchId, userA.profileId); @@ -182,28 +187,38 @@ describe('EthosVouch Increasing', () => { const initialBalance = await userA.getVouchBalance(vouchId); // Calculate fees for first increase - const { deposit: firstDeposit } = calcFeeDistribution(increaseAmount, { - entry: entryFee, - donation: donationFee, - vouchIncentives, - }); + const { deposit: firstDepositWithPool, shares: firstShares } = calcFeeDistribution( + increaseAmount, + { + entry: entryFee, + donation: donationFee, + vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch + }, + ); + // When there are no previous vouchers, the vouchers pool fee is returned to the deposit + const firstDeposit = firstDepositWithPool + firstShares.vouchersPool; // First increase await deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(vouchId, { value: increaseAmount }); + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); // Calculate fees for second increase - const { deposit: secondDeposit } = calcFeeDistribution(increaseAmount, { - entry: entryFee, - donation: donationFee, - vouchIncentives, - }); + const { deposit: secondDepositWithPool, shares: secondShares } = calcFeeDistribution( + increaseAmount, + { + entry: entryFee, + donation: donationFee, + vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch + }, + ); + // When there are no previous vouchers, the vouchers pool fee is returned to the deposit + const secondDeposit = secondDepositWithPool + secondShares.vouchersPool; // Second increase await deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(vouchId, { value: increaseAmount }); + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); // The balance should have increased by both deposits const finalBalance = await userA.getVouchBalance(vouchId); @@ -211,14 +226,16 @@ describe('EthosVouch Increasing', () => { const expectedIncrease = firstDeposit + secondDeposit; // Allow for small rounding differences - expect(actualIncrease).to.be.closeTo(expectedIncrease, 1n); + expect(actualIncrease).to.be.closeTo(expectedIncrease, 5n); }); it('should revert increase with zero value', async () => { const { vouchId } = await userA.vouch(userB, { paymentAmount: initialAmount }); await expect( - deployer.ethosVouch.contract.connect(userA.signer).increaseVouch(vouchId, { value: 0 }), + deployer.ethosVouch.contract + .connect(userA.signer) + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: 0 }), ) .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'MinimumVouchAmount') .withArgs(await deployer.ethosVouch.contract.configuredMinimumVouchAmount()); @@ -233,7 +250,7 @@ describe('EthosVouch Increasing', () => { await expect( deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(vouchId, { value: increaseAmount }), + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }), ) .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'FeeTransferFailed') .withArgs('Protocol fee deposit failed'); @@ -246,20 +263,25 @@ describe('EthosVouch Increasing', () => { const { deposit: initialDeposit } = calcFeeDistribution(initialAmount, { entry: entryFee, donation: donationFee, - vouchIncentives, + vouchIncentives: 0n, // no vouch pool incentives when first vouch }); // Increase vouch await deployer.ethosVouch.contract .connect(userA.signer) - .increaseVouch(vouchId, { value: increaseAmount }); - - // Calculate increase deposit after entry fees - const { deposit: increaseDeposit } = calcFeeDistribution(increaseAmount, { - entry: entryFee, - donation: donationFee, - vouchIncentives, - }); + .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); + + // Calculate increase deposit after fees, accounting for vouchers pool fee adjustment + const { deposit: increaseDepositWithPool, shares: increaseShares } = calcFeeDistribution( + increaseAmount, + { + entry: entryFee, + donation: donationFee, + vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch + }, + ); + // When there are no previous vouchers, the vouchers pool fee is returned to the deposit + const increaseDeposit = increaseDepositWithPool + increaseShares.vouchersPool; // Get userA's balance before unvouching const balanceBefore = await ethers.provider.getBalance(userA.signer.address); @@ -282,6 +304,40 @@ describe('EthosVouch Increasing', () => { const actualReturn = balanceAfter - balanceBefore + gasUsed; // Allow for small rounding differences - expect(actualReturn).to.be.closeTo(expectedReturn, 1n); + expect(actualReturn).to.be.closeTo(expectedReturn, 5n); + }); + + it('should successfully increase vouch amount for mock profile (address-based vouch)', async () => { + // Generate a mock address using ethers Wallet + const mockWallet = ethers.Wallet.createRandom(); + const mockAddress = mockWallet.address; + + // Review the address + await userA.review({ address: mockAddress }); + // Initial vouch by address for mock profile + await deployer.ethosVouch.contract + .connect(userA.signer) + .vouchByAddress(mockAddress, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: initialAmount, + }); + const vouchId = (await deployer.ethosVouch.contract.vouchCount()) - 1n; + const balance = await userA.getVouchBalance(vouchId); + // Calculate fees and expected deposit for increase + const { deposit: depositWithPool, shares } = calcFeeDistribution(increaseAmount, { + entry: entryFee, + donation: donationFee, + vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch + }); + // When there are no previous vouchers, the vouchers pool fee is returned to the deposit + const deposit = depositWithPool + shares.vouchersPool; + + // Increase vouch - note we pass the mock address since this was an address-based vouch + await deployer.ethosVouch.contract + .connect(userA.signer) + .increaseVouch(vouchId, zeroHash, mockAddress, { value: increaseAmount }); + + const finalBalance = await userA.getVouchBalance(vouchId); + + expect(finalBalance).to.be.closeTo(balance + deposit, 1n); }); }); diff --git a/ethos/packages/contracts/test/vouch/vouch.increase.test.ts.rej b/ethos/packages/contracts/test/vouch/vouch.increase.test.ts.rej new file mode 100644 index 0000000..3dbcc0b --- /dev/null +++ b/ethos/packages/contracts/test/vouch/vouch.increase.test.ts.rej @@ -0,0 +1,278 @@ +diff a/ethos/packages/contracts/test/vouch/vouch.increase.test.ts b/ethos/packages/contracts/test/vouch/vouch.increase.test.ts (rejected hunks) +@@ -1,7 +1,9 @@ + import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; + import { expect } from 'chai'; + import hre from 'hardhat'; ++import { zeroHash, zeroAddress } from 'viem'; + import { calcFeeDistribution, calculateFee } from '../utils/common.js'; ++import { DEFAULT } from '../utils/defaults.js'; + import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; + import { type EthosUser } from '../utils/ethosUser.js'; + +@@ -40,18 +42,21 @@ describe('EthosVouch Increasing', () => { + it('should successfully increase vouch amount', async () => { + const { vouchId, balance } = await userA.vouch(userB, { paymentAmount: initialAmount }); + +- // Calculate fees and expected deposit +- const { deposit } = calcFeeDistribution(increaseAmount, { ++ // Calculate fees and expected deposit, accounting for vouchers pool fee adjustment ++ const { deposit: depositWithPool, shares } = calcFeeDistribution(increaseAmount, { + entry: entryFee, + donation: donationFee, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch + }); ++ // When there are no previous vouchers, the vouchers pool fee is returned to the deposit ++ const deposit = depositWithPool + shares.vouchersPool; + + await deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(vouchId, { value: increaseAmount }); ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); + + const finalBalance = await userA.getVouchBalance(vouchId); ++ + expect(finalBalance).to.be.closeTo(balance + deposit, 1n); + }); + +@@ -61,7 +66,7 @@ describe('EthosVouch Increasing', () => { + await expect( + deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(vouchId, { value: increaseAmount }), ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }), + ).to.emit(deployer.ethosVouch.contract, 'VouchIncreased'); + }); + +@@ -75,7 +80,7 @@ describe('EthosVouch Increasing', () => { + // Increase vouch + await deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(vouchId, { value: increaseAmount }); ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); + + // Check protocol fee recipient's balance increased by expected amount + const finalFeeBalance = await ethers.provider.getBalance(protocolFeeAddress); +@@ -83,7 +88,7 @@ describe('EthosVouch Increasing', () => { + const { shares } = calcFeeDistribution(increaseAmount, { + entry: entryFee, + donation: donationFee, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch + }); + + expect(finalFeeBalance - initialFeeBalance).to.equal(shares.protocol); +@@ -98,7 +103,7 @@ describe('EthosVouch Increasing', () => { + // Increase vouch + await deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(vouchId, { value: increaseAmount }); ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); + + // Check userB's rewards balance increased by expected amount + const finalRewardsBalance = await userB.getRewardsBalance(); +@@ -106,10 +111,10 @@ describe('EthosVouch Increasing', () => { + const { shares } = calcFeeDistribution(increaseAmount, { + entry: entryFee, + donation: donationFee, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch + }); + +- expect(finalRewardsBalance - initialRewardsBalance).to.equal(shares.donation); ++ expect(finalRewardsBalance - initialRewardsBalance).to.be.closeTo(shares.donation, 1n); + }); + + it('should apply vouchers pool fee correctly on increased amount', async () => { +@@ -131,7 +136,7 @@ describe('EthosVouch Increasing', () => { + // Increase vouch + await deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(vouchId, { value: increaseAmount }); ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); + + const finalBalanceC = await userC.getVouchBalance(vouchIdC); + const actualIncrease = finalBalanceC - balanceC; +@@ -146,7 +151,7 @@ describe('EthosVouch Increasing', () => { + await expect( + deployer.ethosVouch.contract + .connect(userB.signer) +- .increaseVouch(vouchId, { value: increaseAmount }), ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }), + ) + .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'NotAuthorForVouch') + .withArgs(vouchId, userB.profileId); +@@ -159,7 +164,7 @@ describe('EthosVouch Increasing', () => { + await expect( + deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(vouchId, { value: increaseAmount }), ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }), + ) + .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'AlreadyUnvouched') + .withArgs(vouchId); +@@ -171,7 +176,7 @@ describe('EthosVouch Increasing', () => { + await expect( + deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(nonExistentVouchId, { value: increaseAmount }), ++ .increaseVouch(nonExistentVouchId, zeroHash, zeroAddress, { value: increaseAmount }), + ) + .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'NotAuthorForVouch') + .withArgs(nonExistentVouchId, userA.profileId); +@@ -182,28 +187,38 @@ describe('EthosVouch Increasing', () => { + const initialBalance = await userA.getVouchBalance(vouchId); + + // Calculate fees for first increase +- const { deposit: firstDeposit } = calcFeeDistribution(increaseAmount, { +- entry: entryFee, +- donation: donationFee, +- vouchIncentives, +- }); ++ const { deposit: firstDepositWithPool, shares: firstShares } = calcFeeDistribution( ++ increaseAmount, ++ { ++ entry: entryFee, ++ donation: donationFee, ++ vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch ++ }, ++ ); ++ // When there are no previous vouchers, the vouchers pool fee is returned to the deposit ++ const firstDeposit = firstDepositWithPool + firstShares.vouchersPool; + + // First increase + await deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(vouchId, { value: increaseAmount }); ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); + + // Calculate fees for second increase +- const { deposit: secondDeposit } = calcFeeDistribution(increaseAmount, { +- entry: entryFee, +- donation: donationFee, +- vouchIncentives, +- }); ++ const { deposit: secondDepositWithPool, shares: secondShares } = calcFeeDistribution( ++ increaseAmount, ++ { ++ entry: entryFee, ++ donation: donationFee, ++ vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch ++ }, ++ ); ++ // When there are no previous vouchers, the vouchers pool fee is returned to the deposit ++ const secondDeposit = secondDepositWithPool + secondShares.vouchersPool; + + // Second increase + await deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(vouchId, { value: increaseAmount }); ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); + + // The balance should have increased by both deposits + const finalBalance = await userA.getVouchBalance(vouchId); +@@ -211,14 +226,16 @@ describe('EthosVouch Increasing', () => { + const expectedIncrease = firstDeposit + secondDeposit; + + // Allow for small rounding differences +- expect(actualIncrease).to.be.closeTo(expectedIncrease, 1n); ++ expect(actualIncrease).to.be.closeTo(expectedIncrease, 5n); + }); + + it('should revert increase with zero value', async () => { + const { vouchId } = await userA.vouch(userB, { paymentAmount: initialAmount }); + + await expect( +- deployer.ethosVouch.contract.connect(userA.signer).increaseVouch(vouchId, { value: 0 }), ++ deployer.ethosVouch.contract ++ .connect(userA.signer) ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: 0 }), + ) + .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'MinimumVouchAmount') + .withArgs(await deployer.ethosVouch.contract.configuredMinimumVouchAmount()); +@@ -233,7 +250,7 @@ describe('EthosVouch Increasing', () => { + await expect( + deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(vouchId, { value: increaseAmount }), ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }), + ) + .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'FeeTransferFailed') + .withArgs('Protocol fee deposit failed'); +@@ -246,20 +263,25 @@ describe('EthosVouch Increasing', () => { + const { deposit: initialDeposit } = calcFeeDistribution(initialAmount, { + entry: entryFee, + donation: donationFee, +- vouchIncentives, ++ vouchIncentives: 0n, // no vouch pool incentives when first vouch + }); + + // Increase vouch + await deployer.ethosVouch.contract + .connect(userA.signer) +- .increaseVouch(vouchId, { value: increaseAmount }); +- +- // Calculate increase deposit after entry fees +- const { deposit: increaseDeposit } = calcFeeDistribution(increaseAmount, { +- entry: entryFee, +- donation: donationFee, +- vouchIncentives, +- }); ++ .increaseVouch(vouchId, zeroHash, zeroAddress, { value: increaseAmount }); ++ ++ // Calculate increase deposit after fees, accounting for vouchers pool fee adjustment ++ const { deposit: increaseDepositWithPool, shares: increaseShares } = calcFeeDistribution( ++ increaseAmount, ++ { ++ entry: entryFee, ++ donation: donationFee, ++ vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch ++ }, ++ ); ++ // When there are no previous vouchers, the vouchers pool fee is returned to the deposit ++ const increaseDeposit = increaseDepositWithPool + increaseShares.vouchersPool; + + // Get userA's balance before unvouching + const balanceBefore = await ethers.provider.getBalance(userA.signer.address); +@@ -282,6 +304,40 @@ describe('EthosVouch Increasing', () => { + const actualReturn = balanceAfter - balanceBefore + gasUsed; + + // Allow for small rounding differences +- expect(actualReturn).to.be.closeTo(expectedReturn, 1n); ++ expect(actualReturn).to.be.closeTo(expectedReturn, 5n); ++ }); ++ ++ it('should successfully increase vouch amount for mock profile (address-based vouch)', async () => { ++ // Generate a mock address using ethers Wallet ++ const mockWallet = ethers.Wallet.createRandom(); ++ const mockAddress = mockWallet.address; ++ ++ // Review the address ++ await userA.review({ address: mockAddress }); ++ // Initial vouch by address for mock profile ++ await deployer.ethosVouch.contract ++ .connect(userA.signer) ++ .vouchByAddress(mockAddress, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: initialAmount, ++ }); ++ const vouchId = (await deployer.ethosVouch.contract.vouchCount()) - 1n; ++ const balance = await userA.getVouchBalance(vouchId); ++ // Calculate fees and expected deposit for increase ++ const { deposit: depositWithPool, shares } = calcFeeDistribution(increaseAmount, { ++ entry: entryFee, ++ donation: donationFee, ++ vouchIncentives: 0n, // no vouch pool incentives when increasing your own vouch ++ }); ++ // When there are no previous vouchers, the vouchers pool fee is returned to the deposit ++ const deposit = depositWithPool + shares.vouchersPool; ++ ++ // Increase vouch - note we pass the mock address since this was an address-based vouch ++ await deployer.ethosVouch.contract ++ .connect(userA.signer) ++ .increaseVouch(vouchId, zeroHash, mockAddress, { value: increaseAmount }); ++ ++ const finalBalance = await userA.getVouchBalance(vouchId); ++ ++ expect(finalBalance).to.be.closeTo(balance + deposit, 1n); + }); + }); diff --git a/ethos/packages/contracts/test/vouch/vouch.rewards.test.ts b/ethos/packages/contracts/test/vouch/vouch.rewards.test.ts index 2dc5af5..79fd115 100644 --- a/ethos/packages/contracts/test/vouch/vouch.rewards.test.ts +++ b/ethos/packages/contracts/test/vouch/vouch.rewards.test.ts @@ -1,7 +1,7 @@ import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; import { expect } from 'chai'; import hre from 'hardhat'; -import { calculateFee } from '../utils/common.js'; +import { calcFeeDistribution } from '../utils/common.js'; import { DEFAULT } from '../utils/defaults.js'; import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; import { type EthosUser } from '../utils/ethosUser.js'; @@ -13,6 +13,7 @@ describe('Vouch Rewards', () => { let userA: EthosUser; let userB: EthosUser; const donationFee = 150n; + const vouchersPoolFee = 150n; beforeEach(async () => { deployer = await loadFixture(createDeployer); @@ -23,19 +24,28 @@ describe('Vouch Rewards', () => { await deployer.ethosVouch.contract .connect(deployer.ADMIN) .setEntryDonationFeeBasisPoints(donationFee); + await deployer.ethosVouch.contract + .connect(deployer.ADMIN) + .setEntryVouchersPoolFeeBasisPoints(vouchersPoolFee); } it('should allow withdrawing accumulated rewards', async () => { - const paymentAmount = ethers.parseEther('0.1'); await setupDonationFee(); // Create a vouch to generate rewards for userB - await userA.vouch(userB, { paymentAmount }); + await userA.vouch(userB); const initialBalance = await userB.getBalance(); // Get rewards balance const rewardsBalance = await userB.getRewardsBalance(); - expect(rewardsBalance).to.equal(calculateFee(paymentAmount, donationFee).fee); + const { + shares: { donation }, + } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { + entry: 0n, + donation: donationFee, + vouchIncentives: 0n, // no vouch pool incentives for first vouch + }); + expect(rewardsBalance).to.equal(donation); // Withdraw rewards const withdrawTx = await deployer.ethosVouch.contract.connect(userB.signer).claimRewards(); @@ -58,16 +68,29 @@ describe('Vouch Rewards', () => { }); it('should accumulate rewards from multiple vouches', async () => { - const paymentAmount = ethers.parseEther('0.1'); await setupDonationFee(); // Create multiple vouches to generate rewards from different users const userC = await deployer.createUser(); - await userA.vouch(userB, { paymentAmount }); - await userC.vouch(userB, { paymentAmount }); + await userA.vouch(userB); + const { + shares: { donation: userADonation }, + } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { + entry: 0n, + donation: donationFee, + vouchIncentives: 0n, // no vouch pool incentives for first vouch + }); + await userC.vouch(userB); + const { + shares: { donation: userCDonation }, + } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { + entry: 0n, + donation: donationFee, + vouchIncentives: vouchersPoolFee, + }); const rewardsBalance = await userB.getRewardsBalance(); - const expectedRewards = calculateFee(paymentAmount, donationFee).fee * 2n; + const expectedRewards = userADonation + userCDonation; expect(rewardsBalance).to.equal(expectedRewards); }); @@ -79,9 +102,8 @@ describe('Vouch Rewards', () => { it('should handle failed reward withdrawals gracefully', async () => { // Generate some rewards - const paymentAmount = ethers.parseEther('0.1'); await setupDonationFee(); - await userA.vouch(userB, { paymentAmount }); + await userA.vouch(userB); // Try to withdraw with a contract that doesn't accept ETH const nonPayableContract = await deployer.createUser(); // Using a regular user account instead of mock @@ -90,39 +112,233 @@ describe('Vouch Rewards', () => { ).to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'InsufficientRewardsBalance'); }); + it('should redirect vouchByAttestation to vouchByProfileId for verified profiles', async () => { + await setupDonationFee(); + + // First create an attestation for userB + const attestationHash = await userB.attest({ + service: DEFAULT.SERVICE_X, + account: DEFAULT.ACCOUNT_NAME_EXAMPLE, + }); + + // Try to vouch using vouchByAttestation - should work since profile is verified + await deployer.ethosVouch.contract + .connect(userA.signer) + .vouchByAttestation(attestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); + + // Verify the vouch was created correctly + const vouch = await deployer.ethosVouch.contract.verifiedVouchByAuthorForSubjectProfileId( + userA.profileId, + userB.profileId, + ); + + expect(vouch.authorProfileId).to.equal(userA.profileId); + expect(vouch.subjectProfileId).to.equal(userB.profileId); + expect(vouch.comment).to.equal(DEFAULT.COMMENT); + expect(vouch.metadata).to.equal(DEFAULT.METADATA); + + // Verify rewards were distributed correctly + const rewardsBalance = await userB.getRewardsBalance(); + const { + shares: { donation }, + } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { + entry: 0n, + donation: donationFee, + vouchIncentives: 0n, // first vouch does not apply vouch rewards + }); + expect(rewardsBalance).to.equal(donation); + }); + it('should correctly track rewards across multiple recipients', async () => { - const paymentAmount = ethers.parseEther('0.1'); await setupDonationFee(); const userC = await deployer.createUser(); // Generate rewards for multiple users - await userA.vouch(userB, { paymentAmount }); - await userA.vouch(userC, { paymentAmount }); + await userA.vouch(userB); + await userA.vouch(userC); + + const { + shares: { donation }, + } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { + entry: 0n, + donation: donationFee, + vouchIncentives: 0n, // first vouch does not apply vouch rewards + }); + expect(await userB.getRewardsBalance()).to.equal(donation); + expect(await userC.getRewardsBalance()).to.equal(donation); + }); + + it('should distribute rewards correctly when increasing attestation-based vouch', async () => { + await setupDonationFee(); + + // First create an attestation for userB + const attestationHash = await userB.attest({ + service: DEFAULT.SERVICE_X, + account: DEFAULT.ACCOUNT_NAME_EXAMPLE, + }); + + // Initial vouch by userA using attestation + await deployer.ethosVouch.contract + .connect(userA.signer) + .vouchByAttestation(attestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); - const expectedReward = calculateFee(paymentAmount, donationFee).fee; - expect(await userB.getRewardsBalance()).to.equal(expectedReward); - expect(await userC.getRewardsBalance()).to.equal(expectedReward); + // Create another user to vouch for the same attestation + const userC = await deployer.createUser(); + await deployer.ethosVouch.contract + .connect(userC.signer) + .vouchByAttestation(attestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); + + // Get initial rewards balance + const initialRewards = + await deployer.ethosVouch.contract.rewardsByAttestationHash(attestationHash); + + // Get userC's initial vouch balance + const userCVouchInitial = await deployer.ethosVouch.contract.vouches(1); // Second vouch ID + const initialBalance = userCVouchInitial.balance; + + // Increase vouch amount for userA's vouch + const increaseAmount = DEFAULT.PAYMENT_AMOUNT; + const vouchId = 0; // First vouch ID + const tx = await deployer.ethosVouch.contract + .connect(userA.signer) + .increaseVouch(vouchId, attestationHash, userB.signer.address, { value: increaseAmount }); + + // Wait for the transaction to be mined and get the receipt + const receipt = await tx.wait(); + + if (!receipt) { + throw new Error('Transaction failed'); + } + + // Verify rewards were distributed correctly + const finalRewards = + await deployer.ethosVouch.contract.rewardsByAttestationHash(attestationHash); + const { + shares: { donation }, + } = calcFeeDistribution(increaseAmount, { + entry: 0n, + donation: donationFee, + vouchIncentives: vouchersPoolFee, + }); + // Allow for 1 wei rounding difference + expect(finalRewards - initialRewards).to.be.oneOf([donation, donation + 1n]); + + // Get userC's final vouch balance and verify it increased by their share of the vouchers pool fee + const userCVouchFinal = await deployer.ethosVouch.contract.vouches(1); + const { + shares: { vouchersPool }, + } = calcFeeDistribution(increaseAmount, { + entry: 0n, + donation: donationFee, + vouchIncentives: vouchersPoolFee, + }); + // Allow for 1 wei rounding difference + expect(userCVouchFinal.balance - initialBalance).to.be.oneOf([vouchersPool, vouchersPool + 1n]); + }); + + it('should distribute rewards correctly when increasing address-based vouch', async () => { + await setupDonationFee(); + + // Create a profile for userB first + await userB.registerAddress(userB.signer.address); + + // Initial vouch by userA + await deployer.ethosVouch.contract + .connect(userA.signer) + .vouchByAddress(userB.signer.address, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); + + // Create another user to vouch for the same address + const userC = await deployer.createUser(); + await deployer.ethosVouch.contract + .connect(userC.signer) + .vouchByAddress(userB.signer.address, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); + + // Get initial rewards balance + const initialRewards = await deployer.ethosVouch.contract.rewardsByAddress( + userB.signer.address, + ); + + // Get userC's initial vouch balance + const userCVouchInitial = await deployer.ethosVouch.contract.vouches(1); + const initialBalance = userCVouchInitial.balance; + + // Increase vouch amount for userA's vouch + const increaseAmount = DEFAULT.PAYMENT_AMOUNT; + const vouchId = 0; // First vouch ID + const tx = await deployer.ethosVouch.contract + .connect(userA.signer) + .increaseVouch( + vouchId, + '0x0000000000000000000000000000000000000000000000000000000000000000', + userB.signer.address, + { value: increaseAmount }, + ); + + // Wait for the transaction to be mined and get the receipt + const receipt = await tx.wait(); + + if (!receipt) { + throw new Error('Transaction failed'); + } + + // Verify rewards were distributed correctly + const finalRewards = await deployer.ethosVouch.contract.rewardsByAddress(userB.signer.address); + const { + shares: { donation }, + } = calcFeeDistribution(increaseAmount, { + entry: 0n, + donation: donationFee, + vouchIncentives: vouchersPoolFee, + }); + // Allow for 1 wei rounding difference + expect(finalRewards - initialRewards).to.be.oneOf([donation, donation + 1n]); + + // Get userC's final vouch balance and verify it increased by their share of the vouchers pool fee + const userCVouchFinal = await deployer.ethosVouch.contract.vouches(1); + const { + shares: { vouchersPool }, + } = calcFeeDistribution(increaseAmount, { + entry: 0n, + donation: donationFee, + vouchIncentives: vouchersPoolFee, + }); + // Allow for 1 wei rounding difference + expect(userCVouchFinal.balance - initialBalance).to.be.oneOf([vouchersPool, vouchersPool + 1n]); }); it('should emit DepositedToRewards event when rewards are generated', async () => { - const paymentAmount = ethers.parseEther('0.1'); await setupDonationFee(); - const expectedFee = calculateFee(paymentAmount, donationFee).fee; + const { + shares: { donation }, + } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { + entry: 0n, + donation: donationFee, + vouchIncentives: 0n, + }); - await userA.vouch(userB, { paymentAmount }); + await userA.vouch(userB); const filter = deployer.ethosVouch.contract.filters.DepositedToRewards(userB.profileId); const events = await deployer.ethosVouch.contract.queryFilter(filter); expect(events.length).to.equal(1); expect(events[0].args?.[0]).to.equal(userB.profileId); - expect(events[0].args?.[1]).to.equal(expectedFee); + expect(events[0].args?.[1]).to.equal(donation); }); it('should emit WithdrawnFromRewards event when rewards are withdrawn', async () => { - const paymentAmount = ethers.parseEther('0.1'); await setupDonationFee(); - await userA.vouch(userB, { paymentAmount }); + await userA.vouch(userB); const rewardsBalance = await userB.getRewardsBalance(); await expect(deployer.ethosVouch.contract.connect(userB.signer).claimRewards()) @@ -131,9 +347,8 @@ describe('Vouch Rewards', () => { }); it('should handle rewards for archived profiles', async () => { - const paymentAmount = ethers.parseEther('0.1'); await setupDonationFee(); - await userA.vouch(userB, { paymentAmount }); + await userA.vouch(userB); // Create and archive userB's profile await deployer.ethosProfile.contract.connect(userB.signer).archiveProfile(); @@ -166,4 +381,82 @@ describe('Vouch Rewards', () => { .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'ProfileNotFoundForAddress') .withArgs(reviewedUser.address); }); + + it('should allow claiming rewards for an attestation mock profile after attestation is claimed', async () => { + await setupDonationFee(); + // leave a review for twitter account + await userA.review({ + attestationDetails: { + service: DEFAULT.SERVICE_X, + account: DEFAULT.ACCOUNT_NAME_EXAMPLE, + }, + }); + const attestationHash = await userA.getAttestationHash( + DEFAULT.SERVICE_X, + DEFAULT.ACCOUNT_NAME_EXAMPLE, + ); + + // vouch for the twitter account + await deployer.ethosVouch.contract + ?.connect(userA.signer) + .vouchByAttestation(attestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); + // user claims twitter account + await userB.attest({ + service: DEFAULT.SERVICE_X, + account: DEFAULT.ACCOUNT_NAME_EXAMPLE, + }); + // Check contract balance before claim + const contractBalanceBefore = await ethers.provider.getBalance( + deployer.ethosVouch.contract.target, + ); + const rewardsBalance = + await deployer.ethosVouch.contract?.rewardsByAttestationHash(attestationHash); + expect(rewardsBalance).to.be.greaterThan(0n); + + // Claim rewards + await deployer.ethosVouch.contract + .connect(userB.signer) + .claimRewardsByAttestation(attestationHash); + + // Verify contract balance decreased by rewards amount + const contractBalanceAfter = await ethers.provider.getBalance( + deployer.ethosVouch.contract.target, + ); + expect(contractBalanceAfter).to.equal(contractBalanceBefore - rewardsBalance); + }); + + it('should allow claiming rewards for an address mock profile after address is registered', async () => { + await setupDonationFee(); + const reviewedUser = await deployer.newWallet(); + // leave a review for an address + await userA.review({ address: reviewedUser.address }); + // vouch for the address + await deployer.ethosVouch.contract + ?.connect(userA.signer) + .vouchByAddress(reviewedUser.address, DEFAULT.COMMENT, DEFAULT.METADATA, { + value: DEFAULT.PAYMENT_AMOUNT, + }); + // user claims address + await userB.registerAddress(reviewedUser.address); + + // Check contract balance before claim + const contractBalanceBefore = await ethers.provider.getBalance( + deployer.ethosVouch.contract.target, + ); + const rewardsBalance = await deployer.ethosVouch.contract.rewardsByAddress( + reviewedUser.address, + ); + expect(rewardsBalance).to.be.greaterThan(0n); + + // Claim rewards with the address originally vouched + await deployer.ethosVouch.contract.connect(reviewedUser).claimRewards(); + + // Verify contract balance decreased by rewards amount + const contractBalanceAfter = await ethers.provider.getBalance( + deployer.ethosVouch.contract.target, + ); + expect(contractBalanceAfter).to.equal(contractBalanceBefore - rewardsBalance); + }); }); diff --git a/ethos/packages/contracts/test/vouch/vouch.rewards.test.ts.rej b/ethos/packages/contracts/test/vouch/vouch.rewards.test.ts.rej new file mode 100644 index 0000000..8855fd1 --- /dev/null +++ b/ethos/packages/contracts/test/vouch/vouch.rewards.test.ts.rej @@ -0,0 +1,435 @@ +diff a/ethos/packages/contracts/test/vouch/vouch.rewards.test.ts b/ethos/packages/contracts/test/vouch/vouch.rewards.test.ts (rejected hunks) +@@ -1,7 +1,7 @@ + import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js'; + import { expect } from 'chai'; + import hre from 'hardhat'; +-import { calculateFee } from '../utils/common.js'; ++import { calcFeeDistribution } from '../utils/common.js'; + import { DEFAULT } from '../utils/defaults.js'; + import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js'; + import { type EthosUser } from '../utils/ethosUser.js'; +@@ -13,6 +13,7 @@ describe('Vouch Rewards', () => { + let userA: EthosUser; + let userB: EthosUser; + const donationFee = 150n; ++ const vouchersPoolFee = 150n; + + beforeEach(async () => { + deployer = await loadFixture(createDeployer); +@@ -23,19 +24,28 @@ describe('Vouch Rewards', () => { + await deployer.ethosVouch.contract + .connect(deployer.ADMIN) + .setEntryDonationFeeBasisPoints(donationFee); ++ await deployer.ethosVouch.contract ++ .connect(deployer.ADMIN) ++ .setEntryVouchersPoolFeeBasisPoints(vouchersPoolFee); + } + + it('should allow withdrawing accumulated rewards', async () => { +- const paymentAmount = ethers.parseEther('0.1'); + await setupDonationFee(); + + // Create a vouch to generate rewards for userB +- await userA.vouch(userB, { paymentAmount }); ++ await userA.vouch(userB); + const initialBalance = await userB.getBalance(); + + // Get rewards balance + const rewardsBalance = await userB.getRewardsBalance(); +- expect(rewardsBalance).to.equal(calculateFee(paymentAmount, donationFee).fee); ++ const { ++ shares: { donation }, ++ } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { ++ entry: 0n, ++ donation: donationFee, ++ vouchIncentives: 0n, // no vouch pool incentives for first vouch ++ }); ++ expect(rewardsBalance).to.equal(donation); + + // Withdraw rewards + const withdrawTx = await deployer.ethosVouch.contract.connect(userB.signer).claimRewards(); +@@ -58,16 +68,29 @@ describe('Vouch Rewards', () => { + }); + + it('should accumulate rewards from multiple vouches', async () => { +- const paymentAmount = ethers.parseEther('0.1'); + await setupDonationFee(); + + // Create multiple vouches to generate rewards from different users + const userC = await deployer.createUser(); +- await userA.vouch(userB, { paymentAmount }); +- await userC.vouch(userB, { paymentAmount }); ++ await userA.vouch(userB); ++ const { ++ shares: { donation: userADonation }, ++ } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { ++ entry: 0n, ++ donation: donationFee, ++ vouchIncentives: 0n, // no vouch pool incentives for first vouch ++ }); ++ await userC.vouch(userB); ++ const { ++ shares: { donation: userCDonation }, ++ } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { ++ entry: 0n, ++ donation: donationFee, ++ vouchIncentives: vouchersPoolFee, ++ }); + + const rewardsBalance = await userB.getRewardsBalance(); +- const expectedRewards = calculateFee(paymentAmount, donationFee).fee * 2n; ++ const expectedRewards = userADonation + userCDonation; + expect(rewardsBalance).to.equal(expectedRewards); + }); + +@@ -79,9 +102,8 @@ describe('Vouch Rewards', () => { + + it('should handle failed reward withdrawals gracefully', async () => { + // Generate some rewards +- const paymentAmount = ethers.parseEther('0.1'); + await setupDonationFee(); +- await userA.vouch(userB, { paymentAmount }); ++ await userA.vouch(userB); + + // Try to withdraw with a contract that doesn't accept ETH + const nonPayableContract = await deployer.createUser(); // Using a regular user account instead of mock +@@ -90,39 +112,233 @@ describe('Vouch Rewards', () => { + ).to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'InsufficientRewardsBalance'); + }); + ++ it('should redirect vouchByAttestation to vouchByProfileId for verified profiles', async () => { ++ await setupDonationFee(); ++ ++ // First create an attestation for userB ++ const attestationHash = await userB.attest({ ++ service: DEFAULT.SERVICE_X, ++ account: DEFAULT.ACCOUNT_NAME_EXAMPLE, ++ }); ++ ++ // Try to vouch using vouchByAttestation - should work since profile is verified ++ await deployer.ethosVouch.contract ++ .connect(userA.signer) ++ .vouchByAttestation(attestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); ++ ++ // Verify the vouch was created correctly ++ const vouch = await deployer.ethosVouch.contract.verifiedVouchByAuthorForSubjectProfileId( ++ userA.profileId, ++ userB.profileId, ++ ); ++ ++ expect(vouch.authorProfileId).to.equal(userA.profileId); ++ expect(vouch.subjectProfileId).to.equal(userB.profileId); ++ expect(vouch.comment).to.equal(DEFAULT.COMMENT); ++ expect(vouch.metadata).to.equal(DEFAULT.METADATA); ++ ++ // Verify rewards were distributed correctly ++ const rewardsBalance = await userB.getRewardsBalance(); ++ const { ++ shares: { donation }, ++ } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { ++ entry: 0n, ++ donation: donationFee, ++ vouchIncentives: 0n, // first vouch does not apply vouch rewards ++ }); ++ expect(rewardsBalance).to.equal(donation); ++ }); ++ + it('should correctly track rewards across multiple recipients', async () => { +- const paymentAmount = ethers.parseEther('0.1'); + await setupDonationFee(); + const userC = await deployer.createUser(); + + // Generate rewards for multiple users +- await userA.vouch(userB, { paymentAmount }); +- await userA.vouch(userC, { paymentAmount }); ++ await userA.vouch(userB); ++ await userA.vouch(userC); ++ ++ const { ++ shares: { donation }, ++ } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { ++ entry: 0n, ++ donation: donationFee, ++ vouchIncentives: 0n, // first vouch does not apply vouch rewards ++ }); ++ expect(await userB.getRewardsBalance()).to.equal(donation); ++ expect(await userC.getRewardsBalance()).to.equal(donation); ++ }); ++ ++ it('should distribute rewards correctly when increasing attestation-based vouch', async () => { ++ await setupDonationFee(); ++ ++ // First create an attestation for userB ++ const attestationHash = await userB.attest({ ++ service: DEFAULT.SERVICE_X, ++ account: DEFAULT.ACCOUNT_NAME_EXAMPLE, ++ }); ++ ++ // Initial vouch by userA using attestation ++ await deployer.ethosVouch.contract ++ .connect(userA.signer) ++ .vouchByAttestation(attestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); + +- const expectedReward = calculateFee(paymentAmount, donationFee).fee; +- expect(await userB.getRewardsBalance()).to.equal(expectedReward); +- expect(await userC.getRewardsBalance()).to.equal(expectedReward); ++ // Create another user to vouch for the same attestation ++ const userC = await deployer.createUser(); ++ await deployer.ethosVouch.contract ++ .connect(userC.signer) ++ .vouchByAttestation(attestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); ++ ++ // Get initial rewards balance ++ const initialRewards = ++ await deployer.ethosVouch.contract.rewardsByAttestationHash(attestationHash); ++ ++ // Get userC's initial vouch balance ++ const userCVouchInitial = await deployer.ethosVouch.contract.vouches(1); // Second vouch ID ++ const initialBalance = userCVouchInitial.balance; ++ ++ // Increase vouch amount for userA's vouch ++ const increaseAmount = DEFAULT.PAYMENT_AMOUNT; ++ const vouchId = 0; // First vouch ID ++ const tx = await deployer.ethosVouch.contract ++ .connect(userA.signer) ++ .increaseVouch(vouchId, attestationHash, userB.signer.address, { value: increaseAmount }); ++ ++ // Wait for the transaction to be mined and get the receipt ++ const receipt = await tx.wait(); ++ ++ if (!receipt) { ++ throw new Error('Transaction failed'); ++ } ++ ++ // Verify rewards were distributed correctly ++ const finalRewards = ++ await deployer.ethosVouch.contract.rewardsByAttestationHash(attestationHash); ++ const { ++ shares: { donation }, ++ } = calcFeeDistribution(increaseAmount, { ++ entry: 0n, ++ donation: donationFee, ++ vouchIncentives: vouchersPoolFee, ++ }); ++ // Allow for 1 wei rounding difference ++ expect(finalRewards - initialRewards).to.be.oneOf([donation, donation + 1n]); ++ ++ // Get userC's final vouch balance and verify it increased by their share of the vouchers pool fee ++ const userCVouchFinal = await deployer.ethosVouch.contract.vouches(1); ++ const { ++ shares: { vouchersPool }, ++ } = calcFeeDistribution(increaseAmount, { ++ entry: 0n, ++ donation: donationFee, ++ vouchIncentives: vouchersPoolFee, ++ }); ++ // Allow for 1 wei rounding difference ++ expect(userCVouchFinal.balance - initialBalance).to.be.oneOf([vouchersPool, vouchersPool + 1n]); ++ }); ++ ++ it('should distribute rewards correctly when increasing address-based vouch', async () => { ++ await setupDonationFee(); ++ ++ // Create a profile for userB first ++ await userB.registerAddress(userB.signer.address); ++ ++ // Initial vouch by userA ++ await deployer.ethosVouch.contract ++ .connect(userA.signer) ++ .vouchByAddress(userB.signer.address, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); ++ ++ // Create another user to vouch for the same address ++ const userC = await deployer.createUser(); ++ await deployer.ethosVouch.contract ++ .connect(userC.signer) ++ .vouchByAddress(userB.signer.address, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); ++ ++ // Get initial rewards balance ++ const initialRewards = await deployer.ethosVouch.contract.rewardsByAddress( ++ userB.signer.address, ++ ); ++ ++ // Get userC's initial vouch balance ++ const userCVouchInitial = await deployer.ethosVouch.contract.vouches(1); ++ const initialBalance = userCVouchInitial.balance; ++ ++ // Increase vouch amount for userA's vouch ++ const increaseAmount = DEFAULT.PAYMENT_AMOUNT; ++ const vouchId = 0; // First vouch ID ++ const tx = await deployer.ethosVouch.contract ++ .connect(userA.signer) ++ .increaseVouch( ++ vouchId, ++ '0x0000000000000000000000000000000000000000000000000000000000000000', ++ userB.signer.address, ++ { value: increaseAmount }, ++ ); ++ ++ // Wait for the transaction to be mined and get the receipt ++ const receipt = await tx.wait(); ++ ++ if (!receipt) { ++ throw new Error('Transaction failed'); ++ } ++ ++ // Verify rewards were distributed correctly ++ const finalRewards = await deployer.ethosVouch.contract.rewardsByAddress(userB.signer.address); ++ const { ++ shares: { donation }, ++ } = calcFeeDistribution(increaseAmount, { ++ entry: 0n, ++ donation: donationFee, ++ vouchIncentives: vouchersPoolFee, ++ }); ++ // Allow for 1 wei rounding difference ++ expect(finalRewards - initialRewards).to.be.oneOf([donation, donation + 1n]); ++ ++ // Get userC's final vouch balance and verify it increased by their share of the vouchers pool fee ++ const userCVouchFinal = await deployer.ethosVouch.contract.vouches(1); ++ const { ++ shares: { vouchersPool }, ++ } = calcFeeDistribution(increaseAmount, { ++ entry: 0n, ++ donation: donationFee, ++ vouchIncentives: vouchersPoolFee, ++ }); ++ // Allow for 1 wei rounding difference ++ expect(userCVouchFinal.balance - initialBalance).to.be.oneOf([vouchersPool, vouchersPool + 1n]); + }); + + it('should emit DepositedToRewards event when rewards are generated', async () => { +- const paymentAmount = ethers.parseEther('0.1'); + await setupDonationFee(); +- const expectedFee = calculateFee(paymentAmount, donationFee).fee; ++ const { ++ shares: { donation }, ++ } = calcFeeDistribution(DEFAULT.PAYMENT_AMOUNT, { ++ entry: 0n, ++ donation: donationFee, ++ vouchIncentives: 0n, ++ }); + +- await userA.vouch(userB, { paymentAmount }); ++ await userA.vouch(userB); + + const filter = deployer.ethosVouch.contract.filters.DepositedToRewards(userB.profileId); + const events = await deployer.ethosVouch.contract.queryFilter(filter); + + expect(events.length).to.equal(1); + expect(events[0].args?.[0]).to.equal(userB.profileId); +- expect(events[0].args?.[1]).to.equal(expectedFee); ++ expect(events[0].args?.[1]).to.equal(donation); + }); + + it('should emit WithdrawnFromRewards event when rewards are withdrawn', async () => { +- const paymentAmount = ethers.parseEther('0.1'); + await setupDonationFee(); +- await userA.vouch(userB, { paymentAmount }); ++ await userA.vouch(userB); + + const rewardsBalance = await userB.getRewardsBalance(); + await expect(deployer.ethosVouch.contract.connect(userB.signer).claimRewards()) +@@ -131,9 +347,8 @@ describe('Vouch Rewards', () => { + }); + + it('should handle rewards for archived profiles', async () => { +- const paymentAmount = ethers.parseEther('0.1'); + await setupDonationFee(); +- await userA.vouch(userB, { paymentAmount }); ++ await userA.vouch(userB); + + // Create and archive userB's profile + await deployer.ethosProfile.contract.connect(userB.signer).archiveProfile(); +@@ -166,4 +381,82 @@ describe('Vouch Rewards', () => { + .to.be.revertedWithCustomError(deployer.ethosVouch.contract, 'ProfileNotFoundForAddress') + .withArgs(reviewedUser.address); + }); ++ ++ it('should allow claiming rewards for an attestation mock profile after attestation is claimed', async () => { ++ await setupDonationFee(); ++ // leave a review for twitter account ++ await userA.review({ ++ attestationDetails: { ++ service: DEFAULT.SERVICE_X, ++ account: DEFAULT.ACCOUNT_NAME_EXAMPLE, ++ }, ++ }); ++ const attestationHash = await userA.getAttestationHash( ++ DEFAULT.SERVICE_X, ++ DEFAULT.ACCOUNT_NAME_EXAMPLE, ++ ); ++ ++ // vouch for the twitter account ++ await deployer.ethosVouch.contract ++ ?.connect(userA.signer) ++ .vouchByAttestation(attestationHash, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); ++ // user claims twitter account ++ await userB.attest({ ++ service: DEFAULT.SERVICE_X, ++ account: DEFAULT.ACCOUNT_NAME_EXAMPLE, ++ }); ++ // Check contract balance before claim ++ const contractBalanceBefore = await ethers.provider.getBalance( ++ deployer.ethosVouch.contract.target, ++ ); ++ const rewardsBalance = ++ await deployer.ethosVouch.contract?.rewardsByAttestationHash(attestationHash); ++ expect(rewardsBalance).to.be.greaterThan(0n); ++ ++ // Claim rewards ++ await deployer.ethosVouch.contract ++ .connect(userB.signer) ++ .claimRewardsByAttestation(attestationHash); ++ ++ // Verify contract balance decreased by rewards amount ++ const contractBalanceAfter = await ethers.provider.getBalance( ++ deployer.ethosVouch.contract.target, ++ ); ++ expect(contractBalanceAfter).to.equal(contractBalanceBefore - rewardsBalance); ++ }); ++ ++ it('should allow claiming rewards for an address mock profile after address is registered', async () => { ++ await setupDonationFee(); ++ const reviewedUser = await deployer.newWallet(); ++ // leave a review for an address ++ await userA.review({ address: reviewedUser.address }); ++ // vouch for the address ++ await deployer.ethosVouch.contract ++ ?.connect(userA.signer) ++ .vouchByAddress(reviewedUser.address, DEFAULT.COMMENT, DEFAULT.METADATA, { ++ value: DEFAULT.PAYMENT_AMOUNT, ++ }); ++ // user claims address ++ await userB.registerAddress(reviewedUser.address); ++ ++ // Check contract balance before claim ++ const contractBalanceBefore = await ethers.provider.getBalance( ++ deployer.ethosVouch.contract.target, ++ ); ++ const rewardsBalance = await deployer.ethosVouch.contract.rewardsByAddress( ++ reviewedUser.address, ++ ); ++ expect(rewardsBalance).to.be.greaterThan(0n); ++ ++ // Claim rewards with the address originally vouched ++ await deployer.ethosVouch.contract.connect(reviewedUser).claimRewards(); ++ ++ // Verify contract balance decreased by rewards amount ++ const contractBalanceAfter = await ethers.provider.getBalance( ++ deployer.ethosVouch.contract.target, ++ ); ++ expect(contractBalanceAfter).to.equal(contractBalanceBefore - rewardsBalance); ++ }); + }); diff --git a/ethos/packages/contracts/test/vouch/vouch.slash.test.ts b/ethos/packages/contracts/test/vouch/vouch.slash.test.ts index 61c38b6..22e3d17 100644 --- a/ethos/packages/contracts/test/vouch/vouch.slash.test.ts +++ b/ethos/packages/contracts/test/vouch/vouch.slash.test.ts @@ -136,4 +136,62 @@ describe('EthosVouch Slashing', () => { // Should reduce balance by 0.01% expect(finalBalance.balance).to.equal((initialBalance.balance * 9999n) / 10000n); }); + + describe('Frozen Authors', () => { + let vouchId: bigint; + + beforeEach(async () => { + const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( + userA.profileId, + userB.profileId, + ); + vouchId = vouch.vouchId; + }); + + it('should prevent withdrawals when author is frozen', async () => { + await ethosVouch.connect(slasher.signer).freeze(userA.profileId); + + await expect(ethosVouch.connect(userA.signer).unvouch(vouchId)) + .to.be.revertedWithCustomError(ethosVouch, 'PendingSlash') + .withArgs(vouchId, userA.profileId); + }); + + it('should allow withdrawals after unfreezing', async () => { + await ethosVouch.connect(slasher.signer).freeze(userA.profileId); + await ethosVouch.connect(slasher.signer).unfreeze(userA.profileId); + + await expect(ethosVouch.connect(userA.signer).unvouch(vouchId)) + .to.emit(ethosVouch, 'Unvouched') + .withArgs(vouchId, userA.profileId, userB.profileId, VOUCH_PARAMS.paymentAmount); + }); + + it('should allow withdrawals for non-frozen authors', async () => { + await ethosVouch.connect(slasher.signer).freeze(userA.profileId); + + // userB should still be able to unvouch even though userA is frozen + const userBVouch = await userB.vouch(userA); + await expect(ethosVouch.connect(userB.signer).unvouch(userBVouch.vouchId)) + .to.emit(ethosVouch, 'Unvouched') + .withArgs(userBVouch.vouchId, userB.profileId, userA.profileId, VOUCH_PARAMS.paymentAmount); + }); + + it('should maintain frozen status after slashing', async () => { + await ethosVouch.connect(slasher.signer).freeze(userA.profileId); + await ethosVouch.connect(slasher.signer).slash(userA.profileId, 1000n); // 10% slash + + await expect(ethosVouch.connect(userA.signer).unvouch(vouchId)) + .to.be.revertedWithCustomError(ethosVouch, 'PendingSlash') + .withArgs(vouchId, userA.profileId); + }); + + it('should emit Frozen event when freezing and unfreezing', async () => { + await expect(ethosVouch.connect(slasher.signer).freeze(userA.profileId)) + .to.emit(ethosVouch, 'Frozen') + .withArgs(userA.profileId, true); + + await expect(ethosVouch.connect(slasher.signer).unfreeze(userA.profileId)) + .to.emit(ethosVouch, 'Frozen') + .withArgs(userA.profileId, false); + }); + }); }); diff --git a/ethos/packages/contracts/test/vouch/vouch.slash.test.ts.rej b/ethos/packages/contracts/test/vouch/vouch.slash.test.ts.rej new file mode 100644 index 0000000..4cadaa2 --- /dev/null +++ b/ethos/packages/contracts/test/vouch/vouch.slash.test.ts.rej @@ -0,0 +1,64 @@ +diff a/ethos/packages/contracts/test/vouch/vouch.slash.test.ts b/ethos/packages/contracts/test/vouch/vouch.slash.test.ts (rejected hunks) +@@ -136,4 +136,62 @@ describe('EthosVouch Slashing', () => { + // Should reduce balance by 0.01% + expect(finalBalance.balance).to.equal((initialBalance.balance * 9999n) / 10000n); + }); ++ ++ describe('Frozen Authors', () => { ++ let vouchId: bigint; ++ ++ beforeEach(async () => { ++ const vouch = await ethosVouch.verifiedVouchByAuthorForSubjectProfileId( ++ userA.profileId, ++ userB.profileId, ++ ); ++ vouchId = vouch.vouchId; ++ }); ++ ++ it('should prevent withdrawals when author is frozen', async () => { ++ await ethosVouch.connect(slasher.signer).freeze(userA.profileId); ++ ++ await expect(ethosVouch.connect(userA.signer).unvouch(vouchId)) ++ .to.be.revertedWithCustomError(ethosVouch, 'PendingSlash') ++ .withArgs(vouchId, userA.profileId); ++ }); ++ ++ it('should allow withdrawals after unfreezing', async () => { ++ await ethosVouch.connect(slasher.signer).freeze(userA.profileId); ++ await ethosVouch.connect(slasher.signer).unfreeze(userA.profileId); ++ ++ await expect(ethosVouch.connect(userA.signer).unvouch(vouchId)) ++ .to.emit(ethosVouch, 'Unvouched') ++ .withArgs(vouchId, userA.profileId, userB.profileId, VOUCH_PARAMS.paymentAmount); ++ }); ++ ++ it('should allow withdrawals for non-frozen authors', async () => { ++ await ethosVouch.connect(slasher.signer).freeze(userA.profileId); ++ ++ // userB should still be able to unvouch even though userA is frozen ++ const userBVouch = await userB.vouch(userA); ++ await expect(ethosVouch.connect(userB.signer).unvouch(userBVouch.vouchId)) ++ .to.emit(ethosVouch, 'Unvouched') ++ .withArgs(userBVouch.vouchId, userB.profileId, userA.profileId, VOUCH_PARAMS.paymentAmount); ++ }); ++ ++ it('should maintain frozen status after slashing', async () => { ++ await ethosVouch.connect(slasher.signer).freeze(userA.profileId); ++ await ethosVouch.connect(slasher.signer).slash(userA.profileId, 1000n); // 10% slash ++ ++ await expect(ethosVouch.connect(userA.signer).unvouch(vouchId)) ++ .to.be.revertedWithCustomError(ethosVouch, 'PendingSlash') ++ .withArgs(vouchId, userA.profileId); ++ }); ++ ++ it('should emit Frozen event when freezing and unfreezing', async () => { ++ await expect(ethosVouch.connect(slasher.signer).freeze(userA.profileId)) ++ .to.emit(ethosVouch, 'Frozen') ++ .withArgs(userA.profileId, true); ++ ++ await expect(ethosVouch.connect(slasher.signer).unfreeze(userA.profileId)) ++ .to.emit(ethosVouch, 'Frozen') ++ .withArgs(userA.profileId, false); ++ }); ++ }); + }); diff --git a/ethos/packages/domain/package.json b/ethos/packages/domain/package.json new file mode 100644 index 0000000..4b0267f --- /dev/null +++ b/ethos/packages/domain/package.json @@ -0,0 +1,19 @@ +{ + "name": "@ethos/domain", + "version": "1.0.0", + "description": "Common types, helpers, and constants for Ethos domain model", + "main": "dist/index.js", + "type": "module", + "author": "Ethos Network Inc.", + "license": "UNLICENSED", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + }, + "dependencies": { + "@ethos/blockchain-manager": "^1.0.0", + "@ethos/score": "^1.0.0", + "viem": "^2.21.51", + "zod": "^3.24.1" + } +} diff --git a/ethos/packages/domain/src/__tests__/user.test.ts b/ethos/packages/domain/src/__tests__/user.test.ts new file mode 100644 index 0000000..6cf794e --- /dev/null +++ b/ethos/packages/domain/src/__tests__/user.test.ts @@ -0,0 +1,49 @@ +import { zeroAddress } from 'viem'; +import { english, generateMnemonic, mnemonicToAccount } from 'viem/accounts'; +import { fromUserKey, isTargetValid, toUserKey } from '../user.js'; + +const { address } = mnemonicToAccount(generateMnemonic(english)); + +test('toUserKey', () => { + expect(toUserKey({ profileId: 123 })).toBe('profileId:123'); + expect(toUserKey({ address })).toBe(`address:${address}`); + expect(toUserKey({ service: 'x.com', account: '123456789' })).toBe('service:x.com:123456789'); + expect(toUserKey({ service: 'x.com', username: 'johndoe' })).toBe( + 'service:x.com:username:johndoe', + ); +}); + +test('fromUserKey', () => { + expect(fromUserKey('profileId:123')).toEqual({ profileId: 123 }); + expect(fromUserKey(`address:${address}`)).toEqual({ address }); + expect(fromUserKey('service:x.com:123456789')).toEqual({ + service: 'x.com', + account: '123456789', + }); + expect(fromUserKey('service:x.com:username:johndoe', true)).toEqual({ + service: 'x.com', + username: 'johndoe', + }); + expect(() => fromUserKey('service:x.com:username:johndoe')).toThrow('"username" is not allowed'); +}); + +test('isTargetValid', () => { + expect(isTargetValid({ address })).toBe(true); + expect(isTargetValid({ address: zeroAddress })).toBe(false); + expect(isTargetValid({ service: 'x.com', account: '123456789' })).toBe(true); + expect(isTargetValid({ service: 'x.com', username: 'johndoe' })).toBe(true); + expect(isTargetValid({ profileId: 123 })).toBe(true); + expect(isTargetValid({ profileId: 0 })).toBe(false); + expect(isTargetValid({ profileId: -10 })).toBe(false); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(() => isTargetValid({})).toThrow('Invalid EthosUserTarget'); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + expect(() => isTargetValid({ address, smth: 'x.com' })).toThrow('EthosUserTarget'); + expect(() => isTargetValid({ profileId: 123, address })).toThrow('EthosUserTarget'); + expect(() => isTargetValid({ profileId: 123, service: 'x.com' })).toThrow('EthosUserTarget'); + expect(() => isTargetValid({ profileId: 123, address, service: 'x.com' })).toThrow( + 'EthosUserTarget', + ); +}); diff --git a/ethos/packages/domain/src/activity.ts b/ethos/packages/domain/src/activity.ts new file mode 100644 index 0000000..65bac55 --- /dev/null +++ b/ethos/packages/domain/src/activity.ts @@ -0,0 +1,120 @@ +import { + type Attestation, + type Profile, + type ProfileId, + type Review, + type Vouch, + type VouchFunds, +} from '@ethos/blockchain-manager'; +import { JsonHelper } from '@ethos/helpers'; +import { type Address } from 'viem'; +import { z } from 'zod'; +import { type BlockchainEvent } from './blockchain-event.js'; +import { type ReplySummary } from './reply.js'; +import { type Relationship } from './transaction.js'; + +export const BASE_REVIEW_XP_GAIN = 5; +export const INVITE_ACCEPTED_XP_GAIN = 25_000; +export const BASE_VOUCH_DAY_XP_GAIN = 500; + +export const attestationActivity = 'attestation'; +export const invitationAcceptedActivity = 'invitation-accepted'; +export const reviewActivity = 'review'; +export const vouchActivity = 'vouch'; +export const unvouchActivity = 'unvouch'; + +export const activities = [ + attestationActivity, + invitationAcceptedActivity, + reviewActivity, + vouchActivity, + unvouchActivity, +] as const; + +export type ActivityType = (typeof activities)[number]; + +type ActivityInfoBase> = { + type: T; + data: D; + timestamp: number; + votes: VoteInfo; + replySummary: ReplySummary; + author: ActivityActor; + subject: ActivityActor; + events: BlockchainEvent[]; +}; + +export type AttestationActivityInfo = ActivityInfoBase< + 'attestation', + Attestation & { + username: string; + } +>; +export type InvitationAcceptedActivityInfo = ActivityInfoBase<'invitation-accepted', Profile>; +export type ReviewActivityInfo = ActivityInfoBase<'review', Review>; +export type VouchActivityInfo = ActivityInfoBase<'vouch', Vouch & VouchFunds>; +export type UnvouchActivityInfo = ActivityInfoBase<'unvouch', Vouch & VouchFunds>; + +export type ActivityInfo = + | AttestationActivityInfo + | InvitationAcceptedActivityInfo + | ReviewActivityInfo + | VouchActivityInfo + | UnvouchActivityInfo; + +export type ActivityActor = { + userkey: string; + profileId?: ProfileId; + name: string | null; + username?: string | null; + avatar: string | null; + description: string | null; + score: number; + scoreXpMultiplier: number; + primaryAddress: Address; +}; + +export type ActivityActorWithXp = ActivityActor & { + totalXp: number; +}; + +export type RecentInteractionActivityActor = ActivityActor & { + interaction: Relationship | undefined; +}; + +export type VoteInfo = { + upvotes: number; + downvotes: number; +}; + +// Parse activity metadata +const reviewMetadataSchema = { + description: z.string().optional(), + source: z.string().optional(), + importedFromTestnet: z.number().int().nonnegative().optional(), +}; + +const vouchMetadataSchema = { + description: z.string().optional(), + source: z.string().optional(), + importedFromTestnet: z.number().int().nonnegative().optional(), +}; + +export type ReviewMetadata = z.infer>; +export type VouchMetadata = z.infer>; + +export function parseReviewMetadata(rawMetadata?: string): ReviewMetadata { + const data = JsonHelper.parseSafe(rawMetadata ?? null, { + zodSchema: reviewMetadataSchema, + }); + + return data ?? {}; +} + +export function parseVouchMetadata(rawMetadata?: string): VouchMetadata { + const data = JsonHelper.parseSafe(rawMetadata ?? null, { + zodSchema: vouchMetadataSchema, + }); + + return data ?? {}; +} diff --git a/ethos/packages/domain/src/attestations.ts b/ethos/packages/domain/src/attestations.ts new file mode 100644 index 0000000..371fff8 --- /dev/null +++ b/ethos/packages/domain/src/attestations.ts @@ -0,0 +1 @@ +export const X_SERVICE = 'x.com'; diff --git a/ethos/packages/domain/src/blockchain-event.ts b/ethos/packages/domain/src/blockchain-event.ts new file mode 100644 index 0000000..fd1a885 --- /dev/null +++ b/ethos/packages/domain/src/blockchain-event.ts @@ -0,0 +1,10 @@ +export type BlockchainEvent = { + id: number; + blockIndex: number; + blockNumber: number; + contract: string; + createdAt: number; + processed: boolean; + txHash: string; + updatedAt: number; +}; diff --git a/ethos/packages/domain/src/claim.ts b/ethos/packages/domain/src/claim.ts new file mode 100644 index 0000000..402cedb --- /dev/null +++ b/ethos/packages/domain/src/claim.ts @@ -0,0 +1,32 @@ +export const claimReferralId = { + /** + * Convert Twitter user ID to referral ID + */ + encode(twitterUserId: string) { + // Remove padding at the end such as == in asdfsdfsd== + return btoa(twitterUserId).replace(/=*$/g, ''); + }, + /** + * Extract Twitter user ID from referral ID + */ + decode(referralId: string) { + // Add padding back if needed + const padding = '='.repeat((4 - (referralId.length % 4)) % 4); + + return atob(referralId + padding); + }, +}; + +export const REFERRAL_BONUS_PERCENTAGE = 0.2; +export const MAX_REFERRAL_USES = 10; + +export const claimErrors = { + accessDenied: 'access_denied', + noUser: 'no_user', + invalidReferrer: 'invalid_referrer', + referralLimitReached: 'referral_limit_reached', + failedToClaim: 'failed_to_claim', + unknown: 'unknown', +} as const; + +export type ClaimError = (typeof claimErrors)[keyof typeof claimErrors]; diff --git a/ethos/packages/domain/src/contribution.ts b/ethos/packages/domain/src/contribution.ts new file mode 100644 index 0000000..bb8f038 --- /dev/null +++ b/ethos/packages/domain/src/contribution.ts @@ -0,0 +1,114 @@ +export const ANSWER_XP = 10; +export const VOTE_XP = 50; +export const REVIEW_XP = 90; +export const TOTAL_STEPS = 5; +export const STREAKS_XP_MULTIPLIER_MAP: Array<{ day: number; multiplier: number }> = [ + { day: 1, multiplier: 1.5 }, + { day: 3, multiplier: 2.5 }, + { day: 5, multiplier: 3.5 }, + { day: 7, multiplier: 5 }, +]; +export const DAILY_CONTRIBUTION_LIMIT = 7; + +export const CONTRIBUTION_ANSWER_TYPES = ['POSITIVE', 'NEGATIVE', 'NEUTRAL', 'UNSURE'] as const; +export const CONTRIBUTION_STATUS = ['PENDING', 'COMPLETED', 'SKIPPED'] as const; + +export type ContributionBundleModel = { + id: number; + contributions: ContributionModel[]; +}; + +export type ContributionModel = { + id: number; + experience: number; + status: (typeof CONTRIBUTION_STATUS)[number]; + action: ContributionAction; +}; + +export type ContributionAction = + | ({ type: 'REVIEW' } & ContributionReview) + | ({ type: 'TRUST_BATTLE' } & ContributionTrustBattle) + | ({ type: 'TRUST_CHECK' } & ContributionTrustCheck) + | ({ type: 'REVIEW_CHECK' } & ContributionReviewCheck) + | ({ type: 'SCORE_CHECK' } & ContributionScoreCheck) + | ({ type: 'REVIEW_VOTE' } & ContributionReviewVote); + +export type ContributionType = ContributionAction['type']; +export type ContributionAnswer = (typeof CONTRIBUTION_ANSWER_TYPES)[number]; + +export type ContributionReview = { + targetUserkeys: string[]; + reviewId: number | null; +}; + +export type ContributionTrustBattle = { + targetUserkeys: string[]; + chosenIndex: number | null; +}; + +export type ContributionTrustCheck = { + targetUserkey: string; + answer: ContributionAnswer | null; +}; + +export type ContributionScoreCheck = { + targetUserkey: string; + answer: ContributionAnswer | null; +}; + +export type ContributionReviewCheck = { + reviewId: number; + answer: ContributionAnswer | null; +}; + +export type ContributionReviewVote = { + reviewId: number; + voteId: number | null; +}; + +export type ContributionStats = { + canGenerateDailyContributions: boolean; + resetTimestamp: number; + totalCount: number; + completedCount: number; + skippedCount: number; + pendingCount: number; + pendingBundleCount: number; + todayXp: number; + pendingXp: number; + totalXp: number; + previousXpLookup: Partial>; + previousBundleXpLookup: Partial>; + streakDays: number; + streakDaysOptimistic: number; +}; + +export function streakDaysToMultiplier(days: number): number { + // "toSorted" caused a build error with typescript + const higherStreaksFirst = [...STREAKS_XP_MULTIPLIER_MAP].sort((a, b) => b.day - a.day); + + return higherStreaksFirst.find((x) => x.day <= days)?.multiplier ?? 1; +} + +export type BundleBuilderType = 'TRUST_REVIEW' | 'TRUST_BATTLE' | 'REVIEW_CHECK' | 'SCORE_CHECK'; + +export type BundleConfigItem = { + type: BundleBuilderType; + count: number; +}; + +export const BUNDLE_CONFIG: BundleConfigItem[] = [ + { type: 'TRUST_REVIEW', count: 1 }, + { type: 'TRUST_BATTLE', count: 1 }, + { type: 'REVIEW_CHECK', count: 4 }, + { type: 'SCORE_CHECK', count: 1 }, +] as const; + +// Validate that the total matches DAILY_CONTRIBUTION_LIMIT +const totalBundles = BUNDLE_CONFIG.reduce((sum, config) => sum + config.count, 0); + +if (totalBundles !== DAILY_CONTRIBUTION_LIMIT) { + throw new Error( + `Bundle config total (${totalBundles}) must match DAILY_CONTRIBUTION_LIMIT (${DAILY_CONTRIBUTION_LIMIT})`, + ); +} diff --git a/ethos/packages/domain/src/index.ts b/ethos/packages/domain/src/index.ts new file mode 100644 index 0000000..bb67215 --- /dev/null +++ b/ethos/packages/domain/src/index.ts @@ -0,0 +1,25 @@ +export * from './activity.js'; +export * from './reply.js'; +export * from './contribution.js'; +export * from './links.js'; +export { type BlockchainEvent } from './blockchain-event.js'; +export { type Invitation, InvitationStatus, type PendingInvitation } from './invitation.js'; +export { ScoreImpact } from './score.js'; +export { + type EthosUserTarget, + type EthosUserTargetWithTwitterUsername, + fromUserKey, + toUserKey, + isTargetValid, + deduplicateTargets, +} from './user.js'; +export { type Relationship, type Transaction, type Interaction } from './transaction.js'; +export type { LiteProfile, ProfileAddresses } from './profile.js'; +export { X_SERVICE } from './attestations.js'; +export { + claimErrors, + claimReferralId, + type ClaimError, + REFERRAL_BONUS_PERCENTAGE, + MAX_REFERRAL_USES, +} from './claim.js'; diff --git a/ethos/packages/domain/src/invitation.ts b/ethos/packages/domain/src/invitation.ts new file mode 100644 index 0000000..032f891 --- /dev/null +++ b/ethos/packages/domain/src/invitation.ts @@ -0,0 +1,33 @@ +import { type ProfileId } from '@ethos/blockchain-manager'; +import { type Address } from 'viem'; +import { type ScoreImpact } from './score.js'; + +export type Invitation = { + id: string; + senderProfileId: number; + recipientAddress: Address; + // txnHash: string; NOT IMPLEMENTED YET + status: InvitationStatus; + score: { + value: number; + impact: ScoreImpact; + }; + dateInvited: Date; + dateAccepted?: Date; +}; + +export type PendingInvitation = { + id: ProfileId; + impact: { + value: number; + relativeValue: number; + impact: ScoreImpact; + adjustedRecipientScore: number; + }; +}; + +export enum InvitationStatus { + ACCEPTED = 'ACCEPTED', + INVITED = 'INVITED', + ACCEPTED_OTHER_INVITATION = 'ACCEPTED_OTHER_INVITATION', +} diff --git a/ethos/packages/domain/src/links.ts b/ethos/packages/domain/src/links.ts new file mode 100644 index 0000000..e5e106f --- /dev/null +++ b/ethos/packages/domain/src/links.ts @@ -0,0 +1,32 @@ +// TODO: Remove links.ts from services/web and keep this one only +export const chromeExtensionId = 'jblacjfeljfigeglloiclnoehlhnmgne'; +export const chromeExtensionLink = `https://chromewebstore.google.com/detail/ethos-twitter-extension/${chromeExtensionId}`; + +export const ethosTwitterLink = 'https://x.com/ethos_network'; +export const ethosTwitterHandle = '@ethos_network'; +export const ethosTwitterHashtag = '#ethos'; +export const ethosDiscordLink = 'https://discord.gg/trust-ethos'; + +export const ethosWaitlistLink = 'https://waitlist.ethos.network/standby'; + +export const ethosHelpLink = 'https://help.ethos.network'; + +export const ethosHelpMechanicsInvitationsLink = + 'https://help.ethos.network/en/collections/10242375-mechanics-invitations'; + +export const privacyPolicyLink = + 'https://app.termly.io/policy-viewer/policy.html?policyUUID=87f5bacc-14fd-4de5-be2d-4b6044d344ba'; + +export const termsOfServiceLink = + 'https://app.termly.io/policy-viewer/policy.html?policyUUID=94ef2a4d-c28f-43c8-af60-762ec6e11147'; + +export const featureRequestsLink = 'https://feedback.ethos.network/feature-requests'; + +export const bugReportsLink = 'https://feedback.ethos.network/bugs'; + +export const ethosHomepageLink = 'https://ethos.network'; +export const ethosBlogLink = 'https://ethos.network/blog'; +export const ethosWhitepaperLink = 'https://whitepaper.ethos.network'; + +export const ethosFeesHelpPageLink = + 'https://help.ethos.network/en/articles/9822283-what-is-vouching-and-how-does-it-work'; diff --git a/ethos/packages/domain/src/profile.ts b/ethos/packages/domain/src/profile.ts new file mode 100644 index 0000000..e02f9c3 --- /dev/null +++ b/ethos/packages/domain/src/profile.ts @@ -0,0 +1,22 @@ +import { type ProfileId } from '@ethos/blockchain-manager'; +import { type Address } from 'viem'; + +/** + * A lightweight Ethos profile reference without any additional data. + * Prefer using this type by default & perform additional queries for invites, + * attestations, and events. + */ +export type LiteProfile = { + id: ProfileId; + archived: boolean; + createdAt: number; + updatedAt: number; + invitesAvailable: number; + invitedBy: number; +}; + +export type ProfileAddresses = { + profileId?: ProfileId; + primaryAddress: Address; + allAddresses: Address[]; +}; diff --git a/ethos/packages/domain/src/reply.ts b/ethos/packages/domain/src/reply.ts new file mode 100644 index 0000000..d1ee422 --- /dev/null +++ b/ethos/packages/domain/src/reply.ts @@ -0,0 +1 @@ +export type ReplySummary = { participated: boolean; count: number }; diff --git a/ethos/packages/domain/src/score.ts b/ethos/packages/domain/src/score.ts new file mode 100644 index 0000000..672c471 --- /dev/null +++ b/ethos/packages/domain/src/score.ts @@ -0,0 +1,5 @@ +export enum ScoreImpact { + POSITIVE = 'POSITIVE', + NEGATIVE = 'NEGATIVE', + NEUTRAL = 'NEUTRAL', +} diff --git a/ethos/packages/domain/src/transaction.ts b/ethos/packages/domain/src/transaction.ts new file mode 100644 index 0000000..123f306 --- /dev/null +++ b/ethos/packages/domain/src/transaction.ts @@ -0,0 +1,40 @@ +import { type Review, type Vouch } from '@ethos/blockchain-manager'; +import { type Address } from 'viem'; + +// Represents an arbitrary blockchain transaction +// based on moralis.type.ts EvmWalletHistoryTransactionJSON +export type Transaction = { + hash: string; + from_address: Address; + from_address_label?: string; + from_address_entity_logo?: string; + to_address: Address; + to_address_label?: string; + to_address_entity_logo?: string; + value: string; + block_timestamp: number; + category: string; + summary: string; +}; + +/** + * Represents a collection of transactions for a given address + * @property {Address} address - The common address that all these transactions are associated with + * @property {number} last_transaction_timestamp - Unix timestamp of the most recent transaction + * @property {Transaction[]} transactions - The transactions associated with the address + */ +export type Interaction = { + address: Address; + /** + * unix timestamp + */ + last_transaction_timestamp: number; + transactions: Transaction[]; +}; + +// Represents a relationship between a user and an address +// including any reviews or active vouches +export type Relationship = Interaction & { + reviews: Review[]; + vouch: Vouch | null; +}; diff --git a/ethos/packages/domain/src/user.ts b/ethos/packages/domain/src/user.ts new file mode 100644 index 0000000..7affa72 --- /dev/null +++ b/ethos/packages/domain/src/user.ts @@ -0,0 +1,193 @@ +import { isValidAddress } from '@ethos/helpers'; +import { getAddress, type Address } from 'viem'; + +export type EthosUserTarget = + | { address: Address } + | { service: string; account: string } + | { profileId: number }; + +/** + * An extended version of EthosUserTarget that allows the "username" field on + * service targets. + */ +export type EthosUserTargetWithTwitterUsername = + | EthosUserTarget + | { + service: string; + username: string; + }; + +// key separator +const sep = ':'; + +/** + * Converts an EthosUserTarget object to a string key for caching purposes. + * + * @param targetUser - The EthosUserTarget object to convert. + * @returns A string key representing the user target. + * @throws Error if the EthosUserTarget object is invalid. + * + * @example + * // Returns "address:0x1234..." + * toUserKey({ address: "0x1234..." }) + * + * @example + * // Returns "service:x.com:123456789" + * toUserKey({ service: "x.com", account: "123456789" }) + * + * @example + * // Returns "service:x.com:johndoe" + * toUserKey({ service: "x.com", username: "johndoe" }) + * + * @example + * // Returns "profileId:123" + * toUserKey({ profileId: 123 }) + */ +export function toUserKey( + targetUser: EthosUserTargetWithTwitterUsername, + lowercaseAddress?: boolean, +): string { + if ('address' in targetUser) { + return [ + 'address', + lowercaseAddress ? targetUser.address.toLowerCase() : targetUser.address, + ].join(sep); + } + + if ('service' in targetUser && 'account' in targetUser) { + return ['service', targetUser.service, targetUser.account].join(sep); + } + + if ('service' in targetUser && 'username' in targetUser) { + return ['service', targetUser.service, 'username', targetUser.username].join(sep); + } + + if ('profileId' in targetUser) { + return ['profileId', String(targetUser.profileId)].join(sep); + } + + throw new Error('Invalid EthosUserTarget'); +} + +/** + * Converts a string key back to an EthosUserTarget object. + * + * This function is the inverse of `toUserKey`. It takes a string key + * generated by `toUserKey` and converts it back to an EthosUserTarget object. + * + * @param key - The string key to convert. + * @returns An EthosUserTarget object. + * @throws Error if the key format is invalid. + * + * @example + * // Returns { address: "0x1234..." } + * fromUserKey("address:0x1234...") + * + * @example + * // Returns { service: "x.com", account: "123456789" } + * fromUserKey("service:x.com:123456789") + * + * @example + * // Returns { service: "x.com", username: "johndoe" } + * fromUserKey("service:x.com:username:johndoe") + * + * @example + * // Returns { profileId: 123 } + * fromUserKey("profileId:123") + */ +export function fromUserKey( + key: string, + allowTwitterUsername: true, +): EthosUserTargetWithTwitterUsername; +export function fromUserKey(key: string, allowTwitterUsername?: false): EthosUserTarget; +export function fromUserKey( + key: string, + allowTwitterUsername = false, +): EthosUserTarget | EthosUserTargetWithTwitterUsername { + const [type, ...rest] = key.split(sep); + + switch (type) { + case 'address': + return { address: getAddress(rest.join(sep)) }; + case 'service': { + const [service, account, username] = rest; + + if (account === 'username') { + if (!allowTwitterUsername) { + throw new Error('"username" is not allowed'); + } + + return { service, username }; + } + + return { service, account }; + } + case 'profileId': + return { profileId: Number(rest.join(sep)) }; + default: + throw new Error('Invalid user key format'); + } +} + +/** + * Checks whether an EthosTarget is valid or not + * + * @param targetUser - The EthosUserTarget object to convert. + * @returns A boolean representing validity + * @throws Error if the EthosUserTarget object is invalid. + * + * @example + * // Returns true + * isTargetValid({ address: "0x1234..." }) + * + * @example + * // Returns true + * isTargetValid({ service: "x.com", account: "123456789" }) + * + * @example + * // Returns true + * isTargetValid({ service: "x.com", username: "johndoe" }) + * + * @example + * // Returns true + * isTargetValid({ profileId: 1 }) + */ +export function isTargetValid(targetUser: EthosUserTargetWithTwitterUsername): boolean { + const keysLength = Object.keys(targetUser).length; + + if ('address' in targetUser && keysLength === 1) { + return isValidAddress(targetUser.address); + } + + if ('service' in targetUser && 'account' in targetUser && keysLength === 2) { + return Boolean(targetUser.service) && Boolean(targetUser.account); + } + + if ('service' in targetUser && 'username' in targetUser && keysLength === 2) { + return Boolean(targetUser.service) && Boolean(targetUser.username); + } + + if ('profileId' in targetUser && keysLength === 1) { + return targetUser.profileId > 0; + } + + throw new Error('Invalid EthosUserTarget'); +} + +export function deduplicateTargets(targets: EthosUserTarget[]): { + userkeys: string[]; + targets: EthosUserTarget[]; +} { + const userkeys = new Set(); + + for (const target of targets) { + userkeys.add(toUserKey(target)); + } + + const distinctUserkeys = Array.from(userkeys); + + return { + userkeys: distinctUserkeys, + targets: distinctUserkeys.map((key) => fromUserKey(key)), + }; +} diff --git a/ethos/packages/domain/tsconfig.json b/ethos/packages/domain/tsconfig.json new file mode 100644 index 0000000..cc8c840 --- /dev/null +++ b/ethos/packages/domain/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + } +} diff --git a/ethos/packages/echo-client/package.json b/ethos/packages/echo-client/package.json new file mode 100644 index 0000000..06db4d5 --- /dev/null +++ b/ethos/packages/echo-client/package.json @@ -0,0 +1,24 @@ +{ + "name": "@ethos/echo-client", + "version": "1.0.0", + "description": "A client package for using echo APIs", + "main": "dist/packages/echo-client/src/index.js", + "types": "dist/packages/echo-client/src/index.d.ts", + "author": "Ethos Network Inc.", + "license": "UNLICENSED", + "type": "module", + "scripts": { + "prebuild": "cd ../../services/echo && npm run prebuild", + "build": "tsc -b", + "watch": "tsc -w" + }, + "dependencies": { + "@ethos/domain": "^1.0.0", + "@ethos/helpers": "^1.0.0", + "lodash-es": "^4.17.21" + }, + "devDependencies": { + "@types/lodash-es": "^4.17.12", + "viem": "^2.21.51" + } +} \ No newline at end of file diff --git a/ethos/packages/echo-client/src/echo.ts b/ethos/packages/echo-client/src/echo.ts new file mode 100644 index 0000000..b876e5c --- /dev/null +++ b/ethos/packages/echo-client/src/echo.ts @@ -0,0 +1,899 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { type ActivityType, type EthosUserTarget, isTargetValid, toUserKey } from '@ethos/domain'; +import { isValidAddress, getApi, NetError } from '@ethos/helpers'; +import { type Address } from 'viem'; +// eslint-disable-next-line no-restricted-imports +import { + type ActivitiesRequest, + type ActivitiesResponse, + type ActivityActorsBulkResponse, + type ActivityInfoRequest, + type ActivityInfoResponse, + type ActivityVotesRequest, + type ActivityVotesResponse, + type ActorLookupResponse, + type AttestationQueryRequest, + type AttestationQueryResponse, + type ContractAddressesRequest, + type ContractAddressesResponse, + type EnsDetailsByAddressResponse, + type EnsDetailsByNameResponse, + type EthPriceResponse, + type ExtendedAttestationQueryRequest, + type ExtendedAttestationQueryResponse, + type InvitationQueryRequest, + type InvitationQueryResponse, + type MostCredibleVouchersRequest, + type MostCredibleVouchersResponse, + type MutualVouchersRequest, + type MutualVouchersResponse, + type PendingInvitationsRequest, + type PendingInvitationsResponse, + type ProfileQueryRequest, + type ProfileQueryResponse, + type RecentInteractionsRequest, + type RecentInteractionsResponse, + type RecentProfileQueryRequest, + type RecentProfileQueryResponse, + type RecentTransactionsRequest, + type RecentTransactionsResponse, + type ReplyQueryRequest, + type ReplyQueryResponse, + type ReplySummaryRequest, + type ReplySummaryResponse, + type ResponseSuccess, + type ReviewCountRequest, + type ReviewCountResponse, + type ReviewQueryRequest, + type ReviewQueryResponse, + type ReviewStatsRequest, + type ReviewStatsResponse, + type ScoreHistoryResponse, + type ScoreResponse, + type ScoreSimulationResponse, + type SearchQueryRequest, + type SearchQueryResponse, + type TwitterUserRequest, + type TwitterUserResponse, + type VouchCountRequest, + type VouchCountResponse, + type VouchedEthereumRequest, + type VouchedEthereumResponse, + type VouchQueryRequest, + type VouchQueryResponse, + type VouchRewardsRequest, + type VouchRewardsResponse, + type VouchStatsRequest, + type VouchStatsResponse, + type ProfileAddressesResponse, + type ProfileAddressesRequest, + type ActivityInviteAcceptedByRequest, + type ActivityInviteAcceptedByResponse, + type ScoreSimulationRequest, + type MarketInfoResponse, + type MarketPriceHistoryRequest, + type MarketPriceHistoryResponse, + type HighestScoringActorsResponse, + type FeesInfoResponse, + type MarketTransactionHistoryResponse, + type MarketHoldersResponse, + type MarketSearchResponse, + type MarketSearchRequest, + type EventsProcessRequest, + type EventsProcessResponse, + type ContributionStatsResponse, + type ContributionStatsRequest, + type ContributionActionResponse, + type ContributionByProfileResponse, + type ContributionByProfileRequest, + type ContributionActionRequest, + type ContributionDailyResponse, + type UpdateUserFCMTokenResponse, + type UpdateUserFCMTokenRequest, + type CredibilityLeaderboardQueryResponse, + type XPLeaderboardQueryResponse, + type SignatureRegisterAddressResponse, + type UnifiedActivityRequest, + type UnifiedActivityResponse, + type MarketTransactionHistoryRequest, + type PrivyLoginResponse, + type XpHistoryRequest, + type XpHistoryResponse, + type MarketTransactionHistoryByAddressResponse, + type MarketTransactionHistoryByAddressRequest, + type MarketHoldingsByAddressResponse, + type MarketHoldingsTotalByAddressResponse, + type MarketHoldingsByAddressRequest, + type MarketVolumeTradedByAddressResponse, + type MarketBulkInfoRequest, + type MarketBulkInfoResponse, + type CreateAttestationSignatureRequest, + type CreateAttestationSignatureResponse, + type ClaimStatsResponse, + type AcceptedReferralsRequest, + type AcceptedReferralsResponse, + type ResetClaimResponse, + type MarketNewsResponse, + type MigrationActivitiesRequest, + type MigrationActivitiesResponse, +} from '../../../services/echo/src/types/api.types.js'; + +type EchoClientConfig = { + baseUrl: string; + ethosService: string; + headers?: HeadersInit; +}; + +let echoClientConfig: EchoClientConfig | null = null; + +export function setEchoConfig(config: EchoClientConfig) { + echoClientConfig = config; +} + +function checkClientConfig( + config: EchoClientConfig | null, +): asserts config is NonNullable { + if (!config) { + throw new Error( + 'Echo Client configuration was not set. Call setEchoConfig() with a valid configuration.', + ); + } +} + +export type ActivityActor = ActorLookupResponse['data']; + +function getDefaultHeaders() { + checkClientConfig(echoClientConfig); + const headers: HeadersInit = { + 'Content-Type': 'application/json', + 'X-Ethos-Service': echoClientConfig.ethosService, + ...echoClientConfig.headers, + }; + + return headers; +} + +function getAuthHeaders(token: string) { + return { + Authorization: `Bearer ${token}`, + }; +} + +let rawRequest: ReturnType; + +async function request>( + ...args: Parameters +): Promise { + checkClientConfig(echoClientConfig); + + if (!rawRequest) { + // eslint-disable-next-line no-console + console.debug('Initiating echo API with', echoClientConfig.baseUrl); + + rawRequest = getApi(echoClientConfig.baseUrl, { headers: getDefaultHeaders() }); + } + + const response = await rawRequest(...args); + + return response.data; +} + +async function getActivities(params: ActivitiesRequest) { + return await request('/api/v1/activities', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getUnifiedActivities(params: UnifiedActivityRequest) { + return await request('/api/v1/activities/unified', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getActivityActor(target: EthosUserTarget) { + const userkey = toUserKey(target); + + if (!isTargetValid(target)) { + return null; + } + + return await request(`/api/v1/activities/actor/${userkey}`); +} + +async function getActivityActorsBulk(targets: EthosUserTarget[]) { + const userkeys = targets.map((target) => toUserKey(target)); + + return await request('/api/v1/activities/actors', { + method: 'POST', + body: JSON.stringify({ + userkeys, + }), + }); +} + +async function getActivityVotes(params: ActivityVotesRequest) { + return await request(`/api/v1/activities/votes`, { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getInvitesAcceptedBy(params: ActivityInviteAcceptedByRequest) { + let searchParamsSuffix: string | null = null; + + if (params.limit) { + const searchParams = new URLSearchParams({ limit: String(params.limit) }); + searchParamsSuffix = `?${searchParams.toString()}`; + } + + return await request( + `/api/v1/activities/invite/accepted-by/${params.profileId}${searchParamsSuffix}`, + ); +} + +async function getActivityInfo< + T extends ActivityType, + R = Extract, +>( + type: T, + id: number | string, + currentUserProfileId?: ActivityInfoRequest['currentUserProfileId'], +): Promise { + const search = new URLSearchParams({ + currentUserProfileId: String(currentUserProfileId ?? null), + }).toString(); + + return await request>(`/api/v1/activities/${type}/${id}?${search}`); +} + +async function getAttestationQuery(params: AttestationQueryRequest) { + return await request('/api/v1/attestations', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getExtendedAttestationQuery(params: ExtendedAttestationQueryRequest) { + return await request('/api/v1/attestations/extended', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getEnsDetailsByAddress(address: Address) { + if (!isValidAddress(address)) { + return null; + } + + return await request(`/api/v1/ens-details/by-address/${address}`); +} + +async function getEnsDetailsByName(name: string) { + return await request(`/api/v1/ens-details/by-name/${name}`); +} + +async function getContractAddresses({ targetContracts }: ContractAddressesRequest) { + const searchParams = new URLSearchParams({ + targetContracts: Array.isArray(targetContracts) ? targetContracts.join(',') : targetContracts, + }); + + return await request(`/api/v1/contracts?${searchParams.toString()}`); +} + +async function getEthPriceInUSD() { + return await request('/api/v1/exchange-rates/eth-price'); +} + +async function getProfile(params: ProfileQueryRequest) { + return await request('/api/v1/profiles', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getAddressesByTarget(params: ProfileAddressesRequest) { + return await request(`/api/v1/addresses/${params.userkey}`, { + method: 'GET', + }); +} + +async function getRecentProfiles(params: RecentProfileQueryRequest) { + return await request('/api/v1/profiles/recent', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getCredibilityLeaderboard(params?: { order?: 'asc' | 'desc' }) { + const searchParams = new URLSearchParams(); + + if (params?.order) { + searchParams.set('order', params.order); + } + const queryString = searchParams.toString(); + const url = `/api/v1/profiles/credibility-leaderboard${queryString ? `?${queryString}` : ''}`; + + return await request(url); +} + +async function getXpLeaderboard() { + return await request('/api/v1/profiles/xp-leaderboard'); +} + +async function getSignatureForCreateAttestation( + token: string, + params: CreateAttestationSignatureRequest, +) { + return await request( + '/api/v1/signatures/create-attestation', + { + method: 'POST', + body: JSON.stringify(params), + headers: getAuthHeaders(token), + }, + ); +} + +async function getSignatureForRegisterAddress(token: string) { + return await request('/api/v1/signatures/register-address', { + method: 'POST', + headers: getAuthHeaders(token), + }); +} + +async function getTwitterUser(params: TwitterUserRequest) { + try { + const searchParams = new URLSearchParams(params); + + return await request(`/api/twitter/user/?${searchParams.toString()}`); + } catch (err) { + if (err instanceof NetError && err.status === 404) { + return null; + } + + throw err; + } +} + +async function getReplyQuery(params: ReplyQueryRequest) { + return await request('/api/v1/reply', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getReplySummary(params: ReplySummaryRequest) { + return await request('/api/v1/reply/summary', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getReviews(params: ReviewQueryRequest) { + return await request('/api/v1/reviews', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getReviewStats(params: ReviewStatsRequest) { + return await request('/api/v1/reviews/stats', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function querySearch(params: SearchQueryRequest) { + return await request(`/api/v1/search?query=${params.query}`); +} + +async function getRecentTransactions(params: RecentTransactionsRequest) { + return await request('/api/v1/transactions/recent', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getRecentInteractions(params: RecentInteractionsRequest) { + return await request('/api/v1/transactions/interactions', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getReviewCount(params: ReviewCountRequest) { + return await request('/api/v1/reviews/count', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getVouches(params: VouchQueryRequest) { + return await request('/api/v1/vouches', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getVouchStats(params: VouchStatsRequest) { + return await request('/api/v1/vouches/stats', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getVouchRewards(params: VouchRewardsRequest) { + return await request('/api/v1/vouches/rewards', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getVouchCount(params: VouchCountRequest) { + return await request('/api/v1/vouches/count', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getVouchedEthereum(params: VouchedEthereumRequest) { + return await request('/api/v1/vouches/vouched-ethereum', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getMostCredibleVouchers(params: MostCredibleVouchersRequest) { + return await request('/api/v1/vouches/most-credible-vouchers', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getMutualVouchers(params: MutualVouchersRequest) { + return await request( + `/api/v1/vouches/mutual-vouchers?${new URLSearchParams(params).toString()}`, + ); +} + +async function getInvitations(params: InvitationQueryRequest) { + return await request('/api/v1/invitations', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getPendingInvitations(params: PendingInvitationsRequest) { + return await request(`/api/v1/invitations/pending/${params.address}`); +} + +async function getScore(target: EthosUserTarget) { + const targetKey = toUserKey(target); + + if (!isTargetValid(target)) { + return null; + } + + return await request(`/api/v1/score/${targetKey}`).then((res) => res.score); +} + +async function getScoreHistory( + target: EthosUserTarget, + days: number = 30, + extended: boolean = false, + pagination?: { limit?: number; offset?: number }, +) { + const targetKey = toUserKey(target); + + if (!isTargetValid(target)) return null; + + const searchParams = new URLSearchParams({ duration: `${days}d` }); + + if (extended) searchParams.set('expanded', 'true'); + + if (pagination?.limit) searchParams.set('limit', String(pagination.limit)); + + if (pagination?.offset) searchParams.set('offset', String(pagination.offset)); + + return await request( + `/api/v1/score/${targetKey}/history?${searchParams.toString()}`, + ); +} + +async function getScoreElements(target: EthosUserTarget): Promise { + const targetKey = toUserKey(target); + + if (!isTargetValid(target)) { + return null; + } + + return await request(`/api/v1/score/${targetKey}`); +} + +async function simulateScore( + params: ScoreSimulationRequest, +): Promise { + return await request(`/api/v1/score/simulate`, { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getHighestScoringActors(limit: number = 5) { + return await request( + `/api/v1/score/actors/highest-scores?limit=${limit}`, + ); +} + +async function getMarketInfo(profileId: number): Promise { + try { + return await request(`/api/v1/markets/${profileId}`); + } catch (err) { + if (err instanceof NetError && err.status === 404) { + return null; + } + + throw err; + } +} + +async function getMarketsByIds( + params: MarketBulkInfoRequest, +): Promise { + return await request('/api/v1/markets/bulk', { + method: 'POST', + body: JSON.stringify(params), + }); +} + +async function getMarketPriceHistory( + profileId: number, + window: MarketPriceHistoryRequest['window'], +): Promise { + return await request( + `/api/v1/markets/${profileId}/price/history?window=${window}`, + ); +} + +async function getMarketTransactionHistory({ + profileId, + pagination, + voteTypeFilter, +}: MarketTransactionHistoryRequest) { + const searchParams = new URLSearchParams(); + + if (profileId) { + searchParams.set('profileId', profileId.toString()); + } + + if (pagination?.limit) { + searchParams.set('limit', pagination.limit.toString()); + } + + if (pagination?.offset) { + searchParams.set('offset', pagination.offset.toString()); + } + + if (voteTypeFilter) { + searchParams.set('voteTypeFilter', voteTypeFilter); + } + + return await request( + `/api/v1/markets/tx/history?${searchParams.toString()}`, + ); +} + +export async function getMarketTxHistoryByAddress( + params: MarketTransactionHistoryByAddressRequest, +) { + const searchParams = new URLSearchParams(); + + if (params.pagination?.limit) { + searchParams.set('limit', params.pagination.limit.toString()); + } + + if (params.pagination?.offset) { + searchParams.set('offset', params.pagination.offset.toString()); + } + + if (params.voteTypeFilter) { + searchParams.set('voteTypeFilter', params.voteTypeFilter); + } + + return await request( + `/api/v1/markets/activity/${params.address}?${searchParams.toString()}`, + ); +} + +async function getMarketHolders(profileId: number) { + return await request(`/api/v1/markets/${profileId}/holders`); +} + +async function getMarketHoldingsByAddress(params: MarketHoldingsByAddressRequest) { + const searchParams = new URLSearchParams(); + + if (params.pagination?.limit) { + searchParams.set('limit', params.pagination.limit.toString()); + } + + if (params.pagination?.offset) { + searchParams.set('offset', params.pagination.offset.toString()); + } + + return await request( + `/api/v1/markets/holdings/${params.address}?${searchParams.toString()}`, + ); +} + +async function getMarketHoldingsTotalByAddress(address: Address) { + return await request( + `/api/v1/markets/holdings/${address}/total`, + ); +} + +async function getMarketVolumeTradedByAddress(address: string) { + return await request(`/api/v1/markets/volume/${address}`); +} + +async function getMarketNews(profileIds: number[]) { + return await request(`/api/v1/markets/news`, { + method: 'POST', + body: JSON.stringify(profileIds), + }); +} + +async function searchMarkets(params: MarketSearchRequest) { + return await request( + `/api/v1/markets/search?${new URLSearchParams(params).toString()}`, + ); +} + +async function getFeesInfo() { + return await request(`/api/v1/fees`); +} + +async function processEvents(token: string, params: EventsProcessRequest) { + return await request( + `/api/v1/events/process?${new URLSearchParams(params).toString()}`, + { + headers: getAuthHeaders(token), + }, + ); +} + +async function getContributionByProfile({ profileId, status }: ContributionByProfileRequest) { + const params = new URLSearchParams(); + status.forEach((x) => { + params.append('status[]', x); + }); + + return await request( + `/api/v1/contributions/${profileId}?${params.toString()}`, + ); +} + +async function recordContributionAction(token: string, params: ContributionActionRequest) { + await request('/api/v1/contributions/action', { + method: 'POST', + headers: getAuthHeaders(token), + body: JSON.stringify(params), + }); +} + +async function getContributionStatsByProfile({ profileId }: ContributionStatsRequest) { + return await request(`/api/v1/contributions/${profileId}/stats`); +} + +async function contributionDaily(token: string) { + return await request('/api/v1/contributions/daily', { + method: 'POST', + headers: getAuthHeaders(token), + }); +} + +async function updateUserFCMToken(token: string, params: UpdateUserFCMTokenRequest) { + return await request(`/api/v1/notifications/user-fcm-token`, { + method: 'POST', + headers: getAuthHeaders(token), + body: JSON.stringify(params), + }); +} + +async function createPrivyLogin(token: string, privyIdToken: string) { + await request('/api/v1/privy-logins', { + method: 'POST', + headers: { + ...getAuthHeaders(token), + 'X-Privy-Id-Token': privyIdToken, + }, + }); +} + +async function getXpHistory({ + userkey, + pagination, +}: XpHistoryRequest): Promise { + const searchParams = new URLSearchParams(); + + if (typeof pagination?.limit === 'number') { + searchParams.append('limit', String(pagination.limit)); + } + + if (typeof pagination?.offset === 'number') { + searchParams.append('offset', String(pagination.offset)); + } + + return await request( + `/api/v1/xp/${userkey}/history?${searchParams.toString()}`, + ); +} + +async function getClaimStats() { + return await request('/api/v1/claim/stats', { + credentials: 'include', + }); +} + +async function getAcceptedReferrals({ pagination }: AcceptedReferralsRequest) { + const searchParams = new URLSearchParams(); + + if (pagination?.limit) { + searchParams.set('limit', String(pagination.limit)); + } + + if (pagination?.offset) { + searchParams.set('offset', String(pagination.offset)); + } + + return await request( + `/api/v1/claim/accepted-referrals?${searchParams.toString()}`, + { + credentials: 'include', + }, + ); +} + +async function resetClaim() { + await request(`/api/v1/claim`, { + method: 'DELETE', + credentials: 'include', + }); +} + +async function getMigrationActivities({ query, pagination }: MigrationActivitiesRequest) { + const searchParams = new URLSearchParams({ query }); + + if (pagination?.limit) { + searchParams.set('limit', String(pagination.limit)); + } + + if (pagination?.offset) { + searchParams.set('offset', String(pagination.offset)); + } + + return await request('/api/v1/migration/activities'); +} + +export const echoClient = { + activities: { + unified: getUnifiedActivities, + get: getActivityInfo, + bulk: getActivities, + actor: getActivityActor, + actorsBulk: getActivityActorsBulk, + votes: getActivityVotes, + invitesAcceptedBy: getInvitesAcceptedBy, + }, + addresses: { + getByTarget: getAddressesByTarget, + }, + attestations: { + query: getAttestationQuery, + queryExtended: getExtendedAttestationQuery, + }, + ens: { + getDetailsByAddress: getEnsDetailsByAddress, + getDetailsByName: getEnsDetailsByName, + }, + contracts: { + getAddresses: getContractAddresses, + }, + exchangeRates: { + getEthPriceInUSD, + }, + invitations: { + query: getInvitations, + pending: getPendingInvitations, + }, + profiles: { + query: getProfile, + recent: getRecentProfiles, + credibilityLeaderboard: getCredibilityLeaderboard, + xpLeaderboard: getXpLeaderboard, + }, + xp: { + history: getXpHistory, + }, + signatures: { + createAttestation: getSignatureForCreateAttestation, + registerAddress: getSignatureForRegisterAddress, + }, + twitter: { + user: { + get: getTwitterUser, + }, + }, + replies: { + query: getReplyQuery, + summary: getReplySummary, + }, + reviews: { + query: getReviews, + stats: getReviewStats, + count: getReviewCount, + }, + scores: { + get: getScore, + history: getScoreHistory, + highestScoringActors: getHighestScoringActors, + elements: getScoreElements, + simulate: simulateScore, + }, + search: { + query: querySearch, + }, + transactions: { + recent: getRecentTransactions, + interactions: getRecentInteractions, + }, + vouches: { + query: getVouches, + stats: getVouchStats, + rewards: getVouchRewards, + count: getVouchCount, + vouchedEthereum: getVouchedEthereum, + mostCredibleVouchers: getMostCredibleVouchers, + mutualVouchers: getMutualVouchers, + }, + markets: { + search: searchMarkets, + info: getMarketInfo, + infoByIds: getMarketsByIds, + priceHistory: getMarketPriceHistory, + transactionHistory: getMarketTransactionHistory, + txHistoryByAddress: getMarketTxHistoryByAddress, + holdingsByAddress: getMarketHoldingsByAddress, + holdingsTotalByAddress: getMarketHoldingsTotalByAddress, + holders: getMarketHolders, + volumeByAddress: getMarketVolumeTradedByAddress, + news: getMarketNews, + }, + fees: { + info: getFeesInfo, + }, + events: { + process: processEvents, + }, + contribution: { + getByProfile: getContributionByProfile, + recordAction: recordContributionAction, + statsByProfile: getContributionStatsByProfile, + daily: contributionDaily, + }, + fcm: { + updateUserToken: updateUserFCMToken, + }, + privyLogins: { + create: createPrivyLogin, + }, + claim: { + acceptedReferrals: getAcceptedReferrals, + reset: resetClaim, + stats: getClaimStats, + }, + migration: { + activities: getMigrationActivities, + }, +}; diff --git a/ethos/packages/echo-client/src/index.ts b/ethos/packages/echo-client/src/index.ts new file mode 100644 index 0000000..a7e693d --- /dev/null +++ b/ethos/packages/echo-client/src/index.ts @@ -0,0 +1,3 @@ +export { echoClient, setEchoConfig } from './echo.js'; +export { extractEchoErrorCode, extractEchoErrorMessage } from './utils/request-utils.js'; +export * from '../../../services/echo/src/types/api.types.js'; diff --git a/ethos/packages/echo-client/src/utils/request-utils.ts b/ethos/packages/echo-client/src/utils/request-utils.ts new file mode 100644 index 0000000..9f9fc92 --- /dev/null +++ b/ethos/packages/echo-client/src/utils/request-utils.ts @@ -0,0 +1,23 @@ +import { NetError } from '@ethos/helpers'; + +/** + * Echo API has a standard error response format. This function extracts the + * error message from the response body. + */ +export function extractEchoErrorMessage(err: unknown): string { + let message = 'Something went wrong! Please try again later.'; + + if (err instanceof NetError) { + message = err.body?.error?.message ?? err.message; + } + + return message; +} + +export function extractEchoErrorCode(err: unknown): string | undefined { + if (err instanceof NetError) { + return err.body?.error?.code; + } + + return undefined; +} diff --git a/ethos/packages/echo-client/tsconfig.json b/ethos/packages/echo-client/tsconfig.json new file mode 100644 index 0000000..0736659 --- /dev/null +++ b/ethos/packages/echo-client/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + }, + "include": [ + "src/**/*", + "../../services/echo/src/types/**/*.ts" + ] +} diff --git a/ethos/packages/helpers/src/ens.ts b/ethos/packages/helpers/src/ens.ts index 76717cc..f75b20d 100644 --- a/ethos/packages/helpers/src/ens.ts +++ b/ethos/packages/helpers/src/ens.ts @@ -1,3 +1,5 @@ -export function isValidEnsName(name: string): boolean { +export function isValidEnsName(name: string | undefined): boolean { + if (!name) return false; + return name.endsWith('.eth'); } diff --git a/ethos/packages/helpers/src/ens.ts.rej b/ethos/packages/helpers/src/ens.ts.rej new file mode 100644 index 0000000..a8aa07f --- /dev/null +++ b/ethos/packages/helpers/src/ens.ts.rej @@ -0,0 +1,8 @@ +diff a/ethos/packages/helpers/src/ens.ts b/ethos/packages/helpers/src/ens.ts (rejected hunks) +@@ -1,3 +1,5 @@ +-export function isValidEnsName(name: string): boolean { ++export function isValidEnsName(name: string | undefined): boolean { ++ if (!name) return false; ++ + return name.endsWith('.eth'); + } diff --git a/ethos/packages/helpers/src/hash.ts b/ethos/packages/helpers/src/hash.ts new file mode 100644 index 0000000..ba15c0c --- /dev/null +++ b/ethos/packages/helpers/src/hash.ts @@ -0,0 +1,26 @@ +import { type Hash, isHash, zeroHash } from 'viem'; + +/** + * Safely compares two hashes for equality + */ +export function isHashEqualSafe(a: Hash, b: Hash): boolean { + try { + return a.toLowerCase() === b.toLowerCase(); + } catch { + return false; + } +} + +/** + * Checks if the given hash is valid and not equal to the zero hash. + * + * @param hash The hash to check. + * @returns True if the hash is valid and not equal to the zero hash. + */ +export function isValidHash(hash?: string | null): hash is Hash { + if (!hash) return false; + if (!isHash(hash)) return false; + if (isHashEqualSafe(hash, zeroHash)) return false; + + return true; +} diff --git a/ethos/packages/helpers/src/index.ts b/ethos/packages/helpers/src/index.ts index 6800f83..da069c0 100644 --- a/ethos/packages/helpers/src/index.ts +++ b/ethos/packages/helpers/src/index.ts @@ -1,4 +1,5 @@ export { isAddressEqualSafe, isValidAddress } from './address.js'; +export { isHashEqualSafe, isValidHash } from './hash.js'; export { isValidEnsName } from './ens.js'; export { notEmpty } from './notEmpty.js'; export { formatCurrency, formatEth, formatNumber, toNumber, formatXPScore } from './number.js'; @@ -18,3 +19,5 @@ export { generateSlug } from './urlUtils.js'; export * from './types.js'; export * from './delay.js'; export { capitalize } from './text.js'; +export { generateIntentTweetUrl } from './tweet.js'; +export { getApi, NetError } from './request.js'; diff --git a/ethos/packages/helpers/src/index.ts.rej b/ethos/packages/helpers/src/index.ts.rej new file mode 100644 index 0000000..b341019 --- /dev/null +++ b/ethos/packages/helpers/src/index.ts.rej @@ -0,0 +1,13 @@ +diff a/ethos/packages/helpers/src/index.ts b/ethos/packages/helpers/src/index.ts (rejected hunks) +@@ -1,4 +1,5 @@ + export { isAddressEqualSafe, isValidAddress } from './address.js'; ++export { isHashEqualSafe, isValidHash } from './hash.js'; + export { isValidEnsName } from './ens.js'; + export { notEmpty } from './notEmpty.js'; + export { formatCurrency, formatEth, formatNumber, toNumber, formatXPScore } from './number.js'; +@@ -18,3 +19,5 @@ export { generateSlug } from './urlUtils.js'; + export * from './types.js'; + export * from './delay.js'; + export { capitalize } from './text.js'; ++export { generateIntentTweetUrl } from './tweet.js'; ++export { getApi, NetError } from './request.js'; diff --git a/ethos/packages/helpers/src/request.ts b/ethos/packages/helpers/src/request.ts new file mode 100644 index 0000000..af5c63c --- /dev/null +++ b/ethos/packages/helpers/src/request.ts @@ -0,0 +1,48 @@ +import { cloneDeep, merge } from 'lodash-es'; +import { reviver } from './json.js'; + +export class NetError extends Error { + status: number; + body: any; + code?: string; + + constructor( + message: string, + { status, body, code }: { status: number; body: any; code?: string }, + ) { + super(message); + + this.status = status; + this.body = body; + + if (code) { + this.code = code; + } + } +} + +export function getApi(origin?: string, defaultOptions?: RequestInit) { + return async function request(pathname: string, options?: RequestInit): Promise { + const url = origin ? new URL(pathname, origin) : pathname; + + const response = await fetch(url, merge(cloneDeep(defaultOptions), options)); + + const isJSON = response.headers.get('Content-Type')?.includes('application/json'); + + const body = isJSON + ? await response.text().then((x) => JSON.parse(x, reviver)) + : await response.text(); + + if (response.status > 399) { + const code = isJSON ? body?.error?.code : undefined; + + throw new NetError(`${response.status}: ${response.statusText}`, { + status: response.status, + body, + code, + }); + } + + return body as R; + }; +} diff --git a/ethos/packages/helpers/src/tweet.ts b/ethos/packages/helpers/src/tweet.ts new file mode 100644 index 0000000..f08b9d1 --- /dev/null +++ b/ethos/packages/helpers/src/tweet.ts @@ -0,0 +1,5 @@ +export function generateIntentTweetUrl(tweetContent: string): string { + const searchParams = new URLSearchParams({ text: tweetContent }); + + return `https://twitter.com/intent/tweet?${searchParams.toString()}`; +} diff --git a/ethos/packages/score/package.json b/ethos/packages/score/package.json new file mode 100644 index 0000000..5f47a7e --- /dev/null +++ b/ethos/packages/score/package.json @@ -0,0 +1,25 @@ +{ + "name": "@ethos/score", + "version": "1.0.0", + "description": "Domain Specific Language (DSL) for calculating Ethos Credibility Scores", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + }, + "keywords": [ + "score", + "calculation", + "model" + ], + "author": "Ethos Network Inc.", + "license": "UNLICENSED", + "devDependencies": { + "type-fest": "^4.26.1" + }, + "dependencies": { + "lodash-es": "^4.17.21" + } +} diff --git a/ethos/packages/score/src/__tests__/scoreCalculation.test.ts b/ethos/packages/score/src/__tests__/scoreCalculation.test.ts new file mode 100644 index 0000000..38ded21 --- /dev/null +++ b/ethos/packages/score/src/__tests__/scoreCalculation.test.ts @@ -0,0 +1,130 @@ +import { convertScoreElementToCredibilityFactor, elementRange } from '../convertScore.js'; +import { getDefaultScoreCalculation } from '../defaultScoreRules.js'; +import { DEFAULT_STARTING_SCORE, scoreRanges } from '../score.constant.js'; +import { type LookupInterval, type LookupNumber } from '../score.types.js'; + +describe('convertScoreElementToCredibilityFactor', () => { + describe('LookupInterval', () => { + const lookupInterval: LookupInterval = { + name: 'Test Interval', + type: 'LookupInterval', + ranges: [ + { start: undefined, end: 5, score: 50 }, + { start: 5, end: 90, score: 350 }, + { start: 90, end: undefined, score: 600 }, + ], + outOfRangeScore: 0, + }; + + it('should convert LookupInterval to CredibilityFactor with low value', () => { + const result = convertScoreElementToCredibilityFactor(lookupInterval, 3); + expect(result).toEqual({ + name: 'Test Interval', + range: { min: 50, max: 600 }, + value: 3, + weighted: 50, + }); + }); + + it('should convert LookupInterval to CredibilityFactor with medium value', () => { + const result = convertScoreElementToCredibilityFactor(lookupInterval, 50); + expect(result).toEqual({ + name: 'Test Interval', + range: { min: 50, max: 600 }, + value: 50, + weighted: 350, + }); + }); + + it('should convert LookupInterval to CredibilityFactor with high value', () => { + const result = convertScoreElementToCredibilityFactor(lookupInterval, 95); + expect(result).toEqual({ + name: 'Test Interval', + range: { min: 50, max: 600 }, + value: 95, + weighted: 600, + }); + }); + }); + + describe('LookupNumber', () => { + const lookupNumber: LookupNumber = { + name: 'Test Number', + type: 'LookupNumber', + range: { min: -400, max: 400 }, + }; + + it('should convert LookupNumber to CredibilityFactor with low value', () => { + const result = convertScoreElementToCredibilityFactor(lookupNumber, -300); + expect(result).toEqual({ + name: 'Test Number', + range: { min: -400, max: 400 }, + value: -300, + weighted: -300, + }); + }); + + it('should convert LookupNumber to CredibilityFactor with medium value', () => { + const result = convertScoreElementToCredibilityFactor(lookupNumber, 100); + expect(result).toEqual({ + name: 'Test Number', + range: { min: -400, max: 400 }, + value: 100, + weighted: 100, + }); + }); + + it('should convert LookupNumber to CredibilityFactor with high value', () => { + const result = convertScoreElementToCredibilityFactor(lookupNumber, 350); + expect(result).toEqual({ + name: 'Test Number', + range: { min: -400, max: 400 }, + value: 350, + weighted: 350, + }); + }); + }); +}); + +describe('Score Range Validation', () => { + it('should ensure score ranges do not sum to below 0 or above the maximum score', () => { + const { elementDefinitions } = getDefaultScoreCalculation(); + + let totalMax = DEFAULT_STARTING_SCORE; + + for (const element of elementDefinitions) { + const range = elementRange(element); + + if (element.omit) continue; + totalMax += range.max; + } + + function logScoreAnalysis(): void { + let sumMin = DEFAULT_STARTING_SCORE; + let sumMax = DEFAULT_STARTING_SCORE; + + for (const element of elementDefinitions) { + const range = elementRange(element); + sumMin += range.min; + sumMax += range.max; + // eslint-disable-next-line no-console + console.log( + `${element.name.padEnd(35)} min: ${String(range.min).padStart(4)}, max: ${range.max}`, + ); + } + + // eslint-disable-next-line no-console + console.log( + '\nSum of ranges:'.padEnd(35) + ` min: ${String(sumMin).padStart(4)}, max: ${sumMax}`, + ); + // eslint-disable-next-line no-console + console.log(); + } + + if (totalMax > scoreRanges.exemplary.max) logScoreAnalysis(); + + // okay to go below zero; we will return 0 + // expect(totalMin).toBeGreaterThanOrEqual(scoreRanges.untrusted.min); + expect(totalMax).toBeLessThanOrEqual(scoreRanges.exemplary.max); + }); +}); diff --git a/ethos/packages/score/src/convertScore.ts b/ethos/packages/score/src/convertScore.ts new file mode 100644 index 0000000..f5b8f7c --- /dev/null +++ b/ethos/packages/score/src/convertScore.ts @@ -0,0 +1,90 @@ +import { type Entries } from 'type-fest'; +import { scoreRanges } from './score.constant.js'; +import { + type CredibilityFactor, + type ScoreLevel, + type ScoreElement, + isLookupInterval, + isLookupNumber, + isScoreCalculation, + isConstantValueElement, +} from './score.types.js'; +import { calculateElement } from './scoreElements.js'; + +/** + * Converts a numerical score to its corresponding ScoreLevel category + * We use ScoreLevels to describe in plain terms the credibility of an actor/user + * + * @param score - The numerical score to convert. + * @returns The corresponding ScoreLevel (e.g., 'untrusted', 'questionable', 'neutral', 'reputable', 'exemplary'). + * @throws Error if the score doesn't fall within any defined range. + */ +export function convertScoreToLevel(score: number): ScoreLevel { + const scoreRange = (Object.entries(scoreRanges) as Entries).find( + ([_, { min, max }]) => score >= min && score <= max, + ); + + if (!scoreRange) { + throw new Error(`Invalid score: ${score}`); + } + + return scoreRange[0]; +} + +/** + * Helps explain each score element as a Credibility Factor + * A credibility factor demonstrates the relative contribution of each score element + * to the overall score, and can be used to explain why a particular score was assigned + * + * It includes the maximum possible range by which this element can impact the score, + * as well as the raw value (ie, # of days) and weighted value (ie, determined by interval) + * of that raw value. + * + * @param scoreElement - The ScoreElement to be converted. + * @param value - The numerical value associated with the ScoreElement. + * @returns A CredibilityFactor object containing the name, range, value, and weighted score. + */ +export function convertScoreElementToCredibilityFactor( + scoreElement: ScoreElement, + value: number, +): CredibilityFactor { + if (scoreElement.type !== 'LookupInterval' && scoreElement.type !== 'LookupNumber') { + throw new Error('Invalid score element type'); + } + + const range = elementRange(scoreElement); + const weighted = calculateElement(scoreElement, { [scoreElement.name]: value }).score; + + return { + name: scoreElement.name, + range, + value, + weighted, + }; +} + +/** + * Determine the range by which a score element can impact the score + * (score element impact range differs depending if it's defined by interval or sigmoid) + * @param element - The ScoreElement to determine the range of + * @returns The range of the score element, as a { min, max } object + * @throws Error if the score element type is invalid + */ +export function elementRange(element: ScoreElement): { min: number; max: number } { + if (isLookupInterval(element)) { + const min = element.ranges.reduce((min, r) => Math.min(min, r.score), 1000); + const max = element.ranges.reduce((max, r) => Math.max(max, r.score), -1000); + + return { min, max }; + } + if (isLookupNumber(element)) { + return element.range; + } + if (isScoreCalculation(element)) { + return { min: 0, max: 0 }; + } + if (isConstantValueElement(element)) { + return { min: 0, max: 0 }; + } + throw new Error(`Unsupported element type: ${JSON.stringify(element)}`); +} diff --git a/ethos/packages/score/src/defaultScoreRules.ts b/ethos/packages/score/src/defaultScoreRules.ts new file mode 100644 index 0000000..685c15d --- /dev/null +++ b/ethos/packages/score/src/defaultScoreRules.ts @@ -0,0 +1,123 @@ +import cloneDeep from 'lodash-es/cloneDeep.js'; +import { DEFAULT_STARTING_SCORE } from './score.constant.js'; +import { type ScoreConfig, type ScoreElement } from './score.types.js'; + +/** + * Get the default score calculation configuration. + * Note: do not export the default score rules directly, as modifications could + * persist between returned objects. + * @returns {ScoreConfig} A deep clone of the default score rules configuration + */ +export function getDefaultScoreCalculation(): ScoreConfig { + return cloneDeep(defaultScoreRules); +} + +/** + * Enumeration of all available score element names used in the scoring algorithm. + * Each element represents a different factor that contributes to the final score. + */ +export enum ScoreElementNames { + /** Days since first transaction */ + ETHEREUM_ADDRESS_AGE = 'Ethereum Address Age', + /** Days since profile registered */ + TWITTER_ACCOUNT_AGE = 'Twitter Account Age', + ETHOS_INVITATION_SOURCE_CREDIBILITY = 'Ethos Invitation Source Credibility', + REVIEW_IMPACT = 'Review Impact', + VOUCHED_ETHEREUM_IMPACT = 'Vouched Ethereum Impact', + NUMBER_OF_VOUCHERS_IMPACT = 'Number of Vouchers Impact', + MUTUAL_VOUCHER_BONUS = 'Mutual Vouch Bonus', + VOTE_IMPACT = 'Vote Impact', + OFFCHAIN_REPUTATION = 'Offchain Reputation', +} + +const elementDefinitions: ScoreElement[] = [ + { + name: ScoreElementNames.ETHEREUM_ADDRESS_AGE, + type: 'LookupInterval', + ranges: [ + { start: undefined, end: 90, score: 0 }, + { start: 90, end: 365, score: 15 }, + { start: 365, end: 1461, score: 25 }, + { start: 1461, end: 2922, score: 50 }, + { start: 2922, end: undefined, score: 75 }, + ], + outOfRangeScore: 0, + }, + { + name: ScoreElementNames.TWITTER_ACCOUNT_AGE, + type: 'LookupInterval', + ranges: [ + { start: 0, end: 90, score: -250 }, + { start: 90, end: 365, score: -50 }, + { start: 365, end: 730, score: 0 }, + { start: 1461, end: undefined, score: 25 }, + ], + outOfRangeScore: 0, + }, + { + name: ScoreElementNames.ETHOS_INVITATION_SOURCE_CREDIBILITY, + type: 'LookupNumber', + range: { min: -200, max: 330 }, + }, + { + name: ScoreElementNames.REVIEW_IMPACT, + type: 'LookupNumber', + range: { min: -400, max: 270 }, + }, + { + name: ScoreElementNames.VOUCHED_ETHEREUM_IMPACT, + type: 'LookupNumber', + range: { min: 0, max: 270 }, + }, + { + name: ScoreElementNames.NUMBER_OF_VOUCHERS_IMPACT, + type: 'LookupNumber', + range: { min: 0, max: 270 }, + }, + { + name: ScoreElementNames.MUTUAL_VOUCHER_BONUS, + type: 'LookupNumber', + range: { min: 0, max: 270 }, + }, + { + name: ScoreElementNames.VOTE_IMPACT, + type: 'LookupNumber', + range: { min: -400, max: 90 }, + }, + { + name: ScoreElementNames.OFFCHAIN_REPUTATION, + type: 'LookupNumber', + range: { min: -400, max: 400 }, + omit: true, + }, +]; + +const defaultScoreRules: ScoreConfig = { + rootCalculation: { + type: 'Calculation', + name: 'Root Score Calculation', + operation: '+', + elements: [ + { type: 'Constant', name: 'Base Score', value: DEFAULT_STARTING_SCORE }, + ...elementDefinitions, + ], + }, + elementDefinitions, +}; + +const scoreElementMap = new Map(elementDefinitions.map((element) => [element.name, element])); + +/** + * Retrieves a score element configuration by name. + * @param {ScoreElementNames} name - The name of the score element to retrieve + * @returns {ScoreElement | undefined} The score element configuration if found, undefined otherwise + * + * @example + * const ethAgeElement = getScoreElement(ScoreElementNames.ETHEREUM_ADDRESS_AGE); + * if (ethAgeElement?.type === 'LookupInterval') { + * // Process interval-based scoring + * } + */ +export function getScoreElement(name: ScoreElementNames): ScoreElement | undefined { + return scoreElementMap.get(name); +} diff --git a/ethos/packages/score/src/index.ts b/ethos/packages/score/src/index.ts new file mode 100644 index 0000000..a84d454 --- /dev/null +++ b/ethos/packages/score/src/index.ts @@ -0,0 +1,37 @@ +export { calculateScore, type ScoreCalculation } from './scoreCalculation.js'; +export type { + ScoreElement, + ElementName, + ElementType, + LookupInterval, + LookupNumber, + IntervalRange, + ElementInputs, + ElementResult, + CredibilityFactor, + ScoreConfig, +} from './score.types.js'; +export { + isLookupNumber, + isLookupInterval, + isScoreCalculation, + isConstantValueElement, +} from './score.types.js'; +export { calculateElement } from './scoreElements.js'; +export { + getDefaultScoreCalculation, + ScoreElementNames, + getScoreElement, +} from './defaultScoreRules.js'; +export { scoreRanges } from './score.constant.js'; +export { + bondingPeriod, + invitationScoreFactor, + mutualVouchMultiplier, + maxVouchedEthDays, + scoreLevelXpMultiplier, + DEFAULT_STARTING_SCORE, +} from './score.constant.js'; +export type { ScoreLevel, ScoreRange } from './score.types.js'; +export { convertScoreToLevel, elementRange } from './convertScore.js'; +export { convertScoreElementToCredibilityFactor } from './convertScore.js'; diff --git a/ethos/packages/score/src/score.constant.ts b/ethos/packages/score/src/score.constant.ts new file mode 100644 index 0000000..b212176 --- /dev/null +++ b/ethos/packages/score/src/score.constant.ts @@ -0,0 +1,42 @@ +import { type ScoreRange, type ScoreLevel } from './score.types.js'; + +export const scoreRanges: Record = { + untrusted: { min: 0, max: 799 }, + questionable: { min: 800, max: 1199 }, + neutral: { min: 1200, max: 1599 }, + reputable: { min: 1600, max: 1999 }, + exemplary: { min: 2000, max: 2800 }, +}; + +/** + * The score displayed as a fallback if real score could not be retrieved. + */ +export const DEFAULT_STARTING_SCORE = 1200; + +/** + * The number of days a user is bonded with their inviter after accepting an invitation + */ +export const bondingPeriod = 90; +/** + * The factor by which the inviter's score is multiplied to determine the invitee's score impact + */ +export const invitationScoreFactor = 0.2; + +/** + * The maximum number of days to consider when calculating vouched ETH impact. + */ +export const maxVouchedEthDays = 180; + +/** + * Mutual vouch bonus multiplier + * Added on top of the underlying vouch impact + */ +export const mutualVouchMultiplier = 0.5; + +export const scoreLevelXpMultiplier: Record = { + exemplary: 1.5, + reputable: 1.25, + neutral: 1, + questionable: 0.5, + untrusted: 0.2, +}; diff --git a/ethos/packages/score/src/score.types.ts b/ethos/packages/score/src/score.types.ts new file mode 100644 index 0000000..bc7096d --- /dev/null +++ b/ethos/packages/score/src/score.types.ts @@ -0,0 +1,104 @@ +import { type ScoreCalculation } from './scoreCalculation.js'; + +export type ScoreConfig = { + rootCalculation: ScoreCalculation; + elementDefinitions: ScoreElement[]; +}; + +/** + * Represents an arbitrary score element. + * This should never be used directly; use a subtype instead. + */ +export type ScoreElement = LookupInterval | LookupNumber | ScoreCalculation | ConstantValueElement; + +export type ScoreElementBase = { + name: ElementName; + type: ElementType; + /** Omit this element from the score calculation results if the net impact is 0 */ + omit?: boolean; +}; + +export type ElementName = string; + +export type ElementType = 'LookupInterval' | 'LookupNumber' | 'Calculation' | 'Constant'; + +export function isLookupNumber(element: ScoreElement): element is LookupNumber { + return element.type === 'LookupNumber'; +} + +export function isLookupInterval(element: ScoreElement): element is LookupInterval { + return element.type === 'LookupInterval'; +} + +export function isScoreCalculation(element: ScoreElement): element is ScoreCalculation { + return element.type === 'Calculation'; +} + +export function isConstantValueElement(element: ScoreElement): element is ConstantValueElement { + return element.type === 'Constant'; +} + +/** + * The result of an element calculation, including the element, raw value, + * weighted value (when slotted into an IntervalLookup), and any errors + * encountered during calculation. + */ +export type ElementResult = { + element: ScoreElement; + raw: number; + weighted: number; + error: boolean; +}; + +/** + * Constant elements return a static value. + */ +export type ConstantValueElement = { + type: 'Constant'; + value: number; +} & ScoreElementBase; + +/** + * Generate a score based on placement within a defined set of ranges. + * If the value does not fit into the defined ranges, use the outOfRangeScore. + * If a range start or end is undefined, it's considered to be negative or positive infinity, respectively. + */ +export type LookupInterval = { + type: 'LookupInterval'; + ranges: IntervalRange[]; + outOfRangeScore: number; +} & ScoreElementBase; + +export type IntervalRange = { + start: number | undefined; + end: number | undefined; + score: number; +}; + +/** + * Lookup elements that return a single number. + */ +export type LookupNumber = { + type: 'LookupNumber'; + range: { min: number; max: number }; +} & ScoreElementBase; + +export type ElementInputs = Record; + +export type ScoreLevel = 'untrusted' | 'questionable' | 'neutral' | 'reputable' | 'exemplary'; + +export type ScoreRange = { + min: number; + max: number; +}; + +/** + * Credibility Factor helps explain the relevance of a score element + * by providing a relative status and impact for a given element value + */ +export type CredibilityFactor = { + name: string; + range: { min: number; max: number }; + value: number; + weighted: number; +}; diff --git a/ethos/packages/score/src/scoreCalculation.ts b/ethos/packages/score/src/scoreCalculation.ts new file mode 100644 index 0000000..2433e38 --- /dev/null +++ b/ethos/packages/score/src/scoreCalculation.ts @@ -0,0 +1,98 @@ +import { type ScoreElementBase, type ElementInputs, type ScoreElement } from './score.types.js'; +import { calculateElement } from './scoreElements.js'; + +/** + * Applies a mathematical operation against a set of score components. + * The operation will be applied to each component in sequence. + * ScoreCalculations can be nested to represent complex operations and order of operations. + */ +export type ScoreCalculation = { + operation: ScoreCalculationOperation; + elements: ScoreElement[]; +} & ScoreElementBase; + +export function calculateScore( + rootCalculation: ScoreCalculation, + inputs: ElementInputs, +): { score: number } { + const { score } = calculateElement(rootCalculation, inputs); + + if (score < 0) return { score: 0 }; + + return { score: Math.round(score) }; +} + +const validOperations = ['+', '-', '*', '/', '^', 'log', 'sqrt', 'abs', 'ceil', 'floor'] as const; + +type ScoreCalculationOperation = (typeof validOperations)[number]; + +export function isValidOperation(operation: string): operation is ScoreCalculationOperation { + return validOperations.includes(operation as ScoreCalculationOperation); +} + +export function newCalculation(operation: string): ScoreCalculation { + if (isValidOperation(operation)) { + return { + name: operation, + type: 'Calculation', + operation, + elements: [] as ScoreElement[], + }; + } + throw new Error(`Invalid operation: ${operation}`); +} + +export function applyCalculation( + calculation: ScoreCalculation, + inputs: ElementInputs, +): { score: number } { + switch (calculation.operation) { + case '+': + return { + score: calculation.elements.reduce( + (sum, element) => sum + calculateElement(element, inputs).score, + 0, + ), + }; + case '-': + return { + score: calculation.elements.reduce( + (diff, element) => diff - calculateElement(element, inputs).score, + 0, + ), + }; + case '*': + return { + score: calculation.elements.reduce( + (product, element) => product * calculateElement(element, inputs).score, + 1, + ), + }; + case '/': + return { + score: calculation.elements.reduce( + (quotient, element) => quotient / calculateElement(element, inputs).score, + 1, + ), + }; + case '^': + return { + score: calculation.elements.reduce( + (power, element) => power ** calculateElement(element, inputs).score, + 1, + ), + }; + case 'log': + return { score: Math.log(calculateElement(calculation.elements[0], inputs).score) }; + case 'sqrt': + return { score: Math.sqrt(calculateElement(calculation.elements[0], inputs).score) }; + case 'abs': + return { score: Math.abs(calculateElement(calculation.elements[0], inputs).score) }; + case 'ceil': + return { score: Math.ceil(calculateElement(calculation.elements[0], inputs).score) }; + case 'floor': + return { score: Math.floor(calculateElement(calculation.elements[0], inputs).score) }; + default: + throw new Error(`Unsupported operation: ${String(calculation.operation)}`); + } +} diff --git a/ethos/packages/score/src/scoreElements.ts b/ethos/packages/score/src/scoreElements.ts new file mode 100644 index 0000000..ee4c3c9 --- /dev/null +++ b/ethos/packages/score/src/scoreElements.ts @@ -0,0 +1,140 @@ +import { + type ConstantValueElement, + type ElementInputs, + type ScoreElement, + type LookupInterval, + type LookupNumber, + type ElementName, + isLookupInterval, + isLookupNumber, + isScoreCalculation, + isConstantValueElement, +} from './score.types.js'; +import { applyCalculation } from './scoreCalculation.js'; + +/** + * Recursively apply element calculations (according to each subclass type) to generate a score. + */ +export function calculateElement(element: ScoreElement, inputs: ElementInputs): { score: number } { + if (isLookupInterval(element)) { + return applyLookup(element, inputs); + } + if (isLookupNumber(element)) { + return applyLookup(element, inputs); + } + if (isScoreCalculation(element)) { + return applyCalculation(element, inputs); + } + if (isConstantValueElement(element)) { + return applyConstantValueElement(element, inputs); + } + throw new Error(`Unknown element type: ${(element as any).type ?? 'undefined'}`); +} + +function applyConstantValueElement( + element: ConstantValueElement, + _inputs: ElementInputs, +): { score: number } { + return { score: element.value }; +} + +export function newConstantElement(name: ElementName, value: number): ConstantValueElement { + return { + name, + type: 'Constant', + value, + }; +} + +function applyLookup( + lookup: LookupInterval | LookupNumber, + inputs: ElementInputs, +): { score: number } { + switch (lookup.type) { + case 'LookupInterval': + return applyInterval(lookup, inputs); + case 'LookupNumber': + return applyLookupNumber(lookup, inputs); + } +} + +/** + * Maps an input value to a score based on a set of interval ranges. + * + * Each interval has a series of ranges with start/end boundaries and corresponding scores. + * The function finds which range contains the input value and returns that range's score. + * + * Example for "Ethereum Address Age": + * - 0-90 days: score = 0 + * - 90-365 days: score = 25 + * - 365-1461 days: score = 100 + * - 1461+ days: score = 250 + * + * If the input doesn't fall into any defined range, returns the outOfRangeScore. + * + * @param interval - The interval definition with its ranges and scores + * @param inputs - The raw input values to check against ranges + * @returns The score for the matching range + */ +function applyInterval(interval: LookupInterval, inputs: ElementInputs): { score: number } { + const input = inputs[interval.name]; + + for (const range of interval.ranges) { + if ( + (range.start === undefined || input >= range.start) && + (range.end === undefined || input < range.end) + ) { + return { score: range.score }; + } + } + + return { score: interval.outOfRangeScore }; +} + +export function newLookupNumber(name: ElementName, [min, max]: [number, number]): LookupNumber { + const lookupNumber: LookupNumber = { + name, + type: 'LookupNumber', + range: { min, max }, + }; + + return lookupNumber; +} + +/** + * Restricts a lookup output to a defined range. + * + * Each lookup score element has a minimum and maximum possible value. This function ensures + * the calculated score stays within those bounds: + * + * Example: + * - For "Review Impact": range is -400 to +400 + * - For "Number of Vouchers": range is 0 to +400 + * + * If a calculated score falls outside its range, it gets clamped to the nearest boundary. + * For instance, if a Review Impact score calculates to -500, it would be clamped to -400. + * + * @param lookup - The score element definition with its range constraints + * @param inputs - The raw input values to calculate scores from + * @returns The clamped score value + */ +function applyLookupNumber(lookup: LookupNumber, inputs: ElementInputs): { score: number } { + const input = inputs[lookup.name]; + const min = lookup.range?.min; + const max = lookup.range?.max; + + if (input === undefined) throw new Error(`Element value for ${lookup.name} is undefined`); + + if (typeof min !== 'number' || typeof max !== 'number') { + return { score: input }; + } + + if (input < min) { + return { score: min }; + } + if (input > max) { + return { score: max }; + } + + return { score: input }; +} diff --git a/ethos/packages/score/tsconfig.json b/ethos/packages/score/tsconfig.json new file mode 100644 index 0000000..cc8c840 --- /dev/null +++ b/ethos/packages/score/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + } +} diff --git a/ethos/patches/rc-util+5.44.3.patch b/ethos/patches/rc-util+5.44.3.patch new file mode 100644 index 0000000..3ba6faf --- /dev/null +++ b/ethos/patches/rc-util+5.44.3.patch @@ -0,0 +1,109 @@ +# Prevents an infinite render loop when performing a client-side navigation after rendering the root error boundary. +# https://github.com/react-component/util/issues/603 +diff --git a/node_modules/rc-util/es/Dom/dynamicCSS.js b/node_modules/rc-util/es/Dom/dynamicCSS.js +index 8ddec00..d6c390d 100644 +--- a/node_modules/rc-util/es/Dom/dynamicCSS.js ++++ b/node_modules/rc-util/es/Dom/dynamicCSS.js +@@ -31,7 +31,7 @@ function getOrder(prepend) { + * Find style which inject by rc-util + */ + function findStyles(container) { +- return Array.from((containerCache.get(container) || container).children).filter(function (node) { ++ return Array.from((containerCache.get(container) || container)?.children || []).filter(function (node) { + return node.tagName === 'STYLE'; + }); + } +@@ -56,7 +56,7 @@ export function injectCSS(css) { + } + styleNode.innerHTML = css; + var container = getContainer(option); +- var firstChild = container.firstChild; ++ var firstChild = container?.firstChild; + if (prepend) { + // If is queue `prepend`, it will prepend first style and then append rest style + if (isPrependQueue) { +@@ -77,9 +77,11 @@ export function injectCSS(css) { + } + + // Use `insertBefore` as `prepend` +- container.insertBefore(styleNode, firstChild); ++ if (firstChild) { ++ container.insertBefore(styleNode, firstChild); ++ } + } else { +- container.appendChild(styleNode); ++ container?.appendChild(styleNode); + } + return styleNode; + } +@@ -95,7 +97,7 @@ export function removeCSS(key) { + var existNode = findExistNode(key, option); + if (existNode) { + var container = getContainer(option); +- container.removeChild(existNode); ++ container?.removeChild(existNode); + } + } + +@@ -110,7 +112,7 @@ function syncRealContainer(container, option) { + var placeholderStyle = injectCSS('', option); + var parentNode = placeholderStyle.parentNode; + containerCache.set(container, parentNode); +- container.removeChild(placeholderStyle); ++ container?.removeChild(placeholderStyle); + } + } + +diff --git a/node_modules/rc-util/lib/Dom/dynamicCSS.js b/node_modules/rc-util/lib/Dom/dynamicCSS.js +index 95a5191..1303898 100644 +--- a/node_modules/rc-util/lib/Dom/dynamicCSS.js ++++ b/node_modules/rc-util/lib/Dom/dynamicCSS.js +@@ -41,7 +41,7 @@ function getOrder(prepend) { + * Find style which inject by rc-util + */ + function findStyles(container) { +- return Array.from((containerCache.get(container) || container).children).filter(function (node) { ++ return Array.from((containerCache.get(container) || container)?.children || []).filter(function (node) { + return node.tagName === 'STYLE'; + }); + } +@@ -66,7 +66,7 @@ function injectCSS(css) { + } + styleNode.innerHTML = css; + var container = getContainer(option); +- var firstChild = container.firstChild; ++ var firstChild = container?.firstChild; + if (prepend) { + // If is queue `prepend`, it will prepend first style and then append rest style + if (isPrependQueue) { +@@ -87,9 +87,11 @@ function injectCSS(css) { + } + + // Use `insertBefore` as `prepend` +- container.insertBefore(styleNode, firstChild); ++ if (firstChild) { ++ container.insertBefore(styleNode, firstChild); ++ } + } else { +- container.appendChild(styleNode); ++ container?.appendChild(styleNode); + } + return styleNode; + } +@@ -105,7 +107,7 @@ function removeCSS(key) { + var existNode = findExistNode(key, option); + if (existNode) { + var container = getContainer(option); +- container.removeChild(existNode); ++ container?.removeChild(existNode); + } + } + +@@ -120,7 +122,7 @@ function syncRealContainer(container, option) { + var placeholderStyle = injectCSS('', option); + var parentNode = placeholderStyle.parentNode; + containerCache.set(container, parentNode); +- container.removeChild(placeholderStyle); ++ container?.removeChild(placeholderStyle); + } + } diff --git a/ethos/scripts/cleanup.sh b/ethos/scripts/cleanup.sh new file mode 100755 index 0000000..54491fe --- /dev/null +++ b/ethos/scripts/cleanup.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -ex + +rm -rf .cache +rm -rf test-reports +find . -type d -name "node_modules" -exec rm -rf {} + +find . -type d -name "dist" -exec rm -rf {} + +rm -rf services/web/.next + +rm -rf packages/contracts/artifacts +rm -rf packages/contracts/cache +rm -rf packages/contracts/coverage +rm -rf packages/contracts/typechain-types +rm -rf packages/contracts/src/types +rm -f packages/contracts/coverage.json diff --git a/ethos/scripts/convert-x-username-to-id.ts b/ethos/scripts/convert-x-username-to-id.ts new file mode 100644 index 0000000..32a5a97 --- /dev/null +++ b/ethos/scripts/convert-x-username-to-id.ts @@ -0,0 +1,63 @@ +/* eslint-disable no-console */ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { chunk } from 'lodash-es'; +import { echoClient, setEchoConfig } from '@ethos/echo-client'; +import { echoUrlMap } from '@ethos/env'; + +// Fetch data from testnet as it has most of the users there. Eventually we can +// switch to prod API when testnet will be shut down. +setEchoConfig({ + baseUrl: echoUrlMap.testnet, +}); + +const INPUT_PATH = './.cache/usernames.txt'; +const OUTPUT_PATH = './.cache/username-to-id.csv'; +const CHUNK_SIZE = 20; + +async function run(): Promise { + if (!existsSync(INPUT_PATH)) { + console.error( + `❌ Input file "${INPUT_PATH}" not found. Create this file and put usernames, each on a separate line.\n`, + ); + console.log('ℹ️ Example:\n\nusername1\nusername2\nusername3\n'); + process.exit(1); + } + + const content = readFileSync(INPUT_PATH, 'utf-8'); + const usernames = content.split('\n').filter(Boolean); + + console.log(`\n⏳ Started processing ${usernames.length} usernames...\n`); + + const usernameToId = new Map(); + + for (const usernamesChunk of chunk(usernames, CHUNK_SIZE)) { + await Promise.all( + usernamesChunk.map(async (username) => { + try { + const twitterUser = await echoClient.twitter.user.get({ username }); + + if (twitterUser) { + usernameToId.set(username, twitterUser.id); + } else { + console.warn(`⚠️ User not found for "${username}"`); + } + } catch (err) { + console.warn(`⚠️ User not found for "${username}"`); + } + }), + ); + } + + const csvHeader = 'username,id'; + const csvContent = usernames.map((username) => `${username},${usernameToId.get(username) ?? ''}`); + + writeFileSync(OUTPUT_PATH, [csvHeader, ...csvContent].join('\n')); + + console.log(`\n✅ Successfully converted ${usernameToId.size}/${usernames.length} usernames.`); + console.log(`📄 Output saved to "${OUTPUT_PATH}"`); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/ethos/scripts/deploy-service-echo.sh b/ethos/scripts/deploy-service-echo.sh new file mode 100755 index 0000000..cf36214 --- /dev/null +++ b/ethos/scripts/deploy-service-echo.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -ex + +case "$1" in + dev|testnet|prod) + echo "Deploying to $1" + ;; + *) + echo "Unknown environment: $1" + echo "Usage: $0 " + exit 1 + ;; +esac + +PREFIX="" +if [ -n "$IS_MANUAL_DEPLOYMENT" ]; then + PREFIX="manual-" +fi +DEPLOYMENT_ID="${PREFIX}$GITHUB_RUN_ID-$GITHUB_REF_NAME-${GITHUB_SHA:0:8}" +echo "deploymentId: $DEPLOYMENT_ID" + +CONFIG="services/echo/fly.$1.toml" + +flyctl config validate \ + --access-token "$FLY_ACCESS_TOKEN_ECHO" \ + --config "$CONFIG" + +flyctl deploy \ + --access-token "$FLY_ACCESS_TOKEN_ECHO" \ + --config "$CONFIG" \ + --dockerfile services/echo/Dockerfile \ + --build-arg CI="$CI" \ + --build-arg GITHUB_RUN_NUMBER="$GITHUB_RUN_NUMBER" \ + --build-arg DEPLOYMENT_ID="$DEPLOYMENT_ID" \ + --ha=false \ + --remote-only diff --git a/ethos/scripts/deploy-service-emporos.sh b/ethos/scripts/deploy-service-emporos.sh new file mode 100755 index 0000000..5e39794 --- /dev/null +++ b/ethos/scripts/deploy-service-emporos.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -ex + +case "$1" in + dev|testnet|prod) + echo "Deploying to $1" + ;; + *) + echo "Unknown environment: $1" + echo "Usage: $0 " + exit 1 + ;; +esac + +EMPOROS_PUBLIC_VERSION="ethos-emporos@$1-${GITHUB_SHA:0:8}-$GITHUB_RUN_NUMBER" +CONFIG="services/emporos/deploy/fly.$1.toml" + +flyctl config validate \ + --access-token "$FLY_ACCESS_TOKEN_EMPOROS" \ + --config "$CONFIG" + +flyctl deploy \ + --access-token "$FLY_ACCESS_TOKEN_EMPOROS" \ + --config "$CONFIG" \ + --dockerfile services/emporos/Dockerfile \ + --build-arg CI="$CI" \ + --build-arg GITHUB_RUN_NUMBER="$GITHUB_RUN_NUMBER" \ + --build-arg EMPOROS_PUBLIC_VERSION="$EMPOROS_PUBLIC_VERSION" \ + --build-secret SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN" \ + --remote-only diff --git a/ethos/scripts/deploy-service-rabbitmq.sh b/ethos/scripts/deploy-service-rabbitmq.sh new file mode 100755 index 0000000..8f3c3ef --- /dev/null +++ b/ethos/scripts/deploy-service-rabbitmq.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -ex + +case "$1" in + dev|testnet|prod) + echo "Deploying to $1" + ;; + *) + echo "Unknown environment: $1" + echo "Usage: $0 " + exit 1 + ;; +esac + +CONFIG="services/rabbitmq/fly.$1.toml" + +flyctl config validate \ + --access-token "$FLY_ACCESS_TOKEN_RABBITMQ" \ + --config "$CONFIG" + +flyctl deploy \ + --access-token "$FLY_ACCESS_TOKEN_RABBITMQ" \ + --config "$CONFIG" \ + --dockerfile services/rabbitmq/Dockerfile \ + --build-secret RABBITMQ_USER="$RABBITMQ_USER" \ + --build-secret RABBITMQ_PASSWORD="$RABBITMQ_PASSWORD" \ + --build-secret RABBITMQ_MANAGER_USER="$RABBITMQ_MANAGER_USER" \ + --build-secret RABBITMQ_MANAGER_PASSWORD="$RABBITMQ_MANAGER_PASSWORD" \ + --ha=false \ + --remote-only diff --git a/ethos/scripts/deploy-service-web.sh b/ethos/scripts/deploy-service-web.sh new file mode 100755 index 0000000..de16995 --- /dev/null +++ b/ethos/scripts/deploy-service-web.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -ex + +case "$1" in + dev|testnet|prod) + echo "Deploying to $1" + ;; + *) + echo "Unknown environment: $1" + echo "Usage: $0 " + exit 1 + ;; +esac + +NEXT_PUBLIC_VERSION="ethos-web@$1-${GITHUB_SHA:0:8}-$GITHUB_RUN_NUMBER" +CONFIG="services/web/fly.$1.toml" + +flyctl config validate \ + --access-token "$FLY_ACCESS_TOKEN_WEB" \ + --config "$CONFIG" + +flyctl deploy \ + --depot="${ENABLE_DEPOT_BUILDER:=true}" \ + --access-token "$FLY_ACCESS_TOKEN_WEB" \ + --config "$CONFIG" \ + --dockerfile services/web/Dockerfile \ + --build-arg CI="$CI" \ + --build-arg GITHUB_RUN_NUMBER="$GITHUB_RUN_NUMBER" \ + --build-arg NEXT_PUBLIC_ETHOS_ENV="$1" \ + --build-arg NEXT_PUBLIC_VERSION="$NEXT_PUBLIC_VERSION" \ + --build-arg SENTRY_ENABLED="$SENTRY_ENABLED" \ + --build-secret SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN" \ + --remote-only diff --git a/ethos/scripts/dev/README.md b/ethos/scripts/dev/README.md new file mode 100644 index 0000000..1d0b2d7 --- /dev/null +++ b/ethos/scripts/dev/README.md @@ -0,0 +1,44 @@ +# Database Bootstrap Script + +Often it takes too long to process all existing blockchain events when starting the local Ethos development environment. This script bootstraps the local database with data from the remote development database. + +This script automates the process of exporting the development database, stopping local Docker containers, and bootstrapping a local database with the exported data. + +## Prerequisites + +Before running the script, ensure you have the following: + +1. Docker installed and running +2. OpenSSL + +## Configuration + +### SSL Certificates + +These are available in 1password. Place the following SSL certificates in the `~/certs/dev/` directory: + +- `client-cert.pem` +- `client-key.pem` +- `server-ca.pem` + +### Environment Variables + +Set the following environment variable: + +- `PG_ETHOS_DEV_PASSWORD`: PostgreSQL password (can be found in 1Password) + +If not set, the script will prompt you to enter it. + +## Usage + +Run the script from the root of the Ethos project. + +```bash +npm run db:bootstrap +``` + +To continue to run the local Ethos services, run: + +```bash +npm run start +``` diff --git a/ethos/scripts/dev/db.bootstrap.sh b/ethos/scripts/dev/db.bootstrap.sh new file mode 100755 index 0000000..4c91ada --- /dev/null +++ b/ethos/scripts/dev/db.bootstrap.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +set -e + +ethos_bootstrap_sql_path=".cache/ethos-bootstrap.sql" +emporos_bootstrap_sql_path=".cache/emporos-bootstrap.sql" + +mkdir -p .cache + +# Check for verbose flag +verbose=false +if [[ "$1" == "--verbose" ]]; then + verbose=true +fi + +# Function to print messages only in verbose mode +print_verbose() { + if $verbose; then + echo "$1" + fi +} + +# Set correct permissions for the private key +chmod 600 ~/certs/dev/client-key.pem +print_verbose "🔒 Set permissions for client-key.pem" + +# Convert PEM to P12 format +print_verbose "🔐 Converting PEM to P12 format..." +openssl pkcs12 -export -out ~/certs/dev/client-identity.p12 \ + -inkey ~/certs/dev/client-key.pem \ + -in ~/certs/dev/client-cert.pem \ + -passout pass: +print_verbose "🎉 P12 conversion complete" + +# Check if PG_ETHOS_DEV_PASSWORD is set, if not, prompt for it +if [ -z "$PG_ETHOS_DEV_PASSWORD" ]; then + echo "🔑 PG_ETHOS_DEV_PASSWORD environment variable is not set." + echo "🔒 Please enter your PostgreSQL password (you can find it in 1password):" + read -s PG_ETHOS_DEV_PASSWORD + export PG_ETHOS_DEV_PASSWORD + + if [ -z "$PG_ETHOS_DEV_PASSWORD" ]; then + echo "💥 Error: Password cannot be empty." + exit 1 + fi + print_verbose "🔑 Password set successfully" +else + print_verbose "🔑 PG_ETHOS_DEV_PASSWORD already set" +fi + +echo "📤 Exporting database..." + +# Run pg_dump using a PostgreSQL Docker container +docker run --rm \ + -v ~/certs/dev:/certs \ + -v "$(pwd)":/backup \ + -e PGPASSWORD="$PG_ETHOS_DEV_PASSWORD" \ + -e PGSSLMODE=require \ + -e PGSSLCERT=/certs/client-cert.pem \ + -e PGSSLKEY=/certs/client-key.pem \ + -e PGSSLROOTCERT=/certs/server-ca.pem \ + postgres:16-alpine \ + pg_dump \ + --host=34.31.16.222 \ + --username=ethos-dev \ + --dbname=ethos \ + --port=5432 \ + > $ethos_bootstrap_sql_path + +echo "🎈 Database echo exported to $ethos_bootstrap_sql_path" + +docker run --rm \ + -v ~/certs/dev:/certs \ + -v "$(pwd)":/backup \ + -e PGPASSWORD="$PG_ETHOS_DEV_PASSWORD" \ + -e PGSSLMODE=require \ + -e PGSSLCERT=/certs/client-cert.pem \ + -e PGSSLKEY=/certs/client-key.pem \ + -e PGSSLROOTCERT=/certs/server-ca.pem \ + postgres:16-alpine \ + pg_dump \ + --host=34.31.16.222 \ + --username=ethos-dev \ + --dbname=emporos \ + --port=5432 \ + > $emporos_bootstrap_sql_path + +echo "🎈 Database emporos exported to $emporos_bootstrap_sql_path" + +echo "🛑 Stopping Docker containers..." +docker compose down + +echo "🔄 Starting Docker containers..." + +# Check if ethos-bootstrap.sql exists +if [ ! -f $ethos_bootstrap_sql_path ]; then + echo "⚠️ $ethos_bootstrap_sql_path not found. Unable to bootstrap database." + exit 1 +fi + +# Check if emporos-bootstrap.sql exists +if [ ! -f $emporos_bootstrap_sql_path ]; then + echo "⚠️ $emporos_bootstrap_sql_path not found. Unable to bootstrap database." + exit 1 +fi + +# Retry mechanism +max_attempts=30 +attempt=0 +attempt_connection() { + if $verbose; then + docker exec -i ethos-db-1 psql ethos --username=postgres -c "SELECT 1" + else + docker exec -i ethos-db-1 psql ethos --username=postgres -c "SELECT 1" >/dev/null 2>&1 + fi +} + +if $verbose; then + docker compose up -d +else + docker compose up -d >/dev/null 2>&1 +fi + +echo "⏳ Waiting for PostgreSQL to become available..." + +until attempt_connection; do + attempt=$((attempt + 1)) + if [ $attempt -eq $max_attempts ]; then + echo "💥 Failed to connect to PostgreSQL after $max_attempts attempts. Exiting." + exit 1 + fi + print_verbose "🔄 Attempt $attempt failed. Retrying in 1 second..." + sleep 1 +done + +# Execute bootstrap SQL +if $verbose; then + docker exec -i ethos-db-1 psql ethos --username=postgres < $ethos_bootstrap_sql_path +else + docker exec -i ethos-db-1 psql ethos --username=postgres < $ethos_bootstrap_sql_path >/dev/null 2>&1 +fi + +if $verbose; then + docker exec -i ethos-db-1 psql --username=postgres -c "CREATE DATABASE emporos;" + docker exec -i ethos-db-1 psql emporos --username=postgres < $emporos_bootstrap_sql_path +else + docker exec -i ethos-db-1 psql --username=postgres -c "CREATE DATABASE emporos;" >/dev/null 2>&1 + docker exec -i ethos-db-1 psql emporos --username=postgres < $emporos_bootstrap_sql_path >/dev/null 2>&1 +fi + +SQL_TRUNCATE_USER_FCM_TOKENS="TRUNCATE TABLE user_fcm_tokens RESTART IDENTITY;" + +# Execute SQL to drop all user FCM tokens to ensure that we are not sending push +# notifications to everyone who set up notifications on dev. Otherwise, everyone +# who run this script, has a copy of tokens locally and whenever there's a new +# activity, every locally running instance of echo sends a push notification +# spamming the receiver with duplicates. +if $verbose; then + docker exec -i ethos-db-1 psql ethos --username=postgres -c "$SQL_TRUNCATE_USER_FCM_TOKENS" +else + docker exec -i ethos-db-1 psql ethos --username=postgres -c "$SQL_TRUNCATE_USER_FCM_TOKENS" >/dev/null 2>&1 +fi + +echo "🎉 Database bootstrap completed successfully." diff --git a/ethos/scripts/dev/migrate/format-json-files.ts b/ethos/scripts/dev/migrate/format-json-files.ts new file mode 100644 index 0000000..656fc8a --- /dev/null +++ b/ethos/scripts/dev/migrate/format-json-files.ts @@ -0,0 +1,151 @@ +/* eslint-disable no-console */ +import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; + +const INPUT_PATH = './.cache/migrate-input'; +const OUTPUT_PATH = './.cache/migrate-output'; + +const IDENTIFIER_TO_PROFILE_ID_FILE = 'identifier-to-profile-id'; +const USERKEY_BY_PROFILE_ID_FILE = 'userkey-by-profile-id'; +const REVIEWS_FILE = 'reviews'; +const VOUCHES_FILE = 'vouches'; + +console.log('⏳ Parsing the following JSONL files:\n'); +console.log( + [IDENTIFIER_TO_PROFILE_ID_FILE, USERKEY_BY_PROFILE_ID_FILE, REVIEWS_FILE, VOUCHES_FILE] + .map((file) => `${INPUT_PATH}/${file}.jsonl`) + .join('\n'), +); + +// Make sure the output directory exists +if (!existsSync(OUTPUT_PATH)) { + mkdirSync(OUTPUT_PATH, { recursive: true }); +} + +// Parse JSONL file into an array of parsed objects +function formatJsonl(filename: string): T[] { + const content = readFileSync(`${INPUT_PATH}/${filename}.jsonl`, 'utf8'); + const lines = content.trim().split('\n'); + + return lines.map((line) => JSON.parse(line)); +} + +const activityByProfileId = new Map< + number, + Array<{ type: 'review' | 'vouch'; id: number; ts: number }> +>(); + +function addActivity( + profileId: number, + activity: 'review' | 'vouch', + id: number, + ts: number, +): void { + const profileActivity = activityByProfileId.get(profileId) ?? []; + + profileActivity.push({ type: activity, id, ts }); + activityByProfileId.set(profileId, profileActivity); +} + +// Normalized address/username to profile ID +const userIdentifierByProfileIdMap = new Map(); +const identifierToProfile = formatJsonl<{ profileId: number; identifier: string }>( + IDENTIFIER_TO_PROFILE_ID_FILE, +); + +for (const { identifier, profileId } of identifierToProfile) { + userIdentifierByProfileIdMap.set(identifier.toLocaleLowerCase(), profileId); +} + +writeFileSync( + `${OUTPUT_PATH}/identifier-to-profile-id.json`, + JSON.stringify(Object.fromEntries(userIdentifierByProfileIdMap.entries())), +); + +// Normalize profileId to userkeys +const userkeysByProfileIdMap = new Map(); +const userkeyByProfileId = formatJsonl<{ profileId: number; userkey: string }>( + USERKEY_BY_PROFILE_ID_FILE, +); + +for (const { profileId, userkey } of userkeyByProfileId) { + const existingUserkeys = userkeysByProfileIdMap.get(profileId) ?? []; + + userkeysByProfileIdMap.set(profileId, [...existingUserkeys, userkey]); +} + +writeFileSync( + `${OUTPUT_PATH}/userkeys-by-profile-id.json`, + JSON.stringify(Object.fromEntries(userkeysByProfileIdMap.entries())), +); + +// Normalize reviews +type Review = { + id: number; + authorProfileId: number; + subjectUserkey: string; + score: number; + comment: string; + metadata: string; + createdAt: string; +}; + +const reviewsMap = new Map(); +const reviews = formatJsonl(REVIEWS_FILE); + +for (const review of reviews) { + reviewsMap.set(review.id, review); + addActivity(review.authorProfileId, 'review', review.id, new Date(review.createdAt).valueOf()); +} + +writeFileSync( + `${OUTPUT_PATH}/reviews.json`, + JSON.stringify(Object.fromEntries(reviewsMap.entries())), +); + +// Normalize vouches +type Vouch = { + id: number; + authorProfileId: number; + subjectProfileId: number; + deposited: number; + comment: string; + metadata: string; + vouchedAt: string; +}; + +const vouchesMap = new Map(); +const vouches = formatJsonl(VOUCHES_FILE); + +for (const vouch of vouches) { + vouchesMap.set(vouch.id, vouch); + addActivity(vouch.authorProfileId, 'vouch', vouch.id, new Date(vouch.vouchedAt).valueOf()); +} + +writeFileSync( + `${OUTPUT_PATH}/vouches.json`, + JSON.stringify(Object.fromEntries(vouchesMap.entries())), +); + +// Store the activity by profile ID +for (const activityMap of activityByProfileId.keys()) { + const activity = activityByProfileId.get(activityMap); + + // Type guard + if (!activity) { + throw new Error(`Activity not found for profile ID: ${activityMap}`); + } + + const sortedActivities = [...activity].sort((a, b) => b.ts - a.ts); + + activityByProfileId.set(activityMap, sortedActivities); +} + +writeFileSync( + `${OUTPUT_PATH}/activity-by-profile-id.json`, + JSON.stringify(Object.fromEntries(activityByProfileId.entries())), +); + +const outputFiles = readdirSync(OUTPUT_PATH); + +console.log('\n✅ Successfully formatted the following files:\n'); +console.log(outputFiles.map((file) => `${OUTPUT_PATH}/${file}`).join('\n')); diff --git a/ethos/scripts/dev/migrate/snapshot-data.sql b/ethos/scripts/dev/migrate/snapshot-data.sql new file mode 100644 index 0000000..5dc390b --- /dev/null +++ b/ethos/scripts/dev/migrate/snapshot-data.sql @@ -0,0 +1,49 @@ +-- address/username --> profileId +-- identifier-to-profile-id.jsonl +SELECT "address" AS identifier, "profileId" FROM "profile_addresses" +UNION +SELECT "username" AS identifier, "profileId" FROM "attestations" a +LEFT JOIN "twitter_profiles_cache" tpc ON a.account = tpc.id +WHERE "archived" = FALSE; + +-- profileId --> address/attestation userkey +-- userkey-by-profile-id.jsonl +WITH result AS ( + SELECT "profileId", 'address:' || address AS userkey, NULL AS "updatedAt" + FROM "profile_addresses" + UNION + SELECT "profileId", 'service:' || service || ':' || account AS userkey, "updatedAt" + FROM "attestations" + WHERE archived = FALSE + ORDER BY "updatedAt" DESC +) +SELECT "profileId", userkey FROM result; + +-- reviews +-- reviews.jsonl +SELECT + id, + "authorProfileId", + CASE + WHEN service != '' THEN 'service:x.com:' || account + ELSE 'address:' || subject + END "subjectUserkey", + score, + comment, + metadata, + "createdAt" +FROM reviews +WHERE archived = FALSE; + +-- vouches +-- vouches.jsonl +SELECT + id, + "authorProfileId", + "subjectProfileId", + deposited, + comment, + metadata, + "vouchedAt" +FROM vouches +WHERE archived = FALSE; diff --git a/ethos/scripts/dev/send-test-notification.ts b/ethos/scripts/dev/send-test-notification.ts new file mode 100755 index 0000000..f8e7dda --- /dev/null +++ b/ethos/scripts/dev/send-test-notification.ts @@ -0,0 +1,43 @@ +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { + type NotificationPayload, + sendNotification, + sendNotificationByProfileId, +} from '../../services/echo/src/common/net/firebase-admin'; + +async function main(): Promise { + const argv = yargs(hideBin(process.argv)) + .option('profileId', { + type: 'number', + description: 'The user profile id', + }) + .option('token', { + type: 'string', + description: 'The user FCM token', + }) + .check((argv) => { + if (!argv.profileId && !argv.token) { + throw new Error('You must provide either --profileId or --token'); + } + + return true; + }) + .parseSync(); + + const payload: NotificationPayload = { + title: 'Review received', + body: 'You have received a positive review from doganyilmaz623.eth.fuel', + icon: 'https://i.postimg.cc/PJXXV8Bf/EDITER.png', + badge: 'https://i.postimg.cc/PJXXV8Bf/EDITER.png', + url: 'https://sepolia.ethos.network/activity/review/4448/rc', + }; + + if (argv.profileId) { + await sendNotificationByProfileId(argv.profileId, payload, true); + } else if (argv.token) { + await sendNotification(argv.token, payload, -1); + } +} + +void main(); diff --git a/ethos/scripts/key-detector.ts b/ethos/scripts/key-detector.ts new file mode 100644 index 0000000..fb1406f --- /dev/null +++ b/ethos/scripts/key-detector.ts @@ -0,0 +1,70 @@ +import { exec } from 'node:child_process'; +import { exit } from 'node:process'; +import { Wallet } from 'ethers'; + +const ignoreList = [ + 'c8f300cdd121db675d2c35da636dd69e12072fed0f5daabbe5ac66834259fc0c', // used in EthosAttestation.test.ts - it's just a hash + 'd144e3dcb38b873fbcf648a8b4b7eda64cb5b4b92655b47a46459550927c1ad6', // used in EthosAttestation.test.ts - it's just a hash + 'a4b65882aa82e4aad2a45c0971ae64a003b236f50a5d734b746e6a63d0ee1a1f', // used in EthosAttestation.test.ts - it's just a hash + '442aefcb2e3264611614cafe1cc3b7e5ead53cf1e4e0e2c411eec1c9e1fd6293', // used in EthosAttestation.test.ts - it's just a hash + '360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', // used in upgradeable contract tests +]; + +function verifyPrivateKey(privateKey: string): boolean { + if (ignoreList.includes(privateKey)) { + return false; + } + try { + // eslint-disable-next-line no-new + new Wallet(privateKey); + + return true; + } catch (e) { + return false; + } +} + +async function getGitGrepResults(): Promise { + return await new Promise((resolve, reject) => { + exec('git grep -Ehow "[0-9a-f]{64}"', (error, stdout, stderr) => { + if (error) { + reject(error); + + return; + } + if (stderr) { + reject(new Error(stderr)); + + return; + } + const results = stdout.trim().split('\n'); + resolve(results); + }); + }); +} + +async function main(): Promise { + // Get unique results + const results = Array.from(new Set(await getGitGrepResults())); + let found = 0; + + for (const result of results) { + const isValid = verifyPrivateKey(result); + + if (!isValid) { + continue; + } + console.warn(`⚠️ Found valid private key: ${result}`); + found++; + } + + if (found > 0) { + console.warn(`☠️ Found ${found} valid private keys`); + exit(1); + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/ethos/scripts/ops/bulk_invite.py b/ethos/scripts/ops/bulk_invite.py new file mode 100644 index 0000000..9c5e6a4 --- /dev/null +++ b/ethos/scripts/ops/bulk_invite.py @@ -0,0 +1,44 @@ +import subprocess +import os +import argparse + +def read_addresses(file_path): + with open(file_path, 'r') as f: + return [line.strip() for line in f if line.strip()] + +def send_invites(addresses, dry_run=False): + cmd = ['ETHOS_CLI_ENV=testnet', 'ethos', 'invite', 'bulk', '--wait'] + #cmd = ['ethos', 'invite', 'bulk', '--wait'] + for addr in addresses: + cmd.extend(['-r', addr]) + + cmd_str = ' '.join(cmd) + + if dry_run: + print("Dry run: Command that would be executed:") + print(cmd_str) + print(f"This would send invites to {len(addresses)} addresses.") + else: + try: + subprocess.run(cmd_str, shell=True, check=True) + print(f"Successfully sent invites to {len(addresses)} addresses.") + except subprocess.CalledProcessError as e: + print(f"Error sending invites: {e}") + +def main(): + parser = argparse.ArgumentParser(description="Send bulk invites using Ethos CLI") + parser.add_argument("--dry-run", action="store_true", help="Perform a dry run without sending invites") + args = parser.parse_args() + + input_file = 'extracted_wallets.txt't + + addresses = read_addresses(input_file) + batch_size = 25 + + for i in range(0, len(addresses), batch_size): + batch = addresses[i:i+batch_size] + print(f"Processing batch {i//batch_size + 1} ({len(batch)} addresses)") + send_invites(batch, dry_run=args.dry_run) + +if __name__ == "__main__": + main() diff --git a/ethos/scripts/ops/redeploy-all-contracts.sh b/ethos/scripts/ops/redeploy-all-contracts.sh new file mode 100644 index 0000000..503dc7b --- /dev/null +++ b/ethos/scripts/ops/redeploy-all-contracts.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -e + +# Get the absolute path to the contracts directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONTRACTS_DIR="$(cd "$SCRIPT_DIR/../../packages/contracts" && pwd)" + +# Function to deploy a contract +deploy_contract() { + local contract=$1 + echo "🚀 Deploying $contract..." + npm run deploy --prefix "$CONTRACTS_DIR" -- --action deployProxy --contract "$contract" --environment dev +} + +# Deploy contracts in order +echo "📦 Deploying contracts in dependency order..." + +# No Dependencies +deploy_contract "contractAddressManager" + +# Depends on contractAddressManager +deploy_contract "interactionControl" +deploy_contract "signatureVerifier" + +# Depends on signatureVerifier +deploy_contract "attestation" +deploy_contract "discussion" +deploy_contract "profile" +deploy_contract "review" +deploy_contract "vote" +deploy_contract "vouch" +deploy_contract "slashPenalty" +deploy_contract "vaultManager" + +echo "✅ All contracts deployed successfully!" + +# Update contract address management +echo "🔄 Updating contract address management..." +npm run deploy --prefix "$CONTRACTS_DIR" -- --action updateAddresses --contract contractAddressManager --environment local + +echo "🎉 Deployment and address management update complete!" diff --git a/ethos/scripts/scale-fly-builder.sh b/ethos/scripts/scale-fly-builder.sh new file mode 100755 index 0000000..c490629 --- /dev/null +++ b/ethos/scripts/scale-fly-builder.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -e + +# Fly builders might change between deployments. I couldn't find a way to enforce +# fly.io to set 8GB for our builder when a new one is created. This script can +# dynamically determine the builder machine id and add more memory. +# Make sure you are logged in with fly CLI. If not, run "fly auth login". + +ORG=$1 + +if [ -z "$ORG" ]; then + echo "❌ Missing organization" + echo "Usage: $0 " + exit 1 +fi + +# Get the builder name +BUILDER_NAME=$(fly apps list -o "$ORG" -j | jq -r '.[] | select(.Status=="deployed") | select(.ID | startswith("fly-builder-")).ID') + +if [ -z "$BUILDER_NAME" ]; then + echo "❌ No builder found" + exit 1 +fi + +# Get the machine ID +MACHINE_ID=$(fly machines list -a "$BUILDER_NAME" -j | jq -r '.[0].id') + +if [ -z "$MACHINE_ID" ]; then + echo "❌ No machine found" + exit 1 +fi + +# Scale the machine +fly machine update "$MACHINE_ID" -a "$BUILDER_NAME" --vm-memory "8GB" diff --git a/ethos/scripts/start-dev.ts b/ethos/scripts/start-dev.ts new file mode 100644 index 0000000..9030c4b --- /dev/null +++ b/ethos/scripts/start-dev.ts @@ -0,0 +1,26 @@ +import { readFileSync } from 'node:fs'; +import concurrently from 'concurrently'; +import { globSync } from 'glob'; + +const packagesToWatch = []; + +const packagePaths = globSync('packages/*/package.json'); + +for (const path of packagePaths) { + const pkg = JSON.parse(readFileSync(path, 'utf-8')); + + if (pkg.scripts?.watch) { + packagesToWatch.push(pkg.name); + } +} + +concurrently([ + ...packagesToWatch.map((pkg) => ({ + name: pkg, + command: `npm run watch -w ${pkg}`, + prefixColor: 'cyan', + })), + { name: 'echo', command: 'npm run start:echo', prefixColor: 'yellow' }, + { name: 'web', command: 'npm run start:web', prefixColor: 'magenta' }, + { name: 'emporos', command: 'npm run start:markets', prefixColor: 'green' }, +]); diff --git a/ethos/scripts/start.sh b/ethos/scripts/start.sh new file mode 100755 index 0000000..c8f9c84 --- /dev/null +++ b/ethos/scripts/start.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -ex + +# Run all the services defined in docker-compose.yml +docker compose up -d + +# Build packages +npm run build:packages + +# Start the server and watch for packages updates +npx tsx scripts/start-dev.ts diff --git a/ethos/scripts/validate-locally.sh b/ethos/scripts/validate-locally.sh new file mode 100755 index 0000000..898bdaa --- /dev/null +++ b/ethos/scripts/validate-locally.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -e + +# Detect if there are any secrets in the codebase +npm run validate:secrets +echo "✅ No secrets detected in the codebase." + +# Validate that we can build all packages +npm run build:packages +echo "✅ All packages are built successfully." + +npm run prebuild -w services/emporos + +# Validate that the code is compliant with the linting rules +npm run lint +echo -e "\n✅ Code is compliant with the linting rules." + +# Run typecheck +npm run typecheck +echo "✅ Typecheck passed." + +# Run Vitest tests +npm run test:ci +echo -e "\n✅ Vitest tests passed.\n" + +# Run contract tests if there are changes in the contracts directory +BRANCH="main" +DIRECTORY="packages/contracts" + +# Ensure the index is up-to-date with the latest changes +git update-index -q --refresh + +# Check for changes in the directory compared to the specified branch +CHANGED_FILES=$(git diff-index --name-only $BRANCH -- $DIRECTORY) + +if [ -z "$CHANGED_FILES" ]; then + echo "⏩ No changes in $DIRECTORY. Skipping running contract tests." +else + echo "🔍 Changes detected in $DIRECTORY. Running contract tests..." + + npm -w packages/contracts run test:contracts + echo -e "\n✅ Contract tests passed." +fi + +echo -e "\n🎉 All checkes passed successfully." diff --git a/ethos/scripts/validate-package.ts b/ethos/scripts/validate-package.ts new file mode 100644 index 0000000..08140fb --- /dev/null +++ b/ethos/scripts/validate-package.ts @@ -0,0 +1,24 @@ +import { readFileSync } from 'node:fs'; +import pc from 'picocolors'; + +const packageJson: { dependencies: Record } = JSON.parse( + readFileSync('./package.json', 'utf8'), +); + +const prodDependencies = Object.keys(packageJson.dependencies ?? {}); + +if (prodDependencies.length > 0) { + console.error( + pc.red('\n❌ It looks like you’ve installed the prod dependency in the root package.json.'), + ); + console.error('👉 Please move it to the correct workspace where it’s used.'); + console.error('\nFirst, uninstall it from the root by running the following command:'); + console.error(pc.blue(`npm rm ${prodDependencies[0]}`)); + console.error('\nThen, install it in the correct workspace:'); + console.error(pc.blue(`npm i -w ${prodDependencies[0]}`)); + + process.exit(1); +} else { + // eslint-disable-next-line no-console + console.log('\n✅ Root package.json is valid.\n'); +} diff --git a/ethos/services/echo/.cursorrules b/ethos/services/echo/.cursorrules new file mode 100644 index 0000000..81b3eaf --- /dev/null +++ b/ethos/services/echo/.cursorrules @@ -0,0 +1,91 @@ + +# Reference: https://trust-ethos.atlassian.net/wiki/spaces/ethoscore/pages/39321696/Frontend+code+convention +Prefer function syntax over arrow function syntax. +BAD: `const someFunction = () => {...}` +GOOD: `function someFunction () {...}` + +In order to keep the code clean & readable, do not use random or shortened names for functions, variables, components and basically everything. +Examples: +BAD: `const valToBodyFN = () => {...}` +GOOD: `const formatFormValuesToRequestBody = () => {...}` + + +Try to write code that is easily understandable by others, but if the code still comes out looking complex and hard to understand, leave a comment explaining what different parts of the code do. You do not have to break it down like for a 2 year old, just tell us what is happening and where. JSDoc-style comment above the function or a variable can also help as it would be shown while hovering over it in IDE. +GOOD example: +``` +/** + * This function performs complex data formatting for ... + */ +function someComplexFunction (usersData: Type) { + // format users data into the format we need + const formattedUsersData = userData.reduce(formattedData, user => { + // check if user is admin because we need to use different format for admin users + if (user.type === 'admin') { + // perform some stuff... + }, + [] + } + // some other stuff... +} +``` + +Break down code into smaller pieces : +* If there is a logic inside a component that can be exported into helper/util/hook +* If there is a part of the component that can be exported into separate smaller components +* If there are types/constants/mocks, they can and should be moved to separate files outside of the component + + +use memoization techniques where appropriate: +Example 1: +BAD: +``` +..... +const someFunction = () => { + //heavyCalculation +} +...... +``` + +GOOD: +``` +..... +const someFunction = useCallback(() => { + //heavyCalculation +}, [dependency]) +...... +``` + +Example 2: +BAD: +``` +..... +const selectOptions = users.map(user => ({user.address})) +...... +``` + +GOOD: +``` +..... +const selectOptions = useMemo( + () => users.map(user => ({user.address})), + [users] +) +...... + +And use them instead of adding extra state where it is possible +BAD: +``` +const [selectOptions, setSelectOptions] = useState([]) + +useEffect(() => { + setSelectOptions(users.map(user => ({user.address}))) +}, [users]) +``` + +GOOD: +``` +const selectOptions = useMemo( + () => users.map(user => ({user.address})), + [users] +) +``` diff --git a/ethos/services/echo/.env.sample b/ethos/services/echo/.env.sample new file mode 100644 index 0000000..8a259fa --- /dev/null +++ b/ethos/services/echo/.env.sample @@ -0,0 +1,24 @@ +ALCHEMY_API_KEY= +ALCHEMY_MAINNET_API_URL=https://base-mainnet.g.alchemy.com/v2/ +ALCHEMY_TESTNET_API_URL=https://base-sepolia.g.alchemy.com/v2/ +AMQP_URL=amqp://guest:guest@localhost +DATABASE_URL=postgres://postgres:postgres@localhost:5432/ethos +TIMESCALE_DB_URL=postgres://postgres:postgres@localhost:5433/ethos +FIREBASE_ADMIN_CREDENTIALS= +LOG_LEVEL=info # Available options: trace, debug, info, warn, error, fatal or silent to supress all logs +MORALIS_API_KEY= +PRIVY_APP_ID=cm28tigsl01nrx1wh7ek7gd4w +PRIVY_APP_SECRET= +PRIVY_APP_PUBLIC_KEY="-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEz77qUskTFTp4RQEYcIHHaaS4o8y5bXoPV0k/LI8xShdYHTnmeekigzFeaWUG6Kfe91oW/08J9GSRdq609iDadw== +-----END PUBLIC KEY-----" +SIGNER_ACCOUNT_PRIVATE_KEY= +STATSIG_SECRET_KEY= +TWITTER_BEARER_TOKEN= +TWITTER_CLIENT_ID= +TWITTER_CLIENT_SECRET= +TWITTER_SESSION_SECRET="keyboard cat" + +# Optional: Set to a number between 0-100 to randomly throw Service errors on requests. +# Useful for testing error handling in the frontend. +# CHAOS_RATE=10 diff --git a/ethos/services/echo/Dockerfile b/ethos/services/echo/Dockerfile new file mode 100644 index 0000000..caa236c --- /dev/null +++ b/ethos/services/echo/Dockerfile @@ -0,0 +1,46 @@ +ARG NODE_VERSION=22 + +FROM node:${NODE_VERSION} AS base + +WORKDIR /app + +ARG CI +ARG GITHUB_RUN_NUMBER + +ENV CI=${CI} +ENV GITHUB_RUN_NUMBER=${GITHUB_RUN_NUMBER} + +ENV NODE_ENV="production" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install node modules +COPY . . +RUN NODE_ENV=development npm ci + +# Build application +RUN npm run build + +# Remove development dependencies +RUN npm prune --omit dev --workspaces + + +# Final stage for app image +FROM node:${NODE_VERSION}-alpine AS prod + +ARG DEPLOYMENT_ID +ENV DEPLOYMENT_ID=${DEPLOYMENT_ID} +RUN echo "Deployment ID: ${DEPLOYMENT_ID}" + +# Install OpenSSL for SSL connection to DB +RUN apk add --no-cache openssl + +# Copy built application +COPY --from=build /app /app + +WORKDIR /app + +# Start the server by default, this can be overwritten at runtime +EXPOSE 8080 +CMD [ "npm", "-w", "services/echo", "run", "start:production" ] diff --git a/ethos/services/echo/Dockerfile.dockerignore b/ethos/services/echo/Dockerfile.dockerignore new file mode 100644 index 0000000..e1628a6 --- /dev/null +++ b/ethos/services/echo/Dockerfile.dockerignore @@ -0,0 +1,14 @@ +.git +.github +.husky +.vitest +.vscode +scripts +standalone +test-reports + +# Ignore all services except echo +services/* +!services/echo + +services/echo/fly.*.toml diff --git a/ethos/services/echo/README.md b/ethos/services/echo/README.md new file mode 100644 index 0000000..3fe9956 --- /dev/null +++ b/ethos/services/echo/README.md @@ -0,0 +1,115 @@ +# Ethos Echo service + +The service to listen to events from Ethos smart contracts and replicating them +in Postgres. + +## Development + +### DB migrations + +We use Prisma for two different databases: + +- Postgres is our main database. (schema is in `services/echo/prisma/postgres/schema.prisma`) +- TimescaleDb for time-series data. (schema is in `services/echo/prisma/timescaledb/schema.prisma`) + +To perform a db migration, first modify the appropriate schema file. +Once you modified the schema file, you need to create a migration. To do this: + +1. First, navigate to the Echo service directory `cd services/echo`. +1. Run `prisma:migrate: -- --name `. This will create the + migration file and run it for you. + +### Access Postgres locally + +You can use any GUI or CLI to access the RDS. My recommendation is +[Postico2](https://eggerapps.at/postico2/) if you are on MacOS. The username and +password are `postgres`. The full connection URL is `postgres://postgres:postgres@localhost:5432/ethos`. + +## Architecture + +```mermaid +sequenceDiagram + Note over Client: Sends HTTP request + Client->>Express endpoint: HTTP request + + Express endpoint->>Route: HTTP request + + Note over Service: Have no idea about upper layers (Route) + Note over Prisma: Have no idea about upper layers (Route, Service) + + Note over Route: Extracts data from request and passes to the service + Route->>Service: JS object + + Note over Service: Validates input, executes business-logic + Note over Service: May query DB for any data + Service->>Prisma: Query + + Note over Prisma: Knows how to extract requested data from DB + Prisma->>Service: Query result + + Note over Service: May interact with Blockchain + Service->>Blockchain: Request + Blockchain->>Service: Response + + Note over Service: May interact with other APIs + Service->>1st/3rd-party APIs: Request + 1st/3rd-party APIs->>Service: Response + + Service->>Route: JS object + Note over Route: Knows how to send this\ndata in response + + Route->>Client: HTTP response +``` + +## DB Connectivity + +The DB is a Postgres instance managed by GCP Cloud SQL. When not running locally, connections are secured with SSL client certificates. + +### Dev DB + +In order to connect to the Dev environment DB from a local machine, you will need to use the client certificate and keys, and the server CA certificate. They are stored in 1password. + +- `client-cert.pem` +- `client-key.pem` +- `server-ca.pem` + +### Examples + +You can configure your local postgres client accordingly; this is an example for CLI `psql`: + +```bash +psql "sslmode=verify-ca sslrootcert=server-ca.pem sslcert=client-cert.pem sslkey=client-key.pem hostaddr=34.31.16.222 port=5432 user=ethos-echo dbname=ethos" +``` + +This is an example of a Prisma connection string: + +```bash +DATABASE_URL="postgresql://ethos-echo:@34.31.16.222:5432/ethos?sslmode=require&connection_limit=40&sslcert=/var/run/shm/ssl/ca.pem&sslidentity=/var/run/shm/ssl/client-identity.p12" +``` + +However, note that prisma expect the client identity to be a single file in PKCS12 format, not the client certificate and key separately. To generate the PKCS12 file, you can use the following command: + +```shell +openssl pkcs12 -export -out client-identity.p12 -inkey client-key.pem -in client-cert.pem -passout pass: +``` + +### Deploying DB Connection Secrets + +The client SSL certificates are deployed as secrets in fly.io. +Because they contain newlines, it's recommended to use `flyctl secrets set SECRET=- + +```shell +flyctl secrets set --app trust-ethos-echo-dev --stage DB_SERVER_CA=- < server-ca.pem +flyctl secrets set --app trust-ethos-echo-dev --stage DB_SSL_CERT=- < client-cert.pem +flyctl secrets set --app trust-ethos-echo-dev --stage DB_SSL_KEY=- < client-key.pem +``` + +If you really want to simulate what is happening on production locally, you can export the following environment variables and run the `npm run prestart:production` option. + +```shell +export DB_SERVER_CA=$(echo -e "$(cat server-ca.pem)") +export DB_SSL_CERT=$(echo -e "$(cat client-cert.pem)") +export DB_SSL_KEY=$(echo -e "$(cat client-key.pem)") +npm run prestart:production +``` diff --git a/ethos/services/echo/fly.dev.toml b/ethos/services/echo/fly.dev.toml new file mode 100644 index 0000000..e596880 --- /dev/null +++ b/ethos/services/echo/fly.dev.toml @@ -0,0 +1,44 @@ +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. + +app = 'trust-ethos-echo-dev' +primary_region = 'dfw' +kill_signal = 'SIGINT' +kill_timeout = '5s' + +[build] + +[deploy] + strategy = 'canary' + +[env] + ETHOS_ENV = 'dev' + NODE_ENV = 'production' + PORT_ECHO = '8080' + PRIVY_APP_ID = 'cm28tigsl01nrx1wh7ek7gd4w' + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = "off" + auto_start_machines = true + min_machines_running = 1 + processes = ['app'] + [http_service.concurrency] + type = "requests" + soft_limit = 500 + + [[http_service.checks]] + interval = '30s' + timeout = '5s' + grace_period = '10s' + method = 'GET' + path = '/healthcheck' + +[metrics] + port = 9091 + path = "/metrics" + +[[vm]] + cpu_kind = 'shared' + cpus = 8 + memory_mb = 4096 diff --git a/ethos/services/echo/fly.prod.toml b/ethos/services/echo/fly.prod.toml new file mode 100644 index 0000000..946ad4b --- /dev/null +++ b/ethos/services/echo/fly.prod.toml @@ -0,0 +1,44 @@ +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. + +app = 'trust-ethos-echo-prod' +primary_region = 'dfw' +kill_signal = 'SIGINT' +kill_timeout = '5s' + +[build] + +[deploy] + strategy = 'canary' + +[env] + ETHOS_ENV = 'prod' + NODE_ENV = 'production' + PORT_ECHO = '8080' + PRIVY_APP_ID = 'cm5l76en107pt1lpl2ve2ocfy' + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = "off" + auto_start_machines = true + min_machines_running = 1 + processes = ['app'] + [http_service.concurrency] + type = "requests" + soft_limit = 500 + + [[http_service.checks]] + interval = '60s' + timeout = '25s' + grace_period = '20s' + method = 'GET' + path = '/healthcheck' + +[metrics] + port = 9091 + path = "/metrics" + +[[vm]] + cpu_kind = 'performance' + cpus = 8 + memory_mb = 16384 diff --git a/ethos/services/echo/fly.testnet.toml b/ethos/services/echo/fly.testnet.toml new file mode 100644 index 0000000..6b0219f --- /dev/null +++ b/ethos/services/echo/fly.testnet.toml @@ -0,0 +1,44 @@ +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. + +app = 'trust-ethos-echo-testnet' +primary_region = 'dfw' +kill_signal = 'SIGINT' +kill_timeout = '5s' + +[build] + +[deploy] + strategy = 'canary' + +[env] + ETHOS_ENV = 'testnet' + NODE_ENV = 'production' + PORT_ECHO = '8080' + PRIVY_APP_ID = 'cm33meogs04dfgb0rxfhrbb68' + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = "off" + auto_start_machines = true + min_machines_running = 1 + processes = ['app'] + [http_service.concurrency] + type = "requests" + soft_limit = 500 + + [[http_service.checks]] + interval = '60s' + timeout = '25s' + grace_period = '20s' + method = 'GET' + path = '/healthcheck' + +[metrics] + port = 9091 + path = "/metrics" + +[[vm]] + cpu_kind = 'performance' + cpus = 8 + memory_mb = 16384 diff --git a/ethos/services/echo/package.json b/ethos/services/echo/package.json new file mode 100644 index 0000000..140fdd7 --- /dev/null +++ b/ethos/services/echo/package.json @@ -0,0 +1,81 @@ +{ + "name": "@ethos/echo", + "version": "0.0.1", + "private": true, + "description": "Ethos Echo service", + "type": "module", + "scripts": { + "build": "tsc -b", + "prisma:generate": "npm run prisma:generate:postgres && npm run prisma:generate:timescale", + "prisma:generate:postgres": "prisma generate --schema prisma/postgres/schema.prisma", + "prisma:generate:timescale": "prisma generate --schema prisma/timescale/schema.prisma", + "prisma:migrate:dev": "npm run prisma:migrate:postgres && npm run prisma:migrate:timescale", + "prisma:migrate:postgres": "prisma migrate dev --schema prisma/postgres/schema.prisma", + "prisma:migrate:timescale": "prisma migrate dev --schema prisma/timescale/schema.prisma", + "prisma:deploy": "npm run prisma:deploy:postgres && npm run prisma:deploy:timescale", + "prisma:deploy:postgres": "prisma migrate deploy --schema prisma/postgres/schema.prisma", + "prisma:deploy:timescale": "prisma migrate deploy --schema prisma/timescale/schema.prisma", + "prebuild": "npm run prisma:generate", + "prestart": "npm run prisma:migrate:dev", + "prestart:production": "NODE_ENV=production node -r source-map-support/register dist/bootstrap.db.ssl.js && npm run prisma:deploy", + "start": "tsx watch --clear-screen=false src/bootstrap.all.ts", + "start:production": "NODE_ENV=production node -r source-map-support/register dist/bootstrap.all.js" + }, + "dependencies": { + "@ethos/blockchain-manager": "^1.0.0", + "@ethos/config": "^1.0.0", + "@ethos/contracts": "^1.0.0", + "@ethos/domain": "^1.0.0", + "@ethos/env": "^1.0.0", + "@ethos/helpers": "^1.0.0", + "@ethos/logger": "^1.1.0", + "@ethos/score": "^1.0.0", + "@prisma/client": "^6.2.1", + "@privy-io/server-auth": "^1.17.1", + "@sentry/node": "^8.38.0", + "@sentry/profiling-node": "^8.30.0", + "@superfaceai/passport-twitter-oauth2": "^1.2.4", + "@the-convocation/twitter-scraper": "^0.14.1", + "amqp-connection-manager": "^4.1.14", + "amqplib": "^0.10.5", + "compression": "^1.7.5", + "connect-redis": "^8.0.1", + "cookie": "^1.0.2", + "cors": "^2.8.5", + "cron": "^3.2.1", + "date-fns": "^4.1.0", + "ethers": "^6.13.4", + "express": "^4.21.1", + "express-session": "^1.18.1", + "firebase-admin": "^13.0.2", + "helmet": "^8.0.0", + "ioredis": "^5.4.1", + "jwt-decode": "^4.0.0", + "lodash-es": "^4.17.21", + "lru-cache": "^11.0.1", + "on-finished": "^2.4.1", + "parse-duration": "^1.1.0", + "passport": "^0.7.0", + "pino-sentry-transport": "^1.3.0", + "prom-client": "^15.1.3", + "redlock2": "^5.0.0-beta.3", + "source-map-support": "^0.5.21", + "statsig-node": "^5.27.1", + "viem": "^2.21.51", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/amqplib": "^0.10.6", + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/express-session": "^1.18.1", + "@types/on-finished": "^2.3.4", + "@types/passport": "^1.0.17", + "@types/passport-oauth2": "^1.4.17", + "pino": "^9.5.0", + "prisma": "^6.2.1", + "tsx": "^4.19.2", + "type-fest": "^4.26.1" + } +} diff --git a/ethos/services/echo/prisma/postgres/migrations/20240913012101_testnet_launch/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20240913012101_testnet_launch/migration.sql new file mode 100644 index 0000000..a9eaad5 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20240913012101_testnet_launch/migration.sql @@ -0,0 +1,415 @@ +-- CreateEnum +CREATE TYPE "InvitationStatus" AS ENUM ('PENDING', 'ACCEPTED', 'DECLINED'); + +-- CreateTable +CREATE TABLE "profiles" ( + "id" INTEGER NOT NULL, + "archived" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + "invitesSent" TEXT[], + "invitesAcceptedIds" INTEGER[], + "invitesAvailable" INTEGER NOT NULL, + "invitedBy" INTEGER NOT NULL, + + CONSTRAINT "profiles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "profile_addresses" ( + "id" SERIAL NOT NULL, + "profileId" INTEGER NOT NULL, + "address" TEXT NOT NULL, + + CONSTRAINT "profile_addresses_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "invitations" ( + "id" SERIAL NOT NULL, + "senderProfileId" INTEGER NOT NULL, + "recipient" VARCHAR(42) NOT NULL, + "sentAt" TIMESTAMP(3) NOT NULL, + "acceptedProfileId" INTEGER, + "status" "InvitationStatus" NOT NULL DEFAULT 'PENDING', + "statusUpdatedAt" TIMESTAMP(3), + + CONSTRAINT "invitations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "reviews" ( + "id" INTEGER NOT NULL, + "archived" BOOLEAN NOT NULL, + "author" VARCHAR(42) NOT NULL, + "authorProfileId" INTEGER NOT NULL, + "subject" VARCHAR(42) NOT NULL, + "score" INTEGER NOT NULL, + "comment" TEXT NOT NULL, + "metadata" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + "account" TEXT NOT NULL, + "service" TEXT NOT NULL, + + CONSTRAINT "reviews_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "vouches" ( + "id" INTEGER NOT NULL, + "archived" BOOLEAN NOT NULL, + "unhealthy" BOOLEAN NOT NULL, + "authorProfileId" INTEGER NOT NULL, + "stakeToken" VARCHAR(42) NOT NULL, + "subjectProfileId" INTEGER NOT NULL, + "deposited" DECIMAL(78,0) NOT NULL, + "staked" DECIMAL(78,0) NOT NULL, + "balance" DECIMAL(78,0) NOT NULL, + "withdrawn" DECIMAL(78,0) NOT NULL DEFAULT 0, + "mutualVouchId" INTEGER, + "comment" TEXT NOT NULL, + "metadata" TEXT NOT NULL, + "vouchedAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + "unvouchedAt" TIMESTAMP(3), + "unhealthyAt" TIMESTAMP(3), + + CONSTRAINT "vouches_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "replies" ( + "id" INTEGER NOT NULL, + "parentId" INTEGER NOT NULL, + "targetContract" VARCHAR(42) NOT NULL, + "authorProfileId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + "content" TEXT NOT NULL, + "metadata" TEXT NOT NULL, + "parentIsOriginalComment" BOOLEAN NOT NULL, + + CONSTRAINT "replies_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "votes" ( + "id" INTEGER NOT NULL, + "isUpvote" BOOLEAN NOT NULL, + "isArchived" BOOLEAN NOT NULL, + "voter" INTEGER NOT NULL, + "targetContract" VARCHAR(42) NOT NULL, + "targetId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "votes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "attestations" ( + "id" INTEGER NOT NULL, + "hash" TEXT NOT NULL, + "archived" BOOLEAN NOT NULL, + "profileId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + "account" TEXT NOT NULL, + "service" TEXT NOT NULL, + + CONSTRAINT "attestations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "blockchain_events" ( + "id" SERIAL NOT NULL, + "contract" TEXT NOT NULL, + "logData" JSONB NOT NULL, + "blockNumber" INTEGER NOT NULL, + "blockIndex" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + "txHash" VARCHAR(66) NOT NULL, + "processed" BOOLEAN NOT NULL DEFAULT false, + "attempts" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "blockchain_events_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "profile_events" ( + "eventId" INTEGER NOT NULL, + "profileId" INTEGER NOT NULL +); + +-- CreateTable +CREATE TABLE "review_events" ( + "eventId" INTEGER NOT NULL, + "reviewId" INTEGER NOT NULL +); + +-- CreateTable +CREATE TABLE "vouch_events" ( + "eventId" INTEGER NOT NULL, + "vouchId" INTEGER NOT NULL +); + +-- CreateTable +CREATE TABLE "reply_events" ( + "eventId" INTEGER NOT NULL, + "replyId" INTEGER NOT NULL +); + +-- CreateTable +CREATE TABLE "attestation_events" ( + "eventId" INTEGER NOT NULL, + "attestationId" INTEGER NOT NULL +); + +-- CreateTable +CREATE TABLE "vote_events" ( + "eventId" INTEGER NOT NULL, + "voteId" INTEGER NOT NULL +); + +-- CreateTable +CREATE TABLE "score_history" ( + "id" SERIAL NOT NULL, + "target" TEXT NOT NULL, + "score" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "elements" JSONB NOT NULL, + "errors" TEXT[], + "dirty" BOOLEAN NOT NULL, + + CONSTRAINT "score_history_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ens_cache" ( + "id" SERIAL NOT NULL, + "address" TEXT NOT NULL, + "ensName" TEXT, + "avatarUrl" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ens_cache_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "twitter_profiles_cache" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "name" TEXT NOT NULL, + "avatar" TEXT, + "biography" TEXT, + "website" TEXT, + "followersCount" INTEGER, + "joinedAt" TIMESTAMP(3), + "isBlueVerified" BOOLEAN, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "twitter_profiles_cache_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "escrow" ( + "id" SERIAL NOT NULL, + "profileId" INTEGER NOT NULL, + "token" TEXT NOT NULL, + "balance" TEXT NOT NULL, + "lifetime" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "escrow_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "escrow_events" ( + "id" SERIAL NOT NULL, + "eventId" INTEGER NOT NULL, + "profileId" INTEGER NOT NULL, + "token" TEXT NOT NULL, + "amount" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "escrow_events_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "profiles_archived_idx" ON "profiles"("archived"); + +-- CreateIndex +CREATE UNIQUE INDEX "profile_addresses_address_key" ON "profile_addresses"("address"); + +-- CreateIndex +CREATE INDEX "profile_addresses_profileId_idx" ON "profile_addresses"("profileId"); + +-- CreateIndex +CREATE INDEX "invitations_senderProfileId_idx" ON "invitations"("senderProfileId"); + +-- CreateIndex +CREATE INDEX "invitations_acceptedProfileId_idx" ON "invitations"("acceptedProfileId"); + +-- CreateIndex +CREATE INDEX "invitations_recipient_idx" ON "invitations"("recipient"); + +-- CreateIndex +CREATE INDEX "reviews_service_account_idx" ON "reviews"("service", "account"); + +-- CreateIndex +CREATE INDEX "reviews_archived_idx" ON "reviews"("archived"); + +-- CreateIndex +CREATE INDEX "reviews_author_idx" ON "reviews"("author"); + +-- CreateIndex +CREATE INDEX "reviews_subject_score_idx" ON "reviews"("subject", "score"); + +-- CreateIndex +CREATE UNIQUE INDEX "vouches_mutualVouchId_key" ON "vouches"("mutualVouchId"); + +-- CreateIndex +CREATE INDEX "vouches_archived_idx" ON "vouches"("archived"); + +-- CreateIndex +CREATE INDEX "vouches_authorProfileId_idx" ON "vouches"("authorProfileId"); + +-- CreateIndex +CREATE INDEX "vouches_subjectProfileId_idx" ON "vouches"("subjectProfileId"); + +-- CreateIndex +CREATE INDEX "replies_targetContract_parentId_idx" ON "replies"("targetContract", "parentId"); + +-- CreateIndex +CREATE INDEX "votes_voter_idx" ON "votes"("voter"); + +-- CreateIndex +CREATE INDEX "votes_targetContract_targetId_isArchived_idx" ON "votes"("targetContract", "targetId", "isArchived"); + +-- CreateIndex +CREATE INDEX "votes_targetId_isUpvote_idx" ON "votes"("targetId", "isUpvote"); + +-- CreateIndex +CREATE UNIQUE INDEX "attestations_hash_key" ON "attestations"("hash"); + +-- CreateIndex +CREATE INDEX "attestations_profileId_idx" ON "attestations"("profileId"); + +-- CreateIndex +CREATE INDEX "blockchain_events_contract_idx" ON "blockchain_events"("contract"); + +-- CreateIndex +CREATE UNIQUE INDEX "blockchain_events_blockNumber_blockIndex_txHash_key" ON "blockchain_events"("blockNumber", "blockIndex", "txHash"); + +-- CreateIndex +CREATE UNIQUE INDEX "profile_events_eventId_profileId_key" ON "profile_events"("eventId", "profileId"); + +-- CreateIndex +CREATE UNIQUE INDEX "review_events_eventId_reviewId_key" ON "review_events"("eventId", "reviewId"); + +-- CreateIndex +CREATE UNIQUE INDEX "vouch_events_eventId_vouchId_key" ON "vouch_events"("eventId", "vouchId"); + +-- CreateIndex +CREATE UNIQUE INDEX "reply_events_eventId_replyId_key" ON "reply_events"("eventId", "replyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "attestation_events_eventId_attestationId_key" ON "attestation_events"("eventId", "attestationId"); + +-- CreateIndex +CREATE UNIQUE INDEX "vote_events_eventId_voteId_key" ON "vote_events"("eventId", "voteId"); + +-- CreateIndex +CREATE INDEX "score_history_target_dirty_idx" ON "score_history"("target", "dirty"); + +-- CreateIndex +CREATE INDEX "score_history_score_idx" ON "score_history"("score"); + +-- CreateIndex +CREATE INDEX "score_history_createdAt_idx" ON "score_history"("createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "ens_cache_address_key" ON "ens_cache"("address"); + +-- CreateIndex +CREATE INDEX "ens_cache_address_idx" ON "ens_cache"("address"); + +-- CreateIndex +CREATE INDEX "ens_cache_ensName_idx" ON "ens_cache"("ensName"); + +-- CreateIndex +CREATE INDEX "twitter_profiles_cache_username_idx" ON "twitter_profiles_cache"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "escrow_profileId_token_key" ON "escrow"("profileId", "token"); + +-- CreateIndex +CREATE INDEX "escrow_events_profileId_token_idx" ON "escrow_events"("profileId", "token"); + +-- AddForeignKey +ALTER TABLE "profile_addresses" ADD CONSTRAINT "profile_addresses_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "invitations" ADD CONSTRAINT "invitations_senderProfileId_fkey" FOREIGN KEY ("senderProfileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "invitations" ADD CONSTRAINT "invitations_acceptedProfileId_fkey" FOREIGN KEY ("acceptedProfileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "vouches" ADD CONSTRAINT "vouches_subjectProfileId_fkey" FOREIGN KEY ("subjectProfileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "vouches" ADD CONSTRAINT "vouches_mutualVouchId_fkey" FOREIGN KEY ("mutualVouchId") REFERENCES "vouches"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "attestations" ADD CONSTRAINT "attestations_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profile_events" ADD CONSTRAINT "profile_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "blockchain_events"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "profile_events" ADD CONSTRAINT "profile_events_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "review_events" ADD CONSTRAINT "review_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "blockchain_events"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "review_events" ADD CONSTRAINT "review_events_reviewId_fkey" FOREIGN KEY ("reviewId") REFERENCES "reviews"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "vouch_events" ADD CONSTRAINT "vouch_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "blockchain_events"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "vouch_events" ADD CONSTRAINT "vouch_events_vouchId_fkey" FOREIGN KEY ("vouchId") REFERENCES "vouches"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "reply_events" ADD CONSTRAINT "reply_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "blockchain_events"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "reply_events" ADD CONSTRAINT "reply_events_replyId_fkey" FOREIGN KEY ("replyId") REFERENCES "replies"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "attestation_events" ADD CONSTRAINT "attestation_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "blockchain_events"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "attestation_events" ADD CONSTRAINT "attestation_events_attestationId_fkey" FOREIGN KEY ("attestationId") REFERENCES "attestations"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "vote_events" ADD CONSTRAINT "vote_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "blockchain_events"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "vote_events" ADD CONSTRAINT "vote_events_voteId_fkey" FOREIGN KEY ("voteId") REFERENCES "votes"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "escrow" ADD CONSTRAINT "escrow_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "escrow_events" ADD CONSTRAINT "escrow_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "blockchain_events"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "escrow_events" ADD CONSTRAINT "escrow_events_profileId_token_fkey" FOREIGN KEY ("profileId", "token") REFERENCES "escrow"("profileId", "token") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/ethos/services/echo/prisma/postgres/migrations/20240916193824_twitter_cache_index_update_at/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20240916193824_twitter_cache_index_update_at/migration.sql new file mode 100644 index 0000000..4f3e007 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20240916193824_twitter_cache_index_update_at/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "twitter_profiles_cache_updatedAt_idx" ON "twitter_profiles_cache"("updatedAt"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20240926182421_add_contract_column/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20240926182421_add_contract_column/migration.sql new file mode 100644 index 0000000..376baa8 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20240926182421_add_contract_column/migration.sql @@ -0,0 +1,14 @@ +-- CreateEnum +CREATE TYPE "Contract" AS ENUM ('profile', 'vouch', 'review', 'attestation', 'discussion'); + +-- AlterTable +ALTER TABLE "replies" ADD COLUMN "contract" "Contract"; + +-- AlterTable +ALTER TABLE "votes" ADD COLUMN "contract" "Contract"; + +-- CreateIndex +CREATE INDEX "replies_contract_parentId_idx" ON "replies"("contract", "parentId"); + +-- CreateIndex +CREATE INDEX "votes_contract_targetId_idx" ON "votes"("contract", "targetId"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20240926182500_update_votes_replies/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20240926182500_update_votes_replies/migration.sql new file mode 100644 index 0000000..63ab787 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20240926182500_update_votes_replies/migration.sql @@ -0,0 +1,39 @@ +UPDATE public.votes +SET contract = 'profile' +WHERE "targetContract" IN ('0xF570158319a25a1bf0F1e4Dd38bCEC573bd39229', '0x06abcDFDFC12a2Df54BB9AEF6191c30d60d7dA97'); + +UPDATE public.votes +SET contract = 'review' +WHERE "targetContract" IN ('0xD3cf1Ff9C0a4BBfCfC958E264dFe8486fF1018cc', '0x92FF66D134892A54f887A2E4be14599364963220'); + +UPDATE public.votes +SET contract = 'attestation' +WHERE "targetContract" IN ('0x83523cE61C93064FB48d2A9b20556b15ab2b11Dc', '0x25BE4c848690c587Bd4A9ad3124Bc03837fBd6c0'); + +UPDATE public.votes +SET contract = 'vouch' +WHERE "targetContract" IN ('0xf7C8a5e41705fF781Bdd4F49c493bF6B28cc33a8', '0x093eCeb66acD5d61b5098D3Bc0C72541b5519011'); + +UPDATE public.votes +SET contract = 'discussion' +WHERE "targetContract" IN ('0xA949a3003466b5cdC399481DA8385BD67C8a78B8', '0xc1d75421d22F7228bAf65c6caf952F75e9e043a6'); + +UPDATE public.replies +SET contract = 'profile' +WHERE "targetContract" IN ('0xF570158319a25a1bf0F1e4Dd38bCEC573bd39229', '0x06abcDFDFC12a2Df54BB9AEF6191c30d60d7dA97'); + +UPDATE public.replies +SET contract = 'review' +WHERE "targetContract" IN ('0xD3cf1Ff9C0a4BBfCfC958E264dFe8486fF1018cc', '0x92FF66D134892A54f887A2E4be14599364963220'); + +UPDATE public.replies +SET contract = 'attestation' +WHERE "targetContract" IN ('0x83523cE61C93064FB48d2A9b20556b15ab2b11Dc', '0x25BE4c848690c587Bd4A9ad3124Bc03837fBd6c0'); + +UPDATE public.replies +SET contract = 'vouch' +WHERE "targetContract" IN ('0xf7C8a5e41705fF781Bdd4F49c493bF6B28cc33a8', '0x093eCeb66acD5d61b5098D3Bc0C72541b5519011'); + +UPDATE public.replies +SET contract = 'discussion' +WHERE "targetContract" IN ('0xA949a3003466b5cdC399481DA8385BD67C8a78B8', '0xc1d75421d22F7228bAf65c6caf952F75e9e043a6'); diff --git a/ethos/services/echo/prisma/postgres/migrations/20240926182501_actvities_view/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20240926182501_actvities_view/migration.sql new file mode 100644 index 0000000..14f0966 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20240926182501_actvities_view/migration.sql @@ -0,0 +1,179 @@ +CREATE VIEW "activities" AS +WITH RECURSIVE + PARTIAL_TABLE ( + ID, + PARENTID, + "authorProfileId", + "rootParentId", + CONTRACT + ) AS ( + SELECT + REPLIES.ID, + REPLIES."parentId", + REPLIES."authorProfileId", + REPLIES."parentId" AS "rootParentId", + REPLIES.CONTRACT + FROM + REPLIES + UNION + SELECT + CHILD.ID, + CHILD."parentId", + CHILD."authorProfileId", + PARTIAL_TABLE."rootParentId" AS "rootParentId", + PARTIAL_TABLE.CONTRACT + FROM + REPLIES AS CHILD + JOIN PARTIAL_TABLE ON PARTIAL_TABLE.ID = CHILD."parentId" + WHERE + CHILD.CONTRACT = 'discussion' + ), + TOTAL_REPLIES AS ( + SELECT + COUNT(*) AS REPLIES, + "rootParentId", + CONTRACT + FROM + PARTIAL_TABLE + WHERE + CONTRACT != 'discussion' + GROUP BY + "rootParentId", + CONTRACT + ), + TOTAL_VOTES AS ( + SELECT + CONTRACT, + V."targetId", + SUM( + CASE + WHEN V."isUpvote" THEN 1 + ELSE 0 + END + ) UPVOTES, + SUM( + CASE + WHEN V."isUpvote" THEN 0 + ELSE 1 + END + ) DOWNVOTES + FROM + PUBLIC.VOTES V + WHERE + V."isArchived" = FALSE + GROUP BY + CONTRACT, + V."targetId" + ) +SELECT + CASE + WHEN UVE."eventId" > VE."eventId" THEN 'unvouch' + ELSE 'vouch' + END ACTIVITY_TYPE, + 'create' ACTIVITY_ACTION, + V.ID, + V."authorProfileId", + V."subjectProfileId" || '' "subject", + CASE + WHEN UVE."eventId" > VE."eventId" THEN GREATEST(VE."eventId", UVE."eventId") + ELSE LEAST(VE."eventId", UVE."eventId") + END "eventId", + V.COMMENT TITLE, + V.ARCHIVED, + CASE + WHEN UVE."eventId" > VE."eventId" THEN V."unvouchedAt" + ELSE V."vouchedAt" + END "createdAt", + V."updatedAt", + V.METADATA, + COALESCE(TV.UPVOTES, 0) UPVOTES, + COALESCE(TV.DOWNVOTES, 0) DOWNVOTES, + COALESCE(TR.REPLIES, 0) REPLIES +FROM + PUBLIC.VOUCHES V + JOIN PUBLIC.VOUCH_EVENTS VE ON VE."vouchId" = V.ID + LEFT JOIN PUBLIC.VOUCH_EVENTS UVE ON UVE."vouchId" = V.ID + AND UVE."eventId" <> VE."eventId" + LEFT JOIN TOTAL_VOTES TV ON TV."targetId" = V.ID + AND TV.CONTRACT = 'vouch' + LEFT JOIN TOTAL_REPLIES TR ON TR."rootParentId" = V.ID + AND TR.CONTRACT = 'vouch' +UNION ALL +SELECT + 'review' ACTIVITY_TYPE, + 'create' ACTIVITY_ACTION, + R.ID, + R."authorProfileId", + CASE + WHEN R.SERVICE = '' + AND R.ACCOUNT = '' THEN R.SUBJECT + ELSE CONCAT(R.SERVICE, ':', R.ACCOUNT) + END "subject", + RE."eventId", + R.COMMENT TITLE, + R.ARCHIVED, + R."createdAt", + R."updatedAt", + R.METADATA, + COALESCE(TV.UPVOTES, 0) UPVOTES, + COALESCE(TV.DOWNVOTES, 0) DOWNVOTES, + COALESCE(TR.REPLIES, 0) REPLIES +FROM + PUBLIC.REVIEWS R + JOIN PUBLIC.REVIEW_EVENTS RE ON RE."reviewId" = R.ID + LEFT JOIN TOTAL_VOTES TV ON TV."targetId" = R.ID + AND TV.CONTRACT = 'review' + LEFT JOIN TOTAL_REPLIES TR ON TR."rootParentId" = R.ID + AND TR.CONTRACT = 'review' +UNION ALL +SELECT + 'attestation' ACTIVITY_TYPE, + 'create' ACTIVITY_ACTION, + A.ID, + A."profileId" "authorProfileId", + CONCAT(A.SERVICE, ':', A.ACCOUNT) "subject", + AE."eventId", + NULL TITLE, + A.ARCHIVED, + A."createdAt", + A."updatedAt", + NULL METADATA, + COALESCE(TV.UPVOTES, 0) UPVOTES, + COALESCE(TV.DOWNVOTES, 0) DOWNVOTES, + COALESCE(TR.REPLIES, 0) REPLIES +FROM + PUBLIC.ATTESTATIONS A + JOIN PUBLIC.ATTESTATION_EVENTS AE ON AE."attestationId" = A.ID + LEFT JOIN TOTAL_VOTES TV ON TV."targetId" = A.ID + AND TV.CONTRACT = 'attestation' + LEFT JOIN TOTAL_REPLIES TR ON TR."rootParentId" = A.ID + AND TR.CONTRACT = 'attestation' +UNION ALL +( + SELECT DISTINCT + ON (P.ID) 'invitation-accepted' ACTIVITY_TYPE, + 'create' ACTIVITY_ACTION, + I.ID, + I."senderProfileId" "authorProfileId", + P.ID || '' "subject", + PE."eventId", + NULL TITLE, + P.ARCHIVED, + I."sentAt" "createdAt", + I."statusUpdatedAt" "updatedAt", + NULL METADATA, + COALESCE(TV.UPVOTES, 0) UPVOTES, + COALESCE(TV.DOWNVOTES, 0) DOWNVOTES, + COALESCE(TR.REPLIES, 0) REPLIES + FROM + PUBLIC.INVITATIONS I + JOIN PUBLIC.PROFILES P ON P.ID = I."acceptedProfileId" + JOIN PUBLIC.PROFILE_EVENTS PE ON PE."profileId" = P.ID + LEFT JOIN TOTAL_VOTES TV ON TV."targetId" = P.ID + AND TV.CONTRACT = 'profile' + LEFT JOIN TOTAL_REPLIES TR ON TR."rootParentId" = P.ID + AND TR.CONTRACT = 'profile' + ORDER BY + P.ID, + "eventId" +) diff --git a/ethos/services/echo/prisma/postgres/migrations/20241002211541_reputation_markets/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241002211541_reputation_markets/migration.sql new file mode 100644 index 0000000..02d81c5 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241002211541_reputation_markets/migration.sql @@ -0,0 +1,69 @@ +-- CreateEnum +CREATE TYPE "MarketVoteEventType" AS ENUM ('BUY', 'SELL'); + +-- CreateTable +CREATE TABLE "markets" ( + "profileId" INTEGER NOT NULL, + "creatorAddress" VARCHAR(42) NOT NULL, + "positivePrice" TEXT NOT NULL, + "negativePrice" TEXT NOT NULL, + "trustVotes" INTEGER NOT NULL, + "distrustVotes" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "markets_pkey" PRIMARY KEY ("profileId") +); + +-- CreateTable +CREATE TABLE "market_events" ( + "eventId" INTEGER NOT NULL, + "marketProfileId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "positivePrice" TEXT NOT NULL, + "negativePrice" TEXT NOT NULL, + "deltaVoteTrust" INTEGER NOT NULL, + "deltaVoteDistrust" INTEGER NOT NULL, + "deltaPositivePrice" TEXT NOT NULL, + "deltaNegativePrice" TEXT NOT NULL, + "blockNumber" INTEGER NOT NULL +); + +-- CreateTable +CREATE TABLE "market_vote_events" ( + "eventId" INTEGER NOT NULL, + "type" "MarketVoteEventType" NOT NULL, + "actorAddress" VARCHAR(42) NOT NULL, + "marketProfileId" INTEGER NOT NULL, + "isPositive" BOOLEAN NOT NULL, + "amount" INTEGER NOT NULL, + "funds" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE INDEX "market_events_marketProfileId_createdAt_idx" ON "market_events"("marketProfileId", "createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "market_events_eventId_marketProfileId_key" ON "market_events"("eventId", "marketProfileId"); + +-- CreateIndex +CREATE INDEX "market_vote_events_marketProfileId_createdAt_idx" ON "market_vote_events"("marketProfileId", "createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "market_vote_events_eventId_marketProfileId_key" ON "market_vote_events"("eventId", "marketProfileId"); + +-- AddForeignKey +ALTER TABLE "markets" ADD CONSTRAINT "markets_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "profiles"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "market_events" ADD CONSTRAINT "market_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "blockchain_events"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "market_events" ADD CONSTRAINT "market_events_marketProfileId_fkey" FOREIGN KEY ("marketProfileId") REFERENCES "markets"("profileId") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "market_vote_events" ADD CONSTRAINT "market_vote_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "blockchain_events"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "market_vote_events" ADD CONSTRAINT "market_vote_events_marketProfileId_fkey" FOREIGN KEY ("marketProfileId") REFERENCES "markets"("profileId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241006193909_moralis_cache/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241006193909_moralis_cache/migration.sql new file mode 100644 index 0000000..46d290f --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241006193909_moralis_cache/migration.sql @@ -0,0 +1,52 @@ +-- MANUAL EDIT +-- adds support for case insensitive text fields +CREATE EXTENSION IF NOT EXISTS citext; + +-- CreateTable +CREATE TABLE "address_history_cache" ( + "address" CITEXT NOT NULL, + "firstTransaction" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "address_history_cache_pkey" PRIMARY KEY ("address") +); + +-- CreateTable +CREATE TABLE "transaction_history_cache" ( + "fromAddress" CITEXT NOT NULL, + "toAddress" CITEXT NOT NULL, + "hash" CITEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "value" TEXT NOT NULL, + "blockNumber" INTEGER NOT NULL, + "blockTimestamp" TIMESTAMP(3) NOT NULL, + "fromAddressLabel" TEXT, + "fromAddressLogo" TEXT, + "toAddressLabel" TEXT, + "toAddressLogo" TEXT, + "category" TEXT, + "summary" TEXT +); + +-- CreateIndex +CREATE INDEX "address_history_cache_address_updatedAt_idx" ON "address_history_cache"("address", "updatedAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "transaction_history_cache_hash_key" ON "transaction_history_cache"("hash"); + +-- CreateIndex +CREATE INDEX "transaction_history_cache_updatedAt_fromAddress_idx" ON "transaction_history_cache"("updatedAt", "fromAddress"); + +-- CreateIndex +CREATE INDEX "transaction_history_cache_updatedAt_toAddress_idx" ON "transaction_history_cache"("updatedAt", "toAddress"); + +-- CreateIndex +CREATE INDEX "transaction_history_cache_fromAddress_idx" ON "transaction_history_cache"("fromAddress"); + +-- CreateIndex +CREATE INDEX "transaction_history_cache_toAddress_idx" ON "transaction_history_cache"("toAddress"); + +-- CreateIndex +CREATE INDEX "transaction_history_cache_blockNumber_idx" ON "transaction_history_cache"("blockNumber"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241007132127_market_reset/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241007132127_market_reset/migration.sql new file mode 100644 index 0000000..d29e4d0 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241007132127_market_reset/migration.sql @@ -0,0 +1,4 @@ +-- This migration is used to reset the market data for all profiles, given the new market contracts. +DELETE FROM market_vote_events; +DELETE FROM market_events; +DELETE FROM markets; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241008221038_market_reset/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241008221038_market_reset/migration.sql new file mode 100644 index 0000000..d29e4d0 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241008221038_market_reset/migration.sql @@ -0,0 +1,4 @@ +-- This migration is used to reset the market data for all profiles, given the new market contracts. +DELETE FROM market_vote_events; +DELETE FROM market_events; +DELETE FROM markets; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241009114357_targets_names_scores_views/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241009114357_targets_names_scores_views/migration.sql new file mode 100644 index 0000000..f206896 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241009114357_targets_names_scores_views/migration.sql @@ -0,0 +1,256 @@ +---- targets +CREATE VIEW "targets" AS +SELECT + ID, + 'profileId:' || ID::TEXT TARGET +FROM + PROFILES P +UNION +SELECT + P.ID, + 'address:' || LOWER(PA.ADDRESS) TARGET +FROM + PROFILES P + JOIN PROFILE_ADDRESSES PA ON PA."profileId" = P.ID +UNION +SELECT + P.ID, + 'service:' || A.SERVICE || ':' || A.ACCOUNT::TEXT TARGET +FROM + PROFILES P + JOIN ATTESTATIONS A ON A."profileId" = P.ID; + +---- names +CREATE VIEW "names" AS +WITH + ENS_NAMES AS ( + SELECT + PA."profileId" ID, + 'address:' || LOWER(PA.ADDRESS) TARGET, + FIRST_VALUE(ENS."ensName") OVER ( + PARTITION BY + PA."profileId" + ORDER BY + ENS."createdAt" DESC + ) NAME, + FIRST_VALUE(ENS."avatarUrl") OVER ( + PARTITION BY + PA."profileId" + ORDER BY + ENS."createdAt" DESC + ) AVATAR, + NULL USERNAME, + NULL DESCRIPTION + FROM + PROFILE_ADDRESSES PA + LEFT JOIN ENS_CACHE ENS ON LOWER(PA.ADDRESS) = LOWER(ENS.ADDRESS) + UNION + SELECT -- when there are ens entries for non-users + PA."profileId" ID, + 'address:' || LOWER(ENS.ADDRESS) TARGET, + ENS."ensName" NAME, + ENS."avatarUrl" AVATAR, + NULL USERNAME, + NULL DESCRIPTION + FROM + PROFILE_ADDRESSES PA + RIGHT JOIN ENS_CACHE ENS ON LOWER(PA.ADDRESS) = LOWER(ENS.ADDRESS) + WHERE + PA.ID IS NULL + ), + TWITTER_NAMES AS ( + SELECT + A."profileId" ID, + 'service:x.com:' || A.ACCOUNT::TEXT TARGET, + FIRST_VALUE(TPC.NAME) OVER ( + PARTITION BY + A."profileId" + ORDER BY + TPC."createdAt" DESC + ) NAME, + FIRST_VALUE(TPC.AVATAR) OVER ( + PARTITION BY + A."profileId" + ORDER BY + TPC."createdAt" DESC + ) AVATAR, + FIRST_VALUE(TPC.USERNAME) OVER ( + PARTITION BY + A."profileId" + ORDER BY + TPC."createdAt" DESC + ) USERNAME, + FIRST_VALUE(TPC.BIOGRAPHY) OVER ( + PARTITION BY + A."profileId" + ORDER BY + TPC."createdAt" DESC + ) DESCRIPTION + FROM + ATTESTATIONS A + LEFT JOIN TWITTER_PROFILES_CACHE TPC ON TPC.ID = A.ACCOUNT + WHERE + A.ARCHIVED = FALSE + UNION + SELECT -- when there are entries for non-users + A."profileId" ID, + 'service:x.com:' || TPC.ID::TEXT TARGET, + TPC.NAME, + TPC.AVATAR, + TPC.USERNAME, + TPC.BIOGRAPHY DESCRIPTION + FROM + ATTESTATIONS A + RIGHT JOIN TWITTER_PROFILES_CACHE TPC ON TPC.ID = A.ACCOUNT + WHERE + A.ID IS NULL + ), + MORALIS_CACHE AS ( + SELECT + THC."fromAddress" TARGET, + THC."fromAddressLabel" NAME, + THC."fromAddressLogo" AVATAR + FROM + TRANSACTION_HISTORY_CACHE THC + UNION + SELECT + THC."toAddress" TARGET, + THC."toAddressLabel" NAME, + THC."toAddressLogo" AVATAR + FROM + TRANSACTION_HISTORY_CACHE THC + ), + MORALIS_NAMES AS ( + SELECT DISTINCT + PA."profileId" ID, + 'address:' || LOWER(PA.ADDRESS) TARGET, + FIRST_VALUE(MC.NAME) OVER ( + PARTITION BY + PA."profileId" + ) NAME, + FIRST_VALUE(MC.AVATAR) OVER ( + PARTITION BY + PA."profileId" + ) AVATAR, + NULL USERNAME, + NULL DESCRIPTION + FROM + PROFILE_ADDRESSES PA + LEFT JOIN MORALIS_CACHE MC ON LOWER(MC.TARGET) = LOWER(PA.ADDRESS) + UNION + SELECT DISTINCT -- when there are entries for non-users + PA."profileId" ID, + 'address:' || LOWER(MC.TARGET) TARGET, + MC.NAME, + MC.AVATAR, + NULL USERNAME, + NULL DESCRIPTION + FROM + PROFILE_ADDRESSES PA + RIGHT JOIN MORALIS_CACHE MC ON LOWER(MC.TARGET) = LOWER(PA.ADDRESS) + WHERE + PA.ID IS NULL + ), + NAME_UNION AS ( + SELECT + * + FROM + ENS_NAMES + UNION + SELECT + * + FROM + TWITTER_NAMES + UNION + SELECT + * + FROM + MORALIS_NAMES + ) ( + SELECT DISTINCT + ON (N.ID) N.ID, + COALESCE(ENS.TARGET, TWT.TARGET, MRLS.TARGET) TARGET, + COALESCE(ENS.NAME, TWT.NAME, MRLS.NAME) NAME, + COALESCE(ENS.AVATAR, TWT.AVATAR, MRLS.AVATAR) AVATAR, + COALESCE(ENS.USERNAME, TWT.USERNAME, MRLS.USERNAME) USERNAME, + COALESCE( + ENS.DESCRIPTION, + TWT.DESCRIPTION, + MRLS.DESCRIPTION + ) DESCRIPTION + FROM + NAME_UNION N + LEFT JOIN ENS_NAMES ENS ON ENS.ID = N.ID + LEFT JOIN TWITTER_NAMES TWT ON TWT.ID = N.ID + LEFT JOIN MORALIS_NAMES MRLS ON MRLS.ID = N.ID + WHERE + N.ID IS NOT NULL + ORDER BY + N.ID + ) +UNION +( + SELECT DISTINCT + ON (N.TARGET) -- names for non-users + N.ID, + COALESCE(ENS.TARGET, TWT.TARGET, MRLS.TARGET) TARGET, + COALESCE(ENS.NAME, TWT.NAME, MRLS.NAME) NAME, + COALESCE(ENS.AVATAR, TWT.AVATAR, MRLS.AVATAR) AVATAR, + COALESCE(ENS.USERNAME, TWT.USERNAME, MRLS.USERNAME) USERNAME, + COALESCE( + ENS.DESCRIPTION, + TWT.DESCRIPTION, + MRLS.DESCRIPTION + ) DESCRIPTION + FROM + NAME_UNION N + LEFT JOIN ENS_NAMES ENS ON ENS.TARGET = N.TARGET + LEFT JOIN TWITTER_NAMES TWT ON TWT.TARGET = N.TARGET + LEFT JOIN MORALIS_NAMES MRLS ON MRLS.TARGET = N.TARGET + WHERE + N.ID IS NULL + ORDER BY + N.TARGET +); + +---- scores +CREATE VIEW "scores" AS +WITH SCORE_TARGETS AS ( + SELECT DISTINCT + ON (TARGET) LOWER(TARGET) TARGET, + SCORE, + "createdAt" + FROM + SCORE_HISTORY + ORDER BY + TARGET, + "createdAt" DESC + ) + ( + SELECT DISTINCT + ON (T.ID) T.ID, + T.TARGET, + FIRST_VALUE(SCORE) OVER ( + PARTITION BY + T.ID + ORDER BY + ST."createdAt" DESC + ) SCORE + FROM + TARGETS T + JOIN SCORE_TARGETS ST ON LOWER(ST.TARGET) = LOWER(T.TARGET) + ORDER BY + T.ID + ) + UNION + ( + SELECT -- scores for non-users + T.ID, + ST.TARGET, + SCORE + FROM + TARGETS T + RIGHT JOIN SCORE_TARGETS ST ON LOWER(ST.TARGET) = LOWER(T.TARGET) + WHERE + T.ID IS NULL + ) diff --git a/ethos/services/echo/prisma/postgres/migrations/20241011140541_event_types/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241011140541_event_types/migration.sql new file mode 100644 index 0000000..39d30fc --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241011140541_event_types/migration.sql @@ -0,0 +1,35 @@ +-- CreateEnum +CREATE TYPE "ProfileEventType" AS ENUM ('create', 'archive', 'restore', 'invite', 'uninvite'); + +-- CreateEnum +CREATE TYPE "ReviewEventType" AS ENUM ('create', 'edit', 'archive', 'restore'); + +-- CreateEnum +CREATE TYPE "VouchEventType" AS ENUM ('create', 'unvouch', 'unhealthy'); + +-- CreateEnum +CREATE TYPE "ReplyEventType" AS ENUM ('create', 'edit'); + +-- CreateEnum +CREATE TYPE "AttestationEventType" AS ENUM ('create', 'archive', 'restore', 'claim'); + +-- CreateEnum +CREATE TYPE "VoteEventType" AS ENUM ('create', 'archive', 'update'); + +-- AlterTable +ALTER TABLE "attestation_events" ADD COLUMN "type" "AttestationEventType"; + +-- AlterTable +ALTER TABLE "profile_events" ADD COLUMN "type" "ProfileEventType"; + +-- AlterTable +ALTER TABLE "reply_events" ADD COLUMN "type" "ReplyEventType"; + +-- AlterTable +ALTER TABLE "review_events" ADD COLUMN "type" "ReviewEventType"; + +-- AlterTable +ALTER TABLE "vote_events" ADD COLUMN "type" "VoteEventType"; + +-- AlterTable +ALTER TABLE "vouch_events" ADD COLUMN "type" "VouchEventType"; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241014172830_reset_markets/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241014172830_reset_markets/migration.sql new file mode 100644 index 0000000..af5102c --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241014172830_reset_markets/migration.sql @@ -0,0 +1 @@ +-- This is an empty migration. \ No newline at end of file diff --git a/ethos/services/echo/prisma/postgres/migrations/20241014204916_targets_names_scores_materialized_views/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241014204916_targets_names_scores_materialized_views/migration.sql new file mode 100644 index 0000000..3247247 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241014204916_targets_names_scores_materialized_views/migration.sql @@ -0,0 +1,261 @@ +-- drop virtual views +DROP VIEW IF EXISTS scores; +DROP VIEW IF EXISTS names; +DROP VIEW IF EXISTS targets; + +-- create materialized view targets +CREATE MATERIALIZED VIEW targets AS +SELECT + ID, + 'profileId:' || ID::TEXT AS TARGET +FROM + PROFILES P +UNION +SELECT + P.ID, + 'address:' || LOWER(PA.ADDRESS) AS TARGET +FROM + PROFILES P + JOIN PROFILE_ADDRESSES PA ON PA."profileId" = P.ID +UNION +SELECT + P.ID, + 'service:' || A.SERVICE || ':' || A.ACCOUNT::TEXT AS TARGET +FROM + PROFILES P + JOIN ATTESTATIONS A ON A."profileId" = P.ID; + +-- create materialized view names +CREATE MATERIALIZED VIEW "names" AS +WITH + ENS_NAMES AS ( + SELECT + PA."profileId" ID, + 'address:' || LOWER(PA.ADDRESS) TARGET, + FIRST_VALUE(ENS."ensName") OVER ( + PARTITION BY + PA."profileId" + ORDER BY + ENS."createdAt" DESC + ) NAME, + FIRST_VALUE(ENS."avatarUrl") OVER ( + PARTITION BY + PA."profileId" + ORDER BY + ENS."createdAt" DESC + ) AVATAR, + NULL USERNAME, + NULL DESCRIPTION + FROM + PROFILE_ADDRESSES PA + LEFT JOIN ENS_CACHE ENS ON LOWER(PA.ADDRESS) = LOWER(ENS.ADDRESS) + UNION + SELECT -- when there are ens entries for non-users + PA."profileId" ID, + 'address:' || LOWER(ENS.ADDRESS) TARGET, + ENS."ensName" NAME, + ENS."avatarUrl" AVATAR, + NULL USERNAME, + NULL DESCRIPTION + FROM + PROFILE_ADDRESSES PA + RIGHT JOIN ENS_CACHE ENS ON LOWER(PA.ADDRESS) = LOWER(ENS.ADDRESS) + WHERE + PA.ID IS NULL + ), + TWITTER_NAMES AS ( + SELECT + A."profileId" ID, + 'service:x.com:' || A.ACCOUNT::TEXT TARGET, + FIRST_VALUE(TPC.NAME) OVER ( + PARTITION BY + A."profileId" + ORDER BY + TPC."createdAt" DESC + ) NAME, + FIRST_VALUE(TPC.AVATAR) OVER ( + PARTITION BY + A."profileId" + ORDER BY + TPC."createdAt" DESC + ) AVATAR, + FIRST_VALUE(TPC.USERNAME) OVER ( + PARTITION BY + A."profileId" + ORDER BY + TPC."createdAt" DESC + ) USERNAME, + FIRST_VALUE(TPC.BIOGRAPHY) OVER ( + PARTITION BY + A."profileId" + ORDER BY + TPC."createdAt" DESC + ) DESCRIPTION + FROM + ATTESTATIONS A + LEFT JOIN TWITTER_PROFILES_CACHE TPC ON TPC.ID = A.ACCOUNT + WHERE + A.ARCHIVED = FALSE + UNION + SELECT -- when there are entries for non-users + A."profileId" ID, + 'service:x.com:' || TPC.ID::TEXT TARGET, + TPC.NAME, + TPC.AVATAR, + TPC.USERNAME, + TPC.BIOGRAPHY DESCRIPTION + FROM + ATTESTATIONS A + RIGHT JOIN TWITTER_PROFILES_CACHE TPC ON TPC.ID = A.ACCOUNT + WHERE + A.ID IS NULL + ), + MORALIS_CACHE AS ( + SELECT + THC."fromAddress" TARGET, + THC."fromAddressLabel" NAME, + THC."fromAddressLogo" AVATAR + FROM + TRANSACTION_HISTORY_CACHE THC + UNION + SELECT + THC."toAddress" TARGET, + THC."toAddressLabel" NAME, + THC."toAddressLogo" AVATAR + FROM + TRANSACTION_HISTORY_CACHE THC + ), + MORALIS_NAMES AS ( + SELECT DISTINCT + PA."profileId" ID, + 'address:' || LOWER(PA.ADDRESS) TARGET, + FIRST_VALUE(MC.NAME) OVER ( + PARTITION BY + PA."profileId" + ) NAME, + FIRST_VALUE(MC.AVATAR) OVER ( + PARTITION BY + PA."profileId" + ) AVATAR, + NULL USERNAME, + NULL DESCRIPTION + FROM + PROFILE_ADDRESSES PA + LEFT JOIN MORALIS_CACHE MC ON LOWER(MC.TARGET) = LOWER(PA.ADDRESS) + UNION + SELECT DISTINCT -- when there are entries for non-users + PA."profileId" ID, + 'address:' || LOWER(MC.TARGET) TARGET, + MC.NAME, + MC.AVATAR, + NULL USERNAME, + NULL DESCRIPTION + FROM + PROFILE_ADDRESSES PA + RIGHT JOIN MORALIS_CACHE MC ON LOWER(MC.TARGET) = LOWER(PA.ADDRESS) + WHERE + PA.ID IS NULL + ), + NAME_UNION AS ( + SELECT + * + FROM + ENS_NAMES + UNION + SELECT + * + FROM + TWITTER_NAMES + UNION + SELECT + * + FROM + MORALIS_NAMES + ) ( + SELECT DISTINCT + ON (N.ID) N.ID, + COALESCE(ENS.TARGET, TWT.TARGET, MRLS.TARGET) TARGET, + COALESCE(ENS.NAME, TWT.NAME, MRLS.NAME) NAME, + COALESCE(ENS.AVATAR, TWT.AVATAR, MRLS.AVATAR) AVATAR, + COALESCE(ENS.USERNAME, TWT.USERNAME, MRLS.USERNAME) USERNAME, + COALESCE( + ENS.DESCRIPTION, + TWT.DESCRIPTION, + MRLS.DESCRIPTION + ) DESCRIPTION + FROM + NAME_UNION N + LEFT JOIN ENS_NAMES ENS ON ENS.ID = N.ID + LEFT JOIN TWITTER_NAMES TWT ON TWT.ID = N.ID + LEFT JOIN MORALIS_NAMES MRLS ON MRLS.ID = N.ID + WHERE + N.ID IS NOT NULL + ORDER BY + N.ID + ) +UNION +( + SELECT DISTINCT + ON (N.TARGET) -- names for non-users + N.ID, + COALESCE(ENS.TARGET, TWT.TARGET, MRLS.TARGET) TARGET, + COALESCE(ENS.NAME, TWT.NAME, MRLS.NAME) NAME, + COALESCE(ENS.AVATAR, TWT.AVATAR, MRLS.AVATAR) AVATAR, + COALESCE(ENS.USERNAME, TWT.USERNAME, MRLS.USERNAME) USERNAME, + COALESCE( + ENS.DESCRIPTION, + TWT.DESCRIPTION, + MRLS.DESCRIPTION + ) DESCRIPTION + FROM + NAME_UNION N + LEFT JOIN ENS_NAMES ENS ON ENS.TARGET = N.TARGET + LEFT JOIN TWITTER_NAMES TWT ON TWT.TARGET = N.TARGET + LEFT JOIN MORALIS_NAMES MRLS ON MRLS.TARGET = N.TARGET + WHERE + N.ID IS NULL + ORDER BY + N.TARGET +); + +-- create materialized view scores +CREATE MATERIALIZED VIEW "scores" AS +WITH SCORE_TARGETS AS ( + SELECT DISTINCT + ON (TARGET) LOWER(TARGET) TARGET, + SCORE, + "createdAt" + FROM + SCORE_HISTORY + ORDER BY + TARGET, + "createdAt" DESC + ) + ( + SELECT DISTINCT + ON (T.ID) T.ID, + T.TARGET, + FIRST_VALUE(SCORE) OVER ( + PARTITION BY + T.ID + ORDER BY + ST."createdAt" DESC + ) SCORE + FROM + TARGETS T + JOIN SCORE_TARGETS ST ON LOWER(ST.TARGET) = LOWER(T.TARGET) + ORDER BY + T.ID + ) + UNION + ( + SELECT -- scores for non-users + T.ID, + ST.TARGET, + SCORE + FROM + TARGETS T + RIGHT JOIN SCORE_TARGETS ST ON LOWER(ST.TARGET) = LOWER(T.TARGET) + WHERE + T.ID IS NULL + ); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241014223158_blockchain_event_queue/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241014223158_blockchain_event_queue/migration.sql new file mode 100644 index 0000000..f2b712b --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241014223158_blockchain_event_queue/migration.sql @@ -0,0 +1,4 @@ +ALTER TABLE "blockchain_events" DROP COLUMN "attempts", +ADD COLUMN "jobCreated" BOOLEAN NOT NULL DEFAULT false; + +UPDATE "blockchain_events" SET "jobCreated" = true WHERE processed = true diff --git a/ethos/services/echo/prisma/postgres/migrations/20241018141533_score_attestation_indexes/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241018141533_score_attestation_indexes/migration.sql new file mode 100644 index 0000000..32097ea --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241018141533_score_attestation_indexes/migration.sql @@ -0,0 +1,5 @@ +-- CreateIndex +CREATE INDEX "attestations_account_idx" ON "attestations"("account"); + +-- CreateIndex +CREATE INDEX "score_history_target_createdAt_idx" ON "score_history"("target", "createdAt" DESC); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241024144623_highest_scores_index/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241024144623_highest_scores_index/migration.sql new file mode 100644 index 0000000..dca0500 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241024144623_highest_scores_index/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX "score_history_target_createdAt_idx"; + +-- CreateIndex +CREATE INDEX "score_history_target_createdAt_score_idx" ON "score_history"("target", "createdAt" DESC, "score" DESC); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241026235047_invitation_attestation_indexes/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241026235047_invitation_attestation_indexes/migration.sql new file mode 100644 index 0000000..e997c26 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241026235047_invitation_attestation_indexes/migration.sql @@ -0,0 +1,11 @@ +-- DropIndex +DROP INDEX "attestations_account_idx"; + +-- DropIndex +DROP INDEX "invitations_senderProfileId_idx"; + +-- CreateIndex +CREATE INDEX "attestations_service_account_archived_idx" ON "attestations"("service", "account", "archived"); + +-- CreateIndex +CREATE INDEX "invitations_senderProfileId_status_idx" ON "invitations"("senderProfileId", "status"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241028113902_remove_materialized_views/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241028113902_remove_materialized_views/migration.sql new file mode 100644 index 0000000..ef05ac2 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241028113902_remove_materialized_views/migration.sql @@ -0,0 +1,4 @@ +-- drop virtual views +DROP MATERIALIZED VIEW IF EXISTS scores; +DROP MATERIALIZED VIEW IF EXISTS names; +DROP MATERIALIZED VIEW IF EXISTS targets; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241028163452_case_insensetive_address_ensname_username/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241028163452_case_insensetive_address_ensname_username/migration.sql new file mode 100644 index 0000000..91bf3c0 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241028163452_case_insensetive_address_ensname_username/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE "ens_cache" ALTER COLUMN "address" SET DATA TYPE CITEXT, +ALTER COLUMN "ensName" SET DATA TYPE CITEXT; + +-- AlterTable +ALTER TABLE "profile_addresses" ALTER COLUMN "address" SET DATA TYPE CITEXT; + +-- AlterTable +ALTER TABLE "twitter_profiles_cache" ALTER COLUMN "username" SET DATA TYPE CITEXT; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241028224554_add_index_to_tx_hash_on_blockchain_events/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241028224554_add_index_to_tx_hash_on_blockchain_events/migration.sql new file mode 100644 index 0000000..4e6e0fc --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241028224554_add_index_to_tx_hash_on_blockchain_events/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "blockchain_events_txHash_idx" ON "blockchain_events"("txHash"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241029182634_contribution_tasks/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241029182634_contribution_tasks/migration.sql new file mode 100644 index 0000000..dfeba3b --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241029182634_contribution_tasks/migration.sql @@ -0,0 +1,126 @@ +-- CreateEnum +CREATE TYPE "ContributionType" AS ENUM ('REVIEW', 'TRUST_BATTLE', 'TRUST_CHECK', 'REVIEW_CHECK', 'REVIEW_VOTE', 'SCORE_CHECK'); + +-- CreateEnum +CREATE TYPE "ContributionStatus" AS ENUM ('PENDING', 'COMPLETED', 'SKIPPED'); + +-- CreateEnum +CREATE TYPE "ContributionAnswer" AS ENUM ('POSITIVE', 'NEGATIVE', 'NEUTRAL', 'UNSURE'); + +-- CreateTable +CREATE TABLE "contribution_bundles" ( + "id" SERIAL NOT NULL, + "profileId" INTEGER NOT NULL, + + CONSTRAINT "contribution_bundles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "contributions" ( + "id" SERIAL NOT NULL, + "contributionBundleId" INTEGER NOT NULL, + "type" "ContributionType" NOT NULL, + "experience" DOUBLE PRECISION NOT NULL, + "status" "ContributionStatus" NOT NULL, + + CONSTRAINT "contributions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "contribution_trust_checks" ( + "contributionId" INTEGER NOT NULL, + "targetUserkey" TEXT NOT NULL, + "answer" "ContributionAnswer", + + CONSTRAINT "contribution_trust_checks_pkey" PRIMARY KEY ("contributionId") +); + +-- CreateTable +CREATE TABLE "contribution_reviews" ( + "contributionId" INTEGER NOT NULL, + "targetUserkeys" TEXT[], + "reviewId" INTEGER, + + CONSTRAINT "contribution_reviews_pkey" PRIMARY KEY ("contributionId") +); + +-- CreateTable +CREATE TABLE "contribution_trust_battles" ( + "contributionId" INTEGER NOT NULL, + "targetUserkeys" TEXT[], + "chosenIndex" INTEGER, + + CONSTRAINT "contribution_trust_battles_pkey" PRIMARY KEY ("contributionId") +); + +-- CreateTable +CREATE TABLE "contribution_score_checks" ( + "contributionId" INTEGER NOT NULL, + "targetUserkey" TEXT NOT NULL, + "answer" "ContributionAnswer", + + CONSTRAINT "contribution_score_checks_pkey" PRIMARY KEY ("contributionId") +); + +-- CreateTable +CREATE TABLE "contribution_review_checks" ( + "contributionId" INTEGER NOT NULL, + "reviewId" INTEGER NOT NULL, + "answer" "ContributionAnswer", + + CONSTRAINT "contribution_review_checks_pkey" PRIMARY KEY ("contributionId") +); + +-- CreateTable +CREATE TABLE "contribution_review_votes" ( + "contributionId" INTEGER NOT NULL, + "reviewId" INTEGER NOT NULL, + "voteId" INTEGER, + + CONSTRAINT "contribution_review_votes_pkey" PRIMARY KEY ("contributionId") +); + +-- CreateIndex +CREATE INDEX "contribution_bundles_profileId_idx" ON "contribution_bundles"("profileId"); + +-- CreateIndex +CREATE INDEX "contributions_contributionBundleId_idx" ON "contributions"("contributionBundleId"); + +-- CreateIndex +CREATE INDEX "contributions_status_idx" ON "contributions"("status"); + +-- AddForeignKey +ALTER TABLE "contribution_bundles" ADD CONSTRAINT "contribution_bundles_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contributions" ADD CONSTRAINT "contributions_contributionBundleId_fkey" FOREIGN KEY ("contributionBundleId") REFERENCES "contribution_bundles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contribution_trust_checks" ADD CONSTRAINT "contribution_trust_checks_contributionId_fkey" FOREIGN KEY ("contributionId") REFERENCES "contributions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contribution_reviews" ADD CONSTRAINT "contribution_reviews_contributionId_fkey" FOREIGN KEY ("contributionId") REFERENCES "contributions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contribution_reviews" ADD CONSTRAINT "contribution_reviews_reviewId_fkey" FOREIGN KEY ("reviewId") REFERENCES "reviews"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contribution_trust_battles" ADD CONSTRAINT "contribution_trust_battles_contributionId_fkey" FOREIGN KEY ("contributionId") REFERENCES "contributions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contribution_score_checks" ADD CONSTRAINT "contribution_score_checks_contributionId_fkey" FOREIGN KEY ("contributionId") REFERENCES "contributions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contribution_review_checks" ADD CONSTRAINT "contribution_review_checks_contributionId_fkey" FOREIGN KEY ("contributionId") REFERENCES "contributions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contribution_review_checks" ADD CONSTRAINT "contribution_review_checks_reviewId_fkey" FOREIGN KEY ("reviewId") REFERENCES "reviews"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contribution_review_votes" ADD CONSTRAINT "contribution_review_votes_contributionId_fkey" FOREIGN KEY ("contributionId") REFERENCES "contributions"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contribution_review_votes" ADD CONSTRAINT "contribution_review_votes_reviewId_fkey" FOREIGN KEY ("reviewId") REFERENCES "reviews"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contribution_review_votes" ADD CONSTRAINT "contribution_review_votes_voteId_fkey" FOREIGN KEY ("voteId") REFERENCES "votes"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241031162752_contribution_expiry/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241031162752_contribution_expiry/migration.sql new file mode 100644 index 0000000..47dd008 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241031162752_contribution_expiry/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Added the required column `expireAt` to the `contribution_bundles` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "contribution_bundles" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "expireAt" TIMESTAMP(3) NOT NULL; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241106154118_add_users_fcm_tokens_table/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241106154118_add_users_fcm_tokens_table/migration.sql new file mode 100644 index 0000000..4347aaa --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241106154118_add_users_fcm_tokens_table/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "user_fcm_tokens" ( + "id" SERIAL NOT NULL, + "profileId" INTEGER NOT NULL, + "fcmToken" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "user_fcm_tokens_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "user_fcm_tokens_profileId_idx" ON "user_fcm_tokens"("profileId"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_fcm_tokens_profileId_fcmToken_key" ON "user_fcm_tokens"("profileId", "fcmToken"); + +-- AddForeignKey +ALTER TABLE "user_fcm_tokens" ADD CONSTRAINT "user_fcm_tokens_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241108143621_blockchain_event_last_poll/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241108143621_blockchain_event_last_poll/migration.sql new file mode 100644 index 0000000..56a1816 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241108143621_blockchain_event_last_poll/migration.sql @@ -0,0 +1,9 @@ +-- CreateTable +CREATE TABLE "blockchain_event_polls" ( + "contract" TEXT NOT NULL, + "lastBlockNumber" INTEGER NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "blockchain_event_polls_contract_key" ON "blockchain_event_polls"("contract"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241112202700_reset_markets/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241112202700_reset_markets/migration.sql new file mode 100644 index 0000000..d29e4d0 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241112202700_reset_markets/migration.sql @@ -0,0 +1,4 @@ +-- This migration is used to reset the market data for all profiles, given the new market contracts. +DELETE FROM market_vote_events; +DELETE FROM market_events; +DELETE FROM markets; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241112203800_add_xp_points_history/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241112203800_add_xp_points_history/migration.sql new file mode 100644 index 0000000..9d83a16 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241112203800_add_xp_points_history/migration.sql @@ -0,0 +1,23 @@ +-- CreateEnum +CREATE TYPE "XpPointsHistoryItemType" AS ENUM ('REVIEW'); + +-- CreateTable +CREATE TABLE "xp_points_history" ( + "id" SERIAL NOT NULL, + "profileId" INTEGER NOT NULL, + "type" "XpPointsHistoryItemType" NOT NULL, + "points" INTEGER NOT NULL, + "metadata" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "xp_points_history_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "xp_points_history_profileId_idx" ON "xp_points_history"("profileId"); + +-- CreateIndex +CREATE INDEX "xp_points_history_createdAt_idx" ON "xp_points_history"("createdAt"); + +-- AddForeignKey +ALTER TABLE "xp_points_history" ADD CONSTRAINT "xp_points_history_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241112224330_rep_market_reset/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241112224330_rep_market_reset/migration.sql new file mode 100644 index 0000000..3f447fb --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241112224330_rep_market_reset/migration.sql @@ -0,0 +1,7 @@ +-- This migration is used to reset the market data for all profiles, given the new market contracts. +DELETE FROM market_vote_events; +DELETE FROM market_events; +DELETE FROM markets; + +TRUNCATE TABLE market_vote_events RESTART IDENTITY; +TRUNCATE TABLE market_events RESTART IDENTITY; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241114123607_add_xp_contribution_type/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241114123607_add_xp_contribution_type/migration.sql new file mode 100644 index 0000000..146fa6e --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241114123607_add_xp_contribution_type/migration.sql @@ -0,0 +1,23 @@ +-- AlterEnum +ALTER TYPE "XpPointsHistoryItemType" ADD VALUE 'CONTRIBUTION'; + +-- Commit the transaction to ensure the new enum value is available +COMMIT; + +-- Empty table before inserting data +TRUNCATE TABLE xp_points_history RESTART IDENTITY; + +-- Insert completed contributions into xp_points_history +INSERT INTO xp_points_history ("profileId", points, type, metadata, "createdAt") +SELECT + cb."profileId", + c.experience, + 'CONTRIBUTION' AS type, + jsonb_build_object('id', c.id, 'type', 'contribution', 'subType', c.type) AS metadata, + cb."createdAt" AS "createdAt" +FROM + contributions c +JOIN + contribution_bundles cb ON c."contributionBundleId" = cb.id +WHERE + c.status = 'COMPLETED'; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241114190812_add_device_identifier_to_fcm_tokens_table/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241114190812_add_device_identifier_to_fcm_tokens_table/migration.sql new file mode 100644 index 0000000..d4810a0 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241114190812_add_device_identifier_to_fcm_tokens_table/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "user_fcm_tokens" ADD COLUMN "deviceIdentifier" TEXT, +ADD COLUMN "userAgent" TEXT; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241118165211_change_xp_history_points_col_type/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241118165211_change_xp_history_points_col_type/migration.sql new file mode 100644 index 0000000..e39ab9f --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241118165211_change_xp_history_points_col_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "xp_points_history" ALTER COLUMN "points" SET DATA TYPE DOUBLE PRECISION; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241118211702_score_transactions/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241118211702_score_transactions/migration.sql new file mode 100644 index 0000000..b91d959 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241118211702_score_transactions/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "score_history" ADD COLUMN "txHash" VARCHAR(66); + +-- CreateIndex +CREATE INDEX "score_history_target_txHash_idx" ON "score_history"("target", "txHash"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241119222440_add_vouch_day_type/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241119222440_add_vouch_day_type/migration.sql new file mode 100644 index 0000000..0383605 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241119222440_add_vouch_day_type/migration.sql @@ -0,0 +1,5 @@ +-- AlterEnum +ALTER TYPE "XpPointsHistoryItemType" ADD VALUE 'VOUCH_DAY'; + +-- Flooring and setting to int +ALTER TABLE "xp_points_history" ALTER COLUMN "points" SET DATA TYPE INT; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241120003136_score_elements/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241120003136_score_elements/migration.sql new file mode 100644 index 0000000..67d7285 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241120003136_score_elements/migration.sql @@ -0,0 +1,34 @@ +-- CreateTable +CREATE TABLE "score_algorithms" ( + "version" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "definition" JSONB NOT NULL, + + CONSTRAINT "score_algorithms_pkey" PRIMARY KEY ("version") +); + +-- CreateTable +CREATE TABLE "score_element_definitions" ( + "name" TEXT NOT NULL, + "type" TEXT NOT NULL, + "min" INTEGER NOT NULL, + "max" INTEGER NOT NULL, + "ranges" JSONB, + "outOfRangeScore" INTEGER, + "scoreAlgorithmVersion" INTEGER NOT NULL +); + +-- CreateIndex +CREATE INDEX "score_algorithms_version_idx" ON "score_algorithms"("version"); + +-- CreateIndex +CREATE INDEX "score_algorithms_createdAt_idx" ON "score_algorithms"("createdAt"); + +-- CreateIndex +CREATE INDEX "score_element_definitions_scoreAlgorithmVersion_idx" ON "score_element_definitions"("scoreAlgorithmVersion"); + +-- CreateIndex +CREATE UNIQUE INDEX "score_element_definitions_name_scoreAlgorithmVersion_key" ON "score_element_definitions"("name", "scoreAlgorithmVersion"); + +-- AddForeignKey +ALTER TABLE "score_element_definitions" ADD CONSTRAINT "score_element_definitions_scoreAlgorithmVersion_fkey" FOREIGN KEY ("scoreAlgorithmVersion") REFERENCES "score_algorithms"("version") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241120204255_score_element_history/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241120204255_score_element_history/migration.sql new file mode 100644 index 0000000..859c52d --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241120204255_score_element_history/migration.sql @@ -0,0 +1,45 @@ +/* + Warnings: + + - You are about to drop the column `elements` on the `score_history` table. All the data in the column will be lost. + - You are about to drop the column `errors` on the `score_history` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "score_history" DROP COLUMN "elements", +DROP COLUMN "errors"; + +-- CreateTable +CREATE TABLE "score_history_elements" ( + "scoreHistoryId" INTEGER NOT NULL, + "scoreElementId" INTEGER NOT NULL, + + CONSTRAINT "score_history_elements_pkey" PRIMARY KEY ("scoreHistoryId","scoreElementId") +); + +-- CreateTable +CREATE TABLE "score_element_records" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "version" INTEGER NOT NULL, + "raw" INTEGER NOT NULL, + "weighted" INTEGER NOT NULL, + "error" BOOLEAN NOT NULL, + + CONSTRAINT "score_element_records_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "score_element_records_version_name_idx" ON "score_element_records"("version", "name"); + +-- AddForeignKey +ALTER TABLE "score_history_elements" ADD CONSTRAINT "score_history_elements_scoreHistoryId_fkey" FOREIGN KEY ("scoreHistoryId") REFERENCES "score_history"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "score_history_elements" ADD CONSTRAINT "score_history_elements_scoreElementId_fkey" FOREIGN KEY ("scoreElementId") REFERENCES "score_element_records"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "score_element_records" ADD CONSTRAINT "score_element_records_name_version_fkey" FOREIGN KEY ("name", "version") REFERENCES "score_element_definitions"("name", "scoreAlgorithmVersion") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- mark all existing scores out of date +UPDATE "score_history" SET dirty = true WHERE dirty = false; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241121172856_market_contract_reset/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241121172856_market_contract_reset/migration.sql new file mode 100644 index 0000000..cbce613 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241121172856_market_contract_reset/migration.sql @@ -0,0 +1,2 @@ +-- This migration is used to reset the market data for all profiles, given the new market contracts. +TRUNCATE TABLE markets RESTART IDENTITY CASCADE; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241121234921_privy_logins/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241121234921_privy_logins/migration.sql new file mode 100644 index 0000000..a9054ef --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241121234921_privy_logins/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "privy_logins" ( + "id" TEXT NOT NULL, + "twitterUserId" TEXT NOT NULL, + "embeddedWallet" CITEXT NOT NULL, + "smartWallet" CITEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "privy_logins_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "privy_logins_twitterUserId_idx" ON "privy_logins"("twitterUserId"); + +-- CreateIndex +CREATE INDEX "privy_logins_smartWallet_idx" ON "privy_logins"("smartWallet"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241126140058_add_invite_accepted_type/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241126140058_add_invite_accepted_type/migration.sql new file mode 100644 index 0000000..e96754d --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241126140058_add_invite_accepted_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "XpPointsHistoryItemType" ADD VALUE 'INVITE_ACCEPTED'; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241130014021_audited_contracts/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241130014021_audited_contracts/migration.sql new file mode 100644 index 0000000..3724fd3 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241130014021_audited_contracts/migration.sql @@ -0,0 +1,80 @@ +/* + Warnings: + + - You are about to drop the column `stakeToken` on the `vouches` table. All the data in the column will be lost. + - You are about to drop the `escrow` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `escrow_events` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `authorAddress` to the `vouches` table without a default value. This is not possible if the table is not empty. + +*/ + +-- DropTable +DROP TABLE "escrow" CASCADE; + +-- DropTable +DROP TABLE "escrow_events" CASCADE; + +-- Delete existing blockchain events and data as we are relaunching the contracts +-- Core blockchain event tables +TRUNCATE "blockchain_events" RESTART IDENTITY CASCADE; +TRUNCATE "blockchain_event_polls" RESTART IDENTITY CASCADE; + +-- Event-specific tables and their relations +TRUNCATE "profile_events" RESTART IDENTITY CASCADE; +TRUNCATE "review_events" RESTART IDENTITY CASCADE; +TRUNCATE "vouch_events" RESTART IDENTITY CASCADE; +TRUNCATE "reply_events" RESTART IDENTITY CASCADE; +TRUNCATE "attestation_events" RESTART IDENTITY CASCADE; +TRUNCATE "vote_events" RESTART IDENTITY CASCADE; +TRUNCATE "market_events" RESTART IDENTITY CASCADE; +TRUNCATE "market_vote_events" RESTART IDENTITY CASCADE; + +-- Main data tables that are populated from blockchain events +TRUNCATE "profiles" RESTART IDENTITY CASCADE; +TRUNCATE "vouches" RESTART IDENTITY CASCADE; +TRUNCATE "reviews" RESTART IDENTITY CASCADE; +TRUNCATE "replies" RESTART IDENTITY CASCADE; +TRUNCATE "votes" RESTART IDENTITY CASCADE; +TRUNCATE "attestations" RESTART IDENTITY CASCADE; +TRUNCATE "markets" RESTART IDENTITY CASCADE; + +-- Reset scores and related tables +TRUNCATE "score_history" RESTART IDENTITY CASCADE; +TRUNCATE "score_history_elements" RESTART IDENTITY CASCADE; +TRUNCATE "score_element_records" RESTART IDENTITY CASCADE; +TRUNCATE "score_algorithms" RESTART IDENTITY CASCADE; +TRUNCATE "score_element_definitions" RESTART IDENTITY CASCADE; + +-- Reset XP +TRUNCATE "xp_points_history" RESTART IDENTITY CASCADE; + +-- AlterEnum +ALTER TYPE "VouchEventType" ADD VALUE 'increase'; +ALTER TYPE "VouchEventType" ADD VALUE 'deposit_rewards'; +ALTER TYPE "VouchEventType" ADD VALUE 'withdraw_rewards'; +ALTER TYPE "VouchEventType" ADD VALUE 'slash'; + +-- AlterTable +ALTER TABLE "vouches" DROP COLUMN "stakeToken", +ADD COLUMN "authorAddress" VARCHAR(42) NOT NULL; + +-- CreateTable +CREATE TABLE "rewards" ( + "id" SERIAL NOT NULL, + "profileId" INTEGER NOT NULL, + "balance" DECIMAL(78,0) NOT NULL, + "lifetime" DECIMAL(78,0) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "rewards_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "rewards_profileId_idx" ON "rewards"("profileId"); + +-- CreateIndex +CREATE INDEX "rewards_createdAt_idx" ON "rewards"("createdAt"); + +-- AddForeignKey +ALTER TABLE "rewards" ADD CONSTRAINT "rewards_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241204220423_remove_activity_views/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241204220423_remove_activity_views/migration.sql new file mode 100644 index 0000000..f674c55 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241204220423_remove_activity_views/migration.sql @@ -0,0 +1 @@ +DROP VIEW activities; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241206002056_score_metadata/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241206002056_score_metadata/migration.sql new file mode 100644 index 0000000..b4f0872 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241206002056_score_metadata/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "score_element_records" ADD COLUMN "metadata" JSONB; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241206083756_market_votes_events_index_update/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241206083756_market_votes_events_index_update/migration.sql new file mode 100644 index 0000000..f2782e3 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241206083756_market_votes_events_index_update/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "market_vote_events_actorAddress_createdAt_idx" ON "market_vote_events"("actorAddress", "createdAt"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241209222457_privy_logins_optional_twitter_id/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241209222457_privy_logins_optional_twitter_id/migration.sql new file mode 100644 index 0000000..3a7b5eb --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241209222457_privy_logins_optional_twitter_id/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - A unique constraint covering the columns `[twitterUserId]` on the table `privy_logins` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "privy_logins" ALTER COLUMN "twitterUserId" DROP NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "privy_logins_twitterUserId_key" ON "privy_logins"("twitterUserId"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241216181527_privy_logins_add_connected_wallet/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241216181527_privy_logins_add_connected_wallet/migration.sql new file mode 100644 index 0000000..eef5927 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241216181527_privy_logins_add_connected_wallet/migration.sql @@ -0,0 +1,8 @@ +-- Drop all data before adding a new required column +TRUNCATE "privy_logins" RESTART IDENTITY CASCADE; + +-- AlterTable +ALTER TABLE "privy_logins" ADD COLUMN "connectedWallet" CITEXT NOT NULL; + +-- CreateIndex +CREATE INDEX "privy_logins_connectedWallet_idx" ON "privy_logins"("connectedWallet"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241216233342_attestation_evidence/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241216233342_attestation_evidence/migration.sql new file mode 100644 index 0000000..1a19318 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241216233342_attestation_evidence/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `evidence` to the `attestations` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "attestations" ADD COLUMN "evidence" TEXT NULL; + +UPDATE "attestations" SET "evidence" = ''; + +ALTER TABLE "attestations" ALTER COLUMN "evidence" SET NOT NULL; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241218001450_update_user_fcm_token/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241218001450_update_user_fcm_token/migration.sql new file mode 100644 index 0000000..4bedf56 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241218001450_update_user_fcm_token/migration.sql @@ -0,0 +1,17 @@ +/* + Warnings: + + - A unique constraint covering the columns `[profileId,deviceIdentifier]` on the table `user_fcm_tokens` will be added. If there are existing duplicate values, this will fail. + - Made the column `deviceIdentifier` on table `user_fcm_tokens` required. This step will fail if there are existing NULL values in that column. + - Made the column `userAgent` on table `user_fcm_tokens` required. This step will fail if there are existing NULL values in that column. + +*/ +-- DropIndex +DROP INDEX "user_fcm_tokens_profileId_fcmToken_key"; + +-- AlterTable +ALTER TABLE "user_fcm_tokens" ALTER COLUMN "deviceIdentifier" SET NOT NULL, +ALTER COLUMN "userAgent" SET NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "user_fcm_tokens_profileId_deviceIdentifier_key" ON "user_fcm_tokens"("profileId", "deviceIdentifier"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20241220160949_reset_market_data/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241220160949_reset_market_data/migration.sql new file mode 100644 index 0000000..dd86f09 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241220160949_reset_market_data/migration.sql @@ -0,0 +1,2 @@ +-- New markets smart contract reset. +TRUNCATE TABLE markets RESTART IDENTITY CASCADE; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241223175125_denormalized_market_stats/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241223175125_denormalized_market_stats/migration.sql new file mode 100644 index 0000000..1769000 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241223175125_denormalized_market_stats/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "markets" ADD COLUMN "marketCapWei" TEXT NOT NULL DEFAULT '0', +ADD COLUMN "priceChange24hrPercent" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "volume24hrWei" TEXT NOT NULL DEFAULT '0', +ADD COLUMN "volumeTotalWei" TEXT NOT NULL DEFAULT '0'; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241224050101_market_config_columns/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241224050101_market_config_columns/migration.sql new file mode 100644 index 0000000..da48ed8 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241224050101_market_config_columns/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "markets" ADD COLUMN "basePrice" TEXT NOT NULL DEFAULT '0', +ADD COLUMN "creationCost" TEXT NOT NULL DEFAULT '0'; diff --git a/ethos/services/echo/prisma/postgres/migrations/20241231175124_add_claims_and_referrals/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20241231175124_add_claims_and_referrals/migration.sql new file mode 100644 index 0000000..3022e7c --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20241231175124_add_claims_and_referrals/migration.sql @@ -0,0 +1,34 @@ +-- CreateTable +CREATE TABLE "claims" ( + "twitterUserId" TEXT NOT NULL, + "initialAmount" INTEGER NOT NULL, + "claimed" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "claimedAt" TIMESTAMP(3), + + CONSTRAINT "claims_pkey" PRIMARY KEY ("twitterUserId") +); + +-- CreateTable +CREATE TABLE "claim_referrals" ( + "id" SERIAL NOT NULL, + "fromTwitterUserId" TEXT NOT NULL, + "toTwitterUserId" TEXT NOT NULL, + "bonusAmountForSender" INTEGER NOT NULL, + "bonusAmountForReceiver" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "claim_referrals_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "claim_referrals_fromTwitterUserId_idx" ON "claim_referrals"("fromTwitterUserId"); + +-- CreateIndex +CREATE INDEX "claim_referrals_toTwitterUserId_idx" ON "claim_referrals"("toTwitterUserId"); + +-- CreateIndex +CREATE UNIQUE INDEX "claim_referrals_fromTwitterUserId_toTwitterUserId_key" ON "claim_referrals"("fromTwitterUserId", "toTwitterUserId"); + +-- AddForeignKey +ALTER TABLE "claim_referrals" ADD CONSTRAINT "claim_referrals_fromTwitterUserId_fkey" FOREIGN KEY ("fromTwitterUserId") REFERENCES "claims"("twitterUserId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/ethos/services/echo/prisma/postgres/migrations/20250103064604_twitter_attestation/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20250103064604_twitter_attestation/migration.sql new file mode 100644 index 0000000..3eb0341 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20250103064604_twitter_attestation/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - Added the required column `attestationHash` to the `twitter_profiles_cache` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +TRUNCATE TABLE "twitter_profiles_cache"; +ALTER TABLE "twitter_profiles_cache" ADD COLUMN "attestationHash" CHAR(66) NOT NULL; + +-- AlterTable +ALTER TABLE "vouches" ADD COLUMN "attestationHash" CHAR(66), +ADD COLUMN "subjectAddress" VARCHAR(42); + +-- CreateIndex +CREATE INDEX "twitter_profiles_cache_attestationHash_idx" ON "twitter_profiles_cache"("attestationHash"); + +-- CreateIndex +CREATE INDEX "vouches_subjectAddress_idx" ON "vouches"("subjectAddress"); + +-- CreateIndex +CREATE INDEX "vouches_attestationHash_idx" ON "vouches"("attestationHash"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20250103121212_new_vouch_contract/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20250103121212_new_vouch_contract/migration.sql new file mode 100644 index 0000000..e09ef17 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20250103121212_new_vouch_contract/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +TRUNCATE TABLE vouches CASCADE; +TRUNCATE TABLE vouch_events CASCADE; +DELETE FROM blockchain_event_polls WHERE contract='vouch'; +DELETE FROM blockchain_events WHERE contract='vouch'; diff --git a/ethos/services/echo/prisma/postgres/migrations/20250103201422_make_xp_points_history_profileless/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20250103201422_make_xp_points_history_profileless/migration.sql new file mode 100644 index 0000000..2e589d7 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20250103201422_make_xp_points_history_profileless/migration.sql @@ -0,0 +1,28 @@ +/* + Warnings: + + - You are about to drop the column `profileId` on the `xp_points_history` table. All the data in the column will be lost. +*/ + +-- AlterTable +ALTER TABLE "xp_points_history" ADD COLUMN "userkey" CITEXT; + +-- Custom SQL: convert profileId value to userkey and set it to a new column +UPDATE "xp_points_history" xph +SET userkey = 'profileId:' || xph."profileId"::TEXT; + +-- AlterEnum +ALTER TYPE "XpPointsHistoryItemType" ADD VALUE 'CLAIM'; + +-- DropForeignKey +ALTER TABLE "xp_points_history" DROP CONSTRAINT "xp_points_history_profileId_fkey"; + +-- DropIndex +DROP INDEX "xp_points_history_profileId_idx"; + +-- AlterTable +ALTER TABLE "xp_points_history" DROP COLUMN "profileId", +ALTER COLUMN "userkey" SET NOT NULL; + +-- CreateIndex +CREATE INDEX "xp_points_history_userkey_idx" ON "xp_points_history"("userkey"); diff --git a/ethos/services/echo/prisma/postgres/migrations/20250104001422_case_insensitive_tx_hash/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20250104001422_case_insensitive_tx_hash/migration.sql new file mode 100644 index 0000000..678a058 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20250104001422_case_insensitive_tx_hash/migration.sql @@ -0,0 +1,71 @@ +-- Custom SQL + +-- Change txHash in blockchain_events to be case insensitive + +-- Create a temporary column to store the txHash values in cocrrect type +ALTER TABLE "blockchain_events" +ADD COLUMN "txHash_temp" CITEXT; + +-- Copy the values from txHash to txHash_temp +UPDATE "blockchain_events" +SET "txHash_temp" = "txHash"; + +-- Drop the old txHash column and create a new one with the correct type +ALTER TABLE "blockchain_events" +DROP COLUMN "txHash", +ADD COLUMN "txHash" CITEXT; + +-- Copy the values from txHash_temp to txHash +UPDATE "blockchain_events" +SET "txHash" = "txHash_temp"; + +-- Drop the temporary column +ALTER TABLE "blockchain_events" +DROP COLUMN "txHash_temp", +ALTER COLUMN "txHash" SET NOT NULL; + + +-- Change txHash in score_history to be case insensitive + +-- Create a temporary column to store the txHash values in cocrrect type +ALTER TABLE "score_history" +ADD COLUMN "txHash_temp" CITEXT; + +-- Copy the values from txHash to txHash_temp +UPDATE "score_history" +SET "txHash_temp" = "txHash"; + +-- Drop the old txHash column and create a new one with the correct type +ALTER TABLE "score_history" +DROP COLUMN "txHash", +ADD COLUMN "txHash" CITEXT; + +-- Copy the values from txHash_temp to txHash +UPDATE "score_history" +SET "txHash" = "txHash_temp"; + +-- Drop the temporary column +ALTER TABLE "score_history" +DROP COLUMN "txHash_temp"; + + +-- Create missing indices +-- CreateIndex +CREATE INDEX "blockchain_events_txHash_idx" ON "blockchain_events"("txHash"); + +-- CreateIndex +CREATE UNIQUE INDEX "blockchain_events_blockNumber_blockIndex_txHash_key" ON "blockchain_events"("blockNumber", "blockIndex", "txHash"); + +-- CreateIndex +CREATE INDEX "score_history_target_txHash_idx" ON "score_history"("target", "txHash"); + + +-- Make userkeys case insensitive +-- AlterTable +ALTER TABLE "contribution_score_checks" ALTER COLUMN "targetUserkey" SET DATA TYPE CITEXT; + +-- AlterTable +ALTER TABLE "contribution_trust_checks" ALTER COLUMN "targetUserkey" SET DATA TYPE CITEXT; + +-- AlterTable +ALTER TABLE "score_history" ALTER COLUMN "target" SET DATA TYPE CITEXT; diff --git a/ethos/services/echo/prisma/postgres/migrations/20250106163149_add_claim_referral_type/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20250106163149_add_claim_referral_type/migration.sql new file mode 100644 index 0000000..888a4ab --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20250106163149_add_claim_referral_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "XpPointsHistoryItemType" ADD VALUE 'CLAIM_REFERRAL'; diff --git a/ethos/services/echo/prisma/postgres/migrations/20250107065423_add_extension_checkin/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20250107065423_add_extension_checkin/migration.sql new file mode 100644 index 0000000..1881557 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20250107065423_add_extension_checkin/migration.sql @@ -0,0 +1,11 @@ +-- First transaction: Add the new enum value +BEGIN; +-- AlterEnum +ALTER TYPE "XpPointsHistoryItemType" ADD VALUE 'EXTENSION_CHECK_IN'; +COMMIT; + +-- Second transaction: Create the index +BEGIN; +-- CreateIndex +CREATE UNIQUE INDEX "xp_points_history_daily_checkin_idx" ON "xp_points_history" ("userkey", "type", DATE("createdAt")) WHERE type = 'EXTENSION_CHECK_IN'; +COMMIT; diff --git a/ethos/services/echo/prisma/postgres/migrations/20250107200551_base_credibility/migration.sql b/ethos/services/echo/prisma/postgres/migrations/20250107200551_base_credibility/migration.sql new file mode 100644 index 0000000..1e33e81 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/20250107200551_base_credibility/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "offchain_score" ( + "id" SERIAL NOT NULL, + "userkey" CITEXT NOT NULL, + "score" INTEGER NOT NULL, + "metadata" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "offchain_score_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "offchain_score_userkey_idx" ON "offchain_score"("userkey"); diff --git a/ethos/services/echo/prisma/postgres/migrations/migration_lock.toml b/ethos/services/echo/prisma/postgres/migrations/migration_lock.toml new file mode 100644 index 0000000..648c57f --- /dev/null +++ b/ethos/services/echo/prisma/postgres/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" \ No newline at end of file diff --git a/ethos/services/echo/prisma/postgres/schema.prisma b/ethos/services/echo/prisma/postgres/schema.prisma new file mode 100644 index 0000000..f518a51 --- /dev/null +++ b/ethos/services/echo/prisma/postgres/schema.prisma @@ -0,0 +1,751 @@ +generator client { + provider = "prisma-client-js" + output = "../../../../node_modules/@prisma-pg/client" + // Make sure to generate Prisma client for Alpine correctly + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] + previewFeatures = ["metrics", "relationJoins", "views"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Profile { + id Int @id + archived Boolean @default(false) + createdAt DateTime + updatedAt DateTime @updatedAt + invitesSent String[] + invitesAcceptedIds Int[] + invitesAvailable Int + invitedBy Int + ProfileEvent ProfileEvent[] + Vouch Vouch[] + ProfileAddress ProfileAddress[] + Attestation Attestation[] + SentInvitations Invitation[] @relation("sent") + Invitation Invitation[] + ReputationMarket Market? + ContributionBundle ContributionBundle[] + UserFcmToken UserFcmToken[] + Rewards Rewards[] + + @@index([archived]) + @@map("profiles") +} + +model ProfileAddress { + id Int @id @default(autoincrement()) + profileId Int + profile Profile @relation(fields: [profileId], references: [id], onUpdate: Cascade, onDelete: Cascade) + address String @unique @db.Citext // case insensitive + + @@index([profileId]) + @@map("profile_addresses") +} + +enum InvitationStatus { + PENDING + ACCEPTED + DECLINED +} + +enum Contract { + PROFILE @map("profile") + VOUCH @map("vouch") + REVIEW @map("review") + ATTESTATION @map("attestation") + DISCUSSION @map("discussion") +} + +model Invitation { + id Int @id @default(autoincrement()) + senderProfileId Int + sender Profile @relation("sent", fields: [senderProfileId], references: [id], onUpdate: Cascade, onDelete: Cascade) + recipient String @db.VarChar(42) + sentAt DateTime + acceptedProfileId Int? + acceptedProfile Profile? @relation(fields: [acceptedProfileId], references: [id], onUpdate: Cascade, onDelete: Cascade) + status InvitationStatus @default(PENDING) + statusUpdatedAt DateTime? + + @@index([senderProfileId, status]) + @@index([acceptedProfileId]) + @@index([recipient]) + @@map("invitations") +} + +model Review { + id Int @id + archived Boolean + author String @db.VarChar(42) + authorProfileId Int + subject String @db.VarChar(42) + score Int + comment String + metadata String + createdAt DateTime + updatedAt DateTime @updatedAt + account String + service String + ReviewEvent ReviewEvent[] + ContributionReview ContributionReview[] + ContributionReviewCheck ContributionReviewCheck[] + ContributionReviewVote ContributionReviewVote[] + + @@index([service, account]) + @@index([archived]) + @@index([author]) + @@index([subject, score]) + @@map("reviews") +} + +model Vouch { + id Int @id + archived Boolean + unhealthy Boolean + authorAddress String @db.VarChar(42) + authorProfileId Int + subjectProfileId Int + subjectProfile Profile @relation(fields: [subjectProfileId], references: [id], onUpdate: Cascade, onDelete: Cascade) + subjectAddress String? @db.VarChar(42) + attestationHash String? @db.Char(66) + deposited Decimal @db.Decimal(78, 0) + staked Decimal @db.Decimal(78, 0) + balance Decimal @db.Decimal(78, 0) + withdrawn Decimal @default(0) @db.Decimal(78, 0) + mutualVouchId Int? @unique + mutualVouch Vouch? @relation("mutualVouch", fields: [mutualVouchId], references: [id], onUpdate: Cascade, onDelete: Cascade) + mutualVouchRef Vouch? @relation("mutualVouch") // links to itself for mutual vouch lookups + comment String + metadata String + vouchedAt DateTime + updatedAt DateTime @updatedAt + unvouchedAt DateTime? + unhealthyAt DateTime? + VouchEvent VouchEvent[] + + @@index([archived]) + @@index([authorProfileId]) + @@index([subjectProfileId]) + @@index([subjectAddress]) + @@index([attestationHash]) + @@map("vouches") +} + +model Rewards { + id Int @id @default(autoincrement()) + profileId Int + profile Profile @relation(fields: [profileId], references: [id], onUpdate: Cascade, onDelete: Cascade) + balance Decimal @db.Decimal(78, 0) + lifetime Decimal @db.Decimal(78, 0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([profileId]) + @@index([createdAt]) + @@map("rewards") +} + +model Reply { + id Int @id + parentId Int + targetContract String @db.VarChar(42) + authorProfileId Int + createdAt DateTime + updatedAt DateTime @updatedAt + content String + metadata String + parentIsOriginalComment Boolean + contract Contract? + ReplyEvent ReplyEvent[] + + @@index([targetContract, parentId]) + @@index([contract, parentId]) + @@map("replies") +} + +model Vote { + id Int @id + isUpvote Boolean + isArchived Boolean + voter Int + targetContract String @db.VarChar(42) + targetId Int + createdAt DateTime + updatedAt DateTime @updatedAt + contract Contract? + VoteEvent VoteEvent[] + ContributionReviewVote ContributionReviewVote[] + + @@index([voter]) + @@index([targetContract, targetId, isArchived]) + @@index([targetId, isUpvote]) + @@index([contract, targetId]) + @@map("votes") +} + +model Attestation { + id Int @id + hash String @unique + archived Boolean + evidence String + profileId Int + profile Profile @relation(fields: [profileId], references: [id], onUpdate: Cascade, onDelete: Cascade) + createdAt DateTime + updatedAt DateTime @updatedAt + account String + service String + AttestationEvent AttestationEvent[] + + @@index([profileId]) + @@index([service, account, archived]) + @@map("attestations") +} + +model BlockchainEvent { + id Int @id @default(autoincrement()) + contract String + logData Json @db.JsonB + blockNumber Int + blockIndex Int + createdAt DateTime + updatedAt DateTime @updatedAt + txHash String @db.Citext + processed Boolean @default(false) + jobCreated Boolean @default(false) + AttestationEvent AttestationEvent[] + VoteEvent VoteEvent[] + ReplyEvent ReplyEvent[] + VouchEvent VouchEvent[] + ReviewEvent ReviewEvent[] + ProfileEvent ProfileEvent[] + MarketEvent MarketUpdatedEvent[] + MarketVoteEvent MarketVoteEvent[] + + @@unique([blockNumber, blockIndex, txHash]) + @@index([contract]) + @@index([txHash]) + @@map("blockchain_events") +} + +model BlockchainEventPoll { + contract String + lastBlockNumber Int + updatedAt DateTime @updatedAt + + @@unique([contract]) + @@map("blockchain_event_polls") +} + +model ProfileEvent { + eventId Int + event BlockchainEvent @relation(fields: [eventId], references: [id], onUpdate: Cascade, onDelete: Cascade) + profileId Int + profile Profile @relation(fields: [profileId], references: [id], onUpdate: Cascade, onDelete: Cascade) + type ProfileEventType? + + @@unique([eventId, profileId]) + @@map("profile_events") +} + +enum ProfileEventType { + CREATE @map("create") + ARCHIVE @map("archive") + RESTORE @map("restore") + INVITE @map("invite") + UNINVITE @map("uninvite") +} + +model ReviewEvent { + eventId Int + event BlockchainEvent @relation(fields: [eventId], references: [id], onUpdate: Cascade, onDelete: Cascade) + reviewId Int + review Review @relation(fields: [reviewId], references: [id], onUpdate: Cascade, onDelete: Cascade) + type ReviewEventType? + + @@unique([eventId, reviewId]) + @@map("review_events") +} + +enum ReviewEventType { + CREATE @map("create") + EDIT @map("edit") + ARCHIVE @map("archive") + RESTORE @map("restore") +} + +model VouchEvent { + eventId Int + event BlockchainEvent @relation(fields: [eventId], references: [id], onUpdate: Cascade, onDelete: Cascade) + vouchId Int + vouch Vouch @relation(fields: [vouchId], references: [id], onUpdate: Cascade, onDelete: Cascade) + type VouchEventType? + + @@unique([eventId, vouchId]) + @@map("vouch_events") +} + +enum VouchEventType { + CREATE @map("create") + UNVOUCH @map("unvouch") + UNHEALTHY @map("unhealthy") + INCREASE @map("increase") + DEPOSIT_REWARDS @map("deposit_rewards") + WITHDRAW_REWARDS @map("withdraw_rewards") + SLASH @map("slash") +} + +model ReplyEvent { + eventId Int + event BlockchainEvent @relation(fields: [eventId], references: [id], onUpdate: Cascade, onDelete: Cascade) + replyId Int + reply Reply @relation(fields: [replyId], references: [id], onUpdate: Cascade, onDelete: Cascade) + type ReplyEventType? + + @@unique([eventId, replyId]) + @@map("reply_events") +} + +enum ReplyEventType { + CREATE @map("create") + EDIT @map("edit") +} + +model AttestationEvent { + eventId Int + event BlockchainEvent @relation(fields: [eventId], references: [id], onUpdate: Cascade, onDelete: Cascade) + attestationId Int + attestation Attestation @relation(fields: [attestationId], references: [id], onUpdate: Cascade, onDelete: Cascade) + type AttestationEventType? + + @@unique([eventId, attestationId]) + @@map("attestation_events") +} + +enum AttestationEventType { + CREATE @map("create") + ARCHIVE @map("archive") + RESTORE @map("restore") + CLAIM @map("claim") +} + +model VoteEvent { + eventId Int + event BlockchainEvent @relation(fields: [eventId], references: [id], onUpdate: Cascade, onDelete: Cascade) + voteId Int + vote Vote @relation(fields: [voteId], references: [id], onUpdate: Cascade, onDelete: Cascade) + type VoteEventType? + + @@unique([eventId, voteId]) + @@map("vote_events") +} + +enum VoteEventType { + CREATE @map("create") + ARCHIVE @map("archive") + UPDATE @map("update") +} + +model ScoreHistory { + id Int @id @default(autoincrement()) + target String @db.Citext + score Int + createdAt DateTime @default(now()) + txHash String? @db.Citext + dirty Boolean + ScoreHistoryElement ScoreHistoryElement[] + + @@index([target, dirty]) + @@index([target, txHash]) + @@index([score]) + @@index([createdAt]) + @@index([target, createdAt(sort: Desc), score(sort: Desc)]) + @@map("score_history") +} + +model ScoreHistoryElement { + scoreHistoryId Int + scoreHistory ScoreHistory @relation(fields: [scoreHistoryId], references: [id]) + scoreElementId Int + scoreElement ScoreElementRecord @relation(fields: [scoreElementId], references: [id]) + + @@id([scoreHistoryId, scoreElementId]) + @@map("score_history_elements") +} + +model ScoreAlgorithm { + version Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + definition Json + ScoreElementDefinition ScoreElementDefinition[] + + @@index([version]) + @@index([createdAt]) + @@map("score_algorithms") +} + +model ScoreElementDefinition { + name String + type String + min Int + max Int + ranges Json? + outOfRangeScore Int? + scoreAlgorithmVersion Int + scoreAlgorithm ScoreAlgorithm @relation(fields: [scoreAlgorithmVersion], references: [version]) + ScoreElementRecord ScoreElementRecord[] + + @@unique([name, scoreAlgorithmVersion]) + @@index([scoreAlgorithmVersion]) + @@map("score_element_definitions") +} + +model ScoreElementRecord { + id Int @id @default(autoincrement()) + name String + version Int + raw Int + weighted Int + error Boolean + metadata Json? + ScoreElementDefinition ScoreElementDefinition @relation(fields: [name, version], references: [name, scoreAlgorithmVersion]) + ScoreHistoryElement ScoreHistoryElement[] + + @@index([version, name]) + @@map("score_element_records") +} + +model EnsCache { + id Int @id @default(autoincrement()) + address String @unique @db.Citext // case insensitive + ensName String? @db.Citext // case insensitive + avatarUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([address]) + @@index([ensName]) + @@map("ens_cache") +} + +model TwitterProfileCache { + id String @id + username String @db.Citext // case insensitive + name String + avatar String? + biography String? + website String? + followersCount Int? + joinedAt DateTime? + isBlueVerified Boolean? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + attestationHash String @db.Char(66) + + @@index([username]) + @@index([updatedAt]) + @@index([attestationHash]) + @@map("twitter_profiles_cache") +} + +model AddressHistoryCache { + address String @id @db.Citext // case insensitive + firstTransaction DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([address, updatedAt]) + @@map("address_history_cache") +} + +model TransactionHistoryCache { + fromAddress String @db.Citext // case insensitive + toAddress String @db.Citext + hash String @unique @db.Citext + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + value String + blockNumber Int + blockTimestamp DateTime + fromAddressLabel String? + fromAddressLogo String? + toAddressLabel String? + toAddressLogo String? + category String? + summary String? + + @@index([updatedAt, fromAddress]) + @@index([updatedAt, toAddress]) + @@index([fromAddress]) + @@index([toAddress]) + @@index([blockNumber]) + @@map("transaction_history_cache") +} + +model Market { + profileId Int @id + profile Profile @relation(fields: [profileId], references: [id]) + creatorAddress String @db.VarChar(42) + positivePrice String + negativePrice String + trustVotes Int + distrustVotes Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ReputationMarketEvent MarketUpdatedEvent[] + MarketVoteEvent MarketVoteEvent[] + marketCapWei String @default("0") + volumeTotalWei String @default("0") + volume24hrWei String @default("0") + priceChange24hrPercent Int @default(0) + basePrice String @default("0") + creationCost String @default("0") + + @@map("markets") +} + +model MarketUpdatedEvent { + eventId Int + event BlockchainEvent @relation(fields: [eventId], references: [id]) + marketProfileId Int + market Market @relation(fields: [marketProfileId], references: [profileId]) + createdAt DateTime @default(now()) + positivePrice String + negativePrice String + deltaVoteTrust Int + deltaVoteDistrust Int + deltaPositivePrice String + deltaNegativePrice String + blockNumber Int + + @@unique([eventId, marketProfileId]) + @@index([marketProfileId, createdAt]) + @@map("market_events") +} + +enum MarketVoteEventType { + BUY + SELL +} + +model MarketVoteEvent { + eventId Int + type MarketVoteEventType + actorAddress String @db.VarChar(42) + event BlockchainEvent @relation(fields: [eventId], references: [id]) + marketProfileId Int + market Market @relation(fields: [marketProfileId], references: [profileId]) + isPositive Boolean + amount Int + funds String + createdAt DateTime @default(now()) + + @@unique([eventId, marketProfileId]) + @@index([marketProfileId, createdAt]) + @@index([actorAddress, createdAt]) + @@map("market_vote_events") +} + +enum ContributionType { + REVIEW + TRUST_BATTLE + TRUST_CHECK + REVIEW_CHECK + REVIEW_VOTE + SCORE_CHECK +} + +enum ContributionStatus { + PENDING + COMPLETED + SKIPPED +} + +enum ContributionAnswer { + POSITIVE + NEGATIVE + NEUTRAL + UNSURE +} + +model ContributionBundle { + id Int @id @default(autoincrement()) + profileId Int + Profile Profile @relation(fields: [profileId], references: [id], onUpdate: Cascade, onDelete: Cascade) + expireAt DateTime + createdAt DateTime @default(now()) + Contribution Contribution[] + + @@index([profileId]) + @@map("contribution_bundles") +} + +model Contribution { + id Int @id @default(autoincrement()) + contributionBundleId Int + ContributionBundle ContributionBundle @relation(fields: [contributionBundleId], references: [id], onUpdate: Cascade, onDelete: Cascade) + type ContributionType + experience Float + status ContributionStatus + ContributionReview ContributionReview? + ContributionTrustBattle ContributionTrustBattle? + ContributionTrustCheck ContributionTrustCheck? + ContributionScoreCheck ContributionScoreCheck? + ContributionReviewCheck ContributionReviewCheck? + ContributionReviewVote ContributionReviewVote? + + @@index([contributionBundleId]) + @@index([status]) + @@map("contributions") +} + +model ContributionTrustCheck { + contributionId Int @id + Contribution Contribution @relation(fields: [contributionId], references: [id], onUpdate: Cascade, onDelete: Cascade) + targetUserkey String @db.Citext + answer ContributionAnswer? + + @@map("contribution_trust_checks") +} + +model ContributionReview { + contributionId Int @id + Contribution Contribution @relation(fields: [contributionId], references: [id], onUpdate: Cascade, onDelete: Cascade) + targetUserkeys String[] + reviewId Int? + review Review? @relation(fields: [reviewId], references: [id], onUpdate: Cascade, onDelete: Cascade) + + @@map("contribution_reviews") +} + +model ContributionTrustBattle { + contributionId Int @id + Contribution Contribution @relation(fields: [contributionId], references: [id], onUpdate: Cascade, onDelete: Cascade) + targetUserkeys String[] + chosenIndex Int? + + @@map("contribution_trust_battles") +} + +model ContributionScoreCheck { + contributionId Int @id + Contribution Contribution @relation(fields: [contributionId], references: [id], onUpdate: Cascade, onDelete: Cascade) + targetUserkey String @db.Citext + answer ContributionAnswer? + + @@map("contribution_score_checks") +} + +model ContributionReviewCheck { + contributionId Int @id + Contribution Contribution @relation(fields: [contributionId], references: [id], onUpdate: Cascade, onDelete: Cascade) + reviewId Int + review Review @relation(fields: [reviewId], references: [id], onUpdate: Cascade, onDelete: Cascade) + answer ContributionAnswer? + + @@map("contribution_review_checks") +} + +model ContributionReviewVote { + contributionId Int @id + Contribution Contribution @relation(fields: [contributionId], references: [id], onUpdate: Cascade, onDelete: Cascade) + reviewId Int + review Review @relation(fields: [reviewId], references: [id], onUpdate: Cascade, onDelete: Cascade) + voteId Int? + vote Vote? @relation(fields: [voteId], references: [id], onUpdate: Cascade, onDelete: Cascade) + + @@map("contribution_review_votes") +} + +model UserFcmToken { + id Int @id @default(autoincrement()) + profileId Int + profile Profile @relation(fields: [profileId], references: [id], onUpdate: Cascade, onDelete: Cascade) + fcmToken String + deviceIdentifier String + userAgent String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([profileId, deviceIdentifier]) + @@index([profileId]) + @@map("user_fcm_tokens") +} + +enum XpPointsHistoryItemType { + REVIEW + CONTRIBUTION + INVITE_ACCEPTED + VOUCH_DAY + CLAIM + EXTENSION_CHECK_IN + CLAIM_REFERRAL +} + +model XpPointsHistory { + id Int @id @default(autoincrement()) + userkey String @db.Citext + type XpPointsHistoryItemType + points Int + metadata Json @db.JsonB + createdAt DateTime + + @@index([userkey]) + @@index([createdAt]) + @@map("xp_points_history") +} + +model Claim { + twitterUserId String @id + initialAmount Int + claimed Boolean @default(false) + createdAt DateTime @default(now()) + claimedAt DateTime? + ClaimReferral ClaimReferral[] + + @@map("claims") +} + +model ClaimReferral { + id Int @id @default(autoincrement()) + fromTwitterUserId String + ReferrerTwitterUserClaim Claim @relation(fields: [fromTwitterUserId], references: [twitterUserId], onUpdate: Cascade, onDelete: Cascade) + toTwitterUserId String + bonusAmountForSender Int + bonusAmountForReceiver Int + createdAt DateTime @default(now()) + + @@unique([fromTwitterUserId, toTwitterUserId]) + @@index([fromTwitterUserId]) + @@index([toTwitterUserId]) + @@map("claim_referrals") +} + +model PrivyLogin { + id String @id + twitterUserId String? @unique + connectedWallet String @db.Citext + embeddedWallet String @db.Citext + smartWallet String @db.Citext + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([twitterUserId]) + @@index([connectedWallet]) + @@index([smartWallet]) + @@map("privy_logins") +} + +model offchainScore { + id Int @id @default(autoincrement()) + userkey String @db.Citext + score Int + metadata Json @db.JsonB + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userkey]) + @@map("offchain_score") +} diff --git a/ethos/services/echo/prisma/timescale/migrations/20241124195232_init/migration.sql b/ethos/services/echo/prisma/timescale/migrations/20241124195232_init/migration.sql new file mode 100644 index 0000000..2613d72 --- /dev/null +++ b/ethos/services/echo/prisma/timescale/migrations/20241124195232_init/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +-- This will fail on the first attempt of `prisma migrate reset`. +-- https://github.com/prisma/prisma/issues/8325 +-- Rerunning will succeed. +CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE; + +CREATE TABLE "market_prices" ( + "marketProfileId" INTEGER NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "trustPrice" DECIMAL(78,0) NOT NULL, + "distrustPrice" DECIMAL(78,0) NOT NULL, + "deltaTrustPrice" DECIMAL(78,0) NOT NULL, + "deltaDistrustPrice" DECIMAL(78,0) NOT NULL, + + CONSTRAINT "market_prices_pkey" PRIMARY KEY ("marketProfileId","createdAt") +); + + +SELECT create_hypertable('market_prices', 'createdAt', 'marketProfileId', + number_partitions => 4 ); diff --git a/ethos/services/echo/prisma/timescale/migrations/20241201064843_vote_events/migration.sql b/ethos/services/echo/prisma/timescale/migrations/20241201064843_vote_events/migration.sql new file mode 100644 index 0000000..91a2bbc --- /dev/null +++ b/ethos/services/echo/prisma/timescale/migrations/20241201064843_vote_events/migration.sql @@ -0,0 +1,20 @@ +-- CreateEnum +CREATE TYPE "EventType" AS ENUM ('BUY', 'SELL'); + +-- CreateEnum +CREATE TYPE "VoteType" AS ENUM ('TRUST', 'DISTRUST'); + +-- CreateTable +CREATE TABLE "market_votes" ( + "marketProfileId" INTEGER NOT NULL, + "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "voteType" "VoteType" NOT NULL, + "amount" INTEGER NOT NULL, + "funds" DECIMAL(78,0) NOT NULL, + "eventType" "EventType" NOT NULL, + + CONSTRAINT "market_votes_pkey" PRIMARY KEY ("marketProfileId","createdAt") +); + +-- CreateIndex +CREATE INDEX "market_votes_createdAt_idx" ON "market_votes"("createdAt" DESC); diff --git a/ethos/services/echo/prisma/timescale/migrations/20241220161009_reset_market_data/migration.sql b/ethos/services/echo/prisma/timescale/migrations/20241220161009_reset_market_data/migration.sql new file mode 100644 index 0000000..aa8c65e --- /dev/null +++ b/ethos/services/echo/prisma/timescale/migrations/20241220161009_reset_market_data/migration.sql @@ -0,0 +1,3 @@ +-- New markets smart contract reset. +TRUNCATE TABLE market_prices RESTART IDENTITY CASCADE; +TRUNCATE TABLE market_votes RESTART IDENTITY CASCADE; diff --git a/ethos/services/echo/prisma/timescale/migrations/migration_lock.toml b/ethos/services/echo/prisma/timescale/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/ethos/services/echo/prisma/timescale/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/ethos/services/echo/prisma/timescale/schema.prisma b/ethos/services/echo/prisma/timescale/schema.prisma new file mode 100644 index 0000000..672a934 --- /dev/null +++ b/ethos/services/echo/prisma/timescale/schema.prisma @@ -0,0 +1,49 @@ +generator client { + provider = "prisma-client-js" + output = "../../../../node_modules/@prisma-timescale/client" + // Make sure to generate Prisma client for Alpine correctly + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] +} + +datasource db { + provider = "postgresql" + url = env("TIMESCALE_DB_URL") +} + +model MarketPrices { + marketProfileId Int + createdAt DateTime @default(now()) @db.Timestamptz(3) + trustPrice Decimal @db.Decimal(78, 0) + distrustPrice Decimal @db.Decimal(78, 0) + deltaTrustPrice Decimal @db.Decimal(78, 0) + deltaDistrustPrice Decimal @db.Decimal(78, 0) + + @@id([marketProfileId, createdAt]) + // Index for the timescaledb hypertable. Mirroring here to prevent prisma from dropping it after a migration reset. + @@index([createdAt(sort: Desc)], map: "market_prices_createdAt_idx") + @@map("market_prices") +} + +enum EventType { + BUY + SELL +} + +enum VoteType { + TRUST + DISTRUST +} + +model MarketVotes { + marketProfileId Int + createdAt DateTime @default(now()) @db.Timestamptz(3) + voteType VoteType + amount Int + funds Decimal @db.Decimal(78, 0) + eventType EventType + + @@id([marketProfileId, createdAt]) + // Index for the timescaledb hypertable. Mirroring here to prevent prisma from dropping it after a migration reset. + @@index([createdAt(sort: Desc)], map: "market_votes_createdAt_idx") + @@map("market_votes") +} diff --git a/ethos/services/echo/prometheus.yml b/ethos/services/echo/prometheus.yml new file mode 100644 index 0000000..e4a6f8a --- /dev/null +++ b/ethos/services/echo/prometheus.yml @@ -0,0 +1,5 @@ +scrape_configs: + - job_name: 'services/echo' + scrape_interval: 10s + static_configs: + - targets: ['host.docker.internal:9091'] diff --git a/ethos/services/echo/src/app.ts b/ethos/services/echo/src/app.ts new file mode 100644 index 0000000..6a4618a --- /dev/null +++ b/ethos/services/echo/src/app.ts @@ -0,0 +1,24 @@ +import express, { type Express } from 'express'; +import { rootLogger as logger } from './common/logger.js'; +import { initStatsig } from './common/statsig.js'; +import { initErrorHandler } from './middlewares/error-handler.js'; +import { initMiddlewares } from './middlewares/index.js'; +import { initRoutes } from './routes/routes.js'; + +export function initApp(): Express { + const app = express(); + + initStatsig() + .then(() => { + // Initialize middlewares, routes, and error handler only after Statsig is + // initialized as some middlewares and routes depend on Statsig + initMiddlewares(app); + initRoutes(app); + initErrorHandler(app); + }) + .catch((err) => { + logger.error({ err }, 'Failed to initialize Statsig'); + }); + + return app; +} diff --git a/ethos/services/echo/src/bootstrap.all.ts b/ethos/services/echo/src/bootstrap.all.ts new file mode 100644 index 0000000..18e12a8 --- /dev/null +++ b/ethos/services/echo/src/bootstrap.all.ts @@ -0,0 +1,117 @@ +import cluster from 'node:cluster'; +import { cpus } from 'node:os'; +import { duration } from '@ethos/helpers'; +import { startEventProcessingWorker } from './bootstrap.events.js'; +import { startHttpApiWorker } from './bootstrap.http-api.js'; +import { config } from './common/config.js'; +import { rootLogger as logger } from './common/logger.js'; +import { processInternalEthosJobs } from './contract-events/message-queue.js'; +import { checkLatestScoreVersion } from './data/score/version.js'; +import { + startInternalEventsDailyJob, + startInternalEventsHourlyJob, +} from './internal-events/index.js'; +import { startMetricsServer } from './metrics-server.js'; + +const WORKERS_HEALTH_CHECK_INTERVAL = duration(10, 'seconds').toMilliseconds(); + +// Run the server in the same way locally and on prod. But locally spawn only a +// single worker so we don't use all resources. +const numWorkers = Math.max(config.NODE_ENV === 'production' ? cpus().length : 1, 2); +let eventsWorkerId: number; + +function createFork(workerType: typeof config.WORKER_TYPE): ReturnType { + const worker = cluster.fork({ WORKER_TYPE: workerType }); + + worker.on('exit', (code: number, signal: string) => { + const data = { signal, code, workerId: worker.id }; + + if (signal) { + logger.info({ data }, 'worker.killed'); + } else if (code !== 0) { + logger.error({ data }, 'worker.exited'); + } else { + logger.info({ data }, 'worker.stopped'); + } + }); + + return worker; +} + +// If the current process is the primary process, run all the things that have +// to be run only once, like event processing, metrics server. +// Only the API HTTP server is forked. +if (cluster.isPrimary) { + logger.info({ data: { numWorkers } }, 'bootstrap.all.start'); + + for (let i = 0; i < numWorkers; i++) { + if (i === 0) { + eventsWorkerId = createFork('events').id; + } else { + createFork('http'); + } + } + + // Metrics server must run on the primary process. + startMetricsServer().catch((err: unknown) => { + logger.fatal({ err }, 'Failed to start metrics server'); + process.exit(1); + }); + + // ensure the score definition is up to date + checkLatestScoreVersion().catch((err: unknown) => { + logger.fatal({ err }, 'Failed to check latest score version'); + process.exit(1); + }); + + // Start the internal events jobs + startInternalEventsHourlyJob().catch((err: unknown) => { + logger.fatal({ err }, 'Failed to start internal events job'); + process.exit(1); + }); + startInternalEventsDailyJob().catch((err: unknown) => { + logger.fatal({ err }, 'Failed to start daily ethos jobs'); + process.exit(1); + }); + processInternalEthosJobs().catch((err: unknown) => { + logger.fatal({ err }, 'Failed to start hourly ethos jobs'); + process.exit(1); + }); + + // TODO: [CORE-1110] Temporary solution to restart workers if they die. + // We should have a proper process manager like pm2 or a custom solution + // to monitor, restart workers and cover them with metrics. + setInterval(() => { + const workersCount = Object.keys(cluster.workers ?? {}).length; + + if (workersCount < numWorkers) { + const needsEventWorker = !Object.values(cluster.workers ?? {}).some( + (worker) => worker?.id === eventsWorkerId, + ); + logger.info({ data: { workersCount, numWorkers, needsEventWorker } }, 'workers.restart'); + + for (let i = 0; i < numWorkers - workersCount; i++) { + if (needsEventWorker && i === 0) { + eventsWorkerId = createFork('events').id; + } else { + createFork('http'); + } + } + } + }, WORKERS_HEALTH_CHECK_INTERVAL); +} else { + const worker = cluster.worker; + + if (!worker) { + logger.fatal('Unexpected startup condition: non-primary cluster has no worker.'); + process.exit(1); + } + + if (config.WORKER_TYPE === 'events') { + logger.info({ workerId: worker.id }, 'Starting event processing worker'); + startEventProcessingWorker(); + } else { + logger.info({ workerId: worker.id }, 'Starting express server worker'); + startHttpApiWorker(); + } +} diff --git a/ethos/services/echo/src/bootstrap.db.ssl.ts b/ethos/services/echo/src/bootstrap.db.ssl.ts new file mode 100644 index 0000000..32e3773 --- /dev/null +++ b/ethos/services/echo/src/bootstrap.db.ssl.ts @@ -0,0 +1,118 @@ +/* eslint-disable no-console */ +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { type ZodString, type ZodOptional } from 'zod'; +import { config, dbConfig } from './common/config.js'; + +/** + * Prisma SSL config + * see: https://www.prisma.io/docs/orm/overview/databases/postgresql#configuring-an-ssl-connection + * Prisma expects *files* but we pass the PEM strings as secrets in Fly.io + * Coalesce the secrets into files and pass the paths to Prisma + * + * These functions must be run after config is loaded but before prisma connections are established. + * + * Note: all functions are idempotent; you will not overwrite files. However, we use the shared + * memory filesystem /var/run/shm/ to store the SSL files which will be lost on reboot. + */ + +const SSL_DIR = '/var/run/shm/ssl'; +const SSL_FILES = { + ca: 'ca.pem', + cert: 'client-cert.pem', + key: 'client-key.pem', + p12: 'client-identity.p12', +}; + +// Create a directory to store SSL files +function createSslDirectory(): string { + if (!fs.existsSync(SSL_DIR)) { + fs.mkdirSync(SSL_DIR, { recursive: true }); + } + + return SSL_DIR; +} + +// convert the fly secrets into files +function saveSslFile( + content: ZodOptional, + fileName: string, + configName: string, +): string { + if (!content || typeof content !== 'string') { + throw new Error(`Bootstrap DB SSL: ${configName} is required`); + } + const filePath = path.join(SSL_DIR, fileName); + + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, content); + } + + return filePath; +} + +function saveSslFiles(): { caPath: string; certPath: string; keyPath: string } { + const caPath = saveSslFile(dbConfig.DB_SERVER_CA, SSL_FILES.ca, 'DB_SERVER_CA'); + const certPath = saveSslFile(dbConfig.DB_SSL_CERT, SSL_FILES.cert, 'DB_SSL_CERT'); + const keyPath = saveSslFile(dbConfig.DB_SSL_KEY, SSL_FILES.key, 'DB_SSL_KEY'); + + return { caPath, certPath, keyPath }; +} + +// generate a PKCS12 file from the client key and certificate +function generatePkcs12File( + sslDir: string, + keyPath: string, + certPath: string, +): { p12Path: string } { + const p12Path = path.join(sslDir, SSL_FILES.p12); + + if (!fs.existsSync(p12Path)) { + try { + execSync( + `openssl pkcs12 -export -out ${p12Path} -inkey ${keyPath} -in ${certPath} -passout pass:`, + ); + console.info(`Bootstrap DB SSL: PKCS12 file generated at ${p12Path}`); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + console.error(`Bootstrap DB SSL: Error generating PKCS12 file: ${errorMessage}`); + throw new Error( + 'Unable to establish DB SSL connection; check that DB_SSL_KEY and DB_SSL_CERT are valid PEM files.', + ); + } + } + + return { p12Path }; +} + +// ensure the environment variable matches the configuration we're using +function verifyPrismaUrl(caPath: string, p12Path: string): void { + const sslParams = new URLSearchParams({ + sslmode: 'require', + sslcert: caPath, + sslidentity: p12Path, + }); + + const dbParams = sslParams.toString(); + + if (config.DATABASE_URL.includes(dbParams)) { + const configParams = new URL(config.DATABASE_URL).search; + console.error( + `Bootstrap DB SSL: DATABASE_URL (${configParams}) does not match the SSL configuration (${dbParams})`, + ); + } +} + +// Ensure the SSL prerequisites are met before initializing Prisma Client +export function bootstrapDbSSL(): void { + const sslDir = createSslDirectory(); + const { caPath, certPath, keyPath } = saveSslFiles(); + const { p12Path } = generatePkcs12File(sslDir, keyPath, certPath); + verifyPrismaUrl(caPath, p12Path); +} + +// Run bootstrapDbSSL only if this file is being run directly +if (import.meta.url === new URL(process.argv[1], 'file:').href) { + bootstrapDbSSL(); +} diff --git a/ethos/services/echo/src/bootstrap.events.ts b/ethos/services/echo/src/bootstrap.events.ts new file mode 100644 index 0000000..518d744 --- /dev/null +++ b/ethos/services/echo/src/bootstrap.events.ts @@ -0,0 +1,104 @@ +import { setTimeout } from 'node:timers/promises'; +import { duration } from '@ethos/helpers'; +import { config } from './common/config.js'; +import { rootLogger } from './common/logger.js'; +import { FEATURE_GATES, getGlobalFeatureGate, initStatsig } from './common/statsig.js'; +import { redis } from './data/redis.js'; +import { startEventServer, stopEventServer } from './event-server.js'; + +const logger = rootLogger.child({ process: 'event-worker', machineId: config.FLY_MACHINE_ID }); +const CURRENT_EVENT_PROCESSOR = 'contract-events-processor'; +const CHECK_INTERVAL = duration(5, 'seconds').toMilliseconds(); +const LOCK_TTL = duration(10, 'seconds').toSeconds(); + +function startEventProcessingWorkerInternal(): void { + // Handle shutdown gracefully + const signals = ['SIGQUIT', 'SIGTERM', 'SIGINT']; + + signals.forEach((signal) => { + process.on(signal, () => { + stopEventWorker(signal).catch((err) => { + logger.fatal({ err }, 'Failed to shut down the server'); + process.exit(1); + }); + }); + }); + + process.on('uncaughtException', (err) => { + logger.fatal({ err }, 'uncaught_exception'); + process.exit(1); + }); + + process.on('unhandledRejection', (err) => { + logger.error({ err }, 'unhandled_rejection'); + }); + startEventServer().catch((err) => { + logger.fatal({ err }, 'Failed to start contract events batch job'); + process.exit(1); + }); +} + +async function stopEventWorker(reason: string): Promise { + logger.info(`Received ${reason}. Gracefully shutting down...`); + + await stopEventServer().catch((err) => { + logger.fatal({ err }, 'Failed to stop contract events batch job'); + process.exit(1); + }); + + // Remove the lock from Redis and allow another machine to take over + await redis.del(CURRENT_EVENT_PROCESSOR); + + // Make sure we wait for all the connections to close on production. + // Otherwise, exit almost immediately. + await setTimeout(config.NODE_ENV === 'production' ? 3000 : 500); + + process.exit(0); +} + +async function checkProcessorOwnership(fn: () => void): Promise { + try { + const currentMachineId = await redis.get(CURRENT_EVENT_PROCESSOR); + + // If there is no current processor, we can start processing + if (!currentMachineId) { + // Lock the current processor to this machine so no other machine can take over + await redis.setex(CURRENT_EVENT_PROCESSOR, LOCK_TTL, config.FLY_MACHINE_ID); + + fn(); + + logger.info({ machineId: config.FLY_MACHINE_ID }, 'event_processing.started'); + + return; + } + + // If the current processor is this machine, update the expiration of the key + if (currentMachineId === config.FLY_MACHINE_ID) { + await redis.expire(CURRENT_EVENT_PROCESSOR, LOCK_TTL); + } + } catch (err) { + logger.error({ err }, 'Failed to check processor ownership'); + } +} + +export function startEventProcessingWorker(): void { + initStatsig() + .then(() => { + const isSingleProcessorEnabled = getGlobalFeatureGate( + FEATURE_GATES.ENSURE_SINGLE_CONTRACT_EVENT_PROCESSOR, + ); + + logger.info({ isSingleProcessorEnabled }, 'event_processing_worker.starting'); + + if (isSingleProcessorEnabled) { + setInterval(() => { + void checkProcessorOwnership(startEventProcessingWorkerInternal); + }, CHECK_INTERVAL); + } else { + startEventProcessingWorkerInternal(); + } + }) + .catch((err) => { + logger.error({ err }, 'Failed to initialize Statsig'); + }); +} diff --git a/ethos/services/echo/src/bootstrap.http-api.ts b/ethos/services/echo/src/bootstrap.http-api.ts new file mode 100644 index 0000000..eb420e5 --- /dev/null +++ b/ethos/services/echo/src/bootstrap.http-api.ts @@ -0,0 +1,44 @@ +import { setTimeout } from 'node:timers/promises'; +import { config } from './common/config.js'; +import { rootLogger } from './common/logger.js'; +import { startServer, stopServer } from './http-server.js'; + +const logger = rootLogger.child({ process: 'http-worker' }); + +export function startHttpApiWorker(): void { + // Handle shutdown gracefully + const signals = ['SIGQUIT', 'SIGTERM', 'SIGINT']; + + signals.forEach((signal) => { + process.on(signal, () => { + stopWorker(signal).catch((err) => { + logger.error({ err }, 'Failed to shut down the server'); + }); + }); + }); + + process.on('uncaughtException', (err) => { + logger.fatal({ err }, 'uncaught_exception'); + process.exit(1); + }); + + process.on('unhandledRejection', (err) => { + logger.error({ err }, 'unhandled_rejection'); + }); + + startServer().catch((err) => { + logger.error({ err }, 'Failed to start server'); + }); +} + +async function stopWorker(reason: string): Promise { + logger.info(`Received ${reason}. Gracefully shutting down...`); + + stopServer(); + + // Ensure we wait for all the connections to close on production. + // Otherwise, exit almost immediately. + await setTimeout(config.NODE_ENV === 'production' ? 3000 : 500); + + process.exit(0); +} diff --git a/ethos/services/echo/src/common/blockchain-manager.ts b/ethos/services/echo/src/common/blockchain-manager.ts new file mode 100644 index 0000000..0c8c71d --- /dev/null +++ b/ethos/services/echo/src/common/blockchain-manager.ts @@ -0,0 +1,12 @@ +import { BlockchainManager } from '@ethos/blockchain-manager'; +import { getNetworkByEnvironment } from '@ethos/contracts'; +import { config } from './config.js'; + +const network = getNetworkByEnvironment(config.ETHOS_ENV); +const isMainnet = network === 'base-mainnet'; +const alchemyBaseUrl = isMainnet ? config.ALCHEMY_MAINNET_API_URL : config.ALCHEMY_TESTNET_API_URL; +const alchemyConnectionURL = alchemyBaseUrl + config.ALCHEMY_API_KEY; + +export const blockchainManager = new BlockchainManager(config.ETHOS_ENV, { + alchemyConnectionURL, +}); diff --git a/ethos/services/echo/src/common/cache/cache.types.ts b/ethos/services/echo/src/common/cache/cache.types.ts new file mode 100644 index 0000000..e92f4a1 --- /dev/null +++ b/ethos/services/echo/src/common/cache/cache.types.ts @@ -0,0 +1,7 @@ +export type CacheConfig = { + key: (...args: Array) => string; + ttl: number; + get: (...args: any[]) => Promise; + set: (...args: any[]) => Promise; + delete: (...args: any[]) => Promise; +}; diff --git a/ethos/services/echo/src/common/cache/ens.ts b/ethos/services/echo/src/common/cache/ens.ts new file mode 100644 index 0000000..6616ac0 --- /dev/null +++ b/ethos/services/echo/src/common/cache/ens.ts @@ -0,0 +1,80 @@ +import { duration, isValidAddress } from '@ethos/helpers'; +import { type Address } from 'viem'; +import { prisma } from '../../data/db.js'; +import { type EnsDetailsStrict, type EnsDetailsLoose } from '../net/ens.js'; +import { cachedOperation, createLRUCache } from './lru.cache.js'; + +// two layer cache: +// 1. in-memory LRU cache +// 2. database +// (fallback is pull from ens api) + +const ONE_HOUR = duration(1, 'hour').toMilliseconds(); +const ensAddressCache = createLRUCache(ONE_HOUR); +const ensNameCache = createLRUCache(ONE_HOUR); + +async function getByAddress(address: Address): Promise { + async function getFromDb(): Promise { + const cache = await prisma.ensCache.findUnique({ + where: { + address: address.toLowerCase(), + updatedAt: { + gte: new Date(Date.now() - duration(1, 'day').toMilliseconds()), + }, + }, + }); + + if (!cache) { + return null; + } + + return { + name: cache?.ensName, + avatar: cache?.avatarUrl, + address, + }; + } + + return await cachedOperation('ensCacheByAddress', ensAddressCache, address, getFromDb); +} +async function getByName(ensName: string): Promise { + async function getFromDb(): Promise { + const cache = await prisma.ensCache.findFirst({ + where: { + ensName: { + equals: ensName, + mode: 'insensitive', + }, + }, + orderBy: { updatedAt: 'desc' }, + }); + + if (!cache) { + return null; + } + + return { + name: cache?.ensName, + avatar: cache?.avatarUrl, + address: isValidAddress(cache?.address) ? cache?.address : null, + }; + } + + return await cachedOperation('ensCacheByName', ensNameCache, ensName, getFromDb); +} + +async function set({ name, avatar, address }: EnsDetailsStrict): Promise { + await prisma.ensCache.upsert({ + where: { + address: address.toLowerCase(), + }, + update: { ensName: name, avatarUrl: avatar }, + create: { + address: address.toLowerCase(), + ensName: name, + avatarUrl: avatar, + }, + }); +} + +export const ensDetails = { set, getByAddress, getByName }; diff --git a/ethos/services/echo/src/common/cache/exchange-rates/eth-price.ts b/ethos/services/echo/src/common/cache/exchange-rates/eth-price.ts new file mode 100644 index 0000000..6c6381c --- /dev/null +++ b/ethos/services/echo/src/common/cache/exchange-rates/eth-price.ts @@ -0,0 +1,25 @@ +import { JsonHelper, duration } from '@ethos/helpers'; +import { redis } from '../../../data/redis.js'; +import { type CacheConfig } from '../cache.types.js'; + +const ETH_PRICE_TTL = duration(10, 'minutes').toSeconds(); + +export const ethPriceCache = { + key() { + return 'eth_price'; + }, + + ttl: ETH_PRICE_TTL, + async get() { + const data = await redis.get(this.key()); + const parsed = JsonHelper.parseSafe(data); + + return parsed; + }, + async set(price: number) { + await redis.setex(this.key(), this.ttl, JSON.stringify(price)); + }, + async delete() { + await redis.del(this.key()); + }, +} satisfies CacheConfig; diff --git a/ethos/services/echo/src/common/cache/lru.cache.ts b/ethos/services/echo/src/common/cache/lru.cache.ts new file mode 100644 index 0000000..741a587 --- /dev/null +++ b/ethos/services/echo/src/common/cache/lru.cache.ts @@ -0,0 +1,100 @@ +import { duration, JsonHelper } from '@ethos/helpers'; +import { LRUCache } from 'lru-cache'; +import { redis } from '../../data/redis.js'; +import { metrics } from '../metrics.js'; +import { FEATURE_GATES, getGlobalFeatureGate } from '../statsig.js'; + +const DEFAULT_MAX_CACHE_SIZE = 10000; +const DEFAULT_MAX_CACHE_DURATION = duration(1, 'hour').toMilliseconds(); + +// Add counter for cache operations +const cacheOperationsCounter = metrics.makeCounter({ + name: 'lru_cache_operations', + help: 'Count of LRU cache operations (hits and misses)', + labelNames: ['cache_name', 'operation', 'cache_type'], +}); + +/** + * @param duration in milliseconds + */ +export function createLRUCache( + duration: number = DEFAULT_MAX_CACHE_DURATION, +): LRUCache> { + return new LRUCache>({ + max: DEFAULT_MAX_CACHE_SIZE, + ttl: duration, + }); +} + +export async function cachedOperation( + cacheName: string, + cache: LRUCache>, + key: T, + operation: (key: T) => Promise, +): Promise { + const isRedisEnabled = getGlobalFeatureGate(FEATURE_GATES.USE_REDIS_INSTEAD_OF_LRU); + + if (isRedisEnabled) { + const cacheKey = `${cacheName}:${JSON.stringify(key)}`; + + const cached = await redis.get(cacheKey); + + if (cached) { + const parsed = await JsonHelper.parseSafe>(cached, { + reviver: JsonHelper.reviver, + }); + + if (parsed) { + cacheOperationsCounter.inc({ + cache_name: cacheName, + operation: 'hit', + cache_type: 'redis', + }); + + return parsed; + } + } + + cacheOperationsCounter.inc({ + cache_name: cacheName, + operation: 'miss', + cache_type: 'redis', + }); + + const result = await operation(key); + + await redis.setex( + cacheKey, + duration(cache.ttl, 'milliseconds').toSeconds(), + JSON.stringify(result, JsonHelper.replacer), + ); + + return result; + } else { + const cacheKey = JSON.stringify(key); + + if (cache.has(cacheKey)) { + const cachedResult = await cache.get(cacheKey); + + if (cachedResult !== undefined) { + cacheOperationsCounter.inc({ + cache_name: cacheName, + operation: 'hit', + cache_type: 'lru', + }); + + return cachedResult; + } + } + + cacheOperationsCounter.inc({ + cache_name: cacheName, + operation: 'miss', + cache_type: 'lru', + }); + const result = operation(key); + cache.set(cacheKey, result); + + return await result; + } +} diff --git a/ethos/services/echo/src/common/cache/transactions/__tests__/transaction.cache.test.ts b/ethos/services/echo/src/common/cache/transactions/__tests__/transaction.cache.test.ts new file mode 100644 index 0000000..d5a8c74 --- /dev/null +++ b/ethos/services/echo/src/common/cache/transactions/__tests__/transaction.cache.test.ts @@ -0,0 +1,71 @@ +import { type AddressHistoryCache } from '@prisma-pg/client'; +import { + ADDRESS_HISTORY_CACHE_DURATION, + NO_TRANSACTIONS_CACHE_DURATION, + NO_TRANSACTIONS_DATE, + processAddressHistoryCache, +} from '../history.cache.check.js'; + +const EXPIRED_DATE = new Date(Date.now() - ADDRESS_HISTORY_CACHE_DURATION - 10000); +const RECENT_DATE = new Date(Date.now() - ADDRESS_HISTORY_CACHE_DURATION + 10000); +const NO_TRANSACTIONS_EXPIRED_DATE = new Date(Date.now() - NO_TRANSACTIONS_CACHE_DURATION - 10000); +const NO_TRANSACTIONS_RECENT_DATE = new Date(Date.now() - NO_TRANSACTIONS_CACHE_DURATION + 10000); +const ARBITRARY_DATE = new Date('2022-12-31T00:00:00.000Z'); +const address = '0x123'; + +describe('processAddressHistoryCache', () => { + test('returns undefined when cache is null', () => { + expect(processAddressHistoryCache(null)).toBeUndefined(); + }); + + test('returns the firstTransaction date when it exists and is recent', () => { + const cache: AddressHistoryCache = { + address, + firstTransaction: ARBITRARY_DATE, + createdAt: EXPIRED_DATE, + updatedAt: RECENT_DATE, + }; + expect(processAddressHistoryCache(cache)).toEqual(ARBITRARY_DATE); + }); + + test('returns undefined when firstTransaction date exists and is expired', () => { + const cache: AddressHistoryCache = { + address, + firstTransaction: ARBITRARY_DATE, + createdAt: EXPIRED_DATE, + updatedAt: EXPIRED_DATE, + }; + expect(processAddressHistoryCache(cache)).toBeUndefined(); + }); + + test('returns null when NO_TRANSACTIONS_DATE is recent', () => { + const cache: AddressHistoryCache = { + address, + firstTransaction: NO_TRANSACTIONS_DATE, + createdAt: NO_TRANSACTIONS_EXPIRED_DATE, + updatedAt: NO_TRANSACTIONS_RECENT_DATE, + }; + expect(processAddressHistoryCache(cache)).toBeNull(); + }); + + test('returns undefined when NO_TRANSACTIONS_DATE is expired', () => { + const cache: AddressHistoryCache = { + address, + firstTransaction: NO_TRANSACTIONS_DATE, + createdAt: NO_TRANSACTIONS_EXPIRED_DATE, + updatedAt: NO_TRANSACTIONS_EXPIRED_DATE, + }; + expect(processAddressHistoryCache(cache)).toBeUndefined(); + }); + + test('returns null when firstTransaction is the same date but a different object', () => { + const DIFF_DATE = new Date(0); + const cache: AddressHistoryCache = { + address, + firstTransaction: DIFF_DATE, + createdAt: NO_TRANSACTIONS_RECENT_DATE, + updatedAt: NO_TRANSACTIONS_RECENT_DATE, + }; + expect(processAddressHistoryCache(cache)).toBe(null); + }); +}); diff --git a/ethos/services/echo/src/common/cache/transactions/history.cache.check.ts b/ethos/services/echo/src/common/cache/transactions/history.cache.check.ts new file mode 100644 index 0000000..eba64e7 --- /dev/null +++ b/ethos/services/echo/src/common/cache/transactions/history.cache.check.ts @@ -0,0 +1,38 @@ +import { duration } from '@ethos/helpers'; +import { type AddressHistoryCache } from '@prisma-pg/client'; + +// first transaction per address should never change +export const ADDRESS_HISTORY_CACHE_DURATION = duration(1, 'year').toMilliseconds(); +export const NO_TRANSACTIONS_DATE = new Date(0); +export const NO_TRANSACTIONS_CACHE_DURATION = duration(1, 'hour').toMilliseconds(); + +// Date = first transaction found +// undefined = no address history found (ie, we haven't pulled it from moralis) +// null = no first transactions found (ie, we pulled from moralis and found no transactions) +export function processAddressHistoryCache( + cached: AddressHistoryCache | null, +): Date | undefined | null { + if (!cached) return undefined; + + const expiresAt = new Date(Date.now() - ADDRESS_HISTORY_CACHE_DURATION); + + // Check if the cache is expired + if (cached.updatedAt < expiresAt) { + return undefined; + } + + // check if we've cached that there's no transactions + if (cached.firstTransaction.getTime() === NO_TRANSACTIONS_DATE.getTime()) { + const noTransactionsExpiresAt = new Date(Date.now() - NO_TRANSACTIONS_CACHE_DURATION); + + // if the no transactions cache is expired, return undefined + if (cached.updatedAt < noTransactionsExpiresAt) { + return undefined; + } + + // if there's no first transaction, but the cache is recent, return null + return null; + } + + return cached.firstTransaction; +} diff --git a/ethos/services/echo/src/common/cache/transactions/transaction.cache.ts b/ethos/services/echo/src/common/cache/transactions/transaction.cache.ts new file mode 100644 index 0000000..09cf1d6 --- /dev/null +++ b/ethos/services/echo/src/common/cache/transactions/transaction.cache.ts @@ -0,0 +1,95 @@ +import { duration } from '@ethos/helpers'; +import { type AddressHistoryCache, type TransactionHistoryCache } from '@prisma-pg/client'; +import { type Address } from 'viem'; +import { prisma } from '../../../data/db.js'; +import { type EvmWalletHistoryTransactionJSON } from '../../net/moralis/moralis.type.js'; +import { NO_TRANSACTIONS_DATE, processAddressHistoryCache } from './history.cache.check.js'; + +const TRANSACTION_CACHE_DURATION = duration(1, 'day').toMilliseconds(); + +async function getTxnHistory(address: Address): Promise { + const cached = await prisma.transactionHistoryCache.findMany({ + where: { + OR: [ + { fromAddress: { equals: address, mode: 'insensitive' } }, + { toAddress: { equals: address, mode: 'insensitive' } }, + ], + updatedAt: { gte: new Date(Date.now() - TRANSACTION_CACHE_DURATION) }, + }, + orderBy: { + blockTimestamp: 'asc', + }, + }); + + return cached; +} + +async function setTxnHistory( + transactions: EvmWalletHistoryTransactionJSON[], +): Promise { + const transactionHistory: TransactionHistoryCache[] = []; + + const upsertOperations = transactions.map(async (transaction) => { + const transactionData: TransactionHistoryCache = { + fromAddress: transaction.from_address, + toAddress: transaction.to_address, + hash: transaction.hash, + createdAt: new Date(), + updatedAt: new Date(), + value: transaction.value, + blockNumber: parseInt(transaction.block_number, 10), + blockTimestamp: new Date(transaction.block_timestamp), + category: transaction.category, + summary: transaction.summary, + fromAddressLabel: transaction.from_address_label ?? null, + fromAddressLogo: transaction.from_address_entity_logo ?? null, + toAddressLabel: transaction.to_address_label ?? null, + toAddressLogo: transaction.to_address_entity_logo ?? null, + }; + + transactionHistory.push(transactionData); + + const txnHistory = await prisma.transactionHistoryCache.upsert({ + where: { hash: transaction.hash }, + create: transactionData, + update: transactionData, + }); + + return txnHistory; + }); + + await Promise.all(upsertOperations); + + return transactionHistory; +} + +async function getAddrHistory(address: Address): Promise { + const cached = await prisma.addressHistoryCache.findUnique({ + where: { address }, + }); + + return processAddressHistoryCache(cached); +} + +async function setAddrHistory( + address: Address, + firstTransaction: Date | null, +): Promise { + const cached = await prisma.addressHistoryCache.upsert({ + where: { address }, + create: { address, firstTransaction: firstTransaction ?? NO_TRANSACTIONS_DATE }, + update: { firstTransaction: firstTransaction ?? NO_TRANSACTIONS_DATE }, + }); + + return cached; +} + +export const transactionCache = { + get: getTxnHistory, + set: setTxnHistory, +}; + +export const addressHistoryCache = { + get: getAddrHistory, + set: setAddrHistory, +}; diff --git a/ethos/services/echo/src/common/cache/twitter.cache.ts b/ethos/services/echo/src/common/cache/twitter.cache.ts new file mode 100644 index 0000000..75ebf9a --- /dev/null +++ b/ethos/services/echo/src/common/cache/twitter.cache.ts @@ -0,0 +1,90 @@ +import { hashServiceAndAccount } from '@ethos/blockchain-manager'; +import { X_SERVICE } from '@ethos/domain'; +import { duration } from '@ethos/helpers'; +import { type TwitterProfileCache } from '@prisma-pg/client'; +import { prisma } from '../../data/db.js'; +import { cachedOperation, createLRUCache } from './lru.cache.js'; + +const CACHE_DURATION = duration(1, 'hour').toMilliseconds(); +const twitterUserCache = createLRUCache(CACHE_DURATION); + +async function cachedGet(username: string): Promise { + return await cachedOperation('twitterUserCache', twitterUserCache, username, get); +} + +async function get(username: string): Promise { + const cached = await prisma.twitterProfileCache.findFirst({ + where: { + username, + }, + orderBy: { + // Make sure to get the most recent profile who owns that username. The + // edge case is userA with username "bike" changes it to something else so + // userB can use that username. If both users are Ethos users or they've + // been reviewed, we will have both of them in the database. Because we + // don't update records often (7 days is considered stale now), we might + // have both users have the same username. So whoever has the most recent + // profile is the one we want to return. + updatedAt: 'desc', + }, + }); + + if (!cached) { + return null; + } + + return cached; +} + +async function set({ + id, + username, + name, + avatar, + biography, + website, + followersCount, + joinedAt, + isBlueVerified, +}: { + id: string; + username: string; + name: string; + avatar?: string; + biography?: string; + website?: string; + followersCount?: number; + joinedAt?: Date; + isBlueVerified: boolean; +}): Promise { + const attestationHash = hashServiceAndAccount(X_SERVICE, id); + + return await prisma.twitterProfileCache.upsert({ + where: { id }, + create: { + id, + username, + name, + avatar, + biography, + website, + followersCount, + joinedAt, + isBlueVerified, + attestationHash, + }, + update: { + username, + name, + avatar, + biography, + website, + followersCount, + joinedAt, + isBlueVerified, + attestationHash, + }, + }); +} + +export const twitterUser = { get: cachedGet, set }; diff --git a/ethos/services/echo/src/common/config.ts b/ethos/services/echo/src/common/config.ts new file mode 100644 index 0000000..79d3cde --- /dev/null +++ b/ethos/services/echo/src/common/config.ts @@ -0,0 +1,88 @@ +import { getConfig } from '@ethos/config'; +import { ETHOS_ENVIRONMENTS } from '@ethos/env'; +import { type ServiceAccount } from 'firebase-admin'; +import { jwtDecode } from 'jwt-decode'; +import { z } from 'zod'; + +export const config = getConfig({ + ALCHEMY_API_KEY: z.string(), + ALCHEMY_MAINNET_API_URL: z.string().url(), + ALCHEMY_TESTNET_API_URL: z.string().url(), + DATABASE_URL: z.string(), + AMQP_URL: z.string().url(), + CHAOS_PERCENTAGE_RATE: z.number().nonnegative().default(0), + DEPLOYMENT_ID: z.string().default('local-dev'), + ETHOS_ENV: z.enum(ETHOS_ENVIRONMENTS).default('local'), + FIREBASE_ADMIN_CREDENTIALS: z + .string() + .base64() + .transform((v) => { + try { + const decoded = JSON.parse(Buffer.from(v, 'base64').toString('utf8')); + + const serviceAccount: ServiceAccount = { + projectId: decoded.project_id, + clientEmail: decoded.client_email, + privateKey: decoded.private_key, + }; + + return serviceAccount; + } catch { + throw new Error('FIREBASE_ADMIN_CREDENTIALS is not a valid base64 string'); + } + }) + .pipe( + z.object({ + projectId: z.string(), + clientEmail: z.string().email(), + privateKey: z.string(), + }), + ), + FLY_MACHINE_ID: z.string().optional().default('local'), + MORALIS_API_KEY: z.string().refine(isJWT(), 'Invalid JWT'), + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + PORT_ECHO: z.coerce.number().positive().default(8080), + PORT_ECHO_METRICS: z.coerce.number().positive().default(9091), + PRIVY_APP_ID: z.string(), + PRIVY_APP_SECRET: z.string(), + PRIVY_APP_PUBLIC_KEY: z.string(), + REDIS_URL: z.string().url().default('redis://localhost:6379'), + SIGNER_ACCOUNT_PRIVATE_KEY: z.string().length(64), + STATSIG_SECRET_KEY: z.string(), + TWITTER_BEARER_TOKEN: z.string(), + TWITTER_CLIENT_ID: z.string(), + TWITTER_CLIENT_SECRET: z.string(), + TWITTER_SESSION_SECRET: z.string(), + SENTRY_TRACE_SAMPLE_RATE: z.coerce.number().default(1.0), + SENTRY_PROFILING_SAMPLE_RATE: z.coerce.number().default(1.0), + WORKER_TYPE: z.enum(['events', 'http', 'primary']).default('primary'), +}); + +export const dbConfig = getConfig({ + DB_SERVER_CA: requiredWhenNotLocal(z.string().optional()), + DB_SSL_CERT: requiredWhenNotLocal(z.string().optional()), + DB_SSL_KEY: requiredWhenNotLocal(z.string().optional()), +}); + +function isJWT() { + return (value: string) => { + try { + const decoded = jwtDecode(value); + + return typeof decoded === 'object' && decoded !== null; + } catch { + return false; + } + }; +} + +function requiredWhenNotLocal(schema: T): z.ZodType { + return schema.superRefine((value, ctx) => { + if (config.ETHOS_ENV !== 'local' && value === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${ctx.path.join('.')} is required when ETHOS_ENV is not 'local'`, + }); + } + }); +} diff --git a/ethos/services/echo/src/common/cron.ts b/ethos/services/echo/src/common/cron.ts new file mode 100644 index 0000000..d1b4b13 --- /dev/null +++ b/ethos/services/echo/src/common/cron.ts @@ -0,0 +1,73 @@ +import { duration } from '@ethos/helpers'; +import { CronJob } from 'cron'; +import { type RedlockAbortSignal, ExecutionError, ResourceLockedError } from 'redlock2'; +import { resourceLockedCounter } from '../contract-events/event-metrics.js'; +import { redlock } from '../data/redis.js'; +import { rootLogger } from './logger.js'; + +const logger = rootLogger.child({ module: 'cron' }); + +export class CronJobManager { + private readonly job: CronJob; + private readonly lockKey: string[]; + private readonly lockDuration: number; + private readonly jobName: string; + private readonly logger: typeof rootLogger; + + constructor( + cronExpression: string, + lockKey: string[], + jobName: string, + handler: (signal: RedlockAbortSignal) => Promise, + lockDuration: number = duration(30, 'seconds').toMilliseconds(), + ) { + this.lockKey = lockKey; + this.lockDuration = lockDuration; + this.jobName = jobName; + this.logger = rootLogger.child({ module: jobName }); + + this.job = new CronJob(cronExpression, async () => { + await redlock.using(this.lockKey, this.lockDuration, handler).catch((err) => { + CronJobManager.handleRedlockError(this.lockKey, {}, err); + }); + }); + } + + async start(): Promise { + this.job.start(); + this.logger.info(`${this.jobName}.started`); + } + + async stop(): Promise { + this.job.stop(); + this.logger.info(`${this.jobName}.stopped`); + } + + public static handleRedlockError(key: string[], data: any, err: unknown): void { + // Should only need to check ResourceLockedError, but redlock v5 has a bug https://github.com/mike-marcacci/node-redlock/issues/168 + if (err instanceof ResourceLockedError || err instanceof ExecutionError) { + resourceLockedCounter.inc(); + logger.debug({ data: { key } }, 'resource_locked'); + + return; + } + logger.error({ err, data }, 'cron_job_error'); + } + + public async executeJob( + signal: RedlockAbortSignal, + jobFunction: () => Promise, + ): Promise { + if (signal.aborted) { + if (signal.error) throw signal.error; + logger.warn(`${this.jobName}.aborted`); + + return; + } + try { + await jobFunction(); + } catch (err) { + this.logger.error({ err }, `${this.jobName}_error`); + } + } +} diff --git a/ethos/services/echo/src/common/errors/express-error.ts b/ethos/services/echo/src/common/errors/express-error.ts new file mode 100644 index 0000000..868fd13 --- /dev/null +++ b/ethos/services/echo/src/common/errors/express-error.ts @@ -0,0 +1,12 @@ +export class ExpressError extends Error { + code?: string; + expose?: boolean; + status?: number; + statusCode?: number; + + constructor(message: string, opts: { code?: string; expose?: boolean; status: number }) { + super(message); + + Object.assign(this, opts); + } +} diff --git a/ethos/services/echo/src/common/logger.ts b/ethos/services/echo/src/common/logger.ts new file mode 100644 index 0000000..47bb829 --- /dev/null +++ b/ethos/services/echo/src/common/logger.ts @@ -0,0 +1,55 @@ +import { type Logger, getLogger } from '@ethos/logger'; +import { type Request, type Response } from 'express'; +import { pick } from 'lodash-es'; +import { pino, type LoggerOptions } from 'pino'; +import { config } from './config.js'; +import { commonOptions } from './sentry.js'; + +const serializers: LoggerOptions['serializers'] = { + req({ + body, + headers, + method, + originalUrl, + query, + }: Request>) { + return { + body: Object.keys(body).length ? body : undefined, + headers: pick(headers, ['content-type', 'user-agent']), + method, + pathname: new URL(originalUrl, 'http://localhost').pathname, + query: Object.keys(query).length ? query : undefined, + }; + }, + + res({ statusCode, responseTime }: Response) { + return { statusCode, responseTime }; + }, + + service(service) { + // Hide "service" field when running locally, it saves some vertical spaces + // in local logs. + if (config.NODE_ENV === 'production') return service; + }, +}; + +const transportTargets: pino.TransportMultiOptions['targets'] = [ + { + target: 'pino-sentry-transport', + options: { + sentry: { + ...commonOptions, + }, + context: ['module', 'req_id'], + tags: ['deployId', 'invokingService'], + withLogRecord: true, + minLevel: pino.levels.values.error, + expectPinoConfig: true, + }, + }, +]; + +export const rootLogger: Logger = getLogger('echo', { serializers, transportTargets }).child({ + env: config.NODE_ENV === 'production' ? config.ETHOS_ENV : undefined, + deployId: config.NODE_ENV === 'production' ? config.DEPLOYMENT_ID : undefined, +}); diff --git a/ethos/services/echo/src/common/metrics.ts b/ethos/services/echo/src/common/metrics.ts new file mode 100644 index 0000000..a6d844e --- /dev/null +++ b/ethos/services/echo/src/common/metrics.ts @@ -0,0 +1,88 @@ +import { AggregatorRegistry, collectDefaultMetrics, Counter, Summary } from 'prom-client'; +import { type SnakeCase } from 'type-fest'; + +const PREFIX = 'echo_'; +const aggregatorRegistry = new AggregatorRegistry(); + +export function initDefaultMetrics(): void { + collectDefaultMetrics({ prefix: PREFIX }); +} + +export async function getMetrics(): Promise<{ contentType: string; metrics: string }> { + const metrics = await aggregatorRegistry.clusterMetrics(); + + return { + contentType: aggregatorRegistry.contentType, + metrics, + }; +} + +function prefixName(name: string): string { + return `${PREFIX}${name}`; +} + +/** + * Metrics utility object for creating counters, summaries, and timers. + */ +export const metrics = { + /** + * Creates a Prometheus Counter. + * + * A Counter is a cumulative metric that represents a single monotonically increasing counter + * whose value can only increase or be reset to zero on restart. For example, you can use a + * counter to represent the number of requests served, tasks completed, or errors. + * + * Note: summaries include a count; you don't need both for the same target. + * + * @param name - The name of the counter metric. + * @param help - A description of what the counter represents. + * @param labelNames - An array of label names for the counter. + * @returns A Prometheus Counter instance. + */ + makeCounter({ + name, + help, + labelNames = [], + }: { + name: string; + help: string; + labelNames?: Array>; + }): Counter> { + return new Counter({ + name: prefixName(name), + help, + labelNames, + }); + }, + + /** + * Creates a Prometheus Summary. + * + * A Summary samples observations (usually things like request durations and response sizes) + * and provides a total count of observations and a sum of all observed values. It calculates + * configurable quantiles over a sliding time window. + * + * Summaries are useful when you need to track the size of events, + * observe how long certain operations take, or count events while calculating their average. + * + * @param name - The name of the summary metric. + * @param help - A description of what the summary represents. + * @param labelNames - An array of label names for the summary. + * @returns A Prometheus Summary instance. + */ + makeSummary({ + name, + help, + labelNames, + }: { + name: string; + help: string; + labelNames: Array>; + }): Summary> { + return new Summary({ + name: prefixName(name), + help, + labelNames, + }); + }, +}; diff --git a/ethos/services/echo/src/common/net/client-metrics.ts b/ethos/services/echo/src/common/net/client-metrics.ts new file mode 100644 index 0000000..0561533 --- /dev/null +++ b/ethos/services/echo/src/common/net/client-metrics.ts @@ -0,0 +1,13 @@ +import { snakeCase } from 'lodash-es'; +import { type Summary } from 'prom-client'; +import { metrics } from '../metrics.js'; + +type Label = 'method_name' | 'http_method' | 'response_code'; + +export function getClientSummaryMetric(client: string): Summary