From 641aa3b19258b39b469202d165aae3970838cc44 Mon Sep 17 00:00:00 2001 From: "Ioannis T." Date: Tue, 18 Mar 2025 12:01:12 +0100 Subject: [PATCH 01/60] chore: introduce database migrations - Adds the initial migration --- package-lock.json | 5570 ++++++++++++++----- package.json | 7 +- src/db/database.ts | 12 +- src/db/migrations/20250317125844-initial.ts | 1028 ++++ src/db/runMigrations.ts | 32 + src/index.ts | 2 +- tsconfig.json | 113 +- 7 files changed, 5344 insertions(+), 1420 deletions(-) create mode 100644 src/db/migrations/20250317125844-initial.ts create mode 100644 src/db/runMigrations.ts diff --git a/package-lock.json b/package-lock.json index cca74db..ac1eb9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@efstajas/versioned-parser": "^0.1.4", + "@types/umzug": "^2.3.9", "bee-queue": "^1.5.0", "bull-arena": "^4.0.0", "dotenv": "^16.3.1", @@ -20,6 +21,7 @@ "pg": "^8.11.3", "redis": "^4.6.10", "sequelize": "^6.32.1", + "umzug": "^3.8.2", "winston": "^3.10.0", "zod": "^3.22.2" }, @@ -41,6 +43,7 @@ "jest": "^29.7.0", "nodemon": "^3.0.1", "prettier": "^3.0.3", + "sequelize-cli": "^6.6.2", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typechain": "^8.3.1", @@ -67,1606 +70,2009 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "node": ">=14.0.0" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } + "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true }, - "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/types": "^7.25.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" + "tslib": "^2.6.2" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.768.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.768.0.tgz", + "integrity": "sha512-h/WOvKhuXVIhNKjDcsF6oY2oJuBusspnmEaX20h+GUzIrNMlf6qkJrWziT58KzzESyzeYZcGNWjcOfbVRpH6NA==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.758.0.tgz", + "integrity": "sha512-BoGO6IIWrLyLxQG6txJw6RT2urmbtlwfggapNCrNPyYjlXpzTSJhBYjndg7TpDATFd0SXL0zm8y/tXsUXNkdYQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/core": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.758.0.tgz", + "integrity": "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "node_modules/@aws-sdk/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.768.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.768.0.tgz", + "integrity": "sha512-nNBN+lb2N8Odi0abHln60HqA4z0+UsBw8j7XU+ElEi5E2qOBCJSkLIFDIcYfn+j88FP2oLiQlOPe7H8pav5ayQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.768.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true }, - "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.758.0.tgz", + "integrity": "sha512-N27eFoRrO6MeUNumtNHDW9WOiwfd59LPXPqDrIa3kWL/s+fOKFHb9xIcF++bAwtcZnAxKkgpDCUP+INNZskE+w==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-env/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.758.0.tgz", + "integrity": "sha512-Xt9/U8qUCiw1hihztWkNeIR+arg6P+yda10OuCHX6kFVx3auTlU7+hCqs3UxqniGU4dguHuftf3mRpi5/GJ33Q==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-http/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.758.0.tgz", + "integrity": "sha512-cymSKMcP5d+OsgetoIZ5QCe1wnp2Q/tq+uIxVdh9MbfdBBEnl9Ecq6dH6VlYS89sp4QKuxHxkWXVnbXU3Q19Aw==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/types": "^7.25.2" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.758.0", + "@aws-sdk/credential-provider-web-identity": "3.758.0", + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.758.0.tgz", + "integrity": "sha512-+DaMv63wiq7pJrhIQzZYMn4hSarKiizDoJRvyR7WGhnn0oQ/getX9Z0VNCV3i7lIFoLNTb7WMmQ9k7+z/uD5EQ==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.758.0", + "@aws-sdk/credential-provider-web-identity": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-node/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.758.0.tgz", + "integrity": "sha512-AzcY74QTPqcbXWVgjpPZ3HOmxQZYPROIBz2YINF0OQk0MhezDWV/O7Xec+K1+MPGQO3qS6EDrUUlnPLjsqieHA==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-process/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.758.0.tgz", + "integrity": "sha512-x0FYJqcOLUCv8GLLFDYMXRAQKGjoM+L0BG4BiHYZRDf24yQWFCAZsCQAYKo6XZYh2qznbsW6f//qpyJ5b0QVKQ==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "@aws-sdk/client-sso": "3.758.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-sso/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.758.0.tgz", + "integrity": "sha512-XGguXhBqiCXMXRxcfCAVPlMbm3VyJTou79r/3mxWddHWF0XbhaQiBIbUz6vobVTD25YQRbWSmSch7VA8kI5Lrw==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.768.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.768.0.tgz", + "integrity": "sha512-uEAtcdHArZxq7dbpgI4ofDclefNYnYWrT9bJn2Q6rf7VlQnoD37ptzVLQBLomXnRaBiQB/sRV2MJaugFqwOEQA==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@aws-sdk/client-cognito-identity": "3.768.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-cognito-identity": "3.768.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.758.0", + "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.758.0", + "@aws-sdk/credential-provider-web-identity": "3.758.0", + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.734.0.tgz", + "integrity": "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-host-header/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.734.0.tgz", + "integrity": "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-logger/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.734.0.tgz", + "integrity": "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-recursion-detection/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.758.0.tgz", + "integrity": "sha512-iNyehQXtQlj69JCgfaOssgZD4HeYGOwxcaKeG6F+40cwBjTAi0+Ph1yfDwqk2qiBPIRWJ/9l2LodZbxiBqgrwg==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@smithy/core": "^3.1.5", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-user-agent/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.758.0.tgz", + "integrity": "sha512-YZ5s7PSvyF3Mt2h1EQulCG93uybprNGbBkPmVuy/HMMfbFTt4iL3SbKjxqvOZelm86epFfj7pvK7FliI2WOEcg==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.734.0.tgz", + "integrity": "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/region-config-resolver/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.758.0.tgz", + "integrity": "sha512-ckptN1tNrIfQUaGWm/ayW1ddG+imbKN7HHhjFdS4VfItsP0QQOB0+Ov+tpgb4MoNR4JaUghMIVStjIeHN2ks1w==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/token-providers/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/types": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", + "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/types/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.743.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.743.0.tgz", + "integrity": "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "@smithy/util-endpoints": "^3.0.1", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-endpoints/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.723.0.tgz", + "integrity": "sha512-Yf2CS10BqK688DRsrKI/EO6B8ff5J86NXe4C+VCysK7UOgN0l1zOTeTukZ3H8Q9tYYX3oaF1961o8vRkFm7Nmw==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-locate-window/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.734.0.tgz", + "integrity": "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-user-agent-browser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.758.0.tgz", + "integrity": "sha512-A5EZw85V6WhoKMV2hbuFRvb9NPlxEErb4HPO6/SPXYY4QrjprIzScHxikqcWv1w4J3apB1wto9LPU3IMsYtfrw==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "node_modules/@aws-sdk/util-user-agent-node/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "node_modules/@babel/compat-data": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, "license": "MIT", "dependencies": { - "regenerator-runtime": "^0.14.0" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/template": { + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@babel/types": { + "node_modules/@babel/helper-module-transforms": { "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/traverse": "^7.25.2" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, - "license": "MIT" - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", "license": "MIT", "engines": { - "node": ">=0.1.90" + "node": ">=6.9.0" } }, - "node_modules/@commitlint/config-validator": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.0.3.tgz", - "integrity": "sha512-2D3r4PKjoo59zBc2auodrSCaUnCSALCx54yveOFwwP/i2kfEAQrygwOleFWswLqK0UL/F9r07MFi5ev2ohyM4Q==", + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@commitlint/types": "^19.0.3", - "ajv": "^8.11.0" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { - "node": ">=v18" + "node": ">=6.9.0" } }, - "node_modules/@commitlint/execute-rule": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-19.0.0.tgz", - "integrity": "sha512-mtsdpY1qyWgAO/iOK0L6gSGeR7GFcdW7tIjcNFxcWkfLDF5qVbPHKuGATFqRMsxcO8OUKNj0+3WOHB7EHm4Jdw==", + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "license": "MIT", - "optional": true, "engines": { - "node": ">=v18" + "node": ">=6.9.0" } }, - "node_modules/@commitlint/load": { - "version": "19.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-19.4.0.tgz", - "integrity": "sha512-I4lCWaEZYQJ1y+Y+gdvbGAx9pYPavqZAZ3/7/8BpWh+QjscAn8AjsUpLV2PycBsEx7gupq5gM4BViV9xwTIJuw==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "@commitlint/config-validator": "^19.0.3", - "@commitlint/execute-rule": "^19.0.0", - "@commitlint/resolve-extends": "^19.1.0", - "@commitlint/types": "^19.0.3", - "chalk": "^5.3.0", - "cosmiconfig": "^9.0.0", - "cosmiconfig-typescript-loader": "^5.0.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "lodash.uniq": "^4.5.0" - }, "engines": { - "node": ">=v18" + "node": ">=6.9.0" } }, - "node_modules/@commitlint/load/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "license": "MIT", - "optional": true, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=6.9.0" } }, - "node_modules/@commitlint/resolve-extends": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-19.1.0.tgz", - "integrity": "sha512-z2riI+8G3CET5CPgXJPlzftH+RiWYLMYv4C9tSLdLXdr6pBNimSKukYP9MS27ejmscqCTVA4almdLh0ODD2KYg==", + "node_modules/@babel/helpers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@commitlint/config-validator": "^19.0.3", - "@commitlint/types": "^19.0.3", - "global-directory": "^4.0.1", - "import-meta-resolve": "^4.0.0", - "lodash.mergewith": "^4.6.2", - "resolve-from": "^5.0.0" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { - "node": ">=v18" + "node": ">=6.9.0" } }, - "node_modules/@commitlint/types": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.0.3.tgz", - "integrity": "sha512-tpyc+7i6bPG9mvaBbtKUeghfyZSDgWquIDfMgqYtTbmZ9Y9VzEm2je9EYcQ0aoz5o7NvGS+rcDec93yO08MHYA==", + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@types/conventional-commits-parser": "^5.0.0", - "chalk": "^5.3.0" + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { - "node": ">=v18" + "node": ">=6.9.0" } }, - "node_modules/@commitlint/types/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "node_modules/@babel/parser": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "dependencies": { + "@babel/types": "^7.25.2" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=12" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, "license": "MIT", "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@efstajas/versioned-parser": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@efstajas/versioned-parser/-/versioned-parser-0.1.4.tgz", - "integrity": "sha512-R/MUEOeMGvegThqacHCasp03RtE66szRqt9d6Qa+LI2lNweyV1SyAk66JlgRppoJHXwmcIDJ5hAUEws0g/Xx2Q==", - "license": "Apache-2.0" - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@babel/helper-plugin-utils": "^7.10.4" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { - "node": "*" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=10.10.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": "*" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, "license": "MIT", "dependencies": { - "sprintf-js": "~1.0.2" + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "@babel/helper-plugin-utils": "^7.24.7" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, + "node_modules/@babel/runtime": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "regenerator-runtime": "^0.14.0" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@babel/traverse": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", + "debug": "^4.3.1", + "globals": "^11.1.0" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "node_modules/@babel/types": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=6.9.0" } }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "MIT" }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=0.1.90" } }, - "node_modules/@jest/console/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@commitlint/config-validator": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.0.3.tgz", + "integrity": "sha512-2D3r4PKjoo59zBc2auodrSCaUnCSALCx54yveOFwwP/i2kfEAQrygwOleFWswLqK0UL/F9r07MFi5ev2ohyM4Q==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "color-name": "~1.1.4" + "@commitlint/types": "^19.0.3", + "ajv": "^8.11.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=v18" } }, - "node_modules/@jest/console/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/console/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@commitlint/execute-rule": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-19.0.0.tgz", + "integrity": "sha512-mtsdpY1qyWgAO/iOK0L6gSGeR7GFcdW7tIjcNFxcWkfLDF5qVbPHKuGATFqRMsxcO8OUKNj0+3WOHB7EHm4Jdw==", "dev": true, "license": "MIT", + "optional": true, "engines": { - "node": ">=8" + "node": ">=v18" } }, - "node_modules/@jest/console/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/@commitlint/load": { + "version": "19.4.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-19.4.0.tgz", + "integrity": "sha512-I4lCWaEZYQJ1y+Y+gdvbGAx9pYPavqZAZ3/7/8BpWh+QjscAn8AjsUpLV2PycBsEx7gupq5gM4BViV9xwTIJuw==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "has-flag": "^4.0.0" + "@commitlint/config-validator": "^19.0.3", + "@commitlint/execute-rule": "^19.0.0", + "@commitlint/resolve-extends": "^19.1.0", + "@commitlint/types": "^19.0.3", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^5.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" }, "engines": { - "node": ">=8" + "node": ">=v18" } }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "node_modules/@commitlint/load/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, + "optional": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@commitlint/resolve-extends": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-19.1.0.tgz", + "integrity": "sha512-z2riI+8G3CET5CPgXJPlzftH+RiWYLMYv4C9tSLdLXdr6pBNimSKukYP9MS27ejmscqCTVA4almdLh0ODD2KYg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "color-convert": "^2.0.1" + "@commitlint/config-validator": "^19.0.3", + "@commitlint/types": "^19.0.3", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=v18" } }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@commitlint/types": { + "version": "19.0.3", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.0.3.tgz", + "integrity": "sha512-tpyc+7i6bPG9mvaBbtKUeghfyZSDgWquIDfMgqYtTbmZ9Y9VzEm2je9EYcQ0aoz5o7NvGS+rcDec93yO08MHYA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" }, "engines": { - "node": ">=10" + "node": ">=v18" + } + }, + "node_modules/@commitlint/types/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@jest/core/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { - "node": ">=7.0.0" + "node": ">=12" } }, - "node_modules/@jest/core/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/core/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" } }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "node_modules/@efstajas/versioned-parser": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@efstajas/versioned-parser/-/versioned-parser-0.1.4.tgz", + "integrity": "sha512-R/MUEOeMGvegThqacHCasp03RtE66szRqt9d6Qa+LI2lNweyV1SyAk66JlgRppoJHXwmcIDJ5hAUEws0g/Xx2Q==", + "license": "Apache-2.0" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" + "eslint-visitor-keys": "^3.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } + "license": "MIT" }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "color-convert": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "*" } }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@jest/reporters/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "color-name": "~1.1.4" + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { - "node": ">=7.0.0" + "node": ">=10.10.0" } }, - "node_modules/@jest/reporters/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/reporters/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "has-flag": "^4.0.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=8" + "node": "*" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, + "license": "Apache-2.0", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } + "license": "BSD-3-Clause" }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@jest/transform/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/@jest/transform/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" }, "engines": { "node": ">=8" } }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/types/node_modules/ansi-styles": { + "node_modules/@jest/console/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", @@ -1682,7 +2088,7 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jest/types/node_modules/chalk": { + "node_modules/@jest/console/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", @@ -1699,7 +2105,7 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@jest/types/node_modules/color-convert": { + "node_modules/@jest/console/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", @@ -1712,14 +2118,14 @@ "node": ">=7.0.0" } }, - "node_modules/@jest/types/node_modules/color-name": { + "node_modules/@jest/console/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, - "node_modules/@jest/types/node_modules/has-flag": { + "node_modules/@jest/console/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", @@ -1729,7 +2135,7 @@ "node": ">=8" } }, - "node_modules/@jest/types/node_modules/supports-color": { + "node_modules/@jest/console/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", @@ -1742,297 +2148,1934 @@ "node": ">=8" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=6.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=6.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "license": "MIT" }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "engines": { + "node": ">=8" + } }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.3.2" + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">= 16" + "node": ">=10" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", + "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", + "license": "MIT", + "optional": true, + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@rushstack/node-core-library": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.12.0.tgz", + "integrity": "sha512-QSwwzgzWoil1SCQse+yCHwlhRxNv2dX9siPnAb9zR/UmMhac4mjMrlMZpk64BlCeOFi1kJKgXRkihSwRMbboAQ==", + "license": "MIT", + "dependencies": { + "ajv": "~8.13.0", + "ajv-draft-04": "~1.0.0", + "ajv-formats": "~3.0.1", + "fs-extra": "~11.3.0", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/@rushstack/terminal": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.1.tgz", + "integrity": "sha512-3vgJYwumcjoDOXU3IxZfd616lqOdmr8Ezj4OWgJZfhmiBK4Nh7eWcv8sU8N/HdzXcuHDXCRGn/6O2Q75QvaZMA==", + "license": "MIT", + "dependencies": { + "@rushstack/node-core-library": "5.12.0", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@rushstack/terminal/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.23.6", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.6.tgz", + "integrity": "sha512-7WepygaF3YPEoToh4MAL/mmHkiIImQq3/uAkQX46kVoKTNOOlCtFGyNnze6OYuWw2o9rxsyrHVfIBKxq/am2RA==", + "license": "MIT", + "dependencies": { + "@rushstack/terminal": "0.15.1", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "node_modules/@rushstack/ts-command-line/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz", + "integrity": "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/abort-controller/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/config-resolver": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.0.1.tgz", + "integrity": "sha512-Igfg8lKu3dRVkTSEm98QpZUvKEOa71jDX4vKRcvJVyRc3UgN3j7vFMf0s7xLQhYmKa8kyJGQgUJDOV5V3neVlQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/core": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.1.5.tgz", + "integrity": "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/middleware-serde": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.1.tgz", + "integrity": "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.1.tgz", + "integrity": "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.1.tgz", + "integrity": "sha512-TJ6oZS+3r2Xu4emVse1YPB3Dq3d8RkZDKcPr71Nj/lJsdAP1c7oFzYqEn1IBc915TsgLl2xIJNuxCz+gLbLE0w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.1.tgz", + "integrity": "sha512-gdudFPf4QRQ5pzj7HEnu6FhKRi61BfH/Gk5Yf6O0KiSbr1LlVhgjThcvjdu658VE6Nve8vaIWB8/fodmS1rBPQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.1.tgz", + "integrity": "sha512-OGXo7w5EkB5pPiac7KNzVtfCW2vKBTZNuCctn++TTSOMpe6RZO/n6WEC1AxJINn3+vWLKW49uad3lo/u0WJ9oQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.6.tgz", + "integrity": "sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.0.7.tgz", + "integrity": "sha512-58j9XbUPLkqAcV1kHzVX/kAR16GT+j7DUZJqwzsxh1jtz7G82caZiGyyFgUvogVfNTg3TeAOIJepGc8TXF4AVQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/service-error-classification": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.2.tgz", + "integrity": "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.1.tgz", + "integrity": "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.0.1.tgz", + "integrity": "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.3.tgz", + "integrity": "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.1.tgz", + "integrity": "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.0.1.tgz", + "integrity": "sha512-TE4cpj49jJNB/oHyh/cRVEgNZaoPaxd4vteJNB0yGidOCVR0jCw/hjPVsT8Q8FRmj8Bd3bFZt8Dh7xGCT+xMBQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.1.tgz", + "integrity": "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.1.tgz", + "integrity": "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.1.tgz", + "integrity": "sha512-3JNjBfOWpj/mYfjXJHB4Txc/7E4LVq32bwzE7m28GN79+M1f76XHflUaSUkhOriprPDzev9cX/M+dEB80DNDKA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.1.tgz", + "integrity": "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.0.1.tgz", + "integrity": "sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/smithy-client": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.1.6.tgz", + "integrity": "sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.1.0.tgz", + "integrity": "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.1.tgz", + "integrity": "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "tslib": "^2.6.2" }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-config-provider/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.7.tgz", + "integrity": "sha512-CZgDDrYHLv0RUElOsmZtAnp1pIjwDVCSuZWOPhIOBvG36RDfX1Q9+6lS61xBf+qqvHoqRjHxgINeQz47cYFC2Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-defaults-mode-browser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.7.tgz", + "integrity": "sha512-79fQW3hnfCdrfIi1soPbK3zmooRFnLpSx3Vxi6nUlqaaQeC5dm8plt4OTNDNqEEEDkvKghZSaoti684dQFVrGQ==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@smithy/config-resolver": "^4.0.1", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@redis/bloom": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", - "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } + "node_modules/@smithy/util-defaults-mode-node/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true }, - "node_modules/@redis/client": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", - "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", - "license": "MIT", + "node_modules/@smithy/util-endpoints": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.1.tgz", + "integrity": "sha512-zVdUENQpdtn9jbpD9SCFK4+aSiavRb9BxEtw9ZGUR1TYo6bBHbIoi7VkrFQ0/RwZlzx0wRBaRmPclj8iAoJCLA==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14" + "node": ">=18.0.0" } }, - "node_modules/@redis/client/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "node_modules/@smithy/util-endpoints/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true }, - "node_modules/@redis/graph": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", - "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@redis/json": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", - "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@smithy/util-hex-encoding/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.1.tgz", + "integrity": "sha512-HiLAvlcqhbzhuiOa0Lyct5IIlyIz0PQO5dnMlmQ/ubYM46dPInB+3yQGkfxsk6Q24Y0n3/JmcA1v5iEhmOF5mA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@redis/search": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", - "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@smithy/util-middleware/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.1.tgz", + "integrity": "sha512-WmRHqNVwn3kI3rKk1LsKcVgPBG6iLTBGC1iYOV3GQegwJ3E8yjzHytPt26VNzOWr1qu0xE03nK0Ug8S7T7oufw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/service-error-classification": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@redis/time-series": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", - "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", - "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" + "node_modules/@smithy/util-retry/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-stream": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.1.2.tgz", + "integrity": "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" + "node_modules/@smithy/util-stream/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "type-detect": "4.0.8" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/@smithy/util-uri-escape/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, + "node_modules/@smithy/util-utf8/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -2077,6 +4120,12 @@ "typescript": ">=4.7.0" } }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2122,6 +4171,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bluebird": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", + "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==", + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -2157,6 +4212,15 @@ "@types/node": "*" } }, + "node_modules/@types/continuation-local-storage": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@types/continuation-local-storage/-/continuation-local-storage-3.2.7.tgz", + "integrity": "sha512-Q7dPOymVpRG5Zpz90/o26+OAqOG2Sw+FED7uQmTrJNCF/JAPTylclZofMxZKd6W7g1BDPmT9/C/jX0ZcSNTQwQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -2282,6 +4346,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2365,6 +4435,18 @@ "@types/node": "*" } }, + "node_modules/@types/sequelize": { + "version": "4.28.20", + "resolved": "https://registry.npmjs.org/@types/sequelize/-/sequelize-4.28.20.tgz", + "integrity": "sha512-XaGOKRhdizC87hDgQ0u3btxzbejlF+t6Hhvkek1HyphqCI4y7zVBIVAGmuc4cWJqGpxusZ1RiBToHHnNK/Edlw==", + "license": "MIT", + "dependencies": { + "@types/bluebird": "*", + "@types/continuation-local-storage": "*", + "@types/lodash": "*", + "@types/validator": "*" + } + }, "node_modules/@types/serve-static": { "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", @@ -2390,12 +4472,39 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/umzug": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/umzug/-/umzug-2.3.9.tgz", + "integrity": "sha512-YIA3UL9MgOVP2RC7yF6z6Mrp1hrLFccDuXYxKuXdr/+Qqy8awWnDK1RHNy5Rp+sHQ7qwke85dp+SZTGfS+MqPg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/sequelize": "*", + "mongodb": "^4.1.4" + } + }, "node_modules/@types/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==", "license": "MIT" }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -2618,6 +4727,16 @@ "dev": true, "license": "ISC" }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2677,9 +4796,7 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, "license": "MIT", - "optional": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2691,6 +4808,37 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3158,7 +5306,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -3242,6 +5389,13 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -3281,6 +5435,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT", + "optional": true + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -3295,7 +5456,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3360,11 +5520,22 @@ "node-int64": "^0.4.0" } }, + "node_modules/bson": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", + "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", + "license": "Apache-2.0", + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -3601,6 +5772,23 @@ "dev": true, "license": "MIT" }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -3786,6 +5974,16 @@ "node": ">=8" } }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/commitizen": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.3.0.tgz", @@ -3823,6 +6021,24 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -4046,9 +6262,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -4081,6 +6297,20 @@ "@commitlint/load": ">6.1.1" } }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -4379,6 +6609,48 @@ "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", "license": "MIT" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4412,7 +6684,6 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -4598,6 +6869,62 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -5045,6 +7372,22 @@ "node": ">=8" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -5166,6 +7509,17 @@ "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", "license": "MIT" }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -5276,6 +7630,16 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -5295,14 +7659,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -5319,7 +7681,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -5346,15 +7707,35 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", - "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], "license": "MIT", - "optional": true + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -5432,7 +7813,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -5575,6 +7955,36 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6127,7 +8537,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -6188,6 +8597,15 @@ "node": ">=4" } }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -6409,6 +8827,25 @@ "url": "https://opencollective.com/ioredis" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6498,7 +8935,6 @@ "version": "2.15.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6544,7 +8980,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6574,7 +9009,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -6609,7 +9043,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -6640,6 +9073,13 @@ "node": ">=8" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -6877,6 +9317,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -8749,6 +11205,81 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "license": "MIT" + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -8776,6 +11307,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8807,9 +11344,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -8835,7 +11370,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -9119,6 +11653,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/luxon": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", @@ -9171,6 +11715,33 @@ "node": ">= 0.6" } }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT", + "optional": true + }, "node_modules/merge": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", @@ -9198,7 +11769,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -9217,7 +11787,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -9295,6 +11864,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -9329,6 +11908,34 @@ "node": "*" } }, + "node_modules/mongodb": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.17.2.tgz", + "integrity": "sha512-mLV7SEiov2LHleRJPMPrK2PMyhXFZt2UQLC4VD4pnth3jMjYKHhtqfwwkkvS/NXuo/Fp3vbhaNcXrIDaLRb9Tg==", + "license": "Apache-2.0", + "dependencies": { + "bson": "^4.7.2", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=12.9.0" + }, + "optionalDependencies": { + "@aws-sdk/credential-providers": "^3.186.0", + "@mongodb-js/saslprep": "^1.1.0" + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9397,6 +12004,13 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", @@ -9480,6 +12094,22 @@ "node": "*" } }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -9844,6 +12474,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9928,9 +12565,32 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", @@ -10115,7 +12775,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -10203,6 +12862,15 @@ "node": ">=8" } }, + "node_modules/pony-cause": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", + "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", + "license": "0BSD", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -10343,6 +13011,13 @@ "node": ">= 6" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -10367,7 +13042,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10409,7 +13083,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -10576,9 +13249,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -10587,7 +13258,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -10672,7 +13342,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -10710,7 +13379,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -10938,6 +13606,83 @@ } } }, + "node_modules/sequelize-cli": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.2.tgz", + "integrity": "sha512-V8Oh+XMz2+uquLZltZES6MVAD+yEnmMfwfn+gpXcDiwE3jyQygLt4xoI0zG8gKt6cRcs84hsKnXAKDQjG/JAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-color": "^2.0.3", + "fs-extra": "^9.1.0", + "js-beautify": "^1.14.5", + "lodash": "^4.17.21", + "resolve": "^1.22.1", + "umzug": "^2.3.0", + "yargs": "^16.2.0" + }, + "bin": { + "sequelize": "lib/sequelize", + "sequelize-cli": "lib/sequelize" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sequelize-cli/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/sequelize-cli/node_modules/umzug": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", + "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/sequelize-cli/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sequelize-cli/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/sequelize-pool": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", @@ -11093,6 +13838,30 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11113,6 +13882,16 @@ "source-map": "^0.6.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -11126,7 +13905,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/stack-trace": { @@ -11186,6 +13964,15 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-format": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", @@ -11222,6 +14009,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -11284,6 +14087,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -11307,6 +14124,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -11324,7 +14154,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11434,6 +14263,20 @@ "dev": true, "license": "MIT" }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -11468,7 +14311,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -11502,6 +14344,18 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -11761,6 +14615,13 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "license": "0BSD" }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12042,6 +14903,34 @@ "node": ">=0.8.0" } }, + "node_modules/umzug": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-3.8.2.tgz", + "integrity": "sha512-BEWEF8OJjTYVC56GjELeHl/1XjFejrD7aHzn+HldRJTx+pL1siBrKHZC8n4K/xL3bEzVA9o++qD1tK2CpZu4KA==", + "license": "MIT", + "dependencies": { + "@rushstack/ts-command-line": "^4.12.2", + "emittery": "^0.13.0", + "fast-glob": "^3.3.2", + "pony-cause": "^2.1.4", + "type-fest": "^4.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/umzug/node_modules/type-fest": { + "version": "4.37.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz", + "integrity": "sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -12074,7 +14963,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -12124,7 +15012,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -12214,6 +15101,28 @@ "defaults": "^1.0.3" } }, + "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", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12368,6 +15277,61 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", diff --git a/package.json b/package.json index a792161..8140105 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "postbuild": "rm -rf ./dist/src/config/chainConfigs && cp -r ./src/config/chainConfigs ./dist/src/config/chainConfigs", "dev": "npx nodemon", "start": "node dist/src/index.js", - "check": "tsc --noEmit" + "check": "tsc --noEmit", + "sequelize": "ts-node --transpile-only node_modules/sequelize-cli/lib/sequelize", + "make:migration": "npm run sequelize migration:generate -- --migrations-path src/db/migrations" }, "keywords": [], "author": "", @@ -43,6 +45,7 @@ "jest": "^29.7.0", "nodemon": "^3.0.1", "prettier": "^3.0.3", + "sequelize-cli": "^6.6.2", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typechain": "^8.3.1", @@ -50,6 +53,7 @@ }, "dependencies": { "@efstajas/versioned-parser": "^0.1.4", + "@types/umzug": "^2.3.9", "bee-queue": "^1.5.0", "bull-arena": "^4.0.0", "dotenv": "^16.3.1", @@ -60,6 +64,7 @@ "pg": "^8.11.3", "redis": "^4.6.10", "sequelize": "^6.32.1", + "umzug": "^3.8.2", "winston": "^3.10.0", "zod": "^3.22.2" } diff --git a/src/db/database.ts b/src/db/database.ts index a9d0ace..c3b53f3 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -9,7 +9,7 @@ import { RepoDriverSplitReceiverModel, } from '../models'; import appSettings from '../config/appSettings'; -import unreachableError from '../utils/unreachableError'; +import { runMigrations } from './runMigrations'; const { postgresConnectionString } = appSettings; @@ -23,12 +23,11 @@ export async function connectToDb(): Promise { logger.info('Initializing database...'); await authenticate(); + await runMigrations(dbConnection); registerModels(); await initializeEntities(); defineAssociations(); - await dbConnection.sync(); - logger.info('Database initialized.'); } @@ -38,13 +37,6 @@ async function authenticate(): Promise { logger.info('Connection has been established successfully.'); - const schema = `"${( - appSettings.network || - unreachableError('Missing network in app settings.') - ).replace(/"/g, '""')}"`; - - await dbConnection.query(`CREATE SCHEMA IF NOT EXISTS ${schema};`); - await dbConnection.authenticate(); } catch (error: any) { logger.error( diff --git a/src/db/migrations/20250317125844-initial.ts b/src/db/migrations/20250317125844-initial.ts new file mode 100644 index 0000000..885026d --- /dev/null +++ b/src/db/migrations/20250317125844-initial.ts @@ -0,0 +1,1028 @@ +import { DataTypes, literal, type QueryInterface } from 'sequelize'; +import { COMMON_EVENT_INIT_ATTRIBUTES, FORGES_MAP } from '../../core/constants'; +import { ProjectVerificationStatus } from '../../models/GitProjectModel'; +import getSchema from '../../utils/getSchema'; +import type { DbSchema } from '../../core/types'; + +export async function up({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface = sequelize.getQueryInterface(); + + await createTableIfNotExists( + queryInterface, + schema, + '_LastIndexedBlock', + createLastIndexedBlockTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'AccountMetadataEmittedEvents', + createAccountMetadataEmittedEventsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'GitProjects', + createProjectsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'DripLists', + createDripListsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'AddressDriverSplitReceivers', + createAddressDriverSplitReceiversTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'DripListSplitReceivers', + createDripListSplitReceiversTable, + ); + + await createTableIfNotExists( + queryInterface, + schema, + 'GivenEvents', + createGivenEventsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'OwnerUpdatedEvents', + createOwnerUpdatedEventsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'OwnerUpdateRequestedEvents', + createOwnerUpdateRequestedEventsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'RepoDriverSplitReceivers', + createRepoDriverSplitReceiversTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'SplitEvents', + createSplitEventsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'SplitsSetEvents', + createSplitsSetEventsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'SqueezedStreamsEvents', + createSqueezedStreamsEventsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'StreamReceiverSeenEvents', + createStreamReceiverSeenEventsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'StreamsSetEvents', + createStreamsSetEventsTable, + ); + await createTableIfNotExists( + queryInterface, + schema, + 'TransferEvents', + createTransferEventsTable, + ); +} + +async function createTransferEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'TransferEvents', schema }, + { + tokenId: { + type: DataTypes.STRING, + allowNull: false, + }, + from: { + type: DataTypes.STRING, + allowNull: false, + }, + to: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + ); +} + +async function createStreamsSetEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'StreamsSetEvents', schema }, + { + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + erc20: { + type: DataTypes.STRING, + allowNull: false, + }, + receiversHash: { + type: DataTypes.STRING, + allowNull: false, + }, + streamsHistoryHash: { + type: DataTypes.STRING, + allowNull: false, + }, + balance: { + type: DataTypes.STRING, + allowNull: false, + }, + maxEnd: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + ); + + await queryInterface.addIndex( + { tableName: 'StreamsSetEvents', schema }, + ['accountId'], + { + name: 'IX_StreamsSetEvents_accountId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'StreamsSetEvents', schema }, + ['receiversHash'], + { + name: 'IX_StreamsSetEvents_receiversHash', + unique: false, + }, + ); +} + +async function createStreamReceiverSeenEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'StreamReceiverSeenEvents', schema }, + { + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + receiversHash: { + type: DataTypes.STRING, + allowNull: false, + }, + config: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + ); + + await queryInterface.addIndex( + { tableName: 'StreamReceiverSeenEvents', schema }, + ['accountId'], + { + name: 'IX_StreamReceiverSeenEvents_accountId', + unique: false, + }, + ); +} + +async function createSqueezedStreamsEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'SqueezedStreamsEvents', schema }, + { + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + erc20: { + type: DataTypes.STRING, + allowNull: false, + }, + senderId: { + type: DataTypes.STRING, + allowNull: false, + }, + amount: { + type: DataTypes.STRING, + allowNull: false, + }, + streamsHistoryHashes: { + type: DataTypes.JSON, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + ); +} + +async function createSplitsSetEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'SplitsSetEvents', schema }, + { + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + receiversHash: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + ); +} + +async function createSplitEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'SplitEvents', schema }, + { + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + receiver: { + type: DataTypes.STRING, + allowNull: false, + }, + erc20: { + type: DataTypes.STRING, + allowNull: false, + }, + amt: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SplitEvents', schema }, + ['receiver'], + { + name: 'IX_SplitEvents_receiver', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SplitEvents', schema }, + ['accountId', 'receiver'], + { + name: 'IX_SplitEvents_accountId_receiver', + unique: false, + }, + ); +} + +async function createRepoDriverSplitReceiversTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'RepoDriverSplitReceivers', schema }, + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + fundeeProjectId: { + type: DataTypes.STRING, + allowNull: false, + }, + funderProjectId: { + type: DataTypes.STRING, + allowNull: true, + }, + funderDripListId: { + type: DataTypes.STRING, + allowNull: true, + }, + weight: { + type: DataTypes.INTEGER, + allowNull: true, + }, + type: { + type: DataTypes.ENUM('DependencyType'), + allowNull: false, + }, + blockTimestamp: { + type: DataTypes.DATE, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + }, + ); + + await queryInterface.addIndex( + { tableName: 'RepoDriverSplitReceivers', schema }, + ['fundeeProjectId'], + { + name: 'IX_RepoDriverSplitReceivers_fundeeProjectId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'RepoDriverSplitReceivers', schema }, + ['funderProjectId'], + { + name: 'IX_RepoDriverSplitReceivers_funderProjectId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'RepoDriverSplitReceivers', schema }, + ['funderDripListId'], + { + name: 'IX_RepoDriverSplitReceivers_funderDripListId', + unique: false, + }, + ); +} + +async function createOwnerUpdateRequestedEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'OwnerUpdateRequestedEvents', schema }, + { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + forge: { + type: DataTypes.ENUM(...Object.values(FORGES_MAP)), + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + ); +} + +async function createOwnerUpdatedEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'OwnerUpdatedEvents', schema }, + { + owner: { + type: DataTypes.STRING, + allowNull: false, + }, + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + ); +} + +async function createGivenEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'GivenEvents', schema }, + { + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + receiver: { + type: DataTypes.STRING, + allowNull: false, + }, + erc20: { + type: DataTypes.STRING, + allowNull: false, + }, + amt: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + ); + + await queryInterface.addIndex( + { tableName: 'GivenEvents', schema }, + ['accountId'], + { + name: 'IX_GivenEvents_accountId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'GivenEvents', schema }, + ['receiver'], + { + name: 'IX_GivenEvents_receiver', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'GivenEvents', schema }, + ['erc20'], + { + name: 'IX_GivenEvents_erc20', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'GivenEvents', schema }, + ['transactionHash', 'logIndex'], + { + name: 'IX_GivenEvents_transactionHash_logIndex', + unique: false, + }, + ); +} + +async function createDripListSplitReceiversTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'DripListSplitReceivers', schema }, + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + fundeeDripListId: { + type: DataTypes.STRING, + allowNull: false, + }, + funderProjectId: { + type: DataTypes.STRING, + allowNull: true, + }, + funderDripListId: { + type: DataTypes.STRING, + allowNull: true, + }, + weight: { + type: DataTypes.INTEGER, + allowNull: true, + }, + type: { + type: DataTypes.ENUM('DependencyType'), + allowNull: false, + }, + blockTimestamp: { + type: DataTypes.DATE, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + }, + ); + + await queryInterface.addIndex( + { tableName: 'DripListSplitReceivers', schema }, + ['fundeeDripListId'], + { + name: 'IX_DripListSplitReceivers_fundeeDripListId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'DripListSplitReceivers', schema }, + ['funderProjectId'], + { + name: 'IX_DripListSplitReceivers_funderProjectId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'DripListSplitReceivers', schema }, + ['funderDripListId'], + { + name: 'IX_DripListSplitReceivers_funderDripListId', + unique: false, + }, + ); +} + +async function createDripListsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'DripLists', schema }, + { + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + isValid: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + ownerAddress: { + type: DataTypes.STRING, + allowNull: false, + }, + ownerAccountId: { + type: DataTypes.STRING, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: true, + }, + latestVotingRoundId: { + type: DataTypes.UUID, + allowNull: true, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + creator: { + type: DataTypes.STRING, + allowNull: false, + }, + previousOwnerAddress: { + type: DataTypes.STRING, + allowNull: false, + }, + isVisible: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + lastProcessedIpfsHash: { + type: DataTypes.TEXT, + allowNull: true, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + }, + ); + + await queryInterface.addIndex( + { tableName: 'DripLists', schema }, + ['ownerAddress'], + { + name: 'IX_DripLists_ownerAddress', + unique: false, + where: { isValid: true }, + }, + ); +} + +async function createAddressDriverSplitReceiversTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'AddressDriverSplitReceivers', schema }, + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + fundeeAccountId: { + type: DataTypes.STRING, + allowNull: false, + }, + fundeeAccountAddress: { + type: DataTypes.STRING, + allowNull: false, + }, + funderProjectId: { + type: DataTypes.STRING, + references: { + model: 'GitProjects', + key: 'id', + }, + allowNull: true, + }, + funderDripListId: { + type: DataTypes.STRING, + references: { + model: 'DripLists', + key: 'id', + }, + allowNull: true, + }, + weight: { + type: DataTypes.INTEGER, + allowNull: true, + }, + type: { + type: DataTypes.ENUM('AddressDriverSplitReceiverType'), + allowNull: false, + }, + blockTimestamp: { + type: DataTypes.DATE, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + }, + ); + + await queryInterface.addIndex( + { tableName: 'AddressDriverSplitReceivers', schema }, + ['fundeeAccountId'], + { + name: 'IX_AddressDriverSplitReceivers_fundeeAccountId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'AddressDriverSplitReceivers', schema }, + ['funderDripListId'], + { + name: 'IX_AddressDriverSplitReceivers_funderDripListId', + unique: false, + }, + ); +} + +async function createAccountMetadataEmittedEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'AccountMetadataEmittedEvents', schema }, + { + key: { + type: DataTypes.STRING, + allowNull: false, + }, + value: { + type: DataTypes.STRING, + allowNull: false, + }, + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + ); + + await queryInterface.addIndex( + { tableName: 'AccountMetadataEmittedEvents', schema }, + ['accountId'], + { + name: 'IX_AccountMetadataEmittedEvents_accountId', + unique: false, + }, + ); +} + +async function createLastIndexedBlockTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: '_LastIndexedBlock', schema }, + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + blockNumber: { + type: DataTypes.BIGINT, + allowNull: false, + unique: true, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + }, + ); +} + +async function createProjectsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'GitProjects', schema }, + { + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + isValid: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: true, + }, + verificationStatus: { + type: DataTypes.ENUM(...Object.values(ProjectVerificationStatus)), + allowNull: false, + }, + claimedAt: { + type: DataTypes.DATE, + allowNull: true, + }, + forge: { + type: DataTypes.ENUM(...Object.values(FORGES_MAP)), + allowNull: true, + }, + ownerAddress: { + type: DataTypes.STRING, + allowNull: true, + }, + ownerAccountId: { + type: DataTypes.STRING, + allowNull: true, + }, + url: { + type: DataTypes.STRING, + allowNull: true, + }, + emoji: { + type: DataTypes.STRING, + allowNull: true, + }, + avatarCid: { + type: DataTypes.STRING, + allowNull: true, + }, + color: { + type: DataTypes.STRING, + allowNull: true, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + isVisible: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + lastProcessedIpfsHash: { + type: DataTypes.TEXT, + allowNull: true, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + }, + ); + + await queryInterface.addIndex( + { tableName: 'GitProjects', schema }, + ['ownerAddress'], + { + name: 'IX_GitProjects_ownerAddress', + unique: false, + where: { isValid: true }, + }, + ); + + await queryInterface.addIndex( + { tableName: 'GitProjects', schema }, + ['verificationStatus'], + { + name: 'IX_GitProjects_verificationStatus', + unique: false, + where: { isValid: true }, + }, + ); + + await queryInterface.addIndex({ tableName: 'GitProjects', schema }, ['url'], { + name: 'IX_GitProjects_url', + unique: false, + where: { isValid: true }, + }); +} + +async function createTableIfNotExists( + queryInterface: QueryInterface, + schema: DbSchema, + tableName: string, + createFn: (queryInterface: QueryInterface, schema: DbSchema) => Promise, +) { + const tableFullName = { tableName, schema }; + + const tableExists = await queryInterface + .describeTable(tableFullName) + .catch(() => null); + if (!tableExists) { + await createFn(queryInterface, schema); + } +} + +export async function down(queryInterface: QueryInterface): Promise { + const schema = getSchema(); + await queryInterface.sequelize.query( + `DROP SCHEMA IF EXISTS "${schema}" CASCADE`, + ); +} diff --git a/src/db/runMigrations.ts b/src/db/runMigrations.ts new file mode 100644 index 0000000..0bd81a4 --- /dev/null +++ b/src/db/runMigrations.ts @@ -0,0 +1,32 @@ +import type { Sequelize } from 'sequelize'; +import { Umzug, SequelizeStorage } from 'umzug'; +import logger from '../core/logger'; +import getSchema from '../utils/getSchema'; + +export async function runMigrations(sequelize: Sequelize): Promise { + const schema = getSchema(); + + // Ensure schema exists before running migrations + await sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`); + + const migrator = new Umzug({ + migrations: { + glob: './dist/src/db/migrations/*.js', + }, + context: sequelize, + storage: new SequelizeStorage({ sequelize, schema: getSchema() }), + logger: console, + }); + + // Apply any pending migrations. + const migrations = await migrator.up(); + + if (migrations.length > 0) { + const appliedNames = migrations.map((m) => m.name).join(', '); + logger.info(`Applied migrations: ${appliedNames}`); + } else { + logger.info( + 'No migrations were applied. The database is already up-to-date.', + ); + } +} diff --git a/src/index.ts b/src/index.ts index e6e9601..580210d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ import './events/types'; import networkConstant from '../contracts/CURRENT_NETWORK/network-constant'; process.on('uncaughtException', (error: Error) => { - logger.error(`Uncaught Exception: ${error.message}`); + logger.error(`Uncaught Exception: ${error.message}. Stack: ${error.stack}`); // Railway will restart the process if it exits with a non-zero exit code. process.exit(1); diff --git a/tsconfig.json b/tsconfig.json index 544d2fb..028c5be 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,109 +1,12 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "target": "ES2020", + "module": "commonjs", + "resolveJsonModule": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true } } From cdf933e2f7613a18e5ce2ba1b9596269e70f21b8 Mon Sep 17 00:00:00 2001 From: Ioannis T Date: Wed, 19 Mar 2025 11:19:42 +0100 Subject: [PATCH 02/60] feat: add support for ImmutableSplits code generation --- scripts/codegen-any-chain-types.ts | 10 +- src/abi/filecoin/ImmutableSplitsDriver.json | 358 ++++++++++++++++++ src/abi/goerli/ImmutableSplitsDriver.json | 358 ++++++++++++++++++ .../localtestnet/ImmutableSplitsDriver.json | 358 ++++++++++++++++++ src/abi/mainnet/ImmutableSplitsDriver.json | 358 ++++++++++++++++++ src/abi/metis/ImmutableSplitsDriver.json | 358 ++++++++++++++++++ src/abi/optimism/ImmutableSplitsDriver.json | 358 ++++++++++++++++++ .../ImmutableSplitsDriver.json | 358 ++++++++++++++++++ .../polygon_amoy/ImmutableSplitsDriver.json | 358 ++++++++++++++++++ src/abi/sepolia/ImmutableSplitsDriver.json | 358 ++++++++++++++++++ src/config/appSettings.ts | 1 + src/db/database.ts | 6 +- src/db/modelRegistration.ts | 6 +- src/db/runMigrations.ts | 2 +- src/events/types.ts | 6 +- src/models/index.ts | 3 +- 16 files changed, 3248 insertions(+), 8 deletions(-) create mode 100644 src/abi/filecoin/ImmutableSplitsDriver.json create mode 100644 src/abi/goerli/ImmutableSplitsDriver.json create mode 100644 src/abi/localtestnet/ImmutableSplitsDriver.json create mode 100644 src/abi/mainnet/ImmutableSplitsDriver.json create mode 100644 src/abi/metis/ImmutableSplitsDriver.json create mode 100644 src/abi/optimism/ImmutableSplitsDriver.json create mode 100644 src/abi/optimism_sepolia/ImmutableSplitsDriver.json create mode 100644 src/abi/polygon_amoy/ImmutableSplitsDriver.json create mode 100644 src/abi/sepolia/ImmutableSplitsDriver.json diff --git a/scripts/codegen-any-chain-types.ts b/scripts/codegen-any-chain-types.ts index 97b5a08..33ed3f4 100644 --- a/scripts/codegen-any-chain-types.ts +++ b/scripts/codegen-any-chain-types.ts @@ -41,6 +41,7 @@ import { Drips as ${chainName}Drips } from './${chainName}/'; import { NftDriver as ${chainName}NftDriver } from './${chainName}/'; import { RepoDriver as ${chainName}RepoDriver } from './${chainName}/'; import { AddressDriver as ${chainName}AddressDriver } from './${chainName}/'; +import { ImmutableSplitsDriver as ${chainName}ImmutableSplitsDriver } from './${chainName}/'; import { TypedContractEvent as ${chainName}TypedContractEvent } from './${chainName}/common'; import { TypedLogDescription as ${chainName}TypedLogDescription } from './${chainName}/common';`; } @@ -54,11 +55,13 @@ export type AnyChainDrips = ${chainNames.map((name) => `${name}Drips`).join(' | export type AnyChainNftDriver = ${chainNames.map((name) => `${name}NftDriver`).join(' | ')}; export type AnyChainRepoDriver = ${chainNames.map((name) => `${name}RepoDriver`).join(' | ')}; export type AnyChainAddressDriver = ${chainNames.map((name) => `${name}AddressDriver`).join(' | ')}; +export type AnyChainImmutableSplitsDriver = ${chainNames.map((name) => `${name}ImmutableSplitsDriver`).join(' | ')}; export type AnyChainDripsFilters = ${chainNames.map((name) => `${name}Drips['filters']`).join(' & ')}; export type AnyChainNftDriverFilters = ${chainNames.map((name) => `${name}NftDriver['filters']`).join(' & ')}; export type AnyChainRepoDriverFilters = ${chainNames.map((name) => `${name}RepoDriver['filters']`).join(' & ')}; export type AnyChainAddressDriverFilters = ${chainNames.map((name) => `${name}AddressDriver['filters']`).join(' & ')}; +export type AnyChainImmutableSplitsDriverFilters = ${chainNames.map((name) => `${name}ImmutableSplitsDriver['filters']`).join(' & ')}; export type AnyChainTypedContractEvent = ${chainNames.map((name) => `${name}TypedContractEvent`).join(' | ')}; export type AnyChainTypedLogDescription = ${chainNames.map((name) => `${name}TypedLogDescription`).join(' | ')};`; @@ -70,7 +73,7 @@ function generateContractGetters() { return ` import type { Provider } from 'ethers'; -import { Drips__factory, NftDriver__factory, RepoDriver__factory, AddressDriver__factory } from './${process.env.NETWORK}'; +import { Drips__factory, NftDriver__factory, RepoDriver__factory, AddressDriver__factory, ImmutableSplitsDriver__factory } from './${process.env.NETWORK}'; export const getDripsContract: (contractAddress: string, provider: Provider) => AnyChainDrips = (contractAddress, provider) => Drips__factory.connect( contractAddress, @@ -90,6 +93,11 @@ export const getRepoDriverContract: (contractAddress: string, provider: Provider export const getAddressDriverContract: (contractAddress: string, provider: Provider) => AnyChainAddressDriver = (contractAddress, provider) => AddressDriver__factory.connect( contractAddress, provider +); + +export const getImmutableSplitsDriverContract: (contractAddress: string, provider: Provider) => AnyChainImmutableSplitsDriver = (contractAddress, provider) => ImmutableSplitsDriver__factory.connect( + contractAddress, + provider );`; } diff --git a/src/abi/filecoin/ImmutableSplitsDriver.json b/src/abi/filecoin/ImmutableSplitsDriver.json new file mode 100644 index 0000000..9879574 --- /dev/null +++ b/src/abi/filecoin/ImmutableSplitsDriver.json @@ -0,0 +1,358 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "_drips", "type": "address" }, + { "internalType": "uint32", "name": "_driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "CreatedSplits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "createSplits", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSplitsWeight", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/goerli/ImmutableSplitsDriver.json b/src/abi/goerli/ImmutableSplitsDriver.json new file mode 100644 index 0000000..9879574 --- /dev/null +++ b/src/abi/goerli/ImmutableSplitsDriver.json @@ -0,0 +1,358 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "_drips", "type": "address" }, + { "internalType": "uint32", "name": "_driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "CreatedSplits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "createSplits", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSplitsWeight", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/localtestnet/ImmutableSplitsDriver.json b/src/abi/localtestnet/ImmutableSplitsDriver.json new file mode 100644 index 0000000..9879574 --- /dev/null +++ b/src/abi/localtestnet/ImmutableSplitsDriver.json @@ -0,0 +1,358 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "_drips", "type": "address" }, + { "internalType": "uint32", "name": "_driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "CreatedSplits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "createSplits", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSplitsWeight", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/mainnet/ImmutableSplitsDriver.json b/src/abi/mainnet/ImmutableSplitsDriver.json new file mode 100644 index 0000000..9879574 --- /dev/null +++ b/src/abi/mainnet/ImmutableSplitsDriver.json @@ -0,0 +1,358 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "_drips", "type": "address" }, + { "internalType": "uint32", "name": "_driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "CreatedSplits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "createSplits", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSplitsWeight", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/metis/ImmutableSplitsDriver.json b/src/abi/metis/ImmutableSplitsDriver.json new file mode 100644 index 0000000..9879574 --- /dev/null +++ b/src/abi/metis/ImmutableSplitsDriver.json @@ -0,0 +1,358 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "_drips", "type": "address" }, + { "internalType": "uint32", "name": "_driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "CreatedSplits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "createSplits", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSplitsWeight", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/optimism/ImmutableSplitsDriver.json b/src/abi/optimism/ImmutableSplitsDriver.json new file mode 100644 index 0000000..9879574 --- /dev/null +++ b/src/abi/optimism/ImmutableSplitsDriver.json @@ -0,0 +1,358 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "_drips", "type": "address" }, + { "internalType": "uint32", "name": "_driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "CreatedSplits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "createSplits", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSplitsWeight", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/optimism_sepolia/ImmutableSplitsDriver.json b/src/abi/optimism_sepolia/ImmutableSplitsDriver.json new file mode 100644 index 0000000..9879574 --- /dev/null +++ b/src/abi/optimism_sepolia/ImmutableSplitsDriver.json @@ -0,0 +1,358 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "_drips", "type": "address" }, + { "internalType": "uint32", "name": "_driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "CreatedSplits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "createSplits", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSplitsWeight", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/polygon_amoy/ImmutableSplitsDriver.json b/src/abi/polygon_amoy/ImmutableSplitsDriver.json new file mode 100644 index 0000000..9879574 --- /dev/null +++ b/src/abi/polygon_amoy/ImmutableSplitsDriver.json @@ -0,0 +1,358 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "_drips", "type": "address" }, + { "internalType": "uint32", "name": "_driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "CreatedSplits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "createSplits", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSplitsWeight", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/sepolia/ImmutableSplitsDriver.json b/src/abi/sepolia/ImmutableSplitsDriver.json new file mode 100644 index 0000000..9879574 --- /dev/null +++ b/src/abi/sepolia/ImmutableSplitsDriver.json @@ -0,0 +1,358 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "_drips", "type": "address" }, + { "internalType": "uint32", "name": "_driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "CreatedSplits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "createSplits", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSplitsWeight", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/config/appSettings.ts b/src/config/appSettings.ts index 97dbd2b..21913b4 100644 --- a/src/config/appSettings.ts +++ b/src/config/appSettings.ts @@ -28,6 +28,7 @@ const appSettings = { cacheInvalidationEndpoint: process.env.CACHE_INVALIDATION_ENDPOINT, visibilityThresholdBlockNumber: Number(process.env.VISIBILITY_THRESHOLD_BLOCK_NUMBER) || 0, + nodeEnv: process.env.NODE_ENV || 'development', } as const; export default appSettings; diff --git a/src/db/database.ts b/src/db/database.ts index c3b53f3..b6bf0d8 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -23,7 +23,11 @@ export async function connectToDb(): Promise { logger.info('Initializing database...'); await authenticate(); - await runMigrations(dbConnection); + + if (appSettings.nodeEnv === 'development') { + await runMigrations(dbConnection); + } + registerModels(); await initializeEntities(); defineAssociations(); diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index e6f567e..1bcb858 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -13,10 +13,10 @@ import { SplitsSetEventModel, StreamsSetEventModel, StreamReceiverSeenEventModel, + _LastIndexedBlockModel, + SplitEventModel, + SqueezedStreamsEventModel, } from '../models'; -import _LastIndexedBlockModel from '../models/_LastIndexedBlockModel'; -import SplitEventModel from '../models/SplitEventModel'; -import SqueezedStreamsEventModel from '../models/SqueezedStreamsEventModel'; const REGISTERED_MODELS: ModelStaticMembers[] = []; diff --git a/src/db/runMigrations.ts b/src/db/runMigrations.ts index 0bd81a4..49c6035 100644 --- a/src/db/runMigrations.ts +++ b/src/db/runMigrations.ts @@ -26,7 +26,7 @@ export async function runMigrations(sequelize: Sequelize): Promise { logger.info(`Applied migrations: ${appliedNames}`); } else { logger.info( - 'No migrations were applied. The database is already up-to-date.', + 'No migrations were applied. The database is already up-to-date. If you expected migrations to be applied, ensure that you run "npm run build" before starting the server.', ); } } diff --git a/src/events/types.ts b/src/events/types.ts index cd3ff3a..a7f9a5f 100644 --- a/src/events/types.ts +++ b/src/events/types.ts @@ -2,6 +2,7 @@ import type EventHandlerBase from './EventHandlerBase'; import type { ValuesOf } from '../core/types'; import type { AnyChainDripsFilters, + AnyChainImmutableSplitsDriverFilters, AnyChainNftDriverFilters, AnyChainRepoDriverFilters, AnyChainTypedLogDescription, @@ -10,11 +11,14 @@ import type { // flat object type with all keys and values from the above type AllFilters = AnyChainDripsFilters & AnyChainNftDriverFilters & - AnyChainRepoDriverFilters; + AnyChainRepoDriverFilters & + AnyChainImmutableSplitsDriverFilters; export type DripsContractEvent = ValuesOf; export type NftDriverContractEvent = ValuesOf; export type RepoDriverContractEvent = ValuesOf; +export type ImmutableSplitsDriverContractEvent = + ValuesOf; type OnlySignatures = T extends `${infer Prefix}(${infer Suffix})` ? `${Prefix}(${Suffix})` diff --git a/src/models/index.ts b/src/models/index.ts index 61db940..d572720 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -3,8 +3,9 @@ export { default as GivenEventModel } from './GivenEventModel'; export { default as SplitEventModel } from './SplitEventModel'; export { default as GitProjectModel } from './GitProjectModel'; export { default as TransferEventModel } from './TransferEventModel'; -export { default as StreamsSetEventModel } from './StreamsSetEventModel'; export { default as SplitsSetEventModel } from './SplitsSetEventModel'; +export { default as StreamsSetEventModel } from './StreamsSetEventModel'; +export { default as _LastIndexedBlockModel } from './_LastIndexedBlockModel'; export { default as OwnerUpdatedEventModel } from './OwnerUpdatedEventModel'; export { default as SqueezedStreamsEventModel } from './SqueezedStreamsEventModel'; export { default as DripListSplitReceiverModel } from './DripListSplitReceiverModel'; From e71067bfb4c69bf9ab2affa17d13ec9151d7b676 Mon Sep 17 00:00:00 2001 From: Ioannis T Date: Wed, 19 Mar 2025 12:53:32 +0100 Subject: [PATCH 03/60] refactor: validate app setting with zod and improve logger --- package-lock.json | 8 +-- package.json | 2 +- src/config/appSettings.schema.ts | 35 ++++++++++++ src/config/appSettings.ts | 81 ++++++++++++++++++--------- src/core/environment.d.ts | 26 --------- src/core/logger.ts | 95 ++++++++++++++++++++++++-------- src/db/database.ts | 21 +------ 7 files changed, 168 insertions(+), 100 deletions(-) create mode 100644 src/config/appSettings.schema.ts delete mode 100644 src/core/environment.d.ts diff --git a/package-lock.json b/package-lock.json index ac1eb9d..f0f7779 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "sequelize": "^6.32.1", "umzug": "^3.8.2", "winston": "^3.10.0", - "zod": "^3.22.2" + "zod": "^3.24.2" }, "devDependencies": { "@typechain/ethers-v6": "^0.5.0", @@ -15488,9 +15488,9 @@ } }, "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.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 8140105..1c9b719 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,6 @@ "sequelize": "^6.32.1", "umzug": "^3.8.2", "winston": "^3.10.0", - "zod": "^3.22.2" + "zod": "^3.24.2" } } diff --git a/src/config/appSettings.schema.ts b/src/config/appSettings.schema.ts new file mode 100644 index 0000000..953ff4a --- /dev/null +++ b/src/config/appSettings.schema.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import { SUPPORTED_NETWORKS } from '../core/constants'; + +const loggingConfigSchema = z.object({ + level: z.enum(['trace', 'debug', 'info', 'warn', 'error']).default('info'), + format: z.enum(['json', 'pretty']).default('pretty'), + destination: z.enum(['console', 'file']).default('console'), + filename: z.string().optional(), +}); + +export const appSettingsSchema = z.object({ + network: z.enum(SUPPORTED_NETWORKS), + primaryRpcUrl: z.string().url(), + primaryRpcAccessToken: z.string().optional(), + fallbackRpcUrl: z.string().url().optional(), + fallbackRpcAccessToken: z.string().optional(), + logger: loggingConfigSchema, + pollingInterval: z.number().positive().optional().default(5000), + chunkSize: z.number().positive().optional().default(1000), + confirmations: z.number().positive().optional().default(1), + ipfsGatewayUrl: z + .string() + .url() + .optional() + .default('https://drips.mypinata.cloud'), + queueUiPort: z.number().positive().optional().default(3000), + redisConnectionString: z.string(), + postgresConnectionString: z.string(), + shouldStartMonitoringUI: z.boolean().optional().default(false), + cacheInvalidationEndpoint: z.string(), + visibilityThresholdBlockNumber: z.number().optional().default(0), + nodeEnv: z.enum(['development', 'production', 'test']).default('development'), +}); + +export type AppSettings = z.infer; diff --git a/src/config/appSettings.ts b/src/config/appSettings.ts index 21913b4..135a066 100644 --- a/src/config/appSettings.ts +++ b/src/config/appSettings.ts @@ -1,34 +1,61 @@ import dotenv from 'dotenv'; import dotenvExpand from 'dotenv-expand'; +import { ZodError } from 'zod'; +import { appSettingsSchema, type AppSettings } from './appSettings.schema'; dotenvExpand.expand(dotenv.config()); -function missingEnvVar(name: string): never { - throw new Error(`Missing ${name} in .env file.`); -} +function loadAppSettings(): AppSettings { + const appSettings = { + network: process.env.NETWORK, + primaryRpcUrl: process.env.PRIMARY_RPC_URL, + fallbackRpcUrl: process.env.FALLBACK_RPC_URL, + primaryRpcAccessToken: process.env.PRIMARY_RPC_ACCESS_TOKEN, + fallbackRpcAccessToken: process.env.FALLBACK_RPC_ACCESS_TOKEN, + logger: { + level: process.env.LOG_LEVEL, + format: process.env.LOG_FORMAT, + destination: process.env.LOG_DESTINATION, + filename: process.env.LOG_FILE, + }, + pollingInterval: process.env.POLLING_INTERVAL + ? parseInt(process.env.POLLING_INTERVAL, 10) + : undefined, + chunkSize: process.env.CHUNK_SIZE + ? parseInt(process.env.CHUNK_SIZE, 10) + : undefined, + confirmations: process.env.CONFIRMATIONS + ? parseInt(process.env.CONFIRMATIONS, 10) + : undefined, + ipfsGatewayUrl: process.env.IPFS_GATEWAY_URL, + queueUiPort: process.env.MONITORING_UI_PORT, + redisConnectionString: process.env.REDIS_CONNECTION_STRING, + postgresConnectionString: process.env.POSTGRES_CONNECTION_STRING, + shouldStartMonitoringUI: + (process.env.SHOULD_START_MONITORING_UI as unknown as string) === 'true', + cacheInvalidationEndpoint: process.env.CACHE_INVALIDATION_ENDPOINT, + visibilityThresholdBlockNumber: process.env + .VISIBILITY_THRESHOLD_BLOCK_NUMBER + ? parseInt(process.env.VISIBILITY_THRESHOLD_BLOCK_NUMBER, 10) + : undefined, + nodeEnv: process.env.NODE_ENV, + }; + + try { + return appSettingsSchema.parse(appSettings); + } catch (error) { + if (error instanceof ZodError) { + const details = error.errors + .map((err) => `${err.path.join('.')}: ${err.message}`) + .join('\n'); -const appSettings = { - network: process.env.NETWORK || missingEnvVar('NETWORK.'), - primaryRpcUrl: - process.env.PRIMARY_RPC_URL || missingEnvVar('PRIMARY_RPC_URL is not set.'), - fallbackRpcUrl: process.env.FALLBACK_RPC_URL, - primaryRpcAccessToken: process.env.PRIMARY_RPC_ACCESS_TOKEN, - fallbackRpcAccessToken: process.env.FALLBACK_RPC_ACCESS_TOKEN, - logLevel: process.env.LOG_LEVEL || 'debug', - pollingInterval: Number(process.env.POLLING_INTERVAL) || 5000, - chunkSize: Number(process.env.CHUNK_SIZE) || 1000, - confirmations: Number(process.env.CONFIRMATIONS) || 1, - ipfsGatewayUrl: - process.env.IPFS_GATEWAY_URL || 'https://drips.mypinata.cloud', - queueUiPort: process.env.MONITORING_UI_PORT || 3000, - redisConnectionString: process.env.REDIS_CONNECTION_STRING, - postgresConnectionString: process.env.POSTGRES_CONNECTION_STRING, - shouldStartMonitoringUI: - (process.env.SHOULD_START_MONITORING_UI as unknown as string) === 'true', - cacheInvalidationEndpoint: process.env.CACHE_INVALIDATION_ENDPOINT, - visibilityThresholdBlockNumber: - Number(process.env.VISIBILITY_THRESHOLD_BLOCK_NUMBER) || 0, - nodeEnv: process.env.NODE_ENV || 'development', -} as const; + throw new Error(`Invalid configuration:\n${details}`); + } + + throw error; + } +} -export default appSettings; +// Singleton app settings instance +const config = loadAppSettings(); +export default config; diff --git a/src/core/environment.d.ts b/src/core/environment.d.ts deleted file mode 100644 index 1c5ca93..0000000 --- a/src/core/environment.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { SupportedNetwork } from './types'; - -declare global { - namespace NodeJS { - interface ProcessEnv { - NETWORK: SupportedNetwork; - PRIMARY_RPC_URL: string; - PRIMARY_RPC_ACCESS_TOKEN: string | undefined; - FALLBACK_RPC_URL: string | undefined; - FALLBACK_RPC_ACCESS_TOKEN: string | undefined; - NODE_ENV: 'development' | 'production'; - LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error'; - POLLING_INTERVAL: number; - IPFS_GATEWAY_URL: string; - MONITORING_UI_PORT: number; - REDIS_CONNECTION_STRING: string; - POSTGRES_CONNECTION_STRING: string; - SHOULD_START_MONITORING_UI: boolean; - SHOULD_PROCESS_PAST_EVENTS: boolean; - CACHE_INVALIDATION_ENDPOINT: string; - VISIBILITY_THRESHOLD_BLOCK_NUMBER: number; - } - } -} - -export {}; diff --git a/src/core/logger.ts b/src/core/logger.ts index dabf505..22eef88 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -1,35 +1,84 @@ import winston from 'winston'; +import fs from 'fs'; +import path from 'path'; import appSettings from '../config/appSettings'; -const format = winston.format.combine( +// Ensure the log directory exists before writing to files. +const logDir = path.join(process.cwd(), 'logs'); +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); +} + +const baseFormats = [ winston.format.timestamp(), - winston.format.colorize(), winston.format.errors({ stack: true }), - winston.format.printf( - ({ timestamp, level, message, requestId }) => - `${timestamp} ${level}${requestId ? ` [${requestId}]` : ''}: ${message}`, - ), + winston.format.metadata({ fillExcept: ['message', 'level', 'timestamp'] }), +]; + +const logFormat = winston.format.printf( + ({ timestamp, level, message, requestId, stack, ...meta }) => { + const requestTag = requestId ? ` [${requestId}]` : ''; + + const metaStr = + meta && Object.keys(meta.metadata || {}).length + ? `\n${JSON.stringify(meta.metadata, null, 2)}` + : ''; + + const logText = `${timestamp} ${level}${requestTag}: ${message}${metaStr}`; + return stack ? `${logText}\n${stack}` : logText; + }, ); -const developmentLogger = winston.createLogger({ - level: appSettings.logLevel, - format, - transports: [new winston.transports.Console()], -}); +const jsonFormat = winston.format.combine( + ...baseFormats, + winston.format.json(), +); -const productionLogger = winston.createLogger({ - level: appSettings.logLevel, - format, - transports: [ - new winston.transports.Console(), - // Logs are stored on Railways, so we don't need to store them locally. - // new winston.transports.File({ - // filename: `logs/${new Date().toISOString().slice(0, 10)}.log`, - // }), - ], +const textFormat = winston.format.combine(...baseFormats, logFormat); + +const consoleFormat = + appSettings.logger.format === 'json' + ? jsonFormat + : winston.format.combine( + ...(process.env.NODE_ENV === 'development' + ? [winston.format.colorize()] + : []), + textFormat, + ); + +const transports: winston.transport[] = [ + new winston.transports.Console({ + level: appSettings.logger.level, + format: consoleFormat, + }), +]; + +// const fileFormat = +// appSettings.logger.format === 'json' ? jsonFormat : textFormat; +// if (appSettings.network === 'mainnet') { +// transports.push( +// new winston.transports.File({ +// filename: path.join( +// logDir, +// `${new Date().toISOString().slice(0, 10)}.log`, +// ), +// level: appSettings.logger.level, +// format: fileFormat, +// maxFiles: 7, // Keep last 7 log files +// maxsize: 10 * 1024 * 1024, // Rotate at 10MB per file +// tailable: true, // Keep the latest log file active +// }), +// ); +// } + +const logger = winston.createLogger({ + level: appSettings.logger.level, + transports, }); -const logger = - appSettings.network === 'mainnet' ? productionLogger : developmentLogger; +// Handle logging errors gracefully +logger.on('error', (err) => { + console.error('Logger encountered an error:', err); +}); export default logger; diff --git a/src/db/database.ts b/src/db/database.ts index b6bf0d8..60e1110 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -20,9 +20,7 @@ export const dbConnection = new Sequelize(`${postgresConnectionString}`, { }); export async function connectToDb(): Promise { - logger.info('Initializing database...'); - - await authenticate(); + await dbConnection.authenticate(); if (appSettings.nodeEnv === 'development') { await runMigrations(dbConnection); @@ -32,22 +30,7 @@ export async function connectToDb(): Promise { await initializeEntities(); defineAssociations(); - logger.info('Database initialized.'); -} - -async function authenticate(): Promise { - try { - await dbConnection.authenticate(); - - logger.info('Connection has been established successfully.'); - - await dbConnection.authenticate(); - } catch (error: any) { - logger.error( - `Unable to connect to the database: ${error} in ${error.stack}`, - ); - throw error; - } + logger.info('Connected to the database.'); } async function initializeEntities(): Promise { From a0c02312e22ad788363d644da9d1dbe10fc7c67c Mon Sep 17 00:00:00 2001 From: "Ioannis T." Date: Mon, 24 Mar 2025 10:25:16 +0100 Subject: [PATCH 04/60] refactor: add support for indexing CreatedSplits events, add new Ecosystems and SubLists tables, add the relevant DB migrations and various improvements --- DEVELOPMENT.md | 15 ++ package.json | 7 +- scripts/run-migrations.ts | 49 ++++ servers.json => scripts/servers.json | 0 src/config/appSettings.schema.ts | 3 +- src/config/appSettings.ts | 12 +- src/config/sequelizeConfig.ts | 6 + src/core/logger.ts | 132 +++++------ src/core/types.ts | 11 +- src/db/database.ts | 59 ++--- src/db/migrations/20250317125844-initial.ts | 17 +- ...131551-add_models_to_support_ecosystems.ts | 214 ++++++++++++++++++ src/db/modelRegistration.ts | 8 +- src/db/runMigrations.ts | 32 --- .../CreatedSplitsEventHandler.ts | 90 ++++++++ src/eventHandlers/index.ts | 3 +- src/events/registrations.ts | 5 + src/index.ts | 2 - src/models/CreatedSplitsEventModel.ts | 48 ++++ src/models/EcosystemModel.ts | 93 ++++++++ src/models/SubListModel.ts | 66 ++++++ src/models/index.ts | 3 + src/utils/accountIdUtils.ts | 29 ++- .../CreatedSplitsHandler.test.ts | 103 +++++++++ 24 files changed, 846 insertions(+), 161 deletions(-) create mode 100644 DEVELOPMENT.md create mode 100644 scripts/run-migrations.ts rename servers.json => scripts/servers.json (100%) create mode 100644 src/config/sequelizeConfig.ts create mode 100644 src/db/migrations/20250319131551-add_models_to_support_ecosystems.ts delete mode 100644 src/db/runMigrations.ts create mode 100644 src/eventHandlers/CreatedSplitsEventHandler.ts create mode 100644 src/models/CreatedSplitsEventModel.ts create mode 100644 src/models/EcosystemModel.ts create mode 100644 src/models/SubListModel.ts create mode 100644 tests/eventHandlers/CreatedSplitsHandler.test.ts diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..d0c18bb --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,15 @@ +# Database Migrations + +## Running Migrations + +Migrations do not run automatically. To run the migrations: + +1. Build the TypeScript code: `npm run build` + +2. Run the migrations: `npm run db:run-migrations`. + +## Creating a New Migration + +To generate a new migration: `npm run db:create-migration -- --name your_migration_name` + +This creates a new migration file in src/db/migrations — just add your changes there. diff --git a/package.json b/package.json index 1c9b719..79a696a 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,14 @@ "build": "npm run build:contracts && tsc", "build:contracts": "./scripts/build-contracts.sh && ts-node ./scripts/codegen-any-chain-types.ts", "postbuild": "rm -rf ./dist/src/config/chainConfigs && cp -r ./src/config/chainConfigs ./dist/src/config/chainConfigs", + "check": "tsc --noEmit", "dev": "npx nodemon", "start": "node dist/src/index.js", - "check": "tsc --noEmit", "sequelize": "ts-node --transpile-only node_modules/sequelize-cli/lib/sequelize", - "make:migration": "npm run sequelize migration:generate -- --migrations-path src/db/migrations" + "db:run-migrations": "node dist/scripts/run-migrations.js", + "db:create-migration": "npm run sequelize migration:generate -- --migrations-path src/db/migrations", + "dev:db:revert-migration": "npm run sequelize -- db:migrate:undo --migrations-path src/db/migrations --config dist/src/config/sequelizeConfig.js", + "dev:db:log-pending-migrations": "npm run sequelize -- db:migrate:status --migrations-path src/db/migrations --config dist/src/config/sequelizeConfig.js" }, "keywords": [], "author": "", diff --git a/scripts/run-migrations.ts b/scripts/run-migrations.ts new file mode 100644 index 0000000..f4fe7a1 --- /dev/null +++ b/scripts/run-migrations.ts @@ -0,0 +1,49 @@ +import { Sequelize } from 'sequelize'; +import { Umzug, SequelizeStorage } from 'umzug'; +import getSchema from '../src/utils/getSchema'; + +export async function runMigrations(): Promise { + const sequelize = new Sequelize( + process.env.POSTGRES_CONNECTION_STRING as string, + { + dialect: 'postgres', + logging: false, + timezone: 'UTC', + }, + ); + + const schema = getSchema(); + + // Ensure schema exists before running migrations + await sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`); + + const migrator = new Umzug({ + migrations: { + glob: './dist/src/db/migrations/*.js', // Migrations always run from the build! + }, + context: sequelize, + storage: new SequelizeStorage({ sequelize, schema: getSchema() }), + logger: console, + }); + + try { + const migrations = await migrator.up(); + + if (migrations.length > 0) { + const appliedNames = migrations.map((m) => m.name).join(', '); + console.log(`Applied migrations: ${appliedNames}`); + } else { + console.log( + 'No migrations were applied. The database is already up-to-date. If you expected migrations to be applied, ensure that you run "npm run build" before starting the server.', + ); + } + } finally { + sequelize.close(); + } +} + +runMigrations().catch((error) => { + console.error('Error running migrations:', error); + + throw error; +}); diff --git a/servers.json b/scripts/servers.json similarity index 100% rename from servers.json rename to scripts/servers.json diff --git a/src/config/appSettings.schema.ts b/src/config/appSettings.schema.ts index 953ff4a..866f739 100644 --- a/src/config/appSettings.schema.ts +++ b/src/config/appSettings.schema.ts @@ -29,7 +29,8 @@ export const appSettingsSchema = z.object({ shouldStartMonitoringUI: z.boolean().optional().default(false), cacheInvalidationEndpoint: z.string(), visibilityThresholdBlockNumber: z.number().optional().default(0), - nodeEnv: z.enum(['development', 'production', 'test']).default('development'), + nodeEnv: z.literal('production'), // https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production#why-is-node_env-considered-an-antipattern }); +export type LoggingConfig = z.infer; export type AppSettings = z.infer; diff --git a/src/config/appSettings.ts b/src/config/appSettings.ts index 135a066..d5af94d 100644 --- a/src/config/appSettings.ts +++ b/src/config/appSettings.ts @@ -46,10 +46,14 @@ function loadAppSettings(): AppSettings { } catch (error) { if (error instanceof ZodError) { const details = error.errors - .map((err) => `${err.path.join('.')}: ${err.message}`) + .map((err) => { + const path = err.path.join('.'); + const { message } = err; + return `- ${path ? `'${path}': ` : ''}${message}`; + }) .join('\n'); - throw new Error(`Invalid configuration:\n${details}`); + throw new Error(`Invalid configuration:\n\n${details}\n`); } throw error; @@ -57,5 +61,5 @@ function loadAppSettings(): AppSettings { } // Singleton app settings instance -const config = loadAppSettings(); -export default config; +const appSettings = loadAppSettings(); +export default appSettings; diff --git a/src/config/sequelizeConfig.ts b/src/config/sequelizeConfig.ts new file mode 100644 index 0000000..a85aef4 --- /dev/null +++ b/src/config/sequelizeConfig.ts @@ -0,0 +1,6 @@ +import appSettings from './appSettings'; + +export default { + url: appSettings.postgresConnectionString, + dialect: 'postgres', +}; diff --git a/src/core/logger.ts b/src/core/logger.ts index 22eef88..364387b 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -1,84 +1,60 @@ import winston from 'winston'; -import fs from 'fs'; -import path from 'path'; +import type { LoggingConfig } from '../config/appSettings.schema'; import appSettings from '../config/appSettings'; -// Ensure the log directory exists before writing to files. -const logDir = path.join(process.cwd(), 'logs'); -if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }); +function createLogger(config: LoggingConfig): winston.Logger { + const formats = [ + winston.format.timestamp(), + winston.format.errors({ stack: true }), + ]; + + // Add pretty printing for console if configured. + if (config.format === 'pretty' && config.destination === 'console') { + formats.push( + winston.format.colorize(), + winston.format.printf((info: winston.Logform.TransformableInfo) => { + const { level, message, timestamp, metadata, ...rest } = info; + const metaStr = + metadata || Object.keys(rest).length + ? `\n${JSON.stringify(metadata || rest, null, 2)}` + : ''; + + return `${timestamp} ${level}: ${message}${metaStr}`; + }), + ); + } else { + formats.push(winston.format.json()); + } + + const transports: winston.transport[] = []; + + // Configure transport based on destination. + if (config.destination === 'file' && config.filename) { + transports.push( + new winston.transports.File({ + filename: config.filename, + level: config.level, + format: winston.format.combine(...formats), + maxFiles: 7, + maxsize: 10 * 1024 * 1024, // 10MB + tailable: true, + }), + ); + } else { + transports.push( + new winston.transports.Console({ + level: config.level, + format: winston.format.combine(...formats), + }), + ); + } + + return winston.createLogger({ + level: config.level, + transports, + }); } -const baseFormats = [ - winston.format.timestamp(), - winston.format.errors({ stack: true }), - winston.format.metadata({ fillExcept: ['message', 'level', 'timestamp'] }), -]; - -const logFormat = winston.format.printf( - ({ timestamp, level, message, requestId, stack, ...meta }) => { - const requestTag = requestId ? ` [${requestId}]` : ''; - - const metaStr = - meta && Object.keys(meta.metadata || {}).length - ? `\n${JSON.stringify(meta.metadata, null, 2)}` - : ''; - - const logText = `${timestamp} ${level}${requestTag}: ${message}${metaStr}`; - return stack ? `${logText}\n${stack}` : logText; - }, -); - -const jsonFormat = winston.format.combine( - ...baseFormats, - winston.format.json(), -); - -const textFormat = winston.format.combine(...baseFormats, logFormat); - -const consoleFormat = - appSettings.logger.format === 'json' - ? jsonFormat - : winston.format.combine( - ...(process.env.NODE_ENV === 'development' - ? [winston.format.colorize()] - : []), - textFormat, - ); - -const transports: winston.transport[] = [ - new winston.transports.Console({ - level: appSettings.logger.level, - format: consoleFormat, - }), -]; - -// const fileFormat = -// appSettings.logger.format === 'json' ? jsonFormat : textFormat; -// if (appSettings.network === 'mainnet') { -// transports.push( -// new winston.transports.File({ -// filename: path.join( -// logDir, -// `${new Date().toISOString().slice(0, 10)}.log`, -// ), -// level: appSettings.logger.level, -// format: fileFormat, -// maxFiles: 7, // Keep last 7 log files -// maxsize: 10 * 1024 * 1024, // Rotate at 10MB per file -// tailable: true, // Keep the latest log file active -// }), -// ); -// } - -const logger = winston.createLogger({ - level: appSettings.logger.level, - transports, -}); - -// Handle logging errors gracefully -logger.on('error', (err) => { - console.error('Logger encountered an error:', err); -}); - +// Singleton logger instance +const logger = createLogger(appSettings.logger); export default logger; diff --git a/src/core/types.ts b/src/core/types.ts index 5c386e7..89bd4ca 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -20,11 +20,16 @@ export type AddressDriverId = string & { export type NftDriverId = string & { __brand: 'NftDriverId' }; export type DripListId = NftDriverId; - export type RepoDriverId = string & { __brand: 'RepoDriverId' }; export type ProjectId = RepoDriverId; - -export type AccountId = AddressDriverId | NftDriverId | RepoDriverId; +export type ImmutableSplitsDriverId = string & { + __brand: 'ImmutableSplitsDriverId'; +}; +export type AccountId = + | AddressDriverId + | NftDriverId + | RepoDriverId + | ImmutableSplitsDriverId; export type Address = string & { __brand: 'Address' }; diff --git a/src/db/database.ts b/src/db/database.ts index 60e1110..2c868dd 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -6,48 +6,43 @@ import { DripListModel, DripListSplitReceiverModel, GitProjectModel, + EcosystemModel, RepoDriverSplitReceiverModel, + SubListModel, } from '../models'; import appSettings from '../config/appSettings'; -import { runMigrations } from './runMigrations'; -const { postgresConnectionString } = appSettings; - -export const dbConnection = new Sequelize(`${postgresConnectionString}`, { - dialect: 'postgres', - logging: false, - timezone: 'UTC', -}); +export const dbConnection = new Sequelize( + appSettings.postgresConnectionString, + { + dialect: 'postgres', + logging: false, + timezone: 'UTC', + }, +); export async function connectToDb(): Promise { - await dbConnection.authenticate(); + try { + await dbConnection.authenticate(); - if (appSettings.nodeEnv === 'development') { - await runMigrations(dbConnection); - } + registerModels(); + await initializeEntities(); + defineAssociations(); - registerModels(); - await initializeEntities(); - defineAssociations(); + logger.info('Connected to the database.'); + } catch (error) { + logger.error('Failed to connect to the database.', error); - logger.info('Connected to the database.'); + throw error; + } } async function initializeEntities(): Promise { - try { - logger.info('Initializing database schema...'); + logger.info('Initializing database schema...'); - const promises = getRegisteredModels().map(async (Model) => { - Model.initialize(dbConnection); - }); + getRegisteredModels().map((Model) => Model.initialize(dbConnection)); - await Promise.all(promises); - - logger.info('Database schema initialized.'); - } catch (error: any) { - logger.error(`Unable to initialize the database schema: ${error}.`); - throw error; - } + logger.info('Database schema initialized.'); } function defineAssociations() { @@ -107,6 +102,14 @@ function defineAssociations() { foreignKey: 'funderDripListId', }); + // One-to-Many: An Ecosystem can have multiple SubLists. + EcosystemModel.hasMany(SubListModel, { + foreignKey: 'ecosystemId', + }); + SubListModel.belongsTo(EcosystemModel, { + foreignKey: 'ecosystemId', + }); + // One-to-One: A DripListSplitReceiverModel represents/is a drip list. DripListModel.hasOne(DripListSplitReceiverModel, { foreignKey: 'fundeeDripListId', diff --git a/src/db/migrations/20250317125844-initial.ts b/src/db/migrations/20250317125844-initial.ts index 885026d..19cbff2 100644 --- a/src/db/migrations/20250317125844-initial.ts +++ b/src/db/migrations/20250317125844-initial.ts @@ -2,7 +2,8 @@ import { DataTypes, literal, type QueryInterface } from 'sequelize'; import { COMMON_EVENT_INIT_ATTRIBUTES, FORGES_MAP } from '../../core/constants'; import { ProjectVerificationStatus } from '../../models/GitProjectModel'; import getSchema from '../../utils/getSchema'; -import type { DbSchema } from '../../core/types'; +import { DependencyType, type DbSchema } from '../../core/types'; +import { AddressDriverSplitReceiverType } from '../../models/AddressDriverSplitReceiverModel'; export async function up({ context: sequelize }: any): Promise { const schema = getSchema(); @@ -405,7 +406,7 @@ async function createRepoDriverSplitReceiversTable( allowNull: true, }, type: { - type: DataTypes.ENUM('DependencyType'), + type: DataTypes.ENUM(...Object.values(DependencyType)), allowNull: false, }, blockTimestamp: { @@ -620,7 +621,7 @@ async function createDripListSplitReceiversTable( allowNull: true, }, type: { - type: DataTypes.ENUM('DependencyType'), + type: DataTypes.ENUM(...Object.values(DependencyType)), allowNull: false, }, blockTimestamp: { @@ -784,7 +785,7 @@ async function createAddressDriverSplitReceiversTable( allowNull: true, }, type: { - type: DataTypes.ENUM('AddressDriverSplitReceiverType'), + type: DataTypes.ENUM(...Object.values(AddressDriverSplitReceiverType)), allowNull: false, }, blockTimestamp: { @@ -1020,9 +1021,9 @@ async function createTableIfNotExists( } } -export async function down(queryInterface: QueryInterface): Promise { +export async function down({ context: sequelize }: any): Promise { const schema = getSchema(); - await queryInterface.sequelize.query( - `DROP SCHEMA IF EXISTS "${schema}" CASCADE`, - ); + const queryInterface = sequelize.getQueryInterface(); + + await queryInterface.query.dropSchema(schema); } diff --git a/src/db/migrations/20250319131551-add_models_to_support_ecosystems.ts b/src/db/migrations/20250319131551-add_models_to_support_ecosystems.ts new file mode 100644 index 0000000..903d84c --- /dev/null +++ b/src/db/migrations/20250319131551-add_models_to_support_ecosystems.ts @@ -0,0 +1,214 @@ +import { DataTypes, literal, type QueryInterface } from 'sequelize'; +import getSchema from '../../utils/getSchema'; +import { COMMON_EVENT_INIT_ATTRIBUTES } from '../../core/constants'; +import type { DbSchema } from '../../core/types'; + +export async function up({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface = sequelize.getQueryInterface(); + + await createTableIfNotExists( + queryInterface, + schema, + 'Ecosystems', + createEcosystemsTable, + ); + + await createTableIfNotExists( + queryInterface, + schema, + 'SubLists', + createSubListsTable, + ); + + await createTableIfNotExists( + queryInterface, + schema, + 'CreatedSplitsEvents', + createCreatedSplitsEventsTable, + ); +} + +async function createCreatedSplitsEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'CreatedSplitsEvents', schema }, + { + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + receiversHash: { + type: DataTypes.STRING, + allowNull: false, + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + }, + ); +} + +async function createEcosystemsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'Ecosystems', schema }, + { + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + isValid: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + ownerAddress: { + type: DataTypes.STRING, + allowNull: false, + }, + ownerAccountId: { + type: DataTypes.STRING, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: true, + }, + latestVotingRoundId: { + type: DataTypes.UUID, + allowNull: true, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + creator: { + type: DataTypes.STRING, + allowNull: false, + }, + previousOwnerAddress: { + type: DataTypes.STRING, + allowNull: false, + }, + isVisible: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + lastProcessedIpfsHash: { + type: DataTypes.TEXT, + allowNull: true, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + }, + ); + + await queryInterface.addIndex( + { tableName: 'Ecosystems', schema }, + ['ownerAddress'], + { + name: 'IX_Ecosystems_ownerAddress', + unique: false, + }, + ); +} + +async function createSubListsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'SubLists', schema }, + { + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + ecosystemId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'Ecosystems', + key: 'id', + }, + }, + name: { + type: DataTypes.STRING, + allowNull: true, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + lastProcessedIpfsHash: { + type: DataTypes.TEXT, + allowNull: true, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SubLists', schema }, + ['ecosystemId'], + { + name: 'IX_SubLists_ecosystemId', + unique: false, + }, + ); +} + +async function createTableIfNotExists( + queryInterface: QueryInterface, + schema: DbSchema, + tableName: string, + createFn: (queryInterface: QueryInterface, schema: DbSchema) => Promise, +) { + const tableFullName = { tableName, schema }; + + const tableExists = await queryInterface + .describeTable(tableFullName) + .catch(() => null); + if (!tableExists) { + await createFn(queryInterface, schema); + } +} + +export async function down({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface = sequelize.getQueryInterface(); + + await queryInterface.dropTable({ tableName: 'CreatedSplitsEvents', schema }); + await queryInterface.dropTable({ tableName: 'Ecosystems', schema }); + await queryInterface.dropTable({ tableName: 'SubLists', schema }); +} diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index 1bcb858..7c4f8f9 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -16,6 +16,9 @@ import { _LastIndexedBlockModel, SplitEventModel, SqueezedStreamsEventModel, + SubListModel, + CreatedSplitsEventModel, + EcosystemModel, } from '../models'; const REGISTERED_MODELS: ModelStaticMembers[] = []; @@ -31,14 +34,17 @@ export function getRegisteredModels(): ModelStaticMembers[] { export function registerModels(): void { registerModel(_LastIndexedBlockModel); + registerModel(SubListModel); registerModel(DripListModel); registerModel(GivenEventModel); registerModel(SplitEventModel); registerModel(GitProjectModel); + registerModel(EcosystemModel); registerModel(TransferEventModel); - registerModel(StreamsSetEventModel); registerModel(SplitsSetEventModel); + registerModel(StreamsSetEventModel); registerModel(OwnerUpdatedEventModel); + registerModel(CreatedSplitsEventModel); registerModel(SqueezedStreamsEventModel); registerModel(DripListSplitReceiverModel); registerModel(StreamReceiverSeenEventModel); diff --git a/src/db/runMigrations.ts b/src/db/runMigrations.ts deleted file mode 100644 index 49c6035..0000000 --- a/src/db/runMigrations.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Sequelize } from 'sequelize'; -import { Umzug, SequelizeStorage } from 'umzug'; -import logger from '../core/logger'; -import getSchema from '../utils/getSchema'; - -export async function runMigrations(sequelize: Sequelize): Promise { - const schema = getSchema(); - - // Ensure schema exists before running migrations - await sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`); - - const migrator = new Umzug({ - migrations: { - glob: './dist/src/db/migrations/*.js', - }, - context: sequelize, - storage: new SequelizeStorage({ sequelize, schema: getSchema() }), - logger: console, - }); - - // Apply any pending migrations. - const migrations = await migrator.up(); - - if (migrations.length > 0) { - const appliedNames = migrations.map((m) => m.name).join(', '); - logger.info(`Applied migrations: ${appliedNames}`); - } else { - logger.info( - 'No migrations were applied. The database is already up-to-date. If you expected migrations to be applied, ensure that you run "npm run build" before starting the server.', - ); - } -} diff --git a/src/eventHandlers/CreatedSplitsEventHandler.ts b/src/eventHandlers/CreatedSplitsEventHandler.ts new file mode 100644 index 0000000..8d9ebb1 --- /dev/null +++ b/src/eventHandlers/CreatedSplitsEventHandler.ts @@ -0,0 +1,90 @@ +import EventHandlerBase from '../events/EventHandlerBase'; +import LogManager from '../core/LogManager'; +import { + toAccountId, + toImmutableSplitsDriverId, +} from '../utils/accountIdUtils'; +import type EventHandlerRequest from '../events/EventHandlerRequest'; +import { dbConnection } from '../db/database'; +import type { CreatedSplitsEvent } from '../../contracts/CURRENT_NETWORK/ImmutableSplitsDriver'; +import CreatedSplitsEventModel from '../models/CreatedSplitsEventModel'; +import SubListModel from '../models/SubListModel'; +import unreachableError from '../utils/unreachableError'; + +export default class SplitEventHandler extends EventHandlerBase<'CreatedSplits(uint256,bytes32)'> { + public eventSignatures = ['CreatedSplits(uint256,bytes32)' as const]; + + protected async _handle( + request: EventHandlerRequest<'CreatedSplits(uint256,bytes32)'>, + ): Promise { + const { + id: requestId, + event: { args, logIndex, blockNumber, blockTimestamp, transactionHash }, + } = request; + + const [rawAccountId, rawReceiversHash] = + args as CreatedSplitsEvent.OutputTuple; + + const accountId = toAccountId(rawAccountId); + + LogManager.logRequestInfo( + `📥 ${this.name} is processing the following ${request.event.eventSignature}: + \r\t - accountId: ${rawAccountId} + \r\t - receiversHash: ${rawReceiversHash} + \r\t - logIndex: ${logIndex} + \r\t - tx hash: ${transactionHash}`, + requestId, + ); + + await dbConnection.transaction(async (transaction) => { + const logManager = new LogManager(requestId); + + const [createdSplitsEvent, isEventCreated] = + await CreatedSplitsEventModel.findOrCreate({ + lock: true, + transaction, + where: { + logIndex, + transactionHash, + }, + defaults: { + accountId, + receiversHash: rawReceiversHash, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + }); + + logManager.appendFindOrCreateLog( + CreatedSplitsEventModel, + isEventCreated, + `${createdSplitsEvent.transactionHash}-${createdSplitsEvent.logIndex}`, + ); + + // This must be the only place a `SubList` is created. + const [subList, isSubListCreated] = await SubListModel.findOrCreate({ + transaction, + lock: true, + where: { + id: accountId, + }, + defaults: { + id: toImmutableSplitsDriverId(rawAccountId), + }, + }); + + if (!isSubListCreated) { + // The `SubList` with the given `accountId` was created by another `CreatedSplits` event. + unreachableError(`SubList with id ${accountId} already exists.`); + } + + logManager + .appendFindOrCreateLog(SubListModel, isSubListCreated, subList.id) + .logAllInfo(); + + logManager.logAllInfo(); + }); + } +} diff --git a/src/eventHandlers/index.ts b/src/eventHandlers/index.ts index 34e188a..7cc9b22 100644 --- a/src/eventHandlers/index.ts +++ b/src/eventHandlers/index.ts @@ -1,8 +1,9 @@ export { default as GivenEventHandler } from './GivenEventHandler'; export { default as SplitEventHandler } from './SplitEventHandler'; -export { default as StreamsSetEventHandler } from './StreamsSetEventHandler'; export { default as TransferEventHandler } from './TransferEventHandler'; +export { default as StreamsSetEventHandler } from './StreamsSetEventHandler'; export { default as OwnerUpdatedEventHandler } from './OwnerUpdatedEventHandler'; +export { default as CreatedSplitsEventHandler } from './CreatedSplitsEventHandler'; export { default as SqueezedStreamsEventHandler } from './SqueezedStreamsEventHandler'; export { default as StreamReceiverSeenEventHandler } from './StreamReceiverSeenEventHandler'; export { default as SplitsSetEventHandler } from './SplitsSetEventHandler/SplitsSetEventHandler'; diff --git a/src/events/registrations.ts b/src/events/registrations.ts index ab36fcf..9a1727f 100644 --- a/src/events/registrations.ts +++ b/src/events/registrations.ts @@ -9,6 +9,7 @@ import { StreamReceiverSeenEventHandler, StreamsSetEventHandler, SqueezedStreamsEventHandler, + CreatedSplitsEventHandler, } from '../eventHandlers'; import { registerEventHandler } from './eventHandlerUtils'; @@ -60,4 +61,8 @@ export function registerEventHandlers(): void { 'SqueezedStreams(uint256,address,uint256,uint128,bytes32[])', SqueezedStreamsEventHandler, ); + registerEventHandler<'CreatedSplits(uint256,bytes32)'>( + 'CreatedSplits(uint256,bytes32)', + CreatedSplitsEventHandler, + ); } diff --git a/src/index.ts b/src/index.ts index 580210d..fda5f35 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import logger from './core/logger'; - import appSettings from './config/appSettings'; import initJobProcessingQueue from './queue/initJobProcessingQueue'; import startQueueMonitoringUI from './queue/startQueueMonitoringUI'; @@ -38,7 +37,6 @@ async function init() { } logger.info('Starting the application...'); - logger.info(`App Settings: ${JSON.stringify(appSettings, null, 2)}`); await connectToDb(); await initJobProcessingQueue(); diff --git a/src/models/CreatedSplitsEventModel.ts b/src/models/CreatedSplitsEventModel.ts new file mode 100644 index 0000000..5a6f48c --- /dev/null +++ b/src/models/CreatedSplitsEventModel.ts @@ -0,0 +1,48 @@ +import type { + InferAttributes, + InferCreationAttributes, + Sequelize, +} from 'sequelize'; +import { DataTypes, Model } from 'sequelize'; +import type { AccountId } from '../core/types'; +import getSchema from '../utils/getSchema'; +import { COMMON_EVENT_INIT_ATTRIBUTES } from '../core/constants'; +import type { IEventModel } from '../events/types'; + +export default class CreatedSplitsEventModel + extends Model< + InferAttributes, + InferCreationAttributes + > + implements IEventModel +{ + public declare accountId: AccountId; + public declare receiversHash: string; + + // Common event log properties. + public declare logIndex: number; + public declare blockNumber: number; + public declare blockTimestamp: Date; + public declare transactionHash: string; + + public static initialize(sequelize: Sequelize): void { + this.init( + { + accountId: { + type: DataTypes.STRING, + allowNull: false, + }, + receiversHash: { + type: DataTypes.STRING, + allowNull: false, + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + { + sequelize, + schema: getSchema(), + tableName: 'CreatedSplitsEvents', + }, + ); + } +} diff --git a/src/models/EcosystemModel.ts b/src/models/EcosystemModel.ts new file mode 100644 index 0000000..c1ea621 --- /dev/null +++ b/src/models/EcosystemModel.ts @@ -0,0 +1,93 @@ +import type { + InferAttributes, + InferCreationAttributes, + Sequelize, +} from 'sequelize'; +import { DataTypes, Model } from 'sequelize'; +import type { AddressLike } from 'ethers'; +import type { UUID } from 'crypto'; +import type { AccountId, NftDriverId } from '../core/types'; +import getSchema from '../utils/getSchema'; + +export default class EcosystemModel extends Model< + InferAttributes, + InferCreationAttributes +> { + public declare id: NftDriverId; + public declare isValid: boolean; + public declare name: string | null; + public declare creator: AddressLike; + public declare description: string | null; + public declare ownerAddress: AddressLike; + public declare ownerAccountId: AccountId; + public declare previousOwnerAddress: AddressLike; + public declare latestVotingRoundId: UUID | null; + public declare isVisible: boolean; + public declare lastProcessedIpfsHash: string | null; + + public static initialize(sequelize: Sequelize): void { + this.init( + { + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + isValid: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + ownerAddress: { + type: DataTypes.STRING, + allowNull: false, + }, + ownerAccountId: { + type: DataTypes.STRING, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: true, + }, + latestVotingRoundId: { + type: DataTypes.UUID, + allowNull: true, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + creator: { + type: DataTypes.STRING, + allowNull: false, + }, + previousOwnerAddress: { + type: DataTypes.STRING, + allowNull: false, + }, + isVisible: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + lastProcessedIpfsHash: { + type: DataTypes.TEXT, + allowNull: true, + }, + }, + { + sequelize, + schema: getSchema(), + tableName: 'Ecosystems', + indexes: [ + { + fields: ['ownerAddress'], + name: `IX_Ecosystems_ownerAddress`, + where: { + isValid: true, + }, + unique: false, + }, + ], + }, + ); + } +} diff --git a/src/models/SubListModel.ts b/src/models/SubListModel.ts new file mode 100644 index 0000000..f562d36 --- /dev/null +++ b/src/models/SubListModel.ts @@ -0,0 +1,66 @@ +import type { + InferAttributes, + InferCreationAttributes, + Sequelize, +} from 'sequelize'; +import { DataTypes, Model } from 'sequelize'; +import type { NftDriverId, ImmutableSplitsDriverId } from '../core/types'; +import getSchema from '../utils/getSchema'; + +export default class SubListModel extends Model< + InferAttributes, + InferCreationAttributes +> { + // Populated from `CreatedSplits` event: + public declare id: ImmutableSplitsDriverId; + + // Populated from `AccountMetadataEmitted` event: + public declare name: string | null; + public declare description: string | null; + public declare ecosystemId: NftDriverId | null; + public declare lastProcessedIpfsHash: string | null; + + public static initialize(sequelize: Sequelize): void { + this.init( + { + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + ecosystemId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'Ecosystems', + key: 'id', + }, + }, + name: { + type: DataTypes.STRING, + allowNull: true, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + lastProcessedIpfsHash: { + type: DataTypes.TEXT, + allowNull: true, + }, + }, + { + sequelize, + schema: getSchema(), + tableName: 'SubLists', + indexes: [ + { + fields: ['ecosystemId'], + name: `IX_SubLists_ecosystemId`, + unique: false, + }, + ], + }, + ); + } +} diff --git a/src/models/index.ts b/src/models/index.ts index d572720..8b071ff 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,12 +1,15 @@ +export { default as SubListModel } from './SubListModel'; export { default as DripListModel } from './DripListModel'; export { default as GivenEventModel } from './GivenEventModel'; export { default as SplitEventModel } from './SplitEventModel'; export { default as GitProjectModel } from './GitProjectModel'; +export { default as EcosystemModel } from './EcosystemModel'; export { default as TransferEventModel } from './TransferEventModel'; export { default as SplitsSetEventModel } from './SplitsSetEventModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; export { default as _LastIndexedBlockModel } from './_LastIndexedBlockModel'; export { default as OwnerUpdatedEventModel } from './OwnerUpdatedEventModel'; +export { default as CreatedSplitsEventModel } from './CreatedSplitsEventModel'; export { default as SqueezedStreamsEventModel } from './SqueezedStreamsEventModel'; export { default as DripListSplitReceiverModel } from './DripListSplitReceiverModel'; export { default as RepoDriverSplitReceiverModel } from './RepoDriverSplitReceiverModel'; diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index fe4d3be..ae0e9ed 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -2,6 +2,7 @@ import type { AddressLike } from 'ethers'; import type { AccountId, AddressDriverId, + ImmutableSplitsDriverId, NftDriverId, RepoDriverId, } from '../core/types'; @@ -34,7 +35,8 @@ export function toAccountId(id: bigint): AccountId { if ( isRepoDriverId(accountIdAsString) || isNftDriverId(accountIdAsString) || - isAddressDriverId(accountIdAsString) + isAddressDriverId(accountIdAsString) || + isImmutableSplitsDriverId(accountIdAsString) ) { return accountIdAsString as AccountId; } @@ -105,6 +107,31 @@ export function assertRepoDiverAccountId( } } +// ImmutableSplitsDriver +export function isImmutableSplitsDriverId( + id: string, +): id is ImmutableSplitsDriverId { + const isNaN = Number.isNaN(Number(id)); + const immutableSplitsDriverId = + getContractNameFromAccountId(id) === 'immutableSplitsDriver'; + + if (isNaN || !immutableSplitsDriverId) { + return false; + } + + return true; +} + +export function toImmutableSplitsDriverId(id: bigint): ImmutableSplitsDriverId { + const immutableSplitsDriverId = id.toString(); + + if (!isImmutableSplitsDriverId(immutableSplitsDriverId)) { + throw new Error(`Invalid 'ImmutableSplitsDriver' account ID: ${id}.`); + } + + return immutableSplitsDriverId as ImmutableSplitsDriverId; +} + export async function calcAccountId(owner: AddressLike): Promise { return ( await addressDriverContract.calcAccountId(owner as string) diff --git a/tests/eventHandlers/CreatedSplitsHandler.test.ts b/tests/eventHandlers/CreatedSplitsHandler.test.ts new file mode 100644 index 0000000..6ef303a --- /dev/null +++ b/tests/eventHandlers/CreatedSplitsHandler.test.ts @@ -0,0 +1,103 @@ +/* eslint-disable dot-notation */ +import { randomUUID } from 'crypto'; +import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; +import { dbConnection } from '../../src/db/database'; +import type { EventData } from '../../src/events/types'; +import CreatedSplitsEventModel from '../../src/models/CreatedSplitsEventModel'; +import LogManager from '../../src/core/LogManager'; +import { toAccountId } from '../../src/utils/accountIdUtils'; +import { CreatedSplitsEventHandler } from '../../src/eventHandlers'; +import SubListModel from '../../src/models/SubListModel'; + +jest.mock('../../src/models/CreatedSplitsEventModel'); +jest.mock('../../src/db/database'); +jest.mock('bee-queue'); +jest.mock('../../src/core/LogManager'); + +describe('CreatedSplitsEventHandler', () => { + let mockDbTransaction: {}; + let handler: CreatedSplitsEventHandler; + let mockRequest: EventHandlerRequest<'CreatedSplits(uint256,bytes32)'>; + + beforeAll(() => { + jest.clearAllMocks(); + + handler = new CreatedSplitsEventHandler(); + + mockRequest = { + id: randomUUID(), + event: { + args: [ + 53919893334301279589334030174039261347274288845081144962207220498533n, + 'receiversHash', + ], + logIndex: 1, + blockNumber: 1, + blockTimestamp: new Date(), + transactionHash: 'requestTransactionHash', + } as EventData<'CreatedSplits(uint256,bytes32)'>, + }; + + mockDbTransaction = {}; + + dbConnection.transaction = jest + .fn() + .mockImplementation((callback) => callback(mockDbTransaction)); + }); + + describe('_handle', () => { + test('should create a new CreatedSplitsEventModel', async () => { + // Arrange + CreatedSplitsEventModel.findOrCreate = jest.fn().mockResolvedValue([ + { + transactionHash: 'CreatedSplitsTransactionHash', + logIndex: 1, + }, + true, + ]); + + const mockSubList = { + ownerAddress: '', + previousOwnerAddress: '', + ownerAccountId: '', + save: jest.fn(), + }; + SubListModel.findOrCreate = jest + .fn() + .mockResolvedValue([mockSubList, true]); + + LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); + + // Act + await handler['_handle'](mockRequest); + + // Assert + const { + event: { + args: [rawAccountId, rawReceiversHash], + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + } = mockRequest; + + expect(CreatedSplitsEventModel.findOrCreate).toHaveBeenCalledWith({ + lock: true, + transaction: mockDbTransaction, + where: { + logIndex, + transactionHash, + }, + defaults: { + accountId: toAccountId(rawAccountId), + receiversHash: rawReceiversHash, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + }); + }); + }); +}); From da5f64869d9563879ab0799c94f6354c0d65c540 Mon Sep 17 00:00:00 2001 From: "Ioannis T." Date: Mon, 24 Mar 2025 11:07:57 +0100 Subject: [PATCH 05/60] feat: support nft driver metadata v6 --- src/metadata/schemas/index.ts | 5 ++++ src/metadata/schemas/nft-driver/v2.ts | 2 +- src/metadata/schemas/nft-driver/v6.ts | 34 +++++++++++++++++++++++++++ src/metadata/schemas/sub-list/v1.ts | 29 +++++++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/metadata/schemas/nft-driver/v6.ts create mode 100644 src/metadata/schemas/sub-list/v1.ts diff --git a/src/metadata/schemas/index.ts b/src/metadata/schemas/index.ts index 5fe89e0..57339e8 100644 --- a/src/metadata/schemas/index.ts +++ b/src/metadata/schemas/index.ts @@ -10,6 +10,7 @@ import { repoDriverAccountMetadataSchemaV4 } from './repo-driver/v4'; import { nftDriverAccountMetadataSchemaV4 } from './nft-driver/v4'; import { repoDriverAccountMetadataSchemaV5 } from './repo-driver/v5'; import { nftDriverAccountMetadataSchemaV5 } from './nft-driver/v5'; +import { subListMetadataSchemaV1 } from './sub-list/v1'; export const nftDriverAccountMetadataParser = createVersionedParser([ nftDriverAccountMetadataSchemaV5.parse, @@ -30,3 +31,7 @@ export const repoDriverAccountMetadataParser = createVersionedParser([ repoDriverAccountMetadataSchemaV2.parse, repoDriverAccountMetadataSchemaV1.parse, ]); + +export const immutableSplitsDriverMetadataParser = createVersionedParser([ + subListMetadataSchemaV1.parse, +]); diff --git a/src/metadata/schemas/nft-driver/v2.ts b/src/metadata/schemas/nft-driver/v2.ts index 886e0c4..ba56b61 100644 --- a/src/metadata/schemas/nft-driver/v2.ts +++ b/src/metadata/schemas/nft-driver/v2.ts @@ -17,7 +17,7 @@ const repoDriverSplitReceiverSchema = z.object({ /** * A splits entry that splits directly to a different Drip List. */ -const dripListSplitReceiverSchema = z.object({ +export const dripListSplitReceiverSchema = z.object({ type: z.literal('dripList'), weight: z.number(), accountId: z.string(), diff --git a/src/metadata/schemas/nft-driver/v6.ts b/src/metadata/schemas/nft-driver/v6.ts new file mode 100644 index 0000000..99c5393 --- /dev/null +++ b/src/metadata/schemas/nft-driver/v6.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; +import { nftDriverAccountMetadataSchemaV5 } from './v5'; +import { + addressDriverSplitReceiverSchema, + repoDriverSplitReceiverSchema, +} from '../repo-driver/v2'; +import { subListSplitReceiverSchema } from '../sub-list/v1'; +import { dripListSplitReceiverSchema } from './v2'; + +const base = nftDriverAccountMetadataSchemaV5; + +const ecosystemVariant = base.extend({ + type: z.literal('ecosystem'), + recipients: z.array( + z.union([repoDriverSplitReceiverSchema, subListSplitReceiverSchema]), + ), +}); + +const dripListVariant = base.extend({ + type: z.literal('drip-list'), + recipients: z.array( + z.union([ + repoDriverSplitReceiverSchema, + subListSplitReceiverSchema, + addressDriverSplitReceiverSchema, + dripListSplitReceiverSchema, + ]), + ), +}); + +export const nftDriverAccountMetadataSchemaV6 = z.discriminatedUnion('type', [ + ecosystemVariant, + dripListVariant, +]); diff --git a/src/metadata/schemas/sub-list/v1.ts b/src/metadata/schemas/sub-list/v1.ts new file mode 100644 index 0000000..a425b8e --- /dev/null +++ b/src/metadata/schemas/sub-list/v1.ts @@ -0,0 +1,29 @@ +import z from 'zod'; +import { + addressDriverSplitReceiverSchema, + repoDriverSplitReceiverSchema, +} from '../repo-driver/v2'; +import { dripListSplitReceiverSchema } from '../nft-driver/v2'; + +export const subListSplitReceiverSchema = z.object({ + type: z.literal('sub-list'), + weight: z.number(), + accountId: z.string(), +}); + +export const subListMetadataSchemaV1 = z.object({ + driver: z.literal('immutable-splits'), + type: z.literal('sub-list'), + recipients: z.array( + z.union([ + addressDriverSplitReceiverSchema, + dripListSplitReceiverSchema, + repoDriverSplitReceiverSchema, + subListSplitReceiverSchema, + ]), + ), + parent: z.object({ + driver: z.literal('nft'), + accountId: z.string(), + }), +}); From 8e55442571daf395b57c8f7f64c70588f407adc5 Mon Sep 17 00:00:00 2001 From: "Ioannis T." Date: Mon, 24 Mar 2025 11:30:28 +0100 Subject: [PATCH 06/60] refactor: simplify account ID types --- src/config/appSettings.schema.ts | 1 - src/config/appSettings.ts | 1 - src/core/types.ts | 4 +- .../createDbEntriesForProjectDependency.ts | 12 +-- .../dripList/handleDripListMetadata.ts | 11 +-- .../gitProject/handleGitProjectMetadata.ts | 11 +-- .../splitsValidator.ts | 4 +- .../SplitsSetEventHandler/setIsValidFlag.ts | 4 +- src/models/AddressDriverSplitReceiverModel.ts | 6 +- src/models/DripListModel.ts | 4 +- src/models/DripListSplitReceiverModel.ts | 8 +- src/models/GitProjectModel.ts | 4 +- src/models/RepoDriverSplitReceiverModel.ts | 8 +- src/utils/accountIdUtils.ts | 98 ++++++++----------- 14 files changed, 77 insertions(+), 99 deletions(-) diff --git a/src/config/appSettings.schema.ts b/src/config/appSettings.schema.ts index 866f739..b0b1ca6 100644 --- a/src/config/appSettings.schema.ts +++ b/src/config/appSettings.schema.ts @@ -29,7 +29,6 @@ export const appSettingsSchema = z.object({ shouldStartMonitoringUI: z.boolean().optional().default(false), cacheInvalidationEndpoint: z.string(), visibilityThresholdBlockNumber: z.number().optional().default(0), - nodeEnv: z.literal('production'), // https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production#why-is-node_env-considered-an-antipattern }); export type LoggingConfig = z.infer; diff --git a/src/config/appSettings.ts b/src/config/appSettings.ts index d5af94d..f1b87fe 100644 --- a/src/config/appSettings.ts +++ b/src/config/appSettings.ts @@ -38,7 +38,6 @@ function loadAppSettings(): AppSettings { .VISIBILITY_THRESHOLD_BLOCK_NUMBER ? parseInt(process.env.VISIBILITY_THRESHOLD_BLOCK_NUMBER, 10) : undefined, - nodeEnv: process.env.NODE_ENV, }; try { diff --git a/src/core/types.ts b/src/core/types.ts index 89bd4ca..831dd7f 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -19,9 +19,7 @@ export type AddressDriverId = string & { }; export type NftDriverId = string & { __brand: 'NftDriverId' }; -export type DripListId = NftDriverId; export type RepoDriverId = string & { __brand: 'RepoDriverId' }; -export type ProjectId = RepoDriverId; export type ImmutableSplitsDriverId = string & { __brand: 'ImmutableSplitsDriverId'; }; @@ -79,7 +77,7 @@ export type Dependency = ArrayElement< export type DependencyOfProjectType = { type: 'repoDriver'; - accountId: ProjectId; + accountId: RepoDriverId; source: { forge: 'github'; repoName: string; diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts b/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts index b2fc6f6..ddc9389 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts @@ -1,10 +1,10 @@ import type { Transaction } from 'sequelize'; -import { - DependencyType, - type DependencyOfProjectType, - type DripListId, - type ProjectId, +import type { + DependencyOfProjectType, + NftDriverId, + RepoDriverId, } from '../../core/types'; +import { DependencyType } from '../../core/types'; import GitProjectModel, { ProjectVerificationStatus, } from '../../models/GitProjectModel'; @@ -14,7 +14,7 @@ import RepoDriverSplitReceiverModel from '../../models/RepoDriverSplitReceiverMo import { isNftDriverId, isRepoDriverId } from '../../utils/accountIdUtils'; export default async function createDbEntriesForProjectDependency( - funderAccountId: ProjectId | DripListId, + funderAccountId: RepoDriverId | NftDriverId, projectDependency: DependencyOfProjectType, transaction: Transaction, blockTimestamp: Date, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts index a26143f..bb8e5b5 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts @@ -2,11 +2,8 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Transaction } from 'sequelize'; import type { UUID } from 'crypto'; -import { - DependencyType, - type DripListId, - type IpfsHash, -} from '../../../core/types'; +import type { IpfsHash, NftDriverId } from '../../../core/types'; +import { DependencyType } from '../../../core/types'; import getNftDriverMetadata from '../../../utils/metadataUtils'; import validateDripListMetadata from './validateDripListMetadata'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; @@ -32,7 +29,7 @@ import appSettings from '../../../config/appSettings'; export default async function handleDripListMetadata( logManager: LogManager, - dripListId: DripListId, + dripListId: NftDriverId, transaction: Transaction, ipfsHash: IpfsHash, blockTimestamp: Date, @@ -133,7 +130,7 @@ async function updateDripListMetadata( } async function createDbEntriesForDripListSplits( - funderDripListId: DripListId, + funderDripListId: NftDriverId, splits: AnyVersion['projects'], logManager: LogManager, transaction: Transaction, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts index 1a4baef..0dce610 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts @@ -10,11 +10,8 @@ import { import type { repoDriverAccountMetadataParser } from '../../../metadata/schemas'; import LogManager from '../../../core/LogManager'; import { calculateProjectStatus } from '../../../utils/gitProjectUtils'; -import { - DependencyType, - type IpfsHash, - type ProjectId, -} from '../../../core/types'; +import type { IpfsHash, RepoDriverId } from '../../../core/types'; +import { DependencyType } from '../../../core/types'; import { assertAddressDiverId, isAddressDriverId, @@ -32,7 +29,7 @@ import getUserAddress from '../../../utils/getAccountAddress'; export default async function handleGitProjectMetadata( logManager: LogManager, - projectId: ProjectId, + projectId: RepoDriverId, transaction: Transaction, ipfsHash: IpfsHash, blockTimestamp: Date, @@ -138,7 +135,7 @@ async function updateGitProjectMetadata( } async function createDbEntriesForProjectSplits( - funderProjectId: ProjectId, + funderProjectId: RepoDriverId, splits: AnyVersion['splits'], logManager: LogManager, transaction: Transaction, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts b/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts index 3a4080d..0f3750b 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts @@ -1,9 +1,9 @@ import type { SplitsReceiverStruct } from '../../../contracts/CURRENT_NETWORK/Drips'; -import type { DripListId, ProjectId } from '../../core/types'; +import type { RepoDriverId, NftDriverId } from '../../core/types'; import { dripsContract } from '../../core/contractClients'; export default async function validateSplitsReceivers( - accountId: ProjectId | DripListId, + accountId: RepoDriverId | NftDriverId, splits: SplitsReceiverStruct[], ): Promise< [ diff --git a/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts index 9538629..524452d 100644 --- a/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts @@ -1,4 +1,4 @@ -import type { DripListId, RepoDriverId } from '../../core/types'; +import type { NftDriverId, RepoDriverId } from '../../core/types'; import type { SplitsSetEventModel } from '../../models'; import { AddressDriverSplitReceiverModel, @@ -169,7 +169,7 @@ async function getProjectDbReceivers(accountId: RepoDriverId) { return [...addressReceivers, ...projectReceivers, ...dripListReceivers]; } -async function getDripListDbReceivers(accountId: DripListId) { +async function getDripListDbReceivers(accountId: NftDriverId) { const addressReceivers: SplitsReceiverStruct[] = await AddressDriverSplitReceiverModel.findAll({ lock: true, diff --git a/src/models/AddressDriverSplitReceiverModel.ts b/src/models/AddressDriverSplitReceiverModel.ts index bf93699..f6f7ef1 100644 --- a/src/models/AddressDriverSplitReceiverModel.ts +++ b/src/models/AddressDriverSplitReceiverModel.ts @@ -8,7 +8,7 @@ import { DataTypes, Model } from 'sequelize'; import type { AddressLike } from 'ethers'; import getSchema from '../utils/getSchema'; import GitProjectModel from './GitProjectModel'; -import type { AddressDriverId, DripListId, ProjectId } from '../core/types'; +import type { AddressDriverId, NftDriverId, RepoDriverId } from '../core/types'; import DripListModel from './DripListModel'; export enum AddressDriverSplitReceiverType { @@ -22,8 +22,8 @@ export default class AddressDriverSplitReceiverModel extends Model< InferCreationAttributes > { public declare id: CreationOptional; // Primary key - public declare funderProjectId: ProjectId | null; // Foreign key - public declare funderDripListId: DripListId | null; // Foreign key + public declare funderProjectId: RepoDriverId | null; // Foreign key + public declare funderDripListId: NftDriverId | null; // Foreign key public declare weight: number; public declare type: AddressDriverSplitReceiverType; diff --git a/src/models/DripListModel.ts b/src/models/DripListModel.ts index 2016c6e..c10f742 100644 --- a/src/models/DripListModel.ts +++ b/src/models/DripListModel.ts @@ -6,14 +6,14 @@ import type { import { DataTypes, Model } from 'sequelize'; import type { AddressLike } from 'ethers'; import type { UUID } from 'crypto'; -import type { AccountId, DripListId } from '../core/types'; +import type { AccountId, NftDriverId } from '../core/types'; import getSchema from '../utils/getSchema'; export default class DripListModel extends Model< InferAttributes, InferCreationAttributes > { - public declare id: DripListId; // The `tokenId` from `TransferEvent` event. + public declare id: NftDriverId; // The `tokenId` from `TransferEvent` event. public declare isValid: boolean; public declare name: string | null; public declare creator: AddressLike; diff --git a/src/models/DripListSplitReceiverModel.ts b/src/models/DripListSplitReceiverModel.ts index 0da0755..75754e3 100644 --- a/src/models/DripListSplitReceiverModel.ts +++ b/src/models/DripListSplitReceiverModel.ts @@ -7,7 +7,7 @@ import type { import { DataTypes, Model } from 'sequelize'; import getSchema from '../utils/getSchema'; import { DependencyType } from '../core/types'; -import type { DripListId, ProjectId } from '../core/types'; +import type { NftDriverId, RepoDriverId } from '../core/types'; import DripListModel from './DripListModel'; import GitProjectModel from './GitProjectModel'; @@ -16,9 +16,9 @@ export default class DripListSplitReceiverModel extends Model< InferCreationAttributes > { public declare id: CreationOptional; // Primary key - public declare fundeeDripListId: DripListId; // Foreign key - public declare funderProjectId: ProjectId | null; // Foreign key - public declare funderDripListId: DripListId | null; // Foreign key + public declare fundeeDripListId: NftDriverId; // Foreign key + public declare funderProjectId: RepoDriverId | null; // Foreign key + public declare funderDripListId: NftDriverId | null; // Foreign key public declare weight: number; public declare type: DependencyType; diff --git a/src/models/GitProjectModel.ts b/src/models/GitProjectModel.ts index ede3012..2a2144c 100644 --- a/src/models/GitProjectModel.ts +++ b/src/models/GitProjectModel.ts @@ -6,7 +6,7 @@ import type { import { DataTypes, Model } from 'sequelize'; import type { AddressLike } from 'ethers'; import getSchema from '../utils/getSchema'; -import type { AccountId, Forge, ProjectId } from '../core/types'; +import type { AccountId, Forge, RepoDriverId } from '../core/types'; import { FORGES_MAP } from '../core/constants'; export enum ProjectVerificationStatus { @@ -22,7 +22,7 @@ export default class GitProjectModel extends Model< InferAttributes, InferCreationAttributes > { - public declare id: ProjectId; // The `accountId` from `OwnerUpdatedRequested` event. + public declare id: RepoDriverId; // The `accountId` from `OwnerUpdatedRequested` event. public declare isValid: boolean; public declare name: string | null; public declare forge: Forge | null; diff --git a/src/models/RepoDriverSplitReceiverModel.ts b/src/models/RepoDriverSplitReceiverModel.ts index c1cdcb3..b59d8b3 100644 --- a/src/models/RepoDriverSplitReceiverModel.ts +++ b/src/models/RepoDriverSplitReceiverModel.ts @@ -8,7 +8,7 @@ import { DataTypes, Model } from 'sequelize'; import getSchema from '../utils/getSchema'; import GitProjectModel from './GitProjectModel'; import { DependencyType } from '../core/types'; -import type { DripListId, ProjectId } from '../core/types'; +import type { NftDriverId, RepoDriverId } from '../core/types'; import DripListModel from './DripListModel'; export default class RepoDriverSplitReceiverModel extends Model< @@ -16,9 +16,9 @@ export default class RepoDriverSplitReceiverModel extends Model< InferCreationAttributes > { public declare id: CreationOptional; // Primary key - public declare fundeeProjectId: ProjectId; // Foreign key - public declare funderProjectId: ProjectId | null; // Foreign key - public declare funderDripListId: DripListId | null; // Foreign key + public declare fundeeProjectId: RepoDriverId; // Foreign key + public declare funderProjectId: RepoDriverId | null; // Foreign key + public declare funderDripListId: NftDriverId | null; // Foreign key public declare weight: number; public declare type: DependencyType; diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index ae0e9ed..cd5064a 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -9,6 +9,19 @@ import type { import { addressDriverContract } from '../core/contractClients'; import { getContractNameFromAccountId } from './contractUtils'; +// RepoDriver +export function isRepoDriverId(id: string): id is RepoDriverId { + const isNaN = Number.isNaN(Number(id)); + const isAccountIdOfRepoDriver = + getContractNameFromAccountId(id) === 'repoDriver'; + + if (isNaN || !isAccountIdOfRepoDriver) { + return false; + } + + return true; +} + export function toRepoDriverId(id: bigint): RepoDriverId { const repoDriverId = id.toString(); @@ -19,31 +32,30 @@ export function toRepoDriverId(id: bigint): RepoDriverId { return repoDriverId as RepoDriverId; } -export function toNftDriverId(id: bigint): NftDriverId { - const nftDriverId = id.toString(); +// NftDriver +export function isNftDriverId(id: string): id is NftDriverId { + const isNaN = Number.isNaN(Number(id)); + const isAccountIdOfNftDriver = + getContractNameFromAccountId(id) === 'nftDriver'; - if (!isNftDriverId(nftDriverId)) { - throw new Error(`Invalid 'NftDriver' account ID: ${id}.`); + if (isNaN || !isAccountIdOfNftDriver) { + return false; } - return nftDriverId as NftDriverId; + return true; } -export function toAccountId(id: bigint): AccountId { - const accountIdAsString = id.toString(); +export function toNftDriverId(id: bigint): NftDriverId { + const nftDriverId = id.toString(); - if ( - isRepoDriverId(accountIdAsString) || - isNftDriverId(accountIdAsString) || - isAddressDriverId(accountIdAsString) || - isImmutableSplitsDriverId(accountIdAsString) - ) { - return accountIdAsString as AccountId; + if (!isNftDriverId(nftDriverId)) { + throw new Error(`Invalid 'NftDriver' account ID: ${id}.`); } - throw new Error(`Invalid account ID: ${id}.`); + return nftDriverId as NftDriverId; } +// AddressDriver export function isAddressDriverId( idAsString: string, ): idAsString is AddressDriverId { @@ -67,46 +79,6 @@ export function assertAddressDiverId( } } -export function isNftDriverId(id: string): id is NftDriverId { - const isNaN = Number.isNaN(Number(id)); - const isAccountIdOfNftDriver = - getContractNameFromAccountId(id) === 'nftDriver'; - - if (isNaN || !isAccountIdOfNftDriver) { - return false; - } - - return true; -} - -export function assertNftDriverAccountId( - id: string, -): asserts id is NftDriverId { - if (!isNftDriverId(id)) { - throw new Error(`String ${id} is not a valid 'NftDriverId'.`); - } -} - -export function isRepoDriverId(id: string): id is RepoDriverId { - const isNaN = Number.isNaN(Number(id)); - const isAccountIdOfRepoDriver = - getContractNameFromAccountId(id) === 'repoDriver'; - - if (isNaN || !isAccountIdOfRepoDriver) { - return false; - } - - return true; -} - -export function assertRepoDiverAccountId( - id: string, -): asserts id is RepoDriverId { - if (!isRepoDriverId(id)) { - throw new Error(`String ${id} is not a valid 'RepoDriverId'.`); - } -} - // ImmutableSplitsDriver export function isImmutableSplitsDriverId( id: string, @@ -137,3 +109,19 @@ export async function calcAccountId(owner: AddressLike): Promise { await addressDriverContract.calcAccountId(owner as string) ).toString() as AccountId; } + +// Account ID +export function toAccountId(id: bigint): AccountId { + const accountIdAsString = id.toString(); + + if ( + isRepoDriverId(accountIdAsString) || + isNftDriverId(accountIdAsString) || + isAddressDriverId(accountIdAsString) || + isImmutableSplitsDriverId(accountIdAsString) + ) { + return accountIdAsString as AccountId; + } + + throw new Error(`Invalid account ID: ${id}.`); +} From b047428212228a537f4edfcef62b2bd69d193795 Mon Sep 17 00:00:00 2001 From: "Ioannis T." Date: Wed, 26 Mar 2025 12:30:43 +0100 Subject: [PATCH 07/60] reafactor: introduce new entities to support Ecosystems --- src/core/types.ts | 1 + src/db/database.ts | 33 ++ ...64221-add_sub_list_split_receiver_table.ts | 203 ++++++++++++ ...25100215-make_drip_list_fields_optional.ts | 102 ++++++ src/db/modelRegistration.ts | 2 + .../AccountMetadataEmittedEventHandler.ts | 48 ++- .../dripList/handleDripListMetadata.ts | 298 +++++++++++------- .../dripList/validateDripListMetadata.ts | 40 --- .../handleEcosystemMetadata.ts | 2 + .../handleSubListMetadata.ts | 2 + src/eventHandlers/TransferEventHandler.ts | 33 +- src/metadata/schemas/index.ts | 2 + src/metadata/schemas/nft-driver/v6.ts | 8 +- src/metadata/schemas/sub-list/v1.ts | 4 +- src/models/DripListModel.ts | 20 +- src/models/SubListSplitReceiverModel.ts | 135 ++++++++ src/models/index.ts | 1 + src/utils/accountIdUtils.ts | 20 +- ...AccountMetadataEmittedEventHandler.test.ts | 19 +- .../TransferEventHandler.test.ts | 95 +----- 20 files changed, 758 insertions(+), 310 deletions(-) create mode 100644 src/db/migrations/20250324164221-add_sub_list_split_receiver_table.ts create mode 100644 src/db/migrations/20250325100215-make_drip_list_fields_optional.ts delete mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/dripList/validateDripListMetadata.ts create mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts create mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts create mode 100644 src/models/SubListSplitReceiverModel.ts diff --git a/src/core/types.ts b/src/core/types.ts index 831dd7f..36e9987 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -90,6 +90,7 @@ export type DependencyOfProjectType = { export enum DependencyType { ProjectDependency = 'ProjectDependency', DripListDependency = 'DripListDependency', + EcosystemDependency = 'EcosystemDependency', } export type StreamHistoryHashes = string & { diff --git a/src/db/database.ts b/src/db/database.ts index 2c868dd..28302fb 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -9,6 +9,7 @@ import { EcosystemModel, RepoDriverSplitReceiverModel, SubListModel, + SubListSplitReceiverModel, } from '../models'; import appSettings from '../config/appSettings'; @@ -102,6 +103,14 @@ function defineAssociations() { foreignKey: 'funderDripListId', }); + // One-to-Many: A drip list can fund multiple sub list splits. + DripListModel.hasMany(SubListSplitReceiverModel, { + foreignKey: 'funderDripListId', + }); + SubListSplitReceiverModel.belongsTo(DripListModel, { + foreignKey: 'funderDripListId', + }); + // One-to-Many: An Ecosystem can have multiple SubLists. EcosystemModel.hasMany(SubListModel, { foreignKey: 'ecosystemId', @@ -110,6 +119,22 @@ function defineAssociations() { foreignKey: 'ecosystemId', }); + // One-to-Many: An Ecosystem can fund multiple sub list splits. + EcosystemModel.hasMany(SubListSplitReceiverModel, { + foreignKey: 'funderEcosystemId', + }); + SubListSplitReceiverModel.belongsTo(EcosystemModel, { + foreignKey: 'funderEcosystemId', + }); + + // One-to-Many: An Ecosystem can fund multiple project splits. + EcosystemModel.hasMany(GitProjectModel, { + foreignKey: 'funderEcosystemId', + }); + GitProjectModel.belongsTo(EcosystemModel, { + foreignKey: 'funderEcosystemId', + }); + // One-to-One: A DripListSplitReceiverModel represents/is a drip list. DripListModel.hasOne(DripListSplitReceiverModel, { foreignKey: 'fundeeDripListId', @@ -117,4 +142,12 @@ function defineAssociations() { DripListSplitReceiverModel.belongsTo(DripListModel, { foreignKey: 'fundeeDripListId', }); + + // One-to-One: A SubListSplitReceiverModel represents/is a sub list. + SubListModel.hasOne(SubListSplitReceiverModel, { + foreignKey: 'fundeeImmutableSplitsId', + }); + SubListSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'fundeeImmutableSplitsId', + }); } diff --git a/src/db/migrations/20250324164221-add_sub_list_split_receiver_table.ts b/src/db/migrations/20250324164221-add_sub_list_split_receiver_table.ts new file mode 100644 index 0000000..706915f --- /dev/null +++ b/src/db/migrations/20250324164221-add_sub_list_split_receiver_table.ts @@ -0,0 +1,203 @@ +import { DataTypes, type QueryInterface } from 'sequelize'; +import getSchema from '../../utils/getSchema'; +import { DependencyType, type DbSchema } from '../../core/types'; +import {} from '../../models'; + +export async function up({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface = sequelize.getQueryInterface(); + + await createTableIfNotExists( + queryInterface, + schema, + 'SubLists', + createSubListsTable, + ); + + await createTableIfNotExists( + queryInterface, + schema, + 'SubListSplitReceivers', + createSubListSplitReceiversTable, + ); +} + +async function createSubListsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'SubLists', schema }, + { + id: { + type: DataTypes.STRING, + primaryKey: true, + }, + ecosystemId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'Ecosystems', + key: 'id', + }, + }, + name: { + type: DataTypes.STRING, + allowNull: true, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + lastProcessedIpfsHash: { + type: DataTypes.TEXT, + allowNull: true, + }, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SubLists', schema }, + ['ecosystemId'], + { + name: 'IX_SubLists_ecosystemId', + unique: false, + }, + ); +} + +async function createSubListSplitReceiversTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { tableName: 'SubListSplitReceivers', schema }, + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + fundeeImmutableSplitsId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: 'SubLists', + key: 'id', + }, + allowNull: false, + }, + funderProjectId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: 'GitProjects', + key: 'id', + }, + allowNull: true, + }, + funderDripListId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: 'DripLists', + key: 'id', + }, + allowNull: true, + }, + funderEcosystemId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: 'Ecosystems', + key: 'id', + }, + allowNull: true, + }, + weight: { + type: DataTypes.INTEGER, + allowNull: true, + }, + type: { + type: DataTypes.ENUM(...Object.values(DependencyType)), + allowNull: false, + }, + blockTimestamp: { + type: DataTypes.DATE, + allowNull: false, + }, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SubListSplitReceivers', schema }, + ['fundeeImmutableSplitsId'], + { + name: 'IX_SubListSplitReceivers_fundeeImmutableSplitsId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SubListSplitReceivers', schema }, + ['funderProjectId'], + { + name: 'IX_SubListSplitReceivers_funderProjectId', + where: { + type: DependencyType.ProjectDependency, + }, + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SubListSplitReceivers', schema }, + ['funderDripListId'], + { + name: 'IX_SubListSplitReceivers_funderDripListId', + where: { + type: DependencyType.DripListDependency, + }, + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SubListSplitReceivers', schema }, + ['funderEcosystemId'], + { + name: 'IX_SubListSplitReceivers_funderEcosystemId', + where: { + type: DependencyType.EcosystemDependency, + }, + unique: false, + }, + ); +} + +async function createTableIfNotExists( + queryInterface: QueryInterface, + schema: DbSchema, + tableName: string, + createFn: (queryInterface: QueryInterface, schema: DbSchema) => Promise, +) { + const tableFullName = { tableName, schema }; + + const tableExists = await queryInterface + .describeTable(tableFullName) + .catch(() => null); + if (!tableExists) { + await createFn(queryInterface, schema); + } +} + +export async function down({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface = sequelize.getQueryInterface(); + + await queryInterface.dropTable({ + tableName: 'SubListSplitReceivers', + schema, + }); +} diff --git a/src/db/migrations/20250325100215-make_drip_list_fields_optional.ts b/src/db/migrations/20250325100215-make_drip_list_fields_optional.ts new file mode 100644 index 0000000..5d0e5e9 --- /dev/null +++ b/src/db/migrations/20250325100215-make_drip_list_fields_optional.ts @@ -0,0 +1,102 @@ +import { DataTypes, type QueryInterface } from 'sequelize'; +import getSchema from '../../utils/getSchema'; + +export async function up({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + await queryInterface.changeColumn( + { tableName: 'DripLists', schema }, + 'creator', + { + type: DataTypes.STRING, + allowNull: true, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'DripLists', schema }, + 'ownerAddress', + { + type: DataTypes.STRING, + allowNull: true, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'DripLists', schema }, + 'ownerAccountId', + { + type: DataTypes.STRING, + allowNull: true, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'DripLists', schema }, + 'previousOwnerAddress', + { + type: DataTypes.STRING, + allowNull: true, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'DripLists', schema }, + 'isVisible', + { + type: DataTypes.BOOLEAN, + allowNull: true, + }, + ); +} + +export async function down({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + await queryInterface.changeColumn( + { tableName: 'DripLists', schema }, + 'creator', + { + type: DataTypes.STRING, + allowNull: false, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'DripLists', schema }, + 'ownerAddress', + { + type: DataTypes.STRING, + allowNull: false, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'DripLists', schema }, + 'ownerAccountId', + { + type: DataTypes.STRING, + allowNull: false, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'DripLists', schema }, + 'previousOwnerAddress', + { + type: DataTypes.STRING, + allowNull: false, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'DripLists', schema }, + 'isVisible', + { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + ); +} diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index 7c4f8f9..7e702a1 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -19,6 +19,7 @@ import { SubListModel, CreatedSplitsEventModel, EcosystemModel, + SubListSplitReceiverModel, } from '../models'; const REGISTERED_MODELS: ModelStaticMembers[] = []; @@ -46,6 +47,7 @@ export function registerModels(): void { registerModel(OwnerUpdatedEventModel); registerModel(CreatedSplitsEventModel); registerModel(SqueezedStreamsEventModel); + registerModel(SubListSplitReceiverModel); registerModel(DripListSplitReceiverModel); registerModel(StreamReceiverSeenEventModel); registerModel(RepoDriverSplitReceiverModel); diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index 32975a6..3af9b3f 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -6,17 +6,20 @@ import { DRIPS_APP_USER_METADATA_KEY } from '../../core/constants'; import handleGitProjectMetadata from './gitProject/handleGitProjectMetadata'; import LogManager from '../../core/LogManager'; import { + isImmutableSplitsDriverId, isNftDriverId, isRepoDriverId, toAccountId, } from '../../utils/accountIdUtils'; import { isLatestEvent } from '../../utils/eventUtils'; -import { toIpfsHash } from '../../utils/metadataUtils'; +import getNftDriverMetadata, { toIpfsHash } from '../../utils/metadataUtils'; import handleDripListMetadata from './dripList/handleDripListMetadata'; import type EventHandlerRequest from '../../events/EventHandlerRequest'; import { AccountMetadataEmittedEventModel } from '../../models'; import { dbConnection } from '../../db/database'; import { getCurrentSplitsByAccountId } from '../../utils/getCurrentSplits'; +import handleEcosystemMetadata from './handleEcosystemMetadata'; +import handleSubListMetadata from './handleSubListMetadata'; export default class AccountMetadataEmittedEventHandler extends EventHandlerBase<'AccountMetadataEmitted(uint256,bytes32,bytes)'> { public readonly eventSignatures = [ @@ -115,19 +118,42 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase blockTimestamp, ); } - // The metadata are related to a Drip List. + // The metadata are related to either a Drip List or an Ecosystem. else if (isNftDriverId(typedAccountId)) { - await handleDripListMetadata( - logManager, - typedAccountId, - transaction, - ipfsHash, - blockTimestamp, - blockNumber, - ); + const metadata = await getNftDriverMetadata(ipfsHash); + + if (metadata.isDripList) { + // Legacy metadata version. + await handleDripListMetadata({ + ipfsHash, + metadata, + logManager, + transaction, + blockNumber, + blockTimestamp, + dripListId: typedAccountId, + }); + } else if ('type' in metadata) { + if (metadata.type === 'dripList') { + // Current metadata version. + await handleDripListMetadata({ + ipfsHash, + metadata, + logManager, + transaction, + blockNumber, + blockTimestamp, + dripListId: typedAccountId, + }); + } else { + await handleEcosystemMetadata(); + } + } + } else if (isImmutableSplitsDriverId(typedAccountId)) { + await handleSubListMetadata(); } else { logManager.appendLog( - `Skipping metadata processing because the account with ID ${typedAccountId} is not a Project or a Drip List.`, + `Skipping metadata processing because the account with ID ${typedAccountId} is not a Project, Drip List, or Ecosystem.`, ); } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts index bb8e5b5..051f7c9 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts @@ -2,16 +2,18 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Transaction } from 'sequelize'; import type { UUID } from 'crypto'; +import { toBigInt } from 'ethers'; import type { IpfsHash, NftDriverId } from '../../../core/types'; import { DependencyType } from '../../../core/types'; -import getNftDriverMetadata from '../../../utils/metadataUtils'; -import validateDripListMetadata from './validateDripListMetadata'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; import LogManager from '../../../core/LogManager'; import { isAddressDriverId, isNftDriverId, isRepoDriverId, + toAddressDriverId, + toImmutableSplitsDriverId, + toNftDriverId, } from '../../../utils/accountIdUtils'; import { assertDependencyOfProjectType } from '../../../utils/assert'; import createDbEntriesForProjectDependency from '../createDbEntriesForProjectDependency'; @@ -20,6 +22,7 @@ import { DripListModel, RepoDriverSplitReceiverModel, DripListSplitReceiverModel, + SubListSplitReceiverModel, } from '../../../models'; import unreachableError from '../../../utils/unreachableError'; import validateSplitsReceivers from '../splitsValidator'; @@ -27,29 +30,62 @@ import getUserAddress from '../../../utils/getAccountAddress'; import { AddressDriverSplitReceiverType } from '../../../models/AddressDriverSplitReceiverModel'; import appSettings from '../../../config/appSettings'; -export default async function handleDripListMetadata( - logManager: LogManager, - dripListId: NftDriverId, - transaction: Transaction, - ipfsHash: IpfsHash, - blockTimestamp: Date, - blockNumber: number, -) { - const dripList = await DripListModel.findByPk(dripListId, { - transaction, - lock: true, - }); +type Params = { + ipfsHash: IpfsHash; + blockNumber: number; + blockTimestamp: Date; + logManager: LogManager; + dripListId: NftDriverId; + transaction: Transaction; + metadata: AnyVersion; +}; - if (!dripList) { - throw new Error( - `Failed to update metadata for Drip List with ID ${dripListId}: Drip List not found. - \r Possible reasons: - \r\t - The event that should have created the Drip List was not processed yet. - \r\t - The metadata were manually emitted for a Drip List that does not exist in the app.`, +export default async function handleDripListMetadata({ + ipfsHash, + metadata, + logManager, + dripListId, + blockNumber, + transaction, + blockTimestamp, +}: Params) { + if (dripListId !== metadata.describes.accountId) { + unreachableError( + `Account ID mismatch with: got ${metadata.describes.accountId}, expected ${dripListId}`, + ); + } + + if ('type' in metadata.describes && metadata.describes.type !== 'dripList') { + unreachableError( + `Metadata type mismatch with: got ${metadata.describes.type}, expected a Drip List`, ); } - const metadata = await getNftDriverMetadata(ipfsHash); + // This must be the only place a Drip List is created. + const dripList = await DripListModel.create( + { + id: dripListId, + isValid: false, + name: metadata.name ?? null, + description: + 'description' in metadata ? metadata.description || null : null, + latestVotingRoundId: + 'latestVotingRoundId' in metadata + ? (metadata.latestVotingRoundId as UUID) || null + : null, + lastProcessedIpfsHash: ipfsHash, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, + }, + { transaction }, + ); + + logManager + .appendFindOrCreateLog(DripListModel, true, dripList.id) + .logAllInfo(); const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = await validateSplitsReceivers( @@ -77,121 +113,155 @@ export default async function handleDripListMetadata( return; } - await validateDripListMetadata(dripList, metadata); - - await updateDripListMetadata( - dripList, - logManager, - transaction, + await createDbEntriesForDripListSplits({ metadata, - blockNumber, - ipfsHash, - ); - - await createDbEntriesForDripListSplits( - dripListId, - metadata.projects, logManager, transaction, blockTimestamp, - ); + funderDripListId: dripListId, + }); } -async function updateDripListMetadata( - dripList: DripListModel, - logManager: LogManager, - transaction: Transaction, - metadata: AnyVersion, - blockNumber: number, - metaIpfsHash: IpfsHash, -): Promise { - dripList.isValid = true; - dripList.name = metadata.name ?? null; - dripList.description = - 'description' in metadata ? metadata.description || null : null; - dripList.latestVotingRoundId = - 'latestVotingRoundId' in metadata - ? (metadata.latestVotingRoundId as UUID) || null - : null; - dripList.lastProcessedIpfsHash = metaIpfsHash; - - if ( - blockNumber > appSettings.visibilityThresholdBlockNumber && - 'isVisible' in metadata - ) { - dripList.isVisible = metadata.isVisible; - } else { - dripList.isVisible = true; - } +async function createDbEntriesForDripListSplits({ + metadata, + logManager, + transaction, + blockTimestamp, + funderDripListId, +}: { + funderDripListId: NftDriverId; + metadata: AnyVersion; + logManager: LogManager; + transaction: Transaction; + blockTimestamp: Date; +}) { + await clearCurrentProjectSplits(funderDripListId, transaction); - logManager.appendUpdateLog(dripList, DripListModel, dripList.id); + if (metadata.projects) { + // Legacy metadata version. + const splits = metadata.projects; + const splitsPromises = splits.map((split) => { + if (isRepoDriverId(split.accountId)) { + assertDependencyOfProjectType(split); - await dripList.save({ transaction }); -} + return createDbEntriesForProjectDependency( + funderDripListId, + split, + transaction, + blockTimestamp, + ); + } -async function createDbEntriesForDripListSplits( - funderDripListId: NftDriverId, - splits: AnyVersion['projects'], - logManager: LogManager, - transaction: Transaction, - blockTimestamp: Date, -) { - await clearCurrentProjectSplits(funderDripListId, transaction); + if (isNftDriverId(split.accountId)) { + return DripListSplitReceiverModel.create( + { + funderDripListId, + fundeeDripListId: split.accountId, + weight: split.weight, + type: DependencyType.DripListDependency, + blockTimestamp, + }, + { transaction }, + ); + } - const splitsPromises = splits.map((split) => { - if (isRepoDriverId(split.accountId)) { - assertDependencyOfProjectType(split); + if (isAddressDriverId(split.accountId)) { + return AddressDriverSplitReceiverModel.create( + { + funderDripListId, + weight: split.weight, + fundeeAccountId: split.accountId, + fundeeAccountAddress: getUserAddress(split.accountId), + type: AddressDriverSplitReceiverType.DripListDependency, + blockTimestamp, + }, + { transaction }, + ); + } - return createDbEntriesForProjectDependency( - funderDripListId, - split, - transaction, - blockTimestamp, + return unreachableError( + `Split with account ID ${split.accountId} is not an Address, Drip List, or a Git Project.`, ); - } + }); - if (isNftDriverId(split.accountId)) { - return DripListSplitReceiverModel.create( - { + const result = await Promise.all([...splitsPromises]); + + logManager.appendLog( + `Updated ${LogManager.nameOfType( + DripListModel, + )} with ID ${funderDripListId} splits: ${result + .map((p) => JSON.stringify(p)) + .join(`, `)} + `, + ); + } else if ('recipients' in metadata) { + // Current metadata version. + const splits = metadata.recipients; + + const splitsPromises = splits.map((split) => { + if (split.type === 'repoDriver') { + assertDependencyOfProjectType(split); + + return createDbEntriesForProjectDependency( funderDripListId, - fundeeDripListId: split.accountId, - weight: split.weight, - type: DependencyType.DripListDependency, + split, + transaction, blockTimestamp, - }, - { transaction }, - ); - } + ); + } + if (split.type === 'dripList') { + return DripListSplitReceiverModel.create( + { + funderDripListId, + fundeeDripListId: toNftDriverId(toBigInt(split.accountId)), + weight: split.weight, + type: DependencyType.DripListDependency, + blockTimestamp, + }, + { transaction }, + ); + } + if (split.type === 'address') { + return AddressDriverSplitReceiverModel.create( + { + funderDripListId, + weight: split.weight, + fundeeAccountId: toAddressDriverId(split.accountId), + fundeeAccountAddress: getUserAddress(split.accountId), + type: AddressDriverSplitReceiverType.DripListDependency, + blockTimestamp, + }, + { transaction }, + ); + } - if (isAddressDriverId(split.accountId)) { - return AddressDriverSplitReceiverModel.create( + return SubListSplitReceiverModel.create( { funderDripListId, weight: split.weight, - fundeeAccountId: split.accountId, - fundeeAccountAddress: getUserAddress(split.accountId), - type: AddressDriverSplitReceiverType.DripListDependency, + fundeeImmutableSplitsId: toImmutableSplitsDriverId(split.accountId), + type: DependencyType.DripListDependency, blockTimestamp, }, { transaction }, ); - } + }); - return unreachableError( - `Split with account ID ${split.accountId} is not an Address, Drip List, or a Git Project.`, - ); - }); - - const result = await Promise.all([...splitsPromises]); + const result = await Promise.all([...splitsPromises]); - logManager.appendLog( - `Updated ${LogManager.nameOfType( - DripListModel, - )} with ID ${funderDripListId} splits: ${result - .map((p) => JSON.stringify(p)) - .join(`, `)} - `, - ); + logManager.appendLog( + `Updated ${LogManager.nameOfType( + DripListModel, + )} with ID ${funderDripListId} splits: ${result + .map((p) => JSON.stringify(p)) + .join(`, `)} + `, + ); + } else { + unreachableError( + `Metadata does not contain 'projects' or 'recipients' field.`, + ); + } } async function clearCurrentProjectSplits( @@ -216,4 +286,10 @@ async function clearCurrentProjectSplits( }, transaction, }); + await SubListSplitReceiverModel.destroy({ + where: { + funderDripListId, + }, + transaction, + }); } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/dripList/validateDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/dripList/validateDripListMetadata.ts deleted file mode 100644 index cee09c2..0000000 --- a/src/eventHandlers/AccountMetadataEmittedEvent/dripList/validateDripListMetadata.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { AnyVersion } from '@efstajas/versioned-parser'; -import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; -import unreachableError from '../../../utils/unreachableError'; -import type { DripListModel } from '../../../models'; - -export default async function validateDripListMetadata( - dripList: DripListModel, - metadata: AnyVersion, -): Promise { - if (!metadata) { - throw new Error( - `Drip List metadata not found for Drip List with (token) ID ${dripList.id} but it was expected to exist.`, - ); - } - - const errors = []; - - const { describes, isDripList } = metadata; - const { id: dripListId } = dripList; - - if (describes.accountId !== dripListId) { - errors.push( - `accountId mismatch with: got ${describes.accountId}, expected ${dripListId}`, - ); - } - - if (!isDripList) { - unreachableError( - `isDripList mismatch with: got ${isDripList}, expected true`, - ); - } - - if (errors.length > 0) { - throw new Error( - `Drip List with ID ${dripListId} has metadata that does not match the metadata emitted by the contract (${errors.join( - '; ', - )}).`, - ); - } -} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts new file mode 100644 index 0000000..b54c4d4 --- /dev/null +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-empty-function +export default async function handleEcosystemMetadata() {} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts new file mode 100644 index 0000000..c98ed92 --- /dev/null +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-empty-function +export default async function handleSubListMetadata() {} diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index 048ed67..bd4e4c5 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -64,34 +64,16 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add const { visibilityThresholdBlockNumber } = appSettings; - // This must be the only place a Drip List is created. - const [dripList, isDripListCreated] = await DripListModel.findOrCreate({ + const dripList = await DripListModel.findOne({ transaction, lock: true, where: { id, }, - defaults: { - id, - creator: to, // TODO: https://github.com/drips-network/events-processor/issues/14 - isValid: true, // There are no receivers yet, so the drip list is valid. - ownerAddress: to, - ownerAccountId: await calcAccountId(to), - previousOwnerAddress: from, - - isVisible: - blockNumber > visibilityThresholdBlockNumber - ? from === ZeroAddress // If it's a mint, then the Drip List will be visible. If it's a real transfer, then it's not. - : true, // If the block number is less than the visibility threshold, then the Drip List is visible by default. - }, }); - if (isDripListCreated) { - logManager - .appendFindOrCreateLog(DripListModel, isDripListCreated, dripList.id) - .logAllInfo(); - - return; + if (!dripList) { + throw new Error(`Drip List with tokenId ${id} does not exist.`); } // Here, the Drip List already exists. @@ -117,9 +99,12 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add dripList.ownerAddress = to; dripList.previousOwnerAddress = from; dripList.ownerAccountId = await calcAccountId(to); - - // This is real transfer. The Drip List should not be visible unless the block number is less than the visibility threshold. - dripList.isVisible = blockNumber < visibilityThresholdBlockNumber; + dripList.creator = to; // TODO: https://github.com/drips-network/events-processor/issues/14 + dripList.isVisible = + blockNumber > visibilityThresholdBlockNumber + ? from === ZeroAddress // If it's a mint, then the Drip List will be visible. If it's a real transfer, then it's not. + : true; // If the block number is less than the visibility threshold, then the Drip List is visible by default. + dripList.isValid = false; // The Drip List is not valid until the metadata is processed. logManager .appendIsLatestEventLog() diff --git a/src/metadata/schemas/index.ts b/src/metadata/schemas/index.ts index 57339e8..5e49b62 100644 --- a/src/metadata/schemas/index.ts +++ b/src/metadata/schemas/index.ts @@ -11,8 +11,10 @@ import { nftDriverAccountMetadataSchemaV4 } from './nft-driver/v4'; import { repoDriverAccountMetadataSchemaV5 } from './repo-driver/v5'; import { nftDriverAccountMetadataSchemaV5 } from './nft-driver/v5'; import { subListMetadataSchemaV1 } from './sub-list/v1'; +import { nftDriverAccountMetadataSchemaV6 } from './nft-driver/v6'; export const nftDriverAccountMetadataParser = createVersionedParser([ + nftDriverAccountMetadataSchemaV6.parse, nftDriverAccountMetadataSchemaV5.parse, nftDriverAccountMetadataSchemaV4.parse, nftDriverAccountMetadataSchemaV3.parse, diff --git a/src/metadata/schemas/nft-driver/v6.ts b/src/metadata/schemas/nft-driver/v6.ts index 99c5393..2cb1654 100644 --- a/src/metadata/schemas/nft-driver/v6.ts +++ b/src/metadata/schemas/nft-driver/v6.ts @@ -7,17 +7,15 @@ import { import { subListSplitReceiverSchema } from '../sub-list/v1'; import { dripListSplitReceiverSchema } from './v2'; -const base = nftDriverAccountMetadataSchemaV5; - -const ecosystemVariant = base.extend({ +const ecosystemVariant = nftDriverAccountMetadataSchemaV5.extend({ type: z.literal('ecosystem'), recipients: z.array( z.union([repoDriverSplitReceiverSchema, subListSplitReceiverSchema]), ), }); -const dripListVariant = base.extend({ - type: z.literal('drip-list'), +const dripListVariant = nftDriverAccountMetadataSchemaV5.extend({ + type: z.literal('dripList'), recipients: z.array( z.union([ repoDriverSplitReceiverSchema, diff --git a/src/metadata/schemas/sub-list/v1.ts b/src/metadata/schemas/sub-list/v1.ts index a425b8e..6056015 100644 --- a/src/metadata/schemas/sub-list/v1.ts +++ b/src/metadata/schemas/sub-list/v1.ts @@ -6,14 +6,14 @@ import { import { dripListSplitReceiverSchema } from '../nft-driver/v2'; export const subListSplitReceiverSchema = z.object({ - type: z.literal('sub-list'), + type: z.literal('subList'), weight: z.number(), accountId: z.string(), }); export const subListMetadataSchemaV1 = z.object({ driver: z.literal('immutable-splits'), - type: z.literal('sub-list'), + type: z.literal('subList'), recipients: z.array( z.union([ addressDriverSplitReceiverSchema, diff --git a/src/models/DripListModel.ts b/src/models/DripListModel.ts index c10f742..78a38e0 100644 --- a/src/models/DripListModel.ts +++ b/src/models/DripListModel.ts @@ -16,13 +16,13 @@ export default class DripListModel extends Model< public declare id: NftDriverId; // The `tokenId` from `TransferEvent` event. public declare isValid: boolean; public declare name: string | null; - public declare creator: AddressLike; + public declare creator: AddressLike | null; public declare description: string | null; - public declare ownerAddress: AddressLike; - public declare ownerAccountId: AccountId; - public declare previousOwnerAddress: AddressLike; + public declare ownerAddress: AddressLike | null; + public declare ownerAccountId: AccountId | null; + public declare previousOwnerAddress: AddressLike | null; public declare latestVotingRoundId: UUID | null; - public declare isVisible: boolean; + public declare isVisible: boolean | null; public declare lastProcessedIpfsHash: string | null; public static initialize(sequelize: Sequelize): void { @@ -38,11 +38,11 @@ export default class DripListModel extends Model< }, ownerAddress: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, ownerAccountId: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, name: { type: DataTypes.STRING, @@ -58,15 +58,15 @@ export default class DripListModel extends Model< }, creator: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, previousOwnerAddress: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, isVisible: { type: DataTypes.BOOLEAN, - allowNull: false, + allowNull: true, }, lastProcessedIpfsHash: { type: DataTypes.TEXT, diff --git a/src/models/SubListSplitReceiverModel.ts b/src/models/SubListSplitReceiverModel.ts new file mode 100644 index 0000000..2ab6113 --- /dev/null +++ b/src/models/SubListSplitReceiverModel.ts @@ -0,0 +1,135 @@ +import type { + CreationOptional, + InferAttributes, + InferCreationAttributes, + Sequelize, +} from 'sequelize'; +import { DataTypes, Model } from 'sequelize'; +import getSchema from '../utils/getSchema'; +import { DependencyType } from '../core/types'; +import type { + ImmutableSplitsDriverId, + NftDriverId, + RepoDriverId, +} from '../core/types'; +import DripListModel from './DripListModel'; +import GitProjectModel from './GitProjectModel'; +import SubListModel from './SubListModel'; +import EcosystemModel from './EcosystemModel'; + +export enum SubListSplitReceiverType { + ProjectMaintainer = 'ProjectMaintainer', + ProjectDependency = 'ProjectDependency', + DripListDependency = 'DripListDependency', +} + +export default class SubListSplitReceiverModel extends Model< + InferAttributes, + InferCreationAttributes +> { + public declare id: CreationOptional; // Primary key + public declare fundeeImmutableSplitsId: ImmutableSplitsDriverId; // Foreign key + public declare funderProjectId: RepoDriverId | null; // Foreign key + public declare funderDripListId: NftDriverId | null; // Foreign key + public declare funderEcosystemId: NftDriverId | null; // Foreign key + + public declare weight: number; + public declare type: DependencyType; + public declare blockTimestamp: Date; + + public static initialize(sequelize: Sequelize): void { + this.init( + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + fundeeImmutableSplitsId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: SubListModel, + key: 'id', + }, + allowNull: false, + }, + funderProjectId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: GitProjectModel, + key: 'id', + }, + allowNull: true, + }, + funderDripListId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: DripListModel, + key: 'id', + }, + allowNull: true, + }, + funderEcosystemId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: EcosystemModel, + key: 'id', + }, + allowNull: true, + }, + weight: { + type: DataTypes.INTEGER, + allowNull: true, + }, + type: { + type: DataTypes.ENUM(...Object.values(DependencyType)), + allowNull: false, + }, + blockTimestamp: { + type: DataTypes.DATE, + allowNull: false, + }, + }, + { + sequelize, + schema: getSchema(), + tableName: 'SubListSplitReceivers', + indexes: [ + { + fields: ['fundeeImmutableSplitsId'], + name: `IX_SubListSplitReceivers_fundeeImmutableSplitsId`, + unique: false, + }, + { + fields: ['funderProjectId'], + name: `IX_SubListSplitReceivers_funderProjectId`, + where: { + type: DependencyType.ProjectDependency, + }, + unique: false, + }, + { + fields: ['funderDripListId'], + name: `IX_SubListSplitReceivers_funderDripListId`, + where: { + type: DependencyType.DripListDependency, + }, + unique: false, + }, + { + fields: ['funderEcosystemId'], + name: `IX_SubListSplitReceivers_funderEcosystemId`, + where: { + type: DependencyType.EcosystemDependency, + }, + unique: false, + }, + ], + }, + ); + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 8b071ff..e4f2c40 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -11,6 +11,7 @@ export { default as _LastIndexedBlockModel } from './_LastIndexedBlockModel'; export { default as OwnerUpdatedEventModel } from './OwnerUpdatedEventModel'; export { default as CreatedSplitsEventModel } from './CreatedSplitsEventModel'; export { default as SqueezedStreamsEventModel } from './SqueezedStreamsEventModel'; +export { default as SubListSplitReceiverModel } from './SubListSplitReceiverModel'; export { default as DripListSplitReceiverModel } from './DripListSplitReceiverModel'; export { default as RepoDriverSplitReceiverModel } from './RepoDriverSplitReceiverModel'; export { default as StreamReceiverSeenEventModel } from './StreamReceiverSeenEventModel'; diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index cd5064a..af9657a 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -71,6 +71,14 @@ export function isAddressDriverId( return true; } +export function toAddressDriverId(id: string): AddressDriverId { + if (!isAddressDriverId(id)) { + throw new Error(`Invalid 'AddressDriver' account ID: ${id}.`); + } + + return id as AddressDriverId; +} + export function assertAddressDiverId( id: string, ): asserts id is AddressDriverId { @@ -94,14 +102,16 @@ export function isImmutableSplitsDriverId( return true; } -export function toImmutableSplitsDriverId(id: bigint): ImmutableSplitsDriverId { - const immutableSplitsDriverId = id.toString(); +export function toImmutableSplitsDriverId( + id: string | bigint, +): ImmutableSplitsDriverId { + const stringId = typeof id === 'bigint' ? id.toString() : id; - if (!isImmutableSplitsDriverId(immutableSplitsDriverId)) { - throw new Error(`Invalid 'ImmutableSplitsDriver' account ID: ${id}.`); + if (!isImmutableSplitsDriverId(stringId)) { + throw new Error(`Invalid 'ImmutableSplitsDriver' account ID: ${stringId}.`); } - return immutableSplitsDriverId as ImmutableSplitsDriverId; + return stringId as ImmutableSplitsDriverId; } export async function calcAccountId(owner: AddressLike): Promise { diff --git a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts index b94a328..562bb25 100644 --- a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts +++ b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts @@ -178,7 +178,7 @@ describe('AccountMetadataEmittedHandler', () => { args: [ 42090747530143187925772296541596488845753594998762284015257144913834n, DRIPS_APP_USER_METADATA_KEY, - '0x516d65444e625169575257666333395844754d354d69796337725755465156666b706d5a7675723965757767584a', + '0x516d647379466476796f35484b554d4158795478737163786d795a6f3233556e31764e52786b3331707176587571', ], logIndex: 1, blockNumber: 1, @@ -191,14 +191,15 @@ describe('AccountMetadataEmittedHandler', () => { await new AccountMetadataEmittedEventHandler()['_handle'](request); // Assert - expect(handleDripListMetadata.default).toHaveBeenCalledWith( - expect.anything(), - toAccountId(request.event.args[0]), - mockDbTransaction, - toIpfsHash(request.event.args[2]), - request.event.blockTimestamp, - 1, - ); + expect(handleDripListMetadata.default).toHaveBeenCalledWith({ + logManager: expect.anything(), + dripListId: toAccountId(request.event.args[0]), + transaction: mockDbTransaction, + ipfsHash: toIpfsHash(request.event.args[2]), + blockTimestamp: request.event.blockTimestamp, + blockNumber: 1, + metadata: expect.anything(), + }); }); }); }); diff --git a/tests/eventHandlers/TransferEventHandler.test.ts b/tests/eventHandlers/TransferEventHandler.test.ts index fa74fcd..b7ff455 100644 --- a/tests/eventHandlers/TransferEventHandler.test.ts +++ b/tests/eventHandlers/TransferEventHandler.test.ts @@ -4,8 +4,7 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import { TransferEventHandler } from '../../src/eventHandlers'; import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; -import { calcAccountId, toNftDriverId } from '../../src/utils/accountIdUtils'; -import { isLatestEvent } from '../../src/utils/eventUtils'; +import { toNftDriverId } from '../../src/utils/accountIdUtils'; import LogManager from '../../src/core/LogManager'; import TransferEventModel from '../../src/models/TransferEventModel'; import DripListModel from '../../src/models/DripListModel'; @@ -61,7 +60,7 @@ describe('TransferEventHandler', () => { true, ]); - DripListModel.findOrCreate = jest + DripListModel.findOne = jest .fn() .mockResolvedValue([{ save: jest.fn() }, true]); @@ -99,95 +98,5 @@ describe('TransferEventHandler', () => { }, }); }); - - test('should create a new DripListModel when the token is representing a Drip List', async () => { - // Arrange - TransferEventModel.findOrCreate = jest.fn().mockResolvedValue([ - { - transactionHash: 'TransferEventTransactionHash', - logIndex: 1, - }, - true, - ]); - - (calcAccountId as jest.Mock).mockResolvedValue('ownerAccountId'); - - DripListModel.findOrCreate = jest - .fn() - .mockResolvedValue([{ save: jest.fn() }, true]); - - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); - - // Act - await handler['_handle'](mockRequest); - - // Assert - const { - event: { - args: [from, to, tokenId], - }, - } = mockRequest; - - expect(DripListModel.findOrCreate).toHaveBeenCalledWith({ - transaction: mockDbTransaction, - lock: true, - where: { - id: toNftDriverId(tokenId), - }, - defaults: { - id: toNftDriverId(tokenId), - creator: to, - isValid: true, - isVisible: false, - ownerAddress: to, - ownerAccountId: 'ownerAccountId', - previousOwnerAddress: from, - }, - }); - }); - - test('should update the DripListModel when the incoming event is the latest', async () => { - // Arrange - TransferEventModel.findOrCreate = jest.fn().mockResolvedValue([ - { - transactionHash: 'TransferEventTransactionHash', - logIndex: 1, - }, - true, - ]); - - (calcAccountId as jest.Mock).mockResolvedValue('ownerAccountId'); - - const mockDripList = { - ownerAddress: '', - previousOwnerAddress: '', - ownerAccountId: '', - save: jest.fn(), - }; - DripListModel.findOrCreate = jest - .fn() - .mockResolvedValue([mockDripList, false]); - - (isLatestEvent as jest.Mock).mockResolvedValue(true); - - LogManager.prototype.appendIsLatestEventLog = jest.fn().mockReturnThis(); - - // Act - await handler['_handle'](mockRequest); - - // Assert - const { - event: { - args: [from, to], - }, - } = mockRequest; - - expect(mockDripList.ownerAddress).toBe(to); - expect(mockDripList.previousOwnerAddress).toBe(from); - expect(mockDripList.ownerAccountId).toBe('ownerAccountId'); - expect(mockDripList.save).toHaveBeenCalledWith({ - transaction: mockDbTransaction, - }); - }); }); }); From 29cf144a4b2e7f19a2bb1a1d9f292816a95a0474 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 31 Mar 2025 11:31:53 +0200 Subject: [PATCH 08/60] refactor: process ecosystem metadata --- src/core/types.ts | 13 - src/db/database.ts | 4 +- ...stems_and_created_splits_events_tables.ts} | 67 ----- ...te_sublists_and_split_receivers_tables.ts} | 38 ++- ...25100215-make_driplist_fields_nullable.ts} | 0 ...enum_and_make_ecosystem_fields_nullable.ts | 115 +++++++++ ..._funder_ecosystem_id_to_split_receivers.ts | 112 +++++++++ ...ove-latestVotingRoundId-from-ecosystems.ts | 27 ++ .../AccountMetadataEmittedEventHandler.ts | 80 ++++-- .../createDbEntriesForProjectDependency.ts | 24 +- .../dripList/handleDripListMetadata.ts | 25 +- .../gitProject/handleGitProjectMetadata.ts | 5 +- .../handleEcosystemMetadata.ts | 195 ++++++++++++++- .../handleSubListMetadata.ts | 234 +++++++++++++++++- .../splitsValidator.ts | 8 +- .../SplitsSetEventHandler/setIsValidFlag.ts | 82 ++++-- src/eventHandlers/TransferEventHandler.ts | 76 +++--- src/metadata/schemas/nft-driver/v6.ts | 9 +- src/models/AddressDriverSplitReceiverModel.ts | 20 ++ src/models/DripListSplitReceiverModel.ts | 19 ++ src/models/EcosystemModel.ts | 24 +- src/models/RepoDriverSplitReceiverModel.ts | 19 ++ src/models/SubListModel.ts | 18 +- src/utils/accountIdUtils.ts | 4 +- src/utils/assert.ts | 17 +- src/utils/metadataUtils.ts | 12 +- 26 files changed, 1003 insertions(+), 244 deletions(-) rename src/db/migrations/{20250319131551-add_models_to_support_ecosystems.ts => 20250319131551-create_ecosystems_and_created_splits_events_tables.ts} (71%) rename src/db/migrations/{20250324164221-add_sub_list_split_receiver_table.ts => 20250324164221-create_sublists_and_split_receivers_tables.ts} (86%) rename src/db/migrations/{20250325100215-make_drip_list_fields_optional.ts => 20250325100215-make_driplist_fields_nullable.ts} (100%) create mode 100644 src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts create mode 100644 src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts create mode 100644 src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts diff --git a/src/core/types.ts b/src/core/types.ts index 36e9987..a306a3b 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,9 +1,4 @@ -import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Model, Sequelize } from 'sequelize'; -import type { - nftDriverAccountMetadataParser, - repoDriverAccountMetadataParser, -} from '../metadata/schemas'; import type { DRIPS_CONTRACTS, FORGES_MAP, @@ -67,14 +62,6 @@ export type ModelStaticMembers = { initialize(sequelize: Sequelize): void; }; -type ArrayElement = - ArrayType extends readonly (infer ElementType)[] ? ElementType : never; - -export type Dependency = ArrayElement< - | AnyVersion['splits']['dependencies'] - | AnyVersion['projects'] ->; - export type DependencyOfProjectType = { type: 'repoDriver'; accountId: RepoDriverId; diff --git a/src/db/database.ts b/src/db/database.ts index 28302fb..9b6212f 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -128,10 +128,10 @@ function defineAssociations() { }); // One-to-Many: An Ecosystem can fund multiple project splits. - EcosystemModel.hasMany(GitProjectModel, { + EcosystemModel.hasMany(RepoDriverSplitReceiverModel, { foreignKey: 'funderEcosystemId', }); - GitProjectModel.belongsTo(EcosystemModel, { + RepoDriverSplitReceiverModel.belongsTo(EcosystemModel, { foreignKey: 'funderEcosystemId', }); diff --git a/src/db/migrations/20250319131551-add_models_to_support_ecosystems.ts b/src/db/migrations/20250319131551-create_ecosystems_and_created_splits_events_tables.ts similarity index 71% rename from src/db/migrations/20250319131551-add_models_to_support_ecosystems.ts rename to src/db/migrations/20250319131551-create_ecosystems_and_created_splits_events_tables.ts index 903d84c..1c0b106 100644 --- a/src/db/migrations/20250319131551-add_models_to_support_ecosystems.ts +++ b/src/db/migrations/20250319131551-create_ecosystems_and_created_splits_events_tables.ts @@ -14,13 +14,6 @@ export async function up({ context: sequelize }: any): Promise { createEcosystemsTable, ); - await createTableIfNotExists( - queryInterface, - schema, - 'SubLists', - createSubListsTable, - ); - await createTableIfNotExists( queryInterface, schema, @@ -86,10 +79,6 @@ async function createEcosystemsTable( type: DataTypes.STRING, allowNull: true, }, - latestVotingRoundId: { - type: DataTypes.UUID, - allowNull: true, - }, description: { type: DataTypes.TEXT, allowNull: true, @@ -133,61 +122,6 @@ async function createEcosystemsTable( ); } -async function createSubListsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'SubLists', schema }, - { - id: { - type: DataTypes.STRING, - primaryKey: true, - }, - ecosystemId: { - // Foreign key - type: DataTypes.STRING, - allowNull: true, - references: { - model: 'Ecosystems', - key: 'id', - }, - }, - name: { - type: DataTypes.STRING, - allowNull: true, - }, - description: { - type: DataTypes.TEXT, - allowNull: true, - }, - lastProcessedIpfsHash: { - type: DataTypes.TEXT, - allowNull: true, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SubLists', schema }, - ['ecosystemId'], - { - name: 'IX_SubLists_ecosystemId', - unique: false, - }, - ); -} - async function createTableIfNotExists( queryInterface: QueryInterface, schema: DbSchema, @@ -210,5 +144,4 @@ export async function down({ context: sequelize }: any): Promise { await queryInterface.dropTable({ tableName: 'CreatedSplitsEvents', schema }); await queryInterface.dropTable({ tableName: 'Ecosystems', schema }); - await queryInterface.dropTable({ tableName: 'SubLists', schema }); } diff --git a/src/db/migrations/20250324164221-add_sub_list_split_receiver_table.ts b/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts similarity index 86% rename from src/db/migrations/20250324164221-add_sub_list_split_receiver_table.ts rename to src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts index 706915f..f92ec82 100644 --- a/src/db/migrations/20250324164221-add_sub_list_split_receiver_table.ts +++ b/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts @@ -1,4 +1,4 @@ -import { DataTypes, type QueryInterface } from 'sequelize'; +import { DataTypes, literal, type QueryInterface } from 'sequelize'; import getSchema from '../../utils/getSchema'; import { DependencyType, type DbSchema } from '../../core/types'; import {} from '../../models'; @@ -33,7 +33,7 @@ async function createSubListsTable( type: DataTypes.STRING, primaryKey: true, }, - ecosystemId: { + parentAccountId: { // Foreign key type: DataTypes.STRING, allowNull: true, @@ -42,26 +42,28 @@ async function createSubListsTable( key: 'id', }, }, - name: { - type: DataTypes.STRING, - allowNull: true, - }, - description: { - type: DataTypes.TEXT, - allowNull: true, - }, lastProcessedIpfsHash: { type: DataTypes.TEXT, allowNull: true, }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, }, ); await queryInterface.addIndex( { tableName: 'SubLists', schema }, - ['ecosystemId'], + ['parentAccountId'], { - name: 'IX_SubLists_ecosystemId', + name: 'IX_SubLists_parentAccountId', unique: false, }, ); @@ -127,6 +129,16 @@ async function createSubListSplitReceiversTable( type: DataTypes.DATE, allowNull: false, }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: literal('NOW()'), + }, }, ); @@ -200,4 +212,6 @@ export async function down({ context: sequelize }: any): Promise { tableName: 'SubListSplitReceivers', schema, }); + + await queryInterface.dropTable({ tableName: 'SubLists', schema }); } diff --git a/src/db/migrations/20250325100215-make_drip_list_fields_optional.ts b/src/db/migrations/20250325100215-make_driplist_fields_nullable.ts similarity index 100% rename from src/db/migrations/20250325100215-make_drip_list_fields_optional.ts rename to src/db/migrations/20250325100215-make_driplist_fields_nullable.ts diff --git a/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts b/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts new file mode 100644 index 0000000..a1a0f0f --- /dev/null +++ b/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts @@ -0,0 +1,115 @@ +import { DataTypes, type QueryInterface } from 'sequelize'; +import getSchema from '../../utils/getSchema'; + +export async function up({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + await queryInterface.sequelize.query(` + ALTER TYPE "${schema}"."enum_SubListSplitReceivers_type" ADD VALUE IF NOT EXISTS 'EcosystemDependency'; + `); + await queryInterface.sequelize.query(` + ALTER TYPE "${schema}"."enum_RepoDriverSplitReceivers_type" ADD VALUE IF NOT EXISTS 'EcosystemDependency'; + `); + await queryInterface.sequelize.query(` + ALTER TYPE "${schema}"."enum_DripListSplitReceivers_type" ADD VALUE IF NOT EXISTS 'EcosystemDependency'; + `); + await queryInterface.sequelize.query(` + ALTER TYPE "${schema}"."enum_AddressDriverSplitReceivers_type" ADD VALUE IF NOT EXISTS 'EcosystemDependency'; + `); + + await queryInterface.changeColumn( + { tableName: 'Ecosystems', schema }, + 'creator', + { + type: DataTypes.STRING, + allowNull: true, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'Ecosystems', schema }, + 'ownerAddress', + { + type: DataTypes.STRING, + allowNull: true, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'Ecosystems', schema }, + 'ownerAccountId', + { + type: DataTypes.STRING, + allowNull: true, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'Ecosystems', schema }, + 'previousOwnerAddress', + { + type: DataTypes.STRING, + allowNull: true, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'Ecosystems', schema }, + 'isVisible', + { + type: DataTypes.BOOLEAN, + allowNull: true, + }, + ); +} + +export async function down({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + await queryInterface.changeColumn( + { tableName: 'Ecosystems', schema }, + 'creator', + { + type: DataTypes.STRING, + allowNull: false, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'Ecosystems', schema }, + 'ownerAddress', + { + type: DataTypes.STRING, + allowNull: false, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'Ecosystems', schema }, + 'ownerAccountId', + { + type: DataTypes.STRING, + allowNull: false, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'Ecosystems', schema }, + 'previousOwnerAddress', + { + type: DataTypes.STRING, + allowNull: false, + }, + ); + + await queryInterface.changeColumn( + { tableName: 'Ecosystems', schema }, + 'isVisible', + { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + ); +} diff --git a/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts b/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts new file mode 100644 index 0000000..10c8156 --- /dev/null +++ b/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts @@ -0,0 +1,112 @@ +import { DataTypes, type QueryInterface } from 'sequelize'; +import getSchema from '../../utils/getSchema'; + +export async function up({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + await queryInterface.addColumn( + { tableName: 'RepoDriverSplitReceivers', schema }, + 'funderEcosystemId', + { + type: DataTypes.STRING, + references: { + model: 'Ecosystems', + key: 'id', + }, + allowNull: true, + }, + ); + + await queryInterface.addIndex( + { tableName: 'RepoDriverSplitReceivers', schema }, + ['funderEcosystemId'], + { + name: 'IX_RepoDriverSplitReceivers_funderEcosystemId', + where: { + type: 'EcosystemDependency', + }, + unique: false, + }, + ); + + await queryInterface.addColumn( + { tableName: 'DripListSplitReceivers', schema }, + 'funderEcosystemId', + { + type: DataTypes.STRING, + references: { + model: 'Ecosystems', + key: 'id', + }, + allowNull: true, + }, + ); + + await queryInterface.addIndex( + { tableName: 'DripListSplitReceivers', schema }, + ['funderEcosystemId'], + { + name: 'IX_DripListSplitReceivers_funderEcosystemId', + where: { + type: 'EcosystemDependency', + }, + unique: false, + }, + ); + + await queryInterface.addColumn( + { tableName: 'AddressDriverSplitReceivers', schema }, + 'funderEcosystemId', + { + type: DataTypes.STRING, + references: { + model: 'Ecosystems', + key: 'id', + }, + allowNull: true, + }, + ); + + await queryInterface.addIndex( + { tableName: 'AddressDriverSplitReceivers', schema }, + ['funderEcosystemId'], + { + name: 'IX_AddressDriverSplitReceivers_funderEcosystemId', + where: { + type: 'EcosystemDependency', + }, + unique: false, + }, + ); +} + +export async function down({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + await queryInterface.removeColumn( + { tableName: 'RepoDriverSplitReceivers', schema }, + 'funderEcosystemId', + ); + await queryInterface.removeIndex( + { tableName: 'RepoDriverSplitReceivers', schema }, + 'IX_RepoDriverSplitReceivers_funderEcosystemId', + ); + await queryInterface.removeColumn( + { tableName: 'DripListSplitReceivers', schema }, + 'funderEcosystemId', + ); + await queryInterface.removeIndex( + { tableName: 'DripListSplitReceivers', schema }, + 'IX_DripListSplitReceivers_funderEcosystemId', + ); + await queryInterface.removeColumn( + { tableName: 'AddressDriverSplitReceivers', schema }, + 'funderEcosystemId', + ); + await queryInterface.removeIndex( + { tableName: 'AddressDriverSplitReceivers', schema }, + 'IX_AddressDriverSplitReceivers_funderEcosystemId', + ); +} diff --git a/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts b/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts new file mode 100644 index 0000000..931fb49 --- /dev/null +++ b/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts @@ -0,0 +1,27 @@ +import { DataTypes, type QueryInterface } from 'sequelize'; +import getSchema from '../../utils/getSchema'; + +export async function up({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + await queryInterface.removeColumn( + { tableName: 'Ecosystems', schema }, + 'latestVotingRoundId', + ); +} + +export async function down({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + await queryInterface.addColumn( + { tableName: 'Ecosystems', schema }, + 'latestVotingRoundId', + { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + }, + ); +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index 3af9b3f..acdeddb 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -1,3 +1,4 @@ +import type { AnyVersion } from '@efstajas/versioned-parser'; import type { AccountMetadataEmittedEvent } from '../../../contracts/CURRENT_NETWORK/Drips'; import type { AccountId } from '../../core/types'; @@ -12,7 +13,7 @@ import { toAccountId, } from '../../utils/accountIdUtils'; import { isLatestEvent } from '../../utils/eventUtils'; -import getNftDriverMetadata, { toIpfsHash } from '../../utils/metadataUtils'; +import { getNftDriverMetadata, toIpfsHash } from '../../utils/metadataUtils'; import handleDripListMetadata from './dripList/handleDripListMetadata'; import type EventHandlerRequest from '../../events/EventHandlerRequest'; import { AccountMetadataEmittedEventModel } from '../../models'; @@ -20,6 +21,7 @@ import { dbConnection } from '../../db/database'; import { getCurrentSplitsByAccountId } from '../../utils/getCurrentSplits'; import handleEcosystemMetadata from './handleEcosystemMetadata'; import handleSubListMetadata from './handleSubListMetadata'; +import type { nftDriverAccountMetadataParser } from '../../metadata/schemas'; export default class AccountMetadataEmittedEventHandler extends EventHandlerBase<'AccountMetadataEmitted(uint256,bytes32,bytes)'> { public readonly eventSignatures = [ @@ -108,7 +110,8 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase logManager.appendIsLatestEventLog(); - // The metadata are related to a Project. + let handled = false; + if (isRepoDriverId(typedAccountId)) { await handleGitProjectMetadata( logManager, @@ -117,13 +120,14 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase ipfsHash, blockTimestamp, ); + + handled = true; } - // The metadata are related to either a Drip List or an Ecosystem. - else if (isNftDriverId(typedAccountId)) { + + if (isNftDriverId(typedAccountId)) { const metadata = await getNftDriverMetadata(ipfsHash); - if (metadata.isDripList) { - // Legacy metadata version. + if (this._isDripListMetadata(metadata)) { await handleDripListMetadata({ ipfsHash, metadata, @@ -133,25 +137,38 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase blockTimestamp, dripListId: typedAccountId, }); - } else if ('type' in metadata) { - if (metadata.type === 'dripList') { - // Current metadata version. - await handleDripListMetadata({ - ipfsHash, - metadata, - logManager, - transaction, - blockNumber, - blockTimestamp, - dripListId: typedAccountId, - }); - } else { - await handleEcosystemMetadata(); - } + + handled = true; } - } else if (isImmutableSplitsDriverId(typedAccountId)) { - await handleSubListMetadata(); - } else { + + if (this._isEcosystemMetadata(metadata)) { + await handleEcosystemMetadata({ + ipfsHash, + metadata, + logManager, + transaction, + blockNumber, + blockTimestamp, + ecosystemId: typedAccountId, + }); + + handled = true; + } + } + + if (isImmutableSplitsDriverId(typedAccountId)) { + await handleSubListMetadata({ + ipfsHash, + logManager, + transaction, + blockTimestamp, + subListId: typedAccountId, + }); + + handled = true; + } + + if (!handled) { logManager.appendLog( `Skipping metadata processing because the account with ID ${typedAccountId} is not a Project, Drip List, or Ecosystem.`, ); @@ -182,4 +199,19 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase private _isEmittedByTheDripsApp(key: string): boolean { return key === DRIPS_APP_USER_METADATA_KEY; } + + private _isDripListMetadata( + metadata: AnyVersion, + ): boolean { + return ( + metadata.isDripList || + ('type' in metadata ? metadata.type === 'dripList' : false) + ); + } + + private _isEcosystemMetadata( + metadata: AnyVersion, + ): boolean { + return 'type' in metadata && metadata.type === 'ecosystem'; + } } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts b/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts index ddc9389..04daa43 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts @@ -11,10 +11,12 @@ import GitProjectModel, { import { FORGES_MAP } from '../../core/constants'; import unreachableError from '../../utils/unreachableError'; import RepoDriverSplitReceiverModel from '../../models/RepoDriverSplitReceiverModel'; -import { isNftDriverId, isRepoDriverId } from '../../utils/accountIdUtils'; export default async function createDbEntriesForProjectDependency( - funderAccountId: RepoDriverId | NftDriverId, + funder: + | { type: 'project'; accountId: RepoDriverId } + | { type: 'dripList'; accountId: NftDriverId } + | { type: 'ecosystem'; accountId: NftDriverId }, projectDependency: DependencyOfProjectType, transaction: Transaction, blockTimestamp: Date, @@ -45,15 +47,23 @@ export default async function createDbEntriesForProjectDependency( }, }); + let type: DependencyType; + if (funder.type === 'project') { + type = DependencyType.ProjectDependency; + } else if (funder.type === 'dripList') { + type = DependencyType.DripListDependency; + } else { + type = DependencyType.EcosystemDependency; + } + return RepoDriverSplitReceiverModel.create( { weight, fundeeProjectId, - type: isNftDriverId(funderAccountId) - ? DependencyType.DripListDependency - : DependencyType.ProjectDependency, - funderDripListId: isNftDriverId(funderAccountId) ? funderAccountId : null, - funderProjectId: isRepoDriverId(funderAccountId) ? funderAccountId : null, + type, + funderDripListId: funder.type === 'dripList' ? funder.accountId : null, + funderProjectId: funder.type === 'project' ? funder.accountId : null, + funderEcosystemId: funder.type === 'ecosystem' ? funder.accountId : null, blockTimestamp, }, { transaction }, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts index 051f7c9..a813a2f 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts @@ -55,9 +55,9 @@ export default async function handleDripListMetadata({ ); } - if ('type' in metadata.describes && metadata.describes.type !== 'dripList') { + if ('type' in metadata && metadata.type !== 'dripList') { unreachableError( - `Metadata type mismatch with: got ${metadata.describes.type}, expected a Drip List`, + `Metadata type mismatch with: got '${metadata.type}', expected 'dripList`, ); } @@ -65,7 +65,7 @@ export default async function handleDripListMetadata({ const dripList = await DripListModel.create( { id: dripListId, - isValid: false, + isValid: false, // Until the related `TransferEvent` is processed. name: metadata.name ?? null, description: 'description' in metadata ? metadata.description || null : null, @@ -87,10 +87,17 @@ export default async function handleDripListMetadata({ .appendFindOrCreateLog(DripListModel, true, dripList.id) .logAllInfo(); + if (metadata.projects && 'recipients' in metadata && metadata.recipients) { + unreachableError( + `Metadata contains both 'projects' and 'recipients' fields. This is not allowed.`, + ); + } + const splits = metadata.projects ?? metadata.recipients; + const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = await validateSplitsReceivers( dripList.id, - metadata.projects.map((s) => ({ + splits.map((s) => ({ weight: s.weight, accountId: s.accountId, })), @@ -145,7 +152,10 @@ async function createDbEntriesForDripListSplits({ assertDependencyOfProjectType(split); return createDbEntriesForProjectDependency( - funderDripListId, + { + type: 'dripList', + accountId: funderDripListId, + }, split, transaction, blockTimestamp, @@ -203,7 +213,10 @@ async function createDbEntriesForDripListSplits({ assertDependencyOfProjectType(split); return createDbEntriesForProjectDependency( - funderDripListId, + { + type: 'dripList', + accountId: funderDripListId, + }, split, transaction, blockTimestamp, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts index 0dce610..3b4a873 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts @@ -166,7 +166,10 @@ async function createDbEntriesForProjectSplits( assertDependencyOfProjectType(dependency); return createDbEntriesForProjectDependency( - funderProjectId, + { + type: 'project', + accountId: funderProjectId, + }, dependency, transaction, blockTimestamp, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts index b54c4d4..c652422 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts @@ -1,2 +1,193 @@ -// eslint-disable-next-line no-empty-function -export default async function handleEcosystemMetadata() {} +import type { AnyVersion, LatestVersion } from '@efstajas/versioned-parser'; +import type { UUID } from 'crypto'; +import type { Transaction } from 'sequelize'; +import appSettings from '../../config/appSettings'; +import LogManager from '../../core/LogManager'; +import type { IpfsHash, NftDriverId } from '../../core/types'; +import { DependencyType } from '../../core/types'; +import type { nftDriverAccountMetadataParser } from '../../metadata/schemas'; +import { + SubListSplitReceiverModel, + RepoDriverSplitReceiverModel, + EcosystemModel, +} from '../../models'; +import { toImmutableSplitsDriverId } from '../../utils/accountIdUtils'; +import { assertDependencyOfProjectType } from '../../utils/assert'; +import unreachableError from '../../utils/unreachableError'; +import createDbEntriesForProjectDependency from './createDbEntriesForProjectDependency'; +import validateSplitsReceivers from './splitsValidator'; + +type Params = { + ipfsHash: IpfsHash; + blockNumber: number; + blockTimestamp: Date; + logManager: LogManager; + ecosystemId: NftDriverId; + transaction: Transaction; + metadata: AnyVersion; +}; + +export default async function handleEcosystemMetadata({ + ipfsHash, + metadata, + logManager, + ecosystemId, + blockNumber, + transaction, + blockTimestamp, +}: Params) { + if (ecosystemId !== metadata.describes.accountId) { + unreachableError( + `Account ID mismatch with: got ${metadata.describes.accountId}, expected ${ecosystemId}`, + ); + } + + if ('type' in metadata && metadata.type !== 'ecosystem') { + unreachableError( + `Metadata type mismatch with: got ${metadata.type}, expected 'ecosystem'`, + ); + } + + if (!('recipients' in metadata)) { + unreachableError( + `Unsupported metadata version: missing 'recipients' field`, + ); + } + // This must be the only place an Ecosystem is created. + const ecosystem = await EcosystemModel.create( + { + id: ecosystemId, + isValid: false, + name: metadata.name ?? null, + description: + 'description' in metadata ? metadata.description || null : null, + latestVotingRoundId: + 'latestVotingRoundId' in metadata + ? (metadata.latestVotingRoundId as UUID) || null + : null, + lastProcessedIpfsHash: ipfsHash, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, + }, + { transaction }, + ); + + logManager + .appendFindOrCreateLog(EcosystemModel, true, ecosystem.id) + .logAllInfo(); + + const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = + await validateSplitsReceivers( + ecosystem.id, + metadata.recipients.map((s) => ({ + weight: s.weight, + accountId: s.accountId, + })), + ); + + // If we reach this point, it means that the processed `AccountMetadataEmitted` event is the latest in the DB. + // But we still need to check if the splits are the latest on-chain. + // There is no need to process the metadata if the splits are not the latest on-chain. + + if (!areSplitsValid) { + logManager.appendLog( + `Skipping metadata update for Ecosystem with ID ${ecosystemId} because the splits receivers hashes from the contract and the metadata do not match: + \r\t - On-chain splits receivers hash: ${onChainSplitsHash} + \r\t - Metadata splits receivers hash: ${calculatedSplitsHash} + \r Possible reasons: + \r\t - The metadata were the latest in the DB but not on-chain. + \r\t - The metadata were manually emitted with different splits than the latest on-chain.`, + ); + + return; + } + + await createDbEntriesForEcosystemSplits({ + metadata, + logManager, + transaction, + blockTimestamp, + funderEcosystemId: ecosystemId, + }); +} + +async function createDbEntriesForEcosystemSplits({ + metadata, + logManager, + transaction, + blockTimestamp, + funderEcosystemId, +}: { + funderEcosystemId: NftDriverId; + metadata: LatestVersion; + logManager: LogManager; + transaction: Transaction; + blockTimestamp: Date; +}) { + await clearCurrentEcosystemSplits(funderEcosystemId, transaction); + + const splits = metadata.recipients; + + const splitsPromises = splits.map((split) => { + if (split.type === 'repoDriver') { + assertDependencyOfProjectType(split); + + return createDbEntriesForProjectDependency( + { + type: 'ecosystem', + accountId: funderEcosystemId, + }, + split, + transaction, + blockTimestamp, + ); + } + if (split.type === 'subList') { + return SubListSplitReceiverModel.create( + { + funderEcosystemId, + weight: split.weight, + fundeeImmutableSplitsId: toImmutableSplitsDriverId(split.accountId), + type: DependencyType.EcosystemDependency, + blockTimestamp, + }, + { transaction }, + ); + } + return unreachableError( + `Split with account ID ${split.accountId} is not a Project or an Ecosystem.`, + ); + }); + + const result = await Promise.all([...splitsPromises]); + + logManager.appendLog( + `Updated ${LogManager.nameOfType( + EcosystemModel, + )} with ID ${funderEcosystemId} splits: ${result + .map((p) => JSON.stringify(p)) + .join(`, `)} + `, + ); +} + +async function clearCurrentEcosystemSplits( + funderEcosystemId: string, + transaction: Transaction, +) { + await RepoDriverSplitReceiverModel.destroy({ + where: { + funderEcosystemId, + }, + transaction, + }); + await SubListSplitReceiverModel.destroy({ + where: { + funderEcosystemId, + }, + transaction, + }); +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts index c98ed92..a04f35a 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts @@ -1,2 +1,232 @@ -// eslint-disable-next-line no-empty-function -export default async function handleSubListMetadata() {} +/* eslint-disable no-param-reassign */ +import type { AnyVersion } from '@efstajas/versioned-parser'; +import type { Transaction } from 'sequelize'; +import LogManager from '../../core/LogManager'; +import type { + IpfsHash, + ImmutableSplitsDriverId, + NftDriverId, +} from '../../core/types'; +import { DependencyType } from '../../core/types'; +import type { immutableSplitsDriverMetadataParser } from '../../metadata/schemas'; +import { + SubListModel, + AddressDriverSplitReceiverModel, + DripListSplitReceiverModel, + RepoDriverSplitReceiverModel, + SubListSplitReceiverModel, +} from '../../models'; +import { AddressDriverSplitReceiverType } from '../../models/AddressDriverSplitReceiverModel'; +import { + isRepoDriverId, + isAddressDriverId, + isNftDriverId, + toNftDriverId, + isImmutableSplitsDriverId, +} from '../../utils/accountIdUtils'; +import { assertDependencyOfProjectType } from '../../utils/assert'; +import getUserAddress from '../../utils/getAccountAddress'; +import { getImmutableSpitsDriverMetadata } from '../../utils/metadataUtils'; +import unreachableError from '../../utils/unreachableError'; +import createDbEntriesForProjectDependency from './createDbEntriesForProjectDependency'; +import validateSplitsReceivers from './splitsValidator'; + +type Params = { + ipfsHash: IpfsHash; + blockTimestamp: Date; + logManager: LogManager; + transaction: Transaction; + subListId: ImmutableSplitsDriverId; +}; + +export default async function handleSubListMetadata({ + ipfsHash, + subListId, + logManager, + transaction, + blockTimestamp, +}: Params) { + const subList = await SubListModel.findByPk(subListId, { + transaction, + lock: true, + }); + + if (!subList) { + throw new Error( + `Failed to update metadata for Sub List with ID ${subListId}: Sub List not found. + \r Possible reasons: + \r\t - The event that should have created the Sub List was not processed yet. + \r\t - The metadata were manually emitted for a Sub List that does not exist in the app.`, + ); + } + + const metadata = await getImmutableSpitsDriverMetadata(ipfsHash); + + const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = + await validateSplitsReceivers(subList.id, metadata.recipients); + + // If we reach this point, it means that the processed `AccountMetadataEmitted` event is the latest in the DB. + // But we still need to check if the splits are the latest on-chain. + // There is no need to process the metadata if the splits are not the latest on-chain. + + if (!areSplitsValid) { + logManager.appendLog( + `Skipping metadata update for Sub List with ID ${subListId} because the splits receivers hashes from the contract and the metadata do not match: + \r\t - On-chain splits receivers hash: ${onChainSplitsHash} + \r\t - Metadata splits receivers hash: ${calculatedSplitsHash} + \r Possible reasons: + \r\t - The metadata were the latest in the DB but not on-chain. + \r\t - The metadata were manually emitted with different splits than the latest on-chain.`, + ); + + return; + } + + if (subList.dataValues.parentAccountId !== metadata.parent.accountId) { + throw new Error( + `Sub List with ID ${subListId} has a different on-chain parent account ID than the metadata. Expected: '${subList.dataValues.parentAccountId}', got: '${metadata.parent.accountId}'`, + ); + } + + await updateSubListMetadata( + subList, + logManager, + transaction, + metadata, + ipfsHash, + ); + + await createDbEntriesForSubListSplits( + toNftDriverId(metadata.parent.accountId), + metadata.recipients, + logManager, + transaction, + blockTimestamp, + ); +} + +async function updateSubListMetadata( + subList: SubListModel, + logManager: LogManager, + transaction: Transaction, + metadata: AnyVersion, + metadataIpfsHash: IpfsHash, +): Promise { + subList.parentAccountId = toNftDriverId(metadata.parent.accountId); + subList.lastProcessedIpfsHash = metadataIpfsHash; + + logManager.appendUpdateLog(subList, SubListModel, subList.id); + + await subList.save({ transaction }); +} + +async function createDbEntriesForSubListSplits( + funderEcosystemId: NftDriverId, + splits: AnyVersion['recipients'], + logManager: LogManager, + transaction: Transaction, + blockTimestamp: Date, +) { + await clearCurrentProjectSplits(funderEcosystemId, transaction); + + const splitsPromises = splits.map(async (dependency) => { + if (isRepoDriverId(dependency.accountId)) { + assertDependencyOfProjectType(dependency); + + return createDbEntriesForProjectDependency( + { + type: 'ecosystem', + accountId: funderEcosystemId, + }, + dependency, + transaction, + blockTimestamp, + ); + } + + if (isAddressDriverId(dependency.accountId)) { + return AddressDriverSplitReceiverModel.create( + { + funderEcosystemId, + weight: dependency.weight, + fundeeAccountId: dependency.accountId, + fundeeAccountAddress: getUserAddress(dependency.accountId), + type: AddressDriverSplitReceiverType.EcosystemDependency, + blockTimestamp, + }, + { transaction }, + ); + } + + if (isNftDriverId(dependency.accountId)) { + return DripListSplitReceiverModel.create( + { + funderEcosystemId, + fundeeDripListId: dependency.accountId, + weight: dependency.weight, + type: DependencyType.EcosystemDependency, + blockTimestamp, + }, + { transaction }, + ); + } + + if (isImmutableSplitsDriverId(dependency.accountId)) { + return SubListSplitReceiverModel.create( + { + funderEcosystemId, + fundeeImmutableSplitsId: dependency.accountId, + weight: dependency.weight, + type: DependencyType.EcosystemDependency, + blockTimestamp, + }, + { transaction }, + ); + } + + return unreachableError( + `Dependency with account ID ${dependency.accountId} is not an Address nor a Git Project.`, + ); + }); + + const result = await Promise.all(splitsPromises); + + logManager.appendLog( + `Updated ${LogManager.nameOfType( + SubListModel, + )} with ID ${funderEcosystemId} splits: ${result + .map((p) => JSON.stringify(p)) + .join(`, `)} + `, + ); +} + +async function clearCurrentProjectSplits( + funderEcosystemId: string, + transaction: Transaction, +) { + await AddressDriverSplitReceiverModel.destroy({ + where: { + funderEcosystemId, + }, + transaction, + }); + await RepoDriverSplitReceiverModel.destroy({ + where: { + funderEcosystemId, + }, + transaction, + }); + await DripListSplitReceiverModel.destroy({ + where: { + funderEcosystemId, + }, + transaction, + }); + await SubListSplitReceiverModel.destroy({ + where: { + funderEcosystemId, + }, + transaction, + }); +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts b/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts index 0f3750b..25e15f0 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts @@ -1,9 +1,13 @@ import type { SplitsReceiverStruct } from '../../../contracts/CURRENT_NETWORK/Drips'; -import type { RepoDriverId, NftDriverId } from '../../core/types'; +import type { + RepoDriverId, + NftDriverId, + ImmutableSplitsDriverId, +} from '../../core/types'; import { dripsContract } from '../../core/contractClients'; export default async function validateSplitsReceivers( - accountId: RepoDriverId | NftDriverId, + accountId: RepoDriverId | NftDriverId | ImmutableSplitsDriverId, splits: SplitsReceiverStruct[], ): Promise< [ diff --git a/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts index 524452d..1a6a35d 100644 --- a/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts @@ -4,8 +4,10 @@ import { AddressDriverSplitReceiverModel, DripListModel, DripListSplitReceiverModel, + EcosystemModel, GitProjectModel, RepoDriverSplitReceiverModel, + SubListSplitReceiverModel, } from '../../models'; import { isNftDriverId, isRepoDriverId } from '../../utils/accountIdUtils'; import type { SplitsReceiverStruct } from '../../../contracts/CURRENT_NETWORK/Drips'; @@ -73,35 +75,47 @@ export default async function setIsValidFlag( await project.save(); } } else if (isNftDriverId(accountId)) { - const dripList = await DripListModel.findByPk(accountId, { - lock: true, - }); - - if (!dripList) { + const [dripList, ecosystem] = await Promise.all([ + DripListModel.findByPk(accountId, { + lock: true, + }), + EcosystemModel.findByPk(accountId, { + lock: true, + }), + ]); + + if (!dripList && !ecosystem) { throw new Error( - `Failed to set 'isValid' flag for Drip List with ID '${accountId}': Drip List not found. + `Failed to set 'isValid' flag for account with ID '${accountId}': Account not found. \r Possible reasons: - \r\t - The event that should have created the Drip List was not processed yet. - \r\t - The event was emitted as a result of a manual 'SetSplits' transaction that for a Drip List that does not exist in the app.`, + \r\t - The event that should have created the Account was not processed yet. + \r\t - The event was emitted as a result of a manual 'SetSplits' transaction that for an Account that does not exist in the app.`, ); } + const entity = (dripList ?? ecosystem)!; + const entityModel = dripList ? DripListModel : EcosystemModel; + const storedInDbFromMetaReceiversHash = await dripsContract.hashSplits( - formatSplitReceivers(await getDripListDbReceivers(accountId)), + formatSplitReceivers( + entityModel.name === 'DripListModel' + ? await getDripListDbReceivers(accountId) + : await getEcosystemDbReceivers(accountId), + ), ); // If we reach this point, it means that `receiversHash` is the latest on-chain hash. if (receiversHash !== storedInDbFromMetaReceiversHash) { - dripList.isValid = false; + entity.isValid = false; - logManager.appendUpdateLog(dripList, DripListModel, dripList.id); + logManager.appendUpdateLog(entity, entityModel, entity.id); - await dripList.save(); + await entity.save(); // We need to throw so that the job is retried... throw new Error( - `Splits receivers hashes do not match for Drip List with ID '${accountId}': + `Splits receivers hashes do not match for ${entityModel.name} with ID '${accountId}': \r\t - On-chain splits hash: ${onChainSplitsHash} \r\t - 'SetSplits' event splits hash: ${receiversHash} \r\t - DB (populated from metadata) splits hash: ${storedInDbFromMetaReceiversHash} @@ -109,16 +123,16 @@ export default async function setIsValidFlag( \r\t - The 'AccountMetadataEmitted' event that should have created the Drip List splits was not processed yet. \r\t - The 'SetSplits' event (was manually emitted?) had splits that do not match what's already stored in the DB (from metadata).`, ); - } else if (dripList.isValid === false) { - dripList.isValid = true; + } else if (entity.isValid === false) { + entity.isValid = true; - logManager.appendUpdateLog(dripList, DripListModel, dripList.id); + logManager.appendUpdateLog(entity, entityModel, entity.id); - await dripList.save(); + await entity.save(); } } else { logManager.appendLog( - `Skipping 'isValid' flag update for account with ID '${accountId}' because it's not a Project or a Drip List.`, + `Skipping 'isValid' flag update for account with ID '${accountId}' because it's not a Project, Drip List, or Ecosystem.`, ); } } @@ -155,7 +169,6 @@ async function getProjectDbReceivers(accountId: RepoDriverId) { const dripListReceivers: SplitsReceiverStruct[] = await DripListSplitReceiverModel.findAll({ lock: true, - where: { funderProjectId: accountId, }, @@ -214,3 +227,34 @@ async function getDripListDbReceivers(accountId: NftDriverId) { return [...addressReceivers, ...projectReceivers, ...dripListReceivers]; } + +async function getEcosystemDbReceivers(accountId: NftDriverId) { + const projectReceivers: SplitsReceiverStruct[] = + await RepoDriverSplitReceiverModel.findAll({ + lock: true, + + where: { + funderEcosystemId: accountId, + }, + }).then((receivers) => + receivers.map((receiver) => ({ + accountId: receiver.fundeeProjectId ?? unreachableError(), + weight: receiver.weight, + })), + ); + + const subListReceivers: SplitsReceiverStruct[] = + await SubListSplitReceiverModel.findAll({ + lock: true, + where: { + funderEcosystemId: accountId, + }, + }).then((receivers) => + receivers.map((receiver) => ({ + accountId: receiver.fundeeImmutableSplitsId ?? unreachableError(), + weight: receiver.weight, + })), + ); + + return [...projectReceivers, ...subListReceivers]; +} diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index bd4e4c5..03aceb8 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -4,7 +4,7 @@ import EventHandlerBase from '../events/EventHandlerBase'; import LogManager from '../core/LogManager'; import { calcAccountId, toNftDriverId } from '../utils/accountIdUtils'; import type EventHandlerRequest from '../events/EventHandlerRequest'; -import { DripListModel, TransferEventModel } from '../models'; +import { DripListModel, EcosystemModel, TransferEventModel } from '../models'; import { dbConnection } from '../db/database'; import { isLatestEvent } from '../utils/eventUtils'; import appSettings from '../config/appSettings'; @@ -41,10 +41,7 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add await TransferEventModel.findOrCreate({ lock: true, transaction, - where: { - logIndex, - transactionHash, - }, + where: { logIndex, transactionHash }, defaults: { tokenId: id, to, @@ -62,55 +59,48 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add `${transferEvent.transactionHash}-${transferEvent.logIndex}`, ); - const { visibilityThresholdBlockNumber } = appSettings; - - const dripList = await DripListModel.findOne({ + const isLatest = await isLatestEvent( + transferEvent, + TransferEventModel, + { transactionHash, logIndex, tokenId }, transaction, - lock: true, - where: { - id, - }, - }); - - if (!dripList) { - throw new Error(`Drip List with tokenId ${id} does not exist.`); - } + ); - // Here, the Drip List already exists. - // Only if the event is the latest (in the DB), we process its data. - // After all events are processed, the Drip List will be updated with the latest values. - if ( - !(await isLatestEvent( - transferEvent, - TransferEventModel, - { - transactionHash, - logIndex, - tokenId, - }, - transaction, - )) - ) { + if (!isLatest) { logManager.logAllInfo(); return; } - dripList.ownerAddress = to; - dripList.previousOwnerAddress = from; - dripList.ownerAccountId = await calcAccountId(to); - dripList.creator = to; // TODO: https://github.com/drips-network/events-processor/issues/14 - dripList.isVisible = - blockNumber > visibilityThresholdBlockNumber - ? from === ZeroAddress // If it's a mint, then the Drip List will be visible. If it's a real transfer, then it's not. - : true; // If the block number is less than the visibility threshold, then the Drip List is visible by default. - dripList.isValid = false; // The Drip List is not valid until the metadata is processed. + const [dripList, ecosystem] = await Promise.all([ + DripListModel.findOne({ transaction, lock: true, where: { id } }), + EcosystemModel.findOne({ transaction, lock: true, where: { id } }), + ]); + + if (!dripList && !ecosystem) { + throw new Error( + `Drip List or Ecosystem not found for tokenId '${tokenId}'. Maybe the 'AccountMetadataEmitted' event that should have created the entity was not processed yet?`, + ); + } + + const entity = (dripList ?? ecosystem)!; + const entityModel = dripList ? DripListModel : EcosystemModel; + + entity.ownerAddress = to; + entity.previousOwnerAddress = from; + entity.ownerAccountId = await calcAccountId(to); + entity.creator = to; // TODO: https://github.com/drips-network/events-processor/issues/14 + entity.isVisible = + blockNumber > appSettings.visibilityThresholdBlockNumber + ? from === ZeroAddress + : true; + entity.isValid = true; logManager .appendIsLatestEventLog() - .appendUpdateLog(dripList, DripListModel, dripList.id); + .appendUpdateLog(entity, entityModel, entity.id); - await dripList.save({ transaction }); + await entity.save({ transaction }); logManager.logAllInfo(); }); diff --git a/src/metadata/schemas/nft-driver/v6.ts b/src/metadata/schemas/nft-driver/v6.ts index 2cb1654..780fbf6 100644 --- a/src/metadata/schemas/nft-driver/v6.ts +++ b/src/metadata/schemas/nft-driver/v6.ts @@ -7,14 +7,19 @@ import { import { subListSplitReceiverSchema } from '../sub-list/v1'; import { dripListSplitReceiverSchema } from './v2'; -const ecosystemVariant = nftDriverAccountMetadataSchemaV5.extend({ +const base = nftDriverAccountMetadataSchemaV5.extend({ + isDripList: z.undefined().optional(), + projects: z.undefined().optional(), +}); + +const ecosystemVariant = base.extend({ type: z.literal('ecosystem'), recipients: z.array( z.union([repoDriverSplitReceiverSchema, subListSplitReceiverSchema]), ), }); -const dripListVariant = nftDriverAccountMetadataSchemaV5.extend({ +const dripListVariant = base.extend({ type: z.literal('dripList'), recipients: z.array( z.union([ diff --git a/src/models/AddressDriverSplitReceiverModel.ts b/src/models/AddressDriverSplitReceiverModel.ts index f6f7ef1..82b4ce1 100644 --- a/src/models/AddressDriverSplitReceiverModel.ts +++ b/src/models/AddressDriverSplitReceiverModel.ts @@ -10,11 +10,13 @@ import getSchema from '../utils/getSchema'; import GitProjectModel from './GitProjectModel'; import type { AddressDriverId, NftDriverId, RepoDriverId } from '../core/types'; import DripListModel from './DripListModel'; +import EcosystemModel from './EcosystemModel'; export enum AddressDriverSplitReceiverType { ProjectMaintainer = 'ProjectMaintainer', ProjectDependency = 'ProjectDependency', DripListDependency = 'DripListDependency', + EcosystemDependency = 'EcosystemDependency', } export default class AddressDriverSplitReceiverModel extends Model< @@ -24,6 +26,7 @@ export default class AddressDriverSplitReceiverModel extends Model< public declare id: CreationOptional; // Primary key public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key + public declare funderEcosystemId: NftDriverId | null; // Foreign key public declare weight: number; public declare type: AddressDriverSplitReceiverType; @@ -65,6 +68,15 @@ export default class AddressDriverSplitReceiverModel extends Model< }, allowNull: true, }, + funderEcosystemId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: EcosystemModel, + key: 'id', + }, + allowNull: true, + }, weight: { type: DataTypes.INTEGER, allowNull: true, @@ -98,6 +110,14 @@ export default class AddressDriverSplitReceiverModel extends Model< }, unique: false, }, + { + fields: ['funderEcosystemId'], + name: `IX_AddressDriverSplitReceivers_funderEcosystemId`, + where: { + type: AddressDriverSplitReceiverType.EcosystemDependency, + }, + unique: false, + }, ], }, ); diff --git a/src/models/DripListSplitReceiverModel.ts b/src/models/DripListSplitReceiverModel.ts index 75754e3..6141584 100644 --- a/src/models/DripListSplitReceiverModel.ts +++ b/src/models/DripListSplitReceiverModel.ts @@ -10,6 +10,7 @@ import { DependencyType } from '../core/types'; import type { NftDriverId, RepoDriverId } from '../core/types'; import DripListModel from './DripListModel'; import GitProjectModel from './GitProjectModel'; +import EcosystemModel from './EcosystemModel'; export default class DripListSplitReceiverModel extends Model< InferAttributes, @@ -19,6 +20,7 @@ export default class DripListSplitReceiverModel extends Model< public declare fundeeDripListId: NftDriverId; // Foreign key public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key + public declare funderEcosystemId: NftDriverId | null; // Foreign key public declare weight: number; public declare type: DependencyType; @@ -59,6 +61,15 @@ export default class DripListSplitReceiverModel extends Model< }, allowNull: true, }, + funderEcosystemId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: EcosystemModel, + key: 'id', + }, + allowNull: true, + }, weight: { type: DataTypes.INTEGER, allowNull: true, @@ -98,6 +109,14 @@ export default class DripListSplitReceiverModel extends Model< }, unique: false, }, + { + fields: ['funderEcosystemId'], + name: `IX_DripListSplitReceivers_funderEcosystemId`, + where: { + type: DependencyType.EcosystemDependency, + }, + unique: false, + }, ], }, ); diff --git a/src/models/EcosystemModel.ts b/src/models/EcosystemModel.ts index c1ea621..8cd8988 100644 --- a/src/models/EcosystemModel.ts +++ b/src/models/EcosystemModel.ts @@ -5,7 +5,6 @@ import type { } from 'sequelize'; import { DataTypes, Model } from 'sequelize'; import type { AddressLike } from 'ethers'; -import type { UUID } from 'crypto'; import type { AccountId, NftDriverId } from '../core/types'; import getSchema from '../utils/getSchema'; @@ -16,12 +15,11 @@ export default class EcosystemModel extends Model< public declare id: NftDriverId; public declare isValid: boolean; public declare name: string | null; - public declare creator: AddressLike; + public declare creator: AddressLike | null; public declare description: string | null; - public declare ownerAddress: AddressLike; - public declare ownerAccountId: AccountId; - public declare previousOwnerAddress: AddressLike; - public declare latestVotingRoundId: UUID | null; + public declare ownerAddress: AddressLike | null; + public declare ownerAccountId: AccountId | null; + public declare previousOwnerAddress: AddressLike | null; public declare isVisible: boolean; public declare lastProcessedIpfsHash: string | null; @@ -38,35 +36,31 @@ export default class EcosystemModel extends Model< }, ownerAddress: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, ownerAccountId: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, name: { type: DataTypes.STRING, allowNull: true, }, - latestVotingRoundId: { - type: DataTypes.UUID, - allowNull: true, - }, description: { type: DataTypes.TEXT, allowNull: true, }, creator: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, previousOwnerAddress: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, }, isVisible: { type: DataTypes.BOOLEAN, - allowNull: false, + allowNull: true, }, lastProcessedIpfsHash: { type: DataTypes.TEXT, diff --git a/src/models/RepoDriverSplitReceiverModel.ts b/src/models/RepoDriverSplitReceiverModel.ts index b59d8b3..fbe4da9 100644 --- a/src/models/RepoDriverSplitReceiverModel.ts +++ b/src/models/RepoDriverSplitReceiverModel.ts @@ -10,6 +10,7 @@ import GitProjectModel from './GitProjectModel'; import { DependencyType } from '../core/types'; import type { NftDriverId, RepoDriverId } from '../core/types'; import DripListModel from './DripListModel'; +import EcosystemModel from './EcosystemModel'; export default class RepoDriverSplitReceiverModel extends Model< InferAttributes, @@ -19,6 +20,7 @@ export default class RepoDriverSplitReceiverModel extends Model< public declare fundeeProjectId: RepoDriverId; // Foreign key public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key + public declare funderEcosystemId: NftDriverId | null; // Foreign key public declare weight: number; public declare type: DependencyType; @@ -59,6 +61,15 @@ export default class RepoDriverSplitReceiverModel extends Model< }, allowNull: true, }, + funderEcosystemId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: EcosystemModel, + key: 'id', + }, + allowNull: true, + }, weight: { type: DataTypes.INTEGER, allowNull: true, @@ -98,6 +109,14 @@ export default class RepoDriverSplitReceiverModel extends Model< }, unique: false, }, + { + fields: ['funderEcosystemId'], + name: `IX_RepoDriverSplitReceivers_funderEcosystemId`, + where: { + type: DependencyType.EcosystemDependency, + }, + unique: false, + }, ], }, ); diff --git a/src/models/SubListModel.ts b/src/models/SubListModel.ts index f562d36..f6cbb96 100644 --- a/src/models/SubListModel.ts +++ b/src/models/SubListModel.ts @@ -15,9 +15,7 @@ export default class SubListModel extends Model< public declare id: ImmutableSplitsDriverId; // Populated from `AccountMetadataEmitted` event: - public declare name: string | null; - public declare description: string | null; - public declare ecosystemId: NftDriverId | null; + public declare parentAccountId: NftDriverId | null; public declare lastProcessedIpfsHash: string | null; public static initialize(sequelize: Sequelize): void { @@ -27,7 +25,7 @@ export default class SubListModel extends Model< type: DataTypes.STRING, primaryKey: true, }, - ecosystemId: { + parentAccountId: { // Foreign key type: DataTypes.STRING, allowNull: true, @@ -36,14 +34,6 @@ export default class SubListModel extends Model< key: 'id', }, }, - name: { - type: DataTypes.STRING, - allowNull: true, - }, - description: { - type: DataTypes.TEXT, - allowNull: true, - }, lastProcessedIpfsHash: { type: DataTypes.TEXT, allowNull: true, @@ -55,8 +45,8 @@ export default class SubListModel extends Model< tableName: 'SubLists', indexes: [ { - fields: ['ecosystemId'], - name: `IX_SubLists_ecosystemId`, + fields: ['parentAccountId'], + name: `IX_SubLists_parentAccountId`, unique: false, }, ], diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index af9657a..6e88f57 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -45,8 +45,8 @@ export function isNftDriverId(id: string): id is NftDriverId { return true; } -export function toNftDriverId(id: bigint): NftDriverId { - const nftDriverId = id.toString(); +export function toNftDriverId(id: bigint | string): NftDriverId { + const nftDriverId = typeof id === 'bigint' ? id.toString() : id; if (!isNftDriverId(nftDriverId)) { throw new Error(`Invalid 'NftDriver' account ID: ${id}.`); diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 7183609..544566f 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -1,7 +1,8 @@ import type { UUID } from 'crypto'; import type { Transaction } from 'sequelize'; -import type { Dependency, DependencyOfProjectType } from '../core/types'; +import type { DependencyOfProjectType } from '../core/types'; import type { EventSignature } from '../events/types'; +import { repoDriverSplitReceiverSchema } from '../metadata/schemas/repo-driver/v2'; export function assertTransaction( transaction: Transaction | null | undefined, @@ -31,18 +32,14 @@ export function assertEventSignature( } } -export function isDependencyOfProjectType( - dependency: Dependency, -): dependency is DependencyOfProjectType { - return 'source' in dependency; -} - export function assertDependencyOfProjectType( - project: Dependency, + project: any, ): asserts project is DependencyOfProjectType { - if (!isDependencyOfProjectType(project)) { + const result = repoDriverSplitReceiverSchema.safeParse(project); + + if (!result.success) { throw new Error( - `Dependency with account ID ${project.accountId} is not a valid DependencyOfProjectType.`, + `Invalid project dependency: ${JSON.stringify(result.error.format())}`, ); } } diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts index 6a112b0..d3cef57 100644 --- a/src/utils/metadataUtils.ts +++ b/src/utils/metadataUtils.ts @@ -2,6 +2,7 @@ import { ethers } from 'ethers'; import type { AnyVersion } from '@efstajas/versioned-parser'; import type { IpfsHash } from '../core/types'; import { + immutableSplitsDriverMetadataParser, nftDriverAccountMetadataParser, repoDriverAccountMetadataParser, } from '../metadata/schemas'; @@ -32,7 +33,7 @@ async function getIpfsFile(hash: IpfsHash): Promise { return fetch(`${appSettings.ipfsGatewayUrl}/ipfs/${hash}`); } -export default async function getNftDriverMetadata( +export async function getNftDriverMetadata( ipfsHash: IpfsHash, ): Promise> { const ipfsFile = await (await getIpfsFile(ipfsHash)).json(); @@ -40,3 +41,12 @@ export default async function getNftDriverMetadata( return metadata; } + +export async function getImmutableSpitsDriverMetadata( + ipfsHash: IpfsHash, +): Promise> { + const ipfsFile = await (await getIpfsFile(ipfsHash)).json(); + const metadata = immutableSplitsDriverMetadataParser.parseAny(ipfsFile); + + return metadata; +} From c50d4bfa9b9bf4970e4a6b70aaf73e53e75d99ef Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 3 Apr 2025 15:57:47 +0200 Subject: [PATCH 09/60] refactor: restructure the metadata indexing logic and introduce handlers for sub list and ecosystem metadata --- .gitignore | 2 +- package.json | 3 +- scripts/run-migrations.ts | 21 +- src/config/appSettings.schema.ts | 2 +- src/config/appSettings.ts | 4 +- src/core/types.ts | 13 +- src/db/database.ts | 224 ++++++++-- ...ystems_and_created_splits_events_tables.ts | 4 + ...ate_sublists_and_split_receivers_tables.ts | 82 +++- ..._funder_ecosystem_id_to_split_receivers.ts | 75 ++++ src/db/modelRegistration.ts | 2 - .../AccountMetadataEmittedEventHandler.ts | 72 ++-- .../createDbEntriesForProjectDependency.ts | 71 ---- .../dripList/handleDripListMetadata.ts | 308 -------------- .../gitProject/validateProjectMetadata.ts | 47 --- .../handleEcosystemMetadata.ts | 188 --------- .../handleSubListMetadata.ts | 232 ----------- .../handlers/handleDripListMetadata.ts | 270 ++++++++++++ .../handlers/handleEcosystemMetadata.ts | 224 ++++++++++ .../handleProjectMetadata.ts} | 210 +++++----- .../handlers/handleSubListMetadata.ts | 228 +++++++++++ .../projectVerification.ts | 79 ++++ .../receiversRepository.ts | 385 ++++++++++++++++++ .../splitsValidator.ts | 61 --- .../verifySplitsReceivers.ts | 29 ++ .../CreatedSplitsEventHandler.ts | 90 ---- .../SplitsSetEventHandler/setIsValidFlag.ts | 6 +- src/eventHandlers/index.ts | 1 - src/events/registrations.ts | 5 - src/index.ts | 8 +- src/metadata/schemas/sub-list/v1.ts | 16 +- src/models/AddressDriverSplitReceiverModel.ts | 22 +- src/models/CreatedSplitsEventModel.ts | 48 --- src/models/DripListModel.ts | 2 +- src/models/DripListSplitReceiverModel.ts | 17 +- src/models/RepoDriverSplitReceiverModel.ts | 17 +- src/models/SubListModel.ts | 71 +++- src/models/SubListSplitReceiverModel.ts | 22 +- src/models/index.ts | 1 - src/utils/accountIdUtils.ts | 10 +- src/utils/assert.ts | 14 - src/utils/formatSplitReceivers.ts | 34 ++ ...AccountMetadataEmittedEventHandler.test.ts | 4 +- .../CreatedSplitsHandler.test.ts | 103 ----- 44 files changed, 1941 insertions(+), 1386 deletions(-) delete mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts delete mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts delete mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/gitProject/validateProjectMetadata.ts delete mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts delete mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts create mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts create mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMetadata.ts rename src/eventHandlers/AccountMetadataEmittedEvent/{gitProject/handleGitProjectMetadata.ts => handlers/handleProjectMetadata.ts} (53%) create mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts create mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts create mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts delete mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts create mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts delete mode 100644 src/eventHandlers/CreatedSplitsEventHandler.ts delete mode 100644 src/models/CreatedSplitsEventModel.ts create mode 100644 src/utils/formatSplitReceivers.ts delete mode 100644 tests/eventHandlers/CreatedSplitsHandler.test.ts diff --git a/.gitignore b/.gitignore index 43f6a48..4fa7e32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # vscode -.vscode/settings.json +.vscode/* # Generated code contracts diff --git a/package.json b/package.json index a917879..bf4788a 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,14 @@ "test": "jest --detectOpenHandles --config=jest.config.ts", "test:coverage": "jest --coverage", "build": "npm run build:contracts && tsc", + "clean": "rm -rf dist", "build:contracts": "./scripts/build-contracts.sh && ts-node ./scripts/codegen-any-chain-types.ts", "postbuild": "rm -rf ./dist/src/config/chainConfigs && cp -r ./src/config/chainConfigs ./dist/src/config/chainConfigs", "check": "tsc --noEmit", "dev": "npx nodemon", "start": "node dist/src/index.js", "sequelize": "ts-node --transpile-only node_modules/sequelize-cli/lib/sequelize", - "db:run-migrations": "node dist/scripts/run-migrations.js", + "db:run-migrations": "npm run clean && npm run build && ts-node scripts/run-migrations.ts", "db:create-migration": "npm run sequelize migration:generate -- --migrations-path src/db/migrations", "dev:db:revert-migration": "npm run sequelize -- db:migrate:undo --migrations-path src/db/migrations --config dist/src/config/sequelizeConfig.js", "dev:db:log-pending-migrations": "npm run sequelize -- db:migrate:status --migrations-path src/db/migrations --config dist/src/config/sequelizeConfig.js" diff --git a/scripts/run-migrations.ts b/scripts/run-migrations.ts index f4fe7a1..d5b72e4 100644 --- a/scripts/run-migrations.ts +++ b/scripts/run-migrations.ts @@ -1,6 +1,7 @@ import { Sequelize } from 'sequelize'; import { Umzug, SequelizeStorage } from 'umzug'; import getSchema from '../src/utils/getSchema'; +import logger from '../src/core/logger'; export async function runMigrations(): Promise { const sequelize = new Sequelize( @@ -14,7 +15,17 @@ export async function runMigrations(): Promise { const schema = getSchema(); - // Ensure schema exists before running migrations + await sequelize.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT FROM pg_database WHERE datname = 'dripsdb' + ) THEN + CREATE DATABASE "dripsdb"; + END IF; + END + $$; + `); await sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`); const migrator = new Umzug({ @@ -31,9 +42,11 @@ export async function runMigrations(): Promise { if (migrations.length > 0) { const appliedNames = migrations.map((m) => m.name).join(', '); - console.log(`Applied migrations: ${appliedNames}`); + logger.info( + `✅ Applied ${migrations.length} migration${migrations.length > 1 ? 's' : ''}:\n - ${appliedNames.split(', ').join('\n - ')}`, + ); } else { - console.log( + logger.info( 'No migrations were applied. The database is already up-to-date. If you expected migrations to be applied, ensure that you run "npm run build" before starting the server.', ); } @@ -43,7 +56,7 @@ export async function runMigrations(): Promise { } runMigrations().catch((error) => { - console.error('Error running migrations:', error); + logger.info('Error running migrations:', error); throw error; }); diff --git a/src/config/appSettings.schema.ts b/src/config/appSettings.schema.ts index b0b1ca6..544866d 100644 --- a/src/config/appSettings.schema.ts +++ b/src/config/appSettings.schema.ts @@ -17,7 +17,7 @@ export const appSettingsSchema = z.object({ logger: loggingConfigSchema, pollingInterval: z.number().positive().optional().default(5000), chunkSize: z.number().positive().optional().default(1000), - confirmations: z.number().positive().optional().default(1), + confirmations: z.number().optional().default(1), ipfsGatewayUrl: z .string() .url() diff --git a/src/config/appSettings.ts b/src/config/appSettings.ts index 36836c9..8749eb6 100644 --- a/src/config/appSettings.ts +++ b/src/config/appSettings.ts @@ -28,7 +28,9 @@ function loadAppSettings(): AppSettings { ? parseInt(process.env.CONFIRMATIONS, 10) : undefined, ipfsGatewayUrl: process.env.IPFS_GATEWAY_URL, - monitoringUiPort: process.env.MONITORING_UI_PORT, + queueUiPort: process.env.MONITORING_UI_PORT + ? parseInt(process.env.MONITORING_UI_PORT, 10) + : undefined, redisConnectionString: process.env.REDIS_CONNECTION_STRING, postgresConnectionString: process.env.POSTGRES_CONNECTION_STRING, shouldStartMonitoringUI: diff --git a/src/core/types.ts b/src/core/types.ts index a306a3b..f143b69 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -62,18 +62,7 @@ export type ModelStaticMembers = { initialize(sequelize: Sequelize): void; }; -export type DependencyOfProjectType = { - type: 'repoDriver'; - accountId: RepoDriverId; - source: { - forge: 'github'; - repoName: string; - ownerName: string; - url: string; - }; - weight: number; -}; - +// TODO: Remove this. There is no need to have this in the database. export enum DependencyType { ProjectDependency = 'ProjectDependency', DripListDependency = 'DripListDependency', diff --git a/src/db/database.ts b/src/db/database.ts index 9b6212f..d79d22e 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -28,7 +28,11 @@ export async function connectToDb(): Promise { registerModels(); await initializeEntities(); - defineAssociations(); + + defineProjectAssociations(); + defineDripListAssociations(); + defineEcosystemsAssociations(); + defineSubListAssociations(); logger.info('Connected to the database.'); } catch (error) { @@ -46,8 +50,8 @@ async function initializeEntities(): Promise { logger.info('Database schema initialized.'); } -function defineAssociations() { - // One-to-Many: A project can fund multiple address splits. +function defineProjectAssociations() { + // One-to-Many: A project can fund multiple addresses. GitProjectModel.hasMany(AddressDriverSplitReceiverModel, { foreignKey: 'funderProjectId', }); @@ -55,7 +59,7 @@ function defineAssociations() { foreignKey: 'funderProjectId', }); - // One-to-Many: A project can fund multiple project splits. + // One-to-Many: A project can fund multiple projects. GitProjectModel.hasMany(RepoDriverSplitReceiverModel, { foreignKey: 'funderProjectId', }); @@ -63,7 +67,7 @@ function defineAssociations() { foreignKey: 'funderProjectId', }); - // One-to-Many: A project can fund multiple drip list splits. + // One-to-Many: A project can fund multiple Drip Lists. GitProjectModel.hasMany(DripListSplitReceiverModel, { foreignKey: 'funderProjectId', }); @@ -71,15 +75,25 @@ function defineAssociations() { foreignKey: 'funderProjectId', }); - // One-to-One: A RepoDriverSplitReceiver represents/is a project. + // One-to-Many: A project can fund multiple Sub Lists. + GitProjectModel.hasMany(SubListSplitReceiverModel, { + foreignKey: 'funderSubListId', + }); + SubListSplitReceiverModel.belongsTo(GitProjectModel, { + foreignKey: 'funderSubListId', + }); + + // One-to-One: A project receiver represents/is a project. GitProjectModel.hasOne(RepoDriverSplitReceiverModel, { foreignKey: 'fundeeProjectId', }); RepoDriverSplitReceiverModel.belongsTo(GitProjectModel, { foreignKey: 'fundeeProjectId', }); +} - // One-to-Many: A drip list can fund multiple address splits. +function defineDripListAssociations() { + // One-to-Many: A Drip List can fund multiple addresses. DripListModel.hasMany(AddressDriverSplitReceiverModel, { foreignKey: 'funderDripListId', }); @@ -87,7 +101,7 @@ function defineAssociations() { foreignKey: 'funderDripListId', }); - // One-to-Many: A drip list can fund multiple project splits. + // One-to-Many: A Drip List can fund multiple projects. DripListModel.hasMany(RepoDriverSplitReceiverModel, { foreignKey: 'funderDripListId', }); @@ -95,7 +109,7 @@ function defineAssociations() { foreignKey: 'funderDripListId', }); - // One-to-Many: A drip list can fund multiple drip list splits. + // One-to-Many: A Drip List can fund multiple Drip Lists. DripListModel.hasMany(DripListSplitReceiverModel, { foreignKey: 'funderDripListId', }); @@ -103,7 +117,7 @@ function defineAssociations() { foreignKey: 'funderDripListId', }); - // One-to-Many: A drip list can fund multiple sub list splits. + // One-to-Many: A Drip List can fund multiple Sub Lists. DripListModel.hasMany(SubListSplitReceiverModel, { foreignKey: 'funderDripListId', }); @@ -111,23 +125,25 @@ function defineAssociations() { foreignKey: 'funderDripListId', }); - // One-to-Many: An Ecosystem can have multiple SubLists. - EcosystemModel.hasMany(SubListModel, { - foreignKey: 'ecosystemId', + // One-to-One: A Drip List receiver represents/is a Drip List. + DripListModel.hasOne(DripListSplitReceiverModel, { + foreignKey: 'fundeeDripListId', }); - SubListModel.belongsTo(EcosystemModel, { - foreignKey: 'ecosystemId', + DripListSplitReceiverModel.belongsTo(DripListModel, { + foreignKey: 'fundeeDripListId', }); +} - // One-to-Many: An Ecosystem can fund multiple sub list splits. - EcosystemModel.hasMany(SubListSplitReceiverModel, { +function defineEcosystemsAssociations() { + // One-to-Many: An Ecosystem can fund multiple addresses. + EcosystemModel.hasMany(AddressDriverSplitReceiverModel, { foreignKey: 'funderEcosystemId', }); - SubListSplitReceiverModel.belongsTo(EcosystemModel, { + AddressDriverSplitReceiverModel.belongsTo(EcosystemModel, { foreignKey: 'funderEcosystemId', }); - // One-to-Many: An Ecosystem can fund multiple project splits. + // One-to-Many: An Ecosystem can fund multiple projects. EcosystemModel.hasMany(RepoDriverSplitReceiverModel, { foreignKey: 'funderEcosystemId', }); @@ -135,19 +151,171 @@ function defineAssociations() { foreignKey: 'funderEcosystemId', }); - // One-to-One: A DripListSplitReceiverModel represents/is a drip list. - DripListModel.hasOne(DripListSplitReceiverModel, { - foreignKey: 'fundeeDripListId', + // One-to-Many: An Ecosystem can fund multiple Drip Lists. + EcosystemModel.hasMany(DripListSplitReceiverModel, { + foreignKey: 'funderEcosystemId', }); - DripListSplitReceiverModel.belongsTo(DripListModel, { - foreignKey: 'fundeeDripListId', + DripListSplitReceiverModel.belongsTo(EcosystemModel, { + foreignKey: 'funderEcosystemId', + }); + + // One-to-Many: An Ecosystem can fund multiple Sub Lists. + EcosystemModel.hasMany(SubListSplitReceiverModel, { + foreignKey: 'funderEcosystemId', + }); + SubListSplitReceiverModel.belongsTo(EcosystemModel, { + foreignKey: 'funderEcosystemId', + }); +} + +function defineSubListAssociations() { + // One-to-Many: A parent Sub List can fund multiple addresses. + SubListModel.hasMany(AddressDriverSplitReceiverModel, { + foreignKey: 'parentSubListId', + }); + AddressDriverSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentSubListId', + }); + + // One-to-Many: A parent Sub List can fund multiple projects. + SubListModel.hasMany(RepoDriverSplitReceiverModel, { + foreignKey: 'parentSubListId', + }); + RepoDriverSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentSubListId', + }); + + // One-to-Many: A parent Sub List can fund multiple Drip Lists. + SubListModel.hasMany(DripListSplitReceiverModel, { + foreignKey: 'parentSubListId', + }); + DripListSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentSubListId', + }); + + // One-to-Many: A parent Sub List can fund multiple Sub Lists. + SubListModel.hasMany(SubListSplitReceiverModel, { + foreignKey: 'parentSubListId', + }); + SubListSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentSubListId', + }); + + // One-to-Many: A parent Drip List can fund multiple addresses. + SubListModel.hasMany(AddressDriverSplitReceiverModel, { + foreignKey: 'parentDripListId', + }); + AddressDriverSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentDripListId', + }); + + // One-to-Many: A parent Drip List can fund multiple projects. + SubListModel.hasMany(RepoDriverSplitReceiverModel, { + foreignKey: 'parentDripListId', + }); + RepoDriverSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentDripListId', + }); + + // One-to-Many: A parent Drip List can fund multiple Drip Lists. + SubListModel.hasMany(DripListSplitReceiverModel, { + foreignKey: 'parentDripListId', + }); + DripListSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentDripListId', + }); + + // One-to-Many: A root Drip List can fund multiple addresses. + SubListModel.hasMany(AddressDriverSplitReceiverModel, { + foreignKey: 'rootDripListId', + }); + AddressDriverSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'rootDripListId', + }); + + // One-to-Many: A root Drip List can fund multiple projects. + SubListModel.hasMany(RepoDriverSplitReceiverModel, { + foreignKey: 'rootDripListId', + }); + RepoDriverSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'rootDripListId', + }); + + // One-to-Many: A root Drip List can fund multiple Drip Lists. + SubListModel.hasMany(DripListSplitReceiverModel, { + foreignKey: 'rootDripListId', + }); + DripListSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'rootDripListId', + }); + + // One-to-Many: A parent Ecosystem can fund multiple addresses. + SubListModel.hasMany(AddressDriverSplitReceiverModel, { + foreignKey: 'parentEcosystemId', + }); + AddressDriverSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentEcosystemId', + }); + + // One-to-Many: A parent Ecosystem can fund multiple projects. + SubListModel.hasMany(RepoDriverSplitReceiverModel, { + foreignKey: 'parentEcosystemId', + }); + RepoDriverSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentEcosystemId', + }); + + // One-to-Many: A parent Ecosystem can fund multiple Drip Lists. + SubListModel.hasMany(DripListSplitReceiverModel, { + foreignKey: 'parentEcosystemId', + }); + DripListSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentEcosystemId', + }); + // One-to-Many: A parent Ecosystem can fund multiple Sub Lists. + SubListModel.hasMany(SubListSplitReceiverModel, { + foreignKey: 'parentEcosystemId', + }); + SubListSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'parentEcosystemId', + }); + + // One-to-Many: A root Ecosystem can fund multiple addresses. + SubListModel.hasMany(AddressDriverSplitReceiverModel, { + foreignKey: 'rootEcosystemId', + }); + AddressDriverSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'rootEcosystemId', + }); + + // One-to-Many: A root Ecosystem can fund multiple projects. + SubListModel.hasMany(RepoDriverSplitReceiverModel, { + foreignKey: 'rootEcosystemId', + }); + RepoDriverSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'rootEcosystemId', + }); + + // One-to-Many: A root Ecosystem can fund multiple Drip Lists. + SubListModel.hasMany(DripListSplitReceiverModel, { + foreignKey: 'rootEcosystemId', + }); + DripListSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'rootEcosystemId', + }); + // One-to-Many: A root Ecosystem can fund multiple Sub Lists. + SubListModel.hasMany(SubListSplitReceiverModel, { + foreignKey: 'rootEcosystemId', + }); + SubListSplitReceiverModel.belongsTo(SubListModel, { + foreignKey: 'rootEcosystemId', }); - // One-to-One: A SubListSplitReceiverModel represents/is a sub list. - SubListModel.hasOne(SubListSplitReceiverModel, { - foreignKey: 'fundeeImmutableSplitsId', + // One-to-Many: A parent Drip List can fund multiple Sub Lists. + SubListModel.hasMany(SubListSplitReceiverModel, { + foreignKey: 'parentSubListId', }); SubListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'fundeeImmutableSplitsId', + foreignKey: 'parentSubListId', }); } diff --git a/src/db/migrations/20250319131551-create_ecosystems_and_created_splits_events_tables.ts b/src/db/migrations/20250319131551-create_ecosystems_and_created_splits_events_tables.ts index 1c0b106..eac83ab 100644 --- a/src/db/migrations/20250319131551-create_ecosystems_and_created_splits_events_tables.ts +++ b/src/db/migrations/20250319131551-create_ecosystems_and_created_splits_events_tables.ts @@ -83,6 +83,10 @@ async function createEcosystemsTable( type: DataTypes.TEXT, allowNull: true, }, + latestVotingRoundId: { + type: DataTypes.UUID, + allowNull: true, + }, creator: { type: DataTypes.STRING, allowNull: false, diff --git a/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts b/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts index f92ec82..e2c4d1d 100644 --- a/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts +++ b/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts @@ -33,7 +33,43 @@ async function createSubListsTable( type: DataTypes.STRING, primaryKey: true, }, - parentAccountId: { + parentDripListId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'DripLists', + key: 'id', + }, + }, + parentEcosystemId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'Ecosystems', + key: 'id', + }, + }, + parentSubListId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'SubLists', + key: 'id', + }, + }, + rootDripListId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'DripLists', + key: 'id', + }, + }, + rootEcosystemId: { // Foreign key type: DataTypes.STRING, allowNull: true, @@ -61,9 +97,45 @@ async function createSubListsTable( await queryInterface.addIndex( { tableName: 'SubLists', schema }, - ['parentAccountId'], + ['parentDripListId'], + { + name: 'IX_SubLists_parentDripListId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SubLists', schema }, + ['parentEcosystemId'], + { + name: 'IX_SubLists_parentEcosystemId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SubLists', schema }, + ['parentSubListId'], + { + name: 'IX_SubLists_parentSubListId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SubLists', schema }, + ['rootDripListId'], + { + name: 'IX_SubLists_rootDripListId', + unique: false, + }, + ); + + await queryInterface.addIndex( + { tableName: 'SubLists', schema }, + ['rootEcosystemId'], { - name: 'IX_SubLists_parentAccountId', + name: 'IX_SubLists_rootEcosystemId', unique: false, }, ); @@ -81,7 +153,7 @@ async function createSubListSplitReceiversTable( autoIncrement: true, primaryKey: true, }, - fundeeImmutableSplitsId: { + fundeeSubListId: { // Foreign key type: DataTypes.STRING, references: { @@ -144,7 +216,7 @@ async function createSubListSplitReceiversTable( await queryInterface.addIndex( { tableName: 'SubListSplitReceivers', schema }, - ['fundeeImmutableSplitsId'], + ['fundeeSubListId'], { name: 'IX_SubListSplitReceivers_fundeeImmutableSplitsId', unique: false, diff --git a/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts b/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts index 10c8156..a08b786 100644 --- a/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts +++ b/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts @@ -30,6 +30,31 @@ export async function up({ context: sequelize }: any): Promise { }, ); + await queryInterface.addColumn( + { tableName: 'RepoDriverSplitReceivers', schema }, + 'funderSubListId', + { + type: DataTypes.STRING, + references: { + model: 'SubLists', + key: 'id', + }, + allowNull: true, + }, + ); + + await queryInterface.addIndex( + { tableName: 'RepoDriverSplitReceivers', schema }, + ['funderSubListId'], + { + name: 'IX_RepoDriverSplitReceivers_funderSubListId', + where: { + type: 'EcosystemDependency', + }, + unique: false, + }, + ); + await queryInterface.addColumn( { tableName: 'DripListSplitReceivers', schema }, 'funderEcosystemId', @@ -55,6 +80,31 @@ export async function up({ context: sequelize }: any): Promise { }, ); + await queryInterface.addColumn( + { tableName: 'DripListSplitReceivers', schema }, + 'funderSubListId', + { + type: DataTypes.STRING, + references: { + model: 'SubLists', + key: 'id', + }, + allowNull: true, + }, + ); + + await queryInterface.addIndex( + { tableName: 'DripListSplitReceivers', schema }, + ['funderSubListId'], + { + name: 'IX_DripListSplitReceivers_funderSubListId', + where: { + type: 'EcosystemDependency', + }, + unique: false, + }, + ); + await queryInterface.addColumn( { tableName: 'AddressDriverSplitReceivers', schema }, 'funderEcosystemId', @@ -79,6 +129,31 @@ export async function up({ context: sequelize }: any): Promise { unique: false, }, ); + + await queryInterface.addColumn( + { tableName: 'AddressDriverSplitReceivers', schema }, + 'funderSubListId', + { + type: DataTypes.STRING, + references: { + model: 'SubLists', + key: 'id', + }, + allowNull: true, + }, + ); + + await queryInterface.addIndex( + { tableName: 'AddressDriverSplitReceivers', schema }, + ['funderSubListId'], + { + name: 'IX_AddressDriverSplitReceivers_funderSubListId', + where: { + type: 'EcosystemDependency', + }, + unique: false, + }, + ); } export async function down({ context: sequelize }: any): Promise { diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index 7e702a1..435d311 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -17,7 +17,6 @@ import { SplitEventModel, SqueezedStreamsEventModel, SubListModel, - CreatedSplitsEventModel, EcosystemModel, SubListSplitReceiverModel, } from '../models'; @@ -45,7 +44,6 @@ export function registerModels(): void { registerModel(SplitsSetEventModel); registerModel(StreamsSetEventModel); registerModel(OwnerUpdatedEventModel); - registerModel(CreatedSplitsEventModel); registerModel(SqueezedStreamsEventModel); registerModel(SubListSplitReceiverModel); registerModel(DripListSplitReceiverModel); diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index acdeddb..665c37f 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -4,7 +4,7 @@ import type { AccountId } from '../../core/types'; import EventHandlerBase from '../../events/EventHandlerBase'; import { DRIPS_APP_USER_METADATA_KEY } from '../../core/constants'; -import handleGitProjectMetadata from './gitProject/handleGitProjectMetadata'; +import handleProjectMetadata from './handlers/handleProjectMetadata'; import LogManager from '../../core/LogManager'; import { isImmutableSplitsDriverId, @@ -12,15 +12,14 @@ import { isRepoDriverId, toAccountId, } from '../../utils/accountIdUtils'; -import { isLatestEvent } from '../../utils/eventUtils'; import { getNftDriverMetadata, toIpfsHash } from '../../utils/metadataUtils'; -import handleDripListMetadata from './dripList/handleDripListMetadata'; +import handleDripListMetadata from './handlers/handleDripListMetadata'; import type EventHandlerRequest from '../../events/EventHandlerRequest'; import { AccountMetadataEmittedEventModel } from '../../models'; import { dbConnection } from '../../db/database'; import { getCurrentSplitsByAccountId } from '../../utils/getCurrentSplits'; -import handleEcosystemMetadata from './handleEcosystemMetadata'; -import handleSubListMetadata from './handleSubListMetadata'; +import handleEcosystemMetadata from './handlers/handleEcosystemMetadata'; +import handleSubListMetadata from './handlers/handleSubListMetadata'; import type { nftDriverAccountMetadataParser } from '../../metadata/schemas'; export default class AccountMetadataEmittedEventHandler extends EventHandlerBase<'AccountMetadataEmitted(uint256,bytes32,bytes)'> { @@ -52,12 +51,14 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase const ipfsHash = toIpfsHash(value); LogManager.logRequestInfo( - `📥 ${this.name} is processing the following ${request.event.eventSignature}: - \r\t - key: ${key} - \r\t - value: ${value} (ipfs hash: ${ipfsHash}) - \r\t - accountId: ${typedAccountId} - \r\t - logIndex: ${logIndex} - \r\t - tx hash: ${transactionHash}`, + [ + `📥 ${this.name} is processing ${request.event.eventSignature}:`, + ` - key: ${key}`, + ` - value: ${value} (IPFS hash: ${ipfsHash})`, + ` - accountId: ${accountId}`, + ` - logIndex: ${logIndex}`, + ` - txHash: ${transactionHash}`, + ].join('\n'), requestId, ); @@ -89,37 +90,21 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase `${accountMetadataEmittedEventModel.transactionHash}-${accountMetadataEmittedEventModel.logIndex}`, ); - // Only if the event is the latest (in the DB), we process the metadata. - - if ( - !(await isLatestEvent( - accountMetadataEmittedEventModel, - AccountMetadataEmittedEventModel, - { - logIndex, - transactionHash, - accountId: typedAccountId, - }, - transaction, - )) - ) { - logManager.logAllInfo(); - - return; - } - - logManager.appendIsLatestEventLog(); - let handled = false; if (isRepoDriverId(typedAccountId)) { - await handleGitProjectMetadata( + await handleProjectMetadata({ + ipfsHash, logManager, - typedAccountId, transaction, - ipfsHash, blockTimestamp, - ); + projectId: typedAccountId, + originEventDetails: { + logIndex, + transactionHash, + entity: accountMetadataEmittedEventModel, + }, + }); handled = true; } @@ -136,6 +121,11 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase blockNumber, blockTimestamp, dripListId: typedAccountId, + originEventDetails: { + logIndex, + transactionHash, + entity: accountMetadataEmittedEventModel, + }, }); handled = true; @@ -150,6 +140,11 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase blockNumber, blockTimestamp, ecosystemId: typedAccountId, + originEventDetails: { + logIndex, + transactionHash, + entity: accountMetadataEmittedEventModel, + }, }); handled = true; @@ -163,6 +158,11 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase transaction, blockTimestamp, subListId: typedAccountId, + originEventDetails: { + logIndex, + transactionHash, + entity: accountMetadataEmittedEventModel, + }, }); handled = true; diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts b/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts deleted file mode 100644 index 04daa43..0000000 --- a/src/eventHandlers/AccountMetadataEmittedEvent/createDbEntriesForProjectDependency.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { Transaction } from 'sequelize'; -import type { - DependencyOfProjectType, - NftDriverId, - RepoDriverId, -} from '../../core/types'; -import { DependencyType } from '../../core/types'; -import GitProjectModel, { - ProjectVerificationStatus, -} from '../../models/GitProjectModel'; -import { FORGES_MAP } from '../../core/constants'; -import unreachableError from '../../utils/unreachableError'; -import RepoDriverSplitReceiverModel from '../../models/RepoDriverSplitReceiverModel'; - -export default async function createDbEntriesForProjectDependency( - funder: - | { type: 'project'; accountId: RepoDriverId } - | { type: 'dripList'; accountId: NftDriverId } - | { type: 'ecosystem'; accountId: NftDriverId }, - projectDependency: DependencyOfProjectType, - transaction: Transaction, - blockTimestamp: Date, -) { - const { - weight, - accountId: fundeeProjectId, - source: { forge, ownerName, repoName, url }, - } = projectDependency; - - await GitProjectModel.findOrCreate({ - lock: true, - transaction, - where: { - id: fundeeProjectId, - }, - defaults: { - url, - isVisible: true, // During creation, the project is visible by default. Account metadata will set the final visibility. - isValid: true, // There are no receivers yet, so the project is valid. - id: fundeeProjectId, - name: `${ownerName}/${repoName}`, - verificationStatus: ProjectVerificationStatus.Unclaimed, - forge: - Object.values(FORGES_MAP).find( - (f) => f.toLocaleLowerCase() === forge.toLowerCase(), - ) ?? unreachableError(), - }, - }); - - let type: DependencyType; - if (funder.type === 'project') { - type = DependencyType.ProjectDependency; - } else if (funder.type === 'dripList') { - type = DependencyType.DripListDependency; - } else { - type = DependencyType.EcosystemDependency; - } - - return RepoDriverSplitReceiverModel.create( - { - weight, - fundeeProjectId, - type, - funderDripListId: funder.type === 'dripList' ? funder.accountId : null, - funderProjectId: funder.type === 'project' ? funder.accountId : null, - funderEcosystemId: funder.type === 'ecosystem' ? funder.accountId : null, - blockTimestamp, - }, - { transaction }, - ); -} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts deleted file mode 100644 index a813a2f..0000000 --- a/src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata.ts +++ /dev/null @@ -1,308 +0,0 @@ -/* eslint-disable no-param-reassign */ -import type { AnyVersion } from '@efstajas/versioned-parser'; -import type { Transaction } from 'sequelize'; -import type { UUID } from 'crypto'; -import { toBigInt } from 'ethers'; -import type { IpfsHash, NftDriverId } from '../../../core/types'; -import { DependencyType } from '../../../core/types'; -import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; -import LogManager from '../../../core/LogManager'; -import { - isAddressDriverId, - isNftDriverId, - isRepoDriverId, - toAddressDriverId, - toImmutableSplitsDriverId, - toNftDriverId, -} from '../../../utils/accountIdUtils'; -import { assertDependencyOfProjectType } from '../../../utils/assert'; -import createDbEntriesForProjectDependency from '../createDbEntriesForProjectDependency'; -import { - AddressDriverSplitReceiverModel, - DripListModel, - RepoDriverSplitReceiverModel, - DripListSplitReceiverModel, - SubListSplitReceiverModel, -} from '../../../models'; -import unreachableError from '../../../utils/unreachableError'; -import validateSplitsReceivers from '../splitsValidator'; -import getUserAddress from '../../../utils/getAccountAddress'; -import { AddressDriverSplitReceiverType } from '../../../models/AddressDriverSplitReceiverModel'; -import appSettings from '../../../config/appSettings'; - -type Params = { - ipfsHash: IpfsHash; - blockNumber: number; - blockTimestamp: Date; - logManager: LogManager; - dripListId: NftDriverId; - transaction: Transaction; - metadata: AnyVersion; -}; - -export default async function handleDripListMetadata({ - ipfsHash, - metadata, - logManager, - dripListId, - blockNumber, - transaction, - blockTimestamp, -}: Params) { - if (dripListId !== metadata.describes.accountId) { - unreachableError( - `Account ID mismatch with: got ${metadata.describes.accountId}, expected ${dripListId}`, - ); - } - - if ('type' in metadata && metadata.type !== 'dripList') { - unreachableError( - `Metadata type mismatch with: got '${metadata.type}', expected 'dripList`, - ); - } - - // This must be the only place a Drip List is created. - const dripList = await DripListModel.create( - { - id: dripListId, - isValid: false, // Until the related `TransferEvent` is processed. - name: metadata.name ?? null, - description: - 'description' in metadata ? metadata.description || null : null, - latestVotingRoundId: - 'latestVotingRoundId' in metadata - ? (metadata.latestVotingRoundId as UUID) || null - : null, - lastProcessedIpfsHash: ipfsHash, - isVisible: - blockNumber > appSettings.visibilityThresholdBlockNumber && - 'isVisible' in metadata - ? metadata.isVisible - : true, - }, - { transaction }, - ); - - logManager - .appendFindOrCreateLog(DripListModel, true, dripList.id) - .logAllInfo(); - - if (metadata.projects && 'recipients' in metadata && metadata.recipients) { - unreachableError( - `Metadata contains both 'projects' and 'recipients' fields. This is not allowed.`, - ); - } - const splits = metadata.projects ?? metadata.recipients; - - const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = - await validateSplitsReceivers( - dripList.id, - splits.map((s) => ({ - weight: s.weight, - accountId: s.accountId, - })), - ); - - // If we reach this point, it means that the processed `AccountMetadataEmitted` event is the latest in the DB. - // But we still need to check if the splits are the latest on-chain. - // There is no need to process the metadata if the splits are not the latest on-chain. - - if (!areSplitsValid) { - logManager.appendLog( - `Skipping metadata update for Drip List with ID ${dripListId} because the splits receivers hashes from the contract and the metadata do not match: - \r\t - On-chain splits receivers hash: ${onChainSplitsHash} - \r\t - Metadata splits receivers hash: ${calculatedSplitsHash} - \r Possible reasons: - \r\t - The metadata were the latest in the DB but not on-chain. - \r\t - The metadata were manually emitted with different splits than the latest on-chain.`, - ); - - return; - } - - await createDbEntriesForDripListSplits({ - metadata, - logManager, - transaction, - blockTimestamp, - funderDripListId: dripListId, - }); -} - -async function createDbEntriesForDripListSplits({ - metadata, - logManager, - transaction, - blockTimestamp, - funderDripListId, -}: { - funderDripListId: NftDriverId; - metadata: AnyVersion; - logManager: LogManager; - transaction: Transaction; - blockTimestamp: Date; -}) { - await clearCurrentProjectSplits(funderDripListId, transaction); - - if (metadata.projects) { - // Legacy metadata version. - const splits = metadata.projects; - const splitsPromises = splits.map((split) => { - if (isRepoDriverId(split.accountId)) { - assertDependencyOfProjectType(split); - - return createDbEntriesForProjectDependency( - { - type: 'dripList', - accountId: funderDripListId, - }, - split, - transaction, - blockTimestamp, - ); - } - - if (isNftDriverId(split.accountId)) { - return DripListSplitReceiverModel.create( - { - funderDripListId, - fundeeDripListId: split.accountId, - weight: split.weight, - type: DependencyType.DripListDependency, - blockTimestamp, - }, - { transaction }, - ); - } - - if (isAddressDriverId(split.accountId)) { - return AddressDriverSplitReceiverModel.create( - { - funderDripListId, - weight: split.weight, - fundeeAccountId: split.accountId, - fundeeAccountAddress: getUserAddress(split.accountId), - type: AddressDriverSplitReceiverType.DripListDependency, - blockTimestamp, - }, - { transaction }, - ); - } - - return unreachableError( - `Split with account ID ${split.accountId} is not an Address, Drip List, or a Git Project.`, - ); - }); - - const result = await Promise.all([...splitsPromises]); - - logManager.appendLog( - `Updated ${LogManager.nameOfType( - DripListModel, - )} with ID ${funderDripListId} splits: ${result - .map((p) => JSON.stringify(p)) - .join(`, `)} - `, - ); - } else if ('recipients' in metadata) { - // Current metadata version. - const splits = metadata.recipients; - - const splitsPromises = splits.map((split) => { - if (split.type === 'repoDriver') { - assertDependencyOfProjectType(split); - - return createDbEntriesForProjectDependency( - { - type: 'dripList', - accountId: funderDripListId, - }, - split, - transaction, - blockTimestamp, - ); - } - if (split.type === 'dripList') { - return DripListSplitReceiverModel.create( - { - funderDripListId, - fundeeDripListId: toNftDriverId(toBigInt(split.accountId)), - weight: split.weight, - type: DependencyType.DripListDependency, - blockTimestamp, - }, - { transaction }, - ); - } - if (split.type === 'address') { - return AddressDriverSplitReceiverModel.create( - { - funderDripListId, - weight: split.weight, - fundeeAccountId: toAddressDriverId(split.accountId), - fundeeAccountAddress: getUserAddress(split.accountId), - type: AddressDriverSplitReceiverType.DripListDependency, - blockTimestamp, - }, - { transaction }, - ); - } - - return SubListSplitReceiverModel.create( - { - funderDripListId, - weight: split.weight, - fundeeImmutableSplitsId: toImmutableSplitsDriverId(split.accountId), - type: DependencyType.DripListDependency, - blockTimestamp, - }, - { transaction }, - ); - }); - - const result = await Promise.all([...splitsPromises]); - - logManager.appendLog( - `Updated ${LogManager.nameOfType( - DripListModel, - )} with ID ${funderDripListId} splits: ${result - .map((p) => JSON.stringify(p)) - .join(`, `)} - `, - ); - } else { - unreachableError( - `Metadata does not contain 'projects' or 'recipients' field.`, - ); - } -} - -async function clearCurrentProjectSplits( - funderDripListId: string, - transaction: Transaction, -) { - await AddressDriverSplitReceiverModel.destroy({ - where: { - funderDripListId, - }, - transaction, - }); - await RepoDriverSplitReceiverModel.destroy({ - where: { - funderDripListId, - }, - transaction, - }); - await DripListSplitReceiverModel.destroy({ - where: { - funderDripListId, - }, - transaction, - }); - await SubListSplitReceiverModel.destroy({ - where: { - funderDripListId, - }, - transaction, - }); -} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/validateProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/validateProjectMetadata.ts deleted file mode 100644 index 307f42c..0000000 --- a/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/validateProjectMetadata.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { AnyVersion } from '@efstajas/versioned-parser'; -import type { repoDriverAccountMetadataParser } from '../../../metadata/schemas'; -import type { GitProjectModel } from '../../../models'; -import unreachableError from '../../../utils/unreachableError'; - -export default async function validateProjectMetadata( - project: GitProjectModel, - metadata: AnyVersion, -): Promise { - if (!metadata) { - unreachableError(`Metadata for Git Project with ID ${project.id} is null.`); - } - - const errors = []; - - const { describes, source } = metadata; - const { - url: metaUrl, - repoName: metaRepoName, - ownerName: metaOwnerName, - } = source; - const { id: onChainProjectId, name: onChainProjectName } = project; - - if (`${metaOwnerName}/${metaRepoName}` !== `${onChainProjectName}`) { - errors.push( - `repoName mismatch: got ${metaOwnerName}/${metaRepoName}, expected ${onChainProjectName}.`, - ); - } - - if (metaUrl !== project.url) { - errors.push(`url mismatch: got ${metaUrl}, expected ${project.url}.`); - } - - if (describes.accountId !== onChainProjectId) { - errors.push( - `accountId mismatch with: got ${describes.accountId}, expected ${onChainProjectId}.`, - ); - } - - if (errors.length > 0) { - throw new Error( - `Git Project with ID ${onChainProjectId} has metadata that does not match the metadata emitted by the contract (${errors.join( - '; ', - )}).`, - ); - } -} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts deleted file mode 100644 index e6f8abf..0000000 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handleEcosystemMetadata.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type { AnyVersion, LatestVersion } from '@efstajas/versioned-parser'; -import type { Transaction } from 'sequelize'; -import appSettings from '../../config/appSettings'; -import LogManager from '../../core/LogManager'; -import type { IpfsHash, NftDriverId } from '../../core/types'; -import { DependencyType } from '../../core/types'; -import type { nftDriverAccountMetadataParser } from '../../metadata/schemas'; -import { - SubListSplitReceiverModel, - RepoDriverSplitReceiverModel, - EcosystemModel, -} from '../../models'; -import { toImmutableSplitsDriverId } from '../../utils/accountIdUtils'; -import { assertDependencyOfProjectType } from '../../utils/assert'; -import unreachableError from '../../utils/unreachableError'; -import createDbEntriesForProjectDependency from './createDbEntriesForProjectDependency'; -import validateSplitsReceivers from './splitsValidator'; - -type Params = { - ipfsHash: IpfsHash; - blockNumber: number; - blockTimestamp: Date; - logManager: LogManager; - ecosystemId: NftDriverId; - transaction: Transaction; - metadata: AnyVersion; -}; - -export default async function handleEcosystemMetadata({ - ipfsHash, - metadata, - logManager, - ecosystemId, - blockNumber, - transaction, - blockTimestamp, -}: Params) { - if (ecosystemId !== metadata.describes.accountId) { - unreachableError( - `Account ID mismatch with: got ${metadata.describes.accountId}, expected ${ecosystemId}`, - ); - } - - if ('type' in metadata && metadata.type !== 'ecosystem') { - unreachableError( - `Metadata type mismatch with: got ${metadata.type}, expected 'ecosystem'`, - ); - } - - if (!('recipients' in metadata)) { - unreachableError( - `Unsupported metadata version: missing 'recipients' field`, - ); - } - // This must be the only place an Ecosystem is created. - const ecosystem = await EcosystemModel.create( - { - id: ecosystemId, - isValid: false, - name: metadata.name ?? null, - description: - 'description' in metadata ? metadata.description || null : null, - lastProcessedIpfsHash: ipfsHash, - isVisible: - blockNumber > appSettings.visibilityThresholdBlockNumber && - 'isVisible' in metadata - ? metadata.isVisible - : true, - }, - { transaction }, - ); - - logManager - .appendFindOrCreateLog(EcosystemModel, true, ecosystem.id) - .logAllInfo(); - - const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = - await validateSplitsReceivers( - ecosystem.id, - metadata.recipients.map((s) => ({ - weight: s.weight, - accountId: s.accountId, - })), - ); - - // If we reach this point, it means that the processed `AccountMetadataEmitted` event is the latest in the DB. - // But we still need to check if the splits are the latest on-chain. - // There is no need to process the metadata if the splits are not the latest on-chain. - - if (!areSplitsValid) { - logManager.appendLog( - `Skipping metadata update for Ecosystem with ID ${ecosystemId} because the splits receivers hashes from the contract and the metadata do not match: - \r\t - On-chain splits receivers hash: ${onChainSplitsHash} - \r\t - Metadata splits receivers hash: ${calculatedSplitsHash} - \r Possible reasons: - \r\t - The metadata were the latest in the DB but not on-chain. - \r\t - The metadata were manually emitted with different splits than the latest on-chain.`, - ); - - return; - } - - await createDbEntriesForEcosystemSplits({ - metadata, - logManager, - transaction, - blockTimestamp, - funderEcosystemId: ecosystemId, - }); -} - -async function createDbEntriesForEcosystemSplits({ - metadata, - logManager, - transaction, - blockTimestamp, - funderEcosystemId, -}: { - funderEcosystemId: NftDriverId; - metadata: LatestVersion; - logManager: LogManager; - transaction: Transaction; - blockTimestamp: Date; -}) { - await clearCurrentEcosystemSplits(funderEcosystemId, transaction); - - const splits = metadata.recipients; - - const splitsPromises = splits.map((split) => { - if (split.type === 'repoDriver') { - assertDependencyOfProjectType(split); - - return createDbEntriesForProjectDependency( - { - type: 'ecosystem', - accountId: funderEcosystemId, - }, - split, - transaction, - blockTimestamp, - ); - } - if (split.type === 'subList') { - return SubListSplitReceiverModel.create( - { - funderEcosystemId, - weight: split.weight, - fundeeImmutableSplitsId: toImmutableSplitsDriverId(split.accountId), - type: DependencyType.EcosystemDependency, - blockTimestamp, - }, - { transaction }, - ); - } - return unreachableError( - `Split with account ID ${split.accountId} is not a Project or an Ecosystem.`, - ); - }); - - const result = await Promise.all([...splitsPromises]); - - logManager.appendLog( - `Updated ${LogManager.nameOfType( - EcosystemModel, - )} with ID ${funderEcosystemId} splits: ${result - .map((p) => JSON.stringify(p)) - .join(`, `)} - `, - ); -} - -async function clearCurrentEcosystemSplits( - funderEcosystemId: string, - transaction: Transaction, -) { - await RepoDriverSplitReceiverModel.destroy({ - where: { - funderEcosystemId, - }, - transaction, - }); - await SubListSplitReceiverModel.destroy({ - where: { - funderEcosystemId, - }, - transaction, - }); -} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts deleted file mode 100644 index a04f35a..0000000 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handleSubListMetadata.ts +++ /dev/null @@ -1,232 +0,0 @@ -/* eslint-disable no-param-reassign */ -import type { AnyVersion } from '@efstajas/versioned-parser'; -import type { Transaction } from 'sequelize'; -import LogManager from '../../core/LogManager'; -import type { - IpfsHash, - ImmutableSplitsDriverId, - NftDriverId, -} from '../../core/types'; -import { DependencyType } from '../../core/types'; -import type { immutableSplitsDriverMetadataParser } from '../../metadata/schemas'; -import { - SubListModel, - AddressDriverSplitReceiverModel, - DripListSplitReceiverModel, - RepoDriverSplitReceiverModel, - SubListSplitReceiverModel, -} from '../../models'; -import { AddressDriverSplitReceiverType } from '../../models/AddressDriverSplitReceiverModel'; -import { - isRepoDriverId, - isAddressDriverId, - isNftDriverId, - toNftDriverId, - isImmutableSplitsDriverId, -} from '../../utils/accountIdUtils'; -import { assertDependencyOfProjectType } from '../../utils/assert'; -import getUserAddress from '../../utils/getAccountAddress'; -import { getImmutableSpitsDriverMetadata } from '../../utils/metadataUtils'; -import unreachableError from '../../utils/unreachableError'; -import createDbEntriesForProjectDependency from './createDbEntriesForProjectDependency'; -import validateSplitsReceivers from './splitsValidator'; - -type Params = { - ipfsHash: IpfsHash; - blockTimestamp: Date; - logManager: LogManager; - transaction: Transaction; - subListId: ImmutableSplitsDriverId; -}; - -export default async function handleSubListMetadata({ - ipfsHash, - subListId, - logManager, - transaction, - blockTimestamp, -}: Params) { - const subList = await SubListModel.findByPk(subListId, { - transaction, - lock: true, - }); - - if (!subList) { - throw new Error( - `Failed to update metadata for Sub List with ID ${subListId}: Sub List not found. - \r Possible reasons: - \r\t - The event that should have created the Sub List was not processed yet. - \r\t - The metadata were manually emitted for a Sub List that does not exist in the app.`, - ); - } - - const metadata = await getImmutableSpitsDriverMetadata(ipfsHash); - - const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = - await validateSplitsReceivers(subList.id, metadata.recipients); - - // If we reach this point, it means that the processed `AccountMetadataEmitted` event is the latest in the DB. - // But we still need to check if the splits are the latest on-chain. - // There is no need to process the metadata if the splits are not the latest on-chain. - - if (!areSplitsValid) { - logManager.appendLog( - `Skipping metadata update for Sub List with ID ${subListId} because the splits receivers hashes from the contract and the metadata do not match: - \r\t - On-chain splits receivers hash: ${onChainSplitsHash} - \r\t - Metadata splits receivers hash: ${calculatedSplitsHash} - \r Possible reasons: - \r\t - The metadata were the latest in the DB but not on-chain. - \r\t - The metadata were manually emitted with different splits than the latest on-chain.`, - ); - - return; - } - - if (subList.dataValues.parentAccountId !== metadata.parent.accountId) { - throw new Error( - `Sub List with ID ${subListId} has a different on-chain parent account ID than the metadata. Expected: '${subList.dataValues.parentAccountId}', got: '${metadata.parent.accountId}'`, - ); - } - - await updateSubListMetadata( - subList, - logManager, - transaction, - metadata, - ipfsHash, - ); - - await createDbEntriesForSubListSplits( - toNftDriverId(metadata.parent.accountId), - metadata.recipients, - logManager, - transaction, - blockTimestamp, - ); -} - -async function updateSubListMetadata( - subList: SubListModel, - logManager: LogManager, - transaction: Transaction, - metadata: AnyVersion, - metadataIpfsHash: IpfsHash, -): Promise { - subList.parentAccountId = toNftDriverId(metadata.parent.accountId); - subList.lastProcessedIpfsHash = metadataIpfsHash; - - logManager.appendUpdateLog(subList, SubListModel, subList.id); - - await subList.save({ transaction }); -} - -async function createDbEntriesForSubListSplits( - funderEcosystemId: NftDriverId, - splits: AnyVersion['recipients'], - logManager: LogManager, - transaction: Transaction, - blockTimestamp: Date, -) { - await clearCurrentProjectSplits(funderEcosystemId, transaction); - - const splitsPromises = splits.map(async (dependency) => { - if (isRepoDriverId(dependency.accountId)) { - assertDependencyOfProjectType(dependency); - - return createDbEntriesForProjectDependency( - { - type: 'ecosystem', - accountId: funderEcosystemId, - }, - dependency, - transaction, - blockTimestamp, - ); - } - - if (isAddressDriverId(dependency.accountId)) { - return AddressDriverSplitReceiverModel.create( - { - funderEcosystemId, - weight: dependency.weight, - fundeeAccountId: dependency.accountId, - fundeeAccountAddress: getUserAddress(dependency.accountId), - type: AddressDriverSplitReceiverType.EcosystemDependency, - blockTimestamp, - }, - { transaction }, - ); - } - - if (isNftDriverId(dependency.accountId)) { - return DripListSplitReceiverModel.create( - { - funderEcosystemId, - fundeeDripListId: dependency.accountId, - weight: dependency.weight, - type: DependencyType.EcosystemDependency, - blockTimestamp, - }, - { transaction }, - ); - } - - if (isImmutableSplitsDriverId(dependency.accountId)) { - return SubListSplitReceiverModel.create( - { - funderEcosystemId, - fundeeImmutableSplitsId: dependency.accountId, - weight: dependency.weight, - type: DependencyType.EcosystemDependency, - blockTimestamp, - }, - { transaction }, - ); - } - - return unreachableError( - `Dependency with account ID ${dependency.accountId} is not an Address nor a Git Project.`, - ); - }); - - const result = await Promise.all(splitsPromises); - - logManager.appendLog( - `Updated ${LogManager.nameOfType( - SubListModel, - )} with ID ${funderEcosystemId} splits: ${result - .map((p) => JSON.stringify(p)) - .join(`, `)} - `, - ); -} - -async function clearCurrentProjectSplits( - funderEcosystemId: string, - transaction: Transaction, -) { - await AddressDriverSplitReceiverModel.destroy({ - where: { - funderEcosystemId, - }, - transaction, - }); - await RepoDriverSplitReceiverModel.destroy({ - where: { - funderEcosystemId, - }, - transaction, - }); - await DripListSplitReceiverModel.destroy({ - where: { - funderEcosystemId, - }, - transaction, - }); - await SubListSplitReceiverModel.destroy({ - where: { - funderEcosystemId, - }, - transaction, - }); -} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts new file mode 100644 index 0000000..005cdd4 --- /dev/null +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -0,0 +1,270 @@ +/* eslint-disable no-param-reassign */ +import type { AnyVersion } from '@efstajas/versioned-parser'; +import type { Transaction } from 'sequelize'; +import type { UUID } from 'crypto'; +import type { z } from 'zod'; +import type { IpfsHash, NftDriverId } from '../../../core/types'; +import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; +import LogManager from '../../../core/LogManager'; +import { + AccountMetadataEmittedEventModel, + DripListModel, +} from '../../../models'; +import unreachableError from '../../../utils/unreachableError'; +import verifySplitsReceivers from '../verifySplitsReceivers'; +import appSettings from '../../../config/appSettings'; +import { isLatestEvent } from '../../../utils/eventUtils'; +import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; +import type { + repoDriverSplitReceiverSchema, + addressDriverSplitReceiverSchema, +} from '../../../metadata/schemas/repo-driver/v2'; +import type { subListSplitReceiverSchema } from '../../../metadata/schemas/sub-list/v1'; +import verifyProjectSources from '../projectVerification'; +import { + deleteExistingReceivers, + createProjectAndProjectReceiver, + createSubListReceiver, + createDripListReceiver, + createAddressReceiver, +} from '../receiversRepository'; + +type Params = { + ipfsHash: IpfsHash; + blockNumber: number; + blockTimestamp: Date; + logManager: LogManager; + dripListId: NftDriverId; + transaction: Transaction; + metadata: AnyVersion; + originEventDetails: { + entity: AccountMetadataEmittedEventModel; + logIndex: number; + transactionHash: string; + }; +}; + +export default async function handleDripListMetadata({ + ipfsHash, + metadata, + logManager, + dripListId, + blockNumber, + transaction, + blockTimestamp, + originEventDetails: { entity, logIndex, transactionHash }, +}: Params) { + // Only process metadata if this is the latest event. + if ( + !(await isLatestEvent( + entity, + AccountMetadataEmittedEventModel, + { + logIndex, + transactionHash, + accountId: dripListId, + }, + transaction, + )) + ) { + logManager.logAllInfo(); + + return; + } + logManager.appendIsLatestEventLog(); + + assertMetadataIsValid(dripListId, metadata); + + // Here, is the only place an Drip List is created. + const dripList = await DripListModel.create( + { + id: dripListId, + isValid: false, // Until the related `TransferEvent` is processed. + name: metadata.name ?? null, + description: + 'description' in metadata ? metadata.description || null : null, + latestVotingRoundId: + 'latestVotingRoundId' in metadata + ? (metadata.latestVotingRoundId as UUID) || null + : null, + lastProcessedIpfsHash: ipfsHash, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, + }, + { transaction }, + ); + + logManager + .appendFindOrCreateLog(DripListModel, true, dripList.id) + .logAllInfo(); + + const receivers = metadata.projects ?? metadata.recipients; + + const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = + await verifySplitsReceivers( + dripList.id, + receivers.map(({ weight, accountId }) => ({ + weight, + accountId, + })), + ); + + if (!areSplitsValid) { + logManager.appendLog( + [ + `Skipping metadata update for Drip List ${dripListId} due to mismatch in splits hash.`, + ` On-chain hash: ${onChainSplitsHash}`, + ` Metadata hash: ${calculatedSplitsHash}`, + ` Possible causes:`, + ` - The metadata event is the latest in the DB, but not on-chain.`, + ` - The metadata was manually emitted with outdated or mismatched splits.`, + ].join('\n'), + ); + + return; + } + + await verifyProjectSources(receivers); + + await deleteExistingReceivers({ + for: { + accountId: dripListId, + column: 'funderDripListId', + }, + transaction, + }); + + await setNewReceivers({ + receivers, + logManager, + transaction, + blockTimestamp, + funderDripListId: dripListId, + }); +} + +async function setNewReceivers({ + receivers, + logManager, + transaction, + blockTimestamp, + funderDripListId, +}: { + blockTimestamp: Date; + logManager: LogManager; + transaction: Transaction; + funderDripListId: NftDriverId; + receivers: ( + | z.infer + | z.infer + | z.infer + | z.infer + )[]; +}) { + const receiverPromises = receivers.map(async (receiver) => { + switch (receiver.type) { + case 'repoDriver': + return createProjectAndProjectReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: receiver, + funder: { + type: 'dripList', + accountId: funderDripListId, + }, + }); + + case 'subList': + return createSubListReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: receiver, + funder: { + type: 'dripList', + accountId: funderDripListId, + }, + }); + + case 'dripList': + return createDripListReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: receiver, + funder: { + type: 'dripList', + accountId: funderDripListId, + }, + }); + + case 'address': + return createAddressReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: receiver, + funder: { + type: 'dripList', + accountId: funderDripListId, + }, + }); + + default: + return unreachableError( + `Unhandled receiver type: ${(receiver as any).type}`, + ); + } + }); + + const result = await Promise.all(receiverPromises); + + logManager.appendLog( + `Updated ${LogManager.nameOfType( + DripListModel, + )} with ID ${funderDripListId} splits: ${result + .map((p) => JSON.stringify(p)) + .join(`, `)} + `, + ); +} + +function assertMetadataIsValid( + dripListId: NftDriverId, + metadata: AnyVersion, +): asserts metadata is Extract< + typeof metadata, + | { + type: 'dripList'; + recipients: ( + | z.infer + | z.infer + | z.infer + | z.infer + )[]; + } + | { + projects: ( + | z.infer + | z.infer + | z.infer + )[]; + } +> { + if (dripListId !== metadata.describes.accountId) { + unreachableError( + `Drip List metadata describes account ID '${metadata.describes.accountId}' but it was emitted by ${dripListId}.`, + ); + } + + const isCurrent = 'recipients' in metadata && metadata.type === 'dripList'; + const isLegacy = 'projects' in metadata; + + if (!isCurrent && !isLegacy) { + throw new Error('Invalid Drip List metadata format.'); + } +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMetadata.ts new file mode 100644 index 0000000..d222620 --- /dev/null +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMetadata.ts @@ -0,0 +1,224 @@ +import type { AnyVersion } from '@efstajas/versioned-parser'; +import type { Transaction } from 'sequelize'; +import type { z } from 'zod'; +import appSettings from '../../../config/appSettings'; +import LogManager from '../../../core/LogManager'; +import type { IpfsHash, NftDriverId } from '../../../core/types'; +import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; +import { + EcosystemModel, + AccountMetadataEmittedEventModel, +} from '../../../models'; +import unreachableError from '../../../utils/unreachableError'; +import verifySplitsReceivers from '../verifySplitsReceivers'; +import { isLatestEvent } from '../../../utils/eventUtils'; +import type { repoDriverSplitReceiverSchema } from '../../../metadata/schemas/repo-driver/v2'; +import type { subListSplitReceiverSchema } from '../../../metadata/schemas/sub-list/v1'; +import verifyProjectSources from '../projectVerification'; +import { + createProjectAndProjectReceiver, + createSubListReceiver, + deleteExistingReceivers, +} from '../receiversRepository'; + +type Params = { + ipfsHash: IpfsHash; + blockNumber: number; + blockTimestamp: Date; + logManager: LogManager; + ecosystemId: NftDriverId; + transaction: Transaction; + metadata: AnyVersion; + originEventDetails: { + entity: AccountMetadataEmittedEventModel; + logIndex: number; + transactionHash: string; + }; +}; + +export default async function handleEcosystemMetadata({ + ipfsHash, + metadata, + logManager, + ecosystemId, + blockNumber, + transaction, + blockTimestamp, + originEventDetails: { entity, logIndex, transactionHash }, +}: Params) { + // Only process metadata if this is the latest event. + if ( + !(await isLatestEvent( + entity, + AccountMetadataEmittedEventModel, + { + logIndex, + transactionHash, + accountId: ecosystemId, + }, + transaction, + )) + ) { + logManager.logAllInfo(); + + return; + } + logManager.appendIsLatestEventLog(); + + assertMetadataIsValid(ecosystemId, metadata); + + // Here, is the only place an Ecosystem is created. + const ecosystem = await EcosystemModel.create( + { + id: ecosystemId, + isValid: false, // Until the related `TransferEvent` is processed. + name: metadata.name ?? null, + description: + 'description' in metadata ? metadata.description || null : null, + lastProcessedIpfsHash: ipfsHash, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, + }, + { transaction }, + ); + + logManager + .appendFindOrCreateLog(EcosystemModel, true, ecosystem.id) + .logAllInfo(); + + const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = + await verifySplitsReceivers( + ecosystem.id, + metadata.recipients.map(({ weight, accountId }) => ({ + weight, + accountId, + })), + ); + + if (!areSplitsValid) { + logManager.appendLog( + [ + `Skipping metadata update for Ecosystem ${ecosystemId} due to mismatch in splits hash.`, + ` On-chain hash: ${onChainSplitsHash}`, + ` Metadata hash: ${calculatedSplitsHash}`, + ` Possible causes:`, + ` - The metadata event is the latest in the DB, but not on-chain.`, + ` - The metadata was manually emitted with outdated or mismatched splits.`, + ].join('\n'), + ); + + return; + } + + await verifyProjectSources(metadata.recipients); + + await deleteExistingReceivers({ + for: { + accountId: ecosystemId, + column: 'funderEcosystemId', + }, + transaction, + }); + + await setNewReceivers({ + logManager, + transaction, + blockTimestamp, + receivers: metadata.recipients, + funderEcosystemId: ecosystemId, + }); +} + +async function setNewReceivers({ + receivers, + logManager, + transaction, + blockTimestamp, + funderEcosystemId, +}: { + blockTimestamp: Date; + logManager: LogManager; + transaction: Transaction; + funderEcosystemId: NftDriverId; + receivers: ( + | z.infer + | z.infer + )[]; +}) { + const receiverPromises = receivers.map(async (receiver) => { + switch (receiver.type) { + case 'repoDriver': + return createProjectAndProjectReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: receiver, + funder: { + type: 'ecosystem', + accountId: funderEcosystemId, + }, + }); + + case 'subList': + return createSubListReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: receiver, + funder: { + type: 'ecosystem', + accountId: funderEcosystemId, + }, + }); + + default: + return unreachableError( + `Unhandled receiver type: ${(receiver as any).type}`, + ); + } + }); + + const result = await Promise.all(receiverPromises); + + logManager.appendLog( + `Updated ${LogManager.nameOfType( + EcosystemModel, + )} with ID ${funderEcosystemId} splits: ${result + .map((p) => JSON.stringify(p)) + .join(`, `)} + `, + ); +} + +function assertMetadataIsValid( + ecosystemId: NftDriverId, + metadata: AnyVersion, +): asserts metadata is Extract< + typeof metadata, + { + type: 'ecosystem'; + recipients: ( + | z.infer + | z.infer + )[]; + } +> { + if (ecosystemId !== metadata.describes.accountId) { + unreachableError( + `Ecosystem metadata describes account ID ${metadata.describes.accountId} but it was emitted by ${ecosystemId}.`, + ); + } + + if ( + !( + 'recipients' in metadata && + 'type' in metadata && + metadata.type === 'ecosystem' + ) + ) { + throw new Error('Invalid Ecosystem metadata.'); + } +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts similarity index 53% rename from src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts rename to src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index 3b4a873..ae8992e 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -1,11 +1,11 @@ /* eslint-disable no-param-reassign */ import type { Transaction } from 'sequelize'; import type { AnyVersion } from '@efstajas/versioned-parser'; +import type { z } from 'zod'; import { - AddressDriverSplitReceiverModel, + AccountMetadataEmittedEventModel, DripListSplitReceiverModel, GitProjectModel, - RepoDriverSplitReceiverModel, } from '../../../models'; import type { repoDriverAccountMetadataParser } from '../../../metadata/schemas'; import LogManager from '../../../core/LogManager'; @@ -18,22 +18,61 @@ import { isNftDriverId, isRepoDriverId, } from '../../../utils/accountIdUtils'; -import { AddressDriverSplitReceiverType } from '../../../models/AddressDriverSplitReceiverModel'; -import { assertDependencyOfProjectType } from '../../../utils/assert'; -import createDbEntriesForProjectDependency from '../createDbEntriesForProjectDependency'; import unreachableError from '../../../utils/unreachableError'; import { getProjectMetadata } from '../../../utils/metadataUtils'; -import validateProjectMetadata from './validateProjectMetadata'; -import validateSplitsReceivers from '../splitsValidator'; -import getUserAddress from '../../../utils/getAccountAddress'; +import verifySplitsReceivers from '../verifySplitsReceivers'; +import { isLatestEvent } from '../../../utils/eventUtils'; +import { verifyProjectMetadata } from '../projectVerification'; +import { + createAddressReceiver, + createProjectAndProjectReceiver, + deleteExistingReceivers, +} from '../receiversRepository'; +import type { + addressDriverSplitReceiverSchema, + repoDriverSplitReceiverSchema, +} from '../../../metadata/schemas/repo-driver/v2'; + +type Params = { + ipfsHash: IpfsHash; + blockTimestamp: Date; + logManager: LogManager; + projectId: RepoDriverId; + transaction: Transaction; + originEventDetails: { + entity: AccountMetadataEmittedEventModel; + logIndex: number; + transactionHash: string; + }; +}; + +export default async function handleGitProjectMetadata({ + ipfsHash, + logManager, + projectId, + transaction, + blockTimestamp, + originEventDetails: { entity, logIndex, transactionHash }, +}: Params) { + // Only process metadata if this is the latest event. + if ( + !(await isLatestEvent( + entity, + AccountMetadataEmittedEventModel, + { + logIndex, + transactionHash, + accountId: projectId, + }, + transaction, + )) + ) { + logManager.logAllInfo(); + + return; + } + logManager.appendIsLatestEventLog(); -export default async function handleGitProjectMetadata( - logManager: LogManager, - projectId: RepoDriverId, - transaction: Transaction, - ipfsHash: IpfsHash, - blockTimestamp: Date, -) { const project = await GitProjectModel.findByPk(projectId, { transaction, lock: true, @@ -41,17 +80,19 @@ export default async function handleGitProjectMetadata( if (!project) { throw new Error( - `Failed to update metadata for Project with ID ${projectId}: Project not found. - \r Possible reasons: - \r\t - The event that should have created the Project was not processed yet. - \r\t - The metadata were manually emitted for a Project that does not exist in the app.`, + [ + `Failed to update metadata for Project ${projectId}: Project not found.`, + `Possible reasons:`, + ` - The event that should have created the Project hasn't been processed yet.`, + ` - Metadata was manually emitted for a Project that doesn't exist in the system.`, + ].join('\n'), ); } const metadata = await getProjectMetadata(ipfsHash); const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = - await validateSplitsReceivers( + await verifySplitsReceivers( project.id, metadata.splits.dependencies .concat(metadata.splits.maintainers as any) @@ -61,26 +102,24 @@ export default async function handleGitProjectMetadata( })), ); - // If we reach this point, it means that the processed `AccountMetadataEmitted` event is the latest in the DB. - // But we still need to check if the splits are the latest on-chain. - // There is no need to process the metadata if the splits are not the latest on-chain. - if (!areSplitsValid) { logManager.appendLog( - `Skipping metadata update for Project with ID ${projectId} because the splits receivers hashes from the contract and the metadata do not match: - \r\t - On-chain splits receivers hash: ${onChainSplitsHash} - \r\t - Metadata splits receivers hash: ${calculatedSplitsHash} - \r Possible reasons: - \r\t - The metadata were the latest in the DB but not on-chain. - \r\t - The metadata were manually emitted with different splits than the latest on-chain.`, + [ + `Skipping metadata update for Project ${projectId} due to mismatch in splits hash.`, + ` On-chain hash: ${onChainSplitsHash}`, + ` Metadata hash: ${calculatedSplitsHash}`, + ` Possible causes:`, + ` - The metadata event is the latest in the DB, but not on-chain.`, + ` - The metadata was manually emitted with outdated or mismatched splits.`, + ].join('\n'), ); return; } - await validateProjectMetadata(project, metadata); + await verifyProjectMetadata(project, metadata); - await updateGitProjectMetadata( + await updateProjectMetadata( project, logManager, transaction, @@ -88,7 +127,15 @@ export default async function handleGitProjectMetadata( ipfsHash, ); - await createDbEntriesForProjectSplits( + await deleteExistingReceivers({ + for: { + accountId: projectId, + column: 'funderProjectId', + }, + transaction, + }); + + await setNewReceivers( projectId, metadata.splits, logManager, @@ -97,7 +144,7 @@ export default async function handleGitProjectMetadata( ); } -async function updateGitProjectMetadata( +async function updateProjectMetadata( project: GitProjectModel, logManager: LogManager, transaction: Transaction, @@ -134,60 +181,63 @@ async function updateGitProjectMetadata( await project.save({ transaction }); } -async function createDbEntriesForProjectSplits( +async function setNewReceivers( funderProjectId: RepoDriverId, - splits: AnyVersion['splits'], + receivers: AnyVersion['splits'], logManager: LogManager, transaction: Transaction, blockTimestamp: Date, ) { - await clearCurrentProjectSplits(funderProjectId, transaction); - - const { dependencies, maintainers } = splits; + const { dependencies, maintainers } = receivers; const maintainerPromises = maintainers.map((maintainer) => { assertAddressDiverId(maintainer.accountId); - return AddressDriverSplitReceiverModel.create( - { - funderProjectId, - weight: maintainer.weight, - fundeeAccountId: maintainer.accountId, - fundeeAccountAddress: getUserAddress(maintainer.accountId), - type: AddressDriverSplitReceiverType.ProjectMaintainer, - blockTimestamp, + return createAddressReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: maintainer as z.infer< + typeof addressDriverSplitReceiverSchema + >, + funder: { + type: 'project', + accountId: funderProjectId, + dependencyType: 'maintainer', }, - { transaction }, - ); + }); }); const dependencyPromises = dependencies.map(async (dependency) => { if (isRepoDriverId(dependency.accountId)) { - assertDependencyOfProjectType(dependency); - - return createDbEntriesForProjectDependency( - { + return createProjectAndProjectReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: dependency as z.infer< + typeof repoDriverSplitReceiverSchema + >, // Safe to cast because we already checked the type of accountId. + funder: { type: 'project', accountId: funderProjectId, }, - dependency, - transaction, - blockTimestamp, - ); + }); } if (isAddressDriverId(dependency.accountId)) { - return AddressDriverSplitReceiverModel.create( - { - funderProjectId, - weight: dependency.weight, - fundeeAccountId: dependency.accountId, - fundeeAccountAddress: getUserAddress(dependency.accountId), - type: AddressDriverSplitReceiverType.ProjectDependency, - blockTimestamp, + return createAddressReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: dependency as z.infer< + typeof addressDriverSplitReceiverSchema + >, // Safe to cast because we already checked the type of accountId. + funder: { + type: 'project', + accountId: funderProjectId, + dependencyType: 'dependency', }, - { transaction }, - ); + }); } if (isNftDriverId(dependency.accountId)) { @@ -204,7 +254,7 @@ async function createDbEntriesForProjectSplits( } return unreachableError( - `Dependency with account ID ${dependency.accountId} is not an Address nor a Git Project.`, + `Dependency with account ID ${dependency.accountId} is not an Address nor a Project.`, ); }); @@ -222,27 +272,3 @@ async function createDbEntriesForProjectSplits( `, ); } - -async function clearCurrentProjectSplits( - funderProjectId: string, - transaction: Transaction, -) { - await AddressDriverSplitReceiverModel.destroy({ - where: { - funderProjectId, - }, - transaction, - }); - await RepoDriverSplitReceiverModel.destroy({ - where: { - funderProjectId, - }, - transaction, - }); - await DripListSplitReceiverModel.destroy({ - where: { - funderProjectId, - }, - transaction, - }); -} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts new file mode 100644 index 0000000..b5ab8c3 --- /dev/null +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -0,0 +1,228 @@ +/* eslint-disable no-param-reassign */ +import type { Transaction } from 'sequelize'; +import type { z } from 'zod'; +import LogManager from '../../../core/LogManager'; +import type { ImmutableSplitsDriverId, IpfsHash } from '../../../core/types'; +import { + AccountMetadataEmittedEventModel, + SubListModel, +} from '../../../models'; +import { + toNftDriverId, + toImmutableSplitsDriverId, +} from '../../../utils/accountIdUtils'; +import { getImmutableSpitsDriverMetadata } from '../../../utils/metadataUtils'; +import unreachableError from '../../../utils/unreachableError'; +import verifySplitsReceivers from '../verifySplitsReceivers'; +import { isLatestEvent } from '../../../utils/eventUtils'; +import verifyProjectSources from '../projectVerification'; +import type { + addressDriverSplitReceiverSchema, + repoDriverSplitReceiverSchema, +} from '../../../metadata/schemas/repo-driver/v2'; +import type { subListSplitReceiverSchema } from '../../../metadata/schemas/sub-list/v1'; +import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; +import { + createProjectAndProjectReceiver, + createSubListReceiver, + createDripListReceiver, + deleteExistingReceivers, + createAddressReceiver, +} from '../receiversRepository'; + +type Params = { + ipfsHash: IpfsHash; + blockTimestamp: Date; + logManager: LogManager; + subListId: ImmutableSplitsDriverId; + transaction: Transaction; + originEventDetails: { + entity: AccountMetadataEmittedEventModel; + logIndex: number; + transactionHash: string; + }; +}; + +export default async function handleSubListMetadata({ + ipfsHash, + logManager, + subListId, + transaction, + blockTimestamp, + originEventDetails: { entity, logIndex, transactionHash }, +}: Params) { + // Only process metadata if this is the latest event. + if ( + !(await isLatestEvent( + entity, + AccountMetadataEmittedEventModel, + { + logIndex, + transactionHash, + accountId: subListId, + }, + transaction, + )) + ) { + logManager.logAllInfo(); + + return; + } + logManager.appendIsLatestEventLog(); + + const metadata = await getImmutableSpitsDriverMetadata(ipfsHash); + + // Here, is the only place an Ecosystem is created. + const subList = await SubListModel.create( + { + id: subListId, + parentDripListId: + metadata.parent.type === 'drip-list' + ? toNftDriverId(metadata.parent.accountId) + : null, + parentEcosystemId: + metadata.parent.type === 'ecosystem' + ? toNftDriverId(metadata.parent.accountId) + : null, + parentSubListId: + metadata.parent.type === 'sub-list' + ? toImmutableSplitsDriverId(metadata.parent.accountId) + : null, + rootDripListId: + metadata.root.type === 'drip-list' + ? toNftDriverId(metadata.root.accountId) + : null, + rootEcosystemId: + metadata.root.type === 'ecosystem' + ? toNftDriverId(metadata.root.accountId) + : null, + lastProcessedIpfsHash: ipfsHash, + }, + { transaction }, + ); + + logManager.appendFindOrCreateLog(SubListModel, true, subList.id).logAllInfo(); + + const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = + await verifySplitsReceivers(subList.id, metadata.recipients); + + if (!areSplitsValid) { + logManager.appendLog( + [ + `Skipping metadata update for Sub List ${subListId} due to mismatch in splits hash.`, + ` On-chain hash: ${onChainSplitsHash}`, + ` Metadata hash: ${calculatedSplitsHash}`, + ` Possible causes:`, + ` - The metadata event is the latest in the DB, but not on-chain.`, + ` - The metadata was manually emitted with outdated or mismatched splits.`, + ].join('\n'), + ); + + return; + } + + await verifyProjectSources(metadata.recipients); + + await deleteExistingReceivers({ + for: { + accountId: subListId, + column: 'funderSubListId', + }, + transaction, + }); + + await setNewReceivers({ + logManager, + transaction, + blockTimestamp, + funderSubListId: subListId, + receivers: metadata.recipients, + }); +} + +async function setNewReceivers({ + receivers, + logManager, + transaction, + blockTimestamp, + funderSubListId, +}: { + blockTimestamp: Date; + logManager: LogManager; + transaction: Transaction; + funderSubListId: ImmutableSplitsDriverId; + receivers: ( + | z.infer + | z.infer + | z.infer + | z.infer + )[]; +}) { + const receiverPromises = receivers.map(async (receiver) => { + switch (receiver.type) { + case 'repoDriver': + return createProjectAndProjectReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: receiver, + funder: { + type: 'sub-list', + accountId: funderSubListId, + }, + }); + + case 'subList': + return createSubListReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: receiver, + funder: { + type: 'sub-list', + accountId: funderSubListId, + }, + }); + + case 'dripList': + return createDripListReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: receiver, + funder: { + type: 'sub-list', + accountId: funderSubListId, + }, + }); + + case 'address': + return createAddressReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: receiver, + funder: { + type: 'sub-list', + accountId: funderSubListId, + }, + }); + + default: + return unreachableError( + `Unhandled receiver type: ${(receiver as any).type}`, + ); + } + }); + + const result = await Promise.all(receiverPromises); + + logManager.appendLog( + `Updated ${LogManager.nameOfType( + SubListModel, + )} with ID ${funderSubListId} splits: ${result + .map((p) => JSON.stringify(p)) + .join(`, `)} + `, + ); +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts b/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts new file mode 100644 index 0000000..c590805 --- /dev/null +++ b/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts @@ -0,0 +1,79 @@ +import type { z } from 'zod'; +import type { AnyVersion } from '@efstajas/versioned-parser'; +import { repoDriverContract } from '../../core/contractClients'; +import type { dripListSplitReceiverSchema } from '../../metadata/schemas/nft-driver/v2'; +import type { + repoDriverSplitReceiverSchema, + addressDriverSplitReceiverSchema, +} from '../../metadata/schemas/repo-driver/v2'; +import type { subListSplitReceiverSchema } from '../../metadata/schemas/sub-list/v1'; +import type { repoDriverAccountMetadataParser } from '../../metadata/schemas'; +import type { GitProjectModel } from '../../models'; +import unreachableError from '../../utils/unreachableError'; + +export default async function verifyProjectSources( + recipients: ( + | z.infer + | z.infer + | z.infer + | z.infer + )[], +) { + for (const r of recipients) { + if (r.type === 'repoDriver') { + const accountId = await repoDriverContract.calcAccountId( + r.source.forge, + `${r.source.ownerName}/${r.source.repoName}`, + ); + + if (accountId.toString() !== r.accountId) { + throw new Error( + `Calculated project accountId '${accountId}' does not match the one in metadata ('${r.accountId}') for repo '${r.source.ownerName}/${r.source.repoName}' on '${r.source.forge}'.`, + ); + } + } + } +} + +export async function verifyProjectMetadata( + project: GitProjectModel, + metadata: AnyVersion, +): Promise { + if (!metadata) { + unreachableError(`Metadata for Git Project with ID ${project.id} is null.`); + } + + const errors = []; + + const { describes, source } = metadata; + const { + url: metaUrl, + repoName: metaRepoName, + ownerName: metaOwnerName, + } = source; + const { id: onChainProjectId, name: onChainProjectName } = project; + + if (`${metaOwnerName}/${metaRepoName}` !== `${onChainProjectName}`) { + errors.push( + `repoName mismatch: got ${metaOwnerName}/${metaRepoName}, expected ${onChainProjectName}.`, + ); + } + + if (metaUrl !== project.url) { + errors.push(`url mismatch: got ${metaUrl}, expected ${project.url}.`); + } + + if (describes.accountId !== onChainProjectId) { + errors.push( + `accountId mismatch with: got ${describes.accountId}, expected ${onChainProjectId}.`, + ); + } + + if (errors.length > 0) { + throw new Error( + `Git Project with ID ${onChainProjectId} has metadata that does not match the metadata emitted by the contract (${errors.join( + '; ', + )}).`, + ); + } +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts new file mode 100644 index 0000000..1f5c641 --- /dev/null +++ b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts @@ -0,0 +1,385 @@ +import { Op, type Transaction } from 'sequelize'; +import type { z } from 'zod'; +import { DependencyType } from '../../core/types'; +import type { + AccountId, + ImmutableSplitsDriverId, + NftDriverId, + RepoDriverId, +} from '../../core/types'; +import { + AddressDriverSplitReceiverModel, + DripListSplitReceiverModel, + GitProjectModel, + RepoDriverSplitReceiverModel, + SubListSplitReceiverModel, +} from '../../models'; +import type LogManager from '../../core/LogManager'; +import unreachableError from '../../utils/unreachableError'; +import type { + addressDriverSplitReceiverSchema, + repoDriverSplitReceiverSchema, +} from '../../metadata/schemas/repo-driver/v2'; +import { + toAddressDriverId, + toImmutableSplitsDriverId, + toNftDriverId, + toRepoDriverId, +} from '../../utils/accountIdUtils'; +import getUserAddress from '../../utils/getAccountAddress'; +import { AddressDriverSplitReceiverType } from '../../models/AddressDriverSplitReceiverModel'; +import type { dripListSplitReceiverSchema } from '../../metadata/schemas/nft-driver/v2'; +import type { subListSplitReceiverSchema } from '../../metadata/schemas/sub-list/v1'; +import { FORGES_MAP } from '../../core/constants'; +import { ProjectVerificationStatus } from '../../models/GitProjectModel'; + +export async function createAddressReceiver({ + blockTimestamp, + funder, + metadataReceiver, + logManager, + transaction, +}: { + funder: + | { + type: 'project'; + accountId: RepoDriverId; + dependencyType: 'dependency' | 'maintainer'; + } + | { type: 'dripList'; accountId: NftDriverId } + | { type: 'ecosystem'; accountId: NftDriverId } + | { type: 'sub-list'; accountId: ImmutableSplitsDriverId }; + metadataReceiver: z.infer; + transaction: Transaction; + blockTimestamp: Date; + logManager: LogManager; +}) { + const { weight, accountId: fundeeAccountId } = metadataReceiver; + + const accountId = toAddressDriverId(fundeeAccountId); + + let type: AddressDriverSplitReceiverType; + if (funder.type === 'project') { + type = + funder.dependencyType === 'dependency' + ? AddressDriverSplitReceiverType.ProjectDependency + : AddressDriverSplitReceiverType.ProjectMaintainer; + } else if (funder.type === 'dripList') { + type = AddressDriverSplitReceiverType.DripListDependency; + } else { + // TODO: For now we treat both ecosystem and sub-list cases as EcosystemDependency. Type will shortly be removed either way. + type = AddressDriverSplitReceiverType.EcosystemDependency; + } + + // Create the receiver. + const [receiver, isReceiverCreated] = + await AddressDriverSplitReceiverModel.findOrCreate({ + lock: true, + transaction, + where: { + weight, + fundeeAccountId: accountId, + fundeeAccountAddress: getUserAddress(accountId), + type, + funderProjectId: funder.type === 'project' ? funder.accountId : null, + funderDripListId: funder.type === 'dripList' ? funder.accountId : null, + }, + defaults: { + blockTimestamp, + weight, + fundeeAccountId: accountId, + fundeeAccountAddress: getUserAddress(accountId), + type, + funderProjectId: funder.type === 'project' ? funder.accountId : null, + funderDripListId: funder.type === 'dripList' ? funder.accountId : null, + }, + }); + + if (!isReceiverCreated) { + unreachableError( + `Sub List receiver ${receiver.id} already exists for sub-list ${accountId}. This means the receiver was created outside the expected flow.`, + ); + } + + logManager.appendFindOrCreateLog( + AddressDriverSplitReceiverModel, + isReceiverCreated, + receiver.id.toString(), + ); +} + +export async function createDripListReceiver({ + blockTimestamp, + funder, + metadataReceiver, + logManager, + transaction, +}: { + funder: + | { type: 'project'; accountId: RepoDriverId } + | { type: 'dripList'; accountId: NftDriverId } + | { type: 'ecosystem'; accountId: NftDriverId } + | { type: 'sub-list'; accountId: ImmutableSplitsDriverId }; + metadataReceiver: z.infer; + transaction: Transaction; + blockTimestamp: Date; + logManager: LogManager; +}) { + const { weight, accountId: fundeeDripListId } = metadataReceiver; + + const dripListId = toNftDriverId(fundeeDripListId); + + // Create the receiver. + const [receiver, isReceiverCreated] = + await DripListSplitReceiverModel.findOrCreate({ + lock: true, + transaction, + where: { + weight, + type: DependencyType.DripListDependency, + fundeeDripListId: dripListId, + funderProjectId: funder.type === 'project' ? funder.accountId : null, + funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, + funderDripListId: funder.type === 'dripList' ? funder.accountId : null, + funderEcosystemId: + funder.type === 'ecosystem' ? funder.accountId : null, + }, + defaults: { + blockTimestamp, + weight, + type: DependencyType.DripListDependency, + fundeeDripListId: dripListId, + funderProjectId: funder.type === 'project' ? funder.accountId : null, + funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, + funderDripListId: funder.type === 'dripList' ? funder.accountId : null, + funderEcosystemId: + funder.type === 'ecosystem' ? funder.accountId : null, + }, + }); + + if (!isReceiverCreated) { + unreachableError( + `Drip List receiver ${receiver.id} already exists for sub-list ${dripListId}. This means the receiver was created outside the expected flow.`, + ); + } + + logManager.appendFindOrCreateLog( + DripListSplitReceiverModel, + isReceiverCreated, + receiver.id.toString(), + ); +} + +export async function createSubListReceiver({ + blockTimestamp, + funder, + metadataReceiver, + logManager, + transaction, +}: { + funder: + | { type: 'project'; accountId: RepoDriverId } + | { type: 'dripList'; accountId: NftDriverId } + | { type: 'ecosystem'; accountId: NftDriverId } + | { type: 'sub-list'; accountId: ImmutableSplitsDriverId }; + metadataReceiver: z.infer; + transaction: Transaction; + blockTimestamp: Date; + logManager: LogManager; +}) { + const { weight, accountId: fundeeSubListId } = metadataReceiver; + + const subListId = toImmutableSplitsDriverId(fundeeSubListId); + + // Create the receiver. + const [receiver, isReceiverCreated] = + await SubListSplitReceiverModel.findOrCreate({ + lock: true, + transaction, + where: { + weight, + type: DependencyType.EcosystemDependency, + fundeeSubListId: subListId, + funderProjectId: funder.type === 'project' ? funder.accountId : null, + funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, + funderDripListId: funder.type === 'dripList' ? funder.accountId : null, + funderEcosystemId: + funder.type === 'ecosystem' ? funder.accountId : null, + }, + defaults: { + weight, + blockTimestamp, + type: DependencyType.EcosystemDependency, + fundeeSubListId: subListId, + funderProjectId: funder.type === 'project' ? funder.accountId : null, + funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, + funderDripListId: funder.type === 'dripList' ? funder.accountId : null, + funderEcosystemId: + funder.type === 'ecosystem' ? funder.accountId : null, + }, + }); + + if (!isReceiverCreated) { + unreachableError( + `Sub List receiver ${receiver.id} already exists for sub-list ${subListId}. This means the receiver was created outside the expected flow.`, + ); + } + + logManager.appendFindOrCreateLog( + SubListSplitReceiverModel, + isReceiverCreated, + receiver.id.toString(), + ); +} + +export async function createProjectAndProjectReceiver({ + blockTimestamp, + funder, + metadataReceiver, + logManager, + transaction, +}: { + funder: + | { type: 'project'; accountId: RepoDriverId } + | { type: 'dripList'; accountId: NftDriverId } + | { type: 'ecosystem'; accountId: NftDriverId } + | { type: 'sub-list'; accountId: ImmutableSplitsDriverId }; + metadataReceiver: z.infer; + transaction: Transaction; + blockTimestamp: Date; + logManager: LogManager; +}) { + const { + weight, + accountId: fundeeProjectId, + source: { forge, ownerName, repoName, url }, + } = metadataReceiver; + + const projectId = toRepoDriverId(fundeeProjectId); + + // Create the project the receiver represents. + const [project, isProjectCreated] = await GitProjectModel.findOrCreate({ + lock: true, + transaction, + where: { + id: projectId, + }, + defaults: { + url, + isVisible: true, // During creation, the project is visible by default. Account metadata will set the final visibility. + isValid: true, // There are no receivers yet, so the project is valid. + id: projectId, + name: `${ownerName}/${repoName}`, + verificationStatus: ProjectVerificationStatus.Unclaimed, + forge: + Object.values(FORGES_MAP).find( + (f) => f.toLocaleLowerCase() === forge.toLowerCase(), + ) ?? unreachableError(), + }, + }); + + logManager + .appendFindOrCreateLog(GitProjectModel, isProjectCreated, project.id) + .logAllInfo(); + + let type: DependencyType; + if (funder.type === 'project') { + type = DependencyType.ProjectDependency; + } else if (funder.type === 'dripList') { + type = DependencyType.DripListDependency; + } else { + // TODO: For now we treat both ecosystem and sub-list cases as EcosystemDependency. Type will shortly be removed either way. + type = DependencyType.EcosystemDependency; + } + + // Create the receiver. + const [receiver, isReceiverCreated] = + await RepoDriverSplitReceiverModel.findOrCreate({ + lock: true, + transaction, + where: { + weight, + type, + fundeeProjectId: projectId, + funderProjectId: funder.type === 'project' ? funder.accountId : null, + funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, + funderDripListId: funder.type === 'dripList' ? funder.accountId : null, + funderEcosystemId: + funder.type === 'ecosystem' ? funder.accountId : null, + }, + defaults: { + weight, + type, + blockTimestamp, + fundeeProjectId: projectId, + funderProjectId: funder.type === 'project' ? funder.accountId : null, + funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, + funderDripListId: funder.type === 'dripList' ? funder.accountId : null, + funderEcosystemId: + funder.type === 'ecosystem' ? funder.accountId : null, + }, + }); + + if (!isReceiverCreated) { + unreachableError( + `Project receiver ${receiver.id} already exists for project ${projectId}. This means the receiver was created outside the expected flow.`, + ); + } + + logManager.appendFindOrCreateLog( + RepoDriverSplitReceiverModel, + isReceiverCreated, + receiver.id.toString(), + ); +} + +type ClearReceiversInput = + | { accountId: RepoDriverId; column: 'funderProjectId' } + | { accountId: NftDriverId; column: 'funderDripListId' } + | { accountId: NftDriverId; column: 'funderEcosystemId' } + | { accountId: ImmutableSplitsDriverId; column: 'funderSubListId' }; + +type ClearReceiversParams = { + for: ClearReceiversInput; + transaction: Transaction; + excludeReceivers?: AccountId[]; +}; + +export async function deleteExistingReceivers({ + transaction, + for: { accountId, column }, + excludeReceivers = [], +}: ClearReceiversParams): Promise { + const baseWhere = { [column]: accountId }; + + const whereWithExclusions = + excludeReceivers.length > 0 + ? { + ...baseWhere, + funderProjectId: { [Op.notIn]: excludeReceivers }, + funderSubListId: { [Op.notIn]: excludeReceivers }, + funderDripListId: { [Op.notIn]: excludeReceivers }, + funderEcosystemId: { [Op.notIn]: excludeReceivers }, + } + : baseWhere; + + await AddressDriverSplitReceiverModel.destroy({ + where: whereWithExclusions, + transaction, + }); + + await RepoDriverSplitReceiverModel.destroy({ + where: whereWithExclusions, + transaction, + }); + + await DripListSplitReceiverModel.destroy({ + where: whereWithExclusions, + transaction, + }); + + await SubListSplitReceiverModel.destroy({ + where: whereWithExclusions, + transaction, + }); +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts b/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts deleted file mode 100644 index 25e15f0..0000000 --- a/src/eventHandlers/AccountMetadataEmittedEvent/splitsValidator.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { SplitsReceiverStruct } from '../../../contracts/CURRENT_NETWORK/Drips'; -import type { - RepoDriverId, - NftDriverId, - ImmutableSplitsDriverId, -} from '../../core/types'; -import { dripsContract } from '../../core/contractClients'; - -export default async function validateSplitsReceivers( - accountId: RepoDriverId | NftDriverId | ImmutableSplitsDriverId, - splits: SplitsReceiverStruct[], -): Promise< - [ - areSplitsValid: boolean, - onChainSplitsHash: string, - calculatedSplitsHash: string, - ] -> { - const formattedSplits = formatSplitReceivers(splits); - const calculatedSplitsHash = await dripsContract.hashSplits(formattedSplits); - const onChainSplitsHash = await dripsContract.splitsHash(accountId); - - if (calculatedSplitsHash !== onChainSplitsHash) { - return [false, onChainSplitsHash, calculatedSplitsHash]; - } - - return [true, onChainSplitsHash, calculatedSplitsHash]; -} - -export function formatSplitReceivers( - receivers: SplitsReceiverStruct[], -): SplitsReceiverStruct[] { - // Splits receivers must be sorted by user ID, deduplicated, and without weights <= 0. - - const uniqueReceivers = receivers.reduce( - (unique: SplitsReceiverStruct[], o) => { - if ( - !unique.some( - (obj: SplitsReceiverStruct) => - obj.accountId === o.accountId && obj.weight === o.weight, - ) - ) { - unique.push(o); - } - return unique; - }, - [], - ); - - const sortedReceivers = uniqueReceivers.sort((a, b) => - // Sort by user ID. - // eslint-disable-next-line no-nested-ternary - BigInt(a.accountId) > BigInt(b.accountId) - ? 1 - : BigInt(a.accountId) < BigInt(b.accountId) - ? -1 - : 0, - ); - - return sortedReceivers; -} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts b/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts new file mode 100644 index 0000000..e219c38 --- /dev/null +++ b/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts @@ -0,0 +1,29 @@ +import type { + RepoDriverId, + NftDriverId, + ImmutableSplitsDriverId, +} from '../../core/types'; +import { dripsContract } from '../../core/contractClients'; +import { formatSplitReceivers } from '../../utils/formatSplitReceivers'; +import type { SplitsReceiverStruct } from '../../../contracts/CURRENT_NETWORK/Drips'; + +export default async function verifySplitsReceivers( + accountId: RepoDriverId | NftDriverId | ImmutableSplitsDriverId, + splits: SplitsReceiverStruct[], +): Promise< + [ + areSplitsValid: boolean, + onChainSplitsHash: string, + calculatedSplitsHash: string, + ] +> { + const formattedSplits = formatSplitReceivers(splits); + const calculatedSplitsHash = await dripsContract.hashSplits(formattedSplits); + const onChainSplitsHash = await dripsContract.splitsHash(accountId); + + if (calculatedSplitsHash !== onChainSplitsHash) { + return [false, onChainSplitsHash, calculatedSplitsHash]; + } + + return [true, onChainSplitsHash, calculatedSplitsHash]; +} diff --git a/src/eventHandlers/CreatedSplitsEventHandler.ts b/src/eventHandlers/CreatedSplitsEventHandler.ts deleted file mode 100644 index 8d9ebb1..0000000 --- a/src/eventHandlers/CreatedSplitsEventHandler.ts +++ /dev/null @@ -1,90 +0,0 @@ -import EventHandlerBase from '../events/EventHandlerBase'; -import LogManager from '../core/LogManager'; -import { - toAccountId, - toImmutableSplitsDriverId, -} from '../utils/accountIdUtils'; -import type EventHandlerRequest from '../events/EventHandlerRequest'; -import { dbConnection } from '../db/database'; -import type { CreatedSplitsEvent } from '../../contracts/CURRENT_NETWORK/ImmutableSplitsDriver'; -import CreatedSplitsEventModel from '../models/CreatedSplitsEventModel'; -import SubListModel from '../models/SubListModel'; -import unreachableError from '../utils/unreachableError'; - -export default class SplitEventHandler extends EventHandlerBase<'CreatedSplits(uint256,bytes32)'> { - public eventSignatures = ['CreatedSplits(uint256,bytes32)' as const]; - - protected async _handle( - request: EventHandlerRequest<'CreatedSplits(uint256,bytes32)'>, - ): Promise { - const { - id: requestId, - event: { args, logIndex, blockNumber, blockTimestamp, transactionHash }, - } = request; - - const [rawAccountId, rawReceiversHash] = - args as CreatedSplitsEvent.OutputTuple; - - const accountId = toAccountId(rawAccountId); - - LogManager.logRequestInfo( - `📥 ${this.name} is processing the following ${request.event.eventSignature}: - \r\t - accountId: ${rawAccountId} - \r\t - receiversHash: ${rawReceiversHash} - \r\t - logIndex: ${logIndex} - \r\t - tx hash: ${transactionHash}`, - requestId, - ); - - await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - - const [createdSplitsEvent, isEventCreated] = - await CreatedSplitsEventModel.findOrCreate({ - lock: true, - transaction, - where: { - logIndex, - transactionHash, - }, - defaults: { - accountId, - receiversHash: rawReceiversHash, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - }); - - logManager.appendFindOrCreateLog( - CreatedSplitsEventModel, - isEventCreated, - `${createdSplitsEvent.transactionHash}-${createdSplitsEvent.logIndex}`, - ); - - // This must be the only place a `SubList` is created. - const [subList, isSubListCreated] = await SubListModel.findOrCreate({ - transaction, - lock: true, - where: { - id: accountId, - }, - defaults: { - id: toImmutableSplitsDriverId(rawAccountId), - }, - }); - - if (!isSubListCreated) { - // The `SubList` with the given `accountId` was created by another `CreatedSplits` event. - unreachableError(`SubList with id ${accountId} already exists.`); - } - - logManager - .appendFindOrCreateLog(SubListModel, isSubListCreated, subList.id) - .logAllInfo(); - - logManager.logAllInfo(); - }); - } -} diff --git a/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts index 1a6a35d..e89bd08 100644 --- a/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts @@ -14,7 +14,7 @@ import type { SplitsReceiverStruct } from '../../../contracts/CURRENT_NETWORK/Dr import unreachableError from '../../utils/unreachableError'; import { dripsContract } from '../../core/contractClients'; import type LogManager from '../../core/LogManager'; -import { formatSplitReceivers } from '../AccountMetadataEmittedEvent/splitsValidator'; +import { formatSplitReceivers } from '../../utils/formatSplitReceivers'; export default async function setIsValidFlag( splitsSetEvent: SplitsSetEventModel, @@ -120,7 +120,7 @@ export default async function setIsValidFlag( \r\t - 'SetSplits' event splits hash: ${receiversHash} \r\t - DB (populated from metadata) splits hash: ${storedInDbFromMetaReceiversHash} \r Possible reasons: - \r\t - The 'AccountMetadataEmitted' event that should have created the Drip List splits was not processed yet. + \r\t - The 'AccountMetadataEmitted' event that should have created the splits was not processed yet. \r\t - The 'SetSplits' event (was manually emitted?) had splits that do not match what's already stored in the DB (from metadata).`, ); } else if (entity.isValid === false) { @@ -251,7 +251,7 @@ async function getEcosystemDbReceivers(accountId: NftDriverId) { }, }).then((receivers) => receivers.map((receiver) => ({ - accountId: receiver.fundeeImmutableSplitsId ?? unreachableError(), + accountId: receiver.fundeeSubListId ?? unreachableError(), weight: receiver.weight, })), ); diff --git a/src/eventHandlers/index.ts b/src/eventHandlers/index.ts index 7cc9b22..24cd1f3 100644 --- a/src/eventHandlers/index.ts +++ b/src/eventHandlers/index.ts @@ -3,7 +3,6 @@ export { default as SplitEventHandler } from './SplitEventHandler'; export { default as TransferEventHandler } from './TransferEventHandler'; export { default as StreamsSetEventHandler } from './StreamsSetEventHandler'; export { default as OwnerUpdatedEventHandler } from './OwnerUpdatedEventHandler'; -export { default as CreatedSplitsEventHandler } from './CreatedSplitsEventHandler'; export { default as SqueezedStreamsEventHandler } from './SqueezedStreamsEventHandler'; export { default as StreamReceiverSeenEventHandler } from './StreamReceiverSeenEventHandler'; export { default as SplitsSetEventHandler } from './SplitsSetEventHandler/SplitsSetEventHandler'; diff --git a/src/events/registrations.ts b/src/events/registrations.ts index 9a1727f..ab36fcf 100644 --- a/src/events/registrations.ts +++ b/src/events/registrations.ts @@ -9,7 +9,6 @@ import { StreamReceiverSeenEventHandler, StreamsSetEventHandler, SqueezedStreamsEventHandler, - CreatedSplitsEventHandler, } from '../eventHandlers'; import { registerEventHandler } from './eventHandlerUtils'; @@ -61,8 +60,4 @@ export function registerEventHandlers(): void { 'SqueezedStreams(uint256,address,uint256,uint128,bytes32[])', SqueezedStreamsEventHandler, ); - registerEventHandler<'CreatedSplits(uint256,bytes32)'>( - 'CreatedSplits(uint256,bytes32)', - CreatedSplitsEventHandler, - ); } diff --git a/src/index.ts b/src/index.ts index f7faa42..3a5f590 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import express from 'express'; import logger from './core/logger'; + import appSettings from './config/appSettings'; import initJobProcessingQueue from './queue/initJobProcessingQueue'; import { arenaConfig } from './queue/queueMonitoring'; @@ -21,7 +22,10 @@ import networkConstant from '../contracts/CURRENT_NETWORK/network-constant'; import { healthEndpoint } from './health'; process.on('uncaughtException', (error: Error) => { - logger.error(`Uncaught Exception: ${error.message}. Stack: ${error.stack}`); + logger.error('Uncaught Exception', { + message: error.message, + stack: error.stack, + }); // Railway will restart the process if it exits with a non-zero exit code. process.exit(1); @@ -39,6 +43,7 @@ async function init() { } logger.info('Starting the application...'); + logger.info(`App Settings: ${JSON.stringify(appSettings, null, 2)}`); await connectToDb(); await initJobProcessingQueue(); @@ -68,6 +73,7 @@ async function init() { ], getHandlers(), getProvider(), + startBlock, ); diff --git a/src/metadata/schemas/sub-list/v1.ts b/src/metadata/schemas/sub-list/v1.ts index 6056015..8f8f6f6 100644 --- a/src/metadata/schemas/sub-list/v1.ts +++ b/src/metadata/schemas/sub-list/v1.ts @@ -23,7 +23,21 @@ export const subListMetadataSchemaV1 = z.object({ ]), ), parent: z.object({ - driver: z.literal('nft'), accountId: z.string(), + driver: z.union([z.literal('nft'), z.literal('immutable-splits')]), + type: z.union([ + z.literal('drip-list'), + z.literal('ecosystem'), + z.literal('sub-list'), + ]), + }), + root: z.object({ + accountId: z.string(), + driver: z.union([z.literal('nft'), z.literal('immutable-splits')]), + type: z.union([ + z.literal('drip-list'), + z.literal('ecosystem'), + z.literal('sub-list'), + ]), }), }); diff --git a/src/models/AddressDriverSplitReceiverModel.ts b/src/models/AddressDriverSplitReceiverModel.ts index 82b4ce1..4e7362c 100644 --- a/src/models/AddressDriverSplitReceiverModel.ts +++ b/src/models/AddressDriverSplitReceiverModel.ts @@ -8,9 +8,15 @@ import { DataTypes, Model } from 'sequelize'; import type { AddressLike } from 'ethers'; import getSchema from '../utils/getSchema'; import GitProjectModel from './GitProjectModel'; -import type { AddressDriverId, NftDriverId, RepoDriverId } from '../core/types'; +import type { + AddressDriverId, + ImmutableSplitsDriverId, + NftDriverId, + RepoDriverId, +} from '../core/types'; import DripListModel from './DripListModel'; import EcosystemModel from './EcosystemModel'; +import SubListModel from './SubListModel'; export enum AddressDriverSplitReceiverType { ProjectMaintainer = 'ProjectMaintainer', @@ -24,14 +30,15 @@ export default class AddressDriverSplitReceiverModel extends Model< InferCreationAttributes > { public declare id: CreationOptional; // Primary key + public declare fundeeAccountId: AddressDriverId; + public declare fundeeAccountAddress: AddressLike; public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key public declare funderEcosystemId: NftDriverId | null; // Foreign key + public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key public declare weight: number; public declare type: AddressDriverSplitReceiverType; - public declare fundeeAccountId: AddressDriverId; - public declare fundeeAccountAddress: AddressLike; public declare blockTimestamp: Date; public static initialize(sequelize: Sequelize): void { @@ -77,6 +84,15 @@ export default class AddressDriverSplitReceiverModel extends Model< }, allowNull: true, }, + funderSubListId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: SubListModel, + key: 'id', + }, + allowNull: true, + }, weight: { type: DataTypes.INTEGER, allowNull: true, diff --git a/src/models/CreatedSplitsEventModel.ts b/src/models/CreatedSplitsEventModel.ts deleted file mode 100644 index 5a6f48c..0000000 --- a/src/models/CreatedSplitsEventModel.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { - InferAttributes, - InferCreationAttributes, - Sequelize, -} from 'sequelize'; -import { DataTypes, Model } from 'sequelize'; -import type { AccountId } from '../core/types'; -import getSchema from '../utils/getSchema'; -import { COMMON_EVENT_INIT_ATTRIBUTES } from '../core/constants'; -import type { IEventModel } from '../events/types'; - -export default class CreatedSplitsEventModel - extends Model< - InferAttributes, - InferCreationAttributes - > - implements IEventModel -{ - public declare accountId: AccountId; - public declare receiversHash: string; - - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; - - public static initialize(sequelize: Sequelize): void { - this.init( - { - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - receiversHash: { - type: DataTypes.STRING, - allowNull: false, - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - { - sequelize, - schema: getSchema(), - tableName: 'CreatedSplitsEvents', - }, - ); - } -} diff --git a/src/models/DripListModel.ts b/src/models/DripListModel.ts index 78a38e0..6d2be68 100644 --- a/src/models/DripListModel.ts +++ b/src/models/DripListModel.ts @@ -13,7 +13,7 @@ export default class DripListModel extends Model< InferAttributes, InferCreationAttributes > { - public declare id: NftDriverId; // The `tokenId` from `TransferEvent` event. + public declare id: NftDriverId; public declare isValid: boolean; public declare name: string | null; public declare creator: AddressLike | null; diff --git a/src/models/DripListSplitReceiverModel.ts b/src/models/DripListSplitReceiverModel.ts index 6141584..101e559 100644 --- a/src/models/DripListSplitReceiverModel.ts +++ b/src/models/DripListSplitReceiverModel.ts @@ -7,10 +7,15 @@ import type { import { DataTypes, Model } from 'sequelize'; import getSchema from '../utils/getSchema'; import { DependencyType } from '../core/types'; -import type { NftDriverId, RepoDriverId } from '../core/types'; +import type { + ImmutableSplitsDriverId, + NftDriverId, + RepoDriverId, +} from '../core/types'; import DripListModel from './DripListModel'; import GitProjectModel from './GitProjectModel'; import EcosystemModel from './EcosystemModel'; +import SubListModel from './SubListModel'; export default class DripListSplitReceiverModel extends Model< InferAttributes, @@ -21,6 +26,7 @@ export default class DripListSplitReceiverModel extends Model< public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key public declare funderEcosystemId: NftDriverId | null; // Foreign key + public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key public declare weight: number; public declare type: DependencyType; @@ -70,6 +76,15 @@ export default class DripListSplitReceiverModel extends Model< }, allowNull: true, }, + funderSubListId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: SubListModel, + key: 'id', + }, + allowNull: true, + }, weight: { type: DataTypes.INTEGER, allowNull: true, diff --git a/src/models/RepoDriverSplitReceiverModel.ts b/src/models/RepoDriverSplitReceiverModel.ts index fbe4da9..edee010 100644 --- a/src/models/RepoDriverSplitReceiverModel.ts +++ b/src/models/RepoDriverSplitReceiverModel.ts @@ -8,9 +8,14 @@ import { DataTypes, Model } from 'sequelize'; import getSchema from '../utils/getSchema'; import GitProjectModel from './GitProjectModel'; import { DependencyType } from '../core/types'; -import type { NftDriverId, RepoDriverId } from '../core/types'; +import type { + ImmutableSplitsDriverId, + NftDriverId, + RepoDriverId, +} from '../core/types'; import DripListModel from './DripListModel'; import EcosystemModel from './EcosystemModel'; +import SubListModel from './SubListModel'; export default class RepoDriverSplitReceiverModel extends Model< InferAttributes, @@ -21,6 +26,7 @@ export default class RepoDriverSplitReceiverModel extends Model< public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key public declare funderEcosystemId: NftDriverId | null; // Foreign key + public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key public declare weight: number; public declare type: DependencyType; @@ -70,6 +76,15 @@ export default class RepoDriverSplitReceiverModel extends Model< }, allowNull: true, }, + funderSubListId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: SubListModel, + key: 'id', + }, + allowNull: true, + }, weight: { type: DataTypes.INTEGER, allowNull: true, diff --git a/src/models/SubListModel.ts b/src/models/SubListModel.ts index f6cbb96..837d4a9 100644 --- a/src/models/SubListModel.ts +++ b/src/models/SubListModel.ts @@ -11,11 +11,12 @@ export default class SubListModel extends Model< InferAttributes, InferCreationAttributes > { - // Populated from `CreatedSplits` event: public declare id: ImmutableSplitsDriverId; - - // Populated from `AccountMetadataEmitted` event: - public declare parentAccountId: NftDriverId | null; + public declare parentDripListId: NftDriverId | null; + public declare parentEcosystemId: NftDriverId | null; + public declare parentSubListId: ImmutableSplitsDriverId | null; + public declare rootDripListId: NftDriverId | null; + public declare rootEcosystemId: NftDriverId | null; public declare lastProcessedIpfsHash: string | null; public static initialize(sequelize: Sequelize): void { @@ -25,7 +26,43 @@ export default class SubListModel extends Model< type: DataTypes.STRING, primaryKey: true, }, - parentAccountId: { + parentDripListId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'DripLists', + key: 'id', + }, + }, + parentEcosystemId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'Ecosystems', + key: 'id', + }, + }, + parentSubListId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'SubLists', + key: 'id', + }, + }, + rootDripListId: { + // Foreign key + type: DataTypes.STRING, + allowNull: true, + references: { + model: 'DripLists', + key: 'id', + }, + }, + rootEcosystemId: { // Foreign key type: DataTypes.STRING, allowNull: true, @@ -45,8 +82,28 @@ export default class SubListModel extends Model< tableName: 'SubLists', indexes: [ { - fields: ['parentAccountId'], - name: `IX_SubLists_parentAccountId`, + fields: ['parentDripListId'], + name: `IX_SubLists_parentDripListId`, + unique: false, + }, + { + fields: ['parentEcosystemId'], + name: `IX_SubLists_parentEcosystemId`, + unique: false, + }, + { + fields: ['parentSubListId'], + name: `IX_SubLists_parentSubListId`, + unique: false, + }, + { + fields: ['rootDripListId'], + name: `IX_SubLists_rootDripListId`, + unique: false, + }, + { + fields: ['rootEcosystemId'], + name: `IX_SubLists_rootEcosystemId`, unique: false, }, ], diff --git a/src/models/SubListSplitReceiverModel.ts b/src/models/SubListSplitReceiverModel.ts index 2ab6113..5017195 100644 --- a/src/models/SubListSplitReceiverModel.ts +++ b/src/models/SubListSplitReceiverModel.ts @@ -17,21 +17,16 @@ import GitProjectModel from './GitProjectModel'; import SubListModel from './SubListModel'; import EcosystemModel from './EcosystemModel'; -export enum SubListSplitReceiverType { - ProjectMaintainer = 'ProjectMaintainer', - ProjectDependency = 'ProjectDependency', - DripListDependency = 'DripListDependency', -} - export default class SubListSplitReceiverModel extends Model< InferAttributes, InferCreationAttributes > { public declare id: CreationOptional; // Primary key - public declare fundeeImmutableSplitsId: ImmutableSplitsDriverId; // Foreign key + public declare fundeeSubListId: ImmutableSplitsDriverId; // Foreign key public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key public declare funderEcosystemId: NftDriverId | null; // Foreign key + public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key public declare weight: number; public declare type: DependencyType; @@ -45,7 +40,7 @@ export default class SubListSplitReceiverModel extends Model< autoIncrement: true, primaryKey: true, }, - fundeeImmutableSplitsId: { + fundeeSubListId: { // Foreign key type: DataTypes.STRING, references: { @@ -81,6 +76,15 @@ export default class SubListSplitReceiverModel extends Model< }, allowNull: true, }, + funderSubListId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: EcosystemModel, + key: 'id', + }, + allowNull: true, + }, weight: { type: DataTypes.INTEGER, allowNull: true, @@ -100,7 +104,7 @@ export default class SubListSplitReceiverModel extends Model< tableName: 'SubListSplitReceivers', indexes: [ { - fields: ['fundeeImmutableSplitsId'], + fields: ['fundeeSubListId'], name: `IX_SubListSplitReceivers_fundeeImmutableSplitsId`, unique: false, }, diff --git a/src/models/index.ts b/src/models/index.ts index e4f2c40..d4223af 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -9,7 +9,6 @@ export { default as SplitsSetEventModel } from './SplitsSetEventModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; export { default as _LastIndexedBlockModel } from './_LastIndexedBlockModel'; export { default as OwnerUpdatedEventModel } from './OwnerUpdatedEventModel'; -export { default as CreatedSplitsEventModel } from './CreatedSplitsEventModel'; export { default as SqueezedStreamsEventModel } from './SqueezedStreamsEventModel'; export { default as SubListSplitReceiverModel } from './SubListSplitReceiverModel'; export { default as DripListSplitReceiverModel } from './DripListSplitReceiverModel'; diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index 6e88f57..a753f90 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -22,8 +22,8 @@ export function isRepoDriverId(id: string): id is RepoDriverId { return true; } -export function toRepoDriverId(id: bigint): RepoDriverId { - const repoDriverId = id.toString(); +export function toRepoDriverId(id: bigint | string): RepoDriverId { + const repoDriverId = typeof id === 'bigint' ? id.toString() : id; if (!isRepoDriverId(repoDriverId)) { throw new Error(`Invalid 'RepoDriver' account ID: ${id}.`); @@ -121,8 +121,8 @@ export async function calcAccountId(owner: AddressLike): Promise { } // Account ID -export function toAccountId(id: bigint): AccountId { - const accountIdAsString = id.toString(); +export function toAccountId(id: bigint | string): AccountId { + const accountIdAsString = typeof id === 'bigint' ? id.toString() : id; if ( isRepoDriverId(accountIdAsString) || @@ -133,5 +133,5 @@ export function toAccountId(id: bigint): AccountId { return accountIdAsString as AccountId; } - throw new Error(`Invalid account ID: ${id}.`); + throw new Error(`Invalid account ID: ${accountIdAsString}.`); } diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 544566f..2ae481a 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -1,8 +1,6 @@ import type { UUID } from 'crypto'; import type { Transaction } from 'sequelize'; -import type { DependencyOfProjectType } from '../core/types'; import type { EventSignature } from '../events/types'; -import { repoDriverSplitReceiverSchema } from '../metadata/schemas/repo-driver/v2'; export function assertTransaction( transaction: Transaction | null | undefined, @@ -31,15 +29,3 @@ export function assertEventSignature( ); } } - -export function assertDependencyOfProjectType( - project: any, -): asserts project is DependencyOfProjectType { - const result = repoDriverSplitReceiverSchema.safeParse(project); - - if (!result.success) { - throw new Error( - `Invalid project dependency: ${JSON.stringify(result.error.format())}`, - ); - } -} diff --git a/src/utils/formatSplitReceivers.ts b/src/utils/formatSplitReceivers.ts new file mode 100644 index 0000000..a31ba24 --- /dev/null +++ b/src/utils/formatSplitReceivers.ts @@ -0,0 +1,34 @@ +import type { SplitsReceiverStruct } from '../../contracts/CURRENT_NETWORK/Drips'; + +export function formatSplitReceivers( + receivers: SplitsReceiverStruct[], +): SplitsReceiverStruct[] { + // Splits receivers must be sorted by user ID, deduplicated, and without weights <= 0. + + const uniqueReceivers = receivers.reduce( + (unique: SplitsReceiverStruct[], o) => { + if ( + !unique.some( + (obj: SplitsReceiverStruct) => + obj.accountId === o.accountId && obj.weight === o.weight, + ) + ) { + unique.push(o); + } + return unique; + }, + [], + ); + + const sortedReceivers = uniqueReceivers.sort((a, b) => + // Sort by user ID. + // eslint-disable-next-line no-nested-ternary + BigInt(a.accountId) > BigInt(b.accountId) + ? 1 + : BigInt(a.accountId) < BigInt(b.accountId) + ? -1 + : 0, + ); + + return sortedReceivers; +} diff --git a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts index 562bb25..94c36d9 100644 --- a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts +++ b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts @@ -8,9 +8,9 @@ import AccountMetadataEmittedEventModel from '../../src/models/AccountMetadataEm import { isLatestEvent } from '../../src/utils/eventUtils'; import { toAccountId } from '../../src/utils/accountIdUtils'; import { DRIPS_APP_USER_METADATA_KEY } from '../../src/core/constants'; -import * as handleGitProjectMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata'; +import * as handleGitProjectMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata'; import { toIpfsHash } from '../../src/utils/metadataUtils'; -import * as handleDripListMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata'; +import * as handleDripListMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata'; jest.mock('../../src/models/AccountMetadataEmittedEventModel'); jest.mock('../../src/db/database'); diff --git a/tests/eventHandlers/CreatedSplitsHandler.test.ts b/tests/eventHandlers/CreatedSplitsHandler.test.ts deleted file mode 100644 index 6ef303a..0000000 --- a/tests/eventHandlers/CreatedSplitsHandler.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* eslint-disable dot-notation */ -import { randomUUID } from 'crypto'; -import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; -import { dbConnection } from '../../src/db/database'; -import type { EventData } from '../../src/events/types'; -import CreatedSplitsEventModel from '../../src/models/CreatedSplitsEventModel'; -import LogManager from '../../src/core/LogManager'; -import { toAccountId } from '../../src/utils/accountIdUtils'; -import { CreatedSplitsEventHandler } from '../../src/eventHandlers'; -import SubListModel from '../../src/models/SubListModel'; - -jest.mock('../../src/models/CreatedSplitsEventModel'); -jest.mock('../../src/db/database'); -jest.mock('bee-queue'); -jest.mock('../../src/core/LogManager'); - -describe('CreatedSplitsEventHandler', () => { - let mockDbTransaction: {}; - let handler: CreatedSplitsEventHandler; - let mockRequest: EventHandlerRequest<'CreatedSplits(uint256,bytes32)'>; - - beforeAll(() => { - jest.clearAllMocks(); - - handler = new CreatedSplitsEventHandler(); - - mockRequest = { - id: randomUUID(), - event: { - args: [ - 53919893334301279589334030174039261347274288845081144962207220498533n, - 'receiversHash', - ], - logIndex: 1, - blockNumber: 1, - blockTimestamp: new Date(), - transactionHash: 'requestTransactionHash', - } as EventData<'CreatedSplits(uint256,bytes32)'>, - }; - - mockDbTransaction = {}; - - dbConnection.transaction = jest - .fn() - .mockImplementation((callback) => callback(mockDbTransaction)); - }); - - describe('_handle', () => { - test('should create a new CreatedSplitsEventModel', async () => { - // Arrange - CreatedSplitsEventModel.findOrCreate = jest.fn().mockResolvedValue([ - { - transactionHash: 'CreatedSplitsTransactionHash', - logIndex: 1, - }, - true, - ]); - - const mockSubList = { - ownerAddress: '', - previousOwnerAddress: '', - ownerAccountId: '', - save: jest.fn(), - }; - SubListModel.findOrCreate = jest - .fn() - .mockResolvedValue([mockSubList, true]); - - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); - - // Act - await handler['_handle'](mockRequest); - - // Assert - const { - event: { - args: [rawAccountId, rawReceiversHash], - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - } = mockRequest; - - expect(CreatedSplitsEventModel.findOrCreate).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { - accountId: toAccountId(rawAccountId), - receiversHash: rawReceiversHash, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - }); - }); - }); -}); From 1aff68a3413d923f1b8a7d4256f2d149bc518619 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Sun, 6 Apr 2025 16:07:09 +0200 Subject: [PATCH 10/60] refactor: refine migrations --- scripts/run-migrations.ts | 11 ----- src/db/migrations/20250317125844-initial.ts | 32 +++++++++++- ...ts => 20250319131551-create_ecosystems.ts} | 49 +++---------------- ...ate_sublists_and_split_receivers_tables.ts | 31 ++++++++---- ...enum_and_make_ecosystem_fields_nullable.ts | 20 ++++---- ..._funder_ecosystem_id_to_split_receivers.ts | 24 ++++----- ...ove-latestVotingRoundId-from-ecosystems.ts | 4 +- 7 files changed, 84 insertions(+), 87 deletions(-) rename src/db/migrations/{20250319131551-create_ecosystems_and_created_splits_events_tables.ts => 20250319131551-create_ecosystems.ts} (71%) diff --git a/scripts/run-migrations.ts b/scripts/run-migrations.ts index d5b72e4..dab51cb 100644 --- a/scripts/run-migrations.ts +++ b/scripts/run-migrations.ts @@ -15,17 +15,6 @@ export async function runMigrations(): Promise { const schema = getSchema(); - await sequelize.query(` - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT FROM pg_database WHERE datname = 'dripsdb' - ) THEN - CREATE DATABASE "dripsdb"; - END IF; - END - $$; - `); await sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`); const migrator = new Umzug({ diff --git a/src/db/migrations/20250317125844-initial.ts b/src/db/migrations/20250317125844-initial.ts index 19cbff2..43ad1b4 100644 --- a/src/db/migrations/20250317125844-initial.ts +++ b/src/db/migrations/20250317125844-initial.ts @@ -1,6 +1,6 @@ import { DataTypes, literal, type QueryInterface } from 'sequelize'; import { COMMON_EVENT_INIT_ATTRIBUTES, FORGES_MAP } from '../../core/constants'; -import { ProjectVerificationStatus } from '../../models/GitProjectModel'; +import { ProjectVerificationStatus } from '../../models/ProjectModel'; import getSchema from '../../utils/getSchema'; import { DependencyType, type DbSchema } from '../../core/types'; import { AddressDriverSplitReceiverType } from '../../models/AddressDriverSplitReceiverModel'; @@ -390,15 +390,30 @@ async function createRepoDriverSplitReceiversTable( primaryKey: true, }, fundeeProjectId: { + // Foreign key type: DataTypes.STRING, + references: { + model: 'GitProjects', + key: 'id', + }, allowNull: false, }, funderProjectId: { + // Foreign key type: DataTypes.STRING, + references: { + model: 'GitProjects', + key: 'id', + }, allowNull: true, }, funderDripListId: { + // Foreign key type: DataTypes.STRING, + references: { + model: 'DripLists', + key: 'id', + }, allowNull: true, }, weight: { @@ -605,15 +620,30 @@ async function createDripListSplitReceiversTable( primaryKey: true, }, fundeeDripListId: { + // Foreign key type: DataTypes.STRING, + references: { + model: 'DripLists', + key: 'id', + }, allowNull: false, }, funderProjectId: { + // Foreign key type: DataTypes.STRING, + references: { + model: 'GitProjects', + key: 'id', + }, allowNull: true, }, funderDripListId: { + // Foreign key type: DataTypes.STRING, + references: { + model: 'DripLists', + key: 'id', + }, allowNull: true, }, weight: { diff --git a/src/db/migrations/20250319131551-create_ecosystems_and_created_splits_events_tables.ts b/src/db/migrations/20250319131551-create_ecosystems.ts similarity index 71% rename from src/db/migrations/20250319131551-create_ecosystems_and_created_splits_events_tables.ts rename to src/db/migrations/20250319131551-create_ecosystems.ts index eac83ab..4c46404 100644 --- a/src/db/migrations/20250319131551-create_ecosystems_and_created_splits_events_tables.ts +++ b/src/db/migrations/20250319131551-create_ecosystems.ts @@ -1,6 +1,5 @@ import { DataTypes, literal, type QueryInterface } from 'sequelize'; import getSchema from '../../utils/getSchema'; -import { COMMON_EVENT_INIT_ATTRIBUTES } from '../../core/constants'; import type { DbSchema } from '../../core/types'; export async function up({ context: sequelize }: any): Promise { @@ -10,46 +9,9 @@ export async function up({ context: sequelize }: any): Promise { await createTableIfNotExists( queryInterface, schema, - 'Ecosystems', + 'EcosystemMainIdentities', createEcosystemsTable, ); - - await createTableIfNotExists( - queryInterface, - schema, - 'CreatedSplitsEvents', - createCreatedSplitsEventsTable, - ); -} - -async function createCreatedSplitsEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'CreatedSplitsEvents', schema }, - { - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - receiversHash: { - type: DataTypes.STRING, - allowNull: false, - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); } async function createEcosystemsTable( @@ -57,7 +19,7 @@ async function createEcosystemsTable( schema: DbSchema, ) { await queryInterface.createTable( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, { id: { type: DataTypes.STRING, @@ -117,7 +79,7 @@ async function createEcosystemsTable( ); await queryInterface.addIndex( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, ['ownerAddress'], { name: 'IX_Ecosystems_ownerAddress', @@ -147,5 +109,8 @@ export async function down({ context: sequelize }: any): Promise { const queryInterface = sequelize.getQueryInterface(); await queryInterface.dropTable({ tableName: 'CreatedSplitsEvents', schema }); - await queryInterface.dropTable({ tableName: 'Ecosystems', schema }); + await queryInterface.dropTable({ + tableName: 'EcosystemMainIdentities', + schema, + }); } diff --git a/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts b/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts index e2c4d1d..5b1c36c 100644 --- a/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts +++ b/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts @@ -42,12 +42,12 @@ async function createSubListsTable( key: 'id', }, }, - parentEcosystemId: { + parentEcosystemMainAccountId: { // Foreign key type: DataTypes.STRING, allowNull: true, references: { - model: 'Ecosystems', + model: 'EcosystemMainIdentities', key: 'id', }, }, @@ -69,12 +69,12 @@ async function createSubListsTable( key: 'id', }, }, - rootEcosystemId: { + rootEcosystemMainAccountId: { // Foreign key type: DataTypes.STRING, allowNull: true, references: { - model: 'Ecosystems', + model: 'EcosystemMainIdentities', key: 'id', }, }, @@ -82,6 +82,10 @@ async function createSubListsTable( type: DataTypes.TEXT, allowNull: true, }, + isValid: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, createdAt: { type: DataTypes.DATE, allowNull: false, @@ -106,7 +110,7 @@ async function createSubListsTable( await queryInterface.addIndex( { tableName: 'SubLists', schema }, - ['parentEcosystemId'], + ['parentEcosystemMainAccountId'], { name: 'IX_SubLists_parentEcosystemId', unique: false, @@ -133,7 +137,7 @@ async function createSubListsTable( await queryInterface.addIndex( { tableName: 'SubLists', schema }, - ['rootEcosystemId'], + ['rootEcosystemMainAccountId'], { name: 'IX_SubLists_rootEcosystemId', unique: false, @@ -180,11 +184,20 @@ async function createSubListSplitReceiversTable( }, allowNull: true, }, - funderEcosystemId: { + funderEcosystemMainAccountId: { + // Foreign key + type: DataTypes.STRING, + references: { + model: 'EcosystemMainIdentities', + key: 'id', + }, + allowNull: true, + }, + funderSubListId: { // Foreign key type: DataTypes.STRING, references: { - model: 'Ecosystems', + model: 'SubLists', key: 'id', }, allowNull: true, @@ -249,7 +262,7 @@ async function createSubListSplitReceiversTable( await queryInterface.addIndex( { tableName: 'SubListSplitReceivers', schema }, - ['funderEcosystemId'], + ['funderEcosystemMainAccountId'], { name: 'IX_SubListSplitReceivers_funderEcosystemId', where: { diff --git a/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts b/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts index a1a0f0f..a2b386a 100644 --- a/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts +++ b/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts @@ -19,7 +19,7 @@ export async function up({ context: sequelize }: any): Promise { `); await queryInterface.changeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'creator', { type: DataTypes.STRING, @@ -28,7 +28,7 @@ export async function up({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'ownerAddress', { type: DataTypes.STRING, @@ -37,7 +37,7 @@ export async function up({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'ownerAccountId', { type: DataTypes.STRING, @@ -46,7 +46,7 @@ export async function up({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'previousOwnerAddress', { type: DataTypes.STRING, @@ -55,7 +55,7 @@ export async function up({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'isVisible', { type: DataTypes.BOOLEAN, @@ -69,7 +69,7 @@ export async function down({ context: sequelize }: any): Promise { const queryInterface: QueryInterface = sequelize.getQueryInterface(); await queryInterface.changeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'creator', { type: DataTypes.STRING, @@ -78,7 +78,7 @@ export async function down({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'ownerAddress', { type: DataTypes.STRING, @@ -87,7 +87,7 @@ export async function down({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'ownerAccountId', { type: DataTypes.STRING, @@ -96,7 +96,7 @@ export async function down({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'previousOwnerAddress', { type: DataTypes.STRING, @@ -105,7 +105,7 @@ export async function down({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'isVisible', { type: DataTypes.BOOLEAN, diff --git a/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts b/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts index a08b786..fb2fdd5 100644 --- a/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts +++ b/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts @@ -7,11 +7,11 @@ export async function up({ context: sequelize }: any): Promise { await queryInterface.addColumn( { tableName: 'RepoDriverSplitReceivers', schema }, - 'funderEcosystemId', + 'funderEcosystemMainAccountId', { type: DataTypes.STRING, references: { - model: 'Ecosystems', + model: 'EcosystemMainIdentities', key: 'id', }, allowNull: true, @@ -20,7 +20,7 @@ export async function up({ context: sequelize }: any): Promise { await queryInterface.addIndex( { tableName: 'RepoDriverSplitReceivers', schema }, - ['funderEcosystemId'], + ['funderEcosystemMainAccountId'], { name: 'IX_RepoDriverSplitReceivers_funderEcosystemId', where: { @@ -57,11 +57,11 @@ export async function up({ context: sequelize }: any): Promise { await queryInterface.addColumn( { tableName: 'DripListSplitReceivers', schema }, - 'funderEcosystemId', + 'funderEcosystemMainAccountId', { type: DataTypes.STRING, references: { - model: 'Ecosystems', + model: 'EcosystemMainIdentities', key: 'id', }, allowNull: true, @@ -70,7 +70,7 @@ export async function up({ context: sequelize }: any): Promise { await queryInterface.addIndex( { tableName: 'DripListSplitReceivers', schema }, - ['funderEcosystemId'], + ['funderEcosystemMainAccountId'], { name: 'IX_DripListSplitReceivers_funderEcosystemId', where: { @@ -107,11 +107,11 @@ export async function up({ context: sequelize }: any): Promise { await queryInterface.addColumn( { tableName: 'AddressDriverSplitReceivers', schema }, - 'funderEcosystemId', + 'funderEcosystemMainAccountId', { type: DataTypes.STRING, references: { - model: 'Ecosystems', + model: 'EcosystemMainIdentities', key: 'id', }, allowNull: true, @@ -120,7 +120,7 @@ export async function up({ context: sequelize }: any): Promise { await queryInterface.addIndex( { tableName: 'AddressDriverSplitReceivers', schema }, - ['funderEcosystemId'], + ['funderEcosystemMainAccountId'], { name: 'IX_AddressDriverSplitReceivers_funderEcosystemId', where: { @@ -162,7 +162,7 @@ export async function down({ context: sequelize }: any): Promise { await queryInterface.removeColumn( { tableName: 'RepoDriverSplitReceivers', schema }, - 'funderEcosystemId', + 'funderEcosystemMainAccountId', ); await queryInterface.removeIndex( { tableName: 'RepoDriverSplitReceivers', schema }, @@ -170,7 +170,7 @@ export async function down({ context: sequelize }: any): Promise { ); await queryInterface.removeColumn( { tableName: 'DripListSplitReceivers', schema }, - 'funderEcosystemId', + 'funderEcosystemMainAccountId', ); await queryInterface.removeIndex( { tableName: 'DripListSplitReceivers', schema }, @@ -178,7 +178,7 @@ export async function down({ context: sequelize }: any): Promise { ); await queryInterface.removeColumn( { tableName: 'AddressDriverSplitReceivers', schema }, - 'funderEcosystemId', + 'funderEcosystemMainAccountId', ); await queryInterface.removeIndex( { tableName: 'AddressDriverSplitReceivers', schema }, diff --git a/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts b/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts index 931fb49..db344b8 100644 --- a/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts +++ b/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts @@ -6,7 +6,7 @@ export async function up({ context: sequelize }: any): Promise { const queryInterface: QueryInterface = sequelize.getQueryInterface(); await queryInterface.removeColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'latestVotingRoundId', ); } @@ -16,7 +16,7 @@ export async function down({ context: sequelize }: any): Promise { const queryInterface: QueryInterface = sequelize.getQueryInterface(); await queryInterface.addColumn( - { tableName: 'Ecosystems', schema }, + { tableName: 'EcosystemMainIdentities', schema }, 'latestVotingRoundId', { type: DataTypes.STRING, From 004832b3e8c3e58f5d471cb68b25df7f8cf63655 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Sun, 6 Apr 2025 16:07:42 +0200 Subject: [PATCH 11/60] refactor: add ecosystem deployer env var --- .env.template | 2 ++ src/config/appSettings.schema.ts | 1 + src/config/appSettings.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/.env.template b/.env.template index a413646..8e3b110 100644 --- a/.env.template +++ b/.env.template @@ -18,6 +18,8 @@ SHOULD_START_MONITORING_UI=boolean # Optional. If true, the app will start the m VISIBILITY_THRESHOLD_BLOCK_NUMBER=number # Optional. The block number to start indexing Project and Drip List visibility data from. Defaults to 0. +ECOSYSTEM_DEPLOYER=string # Optional. The address of the ecosystem deployer. If specified, NFT transfers will not be hidden by default. + ### INDEXING OPTIONS POLLING_INTERVAL=number # Optional. The interval in milliseconds to poll the blockchain for new events. Defaults to 5000. diff --git a/src/config/appSettings.schema.ts b/src/config/appSettings.schema.ts index 544866d..7911d0f 100644 --- a/src/config/appSettings.schema.ts +++ b/src/config/appSettings.schema.ts @@ -29,6 +29,7 @@ export const appSettingsSchema = z.object({ shouldStartMonitoringUI: z.boolean().optional().default(false), cacheInvalidationEndpoint: z.string(), visibilityThresholdBlockNumber: z.number().optional().default(0), + ecosystemDeployer: z.string().optional(), }); export type LoggingConfig = z.infer; diff --git a/src/config/appSettings.ts b/src/config/appSettings.ts index 8749eb6..6de8d46 100644 --- a/src/config/appSettings.ts +++ b/src/config/appSettings.ts @@ -40,6 +40,7 @@ function loadAppSettings(): AppSettings { .VISIBILITY_THRESHOLD_BLOCK_NUMBER ? parseInt(process.env.VISIBILITY_THRESHOLD_BLOCK_NUMBER, 10) : undefined, + ecosystemDeployer: process.env.ECOSYSTEM_DEPLOYER, }; try { From 46acfa79879dd5bc74e54df072b53432a8eea80d Mon Sep 17 00:00:00 2001 From: jtourkos Date: Sun, 6 Apr 2025 16:07:59 +0200 Subject: [PATCH 12/60] refactor: improve logger --- src/core/LogManager.ts | 46 ++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/core/LogManager.ts b/src/core/LogManager.ts index b7441d6..cd14632 100644 --- a/src/core/LogManager.ts +++ b/src/core/LogManager.ts @@ -59,14 +59,6 @@ export default class LogManager { return this; } - public appendIsLatestEventLog(): this { - this._logs.push( - 'Handled event is the latest event. Models will be updated.', - ); - - return this; - } - public appendLog(log: string): this { this._logs.push(log); @@ -78,41 +70,43 @@ export default class LogManager { type: { new (): T }, id: string, ): this { + const changes = LogManager.getChangedProperties(instance); + + const formattedChanges = + changes && Object.keys(changes).length > 0 + ? `\n\tChanged properties:\n${JSON.stringify(changes, null, 2) + .split('\n') + .map((line) => `\t ${line}`) + .join('\n')}` + : `\n\tNo changes detected.`; + this._logs.push( - `Updated ${LogManager.nameOfType(type)} with ID ${id}:${ - LogManager.getChangedProperties(instance) - ? `\n\t - ${JSON.stringify( - LogManager.getChangedProperties(instance), - )}` - : '' - }`, + `Updated ${LogManager.nameOfType(type)} with ID ${id}:${formattedChanges}`, ); return this; } - public logAllInfo(): void { - LogManager.logRequestInfo( - `Completed successfully. The following happened:\n\t - ${this._logs.join( - '\n\t - ', - )}`, - this._requestId, - ); + public logAllInfo(handler: string): void { + const formattedLogs = this._logs.map((log) => `\t - ${log}`).join('\n'); + const message = `${handler} completed successfully. The following happened:\n${formattedLogs}`; + + LogManager.logRequestInfo(message, this._requestId); } public static logRequestDebug(message: string, requestId: UUID): void { - logger.debug(`${message}`, { requestId }); + logger.debug(`[${requestId}] ${message}`); } public static logRequestInfo(message: string, requestId: UUID): void { - logger.info(`${message}`, { requestId }); + logger.info(`[${requestId}] ${message}`); } public static logRequestWarn(message: string, requestId: UUID): void { - logger.warn(`${message}`, { requestId }); + logger.warn(`[${requestId}] ${message}`); } public static logRequestError(message: string, requestId: UUID): void { - logger.error(`${message}`, { requestId }); + logger.error(`[${requestId}] ${message}`); } } From d790aae516fb627e1aafedbb59957f2272192fd4 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Sun, 6 Apr 2025 16:09:25 +0200 Subject: [PATCH 13/60] refactor: db associations --- src/db/database.ts | 214 +++++++++--------------------------- src/db/modelRegistration.ts | 8 +- 2 files changed, 56 insertions(+), 166 deletions(-) diff --git a/src/db/database.ts b/src/db/database.ts index d79d22e..0cb727a 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -5,8 +5,8 @@ import { AddressDriverSplitReceiverModel, DripListModel, DripListSplitReceiverModel, - GitProjectModel, - EcosystemModel, + ProjectModel, + EcosystemMainAccountModel, RepoDriverSplitReceiverModel, SubListModel, SubListSplitReceiverModel, @@ -31,7 +31,7 @@ export async function connectToDb(): Promise { defineProjectAssociations(); defineDripListAssociations(); - defineEcosystemsAssociations(); + defineEcosystemMainAccountsAssociations(); defineSubListAssociations(); logger.info('Connected to the database.'); @@ -52,42 +52,42 @@ async function initializeEntities(): Promise { function defineProjectAssociations() { // One-to-Many: A project can fund multiple addresses. - GitProjectModel.hasMany(AddressDriverSplitReceiverModel, { + ProjectModel.hasMany(AddressDriverSplitReceiverModel, { foreignKey: 'funderProjectId', }); - AddressDriverSplitReceiverModel.belongsTo(GitProjectModel, { + AddressDriverSplitReceiverModel.belongsTo(ProjectModel, { foreignKey: 'funderProjectId', }); // One-to-Many: A project can fund multiple projects. - GitProjectModel.hasMany(RepoDriverSplitReceiverModel, { + ProjectModel.hasMany(RepoDriverSplitReceiverModel, { foreignKey: 'funderProjectId', }); - RepoDriverSplitReceiverModel.belongsTo(GitProjectModel, { + RepoDriverSplitReceiverModel.belongsTo(ProjectModel, { foreignKey: 'funderProjectId', }); // One-to-Many: A project can fund multiple Drip Lists. - GitProjectModel.hasMany(DripListSplitReceiverModel, { + ProjectModel.hasMany(DripListSplitReceiverModel, { foreignKey: 'funderProjectId', }); - DripListSplitReceiverModel.belongsTo(GitProjectModel, { + DripListSplitReceiverModel.belongsTo(ProjectModel, { foreignKey: 'funderProjectId', }); // One-to-Many: A project can fund multiple Sub Lists. - GitProjectModel.hasMany(SubListSplitReceiverModel, { - foreignKey: 'funderSubListId', + ProjectModel.hasMany(SubListSplitReceiverModel, { + foreignKey: 'funderProjectId', }); - SubListSplitReceiverModel.belongsTo(GitProjectModel, { - foreignKey: 'funderSubListId', + SubListSplitReceiverModel.belongsTo(ProjectModel, { + foreignKey: 'funderProjectId', }); // One-to-One: A project receiver represents/is a project. - GitProjectModel.hasOne(RepoDriverSplitReceiverModel, { + ProjectModel.hasOne(RepoDriverSplitReceiverModel, { foreignKey: 'fundeeProjectId', }); - RepoDriverSplitReceiverModel.belongsTo(GitProjectModel, { + RepoDriverSplitReceiverModel.belongsTo(ProjectModel, { foreignKey: 'fundeeProjectId', }); } @@ -134,188 +134,78 @@ function defineDripListAssociations() { }); } -function defineEcosystemsAssociations() { - // One-to-Many: An Ecosystem can fund multiple addresses. - EcosystemModel.hasMany(AddressDriverSplitReceiverModel, { - foreignKey: 'funderEcosystemId', +function defineEcosystemMainAccountsAssociations() { + // One-to-Many: An Ecosystem Main Account can fund multiple addresses. + EcosystemMainAccountModel.hasMany(AddressDriverSplitReceiverModel, { + foreignKey: 'funderEcosystemMainAccountId', }); - AddressDriverSplitReceiverModel.belongsTo(EcosystemModel, { - foreignKey: 'funderEcosystemId', + AddressDriverSplitReceiverModel.belongsTo(EcosystemMainAccountModel, { + foreignKey: 'funderEcosystemMainAccountId', }); - // One-to-Many: An Ecosystem can fund multiple projects. - EcosystemModel.hasMany(RepoDriverSplitReceiverModel, { - foreignKey: 'funderEcosystemId', + // One-to-Many: An Ecosystem Main Account can fund multiple projects. + EcosystemMainAccountModel.hasMany(RepoDriverSplitReceiverModel, { + foreignKey: 'funderEcosystemMainAccountId', }); - RepoDriverSplitReceiverModel.belongsTo(EcosystemModel, { - foreignKey: 'funderEcosystemId', + RepoDriverSplitReceiverModel.belongsTo(EcosystemMainAccountModel, { + foreignKey: 'funderEcosystemMainAccountId', }); - // One-to-Many: An Ecosystem can fund multiple Drip Lists. - EcosystemModel.hasMany(DripListSplitReceiverModel, { - foreignKey: 'funderEcosystemId', + // One-to-Many: An Ecosystem Main Account can fund multiple Drip Lists. + EcosystemMainAccountModel.hasMany(DripListSplitReceiverModel, { + foreignKey: 'funderEcosystemMainAccountId', }); - DripListSplitReceiverModel.belongsTo(EcosystemModel, { - foreignKey: 'funderEcosystemId', + DripListSplitReceiverModel.belongsTo(EcosystemMainAccountModel, { + foreignKey: 'funderEcosystemMainAccountId', }); - // One-to-Many: An Ecosystem can fund multiple Sub Lists. - EcosystemModel.hasMany(SubListSplitReceiverModel, { - foreignKey: 'funderEcosystemId', + // One-to-Many: An Ecosystem Main Account can fund multiple Sub Lists. + EcosystemMainAccountModel.hasMany(SubListSplitReceiverModel, { + foreignKey: 'funderEcosystemMainAccountId', }); - SubListSplitReceiverModel.belongsTo(EcosystemModel, { - foreignKey: 'funderEcosystemId', + SubListSplitReceiverModel.belongsTo(EcosystemMainAccountModel, { + foreignKey: 'funderEcosystemMainAccountId', }); } function defineSubListAssociations() { - // One-to-Many: A parent Sub List can fund multiple addresses. + // One-to-Many: A Sub List can fund multiple addresses. SubListModel.hasMany(AddressDriverSplitReceiverModel, { - foreignKey: 'parentSubListId', - }); - AddressDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentSubListId', - }); - - // One-to-Many: A parent Sub List can fund multiple projects. - SubListModel.hasMany(RepoDriverSplitReceiverModel, { - foreignKey: 'parentSubListId', - }); - RepoDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentSubListId', - }); - - // One-to-Many: A parent Sub List can fund multiple Drip Lists. - SubListModel.hasMany(DripListSplitReceiverModel, { - foreignKey: 'parentSubListId', - }); - DripListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentSubListId', - }); - - // One-to-Many: A parent Sub List can fund multiple Sub Lists. - SubListModel.hasMany(SubListSplitReceiverModel, { - foreignKey: 'parentSubListId', - }); - SubListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentSubListId', - }); - - // One-to-Many: A parent Drip List can fund multiple addresses. - SubListModel.hasMany(AddressDriverSplitReceiverModel, { - foreignKey: 'parentDripListId', - }); - AddressDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentDripListId', - }); - - // One-to-Many: A parent Drip List can fund multiple projects. - SubListModel.hasMany(RepoDriverSplitReceiverModel, { - foreignKey: 'parentDripListId', - }); - RepoDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentDripListId', - }); - - // One-to-Many: A parent Drip List can fund multiple Drip Lists. - SubListModel.hasMany(DripListSplitReceiverModel, { - foreignKey: 'parentDripListId', - }); - DripListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentDripListId', - }); - - // One-to-Many: A root Drip List can fund multiple addresses. - SubListModel.hasMany(AddressDriverSplitReceiverModel, { - foreignKey: 'rootDripListId', - }); - AddressDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'rootDripListId', - }); - - // One-to-Many: A root Drip List can fund multiple projects. - SubListModel.hasMany(RepoDriverSplitReceiverModel, { - foreignKey: 'rootDripListId', - }); - RepoDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'rootDripListId', - }); - - // One-to-Many: A root Drip List can fund multiple Drip Lists. - SubListModel.hasMany(DripListSplitReceiverModel, { - foreignKey: 'rootDripListId', - }); - DripListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'rootDripListId', - }); - - // One-to-Many: A parent Ecosystem can fund multiple addresses. - SubListModel.hasMany(AddressDriverSplitReceiverModel, { - foreignKey: 'parentEcosystemId', + foreignKey: 'funderSubListId', }); AddressDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentEcosystemId', + foreignKey: 'funderSubListId', }); - // One-to-Many: A parent Ecosystem can fund multiple projects. + // One-to-Many: A Sub List can fund multiple projects. SubListModel.hasMany(RepoDriverSplitReceiverModel, { - foreignKey: 'parentEcosystemId', + foreignKey: 'funderSubListId', }); RepoDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentEcosystemId', + foreignKey: 'funderSubListId', }); - // One-to-Many: A parent Ecosystem can fund multiple Drip Lists. + // One-to-Many: A Sub List can fund multiple Drip Lists. SubListModel.hasMany(DripListSplitReceiverModel, { - foreignKey: 'parentEcosystemId', + foreignKey: 'funderSubListId', }); DripListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentEcosystemId', - }); - // One-to-Many: A parent Ecosystem can fund multiple Sub Lists. - SubListModel.hasMany(SubListSplitReceiverModel, { - foreignKey: 'parentEcosystemId', - }); - SubListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentEcosystemId', - }); - - // One-to-Many: A root Ecosystem can fund multiple addresses. - SubListModel.hasMany(AddressDriverSplitReceiverModel, { - foreignKey: 'rootEcosystemId', - }); - AddressDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'rootEcosystemId', - }); - - // One-to-Many: A root Ecosystem can fund multiple projects. - SubListModel.hasMany(RepoDriverSplitReceiverModel, { - foreignKey: 'rootEcosystemId', - }); - RepoDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'rootEcosystemId', + foreignKey: 'funderSubListId', }); - // One-to-Many: A root Ecosystem can fund multiple Drip Lists. - SubListModel.hasMany(DripListSplitReceiverModel, { - foreignKey: 'rootEcosystemId', - }); - DripListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'rootEcosystemId', - }); - // One-to-Many: A root Ecosystem can fund multiple Sub Lists. + // One-to-Many: A Sub List can fund multiple Sub Lists. SubListModel.hasMany(SubListSplitReceiverModel, { - foreignKey: 'rootEcosystemId', + foreignKey: 'funderSubListId', }); SubListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'rootEcosystemId', + foreignKey: 'funderSubListId', }); - // One-to-Many: A parent Drip List can fund multiple Sub Lists. - SubListModel.hasMany(SubListSplitReceiverModel, { - foreignKey: 'parentSubListId', + // One-to-One: A Sub List receiver represents/is a Sub List. + SubListModel.hasOne(SubListSplitReceiverModel, { + foreignKey: 'fundeeSubListId', }); SubListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'parentSubListId', + foreignKey: 'fundeeSubListId', }); } diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index 435d311..d7005a1 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -4,7 +4,7 @@ import { AddressDriverSplitReceiverModel, DripListModel, DripListSplitReceiverModel, - GitProjectModel, + ProjectModel, OwnerUpdateRequestedEventModel, OwnerUpdatedEventModel, RepoDriverSplitReceiverModel, @@ -17,7 +17,7 @@ import { SplitEventModel, SqueezedStreamsEventModel, SubListModel, - EcosystemModel, + EcosystemMainAccountModel, SubListSplitReceiverModel, } from '../models'; @@ -38,8 +38,8 @@ export function registerModels(): void { registerModel(DripListModel); registerModel(GivenEventModel); registerModel(SplitEventModel); - registerModel(GitProjectModel); - registerModel(EcosystemModel); + registerModel(ProjectModel); + registerModel(EcosystemMainAccountModel); registerModel(TransferEventModel); registerModel(SplitsSetEventModel); registerModel(StreamsSetEventModel); From 8fb2252d8445213b45406e0ef4b553cd14386fe5 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Sun, 6 Apr 2025 16:10:45 +0200 Subject: [PATCH 14/60] refactor: rename utils funcs --- src/eventHandlers/SplitEventHandler.ts | 25 +++++------ .../SqueezedStreamsEventHandler.ts | 44 ++++++++----------- .../StreamReceiverSeenEventHandler.ts | 38 +++++++--------- src/eventHandlers/StreamsSetEventHandler.ts | 42 ++++++++---------- src/events/EventHandlerBase.ts | 11 ++++- ...AccountMetadataEmittedEventHandler.test.ts | 24 +++++----- tests/eventHandlers/GivenEventHandler.test.ts | 6 +-- .../OwnerUpdateRequestedEventHandler.test.ts | 32 +++++++------- .../OwnerUpdatedEventHandler.test.ts | 37 ++++++++-------- tests/eventHandlers/SplitEventHandler.test.ts | 6 +-- .../SplitsSetEventHandler.test.ts | 4 +- .../SqueezedStreamsEventHandler.test.ts | 6 +-- .../StreamReceiverSeenEventHandler.test.ts | 4 +- .../StreamsSetEventHandler.test.ts | 4 +- .../TransferEventHandler.test.ts | 4 +- 15 files changed, 137 insertions(+), 150 deletions(-) diff --git a/src/eventHandlers/SplitEventHandler.ts b/src/eventHandlers/SplitEventHandler.ts index becf000..65ee216 100644 --- a/src/eventHandlers/SplitEventHandler.ts +++ b/src/eventHandlers/SplitEventHandler.ts @@ -1,6 +1,6 @@ import EventHandlerBase from '../events/EventHandlerBase'; import LogManager from '../core/LogManager'; -import { toAccountId } from '../utils/accountIdUtils'; +import { convertToAccountId } from '../utils/accountIdUtils'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import { SplitEventModel } from '../models'; import { dbConnection } from '../db/database'; @@ -22,8 +22,8 @@ export default class SplitEventHandler extends EventHandlerBase<'Split(uint256,u const [rawAccountId, rawReceiver, rawErc20, rawAmt] = args as SplitEvent.OutputTuple; - const accountId = toAccountId(rawAccountId); - const receiver = toAccountId(rawReceiver); + const accountId = convertToAccountId(rawAccountId); + const receiver = convertToAccountId(rawReceiver); const erc20 = toAddress(rawErc20); const amt = toBigIntString(rawAmt.toString()); @@ -41,14 +41,8 @@ export default class SplitEventHandler extends EventHandlerBase<'Split(uint256,u await dbConnection.transaction(async (transaction) => { const logManager = new LogManager(requestId); - const [givenEvent, isEventCreated] = await SplitEventModel.findOrCreate({ - lock: true, - transaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + const splitEvent = await SplitEventModel.create( + { accountId, receiver, erc20, @@ -58,15 +52,16 @@ export default class SplitEventHandler extends EventHandlerBase<'Split(uint256,u blockTimestamp, transactionHash, }, - }); + { transaction }, + ); logManager.appendFindOrCreateLog( SplitEventModel, - isEventCreated, - `${givenEvent.transactionHash}-${givenEvent.logIndex}`, + true, + `${splitEvent.transactionHash}-${splitEvent.logIndex}`, ); - logManager.logAllInfo(); + logManager.logAllInfo(this.name); }); } } diff --git a/src/eventHandlers/SqueezedStreamsEventHandler.ts b/src/eventHandlers/SqueezedStreamsEventHandler.ts index 8ac633a..6653f18 100644 --- a/src/eventHandlers/SqueezedStreamsEventHandler.ts +++ b/src/eventHandlers/SqueezedStreamsEventHandler.ts @@ -1,6 +1,6 @@ import EventHandlerBase from '../events/EventHandlerBase'; import LogManager from '../core/LogManager'; -import { toAccountId } from '../utils/accountIdUtils'; +import { convertToAccountId } from '../utils/accountIdUtils'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import { dbConnection } from '../db/database'; import type { SqueezedStreamsEvent } from '../../contracts/CURRENT_NETWORK/Drips'; @@ -29,9 +29,9 @@ export default class SqueezedStreamsEventHandler extends EventHandlerBase<'Squee rawStreamsHistoryHashes, ] = args as SqueezedStreamsEvent.OutputTuple; - const accountId = toAccountId(rawAccountId); + const accountId = convertToAccountId(rawAccountId); const erc20 = toAddress(rawErc20); - const senderId = toAccountId(rawSenderId); + const senderId = convertToAccountId(rawSenderId); const amt = toBigIntString(rawAmt.toString()); const streamsHistoryHashes = SqueezedStreamsEventModel.toStreamHistoryHashes(rawStreamsHistoryHashes); @@ -51,34 +51,28 @@ export default class SqueezedStreamsEventHandler extends EventHandlerBase<'Squee await dbConnection.transaction(async (transaction) => { const logManager = new LogManager(requestId); - const [transferEvent, isEventCreated] = - await SqueezedStreamsEventModel.findOrCreate({ - lock: true, - transaction, - where: { - logIndex, - transactionHash, - }, - defaults: { - accountId, - erc20, - senderId, - amount: amt, - streamsHistoryHashes, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - }); + const transferEvent = await SqueezedStreamsEventModel.create( + { + accountId, + erc20, + senderId, + amount: amt, + streamsHistoryHashes, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + { transaction }, + ); logManager.appendFindOrCreateLog( SqueezedStreamsEventModel, - isEventCreated, + true, `${transferEvent.transactionHash}-${transferEvent.logIndex}`, ); - logManager.logAllInfo(); + logManager.logAllInfo(this.name); }); } } diff --git a/src/eventHandlers/StreamReceiverSeenEventHandler.ts b/src/eventHandlers/StreamReceiverSeenEventHandler.ts index 10209f6..19380ce 100644 --- a/src/eventHandlers/StreamReceiverSeenEventHandler.ts +++ b/src/eventHandlers/StreamReceiverSeenEventHandler.ts @@ -5,9 +5,9 @@ import { dbConnection } from '../db/database'; import EventHandlerBase from '../events/EventHandlerBase'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import StreamReceiverSeenEventModel from '../models/StreamReceiverSeenEventModel'; -import { toAccountId } from '../utils/accountIdUtils'; +import { convertToAccountId } from '../utils/accountIdUtils'; import { toBigIntString } from '../utils/bigintUtils'; -import { getCurrentSplitsByReceiversHash } from '../utils/getCurrentSplits'; +import { getCurrentSplitsByReceiversHash } from './AccountMetadataEmittedEvent/receiversRepository'; export default class StreamReceiverSeenEventHandler extends EventHandlerBase<'StreamReceiverSeen(bytes32,uint256,uint256)'> { public eventSignatures = [ @@ -25,7 +25,7 @@ export default class StreamReceiverSeenEventHandler extends EventHandlerBase<'St const [rawReceiversHash, rawAccountId, rawConfig] = args as StreamReceiverSeenEvent.OutputTuple; - const accountId = toAccountId(rawAccountId); + const accountId = convertToAccountId(rawAccountId); const config = toBigIntString(rawConfig.toString()); LogManager.logRequestInfo( @@ -41,28 +41,24 @@ export default class StreamReceiverSeenEventHandler extends EventHandlerBase<'St await dbConnection.transaction(async (transaction) => { const logManager = new LogManager(requestId); - const [streamReceiverSeenEvent, isEventCreated] = - await StreamReceiverSeenEventModel.findOrCreate({ - lock: true, + const streamReceiverSeenEvent = await StreamReceiverSeenEventModel.create( + { + receiversHash: rawReceiversHash, + accountId, + config, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + { transaction, - where: { - logIndex, - transactionHash, - }, - defaults: { - receiversHash: rawReceiversHash, - accountId, - config, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - }); + }, + ); logManager.appendFindOrCreateLog( StreamReceiverSeenEventModel, - isEventCreated, + true, `${streamReceiverSeenEvent.transactionHash}-${streamReceiverSeenEvent.logIndex}`, ); }); diff --git a/src/eventHandlers/StreamsSetEventHandler.ts b/src/eventHandlers/StreamsSetEventHandler.ts index 98445b3..47df218 100644 --- a/src/eventHandlers/StreamsSetEventHandler.ts +++ b/src/eventHandlers/StreamsSetEventHandler.ts @@ -4,7 +4,7 @@ import { dbConnection } from '../db/database'; import EventHandlerBase from '../events/EventHandlerBase'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import StreamsSetEventModel from '../models/StreamsSetEventModel'; -import { toAccountId } from '../utils/accountIdUtils'; +import { convertToAccountId } from '../utils/accountIdUtils'; import { toBigIntString } from '../utils/bigintUtils'; export default class StreamsSetEventHandler extends EventHandlerBase<'StreamsSet(uint256,address,bytes32,bytes32,uint128,uint32)'> { @@ -29,7 +29,7 @@ export default class StreamsSetEventHandler extends EventHandlerBase<'StreamsSet rawMaxEnd, ] = args as StreamsSetEvent.OutputTuple; - const accountId = toAccountId(rawAccountId); + const accountId = convertToAccountId(rawAccountId); const balance = toBigIntString(rawBalance.toString()); const maxEnd = toBigIntString(rawMaxEnd.toString()); @@ -49,31 +49,27 @@ export default class StreamsSetEventHandler extends EventHandlerBase<'StreamsSet await dbConnection.transaction(async (transaction) => { const logManager = new LogManager(requestId); - const [streamsSetEvent, isEventCreated] = - await StreamsSetEventModel.findOrCreate({ - lock: true, + const streamsSetEvent = await StreamsSetEventModel.create( + { + accountId, + erc20: rawErc20, + receiversHash: rawReceiversHash, + streamsHistoryHash: rawStreamsHistoryHash, + balance, + maxEnd, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + { transaction, - where: { - logIndex, - transactionHash, - }, - defaults: { - accountId, - erc20: rawErc20, - receiversHash: rawReceiversHash, - streamsHistoryHash: rawStreamsHistoryHash, - balance, - maxEnd, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - }); + }, + ); logManager.appendFindOrCreateLog( StreamsSetEventModel, - isEventCreated, + true, `${streamsSetEvent.transactionHash}-${streamsSetEvent.logIndex}`, ); }); diff --git a/src/events/EventHandlerBase.ts b/src/events/EventHandlerBase.ts index c4ff740..f44479b 100644 --- a/src/events/EventHandlerBase.ts +++ b/src/events/EventHandlerBase.ts @@ -1,9 +1,10 @@ import { isAddress } from 'ethers'; +import { ValidationError } from 'sequelize'; import appSettings from '../config/appSettings'; import logger from '../core/logger'; import type { AccountId, Result } from '../core/types'; import saveEventProcessingJob from '../queue/saveEventProcessingJob'; -import { toAccountId } from '../utils/accountIdUtils'; +import { convertToAccountId } from '../utils/accountIdUtils'; import getResult from '../utils/getResult'; import type EventHandlerRequest from './EventHandlerRequest'; import type { EventSignature } from './types'; @@ -31,6 +32,12 @@ export default abstract class EventHandlerBase { const result = await getResult(this._handle.bind(this))(request); if (!result.ok) { + if (result.error instanceof ValidationError) { + logger.error( + `[${request.id}] ${this.name} failed to process event: ${result.error.message}`, + ); + } + throw result.error; } @@ -64,7 +71,7 @@ export default abstract class EventHandlerBase { for (const arg of args) { if (!isAddress(arg)) { try { - const accountId = toAccountId(arg); + const accountId = convertToAccountId(arg); if (!accountIds.includes(accountId)) { accountIds.push(accountId); } diff --git a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts index 94c36d9..2ffc4be 100644 --- a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts +++ b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts @@ -5,11 +5,11 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import type { EventData } from '../../src/events/types'; import { dbConnection } from '../../src/db/database'; import AccountMetadataEmittedEventModel from '../../src/models/AccountMetadataEmittedEventModel'; -import { isLatestEvent } from '../../src/utils/eventUtils'; -import { toAccountId } from '../../src/utils/accountIdUtils'; +import { isLatestEvent } from '../../src/utils/isLatestEvent'; +import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { DRIPS_APP_USER_METADATA_KEY } from '../../src/core/constants'; -import * as handleGitProjectMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata'; -import { toIpfsHash } from '../../src/utils/metadataUtils'; +import * as handleProjectMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata'; +import { convertToIpfsHash } from '../../src/utils/metadataUtils'; import * as handleDripListMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata'; jest.mock('../../src/models/AccountMetadataEmittedEventModel'); @@ -18,7 +18,7 @@ jest.mock('bee-queue'); jest.mock('../../src/core/LogManager'); jest.mock('../../src/utils/eventUtils'); jest.mock( - '../../src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleGitProjectMetadata', + '../../src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleProjectMetadata', ); jest.mock( '../../src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata', @@ -122,7 +122,7 @@ describe('AccountMetadataEmittedHandler', () => { blockNumber, blockTimestamp, transactionHash, - accountId: toAccountId(accountId), + accountId: convertToAccountId(accountId), }, }); }); @@ -141,17 +141,17 @@ describe('AccountMetadataEmittedHandler', () => { (isLatestEvent as jest.Mock).mockResolvedValue(true); - (handleGitProjectMetadata.default as jest.Mock) = jest.fn(); + (handleProjectMetadata.default as jest.Mock) = jest.fn(); // Act await handler['_handle'](mockRequest); // Assert - expect(handleGitProjectMetadata.default).toHaveBeenCalledWith( + expect(handleProjectMetadata.default).toHaveBeenCalledWith( expect.anything(), - toAccountId(mockRequest.event.args[0]), + convertToAccountId(mockRequest.event.args[0]), mockDbTransaction, - toIpfsHash(mockRequest.event.args[2]), + convertToIpfsHash(mockRequest.event.args[2]), mockRequest.event.blockTimestamp, ); }); @@ -193,9 +193,9 @@ describe('AccountMetadataEmittedHandler', () => { // Assert expect(handleDripListMetadata.default).toHaveBeenCalledWith({ logManager: expect.anything(), - dripListId: toAccountId(request.event.args[0]), + dripListId: convertToAccountId(request.event.args[0]), transaction: mockDbTransaction, - ipfsHash: toIpfsHash(request.event.args[2]), + ipfsHash: convertToIpfsHash(request.event.args[2]), blockTimestamp: request.event.blockTimestamp, blockNumber: 1, metadata: expect.anything(), diff --git a/tests/eventHandlers/GivenEventHandler.test.ts b/tests/eventHandlers/GivenEventHandler.test.ts index 36e96a7..6692e5c 100644 --- a/tests/eventHandlers/GivenEventHandler.test.ts +++ b/tests/eventHandlers/GivenEventHandler.test.ts @@ -6,7 +6,7 @@ import type { EventData } from '../../src/events/types'; import GivenEventModel from '../../src/models/GivenEventModel'; import LogManager from '../../src/core/LogManager'; import GivenEventHandler from '../../src/eventHandlers/GivenEventHandler'; -import { toAccountId } from '../../src/utils/accountIdUtils'; +import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { toAddress } from '../../src/utils/ethereumAddressUtils'; import { toBigIntString } from '../../src/utils/bigintUtils'; @@ -83,8 +83,8 @@ describe('GivenEventHandler', () => { transactionHash, }, defaults: { - accountId: toAccountId(rawAccountId), - receiver: toAccountId(rawReceiver), + accountId: convertToAccountId(rawAccountId), + receiver: convertToAccountId(rawReceiver), erc20: toAddress(rawErc20), amt: toBigIntString(rawAmt.toString()), logIndex, diff --git a/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts b/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts index 1bcbe74..a472fc0 100644 --- a/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts +++ b/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts @@ -5,22 +5,22 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import { OwnerUpdateRequestedEventHandler } from '../../src/eventHandlers'; import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; -import { toRepoDriverId } from '../../src/utils/accountIdUtils'; +import { convertToRepoDriverId } from '../../src/utils/accountIdUtils'; import { calculateProjectStatus, toForge, toReadable, toUrl, -} from '../../src/utils/gitProjectUtils'; +} from '../../src/utils/projectUtils'; import OwnerUpdateRequestedEventModel from '../../src/models/OwnerUpdateRequestedEventModel'; -import GitProjectModel, { +import ProjectModel, { ProjectVerificationStatus, -} from '../../src/models/GitProjectModel'; +} from '../../src/models/ProjectModel'; import LogManager from '../../src/core/LogManager'; -import { isLatestEvent } from '../../src/utils/eventUtils'; +import { isLatestEvent } from '../../src/utils/isLatestEvent'; jest.mock('../../src/models/OwnerUpdateRequestedEventModel'); -jest.mock('../../src/models/GitProjectModel'); +jest.mock('../../src/models/ProjectModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); jest.mock('../../src/utils/eventUtils'); @@ -59,7 +59,7 @@ describe('OwnerUpdateRequestedEventHandler', () => { }); describe('_handle', () => { - test('should create new OwnerUpdateRequestedEventModel and GitProjectModel', async () => { + test('should create new OwnerUpdateRequestedEventModel and ProjectModel', async () => { // Arrange OwnerUpdateRequestedEventModel.findOrCreate = jest .fn() @@ -71,9 +71,9 @@ describe('OwnerUpdateRequestedEventHandler', () => { true, ]); - GitProjectModel.findOrCreate = jest.fn().mockResolvedValue([ + ProjectModel.findOrCreate = jest.fn().mockResolvedValue([ { - id: toRepoDriverId(mockRequest.event.args[0]), + id: convertToRepoDriverId(mockRequest.event.args[0]), }, true, ]); @@ -108,18 +108,18 @@ describe('OwnerUpdateRequestedEventHandler', () => { transactionHash, name: toReadable(name), forge: toForge(forge), - accountId: toRepoDriverId(accountId), + accountId: convertToRepoDriverId(accountId), }, }); - expect(GitProjectModel.findOrCreate).toHaveBeenCalledWith({ + expect(ProjectModel.findOrCreate).toHaveBeenCalledWith({ transaction: mockDbTransaction, lock: true, where: { - id: toRepoDriverId(accountId), + id: convertToRepoDriverId(accountId), }, defaults: { - id: toRepoDriverId(accountId), + id: convertToRepoDriverId(accountId), isValid: true, isVisible: true, name: toReadable(name), @@ -130,7 +130,7 @@ describe('OwnerUpdateRequestedEventHandler', () => { }); }); - test('should update the GitProjectModel when the incoming event is the latest', async () => { + test('should update the ProjectModel when the incoming event is the latest', async () => { // Arrange OwnerUpdateRequestedEventModel.findOrCreate = jest .fn() @@ -149,14 +149,12 @@ describe('OwnerUpdateRequestedEventHandler', () => { verificationStatus: ProjectVerificationStatus.Unclaimed, save: jest.fn(), }; - GitProjectModel.findOrCreate = jest + ProjectModel.findOrCreate = jest .fn() .mockResolvedValue([mockGitProject, false]); (isLatestEvent as jest.Mock).mockResolvedValue(true); - LogManager.prototype.appendIsLatestEventLog = jest.fn().mockReturnThis(); - // Act await handler['_handle'](mockRequest); diff --git a/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts b/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts index 01f6320..e54e223 100644 --- a/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts +++ b/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts @@ -4,17 +4,20 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import { OwnerUpdatedEventHandler } from '../../src/eventHandlers'; import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; -import { calcAccountId, toRepoDriverId } from '../../src/utils/accountIdUtils'; -import { calculateProjectStatus } from '../../src/utils/gitProjectUtils'; +import { + calcAccountId, + convertToRepoDriverId, +} from '../../src/utils/accountIdUtils'; +import { calculateProjectStatus } from '../../src/utils/projectUtils'; import OwnerUpdatedEventModel from '../../src/models/OwnerUpdatedEventModel'; -import GitProjectModel, { +import ProjectModel, { ProjectVerificationStatus, -} from '../../src/models/GitProjectModel'; +} from '../../src/models/ProjectModel'; import LogManager from '../../src/core/LogManager'; -import { isLatestEvent } from '../../src/utils/eventUtils'; +import { isLatestEvent } from '../../src/utils/isLatestEvent'; jest.mock('../../src/models/OwnerUpdatedEventModel'); -jest.mock('../../src/models/GitProjectModel'); +jest.mock('../../src/models/ProjectModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); jest.mock('../../src/utils/eventUtils'); @@ -53,7 +56,7 @@ describe('OwnerUpdatedEventHandler', () => { }); describe('_handle', () => { - test('should create new OwnerUpdatedEventModel and GitProjectModel', async () => { + test('should create new OwnerUpdatedEventModel and ProjectModel', async () => { // Arrange OwnerUpdatedEventModel.findOrCreate = jest.fn().mockResolvedValue([ { @@ -63,11 +66,11 @@ describe('OwnerUpdatedEventHandler', () => { true, ]); - (toRepoDriverId as jest.Mock).mockReturnValue('1'); + (convertToRepoDriverId as jest.Mock).mockReturnValue('1'); - GitProjectModel.findOrCreate = jest.fn().mockResolvedValue([ + ProjectModel.findOrCreate = jest.fn().mockResolvedValue([ { - id: toRepoDriverId(mockRequest.event.args[0]), + id: convertToRepoDriverId(mockRequest.event.args[0]), }, true, ]); @@ -103,18 +106,18 @@ describe('OwnerUpdatedEventHandler', () => { blockNumber, blockTimestamp, transactionHash, - accountId: toRepoDriverId(accountId), + accountId: convertToRepoDriverId(accountId), }, }); - expect(GitProjectModel.findOrCreate).toHaveBeenCalledWith({ + expect(ProjectModel.findOrCreate).toHaveBeenCalledWith({ transaction: mockDbTransaction, lock: true, where: { - id: toRepoDriverId(accountId), + id: convertToRepoDriverId(accountId), }, defaults: { - id: toRepoDriverId(accountId), + id: convertToRepoDriverId(accountId), isValid: true, isVisible: true, ownerAddress: owner, @@ -125,7 +128,7 @@ describe('OwnerUpdatedEventHandler', () => { }); }); - test('should update the GitProjectModel when the incoming event is the latest', async () => { + test('should update the ProjectModel when the incoming event is the latest', async () => { // Arrange OwnerUpdatedEventModel.findOrCreate = jest.fn().mockResolvedValue([ { @@ -141,14 +144,12 @@ describe('OwnerUpdatedEventHandler', () => { verificationStatus: ProjectVerificationStatus.Unclaimed, save: jest.fn(), }; - GitProjectModel.findOrCreate = jest + ProjectModel.findOrCreate = jest .fn() .mockResolvedValue([mockGitProject, false]); (isLatestEvent as jest.Mock).mockResolvedValue(true); - LogManager.prototype.appendIsLatestEventLog = jest.fn().mockReturnThis(); - (calcAccountId as jest.Mock).mockResolvedValue('ownerAccountId'); // Act diff --git a/tests/eventHandlers/SplitEventHandler.test.ts b/tests/eventHandlers/SplitEventHandler.test.ts index aabb747..557f567 100644 --- a/tests/eventHandlers/SplitEventHandler.test.ts +++ b/tests/eventHandlers/SplitEventHandler.test.ts @@ -6,7 +6,7 @@ import type { EventData } from '../../src/events/types'; import SplitEventModel from '../../src/models/SplitEventModel'; import LogManager from '../../src/core/LogManager'; import SplitEventHandler from '../../src/eventHandlers/SplitEventHandler'; -import { toAccountId } from '../../src/utils/accountIdUtils'; +import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { toAddress } from '../../src/utils/ethereumAddressUtils'; import { toBigIntString } from '../../src/utils/bigintUtils'; @@ -83,8 +83,8 @@ describe('SplitEventHandler', () => { transactionHash, }, defaults: { - accountId: toAccountId(rawAccountId), - receiver: toAccountId(rawReceiver), + accountId: convertToAccountId(rawAccountId), + receiver: convertToAccountId(rawReceiver), erc20: toAddress(rawErc20), amt: toBigIntString(rawAmt.toString()), logIndex, diff --git a/tests/eventHandlers/SplitsSetEventHandler.test.ts b/tests/eventHandlers/SplitsSetEventHandler.test.ts index 63b629e..6341fba 100644 --- a/tests/eventHandlers/SplitsSetEventHandler.test.ts +++ b/tests/eventHandlers/SplitsSetEventHandler.test.ts @@ -5,7 +5,7 @@ import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; import SplitsSetEventModel from '../../src/models/SplitsSetEventModel'; import LogManager from '../../src/core/LogManager'; -import { toAccountId } from '../../src/utils/accountIdUtils'; +import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { SplitsSetEventHandler } from '../../src/eventHandlers'; import setIsValidFlag from '../../src/eventHandlers/SplitsSetEventHandler/setIsValidFlag'; @@ -81,7 +81,7 @@ describe('SplitsSetEventHandler', () => { transactionHash, }, defaults: { - accountId: toAccountId(rawAccountId), + accountId: convertToAccountId(rawAccountId), receiversHash: rawReceiversHash, logIndex, blockNumber, diff --git a/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts b/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts index f017586..75f1057 100644 --- a/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts +++ b/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts @@ -6,7 +6,7 @@ import type { EventData } from '../../src/events/types'; import SqueezedStreamsEventModel from '../../src/models/SqueezedStreamsEventModel'; import LogManager from '../../src/core/LogManager'; import SqueezedStreamsEventHandler from '../../src/eventHandlers/SqueezedStreamsEventHandler'; -import { toAccountId } from '../../src/utils/accountIdUtils'; +import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { toAddress } from '../../src/utils/ethereumAddressUtils'; import { toBigIntString } from '../../src/utils/bigintUtils'; @@ -90,9 +90,9 @@ describe('SqueezedStreamsEventHandler', () => { transactionHash, }, defaults: { - accountId: toAccountId(rawAccountId), + accountId: convertToAccountId(rawAccountId), erc20: toAddress(rawErc20), - senderId: toAccountId(rawSenderId), + senderId: convertToAccountId(rawSenderId), amount: toBigIntString(rawAmt.toString()), streamsHistoryHashes: SqueezedStreamsEventModel.toStreamHistoryHashes( rawStreamsHistoryHashes, diff --git a/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts b/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts index 49067a1..ed7bac5 100644 --- a/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts +++ b/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts @@ -5,7 +5,7 @@ import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; import StreamReceiverSeenEventModel from '../../src/models/StreamReceiverSeenEventModel'; import LogManager from '../../src/core/LogManager'; -import { toAccountId } from '../../src/utils/accountIdUtils'; +import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { StreamReceiverSeenEventHandler } from '../../src/eventHandlers'; import { toBigIntString } from '../../src/utils/bigintUtils'; @@ -81,7 +81,7 @@ describe('StreamReceiverSeenEventHandler', () => { transactionHash, }, defaults: { - accountId: toAccountId(rawAccountId), + accountId: convertToAccountId(rawAccountId), receiversHash: rawReceiversHash, config: toBigIntString(rawConfig.toString()), logIndex, diff --git a/tests/eventHandlers/StreamsSetEventHandler.test.ts b/tests/eventHandlers/StreamsSetEventHandler.test.ts index 12faf5c..ae728bd 100644 --- a/tests/eventHandlers/StreamsSetEventHandler.test.ts +++ b/tests/eventHandlers/StreamsSetEventHandler.test.ts @@ -5,7 +5,7 @@ import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; import StreamsSetEventModel from '../../src/models/StreamsSetEventModel'; import LogManager from '../../src/core/LogManager'; -import { toAccountId } from '../../src/utils/accountIdUtils'; +import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { StreamsSetEventHandler } from '../../src/eventHandlers'; import { toBigIntString } from '../../src/utils/bigintUtils'; @@ -91,7 +91,7 @@ describe('StreamsSetEventHandler', () => { transactionHash, }, defaults: { - accountId: toAccountId(rawAccountId), + accountId: convertToAccountId(rawAccountId), erc20: rawErc20, receiversHash: rawReceiversHash, streamsHistoryHash: rawStreamsHistoryHash, diff --git a/tests/eventHandlers/TransferEventHandler.test.ts b/tests/eventHandlers/TransferEventHandler.test.ts index b7ff455..313e077 100644 --- a/tests/eventHandlers/TransferEventHandler.test.ts +++ b/tests/eventHandlers/TransferEventHandler.test.ts @@ -4,7 +4,7 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import { TransferEventHandler } from '../../src/eventHandlers'; import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; -import { toNftDriverId } from '../../src/utils/accountIdUtils'; +import { convertToNftDriverId } from '../../src/utils/accountIdUtils'; import LogManager from '../../src/core/LogManager'; import TransferEventModel from '../../src/models/TransferEventModel'; import DripListModel from '../../src/models/DripListModel'; @@ -88,7 +88,7 @@ describe('TransferEventHandler', () => { transactionHash, }, defaults: { - tokenId: toNftDriverId(tokenId), + tokenId: convertToNftDriverId(tokenId), to, from, logIndex, From 56f8d31567fe1ac3a7350984fd2090607b07e513 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Sun, 6 Apr 2025 16:11:16 +0200 Subject: [PATCH 15/60] chore: update arena package --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5a55a6..5891dbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@efstajas/versioned-parser": "^0.1.4", "@types/umzug": "^2.3.9", "bee-queue": "^1.5.0", - "bull-arena": "^4.0.0", + "bull-arena": "^4.4.2", "dotenv": "^16.3.1", "dotenv-expand": "^11.0.6", "ethers": "^6.7.1", @@ -27,7 +27,7 @@ }, "devDependencies": { "@typechain/ethers-v6": "^0.5.0", - "@types/bull-arena": "^3.0.7", + "@types/bull-arena": "^3.0.10", "@types/jest": "^29.5.11", "@types/node": "^20.5.9", "@types/pg": "^8.10.2", diff --git a/package.json b/package.json index bf4788a..bb8146c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "devDependencies": { "@typechain/ethers-v6": "^0.5.0", - "@types/bull-arena": "^3.0.7", + "@types/bull-arena": "^3.0.10", "@types/jest": "^29.5.11", "@types/node": "^20.5.9", "@types/pg": "^8.10.2", @@ -59,7 +59,7 @@ "@efstajas/versioned-parser": "^0.1.4", "@types/umzug": "^2.3.9", "bee-queue": "^1.5.0", - "bull-arena": "^4.0.0", + "bull-arena": "^4.4.2", "dotenv": "^16.3.1", "dotenv-expand": "^11.0.6", "ethers": "^6.7.1", From 32d25c374e75c95386e1634479121da1d2ba1146 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Sun, 6 Apr 2025 16:12:26 +0200 Subject: [PATCH 16/60] refactor: re write indexing logic for DripLists and Projects and support indexing of Ecosystems and SubLists --- .../AccountMetadataEmittedEventHandler.ts | 192 ++++---- .../buildFunderAccountFields.ts | 65 +++ .../handlers/handleDripListMetadata.ts | 158 +++---- ... => handleEcosystemMainAccountMetadata.ts} | 160 +++---- .../handlers/handleProjectMetadata.ts | 271 ++++++----- .../handlers/handleSubListMetadata.ts | 202 ++++---- .../projectVerification.ts | 54 ++- .../receiversRepository.ts | 436 +++++++++--------- .../verifySplitsReceivers.ts | 17 +- src/eventHandlers/GivenEventHandler.ts | 23 +- .../OwnerUpdateRequestedEventHandler.ts | 140 +++--- src/eventHandlers/OwnerUpdatedEventHandler.ts | 135 +++--- .../SplitsSetEventHandler.ts | 82 ++-- .../SplitsSetEventHandler/setIsValidFlag.ts | 265 +++++++---- src/eventHandlers/TransferEventHandler.ts | 134 +++--- src/models/AddressDriverSplitReceiverModel.ts | 14 +- src/models/DripListSplitReceiverModel.ts | 14 +- ...mModel.ts => EcosystemMainAccountModel.ts} | 8 +- .../{GitProjectModel.ts => ProjectModel.ts} | 8 +- src/models/RepoDriverSplitReceiverModel.ts | 16 +- src/models/SubListModel.ts | 21 +- src/models/SubListSplitReceiverModel.ts | 16 +- src/models/index.ts | 4 +- src/utils/accountIdUtils.ts | 86 ++-- src/utils/eventUtils.ts | 33 -- src/utils/getCurrentSplits.ts | 65 --- src/utils/isLatestEvent.ts | 35 ++ src/utils/metadataUtils.ts | 4 +- .../{gitProjectUtils.ts => projectUtils.ts} | 27 +- src/utils/recoverableError.ts | 5 + 30 files changed, 1354 insertions(+), 1336 deletions(-) create mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/buildFunderAccountFields.ts rename src/eventHandlers/AccountMetadataEmittedEvent/handlers/{handleEcosystemMetadata.ts => handleEcosystemMainAccountMetadata.ts} (52%) rename src/models/{EcosystemModel.ts => EcosystemMainAccountModel.ts} (90%) rename src/models/{GitProjectModel.ts => ProjectModel.ts} (95%) delete mode 100644 src/utils/eventUtils.ts delete mode 100644 src/utils/getCurrentSplits.ts create mode 100644 src/utils/isLatestEvent.ts rename src/utils/{gitProjectUtils.ts => projectUtils.ts} (68%) create mode 100644 src/utils/recoverableError.ts diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index 665c37f..0a4d717 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -1,7 +1,6 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { AccountMetadataEmittedEvent } from '../../../contracts/CURRENT_NETWORK/Drips'; import type { AccountId } from '../../core/types'; - import EventHandlerBase from '../../events/EventHandlerBase'; import { DRIPS_APP_USER_METADATA_KEY } from '../../core/constants'; import handleProjectMetadata from './handlers/handleProjectMetadata'; @@ -10,49 +9,49 @@ import { isImmutableSplitsDriverId, isNftDriverId, isRepoDriverId, - toAccountId, + convertToAccountId, + convertToRepoDriverId, + convertToNftDriverId, + convertToImmutableSplitsDriverId, } from '../../utils/accountIdUtils'; -import { getNftDriverMetadata, toIpfsHash } from '../../utils/metadataUtils'; +import { + getNftDriverMetadata, + convertToIpfsHash, +} from '../../utils/metadataUtils'; import handleDripListMetadata from './handlers/handleDripListMetadata'; import type EventHandlerRequest from '../../events/EventHandlerRequest'; import { AccountMetadataEmittedEventModel } from '../../models'; import { dbConnection } from '../../db/database'; -import { getCurrentSplitsByAccountId } from '../../utils/getCurrentSplits'; -import handleEcosystemMetadata from './handlers/handleEcosystemMetadata'; +import handleEcosystemMainAccountMetadata from './handlers/handleEcosystemMainAccountMetadata'; import handleSubListMetadata from './handlers/handleSubListMetadata'; import type { nftDriverAccountMetadataParser } from '../../metadata/schemas'; +import { getCurrentSplitsByAccountId } from './receiversRepository'; +import { isLatestEvent } from '../../utils/isLatestEvent'; export default class AccountMetadataEmittedEventHandler extends EventHandlerBase<'AccountMetadataEmitted(uint256,bytes32,bytes)'> { public readonly eventSignatures = [ 'AccountMetadataEmitted(uint256,bytes32,bytes)' as const, ]; - protected async _handle( - request: EventHandlerRequest<'AccountMetadataEmitted(uint256,bytes32,bytes)'>, - ): Promise { - const { - id: requestId, - event: { args, logIndex, blockNumber, blockTimestamp, transactionHash }, - } = request; - + protected async _handle({ + id: requestId, + event: { + args, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + eventSignature, + }, + }: EventHandlerRequest<'AccountMetadataEmitted(uint256,bytes32,bytes)'>): Promise { const [accountId, key, value] = args as AccountMetadataEmittedEvent.OutputTuple; - if (!this._isEmittedByTheDripsApp(key)) { - LogManager.logRequestInfo( - `Skipping ${request.event.eventSignature} event processing because the key '${key}' is not emitted by the Drips App.`, - requestId, - ); - - return; - } - - const typedAccountId = toAccountId(accountId); - const ipfsHash = toIpfsHash(value); + const ipfsHash = convertToIpfsHash(value); LogManager.logRequestInfo( [ - `📥 ${this.name} is processing ${request.event.eventSignature}:`, + `📥 ${this.name} is processing ${eventSignature}:`, ` - key: ${key}`, ` - value: ${value} (IPFS hash: ${ipfsHash})`, ` - accountId: ${accountId}`, @@ -62,54 +61,74 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase requestId, ); + if (!this._isEmittedByTheDripsApp(key)) { + LogManager.logRequestInfo( + `Skipping ${eventSignature} event: key '${key}' not emitted by the Drips App.`, + requestId, + ); + + return; + } + + if (!this._canProcessDriverType(accountId)) { + LogManager.logRequestInfo( + `Skipping ${eventSignature} event: accountId '${accountId}' is not a Driver type that can be processed.`, + requestId, + ); + + return; + } + await dbConnection.transaction(async (transaction) => { const logManager = new LogManager(requestId); - const [accountMetadataEmittedEventModel, isEventCreated] = - await AccountMetadataEmittedEventModel.findOrCreate({ - lock: true, - transaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + const accountMetadataEmittedEvent = + await AccountMetadataEmittedEventModel.create( + { key, value, logIndex, blockNumber, blockTimestamp, transactionHash, - accountId: typedAccountId, + accountId: convertToAccountId(accountId), }, - }); + { + transaction, + }, + ); logManager.appendFindOrCreateLog( AccountMetadataEmittedEventModel, - isEventCreated, - `${accountMetadataEmittedEventModel.transactionHash}-${accountMetadataEmittedEventModel.logIndex}`, + true, + `${accountMetadataEmittedEvent.transactionHash}-${accountMetadataEmittedEvent.logIndex}`, ); - let handled = false; + // Only process metadata if this is the latest event. + if ( + !(await isLatestEvent( + accountMetadataEmittedEvent, + AccountMetadataEmittedEventModel, + { + accountId: convertToAccountId(accountId), + }, + transaction, + )) + ) { + return; + } - if (isRepoDriverId(typedAccountId)) { + if (isRepoDriverId(accountId)) { await handleProjectMetadata({ ipfsHash, logManager, transaction, blockTimestamp, - projectId: typedAccountId, - originEventDetails: { - logIndex, - transactionHash, - entity: accountMetadataEmittedEventModel, - }, + emitterAccountId: convertToRepoDriverId(accountId), }); - - handled = true; } - if (isNftDriverId(typedAccountId)) { + if (isNftDriverId(accountId)) { const metadata = await getNftDriverMetadata(ipfsHash); if (this._isDripListMetadata(metadata)) { @@ -120,79 +139,46 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase transaction, blockNumber, blockTimestamp, - dripListId: typedAccountId, - originEventDetails: { - logIndex, - transactionHash, - entity: accountMetadataEmittedEventModel, - }, + emitterAccountId: convertToNftDriverId(accountId), }); - - handled = true; } - if (this._isEcosystemMetadata(metadata)) { - await handleEcosystemMetadata({ + if (this._isEcosystemMainAccountMetadata(metadata)) { + await handleEcosystemMainAccountMetadata({ ipfsHash, metadata, logManager, transaction, blockNumber, blockTimestamp, - ecosystemId: typedAccountId, - originEventDetails: { - logIndex, - transactionHash, - entity: accountMetadataEmittedEventModel, - }, + emitterAccountId: convertToNftDriverId(accountId), }); - - handled = true; } } - if (isImmutableSplitsDriverId(typedAccountId)) { + if (isImmutableSplitsDriverId(accountId)) { await handleSubListMetadata({ ipfsHash, logManager, transaction, blockTimestamp, - subListId: typedAccountId, - originEventDetails: { - logIndex, - transactionHash, - entity: accountMetadataEmittedEventModel, - }, + emitterAccountId: convertToImmutableSplitsDriverId(accountId), }); - - handled = true; - } - - if (!handled) { - logManager.appendLog( - `Skipping metadata processing because the account with ID ${typedAccountId} is not a Project, Drip List, or Ecosystem.`, - ); } - logManager.logAllInfo(); + logManager.logAllInfo(this.name); }); } - public override async beforeHandle( - request: EventHandlerRequest<'AccountMetadataEmitted(uint256,bytes32,bytes)'>, - ): Promise<{ + public override async beforeHandle({ + event: { args }, + }: EventHandlerRequest<'AccountMetadataEmitted(uint256,bytes32,bytes)'>): Promise<{ accountIdsToInvalidate: AccountId[]; }> { - const { - event: { args }, - } = request; - const [accountId] = args as AccountMetadataEmittedEvent.OutputTuple; - const typedAccountId = toAccountId(accountId); - return { - accountIdsToInvalidate: await getCurrentSplitsByAccountId(typedAccountId), + accountIdsToInvalidate: await getCurrentSplitsByAccountId(accountId), }; } @@ -200,18 +186,26 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase return key === DRIPS_APP_USER_METADATA_KEY; } - private _isDripListMetadata( - metadata: AnyVersion, - ): boolean { + private _canProcessDriverType(accountId: bigint): boolean { return ( - metadata.isDripList || - ('type' in metadata ? metadata.type === 'dripList' : false) + isRepoDriverId(accountId) || + isNftDriverId(accountId) || + isImmutableSplitsDriverId(accountId) ); } - private _isEcosystemMetadata( + private _isEcosystemMainAccountMetadata( metadata: AnyVersion, ): boolean { return 'type' in metadata && metadata.type === 'ecosystem'; } + + private _isDripListMetadata( + metadata: AnyVersion, + ): boolean { + return ( + metadata.isDripList || + ('type' in metadata ? metadata.type === 'dripList' : false) + ); + } } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/buildFunderAccountFields.ts b/src/eventHandlers/AccountMetadataEmittedEvent/buildFunderAccountFields.ts new file mode 100644 index 0000000..9b7ab43 --- /dev/null +++ b/src/eventHandlers/AccountMetadataEmittedEvent/buildFunderAccountFields.ts @@ -0,0 +1,65 @@ +import { DependencyType } from '../../core/types'; +import type { + ImmutableSplitsDriverId, + NftDriverId, + RepoDriverId, +} from '../../core/types'; + +export type Funder = + | { + type: 'project'; + accountId: RepoDriverId; + dependencyType: 'dependency' | 'maintainer'; + } + | { type: 'dripList'; accountId: NftDriverId } + | { type: 'ecosystem'; accountId: NftDriverId } + | { type: 'sub-list'; accountId: ImmutableSplitsDriverId }; + +type FunderKeys = { + funderProjectId: RepoDriverId | null; + funderDripListId: NftDriverId | null; + funderSubListId: ImmutableSplitsDriverId | null; + funderEcosystemMainAccountId: NftDriverId | null; +}; + +export default function buildFunderAccountFields(funder: Funder): FunderKeys { + const keys: FunderKeys = { + funderProjectId: null, + funderSubListId: null, + funderDripListId: null, + funderEcosystemMainAccountId: null, + }; + + switch (funder.type) { + case 'project': + keys.funderProjectId = funder.accountId; + break; + case 'sub-list': + keys.funderSubListId = funder.accountId; + break; + case 'dripList': + keys.funderDripListId = funder.accountId; + break; + case 'ecosystem': + keys.funderEcosystemMainAccountId = funder.accountId; + break; + default: + throw new Error(`Unhandled funder type '${(funder as any).type}'.`); + } + + return keys; +} + +// TODO: Remove this function when the type is removed from the codebase. +export const resolveDependencyType = (funder: Funder): DependencyType => { + if (funder.type === 'project') { + return DependencyType.ProjectDependency; + } + + if (funder.type === 'dripList') { + return DependencyType.DripListDependency; + } + + // TODO: At the moment, both `ecosystem` and `sub-list` are treated as EcosystemDependency. The type is scheduled for removal soon. + return DependencyType.EcosystemDependency; +}; diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 005cdd4..91b99ea 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -5,15 +5,11 @@ import type { UUID } from 'crypto'; import type { z } from 'zod'; import type { IpfsHash, NftDriverId } from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; -import LogManager from '../../../core/LogManager'; -import { - AccountMetadataEmittedEventModel, - DripListModel, -} from '../../../models'; +import type LogManager from '../../../core/LogManager'; +import { DripListModel } from '../../../models'; import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; import appSettings from '../../../config/appSettings'; -import { isLatestEvent } from '../../../utils/eventUtils'; import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; import type { repoDriverSplitReceiverSchema, @@ -23,7 +19,7 @@ import type { subListSplitReceiverSchema } from '../../../metadata/schemas/sub-l import verifyProjectSources from '../projectVerification'; import { deleteExistingReceivers, - createProjectAndProjectReceiver, + createProjectReceiver, createSubListReceiver, createDripListReceiver, createAddressReceiver, @@ -34,78 +30,27 @@ type Params = { blockNumber: number; blockTimestamp: Date; logManager: LogManager; - dripListId: NftDriverId; transaction: Transaction; + emitterAccountId: NftDriverId; metadata: AnyVersion; - originEventDetails: { - entity: AccountMetadataEmittedEventModel; - logIndex: number; - transactionHash: string; - }; }; export default async function handleDripListMetadata({ ipfsHash, metadata, logManager, - dripListId, blockNumber, transaction, blockTimestamp, - originEventDetails: { entity, logIndex, transactionHash }, + emitterAccountId, }: Params) { - // Only process metadata if this is the latest event. - if ( - !(await isLatestEvent( - entity, - AccountMetadataEmittedEventModel, - { - logIndex, - transactionHash, - accountId: dripListId, - }, - transaction, - )) - ) { - logManager.logAllInfo(); - - return; - } - logManager.appendIsLatestEventLog(); - - assertMetadataIsValid(dripListId, metadata); - - // Here, is the only place an Drip List is created. - const dripList = await DripListModel.create( - { - id: dripListId, - isValid: false, // Until the related `TransferEvent` is processed. - name: metadata.name ?? null, - description: - 'description' in metadata ? metadata.description || null : null, - latestVotingRoundId: - 'latestVotingRoundId' in metadata - ? (metadata.latestVotingRoundId as UUID) || null - : null, - lastProcessedIpfsHash: ipfsHash, - isVisible: - blockNumber > appSettings.visibilityThresholdBlockNumber && - 'isVisible' in metadata - ? metadata.isVisible - : true, - }, - { transaction }, - ); - - logManager - .appendFindOrCreateLog(DripListModel, true, dripList.id) - .logAllInfo(); + validateMetadata(emitterAccountId, metadata); const receivers = metadata.projects ?? metadata.recipients; const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = await verifySplitsReceivers( - dripList.id, + emitterAccountId, receivers.map(({ weight, accountId }) => ({ weight, accountId, @@ -114,14 +59,7 @@ export default async function handleDripListMetadata({ if (!areSplitsValid) { logManager.appendLog( - [ - `Skipping metadata update for Drip List ${dripListId} due to mismatch in splits hash.`, - ` On-chain hash: ${onChainSplitsHash}`, - ` Metadata hash: ${calculatedSplitsHash}`, - ` Possible causes:`, - ` - The metadata event is the latest in the DB, but not on-chain.`, - ` - The metadata was manually emitted with outdated or mismatched splits.`, - ].join('\n'), + `Skipped Drip List ${emitterAccountId} metadata processing: on-chain splits hash '${onChainSplitsHash}' does not match hash '${calculatedSplitsHash}' calculated from metadata.`, ); return; @@ -129,9 +67,48 @@ export default async function handleDripListMetadata({ await verifyProjectSources(receivers); + const dripListProps = { + id: emitterAccountId, + name: metadata.name ?? null, + description: + 'description' in metadata ? metadata.description || null : null, + latestVotingRoundId: + 'latestVotingRoundId' in metadata + ? (metadata.latestVotingRoundId as UUID) || null + : null, + lastProcessedIpfsHash: ipfsHash, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, + }; + + const [dripList, isCreated] = await DripListModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + id: emitterAccountId, + }, + defaults: { + ...dripListProps, + isValid: false, // Until the related `TransferEvent` is processed. + }, + }); + + if (isCreated) { + logManager.appendFindOrCreateLog(DripListModel, true, dripList.id); + } else { + dripList.set(dripListProps); + + logManager.appendUpdateLog(dripList, DripListModel, dripList.id); + + await dripList.save({ transaction }); + } + await deleteExistingReceivers({ for: { - accountId: dripListId, + accountId: emitterAccountId, column: 'funderDripListId', }, transaction, @@ -142,7 +119,7 @@ export default async function handleDripListMetadata({ logManager, transaction, blockTimestamp, - funderDripListId: dripListId, + emitterAccountId, }); } @@ -151,12 +128,12 @@ async function setNewReceivers({ logManager, transaction, blockTimestamp, - funderDripListId, + emitterAccountId, }: { blockTimestamp: Date; logManager: LogManager; transaction: Transaction; - funderDripListId: NftDriverId; + emitterAccountId: NftDriverId; receivers: ( | z.infer | z.infer @@ -167,14 +144,14 @@ async function setNewReceivers({ const receiverPromises = receivers.map(async (receiver) => { switch (receiver.type) { case 'repoDriver': - return createProjectAndProjectReceiver({ + return createProjectReceiver({ logManager, transaction, blockTimestamp, metadataReceiver: receiver, funder: { type: 'dripList', - accountId: funderDripListId, + accountId: emitterAccountId, }, }); @@ -186,7 +163,7 @@ async function setNewReceivers({ metadataReceiver: receiver, funder: { type: 'dripList', - accountId: funderDripListId, + accountId: emitterAccountId, }, }); @@ -198,7 +175,7 @@ async function setNewReceivers({ metadataReceiver: receiver, funder: { type: 'dripList', - accountId: funderDripListId, + accountId: emitterAccountId, }, }); @@ -210,7 +187,7 @@ async function setNewReceivers({ metadataReceiver: receiver, funder: { type: 'dripList', - accountId: funderDripListId, + accountId: emitterAccountId, }, }); @@ -221,20 +198,11 @@ async function setNewReceivers({ } }); - const result = await Promise.all(receiverPromises); - - logManager.appendLog( - `Updated ${LogManager.nameOfType( - DripListModel, - )} with ID ${funderDripListId} splits: ${result - .map((p) => JSON.stringify(p)) - .join(`, `)} - `, - ); + await Promise.all(receiverPromises); } -function assertMetadataIsValid( - dripListId: NftDriverId, +function validateMetadata( + emitterAccountId: NftDriverId, metadata: AnyVersion, ): asserts metadata is Extract< typeof metadata, @@ -255,9 +223,9 @@ function assertMetadataIsValid( )[]; } > { - if (dripListId !== metadata.describes.accountId) { - unreachableError( - `Drip List metadata describes account ID '${metadata.describes.accountId}' but it was emitted by ${dripListId}.`, + if (emitterAccountId !== metadata.describes.accountId) { + throw new Error( + `Invalid Drip List metadata: emitter account ID is '${emitterAccountId}', but metadata describes '${metadata.describes.accountId}'.`, ); } @@ -265,6 +233,6 @@ function assertMetadataIsValid( const isLegacy = 'projects' in metadata; if (!isCurrent && !isLegacy) { - throw new Error('Invalid Drip List metadata format.'); + throw new Error('Invalid Drip List metadata schema.'); } } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts similarity index 52% rename from src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMetadata.ts rename to src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index d222620..bb30035 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -2,21 +2,17 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Transaction } from 'sequelize'; import type { z } from 'zod'; import appSettings from '../../../config/appSettings'; -import LogManager from '../../../core/LogManager'; +import type LogManager from '../../../core/LogManager'; import type { IpfsHash, NftDriverId } from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; -import { - EcosystemModel, - AccountMetadataEmittedEventModel, -} from '../../../models'; +import { EcosystemMainAccountModel } from '../../../models'; import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; -import { isLatestEvent } from '../../../utils/eventUtils'; import type { repoDriverSplitReceiverSchema } from '../../../metadata/schemas/repo-driver/v2'; import type { subListSplitReceiverSchema } from '../../../metadata/schemas/sub-list/v1'; import verifyProjectSources from '../projectVerification'; import { - createProjectAndProjectReceiver, + createProjectReceiver, createSubListReceiver, deleteExistingReceivers, } from '../receiversRepository'; @@ -26,72 +22,25 @@ type Params = { blockNumber: number; blockTimestamp: Date; logManager: LogManager; - ecosystemId: NftDriverId; transaction: Transaction; + emitterAccountId: NftDriverId; metadata: AnyVersion; - originEventDetails: { - entity: AccountMetadataEmittedEventModel; - logIndex: number; - transactionHash: string; - }; }; -export default async function handleEcosystemMetadata({ +export default async function handleEcosystemMainAccountMetadata({ ipfsHash, metadata, logManager, - ecosystemId, blockNumber, transaction, blockTimestamp, - originEventDetails: { entity, logIndex, transactionHash }, + emitterAccountId, }: Params) { - // Only process metadata if this is the latest event. - if ( - !(await isLatestEvent( - entity, - AccountMetadataEmittedEventModel, - { - logIndex, - transactionHash, - accountId: ecosystemId, - }, - transaction, - )) - ) { - logManager.logAllInfo(); - - return; - } - logManager.appendIsLatestEventLog(); - - assertMetadataIsValid(ecosystemId, metadata); - - // Here, is the only place an Ecosystem is created. - const ecosystem = await EcosystemModel.create( - { - id: ecosystemId, - isValid: false, // Until the related `TransferEvent` is processed. - name: metadata.name ?? null, - description: - 'description' in metadata ? metadata.description || null : null, - lastProcessedIpfsHash: ipfsHash, - isVisible: - blockNumber > appSettings.visibilityThresholdBlockNumber && - 'isVisible' in metadata - ? metadata.isVisible - : true, - }, - { transaction }, - ); - - logManager - .appendFindOrCreateLog(EcosystemModel, true, ecosystem.id) - .logAllInfo(); + validateMetadata(emitterAccountId, metadata); const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = await verifySplitsReceivers( - ecosystem.id, + emitterAccountId, metadata.recipients.map(({ weight, accountId }) => ({ weight, accountId, @@ -100,35 +49,70 @@ export default async function handleEcosystemMetadata({ if (!areSplitsValid) { logManager.appendLog( - [ - `Skipping metadata update for Ecosystem ${ecosystemId} due to mismatch in splits hash.`, - ` On-chain hash: ${onChainSplitsHash}`, - ` Metadata hash: ${calculatedSplitsHash}`, - ` Possible causes:`, - ` - The metadata event is the latest in the DB, but not on-chain.`, - ` - The metadata was manually emitted with outdated or mismatched splits.`, - ].join('\n'), + `Skipped Drip List ${emitterAccountId} metadata processing: on-chain splits hash '${onChainSplitsHash}' does not match hash '${calculatedSplitsHash}' calculated from metadata.`, ); return; } - await verifyProjectSources(metadata.recipients); + const ecosystemMainAccountProps = { + id: emitterAccountId, + name: metadata.name ?? null, + description: + 'description' in metadata ? metadata.description || null : null, + lastProcessedIpfsHash: ipfsHash, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, + }; + + const [ecosystemMainIdentity, isCreated] = + await EcosystemMainAccountModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { id: emitterAccountId }, + defaults: { + ...ecosystemMainAccountProps, + isValid: false, // Until the related `TransferEvent` is processed. + }, + }); + + if (isCreated) { + logManager.appendFindOrCreateLog( + EcosystemMainAccountModel, + true, + ecosystemMainIdentity.id, + ); + } else { + ecosystemMainIdentity.set(ecosystemMainAccountProps); + + logManager.appendUpdateLog( + ecosystemMainIdentity, + EcosystemMainAccountModel, + ecosystemMainIdentity.id, + ); + + await ecosystemMainIdentity.save({ transaction }); + } + await deleteExistingReceivers({ for: { - accountId: ecosystemId, - column: 'funderEcosystemId', + accountId: emitterAccountId, + column: 'funderEcosystemMainAccountId', }, transaction, }); await setNewReceivers({ + ipfsHash, logManager, transaction, blockTimestamp, + emitterAccountId, receivers: metadata.recipients, - funderEcosystemId: ecosystemId, }); } @@ -137,12 +121,13 @@ async function setNewReceivers({ logManager, transaction, blockTimestamp, - funderEcosystemId, + emitterAccountId, }: { + ipfsHash: IpfsHash; blockTimestamp: Date; logManager: LogManager; transaction: Transaction; - funderEcosystemId: NftDriverId; + emitterAccountId: NftDriverId; receivers: ( | z.infer | z.infer @@ -151,14 +136,14 @@ async function setNewReceivers({ const receiverPromises = receivers.map(async (receiver) => { switch (receiver.type) { case 'repoDriver': - return createProjectAndProjectReceiver({ + return createProjectReceiver({ logManager, transaction, blockTimestamp, metadataReceiver: receiver, funder: { type: 'ecosystem', - accountId: funderEcosystemId, + accountId: emitterAccountId, }, }); @@ -170,7 +155,7 @@ async function setNewReceivers({ metadataReceiver: receiver, funder: { type: 'ecosystem', - accountId: funderEcosystemId, + accountId: emitterAccountId, }, }); @@ -181,20 +166,11 @@ async function setNewReceivers({ } }); - const result = await Promise.all(receiverPromises); - - logManager.appendLog( - `Updated ${LogManager.nameOfType( - EcosystemModel, - )} with ID ${funderEcosystemId} splits: ${result - .map((p) => JSON.stringify(p)) - .join(`, `)} - `, - ); + await Promise.all(receiverPromises); } -function assertMetadataIsValid( - ecosystemId: NftDriverId, +function validateMetadata( + emitterAccountId: NftDriverId, metadata: AnyVersion, ): asserts metadata is Extract< typeof metadata, @@ -206,9 +182,9 @@ function assertMetadataIsValid( )[]; } > { - if (ecosystemId !== metadata.describes.accountId) { - unreachableError( - `Ecosystem metadata describes account ID ${metadata.describes.accountId} but it was emitted by ${ecosystemId}.`, + if (emitterAccountId !== metadata.describes.accountId) { + throw new Error( + `Invalid Ecosystem Main Account metadata: emitter account ID is '${emitterAccountId}', but metadata describes '${metadata.describes.accountId}'.`, ); } @@ -219,6 +195,6 @@ function assertMetadataIsValid( metadata.type === 'ecosystem' ) ) { - throw new Error('Invalid Ecosystem metadata.'); + throw new Error('Invalid Ecosystem Main Account ID metadata schema.'); } } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index ae8992e..56cf7ee 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -1,19 +1,15 @@ -/* eslint-disable no-param-reassign */ -import type { Transaction } from 'sequelize'; +/* eslint-disable no-param-reassign */ // Mutating Sequelize model instance is intentional and safe here. +import { type Transaction } from 'sequelize'; import type { AnyVersion } from '@efstajas/versioned-parser'; -import type { z } from 'zod'; -import { - AccountMetadataEmittedEventModel, - DripListSplitReceiverModel, - GitProjectModel, -} from '../../../models'; +import { type z } from 'zod'; +import { ProjectModel } from '../../../models'; import type { repoDriverAccountMetadataParser } from '../../../metadata/schemas'; import LogManager from '../../../core/LogManager'; -import { calculateProjectStatus } from '../../../utils/gitProjectUtils'; +import { calculateProjectStatus } from '../../../utils/projectUtils'; import type { IpfsHash, RepoDriverId } from '../../../core/types'; -import { DependencyType } from '../../../core/types'; import { assertAddressDiverId, + convertToRepoDriverId, isAddressDriverId, isNftDriverId, isRepoDriverId, @@ -21,173 +17,166 @@ import { import unreachableError from '../../../utils/unreachableError'; import { getProjectMetadata } from '../../../utils/metadataUtils'; import verifySplitsReceivers from '../verifySplitsReceivers'; -import { isLatestEvent } from '../../../utils/eventUtils'; -import { verifyProjectMetadata } from '../projectVerification'; +import verifyProjectSources from '../projectVerification'; import { createAddressReceiver, - createProjectAndProjectReceiver, + createDripListReceiver, + createProjectReceiver, deleteExistingReceivers, } from '../receiversRepository'; import type { addressDriverSplitReceiverSchema, repoDriverSplitReceiverSchema, } from '../../../metadata/schemas/repo-driver/v2'; +import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; type Params = { ipfsHash: IpfsHash; blockTimestamp: Date; logManager: LogManager; - projectId: RepoDriverId; transaction: Transaction; - originEventDetails: { - entity: AccountMetadataEmittedEventModel; - logIndex: number; - transactionHash: string; - }; + emitterAccountId: RepoDriverId; }; -export default async function handleGitProjectMetadata({ +export default async function handleProjectMetadata({ ipfsHash, logManager, - projectId, transaction, blockTimestamp, - originEventDetails: { entity, logIndex, transactionHash }, + emitterAccountId, }: Params) { - // Only process metadata if this is the latest event. - if ( - !(await isLatestEvent( - entity, - AccountMetadataEmittedEventModel, - { - logIndex, - transactionHash, - accountId: projectId, - }, - transaction, - )) - ) { - logManager.logAllInfo(); - - return; - } - logManager.appendIsLatestEventLog(); - - const project = await GitProjectModel.findByPk(projectId, { - transaction, - lock: true, - }); - - if (!project) { - throw new Error( - [ - `Failed to update metadata for Project ${projectId}: Project not found.`, - `Possible reasons:`, - ` - The event that should have created the Project hasn't been processed yet.`, - ` - Metadata was manually emitted for a Project that doesn't exist in the system.`, - ].join('\n'), - ); - } - const metadata = await getProjectMetadata(ipfsHash); const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = - await verifySplitsReceivers( - project.id, - metadata.splits.dependencies - .concat(metadata.splits.maintainers as any) - .map((s) => ({ - weight: s.weight, - accountId: s.accountId, - })), - ); + await verifySplitsReceivers(emitterAccountId, [ + ...metadata.splits.dependencies, + ...metadata.splits.maintainers, + ]); if (!areSplitsValid) { logManager.appendLog( - [ - `Skipping metadata update for Project ${projectId} due to mismatch in splits hash.`, - ` On-chain hash: ${onChainSplitsHash}`, - ` Metadata hash: ${calculatedSplitsHash}`, - ` Possible causes:`, - ` - The metadata event is the latest in the DB, but not on-chain.`, - ` - The metadata was manually emitted with outdated or mismatched splits.`, - ].join('\n'), + `Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: on-chain splits hash '${onChainSplitsHash}' does not match '${calculatedSplitsHash}' calculated from metadata.`, ); return; } - await verifyProjectMetadata(project, metadata); + const projects = metadata.splits.dependencies + .flatMap((s) => ('type' in s && s.type === 'repoDriver' ? [s] : [])) + .filter((dep) => dep.type === 'repoDriver'); - await updateProjectMetadata( - project, - logManager, - transaction, + await verifyProjectSources(projects); + + await createProject({ metadata, ipfsHash, - ); + logManager, + transaction, + }); await deleteExistingReceivers({ for: { - accountId: projectId, + accountId: emitterAccountId, column: 'funderProjectId', }, transaction, }); - await setNewReceivers( - projectId, - metadata.splits, + await setNewReceivers({ logManager, transaction, blockTimestamp, - ); + emitterAccountId, + receivers: metadata.splits, + }); } -async function updateProjectMetadata( - project: GitProjectModel, - logManager: LogManager, - transaction: Transaction, - metadata: AnyVersion, - metadataIpfsHash: IpfsHash, -): Promise { - const { color, source, description } = metadata; +async function createProject({ + ipfsHash, + metadata, + logManager, + transaction, +}: { + ipfsHash: IpfsHash; + logManager: LogManager; + transaction: Transaction; + metadata: AnyVersion; +}): Promise { + const { + color, + source, + description, + describes: { accountId }, + } = metadata; + + const projectId = convertToRepoDriverId(accountId); - project.color = color; - project.url = source.url; - project.description = description ?? null; - project.verificationStatus = calculateProjectStatus(project); - project.isVisible = 'isVisible' in metadata ? metadata.isVisible : true; // Projects without `isVisible` field (V4 and below) are considered visible by default. - project.lastProcessedIpfsHash = metadataIpfsHash; + function getEmoji(): string | null { + if ('avatar' in metadata) { + return metadata.avatar.type === 'emoji' ? metadata.avatar.emoji : null; + } - if ('avatar' in metadata) { - // Metadata V4 + return metadata.emoji ?? null; + } - if (metadata.avatar.type === 'emoji') { - project.emoji = metadata.avatar.emoji; - project.avatarCid = null; - } else if (metadata.avatar.type === 'image') { - project.avatarCid = metadata.avatar.cid; - project.emoji = null; + function getAvatarCid(): string | null { + if ('avatar' in metadata && metadata.avatar.type === 'image') { + return metadata.avatar.cid; } - } else { - // Metadata V3 - project.emoji = metadata.emoji; + return null; } - logManager.appendUpdateLog(project, GitProjectModel, project.id); + const projectProps = { + id: projectId, + color, + url: source.url, + description: description ?? null, + verificationStatus: calculateProjectStatus({ + id: projectId, + color, + ownerAddress: null, + }), + isVisible: 'isVisible' in metadata ? metadata.isVisible : true, // Projects without `isVisible` field (V4 and below) are considered visible by default. + lastProcessedIpfsHash: ipfsHash, + emoji: getEmoji(), + avatarCid: getAvatarCid(), + }; - await project.save({ transaction }); + const [project, isCreated] = await ProjectModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { id: projectId }, + defaults: { + ...projectProps, + isValid: false, // Until the related `SplitsSet` is processed. + }, + }); + + if (isCreated) { + logManager.appendFindOrCreateLog(ProjectModel, true, project.id); + } else { + project.set(projectProps); + + logManager.appendUpdateLog(project, ProjectModel, project.id); + + await project.save({ transaction }); + } } -async function setNewReceivers( - funderProjectId: RepoDriverId, - receivers: AnyVersion['splits'], - logManager: LogManager, - transaction: Transaction, - blockTimestamp: Date, -) { +async function setNewReceivers({ + receivers, + logManager, + transaction, + blockTimestamp, + emitterAccountId, +}: { + blockTimestamp: Date; + logManager: LogManager; + transaction: Transaction; + emitterAccountId: RepoDriverId; + receivers: AnyVersion['splits']; +}) { const { dependencies, maintainers } = receivers; const maintainerPromises = maintainers.map((maintainer) => { @@ -199,18 +188,18 @@ async function setNewReceivers( blockTimestamp, metadataReceiver: maintainer as z.infer< typeof addressDriverSplitReceiverSchema - >, + >, // Safe to cast because we already checked the type of accountId. funder: { type: 'project', - accountId: funderProjectId, dependencyType: 'maintainer', + accountId: emitterAccountId, }, }); }); const dependencyPromises = dependencies.map(async (dependency) => { if (isRepoDriverId(dependency.accountId)) { - return createProjectAndProjectReceiver({ + return createProjectReceiver({ logManager, transaction, blockTimestamp, @@ -219,7 +208,8 @@ async function setNewReceivers( >, // Safe to cast because we already checked the type of accountId. funder: { type: 'project', - accountId: funderProjectId, + dependencyType: 'dependency', + accountId: emitterAccountId, }, }); } @@ -234,27 +224,31 @@ async function setNewReceivers( >, // Safe to cast because we already checked the type of accountId. funder: { type: 'project', - accountId: funderProjectId, dependencyType: 'dependency', + accountId: emitterAccountId, }, }); } if (isNftDriverId(dependency.accountId)) { - return DripListSplitReceiverModel.create( - { - funderProjectId, - fundeeDripListId: dependency.accountId, - weight: dependency.weight, - type: DependencyType.ProjectDependency, - blockTimestamp, + // NFT Driver is always represents a DripList receiver for Projects. Ecosystem Main Account receivers are not yet supported for projects. + return createDripListReceiver({ + logManager, + transaction, + blockTimestamp, + metadataReceiver: dependency as z.infer< + typeof dripListSplitReceiverSchema + >, // Safe to cast because we already checked the type of accountId., + funder: { + type: 'project', + dependencyType: 'dependency', + accountId: emitterAccountId, }, - { transaction }, - ); + }); } return unreachableError( - `Dependency with account ID ${dependency.accountId} is not an Address nor a Project.`, + `Cannot process project dependency '${dependency.accountId}': unsupported Driver type.`, ); }); @@ -264,11 +258,8 @@ async function setNewReceivers( ]); logManager.appendLog( - `Updated ${LogManager.nameOfType( - GitProjectModel, - )} with ID ${funderProjectId} splits: ${result - .map((p) => JSON.stringify(p)) - .join(`, `)} - `, + `Updated ${LogManager.nameOfType(ProjectModel)} with ID ${emitterAccountId} splits:\n${result + .map((p) => ` - ${JSON.stringify(p)}`) + .join('\n')}`, ); } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index b5ab8c3..03fc00f 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -1,131 +1,98 @@ /* eslint-disable no-param-reassign */ import type { Transaction } from 'sequelize'; import type { z } from 'zod'; -import LogManager from '../../../core/LogManager'; +import type { AnyVersion } from '@efstajas/versioned-parser'; +import type LogManager from '../../../core/LogManager'; import type { ImmutableSplitsDriverId, IpfsHash } from '../../../core/types'; -import { - AccountMetadataEmittedEventModel, - SubListModel, -} from '../../../models'; -import { - toNftDriverId, - toImmutableSplitsDriverId, -} from '../../../utils/accountIdUtils'; +import { EcosystemMainAccountModel, SubListModel } from '../../../models'; +import { convertToNftDriverId } from '../../../utils/accountIdUtils'; import { getImmutableSpitsDriverMetadata } from '../../../utils/metadataUtils'; import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; -import { isLatestEvent } from '../../../utils/eventUtils'; -import verifyProjectSources from '../projectVerification'; -import type { - addressDriverSplitReceiverSchema, - repoDriverSplitReceiverSchema, -} from '../../../metadata/schemas/repo-driver/v2'; -import type { subListSplitReceiverSchema } from '../../../metadata/schemas/sub-list/v1'; -import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; import { - createProjectAndProjectReceiver, + createProjectReceiver, createSubListReceiver, createDripListReceiver, deleteExistingReceivers, createAddressReceiver, } from '../receiversRepository'; +import type { + addressDriverSplitReceiverSchema, + repoDriverSplitReceiverSchema, +} from '../../../metadata/schemas/repo-driver/v2'; +import type { subListSplitReceiverSchema } from '../../../metadata/schemas/sub-list/v1'; +import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; +import verifyProjectSources from '../projectVerification'; +import RecoverableError from '../../../utils/recoverableError'; +import type { immutableSplitsDriverMetadataParser } from '../../../metadata/schemas'; type Params = { ipfsHash: IpfsHash; blockTimestamp: Date; logManager: LogManager; - subListId: ImmutableSplitsDriverId; + emitterAccountId: ImmutableSplitsDriverId; transaction: Transaction; - originEventDetails: { - entity: AccountMetadataEmittedEventModel; - logIndex: number; - transactionHash: string; - }; }; export default async function handleSubListMetadata({ ipfsHash, logManager, - subListId, transaction, blockTimestamp, - originEventDetails: { entity, logIndex, transactionHash }, + emitterAccountId, }: Params) { - // Only process metadata if this is the latest event. - if ( - !(await isLatestEvent( - entity, - AccountMetadataEmittedEventModel, - { - logIndex, - transactionHash, - accountId: subListId, - }, - transaction, - )) - ) { - logManager.logAllInfo(); - - return; - } - logManager.appendIsLatestEventLog(); - const metadata = await getImmutableSpitsDriverMetadata(ipfsHash); - // Here, is the only place an Ecosystem is created. - const subList = await SubListModel.create( - { - id: subListId, - parentDripListId: - metadata.parent.type === 'drip-list' - ? toNftDriverId(metadata.parent.accountId) - : null, - parentEcosystemId: - metadata.parent.type === 'ecosystem' - ? toNftDriverId(metadata.parent.accountId) - : null, - parentSubListId: - metadata.parent.type === 'sub-list' - ? toImmutableSplitsDriverId(metadata.parent.accountId) - : null, - rootDripListId: - metadata.root.type === 'drip-list' - ? toNftDriverId(metadata.root.accountId) - : null, - rootEcosystemId: - metadata.root.type === 'ecosystem' - ? toNftDriverId(metadata.root.accountId) - : null, - lastProcessedIpfsHash: ipfsHash, - }, - { transaction }, - ); - - logManager.appendFindOrCreateLog(SubListModel, true, subList.id).logAllInfo(); - const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = - await verifySplitsReceivers(subList.id, metadata.recipients); + await verifySplitsReceivers(emitterAccountId, metadata.recipients); if (!areSplitsValid) { logManager.appendLog( - [ - `Skipping metadata update for Sub List ${subListId} due to mismatch in splits hash.`, - ` On-chain hash: ${onChainSplitsHash}`, - ` Metadata hash: ${calculatedSplitsHash}`, - ` Possible causes:`, - ` - The metadata event is the latest in the DB, but not on-chain.`, - ` - The metadata was manually emitted with outdated or mismatched splits.`, - ].join('\n'), + `Skipped Drip List ${emitterAccountId} metadata processing: on-chain splits hash '${onChainSplitsHash}' does not match hash '${calculatedSplitsHash}' calculated from metadata.`, ); return; } - await verifyProjectSources(metadata.recipients); + await validateRootAndParentExist(metadata, transaction); + + const subListProps = { + id: emitterAccountId, + parentEcosystemMainAccountId: + metadata.parent.type === 'ecosystem' + ? convertToNftDriverId(metadata.parent.accountId) + : null, + rootEcosystemMainAccountId: + metadata.root.type === 'ecosystem' + ? convertToNftDriverId(metadata.root.accountId) + : null, + lastProcessedIpfsHash: ipfsHash, + }; + + const [subList, isCreated] = await SubListModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { id: emitterAccountId }, + defaults: { + ...subListProps, + isValid: false, // Until the related Sub List's metadata is processed. + }, + }); + + if (isCreated) { + logManager.appendFindOrCreateLog(SubListModel, true, subList.id); + } else { + subList.set(subListProps); + + logManager.appendUpdateLog(subList, SubListModel, subList.id); + + await subList.save({ transaction }); + } + await deleteExistingReceivers({ for: { - accountId: subListId, + accountId: emitterAccountId, column: 'funderSubListId', }, transaction, @@ -135,7 +102,7 @@ export default async function handleSubListMetadata({ logManager, transaction, blockTimestamp, - funderSubListId: subListId, + emitterAccountId, receivers: metadata.recipients, }); } @@ -145,12 +112,12 @@ async function setNewReceivers({ logManager, transaction, blockTimestamp, - funderSubListId, + emitterAccountId, }: { blockTimestamp: Date; logManager: LogManager; transaction: Transaction; - funderSubListId: ImmutableSplitsDriverId; + emitterAccountId: ImmutableSplitsDriverId; receivers: ( | z.infer | z.infer @@ -161,14 +128,14 @@ async function setNewReceivers({ const receiverPromises = receivers.map(async (receiver) => { switch (receiver.type) { case 'repoDriver': - return createProjectAndProjectReceiver({ + return createProjectReceiver({ logManager, transaction, blockTimestamp, metadataReceiver: receiver, funder: { type: 'sub-list', - accountId: funderSubListId, + accountId: emitterAccountId, }, }); @@ -180,7 +147,7 @@ async function setNewReceivers({ metadataReceiver: receiver, funder: { type: 'sub-list', - accountId: funderSubListId, + accountId: emitterAccountId, }, }); @@ -192,7 +159,7 @@ async function setNewReceivers({ metadataReceiver: receiver, funder: { type: 'sub-list', - accountId: funderSubListId, + accountId: emitterAccountId, }, }); @@ -204,7 +171,7 @@ async function setNewReceivers({ metadataReceiver: receiver, funder: { type: 'sub-list', - accountId: funderSubListId, + accountId: emitterAccountId, }, }); @@ -215,14 +182,43 @@ async function setNewReceivers({ } }); - const result = await Promise.all(receiverPromises); + await Promise.all(receiverPromises); +} + +async function validateRootAndParentExist( + metadata: AnyVersion, + transaction: Transaction, +) { + if ( + metadata.parent.type !== 'ecosystem' || + metadata.root.type !== 'ecosystem' + ) { + throw new Error('Sub Lists are currently only supported in Ecosystems.'); + } + + const root = await EcosystemMainAccountModel.findByPk( + metadata.root.accountId, + { + transaction, + lock: transaction.LOCK.UPDATE, + }, + ); + if (!root) { + throw new RecoverableError( + `Root Ecosystem Main Account '${metadata.root.accountId}' not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } - logManager.appendLog( - `Updated ${LogManager.nameOfType( - SubListModel, - )} with ID ${funderSubListId} splits: ${result - .map((p) => JSON.stringify(p)) - .join(`, `)} - `, + const parent = await EcosystemMainAccountModel.findByPk( + metadata.parent.accountId, + { + transaction, + lock: transaction.LOCK.UPDATE, + }, ); + if (!parent) { + throw new RecoverableError( + `Parent Ecosystem Main Account '${metadata.parent.accountId}' not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts b/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts index c590805..5547566 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts @@ -1,5 +1,6 @@ import type { z } from 'zod'; import type { AnyVersion } from '@efstajas/versioned-parser'; +import { hexlify, toUtf8Bytes } from 'ethers'; import { repoDriverContract } from '../../core/contractClients'; import type { dripListSplitReceiverSchema } from '../../metadata/schemas/nft-driver/v2'; import type { @@ -8,7 +9,7 @@ import type { } from '../../metadata/schemas/repo-driver/v2'; import type { subListSplitReceiverSchema } from '../../metadata/schemas/sub-list/v1'; import type { repoDriverAccountMetadataParser } from '../../metadata/schemas'; -import type { GitProjectModel } from '../../models'; +import type { ProjectModel } from '../../models'; import unreachableError from '../../utils/unreachableError'; export default async function verifyProjectSources( @@ -22,13 +23,15 @@ export default async function verifyProjectSources( for (const r of recipients) { if (r.type === 'repoDriver') { const accountId = await repoDriverContract.calcAccountId( - r.source.forge, - `${r.source.ownerName}/${r.source.repoName}`, + r.source.forge === 'github' + ? 0 + : unreachableError(`Unexpected forge: ${r.source.forge}`), + hexlify(toUtf8Bytes(`${r.source.ownerName}/${r.source.repoName}`)), ); if (accountId.toString() !== r.accountId) { throw new Error( - `Calculated project accountId '${accountId}' does not match the one in metadata ('${r.accountId}') for repo '${r.source.ownerName}/${r.source.repoName}' on '${r.source.forge}'.`, + `Failed to verify Project's source: calculated accountId '${accountId}' does not match the one in metadata ('${r.accountId}') for repo '${r.source.ownerName}/${r.source.repoName}' on '${r.source.forge}'.`, ); } } @@ -36,44 +39,49 @@ export default async function verifyProjectSources( } export async function verifyProjectMetadata( - project: GitProjectModel, + onChainProject: ProjectModel, metadata: AnyVersion, ): Promise { - if (!metadata) { - unreachableError(`Metadata for Git Project with ID ${project.id} is null.`); - } + const errors: string[] = []; - const errors = []; + const { + id: onChainProjectId, + name: onChainProjectName, + url: onChainProjectUrl, + } = onChainProject; - const { describes, source } = metadata; const { - url: metaUrl, - repoName: metaRepoName, - ownerName: metaOwnerName, - } = source; - const { id: onChainProjectId, name: onChainProjectName } = project; + describes, + source: { + url: metadataUrl, + repoName: metadataRepoName, + ownerName: metadataOwnerName, + }, + } = metadata; - if (`${metaOwnerName}/${metaRepoName}` !== `${onChainProjectName}`) { + if (`${metadataOwnerName}/${metadataRepoName}` !== onChainProjectName) { errors.push( - `repoName mismatch: got ${metaOwnerName}/${metaRepoName}, expected ${onChainProjectName}.`, + `- Repo name mismatch: got '${metadataOwnerName}/${metadataRepoName}', expected '${onChainProjectName}'.`, ); } - if (metaUrl !== project.url) { - errors.push(`url mismatch: got ${metaUrl}, expected ${project.url}.`); + if (metadataUrl !== onChainProjectUrl) { + errors.push( + `- URL mismatch: got '${metadataUrl}', expected '${onChainProjectUrl}'.`, + ); } if (describes.accountId !== onChainProjectId) { errors.push( - `accountId mismatch with: got ${describes.accountId}, expected ${onChainProjectId}.`, + `- Account ID mismatch: got '${describes.accountId}', expected '${onChainProjectId}'.`, ); } if (errors.length > 0) { throw new Error( - `Git Project with ID ${onChainProjectId} has metadata that does not match the metadata emitted by the contract (${errors.join( - '; ', - )}).`, + `Project metadata mismatch for project ID '${onChainProjectId}':\n${errors.join( + '\n', + )}`, ); } } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts index 1f5c641..6fff621 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts @@ -9,54 +9,56 @@ import type { } from '../../core/types'; import { AddressDriverSplitReceiverModel, + DripListModel, DripListSplitReceiverModel, - GitProjectModel, + ProjectModel, RepoDriverSplitReceiverModel, + StreamReceiverSeenEventModel, + SubListModel, SubListSplitReceiverModel, } from '../../models'; import type LogManager from '../../core/LogManager'; -import unreachableError from '../../utils/unreachableError'; import type { addressDriverSplitReceiverSchema, repoDriverSplitReceiverSchema, } from '../../metadata/schemas/repo-driver/v2'; import { - toAddressDriverId, - toImmutableSplitsDriverId, - toNftDriverId, - toRepoDriverId, + assertIsAccountId, + convertToAddressDriverId, + convertToImmutableSplitsDriverId, + convertToNftDriverId, + convertToRepoDriverId, } from '../../utils/accountIdUtils'; import getUserAddress from '../../utils/getAccountAddress'; import { AddressDriverSplitReceiverType } from '../../models/AddressDriverSplitReceiverModel'; import type { dripListSplitReceiverSchema } from '../../metadata/schemas/nft-driver/v2'; import type { subListSplitReceiverSchema } from '../../metadata/schemas/sub-list/v1'; -import { FORGES_MAP } from '../../core/constants'; -import { ProjectVerificationStatus } from '../../models/GitProjectModel'; +import type { Funder } from './buildFunderAccountFields'; +import buildFunderAccountFields, { + resolveDependencyType, +} from './buildFunderAccountFields'; +import RecoverableError from '../../utils/recoverableError'; +import { + calculateProjectStatus, + METADATA_FORGE_MAP, +} from '../../utils/projectUtils'; export async function createAddressReceiver({ - blockTimestamp, funder, - metadataReceiver, logManager, transaction, + blockTimestamp, + metadataReceiver, }: { - funder: - | { - type: 'project'; - accountId: RepoDriverId; - dependencyType: 'dependency' | 'maintainer'; - } - | { type: 'dripList'; accountId: NftDriverId } - | { type: 'ecosystem'; accountId: NftDriverId } - | { type: 'sub-list'; accountId: ImmutableSplitsDriverId }; - metadataReceiver: z.infer; - transaction: Transaction; + funder: Funder; blockTimestamp: Date; logManager: LogManager; + transaction: Transaction; + metadataReceiver: z.infer; }) { - const { weight, accountId: fundeeAccountId } = metadataReceiver; + const { weight, accountId } = metadataReceiver; - const accountId = toAddressDriverId(fundeeAccountId); + const fundeeAccountId = convertToAddressDriverId(accountId); let type: AddressDriverSplitReceiverType; if (funder.type === 'project') { @@ -72,38 +74,21 @@ export async function createAddressReceiver({ } // Create the receiver. - const [receiver, isReceiverCreated] = - await AddressDriverSplitReceiverModel.findOrCreate({ - lock: true, - transaction, - where: { - weight, - fundeeAccountId: accountId, - fundeeAccountAddress: getUserAddress(accountId), - type, - funderProjectId: funder.type === 'project' ? funder.accountId : null, - funderDripListId: funder.type === 'dripList' ? funder.accountId : null, - }, - defaults: { - blockTimestamp, - weight, - fundeeAccountId: accountId, - fundeeAccountAddress: getUserAddress(accountId), - type, - funderProjectId: funder.type === 'project' ? funder.accountId : null, - funderDripListId: funder.type === 'dripList' ? funder.accountId : null, - }, - }); - - if (!isReceiverCreated) { - unreachableError( - `Sub List receiver ${receiver.id} already exists for sub-list ${accountId}. This means the receiver was created outside the expected flow.`, - ); - } + const receiver = await AddressDriverSplitReceiverModel.create( + { + type, + weight, + blockTimestamp, + fundeeAccountId, + ...buildFunderAccountFields(funder), + fundeeAccountAddress: getUserAddress(accountId), + }, + { transaction }, + ); logManager.appendFindOrCreateLog( AddressDriverSplitReceiverModel, - isReceiverCreated, + true, receiver.id.toString(), ); } @@ -115,220 +100,151 @@ export async function createDripListReceiver({ logManager, transaction, }: { - funder: - | { type: 'project'; accountId: RepoDriverId } - | { type: 'dripList'; accountId: NftDriverId } - | { type: 'ecosystem'; accountId: NftDriverId } - | { type: 'sub-list'; accountId: ImmutableSplitsDriverId }; + funder: Funder; metadataReceiver: z.infer; transaction: Transaction; blockTimestamp: Date; logManager: LogManager; }) { - const { weight, accountId: fundeeDripListId } = metadataReceiver; + const { weight, accountId } = metadataReceiver; + const fundeeDripListId = convertToNftDriverId(accountId); - const dripListId = toNftDriverId(fundeeDripListId); + const dripList = await DripListModel.findByPk(fundeeDripListId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); - // Create the receiver. - const [receiver, isReceiverCreated] = - await DripListSplitReceiverModel.findOrCreate({ - lock: true, - transaction, - where: { - weight, - type: DependencyType.DripListDependency, - fundeeDripListId: dripListId, - funderProjectId: funder.type === 'project' ? funder.accountId : null, - funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, - funderDripListId: funder.type === 'dripList' ? funder.accountId : null, - funderEcosystemId: - funder.type === 'ecosystem' ? funder.accountId : null, - }, - defaults: { - blockTimestamp, - weight, - type: DependencyType.DripListDependency, - fundeeDripListId: dripListId, - funderProjectId: funder.type === 'project' ? funder.accountId : null, - funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, - funderDripListId: funder.type === 'dripList' ? funder.accountId : null, - funderEcosystemId: - funder.type === 'ecosystem' ? funder.accountId : null, - }, - }); - - if (!isReceiverCreated) { - unreachableError( - `Drip List receiver ${receiver.id} already exists for sub-list ${dripListId}. This means the receiver was created outside the expected flow.`, + if (!dripList) { + throw new RecoverableError( + `Drip List ${fundeeDripListId} not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, ); } + const receiver = await DripListSplitReceiverModel.create( + { + weight, + fundeeDripListId, + type: resolveDependencyType(funder), + ...buildFunderAccountFields(funder), + blockTimestamp, + }, + { + transaction, + }, + ); + logManager.appendFindOrCreateLog( DripListSplitReceiverModel, - isReceiverCreated, + true, receiver.id.toString(), ); } export async function createSubListReceiver({ - blockTimestamp, funder, - metadataReceiver, logManager, transaction, + blockTimestamp, + metadataReceiver, }: { - funder: - | { type: 'project'; accountId: RepoDriverId } - | { type: 'dripList'; accountId: NftDriverId } - | { type: 'ecosystem'; accountId: NftDriverId } - | { type: 'sub-list'; accountId: ImmutableSplitsDriverId }; - metadataReceiver: z.infer; - transaction: Transaction; + funder: Funder; blockTimestamp: Date; logManager: LogManager; + transaction: Transaction; + metadataReceiver: z.infer; }) { - const { weight, accountId: fundeeSubListId } = metadataReceiver; + const { weight, accountId } = metadataReceiver; + const fundeeSubListId = convertToImmutableSplitsDriverId(accountId); + + const [subList, isCreated] = await SubListModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + id: fundeeSubListId, + }, + defaults: { + id: fundeeSubListId, + isValid: false, // Until the related Sub List's metadata is processed. + }, + }); - const subListId = toImmutableSplitsDriverId(fundeeSubListId); + logManager.appendFindOrCreateLog(SubListModel, isCreated, subList.id); - // Create the receiver. - const [receiver, isReceiverCreated] = - await SubListSplitReceiverModel.findOrCreate({ - lock: true, - transaction, - where: { - weight, - type: DependencyType.EcosystemDependency, - fundeeSubListId: subListId, - funderProjectId: funder.type === 'project' ? funder.accountId : null, - funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, - funderDripListId: funder.type === 'dripList' ? funder.accountId : null, - funderEcosystemId: - funder.type === 'ecosystem' ? funder.accountId : null, - }, - defaults: { - weight, - blockTimestamp, - type: DependencyType.EcosystemDependency, - fundeeSubListId: subListId, - funderProjectId: funder.type === 'project' ? funder.accountId : null, - funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, - funderDripListId: funder.type === 'dripList' ? funder.accountId : null, - funderEcosystemId: - funder.type === 'ecosystem' ? funder.accountId : null, - }, - }); - - if (!isReceiverCreated) { - unreachableError( - `Sub List receiver ${receiver.id} already exists for sub-list ${subListId}. This means the receiver was created outside the expected flow.`, - ); - } + const receiver = await SubListSplitReceiverModel.create( + { + weight, + blockTimestamp, + fundeeSubListId, + type: DependencyType.EcosystemDependency, // All sub-list receivers are ecosystem dependencies (soon to be removed). + ...buildFunderAccountFields(funder), + }, + { transaction }, + ); logManager.appendFindOrCreateLog( SubListSplitReceiverModel, - isReceiverCreated, + true, receiver.id.toString(), ); } -export async function createProjectAndProjectReceiver({ - blockTimestamp, +export async function createProjectReceiver({ funder, - metadataReceiver, logManager, transaction, + blockTimestamp, + metadataReceiver, }: { - funder: - | { type: 'project'; accountId: RepoDriverId } - | { type: 'dripList'; accountId: NftDriverId } - | { type: 'ecosystem'; accountId: NftDriverId } - | { type: 'sub-list'; accountId: ImmutableSplitsDriverId }; - metadataReceiver: z.infer; - transaction: Transaction; + funder: Funder; blockTimestamp: Date; logManager: LogManager; + transaction: Transaction; + metadataReceiver: z.infer; }) { const { weight, - accountId: fundeeProjectId, - source: { forge, ownerName, repoName, url }, + accountId, + source: { url, ownerName, repoName, forge }, } = metadataReceiver; + const fundeeProjectId = convertToRepoDriverId(accountId); - const projectId = toRepoDriverId(fundeeProjectId); - - // Create the project the receiver represents. - const [project, isProjectCreated] = await GitProjectModel.findOrCreate({ - lock: true, + const [project, isCreated] = await ProjectModel.findOrCreate({ transaction, + lock: transaction.LOCK.UPDATE, where: { - id: projectId, + id: fundeeProjectId, }, defaults: { + id: fundeeProjectId, url, - isVisible: true, // During creation, the project is visible by default. Account metadata will set the final visibility. - isValid: true, // There are no receivers yet, so the project is valid. - id: projectId, + isVisible: true, // Default to visible on creation. Final visibility will be determined by account metadata. + isValid: true, // The project is valid by default since there are no receivers yet. name: `${ownerName}/${repoName}`, - verificationStatus: ProjectVerificationStatus.Unclaimed, - forge: - Object.values(FORGES_MAP).find( - (f) => f.toLocaleLowerCase() === forge.toLowerCase(), - ) ?? unreachableError(), + verificationStatus: calculateProjectStatus({ + id: fundeeProjectId, + color: null, + ownerAddress: null, + }), + forge: METADATA_FORGE_MAP[forge], }, }); - logManager - .appendFindOrCreateLog(GitProjectModel, isProjectCreated, project.id) - .logAllInfo(); + logManager.appendFindOrCreateLog(ProjectModel, isCreated, project.id); - let type: DependencyType; - if (funder.type === 'project') { - type = DependencyType.ProjectDependency; - } else if (funder.type === 'dripList') { - type = DependencyType.DripListDependency; - } else { - // TODO: For now we treat both ecosystem and sub-list cases as EcosystemDependency. Type will shortly be removed either way. - type = DependencyType.EcosystemDependency; - } - - // Create the receiver. - const [receiver, isReceiverCreated] = - await RepoDriverSplitReceiverModel.findOrCreate({ - lock: true, - transaction, - where: { - weight, - type, - fundeeProjectId: projectId, - funderProjectId: funder.type === 'project' ? funder.accountId : null, - funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, - funderDripListId: funder.type === 'dripList' ? funder.accountId : null, - funderEcosystemId: - funder.type === 'ecosystem' ? funder.accountId : null, - }, - defaults: { - weight, - type, - blockTimestamp, - fundeeProjectId: projectId, - funderProjectId: funder.type === 'project' ? funder.accountId : null, - funderSubListId: funder.type === 'sub-list' ? funder.accountId : null, - funderDripListId: funder.type === 'dripList' ? funder.accountId : null, - funderEcosystemId: - funder.type === 'ecosystem' ? funder.accountId : null, - }, - }); - - if (!isReceiverCreated) { - unreachableError( - `Project receiver ${receiver.id} already exists for project ${projectId}. This means the receiver was created outside the expected flow.`, - ); - } + const receiver = await RepoDriverSplitReceiverModel.create( + { + weight, + fundeeProjectId, + type: resolveDependencyType(funder), + ...buildFunderAccountFields(funder), + blockTimestamp, + }, + { transaction }, + ); logManager.appendFindOrCreateLog( RepoDriverSplitReceiverModel, - isReceiverCreated, + true, receiver.id.toString(), ); } @@ -336,7 +252,7 @@ export async function createProjectAndProjectReceiver({ type ClearReceiversInput = | { accountId: RepoDriverId; column: 'funderProjectId' } | { accountId: NftDriverId; column: 'funderDripListId' } - | { accountId: NftDriverId; column: 'funderEcosystemId' } + | { accountId: NftDriverId; column: 'funderEcosystemMainAccountId' } | { accountId: ImmutableSplitsDriverId; column: 'funderSubListId' }; type ClearReceiversParams = { @@ -348,38 +264,104 @@ type ClearReceiversParams = { export async function deleteExistingReceivers({ transaction, for: { accountId, column }, - excludeReceivers = [], }: ClearReceiversParams): Promise { - const baseWhere = { [column]: accountId }; - - const whereWithExclusions = - excludeReceivers.length > 0 - ? { - ...baseWhere, - funderProjectId: { [Op.notIn]: excludeReceivers }, - funderSubListId: { [Op.notIn]: excludeReceivers }, - funderDripListId: { [Op.notIn]: excludeReceivers }, - funderEcosystemId: { [Op.notIn]: excludeReceivers }, - } - : baseWhere; + const where = { [column]: accountId }; await AddressDriverSplitReceiverModel.destroy({ - where: whereWithExclusions, + where, transaction, }); await RepoDriverSplitReceiverModel.destroy({ - where: whereWithExclusions, + where, transaction, }); await DripListSplitReceiverModel.destroy({ - where: whereWithExclusions, + where, transaction, }); await SubListSplitReceiverModel.destroy({ - where: whereWithExclusions, + where, transaction, }); } + +export async function getCurrentSplitsByAccountId( + emitterAccountId: bigint, +): Promise { + assertIsAccountId(emitterAccountId); + + const addressSplits = await AddressDriverSplitReceiverModel.findAll({ + where: { + [Op.or]: [ + { funderProjectId: emitterAccountId }, + { funderDripListId: emitterAccountId }, + { funderEcosystemMainAccountId: emitterAccountId }, + { funderSubListId: emitterAccountId }, + ], + }, + lock: true, + }); + + const dripListSplits = await DripListSplitReceiverModel.findAll({ + where: { + [Op.or]: [ + { funderProjectId: emitterAccountId }, + { funderDripListId: emitterAccountId }, + { funderEcosystemMainAccountId: emitterAccountId }, + { funderSubListId: emitterAccountId }, + ], + }, + lock: true, + }); + + const projectSplits = await RepoDriverSplitReceiverModel.findAll({ + where: { + [Op.or]: [ + { funderProjectId: emitterAccountId }, + { funderDripListId: emitterAccountId }, + { funderEcosystemMainAccountId: emitterAccountId }, + { funderSubListId: emitterAccountId }, + ], + }, + lock: true, + }); + + const subListSplits = await SubListSplitReceiverModel.findAll({ + where: { + [Op.or]: [ + { funderProjectId: emitterAccountId }, + { funderDripListId: emitterAccountId }, + { funderEcosystemMainAccountId: emitterAccountId }, + { funderSubListId: emitterAccountId }, + ], + }, + lock: true, + }); + + const accountIds = [ + ...addressSplits.map((receiver) => receiver.fundeeAccountId), + ...dripListSplits.map((receiver) => receiver.fundeeDripListId), + ...projectSplits.map((receiver) => receiver.fundeeProjectId), + ...subListSplits.map((receiver) => receiver.fundeeSubListId), + ]; + + return Array.from(new Set(accountIds)); +} + +export async function getCurrentSplitsByReceiversHash( + receiversHash: string, +): Promise { + const streamReceiverSeenEvents = await StreamReceiverSeenEventModel.findAll({ + where: { + receiversHash, + }, + lock: true, + }); + + const accountIds = streamReceiverSeenEvents.map((event) => event.accountId); + + return Array.from(new Set(accountIds)); +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts b/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts index e219c38..22bc880 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts @@ -7,8 +7,10 @@ import { dripsContract } from '../../core/contractClients'; import { formatSplitReceivers } from '../../utils/formatSplitReceivers'; import type { SplitsReceiverStruct } from '../../../contracts/CURRENT_NETWORK/Drips'; +type AccountId = RepoDriverId | NftDriverId | ImmutableSplitsDriverId; + export default async function verifySplitsReceivers( - accountId: RepoDriverId | NftDriverId | ImmutableSplitsDriverId, + accountId: AccountId, splits: SplitsReceiverStruct[], ): Promise< [ @@ -17,13 +19,12 @@ export default async function verifySplitsReceivers( calculatedSplitsHash: string, ] > { - const formattedSplits = formatSplitReceivers(splits); - const calculatedSplitsHash = await dripsContract.hashSplits(formattedSplits); - const onChainSplitsHash = await dripsContract.splitsHash(accountId); + const calculatedHash = await dripsContract.hashSplits( + formatSplitReceivers(splits), + ); + const onChainHash = await dripsContract.splitsHash(accountId); - if (calculatedSplitsHash !== onChainSplitsHash) { - return [false, onChainSplitsHash, calculatedSplitsHash]; - } + const isValid = calculatedHash === onChainHash; - return [true, onChainSplitsHash, calculatedSplitsHash]; + return [isValid, onChainHash, calculatedHash]; } diff --git a/src/eventHandlers/GivenEventHandler.ts b/src/eventHandlers/GivenEventHandler.ts index 90eb394..0cedffe 100644 --- a/src/eventHandlers/GivenEventHandler.ts +++ b/src/eventHandlers/GivenEventHandler.ts @@ -1,6 +1,6 @@ import EventHandlerBase from '../events/EventHandlerBase'; import LogManager from '../core/LogManager'; -import { toAccountId } from '../utils/accountIdUtils'; +import { convertToAccountId } from '../utils/accountIdUtils'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import { GivenEventModel } from '../models'; import { dbConnection } from '../db/database'; @@ -22,8 +22,8 @@ export default class GivenEventHandler extends EventHandlerBase<'Given(uint256,u const [rawAccountId, rawReceiver, rawErc20, rawAmt] = args as GivenEvent.OutputTuple; - const accountId = toAccountId(rawAccountId); - const receiver = toAccountId(rawReceiver); + const accountId = convertToAccountId(rawAccountId); + const receiver = convertToAccountId(rawReceiver); const erc20 = toAddress(rawErc20); const amt = toBigIntString(rawAmt.toString()); @@ -41,14 +41,8 @@ export default class GivenEventHandler extends EventHandlerBase<'Given(uint256,u await dbConnection.transaction(async (transaction) => { const logManager = new LogManager(requestId); - const [givenEvent, isEventCreated] = await GivenEventModel.findOrCreate({ - lock: true, - transaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + const givenEvent = await GivenEventModel.create( + { accountId, receiver, erc20, @@ -58,15 +52,16 @@ export default class GivenEventHandler extends EventHandlerBase<'Given(uint256,u blockTimestamp, transactionHash, }, - }); + { transaction }, + ); logManager.appendFindOrCreateLog( GivenEventModel, - isEventCreated, + true, `${givenEvent.transactionHash}-${givenEvent.logIndex}`, ); - logManager.logAllInfo(); + logManager.logAllInfo(this.name); }); } } diff --git a/src/eventHandlers/OwnerUpdateRequestedEventHandler.ts b/src/eventHandlers/OwnerUpdateRequestedEventHandler.ts index 424f34e..7b4390f 100644 --- a/src/eventHandlers/OwnerUpdateRequestedEventHandler.ts +++ b/src/eventHandlers/OwnerUpdateRequestedEventHandler.ts @@ -2,20 +2,20 @@ import type { OwnerUpdateRequestedEvent as CurrentNetworkOwnerUpdateRequestedEve import type { OwnerUpdateRequestedEvent as FilecoinOwnerUpdateRequestedEvent } from '../../contracts/filecoin/RepoDriver'; import OwnerUpdateRequestedEventModel from '../models/OwnerUpdateRequestedEventModel'; import EventHandlerBase from '../events/EventHandlerBase'; -import { GitProjectModel } from '../models'; -import { ProjectVerificationStatus } from '../models/GitProjectModel'; -import { - calculateProjectStatus, - toForge, - toReadable, - toUrl, -} from '../utils/gitProjectUtils'; +import { ProjectModel } from '../models'; import LogManager from '../core/LogManager'; -import { toRepoDriverId } from '../utils/accountIdUtils'; -import { isLatestEvent } from '../utils/eventUtils'; +import { convertToRepoDriverId } from '../utils/accountIdUtils'; +import { isLatestEvent } from '../utils/isLatestEvent'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import { dbConnection } from '../db/database'; import { singleOrDefault } from '../utils/linq'; +import { + toForge, + toReadable, + toUrl, + calculateProjectStatus, +} from '../utils/projectUtils'; +import RecoverableError from '../utils/recoverableError'; export default class OwnerUpdateRequestedEventHandler extends EventHandlerBase< | 'OwnerUpdateRequested(uint256,uint8,bytes,address)' @@ -26,125 +26,99 @@ export default class OwnerUpdateRequestedEventHandler extends EventHandlerBase< 'OwnerUpdateRequested(uint256,uint8,bytes)' as const, ]; - protected async _handle( - request: EventHandlerRequest< - | 'OwnerUpdateRequested(uint256,uint8,bytes,address)' - | 'OwnerUpdateRequested(uint256,uint8,bytes)' - >, - ): Promise { - const { - id: requestId, - event: { args, logIndex, blockNumber, blockTimestamp, transactionHash }, - } = request; - + protected async _handle({ + id: requestId, + event: { + args, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + eventSignature, + }, + }: EventHandlerRequest< + | 'OwnerUpdateRequested(uint256,uint8,bytes,address)' + | 'OwnerUpdateRequested(uint256,uint8,bytes)' + >): Promise { const [accountId, forge, name] = args as | CurrentNetworkOwnerUpdateRequestedEvent.OutputTuple | FilecoinOwnerUpdateRequestedEvent.OutputTuple; - const repoDriverId = toRepoDriverId(accountId); const forgeAsString = toForge(forge); const decodedName = toReadable(name); LogManager.logRequestInfo( - `📥 ${this.name} is processing the following ${request.event.eventSignature}: - \r\t - forge: ${forgeAsString} - \r\t - name: ${decodedName} - \r\t - accountId: ${repoDriverId} - \r\t - logIndex: ${logIndex} - \r\t - blockNumber: ${blockNumber} - \r\t - tx hash: ${transactionHash}`, + [ + `📥 ${this.name} is processing the following ${eventSignature}:`, + ` - forge: ${forgeAsString}`, + ` - name: ${decodedName}`, + ` - accountId: ${accountId}`, + ` - logIndex: ${logIndex}`, + ` - blockNumber: ${blockNumber}`, + ` - txHash: ${transactionHash}`, + ].join('\n'), requestId, ); await dbConnection.transaction(async (transaction) => { const logManager = new LogManager(requestId); - const [ownerUpdateRequestedEvent, isEventCreated] = - await OwnerUpdateRequestedEventModel.findOrCreate({ - lock: true, - transaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + const projectId = convertToRepoDriverId(accountId); + + const ownerUpdateRequestedEvent = + await OwnerUpdateRequestedEventModel.create( + { logIndex, blockNumber, blockTimestamp, transactionHash, name: decodedName, forge: forgeAsString, - accountId: repoDriverId, + accountId: projectId, }, - }); + { transaction }, + ); logManager.appendFindOrCreateLog( OwnerUpdateRequestedEventModel, - isEventCreated, + true, `${ownerUpdateRequestedEvent.transactionHash}-${ownerUpdateRequestedEvent.logIndex}`, ); - // Depending on the order of processing, a project can be created: - // - By a `OwnerUpdateRequested` event. - // - By a `OwnerUpdated` event. - // - By an `AccountMetadataEmitted` event, as a (non existing in the DB) Project dependency of the account that emitted the metadata. - const [project, isProjectCreated] = await GitProjectModel.findOrCreate({ - transaction, - lock: true, - where: { - id: repoDriverId, - }, - defaults: { - isVisible: true, // During creation, the project is visible by default. Account metadata will set the final visibility. - id: repoDriverId, - isValid: true, // There are no receivers yet, so the project is valid. - name: decodedName, - forge: forgeAsString, - url: toUrl(forgeAsString, decodedName), - verificationStatus: ProjectVerificationStatus.OwnerUpdateRequested, - }, - }); - - if (isProjectCreated) { - logManager - .appendFindOrCreateLog(GitProjectModel, isProjectCreated, project.id) - .logAllInfo(); - - return; - } - - // Here, the Project already exists. - // Only if the event is the latest (in the DB), we process the metadata. - // After all events are processed, the Project will be updated with the latest values. if ( !(await isLatestEvent( ownerUpdateRequestedEvent, OwnerUpdateRequestedEventModel, { - logIndex, - transactionHash, accountId, }, transaction, )) ) { - logManager.logAllInfo(); + logManager.logAllInfo(this.name); return; } + const project = await ProjectModel.findByPk(projectId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!project) { + throw new RecoverableError( + `Project ${projectId} not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + project.name = decodedName; project.forge = forgeAsString; project.url = toUrl(forgeAsString, decodedName); project.verificationStatus = calculateProjectStatus(project); - logManager - .appendIsLatestEventLog() - .appendUpdateLog(project, GitProjectModel, project.id); - await project.save({ transaction }); - logManager.logAllInfo(); + logManager.logAllInfo(this.name); }); } @@ -156,9 +130,9 @@ export default class OwnerUpdateRequestedEventHandler extends EventHandlerBase< const [accountId] = args; const ownerAccountId = singleOrDefault( - await GitProjectModel.findAll({ + await ProjectModel.findAll({ where: { - id: toRepoDriverId(accountId), + id: convertToRepoDriverId(accountId), }, }), )?.ownerAccountId; diff --git a/src/eventHandlers/OwnerUpdatedEventHandler.ts b/src/eventHandlers/OwnerUpdatedEventHandler.ts index cf87492..093fcdd 100644 --- a/src/eventHandlers/OwnerUpdatedEventHandler.ts +++ b/src/eventHandlers/OwnerUpdatedEventHandler.ts @@ -1,127 +1,102 @@ import type { OwnerUpdatedEvent } from '../../contracts/CURRENT_NETWORK/RepoDriver'; import EventHandlerBase from '../events/EventHandlerBase'; -import { GitProjectModel, OwnerUpdatedEventModel } from '../models'; +import { ProjectModel, OwnerUpdatedEventModel } from '../models'; import LogManager from '../core/LogManager'; -import { calcAccountId, toRepoDriverId } from '../utils/accountIdUtils'; -import { calculateProjectStatus } from '../utils/gitProjectUtils'; -import { isLatestEvent } from '../utils/eventUtils'; +import { calcAccountId, convertToRepoDriverId } from '../utils/accountIdUtils'; +import { isLatestEvent } from '../utils/isLatestEvent'; import type EventHandlerRequest from '../events/EventHandlerRequest'; -import { ProjectVerificationStatus } from '../models/GitProjectModel'; import { dbConnection } from '../db/database'; +import { calculateProjectStatus } from '../utils/projectUtils'; +import RecoverableError from '../utils/recoverableError'; export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpdated(uint256,address)'> { public readonly eventSignatures = ['OwnerUpdated(uint256,address)' as const]; - protected async _handle( - request: EventHandlerRequest<'OwnerUpdated(uint256,address)'>, - ): Promise { - const { - id: requestId, - event: { args, logIndex, blockNumber, blockTimestamp, transactionHash }, - } = request; - + protected async _handle({ + id: requestId, + event: { + args, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + eventSignature, + }, + }: EventHandlerRequest<'OwnerUpdated(uint256,address)'>): Promise { const [accountId, owner] = args as OwnerUpdatedEvent.OutputTuple; - const repoDriverId = toRepoDriverId(accountId); - LogManager.logRequestInfo( - `📥 ${this.name} is processing the following ${request.event.eventSignature}: - \r\t - owner: ${owner} - \r\t - accountId: ${repoDriverId} - \r\t - logIndex: ${logIndex} - \r\t - blockNumber: ${blockNumber} - \r\t - tx hash: ${transactionHash}`, + [ + `📥 ${this.name} is processing the following ${eventSignature}:`, + ` - owner: ${owner}`, + ` - accountId: ${accountId}`, + ` - logIndex: ${logIndex}`, + ` - blockNumber: ${blockNumber}`, + ` - txHash: ${transactionHash}`, + ].join('\n'), requestId, ); await dbConnection.transaction(async (transaction) => { const logManager = new LogManager(requestId); - const [ownerUpdatedEvent, isEventCreated] = - await OwnerUpdatedEventModel.findOrCreate({ - lock: true, - transaction, - where: { - logIndex, - transactionHash, - }, - defaults: { - owner, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - accountId: repoDriverId, - }, - }); + const projectId = convertToRepoDriverId(accountId); + + const ownerUpdatedEvent = await OwnerUpdatedEventModel.create( + { + owner, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + accountId: projectId, + }, + { transaction }, + ); logManager.appendFindOrCreateLog( OwnerUpdatedEventModel, - isEventCreated, + true, `${ownerUpdatedEvent.transactionHash}-${ownerUpdatedEvent.logIndex}`, ); - // Depending on the order of processing, a project can be created: - // - By a `OwnerUpdateRequested` event. - // - By a `OwnerUpdated` event. - // - By an `AccountMetadataEmitted` event, as a (non existing in the DB) Project dependency of the account that emitted the metadata. - const [project, isProjectCreated] = await GitProjectModel.findOrCreate({ - transaction, - lock: true, - where: { - id: repoDriverId, - }, - defaults: { - isVisible: true, // During creation, the project is visible by default. Account metadata will set the final visibility. - id: repoDriverId, - isValid: true, // There are no receivers yet, so the project is valid. - ownerAddress: owner, - claimedAt: blockTimestamp, - ownerAccountId: await calcAccountId(owner), - verificationStatus: ProjectVerificationStatus.OwnerUpdated, - }, - }); - - if (isProjectCreated) { - logManager - .appendFindOrCreateLog(GitProjectModel, isProjectCreated, project.id) - .logAllInfo(); - - return; - } - - // Here, the Project already exists. - // Only if the event is the latest (in the DB), we process the metadata. - // After all events are processed, the Project will be updated with the latest values. + // Only process event further if this is the latest. if ( !isLatestEvent( ownerUpdatedEvent, OwnerUpdatedEventModel, { - logIndex, - transactionHash, - accountId: repoDriverId, + accountId: projectId, }, transaction, ) ) { - logManager.logAllInfo(); + logManager.logAllInfo(this.name); return; } + const project = await ProjectModel.findByPk(projectId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!project) { + throw new RecoverableError( + `Project ${projectId} not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + project.ownerAddress = owner; project.claimedAt = blockTimestamp; - project.ownerAccountId = (await calcAccountId(owner)) ?? null; + project.ownerAccountId = await calcAccountId(owner); project.verificationStatus = calculateProjectStatus(project); - logManager - .appendIsLatestEventLog() - .appendUpdateLog(project, GitProjectModel, project.id); + logManager.appendUpdateLog(project, ProjectModel, project.id); await project.save({ transaction }); - logManager.logAllInfo(); + logManager.logAllInfo(this.name); }); } diff --git a/src/eventHandlers/SplitsSetEventHandler/SplitsSetEventHandler.ts b/src/eventHandlers/SplitsSetEventHandler/SplitsSetEventHandler.ts index c716103..de9b554 100644 --- a/src/eventHandlers/SplitsSetEventHandler/SplitsSetEventHandler.ts +++ b/src/eventHandlers/SplitsSetEventHandler/SplitsSetEventHandler.ts @@ -1,6 +1,6 @@ import EventHandlerBase from '../../events/EventHandlerBase'; import LogManager from '../../core/LogManager'; -import { toAccountId } from '../../utils/accountIdUtils'; +import { convertToAccountId } from '../../utils/accountIdUtils'; import type EventHandlerRequest from '../../events/EventHandlerRequest'; import { SplitsSetEventModel } from '../../models'; import { dbConnection } from '../../db/database'; @@ -19,58 +19,52 @@ export default class SplitsSetEventHandler extends EventHandlerBase<'SplitsSet(u } = request; const [rawAccountId, rawReceiversHash] = args as SplitsSetEvent.OutputTuple; - const accountId = toAccountId(rawAccountId); + const accountId = convertToAccountId(rawAccountId); LogManager.logRequestInfo( - `📥 ${this.name} is processing the following ${request.event.eventSignature}: - \r\t - accountId: ${accountId} - \r\t - receiversHash: ${rawReceiversHash} - \r\t - logIndex: ${logIndex} - \r\t - tx hash: ${transactionHash}`, + [ + `📥 ${this.name} is processing the following ${request.event.eventSignature}:`, + ` - accountId: ${accountId}`, + ` - receiversHash: ${rawReceiversHash}`, + ` - logIndex: ${logIndex}`, + ` - txHash: ${transactionHash}`, + ].join('\n'), requestId, ); - const logManager = new LogManager(requestId); + await dbConnection.transaction(async (transaction) => { + const logManager = new LogManager(requestId); - const splitsSetEvent = await dbConnection.transaction( - async (transaction) => { - const [splitsSetEventModel, isEventCreated] = - await SplitsSetEventModel.findOrCreate({ - lock: true, - transaction, - where: { - logIndex, - transactionHash, - }, - defaults: { - accountId, - receiversHash: rawReceiversHash, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - }); + const splitsSetEvent = await SplitsSetEventModel.create( + { + accountId, + receiversHash: rawReceiversHash, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + { + transaction, + }, + ); - logManager.appendFindOrCreateLog( - SplitsSetEventModel, - isEventCreated, - `${splitsSetEventModel.transactionHash}-${splitsSetEventModel.logIndex}`, - ); + logManager.appendFindOrCreateLog( + SplitsSetEventModel, + true, + `${splitsSetEvent.transactionHash}-${splitsSetEvent.logIndex}`, + ); - return splitsSetEventModel; - }, - ); - - // Account's splits are set from `AccountMetadataEmitted` events. - // `SplitsSet` event only validates the splits receivers. + // Account's splits are set from `AccountMetadataEmitted` events. + // `SplitsSet` event only validates the splits receivers. - try { - await setIsValidFlag(splitsSetEvent, logManager); - } catch (error: any) { - logManager.logAllInfo(); + try { + await setIsValidFlag(splitsSetEvent, logManager, transaction); + } catch (error: any) { + logManager.logAllInfo(this.name); - throw error; - } + throw error; + } + }); } } diff --git a/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts index e89bd08..1a75f6a 100644 --- a/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts @@ -1,147 +1,168 @@ -import type { NftDriverId, RepoDriverId } from '../../core/types'; +import type { Transaction } from 'sequelize'; +import type { + ImmutableSplitsDriverId, + NftDriverId, + RepoDriverId, +} from '../../core/types'; import type { SplitsSetEventModel } from '../../models'; import { AddressDriverSplitReceiverModel, DripListModel, DripListSplitReceiverModel, - EcosystemModel, - GitProjectModel, + EcosystemMainAccountModel, + ProjectModel, RepoDriverSplitReceiverModel, + SubListModel, SubListSplitReceiverModel, } from '../../models'; -import { isNftDriverId, isRepoDriverId } from '../../utils/accountIdUtils'; +import { + isImmutableSplitsDriverId, + isNftDriverId, + isRepoDriverId, +} from '../../utils/accountIdUtils'; import type { SplitsReceiverStruct } from '../../../contracts/CURRENT_NETWORK/Drips'; import unreachableError from '../../utils/unreachableError'; import { dripsContract } from '../../core/contractClients'; import type LogManager from '../../core/LogManager'; import { formatSplitReceivers } from '../../utils/formatSplitReceivers'; +import RecoverableError from '../../utils/recoverableError'; export default async function setIsValidFlag( splitsSetEvent: SplitsSetEventModel, logManager: LogManager, + transaction: Transaction, ): Promise { const { accountId, receiversHash } = splitsSetEvent; const onChainSplitsHash = await dripsContract.splitsHash(accountId); - // Only if the `SplitsSet` event is the latest event (on-chain), we validate the splits. + // Only try to set the 'isValid' flag if this is the latest event. if (receiversHash !== onChainSplitsHash) { return; } - // Here, we know that the `SplitsSet` event is the latest event (on-chain). - if (isRepoDriverId(accountId)) { - const project = await GitProjectModel.findByPk(accountId, { - lock: true, + const project = await ProjectModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, }); if (!project) { - throw new Error( - `Failed to set 'isValid' flag for Project with ID '${accountId}': Project not found. - \r Possible reasons: - \r\t - The event that should have created the Project was not processed yet. - \r\t - The event was emitted as a result of a manual 'SetSplits' transaction that for a Project that does not exist in the app.`, + throw new RecoverableError( + `Failed to set 'isValid' flag for Project: Project '${accountId}' not found.`, ); } const storedInDbFromMetaReceiversHash = await dripsContract.hashSplits( - formatSplitReceivers(await getProjectDbReceivers(accountId)), + formatSplitReceivers(await getProjectDbReceivers(accountId, transaction)), ); - // If we reach this point, it means that `receiversHash` is the latest on-chain hash. - if (receiversHash !== storedInDbFromMetaReceiversHash) { project.isValid = false; - logManager.appendUpdateLog(project, GitProjectModel, project.id); + logManager.appendUpdateLog(project, ProjectModel, project.id); - await project.save(); + await project.save({ transaction }); - // We need to throw so that the job is retried... - throw new Error( - `Splits receivers hashes do not match for Project with ID '${accountId}': - \r\t - On-chain splits hash: ${onChainSplitsHash} - \r\t - 'SetSplits' event splits hash: ${receiversHash} - \r\t - DB (populated from metadata) splits hash: ${storedInDbFromMetaReceiversHash} - \r Possible reasons: - \r\t - The 'AccountMetadataEmitted' event that should have created the Project splits was not processed yet. - \r\t - The 'SetSplits' event (was manually emitted?) had splits that do not match what's already stored in the DB (from metadata).`, + throw new RecoverableError( + `Failed to set 'isValid' for Project '${accountId}': mismatch between on-chain, event, and DB splits receiver hashes (on-chain: ${onChainSplitsHash}, event: ${receiversHash}, db: ${storedInDbFromMetaReceiversHash}).`, ); } else if (project.isValid === false) { project.isValid = true; - logManager.appendUpdateLog(project, GitProjectModel, project.id); + logManager.appendUpdateLog(project, ProjectModel, project.id); - await project.save(); + await project.save({ transaction }); } } else if (isNftDriverId(accountId)) { - const [dripList, ecosystem] = await Promise.all([ - DripListModel.findByPk(accountId, { - lock: true, - }), - EcosystemModel.findByPk(accountId, { - lock: true, - }), - ]); + const dripList = await DripListModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + const ecosystem = await EcosystemMainAccountModel.findByPk(accountId, { + lock: transaction.LOCK.UPDATE, + transaction, + }); + + const entity = (dripList ?? ecosystem)!; + const entityModel = dripList ? DripListModel : EcosystemMainAccountModel; if (!dripList && !ecosystem) { - throw new Error( - `Failed to set 'isValid' flag for account with ID '${accountId}': Account not found. - \r Possible reasons: - \r\t - The event that should have created the Account was not processed yet. - \r\t - The event was emitted as a result of a manual 'SetSplits' transaction that for an Account that does not exist in the app.`, + throw new RecoverableError( + `Failed to set 'isValid' flag for ${entityModel.name}: ${entityModel.name} '${accountId}' not found.`, ); } - const entity = (dripList ?? ecosystem)!; - const entityModel = dripList ? DripListModel : EcosystemModel; - const storedInDbFromMetaReceiversHash = await dripsContract.hashSplits( formatSplitReceivers( entityModel.name === 'DripListModel' - ? await getDripListDbReceivers(accountId) - : await getEcosystemDbReceivers(accountId), + ? await getDripListDbReceivers(accountId, transaction) + : await getEcosystemDbReceivers(accountId, transaction), ), ); - // If we reach this point, it means that `receiversHash` is the latest on-chain hash. - if (receiversHash !== storedInDbFromMetaReceiversHash) { entity.isValid = false; logManager.appendUpdateLog(entity, entityModel, entity.id); - await entity.save(); - - // We need to throw so that the job is retried... - throw new Error( - `Splits receivers hashes do not match for ${entityModel.name} with ID '${accountId}': - \r\t - On-chain splits hash: ${onChainSplitsHash} - \r\t - 'SetSplits' event splits hash: ${receiversHash} - \r\t - DB (populated from metadata) splits hash: ${storedInDbFromMetaReceiversHash} - \r Possible reasons: - \r\t - The 'AccountMetadataEmitted' event that should have created the splits was not processed yet. - \r\t - The 'SetSplits' event (was manually emitted?) had splits that do not match what's already stored in the DB (from metadata).`, + await entity.save({ transaction }); + + throw new RecoverableError( + `Failed to set 'isValid' for ${dripList ? 'Drip List' : 'Ecosystem Main Identity'} '${accountId}': mismatch between on-chain, event, and DB splits receiver hashes (on-chain: ${onChainSplitsHash}, event: ${receiversHash}, db: ${storedInDbFromMetaReceiversHash}).`, ); } else if (entity.isValid === false) { entity.isValid = true; logManager.appendUpdateLog(entity, entityModel, entity.id); - await entity.save(); + await entity.save({ transaction }); + } + } else if (isImmutableSplitsDriverId(accountId)) { + const subList = await SubListModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!subList) { + throw new RecoverableError( + `Failed to set 'isValid' flag for Sub List: Sub List '${accountId}' not found.`, + ); } - } else { - logManager.appendLog( - `Skipping 'isValid' flag update for account with ID '${accountId}' because it's not a Project, Drip List, or Ecosystem.`, + + const storedInDbFromMetaReceiversHash = await dripsContract.hashSplits( + formatSplitReceivers(await getSubListDbReceivers(accountId, transaction)), ); + + // If we reach this point, it means that `receiversHash` is the latest on-chain hash. + + if (receiversHash !== storedInDbFromMetaReceiversHash) { + subList.isValid = false; + + logManager.appendUpdateLog(subList, SubListModel, subList.id); + + await subList.save({ transaction }); + + throw new RecoverableError( + `Failed to set 'isValid' for Sub List '${accountId}': mismatch between on-chain, event, and DB splits receiver hashes (on-chain: ${onChainSplitsHash}, event: ${receiversHash}, db: ${storedInDbFromMetaReceiversHash}).`, + ); + } else if (subList.isValid === false) { + subList.isValid = true; + + logManager.appendUpdateLog(subList, SubListModel, subList.id); + + await subList.save({ transaction }); + } } } -async function getProjectDbReceivers(accountId: RepoDriverId) { +async function getProjectDbReceivers( + accountId: RepoDriverId, + transaction: Transaction, +) { const addressReceivers: SplitsReceiverStruct[] = await AddressDriverSplitReceiverModel.findAll({ - lock: true, - + transaction, + lock: transaction.LOCK.UPDATE, where: { funderProjectId: accountId, }, @@ -154,8 +175,8 @@ async function getProjectDbReceivers(accountId: RepoDriverId) { const projectReceivers: SplitsReceiverStruct[] = await RepoDriverSplitReceiverModel.findAll({ - lock: true, - + transaction, + lock: transaction.LOCK.UPDATE, where: { funderProjectId: accountId, }, @@ -168,7 +189,8 @@ async function getProjectDbReceivers(accountId: RepoDriverId) { const dripListReceivers: SplitsReceiverStruct[] = await DripListSplitReceiverModel.findAll({ - lock: true, + transaction, + lock: transaction.LOCK.UPDATE, where: { funderProjectId: accountId, }, @@ -182,11 +204,14 @@ async function getProjectDbReceivers(accountId: RepoDriverId) { return [...addressReceivers, ...projectReceivers, ...dripListReceivers]; } -async function getDripListDbReceivers(accountId: NftDriverId) { +async function getDripListDbReceivers( + accountId: NftDriverId, + transaction: Transaction, +) { const addressReceivers: SplitsReceiverStruct[] = await AddressDriverSplitReceiverModel.findAll({ - lock: true, - + transaction, + lock: transaction.LOCK.UPDATE, where: { funderDripListId: accountId, }, @@ -199,8 +224,8 @@ async function getDripListDbReceivers(accountId: NftDriverId) { const projectReceivers: SplitsReceiverStruct[] = await RepoDriverSplitReceiverModel.findAll({ - lock: true, - + transaction, + lock: transaction.LOCK.UPDATE, where: { funderDripListId: accountId, }, @@ -213,8 +238,8 @@ async function getDripListDbReceivers(accountId: NftDriverId) { const dripListReceivers: SplitsReceiverStruct[] = await DripListSplitReceiverModel.findAll({ - lock: true, - + transaction, + lock: transaction.LOCK.UPDATE, where: { funderDripListId: accountId, }, @@ -228,13 +253,16 @@ async function getDripListDbReceivers(accountId: NftDriverId) { return [...addressReceivers, ...projectReceivers, ...dripListReceivers]; } -async function getEcosystemDbReceivers(accountId: NftDriverId) { +async function getEcosystemDbReceivers( + accountId: NftDriverId, + transaction: Transaction, +) { const projectReceivers: SplitsReceiverStruct[] = await RepoDriverSplitReceiverModel.findAll({ - lock: true, - + transaction, + lock: transaction.LOCK.UPDATE, where: { - funderEcosystemId: accountId, + funderEcosystemMainAccountId: accountId, }, }).then((receivers) => receivers.map((receiver) => ({ @@ -245,9 +273,10 @@ async function getEcosystemDbReceivers(accountId: NftDriverId) { const subListReceivers: SplitsReceiverStruct[] = await SubListSplitReceiverModel.findAll({ - lock: true, + transaction, + lock: transaction.LOCK.UPDATE, where: { - funderEcosystemId: accountId, + funderEcosystemMainAccountId: accountId, }, }).then((receivers) => receivers.map((receiver) => ({ @@ -258,3 +287,71 @@ async function getEcosystemDbReceivers(accountId: NftDriverId) { return [...projectReceivers, ...subListReceivers]; } + +async function getSubListDbReceivers( + accountId: ImmutableSplitsDriverId, + transaction: Transaction, +) { + const addressReceivers: SplitsReceiverStruct[] = + await AddressDriverSplitReceiverModel.findAll({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + funderSubListId: accountId, + }, + }).then((receivers) => + receivers.map((receiver) => ({ + accountId: receiver.fundeeAccountId ?? unreachableError(), + weight: receiver.weight, + })), + ); + + const projectReceivers: SplitsReceiverStruct[] = + await RepoDriverSplitReceiverModel.findAll({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + funderSubListId: accountId, + }, + }).then((receivers) => + receivers.map((receiver) => ({ + accountId: receiver.fundeeProjectId ?? unreachableError(), + weight: receiver.weight, + })), + ); + + const dripListReceivers: SplitsReceiverStruct[] = + await DripListSplitReceiverModel.findAll({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + funderSubListId: accountId, + }, + }).then((receivers) => + receivers.map((receiver) => ({ + accountId: receiver.fundeeDripListId ?? unreachableError(), + weight: receiver.weight, + })), + ); + + const subListReceivers: SplitsReceiverStruct[] = + await SubListSplitReceiverModel.findAll({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + funderSubListId: accountId, + }, + }).then((receivers) => + receivers.map((receiver) => ({ + accountId: receiver.fundeeSubListId ?? unreachableError(), + weight: receiver.weight, + })), + ); + + return [ + ...projectReceivers, + ...subListReceivers, + ...dripListReceivers, + ...addressReceivers, + ]; +} diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index 03aceb8..cf3cc21 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -2,89 +2,107 @@ import { ZeroAddress } from 'ethers'; import type { TransferEvent } from '../../contracts/CURRENT_NETWORK/NftDriver'; import EventHandlerBase from '../events/EventHandlerBase'; import LogManager from '../core/LogManager'; -import { calcAccountId, toNftDriverId } from '../utils/accountIdUtils'; +import { calcAccountId, convertToNftDriverId } from '../utils/accountIdUtils'; import type EventHandlerRequest from '../events/EventHandlerRequest'; -import { DripListModel, EcosystemModel, TransferEventModel } from '../models'; +import { + DripListModel, + EcosystemMainAccountModel, + TransferEventModel, +} from '../models'; import { dbConnection } from '../db/database'; -import { isLatestEvent } from '../utils/eventUtils'; +import { isLatestEvent } from '../utils/isLatestEvent'; import appSettings from '../config/appSettings'; +import RecoverableError from '../utils/recoverableError'; export default class TransferEventHandler extends EventHandlerBase<'Transfer(address,address,uint256)'> { public eventSignatures = ['Transfer(address,address,uint256)' as const]; - protected async _handle( - request: EventHandlerRequest<'Transfer(address,address,uint256)'>, - ): Promise { - const { - id: requestId, - event: { args, logIndex, blockNumber, blockTimestamp, transactionHash }, - } = request; - - const [from, to, tokenId] = args as TransferEvent.OutputTuple; - - const id = toNftDriverId(tokenId); + protected async _handle({ + id: requestId, + event: { + args, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + eventSignature, + }, + }: EventHandlerRequest<'Transfer(address,address,uint256)'>): Promise { + const [from, to, rawTokenId] = args as TransferEvent.OutputTuple; LogManager.logRequestInfo( - `📥 ${this.name} is processing the following ${request.event.eventSignature}: - \r\t - from: ${from} - \r\t - to: ${to} - \r\t - tokenId: ${tokenId} - \r\t - logIndex: ${logIndex} - \r\t - tx hash: ${transactionHash}`, + [ + `📥 ${this.name} is processing ${eventSignature}:`, + ` - from: ${from}`, + ` - to: ${to}`, + ` - tokenId: ${rawTokenId}`, + ` - logIndex: ${logIndex}`, + ` - txHash: ${transactionHash}`, + ].join('\n'), requestId, ); await dbConnection.transaction(async (transaction) => { const logManager = new LogManager(requestId); - const [transferEvent, isEventCreated] = - await TransferEventModel.findOrCreate({ - lock: true, + const tokenId = convertToNftDriverId(rawTokenId); + + const transferEvent = await TransferEventModel.create( + { + tokenId, + to, + from, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + { transaction, - where: { logIndex, transactionHash }, - defaults: { - tokenId: id, - to, - from, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - }); + }, + ); logManager.appendFindOrCreateLog( TransferEventModel, - isEventCreated, + true, `${transferEvent.transactionHash}-${transferEvent.logIndex}`, ); - const isLatest = await isLatestEvent( - transferEvent, - TransferEventModel, - { transactionHash, logIndex, tokenId }, - transaction, - ); - - if (!isLatest) { - logManager.logAllInfo(); + // Only process if this is the latest event. + if ( + !(await isLatestEvent( + transferEvent, + TransferEventModel, + { tokenId }, + transaction, + )) + ) { + logManager.logAllInfo(this.name); return; } - const [dripList, ecosystem] = await Promise.all([ - DripListModel.findOne({ transaction, lock: true, where: { id } }), - EcosystemModel.findOne({ transaction, lock: true, where: { id } }), - ]); + const dripList = await DripListModel.findByPk(tokenId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + const ecosystemMainAccount = await EcosystemMainAccountModel.findByPk( + tokenId, + { + transaction, + lock: transaction.LOCK.UPDATE, + }, + ); - if (!dripList && !ecosystem) { - throw new Error( - `Drip List or Ecosystem not found for tokenId '${tokenId}'. Maybe the 'AccountMetadataEmitted' event that should have created the entity was not processed yet?`, + if (!dripList && !ecosystemMainAccount) { + throw new RecoverableError( + `Drip List or Ecosystem Main Account '${tokenId}' not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, ); } - const entity = (dripList ?? ecosystem)!; - const entityModel = dripList ? DripListModel : EcosystemModel; + const entity = (dripList ?? ecosystemMainAccount)!; + const entityModel = dripList ? DripListModel : EcosystemMainAccountModel; entity.ownerAddress = to; entity.previousOwnerAddress = from; @@ -92,17 +110,15 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add entity.creator = to; // TODO: https://github.com/drips-network/events-processor/issues/14 entity.isVisible = blockNumber > appSettings.visibilityThresholdBlockNumber - ? from === ZeroAddress + ? from === ZeroAddress || from === appSettings.ecosystemDeployer : true; - entity.isValid = true; + entity.isValid = true; // The entity is initialized with `false` when created during account metadata processing. - logManager - .appendIsLatestEventLog() - .appendUpdateLog(entity, entityModel, entity.id); + logManager.appendUpdateLog(entity, entityModel, entity.id); await entity.save({ transaction }); - logManager.logAllInfo(); + logManager.logAllInfo(this.name); }); } diff --git a/src/models/AddressDriverSplitReceiverModel.ts b/src/models/AddressDriverSplitReceiverModel.ts index 4e7362c..cd67c88 100644 --- a/src/models/AddressDriverSplitReceiverModel.ts +++ b/src/models/AddressDriverSplitReceiverModel.ts @@ -7,7 +7,7 @@ import type { import { DataTypes, Model } from 'sequelize'; import type { AddressLike } from 'ethers'; import getSchema from '../utils/getSchema'; -import GitProjectModel from './GitProjectModel'; +import ProjectModel from './ProjectModel'; import type { AddressDriverId, ImmutableSplitsDriverId, @@ -15,7 +15,7 @@ import type { RepoDriverId, } from '../core/types'; import DripListModel from './DripListModel'; -import EcosystemModel from './EcosystemModel'; +import EcosystemMainAccountModel from './EcosystemMainAccountModel'; import SubListModel from './SubListModel'; export enum AddressDriverSplitReceiverType { @@ -34,7 +34,7 @@ export default class AddressDriverSplitReceiverModel extends Model< public declare fundeeAccountAddress: AddressLike; public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key - public declare funderEcosystemId: NftDriverId | null; // Foreign key + public declare funderEcosystemMainAccountId: NftDriverId | null; // Foreign key public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key public declare weight: number; @@ -61,7 +61,7 @@ export default class AddressDriverSplitReceiverModel extends Model< // Foreign key type: DataTypes.STRING, references: { - model: GitProjectModel, + model: ProjectModel, key: 'id', }, allowNull: true, @@ -75,11 +75,11 @@ export default class AddressDriverSplitReceiverModel extends Model< }, allowNull: true, }, - funderEcosystemId: { + funderEcosystemMainAccountId: { // Foreign key type: DataTypes.STRING, references: { - model: EcosystemModel, + model: EcosystemMainAccountModel, key: 'id', }, allowNull: true, @@ -127,7 +127,7 @@ export default class AddressDriverSplitReceiverModel extends Model< unique: false, }, { - fields: ['funderEcosystemId'], + fields: ['funderEcosystemMainAccountId'], name: `IX_AddressDriverSplitReceivers_funderEcosystemId`, where: { type: AddressDriverSplitReceiverType.EcosystemDependency, diff --git a/src/models/DripListSplitReceiverModel.ts b/src/models/DripListSplitReceiverModel.ts index 101e559..eace747 100644 --- a/src/models/DripListSplitReceiverModel.ts +++ b/src/models/DripListSplitReceiverModel.ts @@ -13,8 +13,8 @@ import type { RepoDriverId, } from '../core/types'; import DripListModel from './DripListModel'; -import GitProjectModel from './GitProjectModel'; -import EcosystemModel from './EcosystemModel'; +import ProjectModel from './ProjectModel'; +import EcosystemMainAccountModel from './EcosystemMainAccountModel'; import SubListModel from './SubListModel'; export default class DripListSplitReceiverModel extends Model< @@ -25,7 +25,7 @@ export default class DripListSplitReceiverModel extends Model< public declare fundeeDripListId: NftDriverId; // Foreign key public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key - public declare funderEcosystemId: NftDriverId | null; // Foreign key + public declare funderEcosystemMainAccountId: NftDriverId | null; // Foreign key public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key public declare weight: number; @@ -53,7 +53,7 @@ export default class DripListSplitReceiverModel extends Model< // Foreign key type: DataTypes.STRING, references: { - model: GitProjectModel, + model: ProjectModel, key: 'id', }, allowNull: true, @@ -67,11 +67,11 @@ export default class DripListSplitReceiverModel extends Model< }, allowNull: true, }, - funderEcosystemId: { + funderEcosystemMainAccountId: { // Foreign key type: DataTypes.STRING, references: { - model: EcosystemModel, + model: EcosystemMainAccountModel, key: 'id', }, allowNull: true, @@ -125,7 +125,7 @@ export default class DripListSplitReceiverModel extends Model< unique: false, }, { - fields: ['funderEcosystemId'], + fields: ['funderEcosystemMainAccountId'], name: `IX_DripListSplitReceivers_funderEcosystemId`, where: { type: DependencyType.EcosystemDependency, diff --git a/src/models/EcosystemModel.ts b/src/models/EcosystemMainAccountModel.ts similarity index 90% rename from src/models/EcosystemModel.ts rename to src/models/EcosystemMainAccountModel.ts index 8cd8988..95fdbce 100644 --- a/src/models/EcosystemModel.ts +++ b/src/models/EcosystemMainAccountModel.ts @@ -8,9 +8,9 @@ import type { AddressLike } from 'ethers'; import type { AccountId, NftDriverId } from '../core/types'; import getSchema from '../utils/getSchema'; -export default class EcosystemModel extends Model< - InferAttributes, - InferCreationAttributes +export default class EcosystemMainAccountModel extends Model< + InferAttributes, + InferCreationAttributes > { public declare id: NftDriverId; public declare isValid: boolean; @@ -70,7 +70,7 @@ export default class EcosystemModel extends Model< { sequelize, schema: getSchema(), - tableName: 'Ecosystems', + tableName: 'EcosystemMainIdentities', indexes: [ { fields: ['ownerAddress'], diff --git a/src/models/GitProjectModel.ts b/src/models/ProjectModel.ts similarity index 95% rename from src/models/GitProjectModel.ts rename to src/models/ProjectModel.ts index 2a2144c..d00b193 100644 --- a/src/models/GitProjectModel.ts +++ b/src/models/ProjectModel.ts @@ -18,9 +18,9 @@ export enum ProjectVerificationStatus { PendingMetadata = 'PendingMetadata', } -export default class GitProjectModel extends Model< - InferAttributes, - InferCreationAttributes +export default class ProjectModel extends Model< + InferAttributes, + InferCreationAttributes > { public declare id: RepoDriverId; // The `accountId` from `OwnerUpdatedRequested` event. public declare isValid: boolean; @@ -57,7 +57,7 @@ export default class GitProjectModel extends Model< validate: { isValidName(value: string) { if (!value) { - throw new Error('Project name is required.'); + return; } const components = value?.split('/'); diff --git a/src/models/RepoDriverSplitReceiverModel.ts b/src/models/RepoDriverSplitReceiverModel.ts index edee010..30c4632 100644 --- a/src/models/RepoDriverSplitReceiverModel.ts +++ b/src/models/RepoDriverSplitReceiverModel.ts @@ -6,7 +6,7 @@ import type { } from 'sequelize'; import { DataTypes, Model } from 'sequelize'; import getSchema from '../utils/getSchema'; -import GitProjectModel from './GitProjectModel'; +import ProjectModel from './ProjectModel'; import { DependencyType } from '../core/types'; import type { ImmutableSplitsDriverId, @@ -14,7 +14,7 @@ import type { RepoDriverId, } from '../core/types'; import DripListModel from './DripListModel'; -import EcosystemModel from './EcosystemModel'; +import EcosystemMainAccountModel from './EcosystemMainAccountModel'; import SubListModel from './SubListModel'; export default class RepoDriverSplitReceiverModel extends Model< @@ -25,7 +25,7 @@ export default class RepoDriverSplitReceiverModel extends Model< public declare fundeeProjectId: RepoDriverId; // Foreign key public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key - public declare funderEcosystemId: NftDriverId | null; // Foreign key + public declare funderEcosystemMainAccountId: NftDriverId | null; // Foreign key public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key public declare weight: number; @@ -44,7 +44,7 @@ export default class RepoDriverSplitReceiverModel extends Model< // Foreign key type: DataTypes.STRING, references: { - model: GitProjectModel, + model: ProjectModel, key: 'id', }, allowNull: false, @@ -53,7 +53,7 @@ export default class RepoDriverSplitReceiverModel extends Model< // Foreign key type: DataTypes.STRING, references: { - model: GitProjectModel, + model: ProjectModel, key: 'id', }, allowNull: true, @@ -67,11 +67,11 @@ export default class RepoDriverSplitReceiverModel extends Model< }, allowNull: true, }, - funderEcosystemId: { + funderEcosystemMainAccountId: { // Foreign key type: DataTypes.STRING, references: { - model: EcosystemModel, + model: EcosystemMainAccountModel, key: 'id', }, allowNull: true, @@ -125,7 +125,7 @@ export default class RepoDriverSplitReceiverModel extends Model< unique: false, }, { - fields: ['funderEcosystemId'], + fields: ['funderEcosystemMainAccountId'], name: `IX_RepoDriverSplitReceivers_funderEcosystemId`, where: { type: DependencyType.EcosystemDependency, diff --git a/src/models/SubListModel.ts b/src/models/SubListModel.ts index 837d4a9..53e837b 100644 --- a/src/models/SubListModel.ts +++ b/src/models/SubListModel.ts @@ -13,11 +13,12 @@ export default class SubListModel extends Model< > { public declare id: ImmutableSplitsDriverId; public declare parentDripListId: NftDriverId | null; - public declare parentEcosystemId: NftDriverId | null; + public declare parentEcosystemMainAccountId: NftDriverId | null; public declare parentSubListId: ImmutableSplitsDriverId | null; public declare rootDripListId: NftDriverId | null; - public declare rootEcosystemId: NftDriverId | null; + public declare rootEcosystemMainAccountId: NftDriverId | null; public declare lastProcessedIpfsHash: string | null; + public declare isValid: boolean; public static initialize(sequelize: Sequelize): void { this.init( @@ -35,12 +36,12 @@ export default class SubListModel extends Model< key: 'id', }, }, - parentEcosystemId: { + parentEcosystemMainAccountId: { // Foreign key type: DataTypes.STRING, allowNull: true, references: { - model: 'Ecosystems', + model: 'EcosystemMainIdentities', key: 'id', }, }, @@ -62,12 +63,12 @@ export default class SubListModel extends Model< key: 'id', }, }, - rootEcosystemId: { + rootEcosystemMainAccountId: { // Foreign key type: DataTypes.STRING, allowNull: true, references: { - model: 'Ecosystems', + model: 'EcosystemMainIdentities', key: 'id', }, }, @@ -75,6 +76,10 @@ export default class SubListModel extends Model< type: DataTypes.TEXT, allowNull: true, }, + isValid: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, }, { sequelize, @@ -87,7 +92,7 @@ export default class SubListModel extends Model< unique: false, }, { - fields: ['parentEcosystemId'], + fields: ['parentEcosystemMainAccountId'], name: `IX_SubLists_parentEcosystemId`, unique: false, }, @@ -102,7 +107,7 @@ export default class SubListModel extends Model< unique: false, }, { - fields: ['rootEcosystemId'], + fields: ['rootEcosystemMainAccountId'], name: `IX_SubLists_rootEcosystemId`, unique: false, }, diff --git a/src/models/SubListSplitReceiverModel.ts b/src/models/SubListSplitReceiverModel.ts index 5017195..94f374a 100644 --- a/src/models/SubListSplitReceiverModel.ts +++ b/src/models/SubListSplitReceiverModel.ts @@ -13,9 +13,9 @@ import type { RepoDriverId, } from '../core/types'; import DripListModel from './DripListModel'; -import GitProjectModel from './GitProjectModel'; +import ProjectModel from './ProjectModel'; import SubListModel from './SubListModel'; -import EcosystemModel from './EcosystemModel'; +import EcosystemMainAccountModel from './EcosystemMainAccountModel'; export default class SubListSplitReceiverModel extends Model< InferAttributes, @@ -25,7 +25,7 @@ export default class SubListSplitReceiverModel extends Model< public declare fundeeSubListId: ImmutableSplitsDriverId; // Foreign key public declare funderProjectId: RepoDriverId | null; // Foreign key public declare funderDripListId: NftDriverId | null; // Foreign key - public declare funderEcosystemId: NftDriverId | null; // Foreign key + public declare funderEcosystemMainAccountId: NftDriverId | null; // Foreign key public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key public declare weight: number; @@ -53,7 +53,7 @@ export default class SubListSplitReceiverModel extends Model< // Foreign key type: DataTypes.STRING, references: { - model: GitProjectModel, + model: ProjectModel, key: 'id', }, allowNull: true, @@ -67,11 +67,11 @@ export default class SubListSplitReceiverModel extends Model< }, allowNull: true, }, - funderEcosystemId: { + funderEcosystemMainAccountId: { // Foreign key type: DataTypes.STRING, references: { - model: EcosystemModel, + model: EcosystemMainAccountModel, key: 'id', }, allowNull: true, @@ -80,7 +80,7 @@ export default class SubListSplitReceiverModel extends Model< // Foreign key type: DataTypes.STRING, references: { - model: EcosystemModel, + model: EcosystemMainAccountModel, key: 'id', }, allowNull: true, @@ -125,7 +125,7 @@ export default class SubListSplitReceiverModel extends Model< unique: false, }, { - fields: ['funderEcosystemId'], + fields: ['funderEcosystemMainAccountId'], name: `IX_SubListSplitReceivers_funderEcosystemId`, where: { type: DependencyType.EcosystemDependency, diff --git a/src/models/index.ts b/src/models/index.ts index d4223af..10f967d 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -2,8 +2,8 @@ export { default as SubListModel } from './SubListModel'; export { default as DripListModel } from './DripListModel'; export { default as GivenEventModel } from './GivenEventModel'; export { default as SplitEventModel } from './SplitEventModel'; -export { default as GitProjectModel } from './GitProjectModel'; -export { default as EcosystemModel } from './EcosystemModel'; +export { default as ProjectModel } from './ProjectModel'; +export { default as EcosystemMainAccountModel } from './EcosystemMainAccountModel'; export { default as TransferEventModel } from './TransferEventModel'; export { default as SplitsSetEventModel } from './SplitsSetEventModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index a753f90..67f86e5 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -10,10 +10,11 @@ import { addressDriverContract } from '../core/contractClients'; import { getContractNameFromAccountId } from './contractUtils'; // RepoDriver -export function isRepoDriverId(id: string): id is RepoDriverId { - const isNaN = Number.isNaN(Number(id)); +export function isRepoDriverId(id: string | bigint): id is RepoDriverId { + const idStr = typeof id === 'bigint' ? id.toString() : id; + const isNaN = Number.isNaN(Number(idStr)); const isAccountIdOfRepoDriver = - getContractNameFromAccountId(id) === 'repoDriver'; + getContractNameFromAccountId(idStr) === 'repoDriver'; if (isNaN || !isAccountIdOfRepoDriver) { return false; @@ -22,21 +23,22 @@ export function isRepoDriverId(id: string): id is RepoDriverId { return true; } -export function toRepoDriverId(id: bigint | string): RepoDriverId { +export function convertToRepoDriverId(id: bigint | string): RepoDriverId { const repoDriverId = typeof id === 'bigint' ? id.toString() : id; if (!isRepoDriverId(repoDriverId)) { - throw new Error(`Invalid 'RepoDriver' account ID: ${id}.`); + throw new Error(`Failed to convert: '${id}' is not a valid RepoDriver ID.`); } return repoDriverId as RepoDriverId; } // NftDriver -export function isNftDriverId(id: string): id is NftDriverId { - const isNaN = Number.isNaN(Number(id)); +export function isNftDriverId(id: string | bigint): id is NftDriverId { + const idStr = typeof id === 'bigint' ? id.toString() : id; + const isNaN = Number.isNaN(Number(idStr)); const isAccountIdOfNftDriver = - getContractNameFromAccountId(id) === 'nftDriver'; + getContractNameFromAccountId(idStr) === 'nftDriver'; if (isNaN || !isAccountIdOfNftDriver) { return false; @@ -45,11 +47,11 @@ export function isNftDriverId(id: string): id is NftDriverId { return true; } -export function toNftDriverId(id: bigint | string): NftDriverId { +export function convertToNftDriverId(id: bigint | string): NftDriverId { const nftDriverId = typeof id === 'bigint' ? id.toString() : id; if (!isNftDriverId(nftDriverId)) { - throw new Error(`Invalid 'NftDriver' account ID: ${id}.`); + throw new Error(`Failed to convert: '${id}' is not a valid NftDriver ID.`); } return nftDriverId as NftDriverId; @@ -57,12 +59,12 @@ export function toNftDriverId(id: bigint | string): NftDriverId { // AddressDriver export function isAddressDriverId( - idAsString: string, -): idAsString is AddressDriverId { - const isNaN = Number.isNaN(Number(idAsString)); + idString: string, +): idString is AddressDriverId { + const isNaN = Number.isNaN(Number(idString)); const isAccountIdOfAddressDriver = - getContractNameFromAccountId(idAsString) === 'addressDriver'; + getContractNameFromAccountId(idString) === 'addressDriver'; if (isNaN || !isAccountIdOfAddressDriver) { return false; @@ -71,9 +73,11 @@ export function isAddressDriverId( return true; } -export function toAddressDriverId(id: string): AddressDriverId { +export function convertToAddressDriverId(id: string): AddressDriverId { if (!isAddressDriverId(id)) { - throw new Error(`Invalid 'AddressDriver' account ID: ${id}.`); + throw new Error( + `Failed to convert: '${id}' is not a valid AddressDriver ID.`, + ); } return id as AddressDriverId; @@ -83,17 +87,20 @@ export function assertAddressDiverId( id: string, ): asserts id is AddressDriverId { if (!isAddressDriverId(id)) { - throw new Error(`String ${id} is not a valid 'AddressDriverId'.`); + throw new Error( + `Failed to assert: '${id}' is not a valid AddressDriver ID.`, + ); } } // ImmutableSplitsDriver export function isImmutableSplitsDriverId( - id: string, + id: string | bigint, ): id is ImmutableSplitsDriverId { - const isNaN = Number.isNaN(Number(id)); + const idString = typeof id === 'bigint' ? id.toString() : id; + const isNaN = Number.isNaN(Number(idString)); const immutableSplitsDriverId = - getContractNameFromAccountId(id) === 'immutableSplitsDriver'; + getContractNameFromAccountId(idString) === 'immutableSplitsDriver'; if (isNaN || !immutableSplitsDriverId) { return false; @@ -102,13 +109,15 @@ export function isImmutableSplitsDriverId( return true; } -export function toImmutableSplitsDriverId( +export function convertToImmutableSplitsDriverId( id: string | bigint, ): ImmutableSplitsDriverId { const stringId = typeof id === 'bigint' ? id.toString() : id; if (!isImmutableSplitsDriverId(stringId)) { - throw new Error(`Invalid 'ImmutableSplitsDriver' account ID: ${stringId}.`); + throw new Error( + `Failed to convert: '${id}' is not a valid ImmutableSplitsDriver ID.`, + ); } return stringId as ImmutableSplitsDriverId; @@ -121,17 +130,34 @@ export async function calcAccountId(owner: AddressLike): Promise { } // Account ID -export function toAccountId(id: bigint | string): AccountId { - const accountIdAsString = typeof id === 'bigint' ? id.toString() : id; +export function convertToAccountId(id: bigint | string): AccountId { + const accountidString = typeof id === 'bigint' ? id.toString() : id; if ( - isRepoDriverId(accountIdAsString) || - isNftDriverId(accountIdAsString) || - isAddressDriverId(accountIdAsString) || - isImmutableSplitsDriverId(accountIdAsString) + isRepoDriverId(accountidString) || + isNftDriverId(accountidString) || + isAddressDriverId(accountidString) || + isImmutableSplitsDriverId(accountidString) ) { - return accountIdAsString as AccountId; + return accountidString as AccountId; } - throw new Error(`Invalid account ID: ${accountIdAsString}.`); + throw new Error(`Failed to convert: '${id}' is not a valid account ID.`); +} + +export function assertIsAccountId( + id: string | bigint, +): asserts id is AccountId { + const accountId = typeof id === 'bigint' ? id.toString() : id; + + if ( + !isRepoDriverId(accountId) && + !isNftDriverId(accountId) && + !isAddressDriverId(accountId) && + !isImmutableSplitsDriverId(accountId) + ) { + throw new Error( + `Failed to assert: '${accountId}' is not a valid account ID.`, + ); + } } diff --git a/src/utils/eventUtils.ts b/src/utils/eventUtils.ts deleted file mode 100644 index 8105832..0000000 --- a/src/utils/eventUtils.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { FindOptions, Model, Transaction, WhereOptions } from 'sequelize'; -import type { IEventModel } from '../events/types'; - -export async function isLatestEvent>( - incomingEvent: T, - model: { findOne(options: FindOptions): Promise }, - where: WhereOptions, - transaction: Transaction, -): Promise { - const latestEventInDb = await model.findOne({ - lock: true, - transaction, - where, - order: [ - ['blockNumber', 'DESC'], - ['logIndex', 'DESC'], - ], - }); - - if (!latestEventInDb) { - return true; - } - - if ( - latestEventInDb.blockNumber > incomingEvent.blockNumber || - (latestEventInDb.blockNumber === incomingEvent.blockNumber && - latestEventInDb.logIndex > incomingEvent.logIndex) - ) { - return false; - } - - return true; -} diff --git a/src/utils/getCurrentSplits.ts b/src/utils/getCurrentSplits.ts deleted file mode 100644 index 1f07e70..0000000 --- a/src/utils/getCurrentSplits.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Op } from 'sequelize'; -import type { AccountId } from '../core/types'; -import { - AddressDriverSplitReceiverModel, - DripListSplitReceiverModel, - RepoDriverSplitReceiverModel, - StreamReceiverSeenEventModel, -} from '../models'; - -export async function getCurrentSplitsByAccountId( - emmiterAccountId: AccountId, -): Promise { - const [addressSplits, dripListSplits, projectSplits] = await Promise.all([ - await AddressDriverSplitReceiverModel.findAll({ - where: { - [Op.or]: [ - { funderProjectId: emmiterAccountId }, - { funderDripListId: emmiterAccountId }, - ], - }, - lock: true, - }), - await DripListSplitReceiverModel.findAll({ - where: { - [Op.or]: [ - { funderProjectId: emmiterAccountId }, - { funderDripListId: emmiterAccountId }, - ], - }, - lock: true, - }), - await RepoDriverSplitReceiverModel.findAll({ - where: { - [Op.or]: [ - { funderProjectId: emmiterAccountId }, - { funderDripListId: emmiterAccountId }, - ], - }, - lock: true, - }), - ]); - - const accountIds = [ - ...addressSplits.map((receiver) => receiver.fundeeAccountId), - ...dripListSplits.map((receiver) => receiver.fundeeDripListId), - ...projectSplits.map((receiver) => receiver.fundeeProjectId), - ]; - - return Array.from(new Set(accountIds)); -} - -export async function getCurrentSplitsByReceiversHash( - receiversHash: string, -): Promise { - const streamReceiverSeenEvents = await StreamReceiverSeenEventModel.findAll({ - where: { - receiversHash, - }, - lock: true, - }); - - const accountIds = streamReceiverSeenEvents.map((event) => event.accountId); - - return Array.from(new Set(accountIds)); -} diff --git a/src/utils/isLatestEvent.ts b/src/utils/isLatestEvent.ts new file mode 100644 index 0000000..71239d0 --- /dev/null +++ b/src/utils/isLatestEvent.ts @@ -0,0 +1,35 @@ +import { + type FindOptions, + type Model, + type Transaction, + type WhereOptions, +} from 'sequelize'; +import type { IEventModel } from '../events/types'; + +export async function isLatestEvent>( + incomingEvent: T, + model: { findOne(options: FindOptions): Promise }, + where: WhereOptions, + transaction: Transaction, +): Promise { + const latest = await model.findOne({ + lock: transaction.LOCK.UPDATE, + transaction, + where, + order: [ + ['blockNumber', 'DESC'], + ['logIndex', 'DESC'], + ], + }); + + if (!latest) { + return true; + } + + const isNewerBlock = latest.blockNumber < incomingEvent.blockNumber; + const isSameBlockNewerLog = + latest.blockNumber === incomingEvent.blockNumber && + latest.logIndex <= incomingEvent.logIndex; + + return isNewerBlock || isSameBlockNewerLog; +} diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts index d3cef57..f85dbf5 100644 --- a/src/utils/metadataUtils.ts +++ b/src/utils/metadataUtils.ts @@ -8,13 +8,13 @@ import { } from '../metadata/schemas'; import appSettings from '../config/appSettings'; -export function toIpfsHash(str: string): IpfsHash { +export function convertToIpfsHash(str: string): IpfsHash { const ipfsHash = ethers.toUtf8String(str); const isIpfsHash = /^(Qm[a-zA-Z0-9]{44})$/.test(ipfsHash); if (!isIpfsHash) { - throw new Error('The provided string is not a valid IPFS hash.'); + throw new Error(`Failed to convert: '${str}' is not a valid IPFS hash.`); } return ipfsHash as IpfsHash; diff --git a/src/utils/gitProjectUtils.ts b/src/utils/projectUtils.ts similarity index 68% rename from src/utils/gitProjectUtils.ts rename to src/utils/projectUtils.ts index cd9d8eb..a03b09c 100644 --- a/src/utils/gitProjectUtils.ts +++ b/src/utils/projectUtils.ts @@ -1,10 +1,11 @@ import type { AddressLike } from 'ethers'; import { ethers } from 'ethers'; +import type { z } from 'zod'; import { FORGES_MAP } from '../core/constants'; import type { Forge } from '../core/types'; -import type { GitProjectModel } from '../models'; -import { ProjectVerificationStatus } from '../models/GitProjectModel'; import unreachableError from './unreachableError'; +import { ProjectVerificationStatus } from '../models/ProjectModel'; +import type { sourceSchema } from '../metadata/schemas/common/sources'; export function toProjectOwnerAddress(address: string): AddressLike { if (!ethers.isAddress(address)) { @@ -33,13 +34,22 @@ export function toForge(forge: bigint): Forge { return forgeAsString; } +export const METADATA_FORGE_MAP: Record< + z.infer['forge'], + Forge +> = { + github: 'GitHub', +}; + export function toReadable(bytes: string): string { return ethers.toUtf8String(bytes); } -export function calculateProjectStatus( - project: GitProjectModel, -): ProjectVerificationStatus { +export function calculateProjectStatus(project: { + id: string; + color: string | null; + ownerAddress: AddressLike | null; +}): ProjectVerificationStatus { if (!project.ownerAddress && !project.color) { return ProjectVerificationStatus.Unclaimed; } @@ -57,7 +67,10 @@ export function calculateProjectStatus( } return unreachableError( - `Project with ID ${project.id} (${project.name}) has an invalid status. - \r\tProject: ${JSON.stringify(project, null, 2)}`, + `Project with ID ${project.id} has an invalid status.\n` + + ` Project:\n${JSON.stringify(project, null, 2) + .split('\n') + .map((line) => ` ${line}`) + .join('\n')}`, ); } diff --git a/src/utils/recoverableError.ts b/src/utils/recoverableError.ts new file mode 100644 index 0000000..a47f87c --- /dev/null +++ b/src/utils/recoverableError.ts @@ -0,0 +1,5 @@ +export default class RecoverableError extends Error { + constructor(message: string) { + super(message); + } +} From 9954044e5bc0091a098147a3fc60f2a3b3375fd4 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 8 Apr 2025 15:00:51 +0200 Subject: [PATCH 17/60] test: fix failing tests --- .../handlers/handleDripListMetadata.ts | 2 +- .../handleEcosystemMainAccountMetadata.ts | 2 +- .../handlers/handleSubListMetadata.ts | 2 +- .../projectVerification.ts | 2 +- .../receiversRepository.ts | 2 +- .../v1.ts | 0 src/metadata/schemas/index.ts | 2 +- src/metadata/schemas/nft-driver/v6.ts | 15 +- ...AccountMetadataEmittedEventHandler.test.ts | 129 ++++++++++-------- tests/eventHandlers/GivenEventHandler.test.ts | 18 +-- .../OwnerUpdateRequestedEventHandler.test.ts | 61 +++------ .../OwnerUpdatedEventHandler.test.ts | 57 +++----- tests/eventHandlers/SplitEventHandler.test.ts | 18 +-- .../SplitsSetEventHandler.test.ts | 18 +-- .../SqueezedStreamsEventHandler.test.ts | 18 +-- .../StreamReceiverSeenEventHandler.test.ts | 18 +-- .../StreamsSetEventHandler.test.ts | 17 +-- .../TransferEventHandler.test.ts | 21 ++- 18 files changed, 175 insertions(+), 227 deletions(-) rename src/metadata/schemas/{sub-list => immutable-splits-driver}/v1.ts (100%) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 91b99ea..ecd8dae 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -15,7 +15,7 @@ import type { repoDriverSplitReceiverSchema, addressDriverSplitReceiverSchema, } from '../../../metadata/schemas/repo-driver/v2'; -import type { subListSplitReceiverSchema } from '../../../metadata/schemas/sub-list/v1'; +import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immutable-splits-driver/v1'; import verifyProjectSources from '../projectVerification'; import { deleteExistingReceivers, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index bb30035..3a13754 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -9,7 +9,7 @@ import { EcosystemMainAccountModel } from '../../../models'; import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; import type { repoDriverSplitReceiverSchema } from '../../../metadata/schemas/repo-driver/v2'; -import type { subListSplitReceiverSchema } from '../../../metadata/schemas/sub-list/v1'; +import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immutable-splits-driver/v1'; import verifyProjectSources from '../projectVerification'; import { createProjectReceiver, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 03fc00f..c9843c5 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -20,7 +20,7 @@ import type { addressDriverSplitReceiverSchema, repoDriverSplitReceiverSchema, } from '../../../metadata/schemas/repo-driver/v2'; -import type { subListSplitReceiverSchema } from '../../../metadata/schemas/sub-list/v1'; +import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immutable-splits-driver/v1'; import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; import verifyProjectSources from '../projectVerification'; import RecoverableError from '../../../utils/recoverableError'; diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts b/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts index 5547566..2809ebd 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts @@ -7,7 +7,7 @@ import type { repoDriverSplitReceiverSchema, addressDriverSplitReceiverSchema, } from '../../metadata/schemas/repo-driver/v2'; -import type { subListSplitReceiverSchema } from '../../metadata/schemas/sub-list/v1'; +import type { subListSplitReceiverSchema } from '../../metadata/schemas/immutable-splits-driver/v1'; import type { repoDriverAccountMetadataParser } from '../../metadata/schemas'; import type { ProjectModel } from '../../models'; import unreachableError from '../../utils/unreachableError'; diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts index 6fff621..f483f0b 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts @@ -32,7 +32,7 @@ import { import getUserAddress from '../../utils/getAccountAddress'; import { AddressDriverSplitReceiverType } from '../../models/AddressDriverSplitReceiverModel'; import type { dripListSplitReceiverSchema } from '../../metadata/schemas/nft-driver/v2'; -import type { subListSplitReceiverSchema } from '../../metadata/schemas/sub-list/v1'; +import type { subListSplitReceiverSchema } from '../../metadata/schemas/immutable-splits-driver/v1'; import type { Funder } from './buildFunderAccountFields'; import buildFunderAccountFields, { resolveDependencyType, diff --git a/src/metadata/schemas/sub-list/v1.ts b/src/metadata/schemas/immutable-splits-driver/v1.ts similarity index 100% rename from src/metadata/schemas/sub-list/v1.ts rename to src/metadata/schemas/immutable-splits-driver/v1.ts diff --git a/src/metadata/schemas/index.ts b/src/metadata/schemas/index.ts index 5e49b62..1f3c0fa 100644 --- a/src/metadata/schemas/index.ts +++ b/src/metadata/schemas/index.ts @@ -10,7 +10,7 @@ import { repoDriverAccountMetadataSchemaV4 } from './repo-driver/v4'; import { nftDriverAccountMetadataSchemaV4 } from './nft-driver/v4'; import { repoDriverAccountMetadataSchemaV5 } from './repo-driver/v5'; import { nftDriverAccountMetadataSchemaV5 } from './nft-driver/v5'; -import { subListMetadataSchemaV1 } from './sub-list/v1'; +import { subListMetadataSchemaV1 } from './immutable-splits-driver/v1'; import { nftDriverAccountMetadataSchemaV6 } from './nft-driver/v6'; export const nftDriverAccountMetadataParser = createVersionedParser([ diff --git a/src/metadata/schemas/nft-driver/v6.ts b/src/metadata/schemas/nft-driver/v6.ts index 780fbf6..e8a875e 100644 --- a/src/metadata/schemas/nft-driver/v6.ts +++ b/src/metadata/schemas/nft-driver/v6.ts @@ -4,13 +4,18 @@ import { addressDriverSplitReceiverSchema, repoDriverSplitReceiverSchema, } from '../repo-driver/v2'; -import { subListSplitReceiverSchema } from '../sub-list/v1'; +import { subListSplitReceiverSchema } from '../immutable-splits-driver/v1'; import { dripListSplitReceiverSchema } from './v2'; -const base = nftDriverAccountMetadataSchemaV5.extend({ - isDripList: z.undefined().optional(), - projects: z.undefined().optional(), -}); +const base = nftDriverAccountMetadataSchemaV5 + .omit({ + isDripList: true, + projects: true, + }) + .extend({ + isDripList: z.undefined().optional(), + projects: z.undefined().optional(), + }); const ecosystemVariant = base.extend({ type: z.literal('ecosystem'), diff --git a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts index 2ffc4be..8753444 100644 --- a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts +++ b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts @@ -9,20 +9,25 @@ import { isLatestEvent } from '../../src/utils/isLatestEvent'; import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { DRIPS_APP_USER_METADATA_KEY } from '../../src/core/constants'; import * as handleProjectMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata'; -import { convertToIpfsHash } from '../../src/utils/metadataUtils'; +import { + convertToIpfsHash, + getNftDriverMetadata, +} from '../../src/utils/metadataUtils'; import * as handleDripListMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata'; jest.mock('../../src/models/AccountMetadataEmittedEventModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); jest.mock('../../src/core/LogManager'); -jest.mock('../../src/utils/eventUtils'); +jest.mock('../../src/events/eventHandlerUtils'); jest.mock( - '../../src/eventHandlers/AccountMetadataEmittedEvent/gitProject/handleProjectMetadata', + '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata', ); jest.mock( - '../../src/eventHandlers/AccountMetadataEmittedEvent/dripList/handleDripListMetadata', + '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata', ); +jest.mock('../../src/utils/isLatestEvent'); +jest.mock('../../src/utils/metadataUtils'); describe('AccountMetadataEmittedHandler', () => { let mockDbTransaction: {}; @@ -51,6 +56,8 @@ describe('AccountMetadataEmittedHandler', () => { mockDbTransaction = {}; + (convertToIpfsHash as jest.Mock).mockReturnValue('ipfsHash'); + dbConnection.transaction = jest .fn() .mockImplementation((callback) => callback(mockDbTransaction)); @@ -65,7 +72,28 @@ describe('AccountMetadataEmittedHandler', () => { args: [ 80920745289880686872077472087501508459438916877610571750365932290048n, 'key', - 'value', + '0x516d65444e625169575257666333395844754d354d69796337725755465156666b706d5a7675723965757767584a', + ], + logIndex: 1, + blockNumber: 1, + blockTimestamp: new Date(), + transactionHash: 'requestTransactionHash', + } as EventData<'AccountMetadataEmitted(uint256,bytes32,bytes)'>, + }); + + // Assert + expect(dbConnection.transaction).not.toHaveBeenCalled(); + }); + + test('should return if the metadata are not emitted by a supported Driver', async () => { + // Act + await handler['_handle']({ + id: randomUUID(), + event: { + args: [ + 1n, + DRIPS_APP_USER_METADATA_KEY, + '0x516d65444e625169575257666333395844754d354d69796337725755465156666b706d5a7675723965757767584a', ], logIndex: 1, blockNumber: 1, @@ -80,15 +108,12 @@ describe('AccountMetadataEmittedHandler', () => { test('should create a new AccountMetadataEmittedEventModel', async () => { // Arrange - AccountMetadataEmittedEventModel.findOrCreate = jest - .fn() - .mockResolvedValue([ - { - transactionHash: 'AccountMetadataEmittedEventTransactionHash', - logIndex: 1, - }, - true, - ]); + AccountMetadataEmittedEventModel.create = jest.fn().mockResolvedValue([ + { + transactionHash: 'AccountMetadataEmittedEventTransactionHash', + logIndex: 1, + }, + ]); (isLatestEvent as jest.Mock).mockResolvedValue(false); @@ -106,16 +131,8 @@ describe('AccountMetadataEmittedHandler', () => { }, } = mockRequest; - expect( - AccountMetadataEmittedEventModel.findOrCreate, - ).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + expect(AccountMetadataEmittedEventModel.create).toHaveBeenCalledWith( + { key, value, logIndex, @@ -124,20 +141,21 @@ describe('AccountMetadataEmittedHandler', () => { transactionHash, accountId: convertToAccountId(accountId), }, - }); + { + transaction: mockDbTransaction, + }, + ); }); test('should handle Project metadata when metadata are coming from a Project and the incoming event is the latest', async () => { // Arrange - AccountMetadataEmittedEventModel.findOrCreate = jest - .fn() - .mockResolvedValue([ - { - transactionHash: 'AccountMetadataEmittedEventTransactionHash', - logIndex: 1, - }, - true, - ]); + AccountMetadataEmittedEventModel.create = jest.fn().mockResolvedValue([ + { + transactionHash: 'AccountMetadataEmittedEventTransactionHash', + logIndex: 1, + }, + true, + ]); (isLatestEvent as jest.Mock).mockResolvedValue(true); @@ -147,26 +165,24 @@ describe('AccountMetadataEmittedHandler', () => { await handler['_handle'](mockRequest); // Assert - expect(handleProjectMetadata.default).toHaveBeenCalledWith( - expect.anything(), - convertToAccountId(mockRequest.event.args[0]), - mockDbTransaction, - convertToIpfsHash(mockRequest.event.args[2]), - mockRequest.event.blockTimestamp, - ); + expect(handleProjectMetadata.default).toHaveBeenCalledWith({ + logManager: expect.anything(), + emitterAccountId: convertToAccountId(mockRequest.event.args[0]), + transaction: mockDbTransaction, + ipfsHash: convertToIpfsHash(mockRequest.event.args[2]), + blockTimestamp: mockRequest.event.blockTimestamp, + }); }); test('should handle Drip List metadata when metadata are coming from a Drip List and the incoming event is the latest', async () => { // Arrange - AccountMetadataEmittedEventModel.findOrCreate = jest - .fn() - .mockResolvedValue([ - { - transactionHash: 'AccountMetadataEmittedEventTransactionHash', - logIndex: 1, - }, - true, - ]); + AccountMetadataEmittedEventModel.create = jest.fn().mockResolvedValue([ + { + transactionHash: 'AccountMetadataEmittedEventTransactionHash', + logIndex: 1, + }, + true, + ]); (isLatestEvent as jest.Mock).mockResolvedValue(true); @@ -187,18 +203,23 @@ describe('AccountMetadataEmittedHandler', () => { } as EventData<'AccountMetadataEmitted(uint256,bytes32,bytes)'>, }; + const mockMetadata = { + type: 'dripList', + }; + (getNftDriverMetadata as jest.Mock).mockResolvedValue(mockMetadata); + // Act await new AccountMetadataEmittedEventHandler()['_handle'](request); // Assert expect(handleDripListMetadata.default).toHaveBeenCalledWith({ + ipfsHash: convertToIpfsHash(request.event.args[2]), + metadata: mockMetadata, logManager: expect.anything(), - dripListId: convertToAccountId(request.event.args[0]), transaction: mockDbTransaction, - ipfsHash: convertToIpfsHash(request.event.args[2]), blockTimestamp: request.event.blockTimestamp, - blockNumber: 1, - metadata: expect.anything(), + blockNumber: request.event.blockNumber, + emitterAccountId: convertToAccountId(request.event.args[0]), }); }); }); diff --git a/tests/eventHandlers/GivenEventHandler.test.ts b/tests/eventHandlers/GivenEventHandler.test.ts index 6692e5c..fccfdc3 100644 --- a/tests/eventHandlers/GivenEventHandler.test.ts +++ b/tests/eventHandlers/GivenEventHandler.test.ts @@ -51,12 +51,11 @@ describe('GivenEventHandler', () => { describe('_handle', () => { test('should create a new GivenEventModel', async () => { // Arrange - GivenEventModel.findOrCreate = jest.fn().mockResolvedValue([ + GivenEventModel.create = jest.fn().mockResolvedValue([ { transactionHash: 'GivenEventTransactionHash', logIndex: 1, }, - true, ]); LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); @@ -75,14 +74,8 @@ describe('GivenEventHandler', () => { }, } = mockRequest; - expect(GivenEventModel.findOrCreate).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + expect(GivenEventModel.create).toHaveBeenCalledWith( + { accountId: convertToAccountId(rawAccountId), receiver: convertToAccountId(rawReceiver), erc20: toAddress(rawErc20), @@ -92,7 +85,10 @@ describe('GivenEventHandler', () => { blockTimestamp, transactionHash, }, - }); + { + transaction: mockDbTransaction, + }, + ); }); }); }); diff --git a/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts b/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts index a472fc0..d4e06e9 100644 --- a/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts +++ b/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts @@ -23,8 +23,9 @@ jest.mock('../../src/models/OwnerUpdateRequestedEventModel'); jest.mock('../../src/models/ProjectModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); -jest.mock('../../src/utils/eventUtils'); +jest.mock('../../src/events/eventHandlerUtils'); jest.mock('../../src/core/LogManager'); +jest.mock('../../src/utils/isLatestEvent'); describe('OwnerUpdateRequestedEventHandler', () => { let mockDbTransaction: {}; @@ -51,7 +52,11 @@ describe('OwnerUpdateRequestedEventHandler', () => { } as EventData<'OwnerUpdateRequested(uint256,uint8,bytes)'>, }; - mockDbTransaction = {}; + mockDbTransaction = { + LOCK: { + UPDATE: 'UPDATE', + }, + }; dbConnection.transaction = jest .fn() @@ -59,23 +64,13 @@ describe('OwnerUpdateRequestedEventHandler', () => { }); describe('_handle', () => { - test('should create new OwnerUpdateRequestedEventModel and ProjectModel', async () => { + test('should create new OwnerUpdateRequestedEventModel', async () => { // Arrange - OwnerUpdateRequestedEventModel.findOrCreate = jest - .fn() - .mockResolvedValue([ - { - transactionHash: 'OwnerUpdateRequestedEventTransactionHash', - logIndex: 1, - }, - true, - ]); - - ProjectModel.findOrCreate = jest.fn().mockResolvedValue([ + OwnerUpdateRequestedEventModel.create = jest.fn().mockResolvedValue([ { - id: convertToRepoDriverId(mockRequest.event.args[0]), + transactionHash: 'OwnerUpdateRequestedEventTransactionHash', + logIndex: 1, }, - true, ]); LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); @@ -94,14 +89,8 @@ describe('OwnerUpdateRequestedEventHandler', () => { }, } = mockRequest; - expect(OwnerUpdateRequestedEventModel.findOrCreate).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + expect(OwnerUpdateRequestedEventModel.create).toHaveBeenCalledWith( + { logIndex, blockNumber, blockTimestamp, @@ -110,24 +99,10 @@ describe('OwnerUpdateRequestedEventHandler', () => { forge: toForge(forge), accountId: convertToRepoDriverId(accountId), }, - }); - - expect(ProjectModel.findOrCreate).toHaveBeenCalledWith({ - transaction: mockDbTransaction, - lock: true, - where: { - id: convertToRepoDriverId(accountId), - }, - defaults: { - id: convertToRepoDriverId(accountId), - isValid: true, - isVisible: true, - name: toReadable(name), - forge: toForge(forge), - url: toUrl(toForge(forge), toReadable(name)), - verificationStatus: ProjectVerificationStatus.OwnerUpdateRequested, + { + transaction: mockDbTransaction, }, - }); + ); }); test('should update the ProjectModel when the incoming event is the latest', async () => { @@ -149,9 +124,7 @@ describe('OwnerUpdateRequestedEventHandler', () => { verificationStatus: ProjectVerificationStatus.Unclaimed, save: jest.fn(), }; - ProjectModel.findOrCreate = jest - .fn() - .mockResolvedValue([mockGitProject, false]); + ProjectModel.findByPk = jest.fn().mockResolvedValue(mockGitProject); (isLatestEvent as jest.Mock).mockResolvedValue(true); diff --git a/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts b/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts index e54e223..728316d 100644 --- a/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts +++ b/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts @@ -20,9 +20,10 @@ jest.mock('../../src/models/OwnerUpdatedEventModel'); jest.mock('../../src/models/ProjectModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); -jest.mock('../../src/utils/eventUtils'); +jest.mock('../../src/events/eventHandlerUtils'); jest.mock('../../src/core/LogManager'); jest.mock('../../src/utils/accountIdUtils'); +jest.mock('../../src/utils/isLatestEvent'); describe('OwnerUpdatedEventHandler', () => { let mockDbTransaction: {}; @@ -48,7 +49,11 @@ describe('OwnerUpdatedEventHandler', () => { } as EventData<'OwnerUpdated(uint256,address)'>, }; - mockDbTransaction = {}; + mockDbTransaction = { + LOCK: { + UPDATE: 'UPDATE', + }, + }; dbConnection.transaction = jest .fn() @@ -56,9 +61,9 @@ describe('OwnerUpdatedEventHandler', () => { }); describe('_handle', () => { - test('should create new OwnerUpdatedEventModel and ProjectModel', async () => { + test('should create new OwnerUpdatedEventModel', async () => { // Arrange - OwnerUpdatedEventModel.findOrCreate = jest.fn().mockResolvedValue([ + OwnerUpdatedEventModel.create = jest.fn().mockResolvedValue([ { transactionHash: 'OwnerUpdatedEventTransactionHash', logIndex: 1, @@ -68,13 +73,6 @@ describe('OwnerUpdatedEventHandler', () => { (convertToRepoDriverId as jest.Mock).mockReturnValue('1'); - ProjectModel.findOrCreate = jest.fn().mockResolvedValue([ - { - id: convertToRepoDriverId(mockRequest.event.args[0]), - }, - true, - ]); - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); (calcAccountId as jest.Mock).mockResolvedValue('ownerAccountId'); @@ -93,14 +91,8 @@ describe('OwnerUpdatedEventHandler', () => { }, } = mockRequest; - expect(OwnerUpdatedEventModel.findOrCreate).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + expect(OwnerUpdatedEventModel.create).toHaveBeenCalledWith( + { owner, logIndex, blockNumber, @@ -108,34 +100,19 @@ describe('OwnerUpdatedEventHandler', () => { transactionHash, accountId: convertToRepoDriverId(accountId), }, - }); - - expect(ProjectModel.findOrCreate).toHaveBeenCalledWith({ - transaction: mockDbTransaction, - lock: true, - where: { - id: convertToRepoDriverId(accountId), - }, - defaults: { - id: convertToRepoDriverId(accountId), - isValid: true, - isVisible: true, - ownerAddress: owner, - claimedAt: blockTimestamp, - ownerAccountId: await calcAccountId(owner), - verificationStatus: ProjectVerificationStatus.OwnerUpdated, + { + transaction: mockDbTransaction, }, - }); + ); }); test('should update the ProjectModel when the incoming event is the latest', async () => { // Arrange - OwnerUpdatedEventModel.findOrCreate = jest.fn().mockResolvedValue([ + OwnerUpdatedEventModel.create = jest.fn().mockResolvedValue([ { transactionHash: 'OwnerUpdatedEventTransactionHash', logIndex: 1, }, - true, ]); const mockGitProject = { @@ -144,9 +121,7 @@ describe('OwnerUpdatedEventHandler', () => { verificationStatus: ProjectVerificationStatus.Unclaimed, save: jest.fn(), }; - ProjectModel.findOrCreate = jest - .fn() - .mockResolvedValue([mockGitProject, false]); + ProjectModel.findByPk = jest.fn().mockResolvedValue(mockGitProject); (isLatestEvent as jest.Mock).mockResolvedValue(true); diff --git a/tests/eventHandlers/SplitEventHandler.test.ts b/tests/eventHandlers/SplitEventHandler.test.ts index 557f567..dcb31e8 100644 --- a/tests/eventHandlers/SplitEventHandler.test.ts +++ b/tests/eventHandlers/SplitEventHandler.test.ts @@ -51,12 +51,11 @@ describe('SplitEventHandler', () => { describe('_handle', () => { test('should create a new SplitEventModel', async () => { // Arrange - SplitEventModel.findOrCreate = jest.fn().mockResolvedValue([ + SplitEventModel.create = jest.fn().mockResolvedValue([ { transactionHash: 'SplitEventTransactionHash', logIndex: 1, }, - true, ]); LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); @@ -75,14 +74,8 @@ describe('SplitEventHandler', () => { }, } = mockRequest; - expect(SplitEventModel.findOrCreate).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + expect(SplitEventModel.create).toHaveBeenCalledWith( + { accountId: convertToAccountId(rawAccountId), receiver: convertToAccountId(rawReceiver), erc20: toAddress(rawErc20), @@ -92,7 +85,10 @@ describe('SplitEventHandler', () => { blockTimestamp, transactionHash, }, - }); + { + transaction: mockDbTransaction, + }, + ); }); }); }); diff --git a/tests/eventHandlers/SplitsSetEventHandler.test.ts b/tests/eventHandlers/SplitsSetEventHandler.test.ts index 6341fba..7c8bd04 100644 --- a/tests/eventHandlers/SplitsSetEventHandler.test.ts +++ b/tests/eventHandlers/SplitsSetEventHandler.test.ts @@ -49,12 +49,11 @@ describe('SplitsSetEventHandler', () => { describe('_handle', () => { test('should create a new SplitsSetEventModel', async () => { // Arrange - SplitsSetEventModel.findOrCreate = jest.fn().mockResolvedValue([ + SplitsSetEventModel.create = jest.fn().mockResolvedValue([ { transactionHash: 'SplitsSetTransactionHash', logIndex: 1, }, - true, ]); LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); @@ -73,14 +72,8 @@ describe('SplitsSetEventHandler', () => { }, } = mockRequest; - expect(SplitsSetEventModel.findOrCreate).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + expect(SplitsSetEventModel.create).toHaveBeenCalledWith( + { accountId: convertToAccountId(rawAccountId), receiversHash: rawReceiversHash, logIndex, @@ -88,7 +81,10 @@ describe('SplitsSetEventHandler', () => { blockTimestamp, transactionHash, }, - }); + { + transaction: mockDbTransaction, + }, + ); expect(setIsValidFlag).toHaveBeenCalled(); }); diff --git a/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts b/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts index 75f1057..49825ac 100644 --- a/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts +++ b/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts @@ -52,12 +52,11 @@ describe('SqueezedStreamsEventHandler', () => { describe('_handle', () => { test('should create a new SqueezedStreamsEventModel', async () => { // Arrange - SqueezedStreamsEventModel.findOrCreate = jest.fn().mockResolvedValue([ + SqueezedStreamsEventModel.create = jest.fn().mockResolvedValue([ { transactionHash: 'SqueezedStreamsTransactionHash', logIndex: 1, }, - true, ]); LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); @@ -82,14 +81,8 @@ describe('SqueezedStreamsEventHandler', () => { }, } = mockRequest; - expect(SqueezedStreamsEventModel.findOrCreate).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + expect(SqueezedStreamsEventModel.create).toHaveBeenCalledWith( + { accountId: convertToAccountId(rawAccountId), erc20: toAddress(rawErc20), senderId: convertToAccountId(rawSenderId), @@ -102,7 +95,10 @@ describe('SqueezedStreamsEventHandler', () => { blockTimestamp, transactionHash, }, - }); + { + transaction: mockDbTransaction, + }, + ); }); }); }); diff --git a/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts b/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts index ed7bac5..e0206cf 100644 --- a/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts +++ b/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts @@ -49,12 +49,11 @@ describe('StreamReceiverSeenEventHandler', () => { describe('_handle', () => { test('should create a new StreamReceiverSeenEventModel', async () => { // Arrange - StreamReceiverSeenEventModel.findOrCreate = jest.fn().mockResolvedValue([ + StreamReceiverSeenEventModel.create = jest.fn().mockResolvedValue([ { transactionHash: 'StreamReceiverSeenTransactionHash', logIndex: 1, }, - true, ]); LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); @@ -73,14 +72,8 @@ describe('StreamReceiverSeenEventHandler', () => { }, } = mockRequest; - expect(StreamReceiverSeenEventModel.findOrCreate).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + expect(StreamReceiverSeenEventModel.create).toHaveBeenCalledWith( + { accountId: convertToAccountId(rawAccountId), receiversHash: rawReceiversHash, config: toBigIntString(rawConfig.toString()), @@ -89,7 +82,10 @@ describe('StreamReceiverSeenEventHandler', () => { blockTimestamp, transactionHash, }, - }); + { + transaction: mockDbTransaction, + }, + ); }); }); }); diff --git a/tests/eventHandlers/StreamsSetEventHandler.test.ts b/tests/eventHandlers/StreamsSetEventHandler.test.ts index ae728bd..f8e570e 100644 --- a/tests/eventHandlers/StreamsSetEventHandler.test.ts +++ b/tests/eventHandlers/StreamsSetEventHandler.test.ts @@ -52,7 +52,7 @@ describe('StreamsSetEventHandler', () => { describe('_handle', () => { test('should create a new StreamsSetEventModel', async () => { // Arrange - StreamsSetEventModel.findOrCreate = jest.fn().mockResolvedValue([ + StreamsSetEventModel.create = jest.fn().mockResolvedValue([ { transactionHash: 'StreamsSetTransactionHash', logIndex: 1, @@ -83,14 +83,8 @@ describe('StreamsSetEventHandler', () => { }, } = mockRequest; - expect(StreamsSetEventModel.findOrCreate).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + expect(StreamsSetEventModel.create).toHaveBeenCalledWith( + { accountId: convertToAccountId(rawAccountId), erc20: rawErc20, receiversHash: rawReceiversHash, @@ -102,7 +96,10 @@ describe('StreamsSetEventHandler', () => { blockTimestamp, transactionHash, }, - }); + { + transaction: mockDbTransaction, + }, + ); }); }); }); diff --git a/tests/eventHandlers/TransferEventHandler.test.ts b/tests/eventHandlers/TransferEventHandler.test.ts index 313e077..748feb6 100644 --- a/tests/eventHandlers/TransferEventHandler.test.ts +++ b/tests/eventHandlers/TransferEventHandler.test.ts @@ -13,9 +13,10 @@ jest.mock('../../src/models/TransferEventModel'); jest.mock('../../src/models/DripListModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); -jest.mock('../../src/utils/eventUtils'); +jest.mock('../../src/events/eventHandlerUtils'); jest.mock('../../src/utils/accountIdUtils'); jest.mock('../../src/core/LogManager'); +jest.mock('../../src/utils/isLatestEvent'); describe('TransferEventHandler', () => { let mockDbTransaction: {}; @@ -52,12 +53,11 @@ describe('TransferEventHandler', () => { describe('_handle', () => { test('should create a new TransferEventModel', async () => { // Arrange - TransferEventModel.findOrCreate = jest.fn().mockResolvedValue([ + TransferEventModel.create = jest.fn().mockResolvedValue([ { transactionHash: 'TransferEventTransactionHash', logIndex: 1, }, - true, ]); DripListModel.findOne = jest @@ -80,14 +80,8 @@ describe('TransferEventHandler', () => { }, } = mockRequest; - expect(TransferEventModel.findOrCreate).toHaveBeenCalledWith({ - lock: true, - transaction: mockDbTransaction, - where: { - logIndex, - transactionHash, - }, - defaults: { + expect(TransferEventModel.create).toHaveBeenCalledWith( + { tokenId: convertToNftDriverId(tokenId), to, from, @@ -96,7 +90,10 @@ describe('TransferEventHandler', () => { blockTimestamp, transactionHash, }, - }); + { + transaction: mockDbTransaction, + }, + ); }); }); }); From 713290a7d3f086fd31c6b1642a1ce8853110056f Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 10 Apr 2025 14:41:48 +0200 Subject: [PATCH 18/60] refactor: rename ecosystem main entity --- .../20250319131551-create_ecosystems.ts | 8 ++++---- ...ate_sublists_and_split_receivers_tables.ts | 6 +++--- ...enum_and_make_ecosystem_fields_nullable.ts | 20 +++++++++---------- ..._funder_ecosystem_id_to_split_receivers.ts | 6 +++--- ...ove-latestVotingRoundId-from-ecosystems.ts | 4 ++-- .../handleEcosystemMainAccountMetadata.ts | 12 +++++------ src/models/EcosystemMainAccountModel.ts | 2 +- src/models/SubListModel.ts | 4 ++-- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/db/migrations/20250319131551-create_ecosystems.ts b/src/db/migrations/20250319131551-create_ecosystems.ts index 4c46404..5d51eaf 100644 --- a/src/db/migrations/20250319131551-create_ecosystems.ts +++ b/src/db/migrations/20250319131551-create_ecosystems.ts @@ -9,7 +9,7 @@ export async function up({ context: sequelize }: any): Promise { await createTableIfNotExists( queryInterface, schema, - 'EcosystemMainIdentities', + 'EcosystemMainAccounts', createEcosystemsTable, ); } @@ -19,7 +19,7 @@ async function createEcosystemsTable( schema: DbSchema, ) { await queryInterface.createTable( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, { id: { type: DataTypes.STRING, @@ -79,7 +79,7 @@ async function createEcosystemsTable( ); await queryInterface.addIndex( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, ['ownerAddress'], { name: 'IX_Ecosystems_ownerAddress', @@ -110,7 +110,7 @@ export async function down({ context: sequelize }: any): Promise { await queryInterface.dropTable({ tableName: 'CreatedSplitsEvents', schema }); await queryInterface.dropTable({ - tableName: 'EcosystemMainIdentities', + tableName: 'EcosystemMainAccounts', schema, }); } diff --git a/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts b/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts index 5b1c36c..57f9513 100644 --- a/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts +++ b/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts @@ -47,7 +47,7 @@ async function createSubListsTable( type: DataTypes.STRING, allowNull: true, references: { - model: 'EcosystemMainIdentities', + model: 'EcosystemMainAccounts', key: 'id', }, }, @@ -74,7 +74,7 @@ async function createSubListsTable( type: DataTypes.STRING, allowNull: true, references: { - model: 'EcosystemMainIdentities', + model: 'EcosystemMainAccounts', key: 'id', }, }, @@ -188,7 +188,7 @@ async function createSubListSplitReceiversTable( // Foreign key type: DataTypes.STRING, references: { - model: 'EcosystemMainIdentities', + model: 'EcosystemMainAccounts', key: 'id', }, allowNull: true, diff --git a/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts b/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts index a2b386a..aa9c1a9 100644 --- a/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts +++ b/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts @@ -19,7 +19,7 @@ export async function up({ context: sequelize }: any): Promise { `); await queryInterface.changeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'creator', { type: DataTypes.STRING, @@ -28,7 +28,7 @@ export async function up({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'ownerAddress', { type: DataTypes.STRING, @@ -37,7 +37,7 @@ export async function up({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'ownerAccountId', { type: DataTypes.STRING, @@ -46,7 +46,7 @@ export async function up({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'previousOwnerAddress', { type: DataTypes.STRING, @@ -55,7 +55,7 @@ export async function up({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'isVisible', { type: DataTypes.BOOLEAN, @@ -69,7 +69,7 @@ export async function down({ context: sequelize }: any): Promise { const queryInterface: QueryInterface = sequelize.getQueryInterface(); await queryInterface.changeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'creator', { type: DataTypes.STRING, @@ -78,7 +78,7 @@ export async function down({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'ownerAddress', { type: DataTypes.STRING, @@ -87,7 +87,7 @@ export async function down({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'ownerAccountId', { type: DataTypes.STRING, @@ -96,7 +96,7 @@ export async function down({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'previousOwnerAddress', { type: DataTypes.STRING, @@ -105,7 +105,7 @@ export async function down({ context: sequelize }: any): Promise { ); await queryInterface.changeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'isVisible', { type: DataTypes.BOOLEAN, diff --git a/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts b/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts index fb2fdd5..1e4a03f 100644 --- a/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts +++ b/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts @@ -11,7 +11,7 @@ export async function up({ context: sequelize }: any): Promise { { type: DataTypes.STRING, references: { - model: 'EcosystemMainIdentities', + model: 'EcosystemMainAccounts', key: 'id', }, allowNull: true, @@ -61,7 +61,7 @@ export async function up({ context: sequelize }: any): Promise { { type: DataTypes.STRING, references: { - model: 'EcosystemMainIdentities', + model: 'EcosystemMainAccounts', key: 'id', }, allowNull: true, @@ -111,7 +111,7 @@ export async function up({ context: sequelize }: any): Promise { { type: DataTypes.STRING, references: { - model: 'EcosystemMainIdentities', + model: 'EcosystemMainAccounts', key: 'id', }, allowNull: true, diff --git a/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts b/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts index db344b8..721b70b 100644 --- a/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts +++ b/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts @@ -6,7 +6,7 @@ export async function up({ context: sequelize }: any): Promise { const queryInterface: QueryInterface = sequelize.getQueryInterface(); await queryInterface.removeColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'latestVotingRoundId', ); } @@ -16,7 +16,7 @@ export async function down({ context: sequelize }: any): Promise { const queryInterface: QueryInterface = sequelize.getQueryInterface(); await queryInterface.addColumn( - { tableName: 'EcosystemMainIdentities', schema }, + { tableName: 'EcosystemMainAccounts', schema }, 'latestVotingRoundId', { type: DataTypes.STRING, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index 3a13754..504d31b 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -69,7 +69,7 @@ export default async function handleEcosystemMainAccountMetadata({ : true, }; - const [ecosystemMainIdentity, isCreated] = + const [ecosystemMainAccount, isCreated] = await EcosystemMainAccountModel.findOrCreate({ transaction, lock: transaction.LOCK.UPDATE, @@ -84,18 +84,18 @@ export default async function handleEcosystemMainAccountMetadata({ logManager.appendFindOrCreateLog( EcosystemMainAccountModel, true, - ecosystemMainIdentity.id, + ecosystemMainAccount.id, ); } else { - ecosystemMainIdentity.set(ecosystemMainAccountProps); + ecosystemMainAccount.set(ecosystemMainAccountProps); logManager.appendUpdateLog( - ecosystemMainIdentity, + ecosystemMainAccount, EcosystemMainAccountModel, - ecosystemMainIdentity.id, + ecosystemMainAccount.id, ); - await ecosystemMainIdentity.save({ transaction }); + await ecosystemMainAccount.save({ transaction }); } await deleteExistingReceivers({ diff --git a/src/models/EcosystemMainAccountModel.ts b/src/models/EcosystemMainAccountModel.ts index 95fdbce..927d0db 100644 --- a/src/models/EcosystemMainAccountModel.ts +++ b/src/models/EcosystemMainAccountModel.ts @@ -70,7 +70,7 @@ export default class EcosystemMainAccountModel extends Model< { sequelize, schema: getSchema(), - tableName: 'EcosystemMainIdentities', + tableName: 'EcosystemMainAccounts', indexes: [ { fields: ['ownerAddress'], diff --git a/src/models/SubListModel.ts b/src/models/SubListModel.ts index 53e837b..3bdec02 100644 --- a/src/models/SubListModel.ts +++ b/src/models/SubListModel.ts @@ -41,7 +41,7 @@ export default class SubListModel extends Model< type: DataTypes.STRING, allowNull: true, references: { - model: 'EcosystemMainIdentities', + model: 'EcosystemMainAccounts', key: 'id', }, }, @@ -68,7 +68,7 @@ export default class SubListModel extends Model< type: DataTypes.STRING, allowNull: true, references: { - model: 'EcosystemMainIdentities', + model: 'EcosystemMainAccounts', key: 'id', }, }, From 9de4d005612e9020d8d6ab00d8b79cc6cd2e5bf1 Mon Sep 17 00:00:00 2001 From: Georgios Jason Efstathiou Date: Thu, 10 Apr 2025 14:42:36 +0200 Subject: [PATCH 19/60] don't re-build on migration --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb8146c..14feb8b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "dev": "npx nodemon", "start": "node dist/src/index.js", "sequelize": "ts-node --transpile-only node_modules/sequelize-cli/lib/sequelize", - "db:run-migrations": "npm run clean && npm run build && ts-node scripts/run-migrations.ts", + "db:run-migrations": "ts-node scripts/run-migrations.ts", "db:create-migration": "npm run sequelize migration:generate -- --migrations-path src/db/migrations", "dev:db:revert-migration": "npm run sequelize -- db:migrate:undo --migrations-path src/db/migrations --config dist/src/config/sequelizeConfig.js", "dev:db:log-pending-migrations": "npm run sequelize -- db:migrate:status --migrations-path src/db/migrations --config dist/src/config/sequelizeConfig.js" From ddc665a384ba2d3a02507250c007e98089cfa5a4 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 14 Apr 2025 17:07:34 +0200 Subject: [PATCH 20/60] chore: polish db migration scripts and start over db migrations --- package.json | 5 +- scripts/revert-migration.ts | 55 + scripts/run-migrations.ts | 37 +- src/config/sequelizeConfig.ts | 8 +- src/db/migrations/20250317125844-initial.ts | 1059 ----------------- .../20250319131551-create_ecosystems.ts | 116 -- ...ate_sublists_and_split_receivers_tables.ts | 302 ----- ...325100215-make_driplist_fields_nullable.ts | 102 -- ...enum_and_make_ecosystem_fields_nullable.ts | 115 -- ..._funder_ecosystem_id_to_split_receivers.ts | 187 --- ...ms.ts => 20250414133746-initial_create.ts} | 28 +- src/health.ts | 6 +- 12 files changed, 104 insertions(+), 1916 deletions(-) create mode 100644 scripts/revert-migration.ts delete mode 100644 src/db/migrations/20250317125844-initial.ts delete mode 100644 src/db/migrations/20250319131551-create_ecosystems.ts delete mode 100644 src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts delete mode 100644 src/db/migrations/20250325100215-make_driplist_fields_nullable.ts delete mode 100644 src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts delete mode 100644 src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts rename src/db/migrations/{20250331091615-remove-latestVotingRoundId-from-ecosystems.ts => 20250414133746-initial_create.ts} (50%) diff --git a/package.json b/package.json index 14feb8b..532e4a0 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,10 @@ "start": "node dist/src/index.js", "sequelize": "ts-node --transpile-only node_modules/sequelize-cli/lib/sequelize", "db:run-migrations": "ts-node scripts/run-migrations.ts", + "db:revert-migration": "ts-node scripts/revert-migration.ts", "db:create-migration": "npm run sequelize migration:generate -- --migrations-path src/db/migrations", - "dev:db:revert-migration": "npm run sequelize -- db:migrate:undo --migrations-path src/db/migrations --config dist/src/config/sequelizeConfig.js", - "dev:db:log-pending-migrations": "npm run sequelize -- db:migrate:status --migrations-path src/db/migrations --config dist/src/config/sequelizeConfig.js" + "db:log-pending-migrations": "npm run sequelize -- db:migrate:status --migrations-path src/db/migrations --config dist/src/config/sequelizeConfig.js", + "dev:db:run-migrations": "npm run clean && npm run build && ts-node scripts/run-migrations.ts" }, "keywords": [], "author": "", diff --git a/scripts/revert-migration.ts b/scripts/revert-migration.ts new file mode 100644 index 0000000..0228797 --- /dev/null +++ b/scripts/revert-migration.ts @@ -0,0 +1,55 @@ +/* eslint-disable no-console */ + +import { Sequelize } from 'sequelize'; +import { Umzug, SequelizeStorage } from 'umzug'; +import getSchema from '../src/utils/getSchema'; + +export async function revertLastMigration(): Promise { + const connectionString = process.env.POSTGRES_CONNECTION_STRING; + if (!connectionString) { + throw new Error( + "'POSTGRES_CONNECTION_STRING' environment variable is missing.", + ); + } + + const sequelize = new Sequelize(connectionString, { + dialect: 'postgres', + logging: false, + }); + + const schema = getSchema(); + + await sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`); + + const migrator = new Umzug({ + migrations: { + glob: './dist/src/db/migrations/*.js', // Migrations must be built before running. + }, + context: sequelize, + storage: new SequelizeStorage({ sequelize, schema }), + logger: console, + }); + + try { + const reverted = await migrator.down(); + + if (reverted.length) { + console.info(`🔁 Reverted migration: ${reverted[0]?.name}`); + } else { + console.info( + 'No migration was reverted. The database is already at the base state.', + ); + } + } finally { + sequelize.close(); + } +} + +revertLastMigration().catch((error) => { + console.error(`❌ Migration revert error: ${error.message}`, { + stack: error.stack, + cause: (error as any).cause, + }); + + process.exitCode = 1; +}); diff --git a/scripts/run-migrations.ts b/scripts/run-migrations.ts index dab51cb..69bb89f 100644 --- a/scripts/run-migrations.ts +++ b/scripts/run-migrations.ts @@ -1,17 +1,21 @@ +/* eslint-disable no-console */ + import { Sequelize } from 'sequelize'; import { Umzug, SequelizeStorage } from 'umzug'; import getSchema from '../src/utils/getSchema'; -import logger from '../src/core/logger'; export async function runMigrations(): Promise { - const sequelize = new Sequelize( - process.env.POSTGRES_CONNECTION_STRING as string, - { - dialect: 'postgres', - logging: false, - timezone: 'UTC', - }, - ); + const connectionString = process.env.POSTGRES_CONNECTION_STRING; + if (!connectionString) { + throw new Error( + "'POSTGRES_CONNECTION_STRING' environment variable is missing.", + ); + } + + const sequelize = new Sequelize(connectionString, { + dialect: 'postgres', + logging: false, + }); const schema = getSchema(); @@ -19,10 +23,10 @@ export async function runMigrations(): Promise { const migrator = new Umzug({ migrations: { - glob: './dist/src/db/migrations/*.js', // Migrations always run from the build! + glob: './dist/src/db/migrations/*.js', // Migrations must be built before running. }, context: sequelize, - storage: new SequelizeStorage({ sequelize, schema: getSchema() }), + storage: new SequelizeStorage({ sequelize, schema }), logger: console, }); @@ -31,11 +35,11 @@ export async function runMigrations(): Promise { if (migrations.length > 0) { const appliedNames = migrations.map((m) => m.name).join(', '); - logger.info( + console.info( `✅ Applied ${migrations.length} migration${migrations.length > 1 ? 's' : ''}:\n - ${appliedNames.split(', ').join('\n - ')}`, ); } else { - logger.info( + console.info( 'No migrations were applied. The database is already up-to-date. If you expected migrations to be applied, ensure that you run "npm run build" before starting the server.', ); } @@ -45,7 +49,10 @@ export async function runMigrations(): Promise { } runMigrations().catch((error) => { - logger.info('Error running migrations:', error); + console.error(`❌ Migration error: ${error.message}`, { + stack: error.stack, + cause: (error as any).cause, + }); - throw error; + process.exitCode = 1; }); diff --git a/src/config/sequelizeConfig.ts b/src/config/sequelizeConfig.ts index a85aef4..e02efd9 100644 --- a/src/config/sequelizeConfig.ts +++ b/src/config/sequelizeConfig.ts @@ -1,6 +1,12 @@ +/* eslint-disable import/no-import-module-exports */ import appSettings from './appSettings'; -export default { +const config = { url: appSettings.postgresConnectionString, dialect: 'postgres', + define: { + schema: appSettings.network, + }, }; + +module.exports = config; diff --git a/src/db/migrations/20250317125844-initial.ts b/src/db/migrations/20250317125844-initial.ts deleted file mode 100644 index 43ad1b4..0000000 --- a/src/db/migrations/20250317125844-initial.ts +++ /dev/null @@ -1,1059 +0,0 @@ -import { DataTypes, literal, type QueryInterface } from 'sequelize'; -import { COMMON_EVENT_INIT_ATTRIBUTES, FORGES_MAP } from '../../core/constants'; -import { ProjectVerificationStatus } from '../../models/ProjectModel'; -import getSchema from '../../utils/getSchema'; -import { DependencyType, type DbSchema } from '../../core/types'; -import { AddressDriverSplitReceiverType } from '../../models/AddressDriverSplitReceiverModel'; - -export async function up({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface = sequelize.getQueryInterface(); - - await createTableIfNotExists( - queryInterface, - schema, - '_LastIndexedBlock', - createLastIndexedBlockTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'AccountMetadataEmittedEvents', - createAccountMetadataEmittedEventsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'GitProjects', - createProjectsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'DripLists', - createDripListsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'AddressDriverSplitReceivers', - createAddressDriverSplitReceiversTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'DripListSplitReceivers', - createDripListSplitReceiversTable, - ); - - await createTableIfNotExists( - queryInterface, - schema, - 'GivenEvents', - createGivenEventsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'OwnerUpdatedEvents', - createOwnerUpdatedEventsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'OwnerUpdateRequestedEvents', - createOwnerUpdateRequestedEventsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'RepoDriverSplitReceivers', - createRepoDriverSplitReceiversTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'SplitEvents', - createSplitEventsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'SplitsSetEvents', - createSplitsSetEventsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'SqueezedStreamsEvents', - createSqueezedStreamsEventsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'StreamReceiverSeenEvents', - createStreamReceiverSeenEventsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'StreamsSetEvents', - createStreamsSetEventsTable, - ); - await createTableIfNotExists( - queryInterface, - schema, - 'TransferEvents', - createTransferEventsTable, - ); -} - -async function createTransferEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'TransferEvents', schema }, - { - tokenId: { - type: DataTypes.STRING, - allowNull: false, - }, - from: { - type: DataTypes.STRING, - allowNull: false, - }, - to: { - type: DataTypes.STRING, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - ); -} - -async function createStreamsSetEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'StreamsSetEvents', schema }, - { - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - erc20: { - type: DataTypes.STRING, - allowNull: false, - }, - receiversHash: { - type: DataTypes.STRING, - allowNull: false, - }, - streamsHistoryHash: { - type: DataTypes.STRING, - allowNull: false, - }, - balance: { - type: DataTypes.STRING, - allowNull: false, - }, - maxEnd: { - type: DataTypes.STRING, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - ); - - await queryInterface.addIndex( - { tableName: 'StreamsSetEvents', schema }, - ['accountId'], - { - name: 'IX_StreamsSetEvents_accountId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'StreamsSetEvents', schema }, - ['receiversHash'], - { - name: 'IX_StreamsSetEvents_receiversHash', - unique: false, - }, - ); -} - -async function createStreamReceiverSeenEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'StreamReceiverSeenEvents', schema }, - { - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - receiversHash: { - type: DataTypes.STRING, - allowNull: false, - }, - config: { - type: DataTypes.STRING, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - ); - - await queryInterface.addIndex( - { tableName: 'StreamReceiverSeenEvents', schema }, - ['accountId'], - { - name: 'IX_StreamReceiverSeenEvents_accountId', - unique: false, - }, - ); -} - -async function createSqueezedStreamsEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'SqueezedStreamsEvents', schema }, - { - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - erc20: { - type: DataTypes.STRING, - allowNull: false, - }, - senderId: { - type: DataTypes.STRING, - allowNull: false, - }, - amount: { - type: DataTypes.STRING, - allowNull: false, - }, - streamsHistoryHashes: { - type: DataTypes.JSON, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - ); -} - -async function createSplitsSetEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'SplitsSetEvents', schema }, - { - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - receiversHash: { - type: DataTypes.STRING, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - ); -} - -async function createSplitEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'SplitEvents', schema }, - { - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - receiver: { - type: DataTypes.STRING, - allowNull: false, - }, - erc20: { - type: DataTypes.STRING, - allowNull: false, - }, - amt: { - type: DataTypes.STRING, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SplitEvents', schema }, - ['receiver'], - { - name: 'IX_SplitEvents_receiver', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SplitEvents', schema }, - ['accountId', 'receiver'], - { - name: 'IX_SplitEvents_accountId_receiver', - unique: false, - }, - ); -} - -async function createRepoDriverSplitReceiversTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'RepoDriverSplitReceivers', schema }, - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - fundeeProjectId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'GitProjects', - key: 'id', - }, - allowNull: false, - }, - funderProjectId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'GitProjects', - key: 'id', - }, - allowNull: true, - }, - funderDripListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'DripLists', - key: 'id', - }, - allowNull: true, - }, - weight: { - type: DataTypes.INTEGER, - allowNull: true, - }, - type: { - type: DataTypes.ENUM(...Object.values(DependencyType)), - allowNull: false, - }, - blockTimestamp: { - type: DataTypes.DATE, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); - - await queryInterface.addIndex( - { tableName: 'RepoDriverSplitReceivers', schema }, - ['fundeeProjectId'], - { - name: 'IX_RepoDriverSplitReceivers_fundeeProjectId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'RepoDriverSplitReceivers', schema }, - ['funderProjectId'], - { - name: 'IX_RepoDriverSplitReceivers_funderProjectId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'RepoDriverSplitReceivers', schema }, - ['funderDripListId'], - { - name: 'IX_RepoDriverSplitReceivers_funderDripListId', - unique: false, - }, - ); -} - -async function createOwnerUpdateRequestedEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'OwnerUpdateRequestedEvents', schema }, - { - name: { - type: DataTypes.STRING, - allowNull: false, - }, - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - forge: { - type: DataTypes.ENUM(...Object.values(FORGES_MAP)), - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - ); -} - -async function createOwnerUpdatedEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'OwnerUpdatedEvents', schema }, - { - owner: { - type: DataTypes.STRING, - allowNull: false, - }, - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - ); -} - -async function createGivenEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'GivenEvents', schema }, - { - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - receiver: { - type: DataTypes.STRING, - allowNull: false, - }, - erc20: { - type: DataTypes.STRING, - allowNull: false, - }, - amt: { - type: DataTypes.STRING, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - ); - - await queryInterface.addIndex( - { tableName: 'GivenEvents', schema }, - ['accountId'], - { - name: 'IX_GivenEvents_accountId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'GivenEvents', schema }, - ['receiver'], - { - name: 'IX_GivenEvents_receiver', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'GivenEvents', schema }, - ['erc20'], - { - name: 'IX_GivenEvents_erc20', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'GivenEvents', schema }, - ['transactionHash', 'logIndex'], - { - name: 'IX_GivenEvents_transactionHash_logIndex', - unique: false, - }, - ); -} - -async function createDripListSplitReceiversTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'DripListSplitReceivers', schema }, - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - fundeeDripListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'DripLists', - key: 'id', - }, - allowNull: false, - }, - funderProjectId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'GitProjects', - key: 'id', - }, - allowNull: true, - }, - funderDripListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'DripLists', - key: 'id', - }, - allowNull: true, - }, - weight: { - type: DataTypes.INTEGER, - allowNull: true, - }, - type: { - type: DataTypes.ENUM(...Object.values(DependencyType)), - allowNull: false, - }, - blockTimestamp: { - type: DataTypes.DATE, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); - - await queryInterface.addIndex( - { tableName: 'DripListSplitReceivers', schema }, - ['fundeeDripListId'], - { - name: 'IX_DripListSplitReceivers_fundeeDripListId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'DripListSplitReceivers', schema }, - ['funderProjectId'], - { - name: 'IX_DripListSplitReceivers_funderProjectId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'DripListSplitReceivers', schema }, - ['funderDripListId'], - { - name: 'IX_DripListSplitReceivers_funderDripListId', - unique: false, - }, - ); -} - -async function createDripListsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'DripLists', schema }, - { - id: { - type: DataTypes.STRING, - primaryKey: true, - }, - isValid: { - type: DataTypes.BOOLEAN, - allowNull: false, - }, - ownerAddress: { - type: DataTypes.STRING, - allowNull: false, - }, - ownerAccountId: { - type: DataTypes.STRING, - allowNull: false, - }, - name: { - type: DataTypes.STRING, - allowNull: true, - }, - latestVotingRoundId: { - type: DataTypes.UUID, - allowNull: true, - }, - description: { - type: DataTypes.TEXT, - allowNull: true, - }, - creator: { - type: DataTypes.STRING, - allowNull: false, - }, - previousOwnerAddress: { - type: DataTypes.STRING, - allowNull: false, - }, - isVisible: { - type: DataTypes.BOOLEAN, - allowNull: false, - }, - lastProcessedIpfsHash: { - type: DataTypes.TEXT, - allowNull: true, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); - - await queryInterface.addIndex( - { tableName: 'DripLists', schema }, - ['ownerAddress'], - { - name: 'IX_DripLists_ownerAddress', - unique: false, - where: { isValid: true }, - }, - ); -} - -async function createAddressDriverSplitReceiversTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'AddressDriverSplitReceivers', schema }, - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - fundeeAccountId: { - type: DataTypes.STRING, - allowNull: false, - }, - fundeeAccountAddress: { - type: DataTypes.STRING, - allowNull: false, - }, - funderProjectId: { - type: DataTypes.STRING, - references: { - model: 'GitProjects', - key: 'id', - }, - allowNull: true, - }, - funderDripListId: { - type: DataTypes.STRING, - references: { - model: 'DripLists', - key: 'id', - }, - allowNull: true, - }, - weight: { - type: DataTypes.INTEGER, - allowNull: true, - }, - type: { - type: DataTypes.ENUM(...Object.values(AddressDriverSplitReceiverType)), - allowNull: false, - }, - blockTimestamp: { - type: DataTypes.DATE, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); - - await queryInterface.addIndex( - { tableName: 'AddressDriverSplitReceivers', schema }, - ['fundeeAccountId'], - { - name: 'IX_AddressDriverSplitReceivers_fundeeAccountId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'AddressDriverSplitReceivers', schema }, - ['funderDripListId'], - { - name: 'IX_AddressDriverSplitReceivers_funderDripListId', - unique: false, - }, - ); -} - -async function createAccountMetadataEmittedEventsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'AccountMetadataEmittedEvents', schema }, - { - key: { - type: DataTypes.STRING, - allowNull: false, - }, - value: { - type: DataTypes.STRING, - allowNull: false, - }, - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - ); - - await queryInterface.addIndex( - { tableName: 'AccountMetadataEmittedEvents', schema }, - ['accountId'], - { - name: 'IX_AccountMetadataEmittedEvents_accountId', - unique: false, - }, - ); -} - -async function createLastIndexedBlockTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: '_LastIndexedBlock', schema }, - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - blockNumber: { - type: DataTypes.BIGINT, - allowNull: false, - unique: true, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); -} - -async function createProjectsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'GitProjects', schema }, - { - id: { - type: DataTypes.STRING, - primaryKey: true, - }, - isValid: { - type: DataTypes.BOOLEAN, - allowNull: false, - }, - name: { - type: DataTypes.STRING, - allowNull: true, - }, - verificationStatus: { - type: DataTypes.ENUM(...Object.values(ProjectVerificationStatus)), - allowNull: false, - }, - claimedAt: { - type: DataTypes.DATE, - allowNull: true, - }, - forge: { - type: DataTypes.ENUM(...Object.values(FORGES_MAP)), - allowNull: true, - }, - ownerAddress: { - type: DataTypes.STRING, - allowNull: true, - }, - ownerAccountId: { - type: DataTypes.STRING, - allowNull: true, - }, - url: { - type: DataTypes.STRING, - allowNull: true, - }, - emoji: { - type: DataTypes.STRING, - allowNull: true, - }, - avatarCid: { - type: DataTypes.STRING, - allowNull: true, - }, - color: { - type: DataTypes.STRING, - allowNull: true, - }, - description: { - type: DataTypes.TEXT, - allowNull: true, - }, - isVisible: { - type: DataTypes.BOOLEAN, - allowNull: false, - }, - lastProcessedIpfsHash: { - type: DataTypes.TEXT, - allowNull: true, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); - - await queryInterface.addIndex( - { tableName: 'GitProjects', schema }, - ['ownerAddress'], - { - name: 'IX_GitProjects_ownerAddress', - unique: false, - where: { isValid: true }, - }, - ); - - await queryInterface.addIndex( - { tableName: 'GitProjects', schema }, - ['verificationStatus'], - { - name: 'IX_GitProjects_verificationStatus', - unique: false, - where: { isValid: true }, - }, - ); - - await queryInterface.addIndex({ tableName: 'GitProjects', schema }, ['url'], { - name: 'IX_GitProjects_url', - unique: false, - where: { isValid: true }, - }); -} - -async function createTableIfNotExists( - queryInterface: QueryInterface, - schema: DbSchema, - tableName: string, - createFn: (queryInterface: QueryInterface, schema: DbSchema) => Promise, -) { - const tableFullName = { tableName, schema }; - - const tableExists = await queryInterface - .describeTable(tableFullName) - .catch(() => null); - if (!tableExists) { - await createFn(queryInterface, schema); - } -} - -export async function down({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface = sequelize.getQueryInterface(); - - await queryInterface.query.dropSchema(schema); -} diff --git a/src/db/migrations/20250319131551-create_ecosystems.ts b/src/db/migrations/20250319131551-create_ecosystems.ts deleted file mode 100644 index 5d51eaf..0000000 --- a/src/db/migrations/20250319131551-create_ecosystems.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { DataTypes, literal, type QueryInterface } from 'sequelize'; -import getSchema from '../../utils/getSchema'; -import type { DbSchema } from '../../core/types'; - -export async function up({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface = sequelize.getQueryInterface(); - - await createTableIfNotExists( - queryInterface, - schema, - 'EcosystemMainAccounts', - createEcosystemsTable, - ); -} - -async function createEcosystemsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'EcosystemMainAccounts', schema }, - { - id: { - type: DataTypes.STRING, - primaryKey: true, - }, - isValid: { - type: DataTypes.BOOLEAN, - allowNull: false, - }, - ownerAddress: { - type: DataTypes.STRING, - allowNull: false, - }, - ownerAccountId: { - type: DataTypes.STRING, - allowNull: false, - }, - name: { - type: DataTypes.STRING, - allowNull: true, - }, - description: { - type: DataTypes.TEXT, - allowNull: true, - }, - latestVotingRoundId: { - type: DataTypes.UUID, - allowNull: true, - }, - creator: { - type: DataTypes.STRING, - allowNull: false, - }, - previousOwnerAddress: { - type: DataTypes.STRING, - allowNull: false, - }, - isVisible: { - type: DataTypes.BOOLEAN, - allowNull: false, - }, - lastProcessedIpfsHash: { - type: DataTypes.TEXT, - allowNull: true, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); - - await queryInterface.addIndex( - { tableName: 'EcosystemMainAccounts', schema }, - ['ownerAddress'], - { - name: 'IX_Ecosystems_ownerAddress', - unique: false, - }, - ); -} - -async function createTableIfNotExists( - queryInterface: QueryInterface, - schema: DbSchema, - tableName: string, - createFn: (queryInterface: QueryInterface, schema: DbSchema) => Promise, -) { - const tableFullName = { tableName, schema }; - - const tableExists = await queryInterface - .describeTable(tableFullName) - .catch(() => null); - if (!tableExists) { - await createFn(queryInterface, schema); - } -} - -export async function down({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface = sequelize.getQueryInterface(); - - await queryInterface.dropTable({ tableName: 'CreatedSplitsEvents', schema }); - await queryInterface.dropTable({ - tableName: 'EcosystemMainAccounts', - schema, - }); -} diff --git a/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts b/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts deleted file mode 100644 index 57f9513..0000000 --- a/src/db/migrations/20250324164221-create_sublists_and_split_receivers_tables.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { DataTypes, literal, type QueryInterface } from 'sequelize'; -import getSchema from '../../utils/getSchema'; -import { DependencyType, type DbSchema } from '../../core/types'; -import {} from '../../models'; - -export async function up({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface = sequelize.getQueryInterface(); - - await createTableIfNotExists( - queryInterface, - schema, - 'SubLists', - createSubListsTable, - ); - - await createTableIfNotExists( - queryInterface, - schema, - 'SubListSplitReceivers', - createSubListSplitReceiversTable, - ); -} - -async function createSubListsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'SubLists', schema }, - { - id: { - type: DataTypes.STRING, - primaryKey: true, - }, - parentDripListId: { - // Foreign key - type: DataTypes.STRING, - allowNull: true, - references: { - model: 'DripLists', - key: 'id', - }, - }, - parentEcosystemMainAccountId: { - // Foreign key - type: DataTypes.STRING, - allowNull: true, - references: { - model: 'EcosystemMainAccounts', - key: 'id', - }, - }, - parentSubListId: { - // Foreign key - type: DataTypes.STRING, - allowNull: true, - references: { - model: 'SubLists', - key: 'id', - }, - }, - rootDripListId: { - // Foreign key - type: DataTypes.STRING, - allowNull: true, - references: { - model: 'DripLists', - key: 'id', - }, - }, - rootEcosystemMainAccountId: { - // Foreign key - type: DataTypes.STRING, - allowNull: true, - references: { - model: 'EcosystemMainAccounts', - key: 'id', - }, - }, - lastProcessedIpfsHash: { - type: DataTypes.TEXT, - allowNull: true, - }, - isValid: { - type: DataTypes.BOOLEAN, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SubLists', schema }, - ['parentDripListId'], - { - name: 'IX_SubLists_parentDripListId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SubLists', schema }, - ['parentEcosystemMainAccountId'], - { - name: 'IX_SubLists_parentEcosystemId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SubLists', schema }, - ['parentSubListId'], - { - name: 'IX_SubLists_parentSubListId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SubLists', schema }, - ['rootDripListId'], - { - name: 'IX_SubLists_rootDripListId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SubLists', schema }, - ['rootEcosystemMainAccountId'], - { - name: 'IX_SubLists_rootEcosystemId', - unique: false, - }, - ); -} - -async function createSubListSplitReceiversTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable( - { tableName: 'SubListSplitReceivers', schema }, - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - fundeeSubListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'SubLists', - key: 'id', - }, - allowNull: false, - }, - funderProjectId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'GitProjects', - key: 'id', - }, - allowNull: true, - }, - funderDripListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'DripLists', - key: 'id', - }, - allowNull: true, - }, - funderEcosystemMainAccountId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'EcosystemMainAccounts', - key: 'id', - }, - allowNull: true, - }, - funderSubListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: 'SubLists', - key: 'id', - }, - allowNull: true, - }, - weight: { - type: DataTypes.INTEGER, - allowNull: true, - }, - type: { - type: DataTypes.ENUM(...Object.values(DependencyType)), - allowNull: false, - }, - blockTimestamp: { - type: DataTypes.DATE, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: literal('NOW()'), - }, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SubListSplitReceivers', schema }, - ['fundeeSubListId'], - { - name: 'IX_SubListSplitReceivers_fundeeImmutableSplitsId', - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SubListSplitReceivers', schema }, - ['funderProjectId'], - { - name: 'IX_SubListSplitReceivers_funderProjectId', - where: { - type: DependencyType.ProjectDependency, - }, - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SubListSplitReceivers', schema }, - ['funderDripListId'], - { - name: 'IX_SubListSplitReceivers_funderDripListId', - where: { - type: DependencyType.DripListDependency, - }, - unique: false, - }, - ); - - await queryInterface.addIndex( - { tableName: 'SubListSplitReceivers', schema }, - ['funderEcosystemMainAccountId'], - { - name: 'IX_SubListSplitReceivers_funderEcosystemId', - where: { - type: DependencyType.EcosystemDependency, - }, - unique: false, - }, - ); -} - -async function createTableIfNotExists( - queryInterface: QueryInterface, - schema: DbSchema, - tableName: string, - createFn: (queryInterface: QueryInterface, schema: DbSchema) => Promise, -) { - const tableFullName = { tableName, schema }; - - const tableExists = await queryInterface - .describeTable(tableFullName) - .catch(() => null); - if (!tableExists) { - await createFn(queryInterface, schema); - } -} - -export async function down({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface = sequelize.getQueryInterface(); - - await queryInterface.dropTable({ - tableName: 'SubListSplitReceivers', - schema, - }); - - await queryInterface.dropTable({ tableName: 'SubLists', schema }); -} diff --git a/src/db/migrations/20250325100215-make_driplist_fields_nullable.ts b/src/db/migrations/20250325100215-make_driplist_fields_nullable.ts deleted file mode 100644 index 5d0e5e9..0000000 --- a/src/db/migrations/20250325100215-make_driplist_fields_nullable.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { DataTypes, type QueryInterface } from 'sequelize'; -import getSchema from '../../utils/getSchema'; - -export async function up({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface: QueryInterface = sequelize.getQueryInterface(); - - await queryInterface.changeColumn( - { tableName: 'DripLists', schema }, - 'creator', - { - type: DataTypes.STRING, - allowNull: true, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'DripLists', schema }, - 'ownerAddress', - { - type: DataTypes.STRING, - allowNull: true, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'DripLists', schema }, - 'ownerAccountId', - { - type: DataTypes.STRING, - allowNull: true, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'DripLists', schema }, - 'previousOwnerAddress', - { - type: DataTypes.STRING, - allowNull: true, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'DripLists', schema }, - 'isVisible', - { - type: DataTypes.BOOLEAN, - allowNull: true, - }, - ); -} - -export async function down({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface: QueryInterface = sequelize.getQueryInterface(); - - await queryInterface.changeColumn( - { tableName: 'DripLists', schema }, - 'creator', - { - type: DataTypes.STRING, - allowNull: false, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'DripLists', schema }, - 'ownerAddress', - { - type: DataTypes.STRING, - allowNull: false, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'DripLists', schema }, - 'ownerAccountId', - { - type: DataTypes.STRING, - allowNull: false, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'DripLists', schema }, - 'previousOwnerAddress', - { - type: DataTypes.STRING, - allowNull: false, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'DripLists', schema }, - 'isVisible', - { - type: DataTypes.BOOLEAN, - allowNull: false, - }, - ); -} diff --git a/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts b/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts deleted file mode 100644 index aa9c1a9..0000000 --- a/src/db/migrations/20250326115607-add_ecosystem_dependency_enum_and_make_ecosystem_fields_nullable.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { DataTypes, type QueryInterface } from 'sequelize'; -import getSchema from '../../utils/getSchema'; - -export async function up({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface: QueryInterface = sequelize.getQueryInterface(); - - await queryInterface.sequelize.query(` - ALTER TYPE "${schema}"."enum_SubListSplitReceivers_type" ADD VALUE IF NOT EXISTS 'EcosystemDependency'; - `); - await queryInterface.sequelize.query(` - ALTER TYPE "${schema}"."enum_RepoDriverSplitReceivers_type" ADD VALUE IF NOT EXISTS 'EcosystemDependency'; - `); - await queryInterface.sequelize.query(` - ALTER TYPE "${schema}"."enum_DripListSplitReceivers_type" ADD VALUE IF NOT EXISTS 'EcosystemDependency'; - `); - await queryInterface.sequelize.query(` - ALTER TYPE "${schema}"."enum_AddressDriverSplitReceivers_type" ADD VALUE IF NOT EXISTS 'EcosystemDependency'; - `); - - await queryInterface.changeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'creator', - { - type: DataTypes.STRING, - allowNull: true, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'ownerAddress', - { - type: DataTypes.STRING, - allowNull: true, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'ownerAccountId', - { - type: DataTypes.STRING, - allowNull: true, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'previousOwnerAddress', - { - type: DataTypes.STRING, - allowNull: true, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'isVisible', - { - type: DataTypes.BOOLEAN, - allowNull: true, - }, - ); -} - -export async function down({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface: QueryInterface = sequelize.getQueryInterface(); - - await queryInterface.changeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'creator', - { - type: DataTypes.STRING, - allowNull: false, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'ownerAddress', - { - type: DataTypes.STRING, - allowNull: false, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'ownerAccountId', - { - type: DataTypes.STRING, - allowNull: false, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'previousOwnerAddress', - { - type: DataTypes.STRING, - allowNull: false, - }, - ); - - await queryInterface.changeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'isVisible', - { - type: DataTypes.BOOLEAN, - allowNull: false, - }, - ); -} diff --git a/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts b/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts deleted file mode 100644 index 1e4a03f..0000000 --- a/src/db/migrations/20250328114018-add_funder_ecosystem_id_to_split_receivers.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { DataTypes, type QueryInterface } from 'sequelize'; -import getSchema from '../../utils/getSchema'; - -export async function up({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface: QueryInterface = sequelize.getQueryInterface(); - - await queryInterface.addColumn( - { tableName: 'RepoDriverSplitReceivers', schema }, - 'funderEcosystemMainAccountId', - { - type: DataTypes.STRING, - references: { - model: 'EcosystemMainAccounts', - key: 'id', - }, - allowNull: true, - }, - ); - - await queryInterface.addIndex( - { tableName: 'RepoDriverSplitReceivers', schema }, - ['funderEcosystemMainAccountId'], - { - name: 'IX_RepoDriverSplitReceivers_funderEcosystemId', - where: { - type: 'EcosystemDependency', - }, - unique: false, - }, - ); - - await queryInterface.addColumn( - { tableName: 'RepoDriverSplitReceivers', schema }, - 'funderSubListId', - { - type: DataTypes.STRING, - references: { - model: 'SubLists', - key: 'id', - }, - allowNull: true, - }, - ); - - await queryInterface.addIndex( - { tableName: 'RepoDriverSplitReceivers', schema }, - ['funderSubListId'], - { - name: 'IX_RepoDriverSplitReceivers_funderSubListId', - where: { - type: 'EcosystemDependency', - }, - unique: false, - }, - ); - - await queryInterface.addColumn( - { tableName: 'DripListSplitReceivers', schema }, - 'funderEcosystemMainAccountId', - { - type: DataTypes.STRING, - references: { - model: 'EcosystemMainAccounts', - key: 'id', - }, - allowNull: true, - }, - ); - - await queryInterface.addIndex( - { tableName: 'DripListSplitReceivers', schema }, - ['funderEcosystemMainAccountId'], - { - name: 'IX_DripListSplitReceivers_funderEcosystemId', - where: { - type: 'EcosystemDependency', - }, - unique: false, - }, - ); - - await queryInterface.addColumn( - { tableName: 'DripListSplitReceivers', schema }, - 'funderSubListId', - { - type: DataTypes.STRING, - references: { - model: 'SubLists', - key: 'id', - }, - allowNull: true, - }, - ); - - await queryInterface.addIndex( - { tableName: 'DripListSplitReceivers', schema }, - ['funderSubListId'], - { - name: 'IX_DripListSplitReceivers_funderSubListId', - where: { - type: 'EcosystemDependency', - }, - unique: false, - }, - ); - - await queryInterface.addColumn( - { tableName: 'AddressDriverSplitReceivers', schema }, - 'funderEcosystemMainAccountId', - { - type: DataTypes.STRING, - references: { - model: 'EcosystemMainAccounts', - key: 'id', - }, - allowNull: true, - }, - ); - - await queryInterface.addIndex( - { tableName: 'AddressDriverSplitReceivers', schema }, - ['funderEcosystemMainAccountId'], - { - name: 'IX_AddressDriverSplitReceivers_funderEcosystemId', - where: { - type: 'EcosystemDependency', - }, - unique: false, - }, - ); - - await queryInterface.addColumn( - { tableName: 'AddressDriverSplitReceivers', schema }, - 'funderSubListId', - { - type: DataTypes.STRING, - references: { - model: 'SubLists', - key: 'id', - }, - allowNull: true, - }, - ); - - await queryInterface.addIndex( - { tableName: 'AddressDriverSplitReceivers', schema }, - ['funderSubListId'], - { - name: 'IX_AddressDriverSplitReceivers_funderSubListId', - where: { - type: 'EcosystemDependency', - }, - unique: false, - }, - ); -} - -export async function down({ context: sequelize }: any): Promise { - const schema = getSchema(); - const queryInterface: QueryInterface = sequelize.getQueryInterface(); - - await queryInterface.removeColumn( - { tableName: 'RepoDriverSplitReceivers', schema }, - 'funderEcosystemMainAccountId', - ); - await queryInterface.removeIndex( - { tableName: 'RepoDriverSplitReceivers', schema }, - 'IX_RepoDriverSplitReceivers_funderEcosystemId', - ); - await queryInterface.removeColumn( - { tableName: 'DripListSplitReceivers', schema }, - 'funderEcosystemMainAccountId', - ); - await queryInterface.removeIndex( - { tableName: 'DripListSplitReceivers', schema }, - 'IX_DripListSplitReceivers_funderEcosystemId', - ); - await queryInterface.removeColumn( - { tableName: 'AddressDriverSplitReceivers', schema }, - 'funderEcosystemMainAccountId', - ); - await queryInterface.removeIndex( - { tableName: 'AddressDriverSplitReceivers', schema }, - 'IX_AddressDriverSplitReceivers_funderEcosystemId', - ); -} diff --git a/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts b/src/db/migrations/20250414133746-initial_create.ts similarity index 50% rename from src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts rename to src/db/migrations/20250414133746-initial_create.ts index 721b70b..7256bc1 100644 --- a/src/db/migrations/20250331091615-remove-latestVotingRoundId-from-ecosystems.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -1,27 +1,27 @@ -import { DataTypes, type QueryInterface } from 'sequelize'; +import { type QueryInterface } from 'sequelize'; import getSchema from '../../utils/getSchema'; export async function up({ context: sequelize }: any): Promise { const schema = getSchema(); const queryInterface: QueryInterface = sequelize.getQueryInterface(); - await queryInterface.removeColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'latestVotingRoundId', - ); + await queryInterface.sequelize.query(` + CREATE TYPE "${schema}"."account_type" AS ENUM ( + 'project', + 'dripList', + 'ecosystemMainAccount', + 'subList', + 'deadline', + 'address' + ); + `); } export async function down({ context: sequelize }: any): Promise { const schema = getSchema(); const queryInterface: QueryInterface = sequelize.getQueryInterface(); - await queryInterface.addColumn( - { tableName: 'EcosystemMainAccounts', schema }, - 'latestVotingRoundId', - { - type: DataTypes.STRING, - allowNull: true, - defaultValue: null, - }, - ); + await queryInterface.sequelize.query(` + DROP TYPE IF EXISTS "${schema}"."account_type"; + `); } diff --git a/src/health.ts b/src/health.ts index 7c5028f..73d5154 100644 --- a/src/health.ts +++ b/src/health.ts @@ -21,7 +21,7 @@ export const healthEndpoint: RequestHandler = async (req, res) => { const blockDifference = latestChainBlock - lastIndexedBlock; if (blockDifference < HEALTH_THRESHOLD) { - return res.status(200).send({ + res.status(200).send({ status: 'OK', latestChainBlock, lastIndexedBlock, @@ -32,7 +32,7 @@ export const healthEndpoint: RequestHandler = async (req, res) => { logger.warn( `Health check failed: Service is ${blockDifference} blocks behind (Threshold: ${HEALTH_THRESHOLD}).`, ); - return res.status(503).send({ + res.status(503).send({ status: 'Unhealthy', latestChainBlock, lastIndexedBlock, @@ -41,7 +41,7 @@ export const healthEndpoint: RequestHandler = async (req, res) => { }); } catch (error: any) { logger.error(`Health check endpoint error: ${error.message}`, error); - return res.status(500).send({ + res.status(500).send({ status: 'Error', message: 'Internal server error during health check.', }); From 8199129e2ae844ae4183edd094dc59c949b53fe2 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 14 Apr 2025 17:08:12 +0200 Subject: [PATCH 21/60] docs: update DEVELOPMENT.md --- DEVELOPMENT.md | 89 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d0c18bb..9d787ed 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,15 +1,88 @@ -# Database Migrations +# 🗂️ Database Migrations -## Running Migrations +## 🛠️ Creating a New Migration -Migrations do not run automatically. To run the migrations: +To generate a new migration, run: -1. Build the TypeScript code: `npm run build` +```bash +npm run db:create-migration -- --name your_migration_name +``` -2. Run the migrations: `npm run db:run-migrations`. +This will create a new file in `src/db/migrations`. -## Creating a New Migration +Rename the file extension from `.js` to `.ts`, and implement your migration using the following structure: -To generate a new migration: `npm run db:create-migration -- --name your_migration_name` +```ts +import { QueryInterface } from 'sequelize'; -This creates a new migration file in src/db/migrations — just add your changes there. +export async function up({ context: sequelize }: any): Promise { + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + // Add your migration changes here. +} + +export async function down({ context: sequelize }: any): Promise { + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + // Add your revert logic here. +} +``` + +Sequelize does not fully support TypeScript out of the box. Manual steps like renaming and typing are required... + +> ⚠️ Make sure the project is built before running this scripts, as it uses the compiled .js files under dist. + +--- + +## 🚀 Running Migrations + +Migrations do **not** run automatically. To apply them: + +1. **Clean the project** + + ```bash + npm run clean + ``` + +2. **Build the project** + + ```bash + npm run build + ``` + +3. **Run pending migrations** + + ```bash + npm run db:run-migrations + ``` + +🤓 Alternatively, use: + +```bash +npm run dev:db:run-migrations +``` + +This runs all the above steps in one go. + +## 🔁 Reverting the Last Migration + +To undo the most recently applied migration: + +```bash +npm run db:revert-last-migration +``` + +> ⚠️ Make sure the project is built before running this script, as it uses the compiled .js files under dist. + +## 📋 Checking Migration Status + +To view the status of all migrations (which ones have been applied and which are pending), run: + +```bash +npm run dev:db:log-pending-migrations +``` + +This will output a list of migrations with their current status: + +- `up` indicates the migration has been applied. +- `down` indicates the migration is still pending. From 99e7b44b641110b26e7291e1bb8277d130aa04dc Mon Sep 17 00:00:00 2001 From: Georgios Jason Efstathiou Date: Tue, 15 Apr 2025 10:10:58 +0200 Subject: [PATCH 22/60] add zksync-era-sepolia config --- src/abi/zksync-era-sepolia/AddressDriver.json | 414 ++++++ src/abi/zksync-era-sepolia/Drips.json | 1221 +++++++++++++++++ .../ImmutableSplitsDriver.json | 358 +++++ src/abi/zksync-era-sepolia/NftDriver.json | 728 ++++++++++ src/abi/zksync-era-sepolia/RepoDriver.json | 966 +++++++++++++ .../chainConfigs/zksync-era-sepolia.json | 21 + src/core/constants.ts | 1 + 7 files changed, 3709 insertions(+) create mode 100644 src/abi/zksync-era-sepolia/AddressDriver.json create mode 100644 src/abi/zksync-era-sepolia/Drips.json create mode 100644 src/abi/zksync-era-sepolia/ImmutableSplitsDriver.json create mode 100644 src/abi/zksync-era-sepolia/NftDriver.json create mode 100644 src/abi/zksync-era-sepolia/RepoDriver.json create mode 100644 src/config/chainConfigs/zksync-era-sepolia.json diff --git a/src/abi/zksync-era-sepolia/AddressDriver.json b/src/abi/zksync-era-sepolia/AddressDriver.json new file mode 100644 index 0000000..cc1b059 --- /dev/null +++ b/src/abi/zksync-era-sepolia/AddressDriver.json @@ -0,0 +1,414 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "drips_", "type": "address" }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "addr", "type": "address" } + ], + "name": "calcAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/zksync-era-sepolia/Drips.json b/src/abi/zksync-era-sepolia/Drips.json new file mode 100644 index 0000000..341b9f8 --- /dev/null +++ b/src/abi/zksync-era-sepolia/Drips.json @@ -0,0 +1,1221 @@ +[ + { + "inputs": [ + { "internalType": "uint32", "name": "cycleSecs_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "key", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "value", + "type": "bytes" + } + ], + "name": "AccountMetadataEmitted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amt", + "type": "uint128" + } + ], + "name": "Collectable", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "collected", + "type": "uint128" + } + ], + "name": "Collected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "driverId", + "type": "uint32" + }, + { + "indexed": true, + "internalType": "address", + "name": "oldDriverAddr", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newDriverAddr", + "type": "address" + } + ], + "name": "DriverAddressUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "driverId", + "type": "uint32" + }, + { + "indexed": true, + "internalType": "address", + "name": "driverAddr", + "type": "address" + } + ], + "name": "DriverRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "receiver", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amt", + "type": "uint128" + } + ], + "name": "Given", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amt", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "receivableCycles", + "type": "uint32" + } + ], + "name": "ReceivedStreams", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "receiver", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amt", + "type": "uint128" + } + ], + "name": "Split", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "weight", + "type": "uint32" + } + ], + "name": "SplitsReceiverSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "SplitsSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "senderId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amt", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "bytes32[]", + "name": "streamsHistoryHashes", + "type": "bytes32[]" + } + ], + "name": "SqueezedStreams", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "name": "StreamReceiverSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "streamsHistoryHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "balance", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "maxEnd", + "type": "uint32" + } + ], + "name": "StreamsSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amt", + "type": "uint256" + } + ], + "name": "Withdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "AMT_PER_SEC_EXTRA_DECIMALS", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "AMT_PER_SEC_MULTIPLIER", + "outputs": [{ "internalType": "uint160", "name": "", "type": "uint160" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DRIVER_ID_OFFSET", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_SPLITS_RECEIVERS", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_STREAMS_RECEIVERS", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_TOTAL_BALANCE", + "outputs": [{ "internalType": "uint128", "name": "", "type": "uint128" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "TOTAL_SPLITS_WEIGHT", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "timestamp", "type": "uint32" } + ], + "name": "balanceAt", + "outputs": [ + { "internalType": "uint128", "name": "balance", "type": "uint128" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "balances", + "outputs": [ + { + "internalType": "uint128", + "name": "streamsBalance", + "type": "uint128" + }, + { "internalType": "uint128", "name": "splitsBalance", "type": "uint128" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collectable", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cycleSecs", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint32", "name": "driverId", "type": "uint32" } + ], + "name": "driverAddress", + "outputs": [ + { "internalType": "address", "name": "driverAddr", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "hashSplits", + "outputs": [ + { "internalType": "bytes32", "name": "receiversHash", "type": "bytes32" } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "hashStreams", + "outputs": [ + { "internalType": "bytes32", "name": "streamsHash", "type": "bytes32" } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "oldStreamsHistoryHash", + "type": "bytes32" + }, + { "internalType": "bytes32", "name": "streamsHash", "type": "bytes32" }, + { "internalType": "uint32", "name": "updateTime", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEnd", "type": "uint32" } + ], + "name": "hashStreamsHistory", + "outputs": [ + { + "internalType": "bytes32", + "name": "streamsHistoryHash", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minAmtPerSec", + "outputs": [{ "internalType": "uint160", "name": "", "type": "uint160" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextDriverId", + "outputs": [ + { "internalType": "uint32", "name": "driverId", "type": "uint32" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "receivableStreamsCycles", + "outputs": [ + { "internalType": "uint32", "name": "cycles", "type": "uint32" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint32", "name": "maxCycles", "type": "uint32" } + ], + "name": "receiveStreams", + "outputs": [ + { "internalType": "uint128", "name": "receivedAmt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint32", "name": "maxCycles", "type": "uint32" } + ], + "name": "receiveStreamsResult", + "outputs": [ + { "internalType": "uint128", "name": "receivableAmt", "type": "uint128" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "driverAddr", "type": "address" } + ], + "name": "registerDriver", + "outputs": [ + { "internalType": "uint32", "name": "driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + } + ], + "name": "split", + "outputs": [ + { + "internalType": "uint128", + "name": "collectableAmt", + "type": "uint128" + }, + { "internalType": "uint128", "name": "splitAmt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "uint128", "name": "amount", "type": "uint128" } + ], + "name": "splitResult", + "outputs": [ + { + "internalType": "uint128", + "name": "collectableAmt", + "type": "uint128" + }, + { "internalType": "uint128", "name": "splitAmt", "type": "uint128" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "splitsHash", + "outputs": [ + { "internalType": "bytes32", "name": "currSplitsHash", "type": "bytes32" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "splittable", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint256", "name": "senderId", "type": "uint256" }, + { "internalType": "bytes32", "name": "historyHash", "type": "bytes32" }, + { + "components": [ + { + "internalType": "bytes32", + "name": "streamsHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "updateTime", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEnd", "type": "uint32" } + ], + "internalType": "struct StreamsHistory[]", + "name": "streamsHistory", + "type": "tuple[]" + } + ], + "name": "squeezeStreams", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint256", "name": "senderId", "type": "uint256" }, + { "internalType": "bytes32", "name": "historyHash", "type": "bytes32" }, + { + "components": [ + { + "internalType": "bytes32", + "name": "streamsHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "updateTime", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEnd", "type": "uint32" } + ], + "internalType": "struct StreamsHistory[]", + "name": "streamsHistory", + "type": "tuple[]" + } + ], + "name": "squeezeStreamsResult", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "streamsState", + "outputs": [ + { "internalType": "bytes32", "name": "streamsHash", "type": "bytes32" }, + { + "internalType": "bytes32", + "name": "streamsHistoryHash", + "type": "bytes32" + }, + { "internalType": "uint32", "name": "updateTime", "type": "uint32" }, + { "internalType": "uint128", "name": "balance", "type": "uint128" }, + { "internalType": "uint32", "name": "maxEnd", "type": "uint32" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint32", "name": "driverId", "type": "uint32" }, + { "internalType": "address", "name": "newDriverAddr", "type": "address" } + ], + "name": "updateDriverAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "receiver", "type": "address" }, + { "internalType": "uint256", "name": "amt", "type": "uint256" } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/abi/zksync-era-sepolia/ImmutableSplitsDriver.json b/src/abi/zksync-era-sepolia/ImmutableSplitsDriver.json new file mode 100644 index 0000000..9879574 --- /dev/null +++ b/src/abi/zksync-era-sepolia/ImmutableSplitsDriver.json @@ -0,0 +1,358 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "_drips", "type": "address" }, + { "internalType": "uint32", "name": "_driverId", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "receiversHash", + "type": "bytes32" + } + ], + "name": "CreatedSplits", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "createSplits", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSplitsWeight", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/zksync-era-sepolia/NftDriver.json b/src/abi/zksync-era-sepolia/NftDriver.json new file mode 100644 index 0000000..a764c14 --- /dev/null +++ b/src/abi/zksync-era-sepolia/NftDriver.json @@ -0,0 +1,728 @@ +[ + { + "inputs": [ + { "internalType": "contract Drips", "name": "drips_", "type": "address" }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "minter", "type": "address" }, + { "internalType": "uint64", "name": "salt", "type": "uint64" } + ], + "name": "calcTokenIdWithSalt", + "outputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "getApproved", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "operator", "type": "address" } + ], + "name": "isApprovedForAll", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "minter", "type": "address" }, + { "internalType": "uint64", "name": "salt", "type": "uint64" } + ], + "name": "isSaltUsed", + "outputs": [{ "internalType": "bool", "name": "isUsed", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "mint", + "outputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint64", "name": "salt", "type": "uint64" }, + { "internalType": "address", "name": "to", "type": "address" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "mintWithSalt", + "outputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "nextTokenId", + "outputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "safeMint", + "outputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint64", "name": "salt", "type": "uint64" }, + { "internalType": "address", "name": "to", "type": "address" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "safeMintWithSalt", + "outputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "operator", "type": "address" }, + { "internalType": "bool", "name": "approved", "type": "bool" } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" } + ], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "tokenURI", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "tokenId", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/zksync-era-sepolia/RepoDriver.json b/src/abi/zksync-era-sepolia/RepoDriver.json new file mode 100644 index 0000000..7fecd71 --- /dev/null +++ b/src/abi/zksync-era-sepolia/RepoDriver.json @@ -0,0 +1,966 @@ +[ + { + "inputs": [ + { + "internalType": "contract Drips", + "name": "drips_", + "type": "address" + }, + { + "internalType": "address", + "name": "forwarder", + "type": "address" + }, + { + "internalType": "uint32", + "name": "driverId_", + "type": "uint32" + }, + { + "internalType": "contract IAutomate", + "name": "gelatoAutomate_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "userFundsUsed", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "commonFundsUsed", + "type": "uint256" + } + ], + "name": "GelatoFeePaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract GelatoTasksOwner", + "name": "gelatoTasksOwner", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "taskId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "ipfsCid", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "maxRequestsPerBlock", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "maxRequestsPer31Days", + "type": "uint32" + } + ], + "name": "GelatoTaskUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "enum Forge", + "name": "forge", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "name", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "address", + "name": "payer", + "type": "address" + } + ], + "name": "OwnerUpdateRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnerUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "UserFundsDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address payable", + "name": "receiver", + "type": "address" + } + ], + "name": "UserFundsWithdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum Forge", + "name": "forge", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "name", + "type": "bytes" + } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "internalType": "address", + "name": "transferTo", + "type": "address" + } + ], + "name": "collect", + "outputs": [ + { + "internalType": "uint128", + "name": "amt", + "type": "uint128" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "commonFunds", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "depositUserFunds", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { + "internalType": "contract Drips", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "key", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "value", + "type": "bytes" + } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "gelatoAutomate", + "outputs": [ + { + "internalType": "contract IAutomate", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gelatoTasksOwner", + "outputs": [ + { + "internalType": "contract GelatoTasksOwner", + "name": "tasksOwner", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "receiver", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "internalType": "uint128", + "name": "amt", + "type": "uint128" + } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "isPauser", + "outputs": [ + { + "internalType": "bool", + "name": "isAddrPauser", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "forwarder", + "type": "address" + } + ], + "name": "isTrustedForwarder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum Forge", + "name": "forge", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "name", + "type": "bytes" + } + ], + "name": "requestUpdateOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "requestUpdateOwnerGasPenalty", + "outputs": [ + { + "internalType": "uint256", + "name": "gasPenalty", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "weight", + "type": "uint32" + } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "internalType": "contract IERC20", + "name": "erc20", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { + "internalType": "int128", + "name": "balanceDelta", + "type": "int128" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { + "internalType": "uint32", + "name": "maxEndHint1", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxEndHint2", + "type": "uint32" + }, + { + "internalType": "address", + "name": "transferTo", + "type": "address" + } + ], + "name": "setStreams", + "outputs": [ + { + "internalType": "int128", + "name": "realBalanceDelta", + "type": "int128" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "ipfsCid", + "type": "string" + }, + { + "internalType": "uint32", + "name": "maxRequestsPerBlock", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "maxRequestsPer31Days", + "type": "uint32" + } + ], + "name": "updateGelatoTask", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "payer", + "type": "address" + } + ], + "name": "updateOwnerByGelato", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "userFunds", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "receiver", + "type": "address" + } + ], + "name": "withdrawUserFunds", + "outputs": [ + { + "internalType": "uint256", + "name": "withdrawnAmount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/src/config/chainConfigs/zksync-era-sepolia.json b/src/config/chainConfigs/zksync-era-sepolia.json new file mode 100644 index 0000000..f0f735d --- /dev/null +++ b/src/config/chainConfigs/zksync-era-sepolia.json @@ -0,0 +1,21 @@ +{ + "network": "zksync-era-sepolia", + "block": 5023276, + "contracts": { + "drips": { + "address": "0xe190AB5e92F937751c5ECec9416349666e8C54b9" + }, + "repoDriver": { + "address": "0x8bDC23877A23Ce59fEF1712A1486810d9A6E2B94" + }, + "addressDriver": { + "address": "0x0557b6BA791A24df0Fa6167E1Dc304F403ee777A" + }, + "nftDriver": { + "address": "0xB6B3b6a24cB9A9E2De4c6f2Ea54870E411bFc30d" + }, + "immutableSplitsDriver": { + "address": "0x47A51E1b8BcB885D0fE6b47d514EEf1E28F0ae95" + } + } +} diff --git a/src/core/constants.ts b/src/core/constants.ts index c66ae43..ba92425 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -10,6 +10,7 @@ export const SUPPORTED_NETWORKS = [ 'filecoin', 'metis', 'optimism', + 'zksync-era-sepolia', ] as const; export const FORGES_MAP = { From 6027cc4aef8feb744b8c18158c455921f6d4cc6d Mon Sep 17 00:00:00 2001 From: Georgios Jason Efstathiou Date: Tue, 15 Apr 2025 10:15:02 +0200 Subject: [PATCH 23/60] use underscores --- .../{zksync-era-sepolia.json => zksync_era_sepolia.json} | 2 +- src/core/constants.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/config/chainConfigs/{zksync-era-sepolia.json => zksync_era_sepolia.json} (93%) diff --git a/src/config/chainConfigs/zksync-era-sepolia.json b/src/config/chainConfigs/zksync_era_sepolia.json similarity index 93% rename from src/config/chainConfigs/zksync-era-sepolia.json rename to src/config/chainConfigs/zksync_era_sepolia.json index f0f735d..fa0bd6e 100644 --- a/src/config/chainConfigs/zksync-era-sepolia.json +++ b/src/config/chainConfigs/zksync_era_sepolia.json @@ -1,5 +1,5 @@ { - "network": "zksync-era-sepolia", + "network": "zksync_era_sepolia", "block": 5023276, "contracts": { "drips": { diff --git a/src/core/constants.ts b/src/core/constants.ts index ba92425..0f46ef9 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -10,7 +10,7 @@ export const SUPPORTED_NETWORKS = [ 'filecoin', 'metis', 'optimism', - 'zksync-era-sepolia', + 'zksync_era_sepolia', ] as const; export const FORGES_MAP = { From fd2f45f71c7cd9c30ea440442c865e7526a04f2a Mon Sep 17 00:00:00 2001 From: Georgios Jason Efstathiou Date: Tue, 15 Apr 2025 10:20:41 +0200 Subject: [PATCH 24/60] fix --- .../{zksync-era-sepolia => zksync_era_sepolia}/AddressDriver.json | 0 src/abi/{zksync-era-sepolia => zksync_era_sepolia}/Drips.json | 0 .../ImmutableSplitsDriver.json | 0 src/abi/{zksync-era-sepolia => zksync_era_sepolia}/NftDriver.json | 0 .../{zksync-era-sepolia => zksync_era_sepolia}/RepoDriver.json | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename src/abi/{zksync-era-sepolia => zksync_era_sepolia}/AddressDriver.json (100%) rename src/abi/{zksync-era-sepolia => zksync_era_sepolia}/Drips.json (100%) rename src/abi/{zksync-era-sepolia => zksync_era_sepolia}/ImmutableSplitsDriver.json (100%) rename src/abi/{zksync-era-sepolia => zksync_era_sepolia}/NftDriver.json (100%) rename src/abi/{zksync-era-sepolia => zksync_era_sepolia}/RepoDriver.json (100%) diff --git a/src/abi/zksync-era-sepolia/AddressDriver.json b/src/abi/zksync_era_sepolia/AddressDriver.json similarity index 100% rename from src/abi/zksync-era-sepolia/AddressDriver.json rename to src/abi/zksync_era_sepolia/AddressDriver.json diff --git a/src/abi/zksync-era-sepolia/Drips.json b/src/abi/zksync_era_sepolia/Drips.json similarity index 100% rename from src/abi/zksync-era-sepolia/Drips.json rename to src/abi/zksync_era_sepolia/Drips.json diff --git a/src/abi/zksync-era-sepolia/ImmutableSplitsDriver.json b/src/abi/zksync_era_sepolia/ImmutableSplitsDriver.json similarity index 100% rename from src/abi/zksync-era-sepolia/ImmutableSplitsDriver.json rename to src/abi/zksync_era_sepolia/ImmutableSplitsDriver.json diff --git a/src/abi/zksync-era-sepolia/NftDriver.json b/src/abi/zksync_era_sepolia/NftDriver.json similarity index 100% rename from src/abi/zksync-era-sepolia/NftDriver.json rename to src/abi/zksync_era_sepolia/NftDriver.json diff --git a/src/abi/zksync-era-sepolia/RepoDriver.json b/src/abi/zksync_era_sepolia/RepoDriver.json similarity index 100% rename from src/abi/zksync-era-sepolia/RepoDriver.json rename to src/abi/zksync_era_sepolia/RepoDriver.json From 51e2b4aaea558d0af9ebc95426117de2131a5a92 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 15 Apr 2025 13:09:09 +0200 Subject: [PATCH 25/60] feat: stop processing OwnerUpdateRequested, OwnerUpdated and SplitsSet events --- src/db/modelRegistration.ts | 10 +- .../OwnerUpdateRequestedEventHandler.ts | 145 ------- src/eventHandlers/OwnerUpdatedEventHandler.ts | 117 ------ .../SplitsSetEventHandler.ts | 70 ---- .../SplitsSetEventHandler/setIsValidFlag.ts | 357 ------------------ src/eventHandlers/index.ts | 3 - src/models/OwnerUpdatedEventModel.ts | 50 --- src/models/SplitsSetEventModel.ts | 48 --- src/models/index.ts | 5 +- .../OwnerUpdateRequestedEventHandler.test.ts | 153 -------- .../OwnerUpdatedEventHandler.test.ts | 151 -------- .../SplitsSetEventHandler.test.ts | 92 ----- 12 files changed, 3 insertions(+), 1198 deletions(-) delete mode 100644 src/eventHandlers/OwnerUpdateRequestedEventHandler.ts delete mode 100644 src/eventHandlers/OwnerUpdatedEventHandler.ts delete mode 100644 src/eventHandlers/SplitsSetEventHandler/SplitsSetEventHandler.ts delete mode 100644 src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts delete mode 100644 src/models/OwnerUpdatedEventModel.ts delete mode 100644 src/models/SplitsSetEventModel.ts delete mode 100644 tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts delete mode 100644 tests/eventHandlers/OwnerUpdatedEventHandler.test.ts delete mode 100644 tests/eventHandlers/SplitsSetEventHandler.test.ts diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index d7005a1..e6e7265 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -4,13 +4,10 @@ import { AddressDriverSplitReceiverModel, DripListModel, DripListSplitReceiverModel, - ProjectModel, - OwnerUpdateRequestedEventModel, - OwnerUpdatedEventModel, + Project, RepoDriverSplitReceiverModel, TransferEventModel, GivenEventModel, - SplitsSetEventModel, StreamsSetEventModel, StreamReceiverSeenEventModel, _LastIndexedBlockModel, @@ -38,18 +35,15 @@ export function registerModels(): void { registerModel(DripListModel); registerModel(GivenEventModel); registerModel(SplitEventModel); - registerModel(ProjectModel); + registerModel(Project); registerModel(EcosystemMainAccountModel); registerModel(TransferEventModel); - registerModel(SplitsSetEventModel); registerModel(StreamsSetEventModel); - registerModel(OwnerUpdatedEventModel); registerModel(SqueezedStreamsEventModel); registerModel(SubListSplitReceiverModel); registerModel(DripListSplitReceiverModel); registerModel(StreamReceiverSeenEventModel); registerModel(RepoDriverSplitReceiverModel); - registerModel(OwnerUpdateRequestedEventModel); registerModel(AddressDriverSplitReceiverModel); registerModel(AccountMetadataEmittedEventModel); } diff --git a/src/eventHandlers/OwnerUpdateRequestedEventHandler.ts b/src/eventHandlers/OwnerUpdateRequestedEventHandler.ts deleted file mode 100644 index 7b4390f..0000000 --- a/src/eventHandlers/OwnerUpdateRequestedEventHandler.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { OwnerUpdateRequestedEvent as CurrentNetworkOwnerUpdateRequestedEvent } from '../../contracts/CURRENT_NETWORK/RepoDriver'; -import type { OwnerUpdateRequestedEvent as FilecoinOwnerUpdateRequestedEvent } from '../../contracts/filecoin/RepoDriver'; -import OwnerUpdateRequestedEventModel from '../models/OwnerUpdateRequestedEventModel'; -import EventHandlerBase from '../events/EventHandlerBase'; -import { ProjectModel } from '../models'; -import LogManager from '../core/LogManager'; -import { convertToRepoDriverId } from '../utils/accountIdUtils'; -import { isLatestEvent } from '../utils/isLatestEvent'; -import type EventHandlerRequest from '../events/EventHandlerRequest'; -import { dbConnection } from '../db/database'; -import { singleOrDefault } from '../utils/linq'; -import { - toForge, - toReadable, - toUrl, - calculateProjectStatus, -} from '../utils/projectUtils'; -import RecoverableError from '../utils/recoverableError'; - -export default class OwnerUpdateRequestedEventHandler extends EventHandlerBase< - | 'OwnerUpdateRequested(uint256,uint8,bytes,address)' - | 'OwnerUpdateRequested(uint256,uint8,bytes)' -> { - public readonly eventSignatures = [ - 'OwnerUpdateRequested(uint256,uint8,bytes,address)' as const, - 'OwnerUpdateRequested(uint256,uint8,bytes)' as const, - ]; - - protected async _handle({ - id: requestId, - event: { - args, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - eventSignature, - }, - }: EventHandlerRequest< - | 'OwnerUpdateRequested(uint256,uint8,bytes,address)' - | 'OwnerUpdateRequested(uint256,uint8,bytes)' - >): Promise { - const [accountId, forge, name] = args as - | CurrentNetworkOwnerUpdateRequestedEvent.OutputTuple - | FilecoinOwnerUpdateRequestedEvent.OutputTuple; - - const forgeAsString = toForge(forge); - const decodedName = toReadable(name); - - LogManager.logRequestInfo( - [ - `📥 ${this.name} is processing the following ${eventSignature}:`, - ` - forge: ${forgeAsString}`, - ` - name: ${decodedName}`, - ` - accountId: ${accountId}`, - ` - logIndex: ${logIndex}`, - ` - blockNumber: ${blockNumber}`, - ` - txHash: ${transactionHash}`, - ].join('\n'), - requestId, - ); - - await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - - const projectId = convertToRepoDriverId(accountId); - - const ownerUpdateRequestedEvent = - await OwnerUpdateRequestedEventModel.create( - { - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - name: decodedName, - forge: forgeAsString, - accountId: projectId, - }, - { transaction }, - ); - - logManager.appendFindOrCreateLog( - OwnerUpdateRequestedEventModel, - true, - `${ownerUpdateRequestedEvent.transactionHash}-${ownerUpdateRequestedEvent.logIndex}`, - ); - - if ( - !(await isLatestEvent( - ownerUpdateRequestedEvent, - OwnerUpdateRequestedEventModel, - { - accountId, - }, - transaction, - )) - ) { - logManager.logAllInfo(this.name); - - return; - } - - const project = await ProjectModel.findByPk(projectId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - if (!project) { - throw new RecoverableError( - `Project ${projectId} not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } - - project.name = decodedName; - project.forge = forgeAsString; - project.url = toUrl(forgeAsString, decodedName); - project.verificationStatus = calculateProjectStatus(project); - - await project.save({ transaction }); - - logManager.logAllInfo(this.name); - }); - } - - override async afterHandle(context: { - args: [accountId: bigint]; - blockTimestamp: Date; - }): Promise { - const { args, blockTimestamp } = context; - const [accountId] = args; - - const ownerAccountId = singleOrDefault( - await ProjectModel.findAll({ - where: { - id: convertToRepoDriverId(accountId), - }, - }), - )?.ownerAccountId; - - super.afterHandle({ - args: [accountId, ownerAccountId], - blockTimestamp, - }); - } -} diff --git a/src/eventHandlers/OwnerUpdatedEventHandler.ts b/src/eventHandlers/OwnerUpdatedEventHandler.ts deleted file mode 100644 index 093fcdd..0000000 --- a/src/eventHandlers/OwnerUpdatedEventHandler.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { OwnerUpdatedEvent } from '../../contracts/CURRENT_NETWORK/RepoDriver'; -import EventHandlerBase from '../events/EventHandlerBase'; -import { ProjectModel, OwnerUpdatedEventModel } from '../models'; -import LogManager from '../core/LogManager'; -import { calcAccountId, convertToRepoDriverId } from '../utils/accountIdUtils'; -import { isLatestEvent } from '../utils/isLatestEvent'; -import type EventHandlerRequest from '../events/EventHandlerRequest'; -import { dbConnection } from '../db/database'; -import { calculateProjectStatus } from '../utils/projectUtils'; -import RecoverableError from '../utils/recoverableError'; - -export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpdated(uint256,address)'> { - public readonly eventSignatures = ['OwnerUpdated(uint256,address)' as const]; - - protected async _handle({ - id: requestId, - event: { - args, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - eventSignature, - }, - }: EventHandlerRequest<'OwnerUpdated(uint256,address)'>): Promise { - const [accountId, owner] = args as OwnerUpdatedEvent.OutputTuple; - - LogManager.logRequestInfo( - [ - `📥 ${this.name} is processing the following ${eventSignature}:`, - ` - owner: ${owner}`, - ` - accountId: ${accountId}`, - ` - logIndex: ${logIndex}`, - ` - blockNumber: ${blockNumber}`, - ` - txHash: ${transactionHash}`, - ].join('\n'), - requestId, - ); - - await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - - const projectId = convertToRepoDriverId(accountId); - - const ownerUpdatedEvent = await OwnerUpdatedEventModel.create( - { - owner, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - accountId: projectId, - }, - { transaction }, - ); - - logManager.appendFindOrCreateLog( - OwnerUpdatedEventModel, - true, - `${ownerUpdatedEvent.transactionHash}-${ownerUpdatedEvent.logIndex}`, - ); - - // Only process event further if this is the latest. - if ( - !isLatestEvent( - ownerUpdatedEvent, - OwnerUpdatedEventModel, - { - accountId: projectId, - }, - transaction, - ) - ) { - logManager.logAllInfo(this.name); - - return; - } - - const project = await ProjectModel.findByPk(projectId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - if (!project) { - throw new RecoverableError( - `Project ${projectId} not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } - - project.ownerAddress = owner; - project.claimedAt = blockTimestamp; - project.ownerAccountId = await calcAccountId(owner); - project.verificationStatus = calculateProjectStatus(project); - - logManager.appendUpdateLog(project, ProjectModel, project.id); - - await project.save({ transaction }); - - logManager.logAllInfo(this.name); - }); - } - - override async afterHandle(context: { - args: [accountId: bigint, owner: string]; - blockTimestamp: Date; - }): Promise { - const { args, blockTimestamp } = context; - const [accountId, owner] = args; - - const ownerAccountId = await calcAccountId(owner); - - super.afterHandle({ - args: [accountId, ownerAccountId], - blockTimestamp, - }); - } -} diff --git a/src/eventHandlers/SplitsSetEventHandler/SplitsSetEventHandler.ts b/src/eventHandlers/SplitsSetEventHandler/SplitsSetEventHandler.ts deleted file mode 100644 index de9b554..0000000 --- a/src/eventHandlers/SplitsSetEventHandler/SplitsSetEventHandler.ts +++ /dev/null @@ -1,70 +0,0 @@ -import EventHandlerBase from '../../events/EventHandlerBase'; -import LogManager from '../../core/LogManager'; -import { convertToAccountId } from '../../utils/accountIdUtils'; -import type EventHandlerRequest from '../../events/EventHandlerRequest'; -import { SplitsSetEventModel } from '../../models'; -import { dbConnection } from '../../db/database'; -import type { SplitsSetEvent } from '../../../contracts/CURRENT_NETWORK/Drips'; -import setIsValidFlag from './setIsValidFlag'; - -export default class SplitsSetEventHandler extends EventHandlerBase<'SplitsSet(uint256,bytes32)'> { - public eventSignatures = ['SplitsSet(uint256,bytes32)' as const]; - - protected async _handle( - request: EventHandlerRequest<'SplitsSet(uint256,bytes32)'>, - ): Promise { - const { - id: requestId, - event: { args, logIndex, blockNumber, blockTimestamp, transactionHash }, - } = request; - - const [rawAccountId, rawReceiversHash] = args as SplitsSetEvent.OutputTuple; - const accountId = convertToAccountId(rawAccountId); - - LogManager.logRequestInfo( - [ - `📥 ${this.name} is processing the following ${request.event.eventSignature}:`, - ` - accountId: ${accountId}`, - ` - receiversHash: ${rawReceiversHash}`, - ` - logIndex: ${logIndex}`, - ` - txHash: ${transactionHash}`, - ].join('\n'), - requestId, - ); - - await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - - const splitsSetEvent = await SplitsSetEventModel.create( - { - accountId, - receiversHash: rawReceiversHash, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - { - transaction, - }, - ); - - logManager.appendFindOrCreateLog( - SplitsSetEventModel, - true, - `${splitsSetEvent.transactionHash}-${splitsSetEvent.logIndex}`, - ); - - // Account's splits are set from `AccountMetadataEmitted` events. - // `SplitsSet` event only validates the splits receivers. - - try { - await setIsValidFlag(splitsSetEvent, logManager, transaction); - } catch (error: any) { - logManager.logAllInfo(this.name); - - throw error; - } - }); - } -} diff --git a/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts deleted file mode 100644 index 1a75f6a..0000000 --- a/src/eventHandlers/SplitsSetEventHandler/setIsValidFlag.ts +++ /dev/null @@ -1,357 +0,0 @@ -import type { Transaction } from 'sequelize'; -import type { - ImmutableSplitsDriverId, - NftDriverId, - RepoDriverId, -} from '../../core/types'; -import type { SplitsSetEventModel } from '../../models'; -import { - AddressDriverSplitReceiverModel, - DripListModel, - DripListSplitReceiverModel, - EcosystemMainAccountModel, - ProjectModel, - RepoDriverSplitReceiverModel, - SubListModel, - SubListSplitReceiverModel, -} from '../../models'; -import { - isImmutableSplitsDriverId, - isNftDriverId, - isRepoDriverId, -} from '../../utils/accountIdUtils'; -import type { SplitsReceiverStruct } from '../../../contracts/CURRENT_NETWORK/Drips'; -import unreachableError from '../../utils/unreachableError'; -import { dripsContract } from '../../core/contractClients'; -import type LogManager from '../../core/LogManager'; -import { formatSplitReceivers } from '../../utils/formatSplitReceivers'; -import RecoverableError from '../../utils/recoverableError'; - -export default async function setIsValidFlag( - splitsSetEvent: SplitsSetEventModel, - logManager: LogManager, - transaction: Transaction, -): Promise { - const { accountId, receiversHash } = splitsSetEvent; - const onChainSplitsHash = await dripsContract.splitsHash(accountId); - - // Only try to set the 'isValid' flag if this is the latest event. - if (receiversHash !== onChainSplitsHash) { - return; - } - - if (isRepoDriverId(accountId)) { - const project = await ProjectModel.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - if (!project) { - throw new RecoverableError( - `Failed to set 'isValid' flag for Project: Project '${accountId}' not found.`, - ); - } - - const storedInDbFromMetaReceiversHash = await dripsContract.hashSplits( - formatSplitReceivers(await getProjectDbReceivers(accountId, transaction)), - ); - - if (receiversHash !== storedInDbFromMetaReceiversHash) { - project.isValid = false; - - logManager.appendUpdateLog(project, ProjectModel, project.id); - - await project.save({ transaction }); - - throw new RecoverableError( - `Failed to set 'isValid' for Project '${accountId}': mismatch between on-chain, event, and DB splits receiver hashes (on-chain: ${onChainSplitsHash}, event: ${receiversHash}, db: ${storedInDbFromMetaReceiversHash}).`, - ); - } else if (project.isValid === false) { - project.isValid = true; - - logManager.appendUpdateLog(project, ProjectModel, project.id); - - await project.save({ transaction }); - } - } else if (isNftDriverId(accountId)) { - const dripList = await DripListModel.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - const ecosystem = await EcosystemMainAccountModel.findByPk(accountId, { - lock: transaction.LOCK.UPDATE, - transaction, - }); - - const entity = (dripList ?? ecosystem)!; - const entityModel = dripList ? DripListModel : EcosystemMainAccountModel; - - if (!dripList && !ecosystem) { - throw new RecoverableError( - `Failed to set 'isValid' flag for ${entityModel.name}: ${entityModel.name} '${accountId}' not found.`, - ); - } - - const storedInDbFromMetaReceiversHash = await dripsContract.hashSplits( - formatSplitReceivers( - entityModel.name === 'DripListModel' - ? await getDripListDbReceivers(accountId, transaction) - : await getEcosystemDbReceivers(accountId, transaction), - ), - ); - - if (receiversHash !== storedInDbFromMetaReceiversHash) { - entity.isValid = false; - - logManager.appendUpdateLog(entity, entityModel, entity.id); - - await entity.save({ transaction }); - - throw new RecoverableError( - `Failed to set 'isValid' for ${dripList ? 'Drip List' : 'Ecosystem Main Identity'} '${accountId}': mismatch between on-chain, event, and DB splits receiver hashes (on-chain: ${onChainSplitsHash}, event: ${receiversHash}, db: ${storedInDbFromMetaReceiversHash}).`, - ); - } else if (entity.isValid === false) { - entity.isValid = true; - - logManager.appendUpdateLog(entity, entityModel, entity.id); - - await entity.save({ transaction }); - } - } else if (isImmutableSplitsDriverId(accountId)) { - const subList = await SubListModel.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - if (!subList) { - throw new RecoverableError( - `Failed to set 'isValid' flag for Sub List: Sub List '${accountId}' not found.`, - ); - } - - const storedInDbFromMetaReceiversHash = await dripsContract.hashSplits( - formatSplitReceivers(await getSubListDbReceivers(accountId, transaction)), - ); - - // If we reach this point, it means that `receiversHash` is the latest on-chain hash. - - if (receiversHash !== storedInDbFromMetaReceiversHash) { - subList.isValid = false; - - logManager.appendUpdateLog(subList, SubListModel, subList.id); - - await subList.save({ transaction }); - - throw new RecoverableError( - `Failed to set 'isValid' for Sub List '${accountId}': mismatch between on-chain, event, and DB splits receiver hashes (on-chain: ${onChainSplitsHash}, event: ${receiversHash}, db: ${storedInDbFromMetaReceiversHash}).`, - ); - } else if (subList.isValid === false) { - subList.isValid = true; - - logManager.appendUpdateLog(subList, SubListModel, subList.id); - - await subList.save({ transaction }); - } - } -} - -async function getProjectDbReceivers( - accountId: RepoDriverId, - transaction: Transaction, -) { - const addressReceivers: SplitsReceiverStruct[] = - await AddressDriverSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderProjectId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeAccountId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - const projectReceivers: SplitsReceiverStruct[] = - await RepoDriverSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderProjectId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeProjectId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - const dripListReceivers: SplitsReceiverStruct[] = - await DripListSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderProjectId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeDripListId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - return [...addressReceivers, ...projectReceivers, ...dripListReceivers]; -} - -async function getDripListDbReceivers( - accountId: NftDriverId, - transaction: Transaction, -) { - const addressReceivers: SplitsReceiverStruct[] = - await AddressDriverSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderDripListId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeAccountId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - const projectReceivers: SplitsReceiverStruct[] = - await RepoDriverSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderDripListId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeProjectId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - const dripListReceivers: SplitsReceiverStruct[] = - await DripListSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderDripListId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeDripListId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - return [...addressReceivers, ...projectReceivers, ...dripListReceivers]; -} - -async function getEcosystemDbReceivers( - accountId: NftDriverId, - transaction: Transaction, -) { - const projectReceivers: SplitsReceiverStruct[] = - await RepoDriverSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderEcosystemMainAccountId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeProjectId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - const subListReceivers: SplitsReceiverStruct[] = - await SubListSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderEcosystemMainAccountId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeSubListId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - return [...projectReceivers, ...subListReceivers]; -} - -async function getSubListDbReceivers( - accountId: ImmutableSplitsDriverId, - transaction: Transaction, -) { - const addressReceivers: SplitsReceiverStruct[] = - await AddressDriverSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderSubListId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeAccountId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - const projectReceivers: SplitsReceiverStruct[] = - await RepoDriverSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderSubListId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeProjectId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - const dripListReceivers: SplitsReceiverStruct[] = - await DripListSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderSubListId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeDripListId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - const subListReceivers: SplitsReceiverStruct[] = - await SubListSplitReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - funderSubListId: accountId, - }, - }).then((receivers) => - receivers.map((receiver) => ({ - accountId: receiver.fundeeSubListId ?? unreachableError(), - weight: receiver.weight, - })), - ); - - return [ - ...projectReceivers, - ...subListReceivers, - ...dripListReceivers, - ...addressReceivers, - ]; -} diff --git a/src/eventHandlers/index.ts b/src/eventHandlers/index.ts index 24cd1f3..e2f5eff 100644 --- a/src/eventHandlers/index.ts +++ b/src/eventHandlers/index.ts @@ -2,9 +2,6 @@ export { default as GivenEventHandler } from './GivenEventHandler'; export { default as SplitEventHandler } from './SplitEventHandler'; export { default as TransferEventHandler } from './TransferEventHandler'; export { default as StreamsSetEventHandler } from './StreamsSetEventHandler'; -export { default as OwnerUpdatedEventHandler } from './OwnerUpdatedEventHandler'; export { default as SqueezedStreamsEventHandler } from './SqueezedStreamsEventHandler'; export { default as StreamReceiverSeenEventHandler } from './StreamReceiverSeenEventHandler'; -export { default as SplitsSetEventHandler } from './SplitsSetEventHandler/SplitsSetEventHandler'; -export { default as OwnerUpdateRequestedEventHandler } from './OwnerUpdateRequestedEventHandler'; export { default as AccountMetadataEmittedEventHandler } from './AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler'; diff --git a/src/models/OwnerUpdatedEventModel.ts b/src/models/OwnerUpdatedEventModel.ts deleted file mode 100644 index 905e773..0000000 --- a/src/models/OwnerUpdatedEventModel.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { - InferAttributes, - InferCreationAttributes, - Sequelize, -} from 'sequelize'; -import { DataTypes, Model } from 'sequelize'; -import type { AddressLike } from 'ethers'; -import { COMMON_EVENT_INIT_ATTRIBUTES } from '../core/constants'; -import type { RepoDriverId } from '../core/types'; -import getSchema from '../utils/getSchema'; -import type { IEventModel } from '../events/types'; - -export default class OwnerUpdatedEventModel - extends Model< - InferAttributes, - InferCreationAttributes - > - implements IEventModel -{ - // Properties from event output. - public declare owner: AddressLike; - public declare accountId: RepoDriverId; - - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; - - public static initialize(sequelize: Sequelize): void { - this.init( - { - owner: { - type: DataTypes.STRING, - allowNull: false, - }, - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - { - sequelize, - schema: getSchema(), - tableName: 'OwnerUpdatedEvents', - }, - ); - } -} diff --git a/src/models/SplitsSetEventModel.ts b/src/models/SplitsSetEventModel.ts deleted file mode 100644 index 2b0e716..0000000 --- a/src/models/SplitsSetEventModel.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { - InferAttributes, - InferCreationAttributes, - Sequelize, -} from 'sequelize'; -import { DataTypes, Model } from 'sequelize'; -import type { AccountId } from '../core/types'; -import getSchema from '../utils/getSchema'; -import { COMMON_EVENT_INIT_ATTRIBUTES } from '../core/constants'; -import type { IEventModel } from '../events/types'; - -export default class SplitsSetEventModel - extends Model< - InferAttributes, - InferCreationAttributes - > - implements IEventModel -{ - public declare accountId: AccountId; - public declare receiversHash: string; - - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; - - public static initialize(sequelize: Sequelize): void { - this.init( - { - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - receiversHash: { - type: DataTypes.STRING, - allowNull: false, - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - { - sequelize, - schema: getSchema(), - tableName: 'SplitsSetEvents', - }, - ); - } -} diff --git a/src/models/index.ts b/src/models/index.ts index 10f967d..42c4b5d 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -2,18 +2,15 @@ export { default as SubListModel } from './SubListModel'; export { default as DripListModel } from './DripListModel'; export { default as GivenEventModel } from './GivenEventModel'; export { default as SplitEventModel } from './SplitEventModel'; -export { default as ProjectModel } from './ProjectModel'; +export { default as Project } from './Project'; export { default as EcosystemMainAccountModel } from './EcosystemMainAccountModel'; export { default as TransferEventModel } from './TransferEventModel'; -export { default as SplitsSetEventModel } from './SplitsSetEventModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; export { default as _LastIndexedBlockModel } from './_LastIndexedBlockModel'; -export { default as OwnerUpdatedEventModel } from './OwnerUpdatedEventModel'; export { default as SqueezedStreamsEventModel } from './SqueezedStreamsEventModel'; export { default as SubListSplitReceiverModel } from './SubListSplitReceiverModel'; export { default as DripListSplitReceiverModel } from './DripListSplitReceiverModel'; export { default as RepoDriverSplitReceiverModel } from './RepoDriverSplitReceiverModel'; export { default as StreamReceiverSeenEventModel } from './StreamReceiverSeenEventModel'; -export { default as OwnerUpdateRequestedEventModel } from './OwnerUpdateRequestedEventModel'; export { default as AddressDriverSplitReceiverModel } from './AddressDriverSplitReceiverModel'; export { default as AccountMetadataEmittedEventModel } from './AccountMetadataEmittedEventModel'; diff --git a/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts b/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts deleted file mode 100644 index d4e06e9..0000000 --- a/tests/eventHandlers/OwnerUpdateRequestedEventHandler.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* eslint-disable dot-notation */ -import { randomUUID } from 'crypto'; -import { ethers } from 'ethers'; -import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; -import { OwnerUpdateRequestedEventHandler } from '../../src/eventHandlers'; -import { dbConnection } from '../../src/db/database'; -import type { EventData } from '../../src/events/types'; -import { convertToRepoDriverId } from '../../src/utils/accountIdUtils'; -import { - calculateProjectStatus, - toForge, - toReadable, - toUrl, -} from '../../src/utils/projectUtils'; -import OwnerUpdateRequestedEventModel from '../../src/models/OwnerUpdateRequestedEventModel'; -import ProjectModel, { - ProjectVerificationStatus, -} from '../../src/models/ProjectModel'; -import LogManager from '../../src/core/LogManager'; -import { isLatestEvent } from '../../src/utils/isLatestEvent'; - -jest.mock('../../src/models/OwnerUpdateRequestedEventModel'); -jest.mock('../../src/models/ProjectModel'); -jest.mock('../../src/db/database'); -jest.mock('bee-queue'); -jest.mock('../../src/events/eventHandlerUtils'); -jest.mock('../../src/core/LogManager'); -jest.mock('../../src/utils/isLatestEvent'); - -describe('OwnerUpdateRequestedEventHandler', () => { - let mockDbTransaction: {}; - let handler: OwnerUpdateRequestedEventHandler; - let mockRequest: EventHandlerRequest<'OwnerUpdateRequested(uint256,uint8,bytes)'>; - - beforeAll(() => { - jest.clearAllMocks(); - - handler = new OwnerUpdateRequestedEventHandler(); - - mockRequest = { - id: randomUUID(), - event: { - args: [ - 80920745289880686872077472087501508459438916877610571750365932290048n, - 0n, - ethers.encodeBytes32String('name'), - ], - logIndex: 1, - blockNumber: 1, - blockTimestamp: new Date(), - transactionHash: 'requestTransactionHash', - } as EventData<'OwnerUpdateRequested(uint256,uint8,bytes)'>, - }; - - mockDbTransaction = { - LOCK: { - UPDATE: 'UPDATE', - }, - }; - - dbConnection.transaction = jest - .fn() - .mockImplementation((callback) => callback(mockDbTransaction)); - }); - - describe('_handle', () => { - test('should create new OwnerUpdateRequestedEventModel', async () => { - // Arrange - OwnerUpdateRequestedEventModel.create = jest.fn().mockResolvedValue([ - { - transactionHash: 'OwnerUpdateRequestedEventTransactionHash', - logIndex: 1, - }, - ]); - - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); - - // Act - await handler['_handle'](mockRequest); - - // Assert - const { - event: { - args: [accountId, forge, name], - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - } = mockRequest; - - expect(OwnerUpdateRequestedEventModel.create).toHaveBeenCalledWith( - { - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - name: toReadable(name), - forge: toForge(forge), - accountId: convertToRepoDriverId(accountId), - }, - { - transaction: mockDbTransaction, - }, - ); - }); - - test('should update the ProjectModel when the incoming event is the latest', async () => { - // Arrange - OwnerUpdateRequestedEventModel.findOrCreate = jest - .fn() - .mockResolvedValue([ - { - transactionHash: 'OwnerUpdateRequestedEventTransactionHash', - logIndex: 1, - }, - true, - ]); - - const mockGitProject = { - name: '', - forge: '', - url: '', - verificationStatus: ProjectVerificationStatus.Unclaimed, - save: jest.fn(), - }; - ProjectModel.findByPk = jest.fn().mockResolvedValue(mockGitProject); - - (isLatestEvent as jest.Mock).mockResolvedValue(true); - - // Act - await handler['_handle'](mockRequest); - - // Assert - const { - event: { - // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars - args: [_, forge, name], - }, - } = mockRequest; - - expect(mockGitProject.name).toBe(toReadable(name)); - expect(mockGitProject.forge).toBe(toForge(forge)); - expect(mockGitProject.url).toBe(toUrl(toForge(forge), toReadable(name))); - expect(mockGitProject.verificationStatus).toBe( - calculateProjectStatus(mockGitProject as any), - ); - expect(mockGitProject.save).toHaveBeenCalledWith({ - transaction: mockDbTransaction, - }); - }); - }); -}); diff --git a/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts b/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts deleted file mode 100644 index 728316d..0000000 --- a/tests/eventHandlers/OwnerUpdatedEventHandler.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* eslint-disable dot-notation */ -import { randomUUID } from 'crypto'; -import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; -import { OwnerUpdatedEventHandler } from '../../src/eventHandlers'; -import { dbConnection } from '../../src/db/database'; -import type { EventData } from '../../src/events/types'; -import { - calcAccountId, - convertToRepoDriverId, -} from '../../src/utils/accountIdUtils'; -import { calculateProjectStatus } from '../../src/utils/projectUtils'; -import OwnerUpdatedEventModel from '../../src/models/OwnerUpdatedEventModel'; -import ProjectModel, { - ProjectVerificationStatus, -} from '../../src/models/ProjectModel'; -import LogManager from '../../src/core/LogManager'; -import { isLatestEvent } from '../../src/utils/isLatestEvent'; - -jest.mock('../../src/models/OwnerUpdatedEventModel'); -jest.mock('../../src/models/ProjectModel'); -jest.mock('../../src/db/database'); -jest.mock('bee-queue'); -jest.mock('../../src/events/eventHandlerUtils'); -jest.mock('../../src/core/LogManager'); -jest.mock('../../src/utils/accountIdUtils'); -jest.mock('../../src/utils/isLatestEvent'); - -describe('OwnerUpdatedEventHandler', () => { - let mockDbTransaction: {}; - let handler: OwnerUpdatedEventHandler; - let mockRequest: EventHandlerRequest<'OwnerUpdated(uint256,address)'>; - - beforeAll(() => { - jest.clearAllMocks(); - - handler = new OwnerUpdatedEventHandler(); - - mockRequest = { - id: randomUUID(), - event: { - args: [ - 80920745289880686872077472087501508459438916877610571750365932290048n, - '0x20a9273a452268E5a034951ae5381a45E14aC2a3', - ], - logIndex: 1, - blockNumber: 1, - blockTimestamp: new Date(), - transactionHash: 'requestTransactionHash', - } as EventData<'OwnerUpdated(uint256,address)'>, - }; - - mockDbTransaction = { - LOCK: { - UPDATE: 'UPDATE', - }, - }; - - dbConnection.transaction = jest - .fn() - .mockImplementation((callback) => callback(mockDbTransaction)); - }); - - describe('_handle', () => { - test('should create new OwnerUpdatedEventModel', async () => { - // Arrange - OwnerUpdatedEventModel.create = jest.fn().mockResolvedValue([ - { - transactionHash: 'OwnerUpdatedEventTransactionHash', - logIndex: 1, - }, - true, - ]); - - (convertToRepoDriverId as jest.Mock).mockReturnValue('1'); - - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); - - (calcAccountId as jest.Mock).mockResolvedValue('ownerAccountId'); - - // Act - await handler['_handle'](mockRequest); - - // Assert - const { - event: { - args: [accountId, owner], - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - } = mockRequest; - - expect(OwnerUpdatedEventModel.create).toHaveBeenCalledWith( - { - owner, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - accountId: convertToRepoDriverId(accountId), - }, - { - transaction: mockDbTransaction, - }, - ); - }); - - test('should update the ProjectModel when the incoming event is the latest', async () => { - // Arrange - OwnerUpdatedEventModel.create = jest.fn().mockResolvedValue([ - { - transactionHash: 'OwnerUpdatedEventTransactionHash', - logIndex: 1, - }, - ]); - - const mockGitProject = { - ownerAddress: '', - ownerAccountId: '', - verificationStatus: ProjectVerificationStatus.Unclaimed, - save: jest.fn(), - }; - ProjectModel.findByPk = jest.fn().mockResolvedValue(mockGitProject); - - (isLatestEvent as jest.Mock).mockResolvedValue(true); - - (calcAccountId as jest.Mock).mockResolvedValue('ownerAccountId'); - - // Act - await handler['_handle'](mockRequest); - - // Assert - const { - event: { - // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars - args: [accountId, owner], - }, - } = mockRequest; - - expect(mockGitProject.ownerAddress).toBe(owner); - expect(mockGitProject.ownerAccountId).toBe(await calcAccountId(owner)); - expect(mockGitProject.verificationStatus).toBe( - calculateProjectStatus(mockGitProject as any), - ); - expect(mockGitProject.save).toHaveBeenCalledWith({ - transaction: mockDbTransaction, - }); - }); - }); -}); diff --git a/tests/eventHandlers/SplitsSetEventHandler.test.ts b/tests/eventHandlers/SplitsSetEventHandler.test.ts deleted file mode 100644 index 7c8bd04..0000000 --- a/tests/eventHandlers/SplitsSetEventHandler.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-disable dot-notation */ -import { randomUUID } from 'crypto'; -import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; -import { dbConnection } from '../../src/db/database'; -import type { EventData } from '../../src/events/types'; -import SplitsSetEventModel from '../../src/models/SplitsSetEventModel'; -import LogManager from '../../src/core/LogManager'; -import { convertToAccountId } from '../../src/utils/accountIdUtils'; -import { SplitsSetEventHandler } from '../../src/eventHandlers'; -import setIsValidFlag from '../../src/eventHandlers/SplitsSetEventHandler/setIsValidFlag'; - -jest.mock('../../src/models/SplitsSetEventModel'); -jest.mock('../../src/db/database'); -jest.mock('bee-queue'); -jest.mock('../../src/core/LogManager'); -jest.mock('../../src/eventHandlers/SplitsSetEventHandler/setIsValidFlag'); - -describe('SplitsSetEventHandler', () => { - let mockDbTransaction: {}; - let handler: SplitsSetEventHandler; - let mockRequest: EventHandlerRequest<'SplitsSet(uint256,bytes32)'>; - - beforeAll(() => { - jest.clearAllMocks(); - - handler = new SplitsSetEventHandler(); - - mockRequest = { - id: randomUUID(), - event: { - args: [ - 80920745289880686872077472087501508459438916877610571750365932290048n, - 'receiversHash', - ], - logIndex: 1, - blockNumber: 1, - blockTimestamp: new Date(), - transactionHash: 'requestTransactionHash', - } as EventData<'SplitsSet(uint256,bytes32)'>, - }; - - mockDbTransaction = {}; - - dbConnection.transaction = jest - .fn() - .mockImplementation((callback) => callback(mockDbTransaction)); - }); - - describe('_handle', () => { - test('should create a new SplitsSetEventModel', async () => { - // Arrange - SplitsSetEventModel.create = jest.fn().mockResolvedValue([ - { - transactionHash: 'SplitsSetTransactionHash', - logIndex: 1, - }, - ]); - - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); - - // Act - await handler['_handle'](mockRequest); - - // Assert - const { - event: { - args: [rawAccountId, rawReceiversHash], - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - } = mockRequest; - - expect(SplitsSetEventModel.create).toHaveBeenCalledWith( - { - accountId: convertToAccountId(rawAccountId), - receiversHash: rawReceiversHash, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - { - transaction: mockDbTransaction, - }, - ); - - expect(setIsValidFlag).toHaveBeenCalled(); - }); - }); -}); From eb66d6298463e91da8063584d26567903c2c967a Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 17 Apr 2025 11:59:20 +0200 Subject: [PATCH 26/60] feat: remove specific receiver types and add one generic splitsreceiver --- src/core/LogManager.ts | 39 ++ src/core/constants.ts | 21 +- src/core/splitRules.ts | 148 +++++ src/core/types.ts | 23 +- src/db/database.ts | 178 +----- .../20250414133746-initial_create.ts | 511 +++++++++++++++++- src/db/modelRegistration.ts | 14 +- .../AccountMetadataEmittedEventHandler.ts | 20 +- .../buildFunderAccountFields.ts | 65 --- .../handlers/handleDripListMetadata.ts | 230 ++++---- .../handleEcosystemMainAccountMetadata.ts | 198 ++++--- .../handlers/handleProjectMetadata.ts | 248 ++++----- .../handlers/handleSubListMetadata.ts | 208 ++++--- .../projectVerification.ts | 87 --- .../receiversRepository.ts | 348 +----------- .../verifySplitsReceivers.ts | 35 +- .../StreamReceiverSeenEventHandler.ts | 4 +- src/eventHandlers/TransferEventHandler.ts | 16 +- src/events/registrations.ts | 22 - .../schemas/immutable-splits-driver/v1.ts | 8 +- .../AccountMetadataEmittedEventModel.ts | 29 +- src/models/AddressDriverSplitReceiverModel.ts | 141 ----- src/models/DripListModel.ts | 66 ++- src/models/DripListSplitReceiverModel.ts | 139 ----- src/models/EcosystemMainAccountModel.ts | 62 ++- src/models/GivenEventModel.ts | 18 +- src/models/OwnerUpdateRequestedEventModel.ts | 54 -- src/models/ProjectModel.ts | 157 +++--- src/models/RepoDriverSplitReceiverModel.ts | 139 ----- src/models/SplitReceiver.ts | 95 ++++ src/models/SubListModel.ts | 107 ++-- src/models/SubListSplitReceiverModel.ts | 139 ----- src/models/TransferEventModel.ts | 27 +- src/models/_LastIndexedBlockModel.ts | 23 +- src/models/index.ts | 9 +- src/utils/accountIdUtils.ts | 108 +++- src/utils/contractUtils.ts | 32 -- src/utils/metadataUtils.ts | 4 +- src/utils/projectUtils.ts | 103 ++-- src/utils/retryOperation.ts | 58 -- ...AccountMetadataEmittedEventHandler.test.ts | 4 +- .../TransferEventHandler.test.ts | 2 +- 42 files changed, 1740 insertions(+), 2199 deletions(-) create mode 100644 src/core/splitRules.ts delete mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/buildFunderAccountFields.ts delete mode 100644 src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts delete mode 100644 src/models/AddressDriverSplitReceiverModel.ts delete mode 100644 src/models/DripListSplitReceiverModel.ts delete mode 100644 src/models/OwnerUpdateRequestedEventModel.ts delete mode 100644 src/models/RepoDriverSplitReceiverModel.ts create mode 100644 src/models/SplitReceiver.ts delete mode 100644 src/models/SubListSplitReceiverModel.ts delete mode 100644 src/utils/contractUtils.ts delete mode 100644 src/utils/retryOperation.ts diff --git a/src/core/LogManager.ts b/src/core/LogManager.ts index cd14632..1c3eba6 100644 --- a/src/core/LogManager.ts +++ b/src/core/LogManager.ts @@ -59,6 +59,45 @@ export default class LogManager { return this; } + public appendCreateLog( + type: { new (): T }, + id: string, + ): this { + this._logs.push( + `Created a new ${LogManager.nameOfType(type)} with ID ${id}.`, + ); + + return this; + } + + public appendUpsertLog( + instance: T, + type: { new (): T }, + id: string, + wasCreated: boolean, + ): this { + const action = wasCreated ? 'Created new' : 'Upserted existing'; + const baseMessage = `${action} ${LogManager.nameOfType(type)} with ID ${id}.`; + + if (wasCreated) { + this._logs.push(baseMessage); + } else { + const changes = LogManager.getChangedProperties(instance); + + const formattedChanges = + changes && Object.keys(changes).length > 0 + ? `\n\tChanged properties:\n${JSON.stringify(changes, null, 2) + .split('\n') + .map((line) => `\t ${line}`) + .join('\n')}` + : `\n\tNo changes detected.`; + + this._logs.push(`${baseMessage}${formattedChanges}`); + } + + return this; + } + public appendLog(log: string): this { this._logs.push(log); diff --git a/src/core/constants.ts b/src/core/constants.ts index c66ae43..cdf613f 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -1,4 +1,4 @@ -import { ethers } from 'ethers'; +import { encodeBytes32String } from 'ethers'; import { DataTypes } from 'sequelize'; export const SUPPORTED_NETWORKS = [ @@ -12,11 +12,6 @@ export const SUPPORTED_NETWORKS = [ 'optimism', ] as const; -export const FORGES_MAP = { - 0: 'GitHub', - 1: 'GitLab', -} as const; - export const DRIPS_CONTRACTS = [ 'drips', 'nftDriver', @@ -25,25 +20,25 @@ export const DRIPS_CONTRACTS = [ 'immutableSplitsDriver', ] as const; -export const DRIPS_APP_USER_METADATA_KEY = ethers.encodeBytes32String('ipfs'); +export const DRIPS_APP_USER_METADATA_KEY = encodeBytes32String('ipfs'); export const COMMON_EVENT_INIT_ATTRIBUTES = { transactionHash: { - type: DataTypes.STRING, - allowNull: false, primaryKey: true, + allowNull: false, + type: DataTypes.STRING, }, logIndex: { - type: DataTypes.INTEGER, - allowNull: false, primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, }, blockTimestamp: { - type: DataTypes.DATE, allowNull: false, + type: DataTypes.DATE, }, blockNumber: { - type: DataTypes.INTEGER, allowNull: false, + type: DataTypes.INTEGER, }, } as const; diff --git a/src/core/splitRules.ts b/src/core/splitRules.ts new file mode 100644 index 0000000..f5562eb --- /dev/null +++ b/src/core/splitRules.ts @@ -0,0 +1,148 @@ +import type { + RepoDriverId, + NftDriverId, + ImmutableSplitsDriverId, + AddressDriverId, + RepoDeadlineDriverId, +} from './types'; + +const SPLIT_RULES = Object.freeze([ + // Project Rules + { + senderAccountType: 'project', + receiverAccountType: 'address', + relationshipType: 'project_maintainer', + }, + { + senderAccountType: 'project', + receiverAccountType: 'project', + relationshipType: 'project_dependency', + }, + { + senderAccountType: 'project', + receiverAccountType: 'address', + relationshipType: 'project_dependency', + }, + { + senderAccountType: 'project', + receiverAccountType: 'drip_list', + relationshipType: 'project_dependency', + }, + + // Drip List Rules + { + senderAccountType: 'drip_list', + receiverAccountType: 'address', + relationshipType: 'drip_list_receiver', + }, + { + senderAccountType: 'drip_list', + receiverAccountType: 'drip_list', + relationshipType: 'drip_list_receiver', + }, + { + senderAccountType: 'drip_list', + receiverAccountType: 'project', + relationshipType: 'drip_list_receiver', + }, + + // Ecosystem Main Account Rules + { + senderAccountType: 'ecosystem_main_account', + receiverAccountType: 'project', + relationshipType: 'ecosystem_receiver', + }, + { + senderAccountType: 'ecosystem_main_account', + receiverAccountType: 'sub_list', + relationshipType: 'sub_list_link', + }, + + // Sub List Rules + { + senderAccountType: 'sub_list', + receiverAccountType: 'address', + relationshipType: 'sub_list_receiver', + }, + { + senderAccountType: 'sub_list', + receiverAccountType: 'drip_list', + relationshipType: 'sub_list_receiver', + }, + { + senderAccountType: 'sub_list', + receiverAccountType: 'project', + relationshipType: 'sub_list_receiver', + }, + { + senderAccountType: 'sub_list', + receiverAccountType: 'sub_list', + relationshipType: 'sub_list_receiver', + }, +] as const); + +type EntityIdMap = { + project: RepoDriverId; + drip_list: NftDriverId; + ecosystem_main_account: NftDriverId; + sub_list: ImmutableSplitsDriverId; + deadline: RepoDeadlineDriverId; + address: AddressDriverId; +}; + +export type RelationshipType = (typeof SPLIT_RULES)[number]['relationshipType']; +export type AccountType = + | (typeof SPLIT_RULES)[number]['senderAccountType'] + | (typeof SPLIT_RULES)[number]['receiverAccountType']; + +type SplitRuleFromRaw = R extends { + senderAccountType: infer S extends keyof EntityIdMap; + receiverAccountType: infer T extends keyof EntityIdMap; + relationshipType: string; +} + ? { + senderAccountType: S; + senderAccountId: EntityIdMap[S]; + receiverAccountType: T; + receiverAccountId: EntityIdMap[T]; + relationshipType: R['relationshipType']; + } + : never; + +export type SplitReceiverShape = SplitRuleFromRaw< + (typeof SPLIT_RULES)[number] +> & { + weight: number; + blockTimestamp: Date; +}; + +export const ACCOUNT_TYPES = Array.from( + new Set( + SPLIT_RULES.flatMap((rule) => [ + rule.senderAccountType, + rule.receiverAccountType, + ]), + ), +); + +export const RELATIONSHIP_TYPES = Array.from( + new Set(SPLIT_RULES.map((rule) => rule.relationshipType)), +) as (typeof SPLIT_RULES)[number]['relationshipType'][]; + +export const ACCOUNT_TYPE_TO_METADATA_RECEIVER_TYPE: Record< + AccountType, + string +> = { + project: 'repoDriver', + drip_list: 'dripList', + ecosystem_main_account: 'ecosystem', + sub_list: 'subList', + address: 'address', +}; + +export const METADATA_RECEIVER_TYPE_TO_ACCOUNT_TYPE = Object.fromEntries( + Object.entries(ACCOUNT_TYPE_TO_METADATA_RECEIVER_TYPE).map(([key, value]) => [ + value, + key, + ]), +) as Record; diff --git a/src/core/types.ts b/src/core/types.ts index f143b69..c8a3979 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,9 +1,5 @@ import type { Model, Sequelize } from 'sequelize'; -import type { - DRIPS_CONTRACTS, - FORGES_MAP, - SUPPORTED_NETWORKS, -} from './constants'; +import type { DRIPS_CONTRACTS, SUPPORTED_NETWORKS } from './constants'; export type KnownAny = any; @@ -12,17 +8,20 @@ export type IpfsHash = string & { __brand: 'IpfsHash' }; export type AddressDriverId = string & { __brand: 'AddressDriverId'; }; - export type NftDriverId = string & { __brand: 'NftDriverId' }; export type RepoDriverId = string & { __brand: 'RepoDriverId' }; export type ImmutableSplitsDriverId = string & { __brand: 'ImmutableSplitsDriverId'; }; +export type RepoDeadlineDriverId = string & { + __brand: 'RepoDeadlineDriverId'; +}; export type AccountId = | AddressDriverId | NftDriverId | RepoDriverId - | ImmutableSplitsDriverId; + | ImmutableSplitsDriverId + | RepoDeadlineDriverId; export type Address = string & { __brand: 'Address' }; @@ -44,8 +43,6 @@ export type SupportedNetwork = (typeof SUPPORTED_NETWORKS)[number]; export type DbSchema = SupportedNetwork & { __brand: 'DbSchema' }; -export type Forge = ValuesOf; - export type DripsContract = (typeof DRIPS_CONTRACTS)[number]; export type ChainConfig = { @@ -60,15 +57,9 @@ export type ChainConfig = { export type ModelStaticMembers = { new (): Model; initialize(sequelize: Sequelize): void; + defineAssociations?(): void; }; -// TODO: Remove this. There is no need to have this in the database. -export enum DependencyType { - ProjectDependency = 'ProjectDependency', - DripListDependency = 'DripListDependency', - EcosystemDependency = 'EcosystemDependency', -} - export type StreamHistoryHashes = string & { __type: 'StreamHistoryHashes'; }; diff --git a/src/db/database.ts b/src/db/database.ts index 0cb727a..d630904 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -1,17 +1,7 @@ import { Sequelize } from 'sequelize'; import logger from '../core/logger'; -import { getRegisteredModels, registerModels } from './modelRegistration'; -import { - AddressDriverSplitReceiverModel, - DripListModel, - DripListSplitReceiverModel, - ProjectModel, - EcosystemMainAccountModel, - RepoDriverSplitReceiverModel, - SubListModel, - SubListSplitReceiverModel, -} from '../models'; import appSettings from '../config/appSettings'; +import { getRegisteredModels, registerModels } from './modelRegistration'; export const dbConnection = new Sequelize( appSettings.postgresConnectionString, @@ -29,11 +19,6 @@ export async function connectToDb(): Promise { registerModels(); await initializeEntities(); - defineProjectAssociations(); - defineDripListAssociations(); - defineEcosystemMainAccountsAssociations(); - defineSubListAssociations(); - logger.info('Connected to the database.'); } catch (error) { logger.error('Failed to connect to the database.', error); @@ -45,167 +30,8 @@ export async function connectToDb(): Promise { async function initializeEntities(): Promise { logger.info('Initializing database schema...'); + getRegisteredModels().map((Model) => Model.defineAssociations?.()); getRegisteredModels().map((Model) => Model.initialize(dbConnection)); logger.info('Database schema initialized.'); } - -function defineProjectAssociations() { - // One-to-Many: A project can fund multiple addresses. - ProjectModel.hasMany(AddressDriverSplitReceiverModel, { - foreignKey: 'funderProjectId', - }); - AddressDriverSplitReceiverModel.belongsTo(ProjectModel, { - foreignKey: 'funderProjectId', - }); - - // One-to-Many: A project can fund multiple projects. - ProjectModel.hasMany(RepoDriverSplitReceiverModel, { - foreignKey: 'funderProjectId', - }); - RepoDriverSplitReceiverModel.belongsTo(ProjectModel, { - foreignKey: 'funderProjectId', - }); - - // One-to-Many: A project can fund multiple Drip Lists. - ProjectModel.hasMany(DripListSplitReceiverModel, { - foreignKey: 'funderProjectId', - }); - DripListSplitReceiverModel.belongsTo(ProjectModel, { - foreignKey: 'funderProjectId', - }); - - // One-to-Many: A project can fund multiple Sub Lists. - ProjectModel.hasMany(SubListSplitReceiverModel, { - foreignKey: 'funderProjectId', - }); - SubListSplitReceiverModel.belongsTo(ProjectModel, { - foreignKey: 'funderProjectId', - }); - - // One-to-One: A project receiver represents/is a project. - ProjectModel.hasOne(RepoDriverSplitReceiverModel, { - foreignKey: 'fundeeProjectId', - }); - RepoDriverSplitReceiverModel.belongsTo(ProjectModel, { - foreignKey: 'fundeeProjectId', - }); -} - -function defineDripListAssociations() { - // One-to-Many: A Drip List can fund multiple addresses. - DripListModel.hasMany(AddressDriverSplitReceiverModel, { - foreignKey: 'funderDripListId', - }); - AddressDriverSplitReceiverModel.belongsTo(DripListModel, { - foreignKey: 'funderDripListId', - }); - - // One-to-Many: A Drip List can fund multiple projects. - DripListModel.hasMany(RepoDriverSplitReceiverModel, { - foreignKey: 'funderDripListId', - }); - RepoDriverSplitReceiverModel.belongsTo(DripListModel, { - foreignKey: 'funderDripListId', - }); - - // One-to-Many: A Drip List can fund multiple Drip Lists. - DripListModel.hasMany(DripListSplitReceiverModel, { - foreignKey: 'funderDripListId', - }); - DripListSplitReceiverModel.belongsTo(DripListModel, { - foreignKey: 'funderDripListId', - }); - - // One-to-Many: A Drip List can fund multiple Sub Lists. - DripListModel.hasMany(SubListSplitReceiverModel, { - foreignKey: 'funderDripListId', - }); - SubListSplitReceiverModel.belongsTo(DripListModel, { - foreignKey: 'funderDripListId', - }); - - // One-to-One: A Drip List receiver represents/is a Drip List. - DripListModel.hasOne(DripListSplitReceiverModel, { - foreignKey: 'fundeeDripListId', - }); - DripListSplitReceiverModel.belongsTo(DripListModel, { - foreignKey: 'fundeeDripListId', - }); -} - -function defineEcosystemMainAccountsAssociations() { - // One-to-Many: An Ecosystem Main Account can fund multiple addresses. - EcosystemMainAccountModel.hasMany(AddressDriverSplitReceiverModel, { - foreignKey: 'funderEcosystemMainAccountId', - }); - AddressDriverSplitReceiverModel.belongsTo(EcosystemMainAccountModel, { - foreignKey: 'funderEcosystemMainAccountId', - }); - - // One-to-Many: An Ecosystem Main Account can fund multiple projects. - EcosystemMainAccountModel.hasMany(RepoDriverSplitReceiverModel, { - foreignKey: 'funderEcosystemMainAccountId', - }); - RepoDriverSplitReceiverModel.belongsTo(EcosystemMainAccountModel, { - foreignKey: 'funderEcosystemMainAccountId', - }); - - // One-to-Many: An Ecosystem Main Account can fund multiple Drip Lists. - EcosystemMainAccountModel.hasMany(DripListSplitReceiverModel, { - foreignKey: 'funderEcosystemMainAccountId', - }); - DripListSplitReceiverModel.belongsTo(EcosystemMainAccountModel, { - foreignKey: 'funderEcosystemMainAccountId', - }); - - // One-to-Many: An Ecosystem Main Account can fund multiple Sub Lists. - EcosystemMainAccountModel.hasMany(SubListSplitReceiverModel, { - foreignKey: 'funderEcosystemMainAccountId', - }); - SubListSplitReceiverModel.belongsTo(EcosystemMainAccountModel, { - foreignKey: 'funderEcosystemMainAccountId', - }); -} - -function defineSubListAssociations() { - // One-to-Many: A Sub List can fund multiple addresses. - SubListModel.hasMany(AddressDriverSplitReceiverModel, { - foreignKey: 'funderSubListId', - }); - AddressDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'funderSubListId', - }); - - // One-to-Many: A Sub List can fund multiple projects. - SubListModel.hasMany(RepoDriverSplitReceiverModel, { - foreignKey: 'funderSubListId', - }); - RepoDriverSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'funderSubListId', - }); - - // One-to-Many: A Sub List can fund multiple Drip Lists. - SubListModel.hasMany(DripListSplitReceiverModel, { - foreignKey: 'funderSubListId', - }); - DripListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'funderSubListId', - }); - - // One-to-Many: A Sub List can fund multiple Sub Lists. - SubListModel.hasMany(SubListSplitReceiverModel, { - foreignKey: 'funderSubListId', - }); - SubListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'funderSubListId', - }); - - // One-to-One: A Sub List receiver represents/is a Sub List. - SubListModel.hasOne(SubListSplitReceiverModel, { - foreignKey: 'fundeeSubListId', - }); - SubListSplitReceiverModel.belongsTo(SubListModel, { - foreignKey: 'fundeeSubListId', - }); -} diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index 7256bc1..9213190 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -1,27 +1,526 @@ -import { type QueryInterface } from 'sequelize'; +import { DataTypes, literal } from 'sequelize'; +import type { DataType, QueryInterface } from 'sequelize'; import getSchema from '../../utils/getSchema'; +import type { DbSchema } from '../../core/types'; export async function up({ context: sequelize }: any): Promise { const schema = getSchema(); const queryInterface: QueryInterface = sequelize.getQueryInterface(); await queryInterface.sequelize.query(` - CREATE TYPE "${schema}"."account_type" AS ENUM ( + CREATE TYPE ${schema}.account_type AS ENUM ( 'project', - 'dripList', - 'ecosystemMainAccount', - 'subList', + 'drip_list', + 'ecosystem_main_account', + 'sub_list', 'deadline', 'address' ); `); + + await queryInterface.sequelize.query(` + CREATE TYPE ${schema}.relationship_type AS ENUM ( + 'project_dependency', + 'project_maintainer', + 'drip_list_receiver', + 'ecosystem_receiver', + 'sub_list_link', + ); + `); + + await queryInterface.sequelize.query(` + CREATE TYPE ${schema}.project_verification_status AS ENUM ( + 'claimed', + 'owner_update_requested', + 'owner_updated', + 'unclaimed', + 'pending_owner', + 'pending_metadata', + ); + `); + + await queryInterface.sequelize.query(` + CREATE TYPE ${schema}.forges AS ENUM ( + 'gitHub', + 'gitLab', + ); + `); + + await createSplitReceiversTable(queryInterface, schema); + await createAccountMetadataEventsTable(queryInterface, schema); + await createProjectsTable(queryInterface, schema); + await createDripListsTable(queryInterface, schema); + await createEcosystemMainAccountsTable(queryInterface, schema); + await createTransferEventsTable(queryInterface, schema); + await createSubListsEventsTable(queryInterface, schema); + await createLastIndexedBlockTable(queryInterface, schema); +} + +async function createLastIndexedBlockTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable(`${schema}._last_indexed_block`, { + id: { + primaryKey: true, + autoIncrement: true, + type: DataTypes.INTEGER, + }, + blockNumber: { + unique: true, + allowNull: false, + type: DataTypes.BIGINT, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }); +} + +async function createSubListsEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable(`${schema}.sub_lists`, { + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + parentAccountType: { + allowNull: false, + type: literal(`"${schema}".account_type`) as unknown as DataType, + }, + parentId: { + allowNull: false, + type: DataTypes.STRING, + }, + rootAccountType: { + allowNull: false, + type: literal(`"${schema}".account_type`) as unknown as DataType, + }, + rootId: { + allowNull: false, + type: DataTypes.STRING, + }, + lastProcessedIpfsHash: { + allowNull: false, + type: DataTypes.TEXT, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }); + + await queryInterface.addIndex(`${schema}.sub_lists`, ['accountId'], { + name: 'idx_sub_lists_account_id', + }); + await queryInterface.addIndex(`${schema}.sub_lists`, ['parentId'], { + name: 'idx_sub_lists_parent_id', + }); + await queryInterface.addIndex(`${schema}.sub_lists`, ['rootId'], { + name: 'idx_sub_lists_root_id', + }); +} + +async function createDripListsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable(`${schema}.drip_lists`, { + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + isValid: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + ownerAddress: { + allowNull: true, + type: DataTypes.STRING, + }, + ownerAccountId: { + allowNull: true, + type: DataTypes.STRING, + }, + name: { + allowNull: true, + type: DataTypes.STRING, + }, + latestVotingRoundId: { + allowNull: true, + type: DataTypes.UUID, + }, + description: { + allowNull: true, + type: DataTypes.TEXT, + }, + creator: { + allowNull: true, + type: DataTypes.STRING, + }, + previousOwnerAddress: { + allowNull: true, + type: DataTypes.STRING, + }, + isVisible: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + lastProcessedIpfsHash: { + allowNull: false, + type: DataTypes.TEXT, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }); + + await queryInterface.addIndex(`${schema}.drip_lists`, ['ownerAddress'], { + name: 'idx_drip_lists_owner_address', + }); +} + +async function createEcosystemMainAccountsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable(`${schema}.ecosystem_main_accounts`, { + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + isValid: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + ownerAddress: { + allowNull: true, + type: DataTypes.STRING, + }, + ownerAccountId: { + allowNull: true, + type: DataTypes.STRING, + }, + name: { + allowNull: true, + type: DataTypes.STRING, + }, + latestVotingRoundId: { + allowNull: true, + type: DataTypes.UUID, + }, + description: { + allowNull: true, + type: DataTypes.TEXT, + }, + creator: { + allowNull: true, + type: DataTypes.STRING, + }, + previousOwnerAddress: { + allowNull: true, + type: DataTypes.STRING, + }, + isVisible: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + lastProcessedIpfsHash: { + allowNull: false, + type: DataTypes.TEXT, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }); + + await queryInterface.addIndex( + `${schema}.ecosystem_main_accounts`, + ['ownerAddress'], + { + name: 'idx_ecosystem_main_accounts_owner_address', + }, + ); +} + +async function createProjectsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable(`${schema}.projects`, { + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + name: { + allowNull: false, + type: DataTypes.STRING, + }, + verificationStatus: { + allowNull: false, + type: literal( + `"${schema}".project_verification_status`, + ) as unknown as DataType, + }, + forge: { + allowNull: false, + type: literal(`"${schema}".forges`) as unknown as DataType, + }, + ownerAddress: { + allowNull: false, + type: DataTypes.STRING, + }, + ownerAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + url: { + allowNull: false, + type: DataTypes.STRING, + }, + emoji: { + allowNull: true, + type: DataTypes.STRING, + }, + avatarCid: { + allowNull: true, + type: DataTypes.STRING, + }, + color: { + allowNull: false, + type: DataTypes.STRING, + }, + isVisible: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + lastProcessedIpfsHash: { + allowNull: false, + type: DataTypes.TEXT, + }, + claimedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }); + + await queryInterface.addIndex(`${schema}.projects`, ['ownerAddress'], { + name: 'idx_projects_owner_address', + }); + await queryInterface.addIndex(`${schema}.projects`, ['verificationStatus'], { + name: 'idx_projects_verification_status', + }); + await queryInterface.addIndex(`${schema}.projects`, ['url'], { + name: 'idx_projects_url', + }); +} + +async function createAccountMetadataEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + `${schema}.account_metadata_emitted_events`, + { + key: { + allowNull: false, + type: DataTypes.STRING, + }, + value: { + allowNull: false, + type: DataTypes.STRING, + }, + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + }, + ); + + await queryInterface.addIndex( + `${schema}.account_metadata_emitted_events`, + ['accountId'], + { + name: 'idx_account_metadata_emitted_events_accountId', + }, + ); +} + +async function createTransferEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable(`${schema}.transfer_events`, { + tokenId: { + allowNull: false, + type: DataTypes.STRING, + }, + from: { + allowNull: false, + type: DataTypes.STRING, + }, + to: { + allowNull: false, + type: DataTypes.STRING, + }, + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + }); +} + +async function createSplitReceiversTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable(`${schema}.split_receivers`, { + id: { + primaryKey: true, + autoIncrement: true, + type: DataTypes.INTEGER, + }, + receiverAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiverAccountType: { + allowNull: false, + type: literal(`"${schema}".account_type`) as unknown as DataType, + }, + senderAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + senderAccountType: { + allowNull: false, + type: literal(`"${schema}".account_type`) as unknown as DataType, + }, + relationshipType: { + allowNull: false, + type: literal(`"${schema}".relationship_type`) as unknown as DataType, + }, + weight: { + type: DataTypes.INTEGER, + allowNull: false, + }, + blockTimestamp: { + type: DataTypes.DATE, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + }, + }); + + await queryInterface.addIndex( + `${schema}.split_receivers`, + ['receiverAccountId', 'senderAccountId'], + { + name: 'idx_split_receivers_receiver_sender', + }, + ); + await queryInterface.addIndex( + `${schema}.split_receivers`, + ['senderAccountId', 'receiverAccountId'], + { + name: 'idx_split_receivers_sender_receiver', + }, + ); } export async function down({ context: sequelize }: any): Promise { const schema = getSchema(); const queryInterface: QueryInterface = sequelize.getQueryInterface(); + await queryInterface.dropTable(`${schema}.split_receivers`); + await queryInterface.dropTable(`${schema}.account_metadata_emitted_events`); + await queryInterface.dropTable(`${schema}.projects`); + await queryInterface.dropTable(`${schema}.drip_lists`); + await queryInterface.dropTable(`${schema}.ecosystem_main_accounts`); + await queryInterface.dropTable(`${schema}.transfer_events`); + await queryInterface.dropTable(`${schema}.sub_lists`); + await queryInterface.dropTable(`${schema}._last_indexed_block`); + + await queryInterface.sequelize.query(` + DROP TYPE IF EXISTS ${schema}.account_type; + `); + + await queryInterface.sequelize.query(` + DROP TYPE IF EXISTS ${schema}.relationship_type; + `); + + await queryInterface.sequelize.query(` + DROP TYPE IF EXISTS ${schema}.project_verification_status; + `); + await queryInterface.sequelize.query(` - DROP TYPE IF EXISTS "${schema}"."account_type"; + DROP TYPE IF EXISTS ${schema}.forges; `); } diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index e6e7265..6c42baf 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -1,11 +1,8 @@ import type { ModelStaticMembers } from '../core/types'; import { AccountMetadataEmittedEventModel, - AddressDriverSplitReceiverModel, DripListModel, - DripListSplitReceiverModel, - Project, - RepoDriverSplitReceiverModel, + ProjectModel, TransferEventModel, GivenEventModel, StreamsSetEventModel, @@ -15,7 +12,6 @@ import { SqueezedStreamsEventModel, SubListModel, EcosystemMainAccountModel, - SubListSplitReceiverModel, } from '../models'; const REGISTERED_MODELS: ModelStaticMembers[] = []; @@ -35,15 +31,11 @@ export function registerModels(): void { registerModel(DripListModel); registerModel(GivenEventModel); registerModel(SplitEventModel); - registerModel(Project); - registerModel(EcosystemMainAccountModel); + registerModel(ProjectModel); registerModel(TransferEventModel); registerModel(StreamsSetEventModel); + registerModel(EcosystemMainAccountModel); registerModel(SqueezedStreamsEventModel); - registerModel(SubListSplitReceiverModel); - registerModel(DripListSplitReceiverModel); registerModel(StreamReceiverSeenEventModel); - registerModel(RepoDriverSplitReceiverModel); - registerModel(AddressDriverSplitReceiverModel); registerModel(AccountMetadataEmittedEventModel); } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index 0a4d717..c4544c3 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -1,5 +1,5 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; -import type { AccountMetadataEmittedEvent } from '../../../contracts/CURRENT_NETWORK/Drips'; +import { toUtf8String } from 'ethers'; import type { AccountId } from '../../core/types'; import EventHandlerBase from '../../events/EventHandlerBase'; import { DRIPS_APP_USER_METADATA_KEY } from '../../core/constants'; @@ -20,13 +20,14 @@ import { } from '../../utils/metadataUtils'; import handleDripListMetadata from './handlers/handleDripListMetadata'; import type EventHandlerRequest from '../../events/EventHandlerRequest'; -import { AccountMetadataEmittedEventModel } from '../../models'; import { dbConnection } from '../../db/database'; import handleEcosystemMainAccountMetadata from './handlers/handleEcosystemMainAccountMetadata'; import handleSubListMetadata from './handlers/handleSubListMetadata'; import type { nftDriverAccountMetadataParser } from '../../metadata/schemas'; -import { getCurrentSplitsByAccountId } from './receiversRepository'; +import { getCurrentSplitReceiversBySender } from './receiversRepository'; import { isLatestEvent } from '../../utils/isLatestEvent'; +import type { AccountMetadataEmittedEvent } from '../../../contracts/CURRENT_NETWORK/Drips'; +import { AccountMetadataEmittedEventModel } from '../../models'; export default class AccountMetadataEmittedEventHandler extends EventHandlerBase<'AccountMetadataEmitted(uint256,bytes32,bytes)'> { public readonly eventSignatures = [ @@ -52,8 +53,8 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase LogManager.logRequestInfo( [ `📥 ${this.name} is processing ${eventSignature}:`, - ` - key: ${key}`, - ` - value: ${value} (IPFS hash: ${ipfsHash})`, + ` - key: ${key} (decoded: ${toUtf8String(key)})`, + ` - value: ${value} (decoded: ${ipfsHash})`, ` - accountId: ${accountId}`, ` - logIndex: ${logIndex}`, ` - txHash: ${transactionHash}`, @@ -72,7 +73,7 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase if (!this._canProcessDriverType(accountId)) { LogManager.logRequestInfo( - `Skipping ${eventSignature} event: accountId '${accountId}' is not a Driver type that can be processed.`, + `Skipping ${eventSignature} event: accountId '${accountId}' is not of a Driver that can be processed.`, requestId, ); @@ -98,9 +99,8 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase }, ); - logManager.appendFindOrCreateLog( + logManager.appendCreateLog( AccountMetadataEmittedEventModel, - true, `${accountMetadataEmittedEvent.transactionHash}-${accountMetadataEmittedEvent.logIndex}`, ); @@ -178,7 +178,9 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase const [accountId] = args as AccountMetadataEmittedEvent.OutputTuple; return { - accountIdsToInvalidate: await getCurrentSplitsByAccountId(accountId), + accountIdsToInvalidate: await getCurrentSplitReceiversBySender( + convertToAccountId(accountId), + ), }; } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/buildFunderAccountFields.ts b/src/eventHandlers/AccountMetadataEmittedEvent/buildFunderAccountFields.ts deleted file mode 100644 index 9b7ab43..0000000 --- a/src/eventHandlers/AccountMetadataEmittedEvent/buildFunderAccountFields.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { DependencyType } from '../../core/types'; -import type { - ImmutableSplitsDriverId, - NftDriverId, - RepoDriverId, -} from '../../core/types'; - -export type Funder = - | { - type: 'project'; - accountId: RepoDriverId; - dependencyType: 'dependency' | 'maintainer'; - } - | { type: 'dripList'; accountId: NftDriverId } - | { type: 'ecosystem'; accountId: NftDriverId } - | { type: 'sub-list'; accountId: ImmutableSplitsDriverId }; - -type FunderKeys = { - funderProjectId: RepoDriverId | null; - funderDripListId: NftDriverId | null; - funderSubListId: ImmutableSplitsDriverId | null; - funderEcosystemMainAccountId: NftDriverId | null; -}; - -export default function buildFunderAccountFields(funder: Funder): FunderKeys { - const keys: FunderKeys = { - funderProjectId: null, - funderSubListId: null, - funderDripListId: null, - funderEcosystemMainAccountId: null, - }; - - switch (funder.type) { - case 'project': - keys.funderProjectId = funder.accountId; - break; - case 'sub-list': - keys.funderSubListId = funder.accountId; - break; - case 'dripList': - keys.funderDripListId = funder.accountId; - break; - case 'ecosystem': - keys.funderEcosystemMainAccountId = funder.accountId; - break; - default: - throw new Error(`Unhandled funder type '${(funder as any).type}'.`); - } - - return keys; -} - -// TODO: Remove this function when the type is removed from the codebase. -export const resolveDependencyType = (funder: Funder): DependencyType => { - if (funder.type === 'project') { - return DependencyType.ProjectDependency; - } - - if (funder.type === 'dripList') { - return DependencyType.DripListDependency; - } - - // TODO: At the moment, both `ecosystem` and `sub-list` are treated as EcosystemDependency. The type is scheduled for removal soon. - return DependencyType.EcosystemDependency; -}; diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index ecd8dae..ac8805c 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -6,7 +6,6 @@ import type { z } from 'zod'; import type { IpfsHash, NftDriverId } from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; import type LogManager from '../../../core/LogManager'; -import { DripListModel } from '../../../models'; import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; import appSettings from '../../../config/appSettings'; @@ -16,14 +15,18 @@ import type { addressDriverSplitReceiverSchema, } from '../../../metadata/schemas/repo-driver/v2'; import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immutable-splits-driver/v1'; -import verifyProjectSources from '../projectVerification'; import { - deleteExistingReceivers, - createProjectReceiver, - createSubListReceiver, - createDripListReceiver, - createAddressReceiver, + createSplitReceiver, + deleteExistingSplitReceivers, } from '../receiversRepository'; +import { verifyProjectSources } from '../../../utils/projectUtils'; +import DripListModel from '../../../models/DripListModel'; +import { + assertIsAddressDiverId, + assertIsNftDriverId, + assertIsRepoDriverId, + convertToNftDriverId, +} from '../../../utils/accountIdUtils'; type Params = { ipfsHash: IpfsHash; @@ -44,87 +47,113 @@ export default async function handleDripListMetadata({ blockTimestamp, emitterAccountId, }: Params) { - validateMetadata(emitterAccountId, metadata); - - const receivers = metadata.projects ?? metadata.recipients; + validateMetadata(metadata); - const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = - await verifySplitsReceivers( - emitterAccountId, - receivers.map(({ weight, accountId }) => ({ - weight, - accountId, - })), + if (metadata.describes.accountId !== emitterAccountId) { + logManager.appendLog( + `🚨🕵️‍♂️ Skipped Drip List ${emitterAccountId} metadata processing: metadata describes account ID '${metadata.describes.accountId}' but metadata emitted by '${emitterAccountId}'.`, ); - if (!areSplitsValid) { + return; + } + + const splitReceivers = metadata.projects ?? metadata.recipients; + + const { isMatch, actualHash, onChainHash } = await verifySplitsReceivers( + emitterAccountId, + splitReceivers.map(({ weight, accountId }) => ({ + weight, + accountId, + })), + ); + + if (!isMatch) { logManager.appendLog( - `Skipped Drip List ${emitterAccountId} metadata processing: on-chain splits hash '${onChainSplitsHash}' does not match hash '${calculatedSplitsHash}' calculated from metadata.`, + `Skipped Drip List ${emitterAccountId} metadata processing: on-chain splits hash '${onChainHash}' does not match hash '${actualHash}' calculated from metadata.`, ); return; } - await verifyProjectSources(receivers); - - const dripListProps = { - id: emitterAccountId, - name: metadata.name ?? null, - description: - 'description' in metadata ? metadata.description || null : null, - latestVotingRoundId: - 'latestVotingRoundId' in metadata - ? (metadata.latestVotingRoundId as UUID) || null - : null, - lastProcessedIpfsHash: ipfsHash, - isVisible: - blockNumber > appSettings.visibilityThresholdBlockNumber && - 'isVisible' in metadata - ? metadata.isVisible - : true, - }; - - const [dripList, isCreated] = await DripListModel.findOrCreate({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - id: emitterAccountId, - }, - defaults: { - ...dripListProps, - isValid: false, // Until the related `TransferEvent` is processed. - }, - }); - - if (isCreated) { - logManager.appendFindOrCreateLog(DripListModel, true, dripList.id); - } else { - dripList.set(dripListProps); + const { areProjectsValid, message } = await verifyProjectSources( + splitReceivers.filter((r) => r.type === 'repoDriver'), + ); - logManager.appendUpdateLog(dripList, DripListModel, dripList.id); + if (!areProjectsValid) { + logManager.appendLog( + `🚨🕵️‍♂️ Skipped Drip List ${emitterAccountId} metadata processing: ${message}`, + ); - await dripList.save({ transaction }); + return; } - await deleteExistingReceivers({ - for: { - accountId: emitterAccountId, - column: 'funderDripListId', - }, + // ✅ All checks passed, we can proceed with the processing. + + await upsertDripList({ + ipfsHash, + metadata, + logManager, + blockNumber, transaction, }); - await setNewReceivers({ - receivers, + deleteExistingSplitReceivers(emitterAccountId, transaction); + + await createNewSplitReceivers({ logManager, transaction, blockTimestamp, emitterAccountId, + splitReceivers, }); } -async function setNewReceivers({ - receivers, +async function upsertDripList({ + ipfsHash, + metadata, + logManager, + blockNumber, + transaction, +}: { + ipfsHash: IpfsHash; + blockNumber: number; + logManager: LogManager; + transaction: Transaction; + metadata: AnyVersion; +}) { + const [dripList, isCreated] = await DripListModel.upsert( + { + accountId: convertToNftDriverId(metadata.describes.accountId), + name: metadata.name ?? null, + description: + 'description' in metadata ? metadata.description || null : null, + latestVotingRoundId: + 'latestVotingRoundId' in metadata + ? (metadata.latestVotingRoundId as UUID) || null + : null, + lastProcessedIpfsHash: ipfsHash, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, + isValid: false, // Until the `Transfer` event is processed. + }, + { + transaction, + }, + ); + + logManager.appendUpsertLog( + dripList, + DripListModel, + dripList.accountId, + Boolean(isCreated), + ); +} + +async function createNewSplitReceivers({ + splitReceivers, logManager, transaction, blockTimestamp, @@ -134,66 +163,68 @@ async function setNewReceivers({ logManager: LogManager; transaction: Transaction; emitterAccountId: NftDriverId; - receivers: ( + splitReceivers: ( | z.infer | z.infer | z.infer | z.infer )[]; }) { - const receiverPromises = receivers.map(async (receiver) => { + const receiverPromises = splitReceivers.map(async (receiver) => { switch (receiver.type) { case 'repoDriver': - return createProjectReceiver({ - logManager, - transaction, - blockTimestamp, - metadataReceiver: receiver, - funder: { - type: 'dripList', - accountId: emitterAccountId, - }, - }); - - case 'subList': - return createSubListReceiver({ + assertIsRepoDriverId(receiver.accountId); + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: receiver, - funder: { - type: 'dripList', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'drip_list', + receiverAccountId: receiver.accountId, + receiverAccountType: 'project', + relationshipType: 'drip_list_receiver', + weight: receiver.weight, + blockTimestamp, }, }); case 'dripList': - return createDripListReceiver({ + assertIsNftDriverId(receiver.accountId); + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: receiver, - funder: { - type: 'dripList', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'drip_list', + receiverAccountId: receiver.accountId, + receiverAccountType: 'drip_list', + relationshipType: 'drip_list_receiver', + weight: receiver.weight, + blockTimestamp, }, }); case 'address': - return createAddressReceiver({ + assertIsAddressDiverId(receiver.accountId); + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: receiver, - funder: { - type: 'dripList', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'drip_list', + receiverAccountId: receiver.accountId, + receiverAccountType: 'address', + relationshipType: 'drip_list_receiver', + weight: receiver.weight, + blockTimestamp, }, }); - default: return unreachableError( - `Unhandled receiver type: ${(receiver as any).type}`, + `Unhandled Drip List Split Receiver type: ${(receiver as any).type}`, ); } }); @@ -202,7 +233,6 @@ async function setNewReceivers({ } function validateMetadata( - emitterAccountId: NftDriverId, metadata: AnyVersion, ): asserts metadata is Extract< typeof metadata, @@ -223,12 +253,6 @@ function validateMetadata( )[]; } > { - if (emitterAccountId !== metadata.describes.accountId) { - throw new Error( - `Invalid Drip List metadata: emitter account ID is '${emitterAccountId}', but metadata describes '${metadata.describes.accountId}'.`, - ); - } - const isCurrent = 'recipients' in metadata && metadata.type === 'dripList'; const isLegacy = 'projects' in metadata; diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index 504d31b..c23d1c5 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -1,21 +1,25 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Transaction } from 'sequelize'; import type { z } from 'zod'; -import appSettings from '../../../config/appSettings'; import type LogManager from '../../../core/LogManager'; import type { IpfsHash, NftDriverId } from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; -import { EcosystemMainAccountModel } from '../../../models'; -import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; import type { repoDriverSplitReceiverSchema } from '../../../metadata/schemas/repo-driver/v2'; import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immutable-splits-driver/v1'; -import verifyProjectSources from '../projectVerification'; +import { verifyProjectSources } from '../../../utils/projectUtils'; +import { + assertIsImmutableSplitsDriverId, + assertIsRepoDriverId, + convertToNftDriverId, +} from '../../../utils/accountIdUtils'; +import { EcosystemMainAccountModel } from '../../../models'; +import appSettings from '../../../config/appSettings'; import { - createProjectReceiver, - createSubListReceiver, - deleteExistingReceivers, + createSplitReceiver, + deleteExistingSplitReceivers, } from '../receiversRepository'; +import unreachableError from '../../../utils/unreachableError'; type Params = { ipfsHash: IpfsHash; @@ -36,132 +40,155 @@ export default async function handleEcosystemMainAccountMetadata({ blockTimestamp, emitterAccountId, }: Params) { - validateMetadata(emitterAccountId, metadata); - - const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = - await verifySplitsReceivers( - emitterAccountId, - metadata.recipients.map(({ weight, accountId }) => ({ - weight, - accountId, - })), - ); + validateMetadata(metadata); - if (!areSplitsValid) { + if (metadata.describes.accountId !== emitterAccountId) { logManager.appendLog( - `Skipped Drip List ${emitterAccountId} metadata processing: on-chain splits hash '${onChainSplitsHash}' does not match hash '${calculatedSplitsHash}' calculated from metadata.`, + `🚨🕵️‍♂️ Skipped Ecosystem Main Account ${emitterAccountId} metadata processing: metadata describes account ID '${metadata.describes.accountId}' but metadata emitted by '${emitterAccountId}'.`, ); return; } - await verifyProjectSources(metadata.recipients); - - const ecosystemMainAccountProps = { - id: emitterAccountId, - name: metadata.name ?? null, - description: - 'description' in metadata ? metadata.description || null : null, - lastProcessedIpfsHash: ipfsHash, - isVisible: - blockNumber > appSettings.visibilityThresholdBlockNumber && - 'isVisible' in metadata - ? metadata.isVisible - : true, - }; - - const [ecosystemMainAccount, isCreated] = - await EcosystemMainAccountModel.findOrCreate({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { id: emitterAccountId }, - defaults: { - ...ecosystemMainAccountProps, - isValid: false, // Until the related `TransferEvent` is processed. - }, - }); - - if (isCreated) { - logManager.appendFindOrCreateLog( - EcosystemMainAccountModel, - true, - ecosystemMainAccount.id, - ); - } else { - ecosystemMainAccount.set(ecosystemMainAccountProps); - logManager.appendUpdateLog( - ecosystemMainAccount, - EcosystemMainAccountModel, - ecosystemMainAccount.id, + const { isMatch, actualHash, onChainHash } = await verifySplitsReceivers( + emitterAccountId, + metadata.recipients, + ); + + if (!isMatch) { + logManager.appendLog( + `Skipped Ecosystem Main Account ${emitterAccountId} metadata processing: on-chain splits hash '${onChainHash}' does not match hash '${actualHash}' calculated from metadata.`, ); - await ecosystemMainAccount.save({ transaction }); + return; } - await deleteExistingReceivers({ - for: { - accountId: emitterAccountId, - column: 'funderEcosystemMainAccountId', - }, + const { areProjectsValid, message } = await verifyProjectSources( + metadata.recipients.filter((r) => r.type === 'repoDriver'), + ); + + if (!areProjectsValid) { + logManager.appendLog( + `🚨🕵️‍♂️ Skipped Ecosystem Main Account ${emitterAccountId} metadata processing: ${message}`, + ); + } + + // ✅ All checks passed, we can proceed with the processing. + + await upsertEcosystemMainAccount({ + ipfsHash, + metadata, + logManager, + blockNumber, transaction, }); - await setNewReceivers({ - ipfsHash, + deleteExistingSplitReceivers(emitterAccountId, transaction); + + await createNewSplitReceivers({ logManager, transaction, blockTimestamp, emitterAccountId, - receivers: metadata.recipients, + splitReceivers: metadata.recipients, }); } -async function setNewReceivers({ - receivers, +async function upsertEcosystemMainAccount({ + ipfsHash, + metadata, + logManager, + blockNumber, + transaction, +}: { + ipfsHash: IpfsHash; + blockNumber: number; + logManager: LogManager; + transaction: Transaction; + metadata: AnyVersion; +}) { + const [dripList, isCreated] = await EcosystemMainAccountModel.upsert( + { + accountId: convertToNftDriverId(metadata.describes.accountId), + name: metadata.name ?? null, + description: + 'description' in metadata ? metadata.description || null : null, + lastProcessedIpfsHash: ipfsHash, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, + isValid: false, // Until the `Transfer` event is processed. + }, + { + transaction, + }, + ); + + logManager.appendUpsertLog( + dripList, + EcosystemMainAccountModel, + dripList.accountId, + Boolean(isCreated), + ); +} + +async function createNewSplitReceivers({ + splitReceivers, logManager, transaction, blockTimestamp, emitterAccountId, }: { - ipfsHash: IpfsHash; blockTimestamp: Date; logManager: LogManager; transaction: Transaction; emitterAccountId: NftDriverId; - receivers: ( + splitReceivers: ( | z.infer | z.infer )[]; }) { - const receiverPromises = receivers.map(async (receiver) => { + const receiverPromises = splitReceivers.map(async (receiver) => { switch (receiver.type) { case 'repoDriver': - return createProjectReceiver({ + assertIsRepoDriverId(receiver.accountId); + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: receiver, - funder: { - type: 'ecosystem', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'ecosystem_main_account', + receiverAccountId: receiver.accountId, + receiverAccountType: 'project', + relationshipType: 'ecosystem_receiver', + weight: receiver.weight, + blockTimestamp, }, }); case 'subList': - return createSubListReceiver({ + assertIsImmutableSplitsDriverId(receiver.accountId); + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: receiver, - funder: { - type: 'ecosystem', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'ecosystem_main_account', + receiverAccountId: receiver.accountId, + receiverAccountType: 'sub_list', + relationshipType: 'sub_list_link', + weight: receiver.weight, + blockTimestamp, }, }); default: return unreachableError( - `Unhandled receiver type: ${(receiver as any).type}`, + `Unhandled Ecosystem Main Account Split Receiver type: ${(receiver as any).type}`, ); } }); @@ -170,7 +197,6 @@ async function setNewReceivers({ } function validateMetadata( - emitterAccountId: NftDriverId, metadata: AnyVersion, ): asserts metadata is Extract< typeof metadata, @@ -182,12 +208,6 @@ function validateMetadata( )[]; } > { - if (emitterAccountId !== metadata.describes.accountId) { - throw new Error( - `Invalid Ecosystem Main Account metadata: emitter account ID is '${emitterAccountId}', but metadata describes '${metadata.describes.accountId}'.`, - ); - } - if ( !( 'recipients' in metadata && diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index 56cf7ee..e9e037f 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -1,15 +1,20 @@ /* eslint-disable no-param-reassign */ // Mutating Sequelize model instance is intentional and safe here. import { type Transaction } from 'sequelize'; import type { AnyVersion } from '@efstajas/versioned-parser'; -import { type z } from 'zod'; +import { ZeroAddress } from 'ethers'; import { ProjectModel } from '../../../models'; import type { repoDriverAccountMetadataParser } from '../../../metadata/schemas'; -import LogManager from '../../../core/LogManager'; -import { calculateProjectStatus } from '../../../utils/projectUtils'; -import type { IpfsHash, RepoDriverId } from '../../../core/types'; +import type LogManager from '../../../core/LogManager'; import { - assertAddressDiverId, + calculateProjectStatus, + verifyProjectSources, +} from '../../../utils/projectUtils'; +import type { Address, IpfsHash, RepoDriverId } from '../../../core/types'; +import { + assertIsAddressDiverId, + convertToAddressDriverId, convertToRepoDriverId, + getAddress, isAddressDriverId, isNftDriverId, isRepoDriverId, @@ -17,18 +22,12 @@ import { import unreachableError from '../../../utils/unreachableError'; import { getProjectMetadata } from '../../../utils/metadataUtils'; import verifySplitsReceivers from '../verifySplitsReceivers'; -import verifyProjectSources from '../projectVerification'; +import { repoDriverContract } from '../../../core/contractClients'; +import type { ProjectName } from '../../../models/ProjectModel'; import { - createAddressReceiver, - createDripListReceiver, - createProjectReceiver, - deleteExistingReceivers, + createSplitReceiver, + deleteExistingSplitReceivers, } from '../receiversRepository'; -import type { - addressDriverSplitReceiverSchema, - repoDriverSplitReceiverSchema, -} from '../../../metadata/schemas/repo-driver/v2'; -import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; type Params = { ipfsHash: IpfsHash; @@ -47,70 +46,91 @@ export default async function handleProjectMetadata({ }: Params) { const metadata = await getProjectMetadata(ipfsHash); - const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = - await verifySplitsReceivers(emitterAccountId, [ - ...metadata.splits.dependencies, - ...metadata.splits.maintainers, - ]); + if (metadata.describes.accountId !== emitterAccountId) { + logManager.appendLog( + `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: metadata describes account ID '${metadata.describes.accountId}' but metadata emitted by '${emitterAccountId}'.`, + ); + + return; + } + + const { isMatch, actualHash, onChainHash } = await verifySplitsReceivers( + emitterAccountId, + [...metadata.splits.dependencies, ...metadata.splits.maintainers], + ); - if (!areSplitsValid) { + if (!isMatch) { logManager.appendLog( - `Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: on-chain splits hash '${onChainSplitsHash}' does not match '${calculatedSplitsHash}' calculated from metadata.`, + `🚨 Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: on-chain splits hash '${onChainHash}' does not match '${actualHash}' calculated from metadata.`, ); return; } - const projects = metadata.splits.dependencies + const projectReceivers = metadata.splits.dependencies .flatMap((s) => ('type' in s && s.type === 'repoDriver' ? [s] : [])) .filter((dep) => dep.type === 'repoDriver'); + const { areProjectsValid, message } = + await verifyProjectSources(projectReceivers); + + if (!areProjectsValid) { + logManager.appendLog( + `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: ${message}`, + ); + + return; + } - await verifyProjectSources(projects); + const onChainOwner = await repoDriverContract.ownerOf(emitterAccountId); + if (!onChainOwner || onChainOwner === ZeroAddress) { + logManager.appendLog( + `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: on-chain owner is not set.`, + ); + } - await createProject({ + // ✅ All checks passed, we can proceed with the processing. + + await upsertProject({ metadata, ipfsHash, logManager, transaction, + blockTimestamp, + onChainOwner: onChainOwner as Address, }); - await deleteExistingReceivers({ - for: { - accountId: emitterAccountId, - column: 'funderProjectId', - }, - transaction, - }); + deleteExistingSplitReceivers(emitterAccountId, transaction); - await setNewReceivers({ + await createNewSplitReceivers({ logManager, transaction, blockTimestamp, emitterAccountId, - receivers: metadata.splits, + splitReceivers: metadata.splits, }); } -async function createProject({ +async function upsertProject({ ipfsHash, metadata, logManager, transaction, + onChainOwner, + blockTimestamp, }: { ipfsHash: IpfsHash; + blockTimestamp: Date; logManager: LogManager; transaction: Transaction; + onChainOwner: Address; metadata: AnyVersion; }): Promise { const { color, - source, - description, + source: { forge, ownerName, repoName, url }, describes: { accountId }, } = metadata; - const projectId = convertToRepoDriverId(accountId); - function getEmoji(): string | null { if ('avatar' in metadata) { return metadata.avatar.type === 'emoji' ? metadata.avatar.emoji : null; @@ -127,47 +147,39 @@ async function createProject({ return null; } - const projectProps = { - id: projectId, - color, - url: source.url, - description: description ?? null, - verificationStatus: calculateProjectStatus({ - id: projectId, + const [project, isCreated] = await ProjectModel.upsert( + { + accountId: convertToRepoDriverId(accountId), + url, + forge, + emoji: getEmoji(), color, - ownerAddress: null, - }), - isVisible: 'isVisible' in metadata ? metadata.isVisible : true, // Projects without `isVisible` field (V4 and below) are considered visible by default. - lastProcessedIpfsHash: ipfsHash, - emoji: getEmoji(), - avatarCid: getAvatarCid(), - }; - - const [project, isCreated] = await ProjectModel.findOrCreate({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { id: projectId }, - defaults: { - ...projectProps, - isValid: false, // Until the related `SplitsSet` is processed. + name: `${ownerName}/${repoName}` as ProjectName, + avatarCid: getAvatarCid(), + verificationStatus: calculateProjectStatus(onChainOwner), + isVisible: 'isVisible' in metadata ? metadata.isVisible : true, // Projects without `isVisible` field (V4 and below) are considered visible by default. + lastProcessedIpfsHash: ipfsHash, + ownerAddress: getAddress(onChainOwner), + ownerAccountId: convertToAddressDriverId(onChainOwner), + claimedAt: blockTimestamp, }, - }); - - if (isCreated) { - logManager.appendFindOrCreateLog(ProjectModel, true, project.id); - } else { - project.set(projectProps); - - logManager.appendUpdateLog(project, ProjectModel, project.id); + { + transaction, + }, + ); - await project.save({ transaction }); - } + logManager.appendUpsertLog( + project, + ProjectModel, + project.accountId, + Boolean(isCreated), + ); } -async function setNewReceivers({ - receivers, +async function createNewSplitReceivers({ logManager, transaction, + splitReceivers, blockTimestamp, emitterAccountId, }: { @@ -175,91 +187,85 @@ async function setNewReceivers({ logManager: LogManager; transaction: Transaction; emitterAccountId: RepoDriverId; - receivers: AnyVersion['splits']; + splitReceivers: AnyVersion['splits']; }) { - const { dependencies, maintainers } = receivers; + const { dependencies, maintainers } = splitReceivers; - const maintainerPromises = maintainers.map((maintainer) => { - assertAddressDiverId(maintainer.accountId); + const maintainerPromises = maintainers.map(async (maintainer) => { + assertIsAddressDiverId(maintainer.accountId); - return createAddressReceiver({ + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: maintainer as z.infer< - typeof addressDriverSplitReceiverSchema - >, // Safe to cast because we already checked the type of accountId. - funder: { - type: 'project', - dependencyType: 'maintainer', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'project', + receiverAccountId: maintainer.accountId, + receiverAccountType: 'address', + relationshipType: 'project_maintainer', + weight: maintainer.weight, + blockTimestamp, }, }); }); const dependencyPromises = dependencies.map(async (dependency) => { if (isRepoDriverId(dependency.accountId)) { - return createProjectReceiver({ + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: dependency as z.infer< - typeof repoDriverSplitReceiverSchema - >, // Safe to cast because we already checked the type of accountId. - funder: { - type: 'project', - dependencyType: 'dependency', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'project', + receiverAccountId: dependency.accountId, + receiverAccountType: 'project', + relationshipType: 'project_dependency', + weight: dependency.weight, + blockTimestamp, }, }); } if (isAddressDriverId(dependency.accountId)) { - return createAddressReceiver({ + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: dependency as z.infer< - typeof addressDriverSplitReceiverSchema - >, // Safe to cast because we already checked the type of accountId. - funder: { - type: 'project', - dependencyType: 'dependency', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'project', + receiverAccountId: dependency.accountId, + receiverAccountType: 'address', + relationshipType: 'project_dependency', + weight: dependency.weight, + blockTimestamp, }, }); } if (isNftDriverId(dependency.accountId)) { - // NFT Driver is always represents a DripList receiver for Projects. Ecosystem Main Account receivers are not yet supported for projects. - return createDripListReceiver({ + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: dependency as z.infer< - typeof dripListSplitReceiverSchema - >, // Safe to cast because we already checked the type of accountId., - funder: { - type: 'project', - dependencyType: 'dependency', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'project', + receiverAccountId: dependency.accountId, + receiverAccountType: 'drip_list', + relationshipType: 'project_dependency', + weight: dependency.weight, + blockTimestamp, }, }); } return unreachableError( - `Cannot process project dependency '${dependency.accountId}': unsupported Driver type.`, + `Unhandled Project Split Receiver type: ${(dependency as any).type}`, ); }); - const result = await Promise.all([ - ...maintainerPromises, - ...dependencyPromises, - ]); - - logManager.appendLog( - `Updated ${LogManager.nameOfType(ProjectModel)} with ID ${emitterAccountId} splits:\n${result - .map((p) => ` - ${JSON.stringify(p)}`) - .join('\n')}`, - ); + await Promise.all([...maintainerPromises, ...dependencyPromises]); } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index c9843c5..2e25282 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -5,26 +5,30 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type LogManager from '../../../core/LogManager'; import type { ImmutableSplitsDriverId, IpfsHash } from '../../../core/types'; import { EcosystemMainAccountModel, SubListModel } from '../../../models'; -import { convertToNftDriverId } from '../../../utils/accountIdUtils'; import { getImmutableSpitsDriverMetadata } from '../../../utils/metadataUtils'; import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; -import { - createProjectReceiver, - createSubListReceiver, - createDripListReceiver, - deleteExistingReceivers, - createAddressReceiver, -} from '../receiversRepository'; import type { addressDriverSplitReceiverSchema, repoDriverSplitReceiverSchema, } from '../../../metadata/schemas/repo-driver/v2'; import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immutable-splits-driver/v1'; import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; -import verifyProjectSources from '../projectVerification'; import RecoverableError from '../../../utils/recoverableError'; import type { immutableSplitsDriverMetadataParser } from '../../../metadata/schemas'; +import { verifyProjectSources } from '../../../utils/projectUtils'; +import { + createSplitReceiver, + deleteExistingSplitReceivers, +} from '../receiversRepository'; +import { METADATA_RECEIVER_TYPE_TO_ACCOUNT_TYPE } from '../../../core/splitRules'; +import { + assertIsAddressDiverId, + assertIsImmutableSplitsDriverId, + assertIsNftDriverId, + assertIsRepoDriverId, + convertToAccountId, +} from '../../../utils/accountIdUtils'; type Params = { ipfsHash: IpfsHash; @@ -43,62 +47,57 @@ export default async function handleSubListMetadata({ }: Params) { const metadata = await getImmutableSpitsDriverMetadata(ipfsHash); - const [areSplitsValid, onChainSplitsHash, calculatedSplitsHash] = - await verifySplitsReceivers(emitterAccountId, metadata.recipients); - - if (!areSplitsValid) { + if ( + metadata.parent.type !== 'ecosystem' || + metadata.root.type !== 'ecosystem' + ) { logManager.appendLog( - `Skipped Drip List ${emitterAccountId} metadata processing: on-chain splits hash '${onChainSplitsHash}' does not match hash '${calculatedSplitsHash}' calculated from metadata.`, + `🚨 Skipped Sub-List metadata processing: parent and root must be of type 'ecosystem'.`, ); return; } - await verifyProjectSources(metadata.recipients); - await validateRootAndParentExist(metadata, transaction); + const { isMatch, actualHash, onChainHash } = await verifySplitsReceivers( + emitterAccountId, + metadata.recipients, + ); - const subListProps = { - id: emitterAccountId, - parentEcosystemMainAccountId: - metadata.parent.type === 'ecosystem' - ? convertToNftDriverId(metadata.parent.accountId) - : null, - rootEcosystemMainAccountId: - metadata.root.type === 'ecosystem' - ? convertToNftDriverId(metadata.root.accountId) - : null, - lastProcessedIpfsHash: ipfsHash, - }; - - const [subList, isCreated] = await SubListModel.findOrCreate({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { id: emitterAccountId }, - defaults: { - ...subListProps, - isValid: false, // Until the related Sub List's metadata is processed. - }, - }); + if (!isMatch) { + logManager.appendLog( + `🚨 Skipped Sub-List metadata processing: on-chain splits hash '${onChainHash}' does not match '${actualHash}' calculated from metadata.`, + ); - if (isCreated) { - logManager.appendFindOrCreateLog(SubListModel, true, subList.id); - } else { - subList.set(subListProps); + return; + } - logManager.appendUpdateLog(subList, SubListModel, subList.id); + const { areProjectsValid, message } = await verifyProjectSources( + metadata.recipients.filter((r) => r.type === 'repoDriver'), + ); - await subList.save({ transaction }); + if (!areProjectsValid) { + logManager.appendLog( + `🚨🕵️‍♂️ Skipped Sub-List metadata processing: ${message}`, + ); + + return; } - await deleteExistingReceivers({ - for: { - accountId: emitterAccountId, - column: 'funderSubListId', - }, + await validateRootAndParentExist(metadata, transaction); + + // ✅ All checks passed, we can proceed with the processing. + + await upsertSubList({ + metadata, + ipfsHash, + logManager, transaction, + emitterAccountId, }); - await setNewReceivers({ + deleteExistingSplitReceivers(emitterAccountId, transaction); + + await createNewSplitReceivers({ logManager, transaction, blockTimestamp, @@ -107,7 +106,44 @@ export default async function handleSubListMetadata({ }); } -async function setNewReceivers({ +async function upsertSubList({ + ipfsHash, + metadata, + logManager, + transaction, + emitterAccountId, +}: { + ipfsHash: IpfsHash; + logManager: LogManager; + transaction: Transaction; + emitterAccountId: ImmutableSplitsDriverId; + metadata: AnyVersion; +}): Promise { + const [subList, isCreated] = await SubListModel.upsert( + { + accountId: emitterAccountId, + parentAccountType: + METADATA_RECEIVER_TYPE_TO_ACCOUNT_TYPE[metadata.parent.type], + parentId: convertToAccountId(metadata.parent.accountId), + rootAccountType: + METADATA_RECEIVER_TYPE_TO_ACCOUNT_TYPE.ecosystem_main_account, + rootId: convertToAccountId(metadata.root.accountId), + lastProcessedIpfsHash: ipfsHash, + }, + { + transaction, + }, + ); + + logManager.appendUpsertLog( + subList, + SubListModel, + subList.accountId, + Boolean(isCreated), + ); +} + +async function createNewSplitReceivers({ receivers, logManager, transaction, @@ -128,56 +164,76 @@ async function setNewReceivers({ const receiverPromises = receivers.map(async (receiver) => { switch (receiver.type) { case 'repoDriver': - return createProjectReceiver({ + assertIsRepoDriverId(receiver.accountId); + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: receiver, - funder: { - type: 'sub-list', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'sub_list', + receiverAccountId: receiver.accountId, + receiverAccountType: 'project', + relationshipType: 'sub_list_receiver', + weight: receiver.weight, + blockTimestamp, }, }); case 'subList': - return createSubListReceiver({ + assertIsImmutableSplitsDriverId(receiver.accountId); + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: receiver, - funder: { - type: 'sub-list', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'sub_list', + receiverAccountId: receiver.accountId, + receiverAccountType: 'sub_list', + relationshipType: 'sub_list_receiver', + weight: receiver.weight, + blockTimestamp, }, }); case 'dripList': - return createDripListReceiver({ + assertIsNftDriverId(receiver.accountId); + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: receiver, - funder: { - type: 'sub-list', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'sub_list', + receiverAccountId: receiver.accountId, + receiverAccountType: 'drip_list', + relationshipType: 'sub_list_receiver', + weight: receiver.weight, + blockTimestamp, }, }); case 'address': - return createAddressReceiver({ + assertIsAddressDiverId(receiver.accountId); + return createSplitReceiver({ logManager, transaction, blockTimestamp, - metadataReceiver: receiver, - funder: { - type: 'sub-list', - accountId: emitterAccountId, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'sub_list', + receiverAccountId: receiver.accountId, + receiverAccountType: 'address', + relationshipType: 'sub_list_receiver', + weight: receiver.weight, + blockTimestamp, }, }); default: return unreachableError( - `Unhandled receiver type: ${(receiver as any).type}`, + `Unhandled Sub-List Receiver type: ${(receiver as any).type}`, ); } }); @@ -189,13 +245,6 @@ async function validateRootAndParentExist( metadata: AnyVersion, transaction: Transaction, ) { - if ( - metadata.parent.type !== 'ecosystem' || - metadata.root.type !== 'ecosystem' - ) { - throw new Error('Sub Lists are currently only supported in Ecosystems.'); - } - const root = await EcosystemMainAccountModel.findByPk( metadata.root.accountId, { @@ -203,6 +252,7 @@ async function validateRootAndParentExist( lock: transaction.LOCK.UPDATE, }, ); + if (!root) { throw new RecoverableError( `Root Ecosystem Main Account '${metadata.root.accountId}' not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts b/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts deleted file mode 100644 index 2809ebd..0000000 --- a/src/eventHandlers/AccountMetadataEmittedEvent/projectVerification.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { z } from 'zod'; -import type { AnyVersion } from '@efstajas/versioned-parser'; -import { hexlify, toUtf8Bytes } from 'ethers'; -import { repoDriverContract } from '../../core/contractClients'; -import type { dripListSplitReceiverSchema } from '../../metadata/schemas/nft-driver/v2'; -import type { - repoDriverSplitReceiverSchema, - addressDriverSplitReceiverSchema, -} from '../../metadata/schemas/repo-driver/v2'; -import type { subListSplitReceiverSchema } from '../../metadata/schemas/immutable-splits-driver/v1'; -import type { repoDriverAccountMetadataParser } from '../../metadata/schemas'; -import type { ProjectModel } from '../../models'; -import unreachableError from '../../utils/unreachableError'; - -export default async function verifyProjectSources( - recipients: ( - | z.infer - | z.infer - | z.infer - | z.infer - )[], -) { - for (const r of recipients) { - if (r.type === 'repoDriver') { - const accountId = await repoDriverContract.calcAccountId( - r.source.forge === 'github' - ? 0 - : unreachableError(`Unexpected forge: ${r.source.forge}`), - hexlify(toUtf8Bytes(`${r.source.ownerName}/${r.source.repoName}`)), - ); - - if (accountId.toString() !== r.accountId) { - throw new Error( - `Failed to verify Project's source: calculated accountId '${accountId}' does not match the one in metadata ('${r.accountId}') for repo '${r.source.ownerName}/${r.source.repoName}' on '${r.source.forge}'.`, - ); - } - } - } -} - -export async function verifyProjectMetadata( - onChainProject: ProjectModel, - metadata: AnyVersion, -): Promise { - const errors: string[] = []; - - const { - id: onChainProjectId, - name: onChainProjectName, - url: onChainProjectUrl, - } = onChainProject; - - const { - describes, - source: { - url: metadataUrl, - repoName: metadataRepoName, - ownerName: metadataOwnerName, - }, - } = metadata; - - if (`${metadataOwnerName}/${metadataRepoName}` !== onChainProjectName) { - errors.push( - `- Repo name mismatch: got '${metadataOwnerName}/${metadataRepoName}', expected '${onChainProjectName}'.`, - ); - } - - if (metadataUrl !== onChainProjectUrl) { - errors.push( - `- URL mismatch: got '${metadataUrl}', expected '${onChainProjectUrl}'.`, - ); - } - - if (describes.accountId !== onChainProjectId) { - errors.push( - `- Account ID mismatch: got '${describes.accountId}', expected '${onChainProjectId}'.`, - ); - } - - if (errors.length > 0) { - throw new Error( - `Project metadata mismatch for project ID '${onChainProjectId}':\n${errors.join( - '\n', - )}`, - ); - } -} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts index f483f0b..9736b53 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts @@ -1,357 +1,57 @@ -import { Op, type Transaction } from 'sequelize'; -import type { z } from 'zod'; -import { DependencyType } from '../../core/types'; -import type { - AccountId, - ImmutableSplitsDriverId, - NftDriverId, - RepoDriverId, -} from '../../core/types'; -import { - AddressDriverSplitReceiverModel, - DripListModel, - DripListSplitReceiverModel, - ProjectModel, - RepoDriverSplitReceiverModel, - StreamReceiverSeenEventModel, - SubListModel, - SubListSplitReceiverModel, -} from '../../models'; +import { type Transaction } from 'sequelize'; +import type { AccountId } from '../../core/types'; import type LogManager from '../../core/LogManager'; -import type { - addressDriverSplitReceiverSchema, - repoDriverSplitReceiverSchema, -} from '../../metadata/schemas/repo-driver/v2'; -import { - assertIsAccountId, - convertToAddressDriverId, - convertToImmutableSplitsDriverId, - convertToNftDriverId, - convertToRepoDriverId, -} from '../../utils/accountIdUtils'; -import getUserAddress from '../../utils/getAccountAddress'; -import { AddressDriverSplitReceiverType } from '../../models/AddressDriverSplitReceiverModel'; -import type { dripListSplitReceiverSchema } from '../../metadata/schemas/nft-driver/v2'; -import type { subListSplitReceiverSchema } from '../../metadata/schemas/immutable-splits-driver/v1'; -import type { Funder } from './buildFunderAccountFields'; -import buildFunderAccountFields, { - resolveDependencyType, -} from './buildFunderAccountFields'; -import RecoverableError from '../../utils/recoverableError'; -import { - calculateProjectStatus, - METADATA_FORGE_MAP, -} from '../../utils/projectUtils'; - -export async function createAddressReceiver({ - funder, - logManager, - transaction, - blockTimestamp, - metadataReceiver, -}: { - funder: Funder; - blockTimestamp: Date; - logManager: LogManager; - transaction: Transaction; - metadataReceiver: z.infer; -}) { - const { weight, accountId } = metadataReceiver; - - const fundeeAccountId = convertToAddressDriverId(accountId); - - let type: AddressDriverSplitReceiverType; - if (funder.type === 'project') { - type = - funder.dependencyType === 'dependency' - ? AddressDriverSplitReceiverType.ProjectDependency - : AddressDriverSplitReceiverType.ProjectMaintainer; - } else if (funder.type === 'dripList') { - type = AddressDriverSplitReceiverType.DripListDependency; - } else { - // TODO: For now we treat both ecosystem and sub-list cases as EcosystemDependency. Type will shortly be removed either way. - type = AddressDriverSplitReceiverType.EcosystemDependency; - } - - // Create the receiver. - const receiver = await AddressDriverSplitReceiverModel.create( - { - type, - weight, - blockTimestamp, - fundeeAccountId, - ...buildFunderAccountFields(funder), - fundeeAccountAddress: getUserAddress(accountId), +import type { SplitReceiverShape } from '../../core/splitRules'; +import { SplitReceiver, StreamReceiverSeenEventModel } from '../../models'; + +export async function deleteExistingSplitReceivers( + senderAccountId: AccountId, + transaction: Transaction, +) { + await SplitReceiver.destroy({ + where: { + senderAccountId, }, - { transaction }, - ); - - logManager.appendFindOrCreateLog( - AddressDriverSplitReceiverModel, - true, - receiver.id.toString(), - ); -} - -export async function createDripListReceiver({ - blockTimestamp, - funder, - metadataReceiver, - logManager, - transaction, -}: { - funder: Funder; - metadataReceiver: z.infer; - transaction: Transaction; - blockTimestamp: Date; - logManager: LogManager; -}) { - const { weight, accountId } = metadataReceiver; - const fundeeDripListId = convertToNftDriverId(accountId); - - const dripList = await DripListModel.findByPk(fundeeDripListId, { transaction, - lock: transaction.LOCK.UPDATE, }); - - if (!dripList) { - throw new RecoverableError( - `Drip List ${fundeeDripListId} not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } - - const receiver = await DripListSplitReceiverModel.create( - { - weight, - fundeeDripListId, - type: resolveDependencyType(funder), - ...buildFunderAccountFields(funder), - blockTimestamp, - }, - { - transaction, - }, - ); - - logManager.appendFindOrCreateLog( - DripListSplitReceiverModel, - true, - receiver.id.toString(), - ); } -export async function createSubListReceiver({ - funder, +export async function createSplitReceiver({ logManager, transaction, - blockTimestamp, - metadataReceiver, + splitReceiverShape, }: { - funder: Funder; blockTimestamp: Date; logManager: LogManager; transaction: Transaction; - metadataReceiver: z.infer; + splitReceiverShape: SplitReceiverShape; }) { - const { weight, accountId } = metadataReceiver; - const fundeeSubListId = convertToImmutableSplitsDriverId(accountId); - - const [subList, isCreated] = await SubListModel.findOrCreate({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - id: fundeeSubListId, - }, - defaults: { - id: fundeeSubListId, - isValid: false, // Until the related Sub List's metadata is processed. - }, - }); - - logManager.appendFindOrCreateLog(SubListModel, isCreated, subList.id); - - const receiver = await SubListSplitReceiverModel.create( - { - weight, - blockTimestamp, - fundeeSubListId, - type: DependencyType.EcosystemDependency, // All sub-list receivers are ecosystem dependencies (soon to be removed). - ...buildFunderAccountFields(funder), - }, + const splitReceiver = await SplitReceiver.create( + { ...splitReceiverShape }, { transaction }, ); - logManager.appendFindOrCreateLog( - SubListSplitReceiverModel, - true, - receiver.id.toString(), - ); + logManager.appendCreateLog(SplitReceiver, splitReceiver.id.toString()); } -export async function createProjectReceiver({ - funder, - logManager, - transaction, - blockTimestamp, - metadataReceiver, -}: { - funder: Funder; - blockTimestamp: Date; - logManager: LogManager; - transaction: Transaction; - metadataReceiver: z.infer; -}) { - const { - weight, - accountId, - source: { url, ownerName, repoName, forge }, - } = metadataReceiver; - const fundeeProjectId = convertToRepoDriverId(accountId); - - const [project, isCreated] = await ProjectModel.findOrCreate({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { - id: fundeeProjectId, - }, - defaults: { - id: fundeeProjectId, - url, - isVisible: true, // Default to visible on creation. Final visibility will be determined by account metadata. - isValid: true, // The project is valid by default since there are no receivers yet. - name: `${ownerName}/${repoName}`, - verificationStatus: calculateProjectStatus({ - id: fundeeProjectId, - color: null, - ownerAddress: null, - }), - forge: METADATA_FORGE_MAP[forge], - }, - }); - - logManager.appendFindOrCreateLog(ProjectModel, isCreated, project.id); - - const receiver = await RepoDriverSplitReceiverModel.create( - { - weight, - fundeeProjectId, - type: resolveDependencyType(funder), - ...buildFunderAccountFields(funder), - blockTimestamp, - }, - { transaction }, - ); - - logManager.appendFindOrCreateLog( - RepoDriverSplitReceiverModel, - true, - receiver.id.toString(), - ); -} - -type ClearReceiversInput = - | { accountId: RepoDriverId; column: 'funderProjectId' } - | { accountId: NftDriverId; column: 'funderDripListId' } - | { accountId: NftDriverId; column: 'funderEcosystemMainAccountId' } - | { accountId: ImmutableSplitsDriverId; column: 'funderSubListId' }; - -type ClearReceiversParams = { - for: ClearReceiversInput; - transaction: Transaction; - excludeReceivers?: AccountId[]; -}; - -export async function deleteExistingReceivers({ - transaction, - for: { accountId, column }, -}: ClearReceiversParams): Promise { - const where = { [column]: accountId }; - - await AddressDriverSplitReceiverModel.destroy({ - where, - transaction, - }); - - await RepoDriverSplitReceiverModel.destroy({ - where, - transaction, - }); - - await DripListSplitReceiverModel.destroy({ - where, - transaction, - }); - - await SubListSplitReceiverModel.destroy({ - where, - transaction, - }); -} - -export async function getCurrentSplitsByAccountId( - emitterAccountId: bigint, +export async function getCurrentSplitReceiversBySender( + senderAccountId: AccountId, ): Promise { - assertIsAccountId(emitterAccountId); - - const addressSplits = await AddressDriverSplitReceiverModel.findAll({ - where: { - [Op.or]: [ - { funderProjectId: emitterAccountId }, - { funderDripListId: emitterAccountId }, - { funderEcosystemMainAccountId: emitterAccountId }, - { funderSubListId: emitterAccountId }, - ], - }, - lock: true, - }); - - const dripListSplits = await DripListSplitReceiverModel.findAll({ - where: { - [Op.or]: [ - { funderProjectId: emitterAccountId }, - { funderDripListId: emitterAccountId }, - { funderEcosystemMainAccountId: emitterAccountId }, - { funderSubListId: emitterAccountId }, - ], - }, - lock: true, - }); - - const projectSplits = await RepoDriverSplitReceiverModel.findAll({ - where: { - [Op.or]: [ - { funderProjectId: emitterAccountId }, - { funderDripListId: emitterAccountId }, - { funderEcosystemMainAccountId: emitterAccountId }, - { funderSubListId: emitterAccountId }, - ], - }, - lock: true, - }); - - const subListSplits = await SubListSplitReceiverModel.findAll({ + const splitReceivers = await SplitReceiver.findAll({ where: { - [Op.or]: [ - { funderProjectId: emitterAccountId }, - { funderDripListId: emitterAccountId }, - { funderEcosystemMainAccountId: emitterAccountId }, - { funderSubListId: emitterAccountId }, - ], + senderAccountId, }, lock: true, }); const accountIds = [ - ...addressSplits.map((receiver) => receiver.fundeeAccountId), - ...dripListSplits.map((receiver) => receiver.fundeeDripListId), - ...projectSplits.map((receiver) => receiver.fundeeProjectId), - ...subListSplits.map((receiver) => receiver.fundeeSubListId), + ...splitReceivers.map((receiver) => receiver.senderAccountId), ]; return Array.from(new Set(accountIds)); } -export async function getCurrentSplitsByReceiversHash( +export async function getCurrentSplitReceiversByReceiversHash( receiversHash: string, ): Promise { const streamReceiverSeenEvents = await StreamReceiverSeenEventModel.findAll({ diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts b/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts index 22bc880..a4f97f0 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/verifySplitsReceivers.ts @@ -1,30 +1,27 @@ -import type { - RepoDriverId, - NftDriverId, - ImmutableSplitsDriverId, -} from '../../core/types'; import { dripsContract } from '../../core/contractClients'; import { formatSplitReceivers } from '../../utils/formatSplitReceivers'; import type { SplitsReceiverStruct } from '../../../contracts/CURRENT_NETWORK/Drips'; +import type { AccountId } from '../../core/types'; -type AccountId = RepoDriverId | NftDriverId | ImmutableSplitsDriverId; +type SplitsHashVerificationResult = { + isMatch: boolean; + onChainHash: string; + actualHash: string; +}; export default async function verifySplitsReceivers( accountId: AccountId, - splits: SplitsReceiverStruct[], -): Promise< - [ - areSplitsValid: boolean, - onChainSplitsHash: string, - calculatedSplitsHash: string, - ] -> { - const calculatedHash = await dripsContract.hashSplits( - formatSplitReceivers(splits), + splitReceivers: SplitsReceiverStruct[], +): Promise { + const actualHash = await dripsContract.hashSplits( + formatSplitReceivers(splitReceivers), ); - const onChainHash = await dripsContract.splitsHash(accountId); - const isValid = calculatedHash === onChainHash; + const onChainHash = await dripsContract.splitsHash(accountId); - return [isValid, onChainHash, calculatedHash]; + return { + isMatch: actualHash === onChainHash, + onChainHash, + actualHash, + }; } diff --git a/src/eventHandlers/StreamReceiverSeenEventHandler.ts b/src/eventHandlers/StreamReceiverSeenEventHandler.ts index 19380ce..b19f194 100644 --- a/src/eventHandlers/StreamReceiverSeenEventHandler.ts +++ b/src/eventHandlers/StreamReceiverSeenEventHandler.ts @@ -7,7 +7,7 @@ import type EventHandlerRequest from '../events/EventHandlerRequest'; import StreamReceiverSeenEventModel from '../models/StreamReceiverSeenEventModel'; import { convertToAccountId } from '../utils/accountIdUtils'; import { toBigIntString } from '../utils/bigintUtils'; -import { getCurrentSplitsByReceiversHash } from './AccountMetadataEmittedEvent/receiversRepository'; +import { getCurrentSplitReceiversByReceiversHash } from './AccountMetadataEmittedEvent/receiversRepository'; export default class StreamReceiverSeenEventHandler extends EventHandlerBase<'StreamReceiverSeen(bytes32,uint256,uint256)'> { public eventSignatures = [ @@ -77,7 +77,7 @@ export default class StreamReceiverSeenEventHandler extends EventHandlerBase<'St return { accountIdsToInvalidate: - await getCurrentSplitsByReceiversHash(rawReceiversHash), + await getCurrentSplitReceiversByReceiversHash(rawReceiversHash), }; } } diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index cf3cc21..2365f55 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -13,6 +13,7 @@ import { dbConnection } from '../db/database'; import { isLatestEvent } from '../utils/isLatestEvent'; import appSettings from '../config/appSettings'; import RecoverableError from '../utils/recoverableError'; +import type { Address } from '../core/types'; export default class TransferEventHandler extends EventHandlerBase<'Transfer(address,address,uint256)'> { public eventSignatures = ['Transfer(address,address,uint256)' as const]; @@ -50,8 +51,8 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add const transferEvent = await TransferEventModel.create( { tokenId, - to, - from, + to: to as Address, + from: from as Address, logIndex, blockNumber, blockTimestamp, @@ -62,9 +63,8 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add }, ); - logManager.appendFindOrCreateLog( + logManager.appendCreateLog( TransferEventModel, - true, `${transferEvent.transactionHash}-${transferEvent.logIndex}`, ); @@ -104,17 +104,17 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add const entity = (dripList ?? ecosystemMainAccount)!; const entityModel = dripList ? DripListModel : EcosystemMainAccountModel; - entity.ownerAddress = to; - entity.previousOwnerAddress = from; + entity.ownerAddress = to as Address; + entity.previousOwnerAddress = from as Address; entity.ownerAccountId = await calcAccountId(to); - entity.creator = to; // TODO: https://github.com/drips-network/events-processor/issues/14 + entity.creator = to as Address; // TODO: https://github.com/drips-network/events-processor/issues/14 entity.isVisible = blockNumber > appSettings.visibilityThresholdBlockNumber ? from === ZeroAddress || from === appSettings.ecosystemDeployer : true; entity.isValid = true; // The entity is initialized with `false` when created during account metadata processing. - logManager.appendUpdateLog(entity, entityModel, entity.id); + logManager.appendUpdateLog(entity, entityModel, entity.accountId); await entity.save({ transaction }); diff --git a/src/events/registrations.ts b/src/events/registrations.ts index ab36fcf..38ec273 100644 --- a/src/events/registrations.ts +++ b/src/events/registrations.ts @@ -1,10 +1,7 @@ import { AccountMetadataEmittedEventHandler, GivenEventHandler, - OwnerUpdateRequestedEventHandler, - OwnerUpdatedEventHandler, SplitEventHandler, - SplitsSetEventHandler, TransferEventHandler, StreamReceiverSeenEventHandler, StreamsSetEventHandler, @@ -13,21 +10,6 @@ import { import { registerEventHandler } from './eventHandlerUtils'; export function registerEventHandlers(): void { - registerEventHandler< - | 'OwnerUpdateRequested(uint256,uint8,bytes,address)' - | 'OwnerUpdateRequested(uint256,uint8,bytes)' - >( - [ - 'OwnerUpdateRequested(uint256,uint8,bytes,address)', - 'OwnerUpdateRequested(uint256,uint8,bytes)', - ], - OwnerUpdateRequestedEventHandler, - ); - - registerEventHandler<'OwnerUpdated(uint256,address)'>( - 'OwnerUpdated(uint256,address)', - OwnerUpdatedEventHandler, - ); registerEventHandler<'AccountMetadataEmitted(uint256,bytes32,bytes)'>( 'AccountMetadataEmitted(uint256,bytes32,bytes)', AccountMetadataEmittedEventHandler, @@ -44,10 +26,6 @@ export function registerEventHandlers(): void { 'Split(uint256,uint256,address,uint128)', SplitEventHandler, ); - registerEventHandler<'SplitsSet(uint256,bytes32)'>( - 'SplitsSet(uint256,bytes32)', - SplitsSetEventHandler, - ); registerEventHandler<'StreamsSet(uint256,address,bytes32,bytes32,uint128,uint32)'>( 'StreamsSet(uint256,address,bytes32,bytes32,uint128,uint32)', StreamsSetEventHandler, diff --git a/src/metadata/schemas/immutable-splits-driver/v1.ts b/src/metadata/schemas/immutable-splits-driver/v1.ts index 8f8f6f6..18c5ee8 100644 --- a/src/metadata/schemas/immutable-splits-driver/v1.ts +++ b/src/metadata/schemas/immutable-splits-driver/v1.ts @@ -26,18 +26,18 @@ export const subListMetadataSchemaV1 = z.object({ accountId: z.string(), driver: z.union([z.literal('nft'), z.literal('immutable-splits')]), type: z.union([ - z.literal('drip-list'), + z.literal('dripList'), z.literal('ecosystem'), - z.literal('sub-list'), + z.literal('subList'), ]), }), root: z.object({ accountId: z.string(), driver: z.union([z.literal('nft'), z.literal('immutable-splits')]), type: z.union([ - z.literal('drip-list'), + z.literal('dripList'), z.literal('ecosystem'), - z.literal('sub-list'), + z.literal('subList'), ]), }), }); diff --git a/src/models/AccountMetadataEmittedEventModel.ts b/src/models/AccountMetadataEmittedEventModel.ts index 907c097..506d6f4 100644 --- a/src/models/AccountMetadataEmittedEventModel.ts +++ b/src/models/AccountMetadataEmittedEventModel.ts @@ -7,6 +7,7 @@ import { DataTypes, Model } from 'sequelize'; import { COMMON_EVENT_INIT_ATTRIBUTES } from '../core/constants'; import getSchema from '../utils/getSchema'; import type { IEventModel } from '../events/types'; +import type { AccountId } from '../core/types'; export default class AccountMetadataEmittedEventModel extends Model< @@ -15,43 +16,39 @@ export default class AccountMetadataEmittedEventModel > implements IEventModel { - // Properties from event output. - public declare key: string; - public declare value: string; - public declare accountId: string; - - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; + declare public key: string; + declare public value: string; + declare public accountId: AccountId; + declare public logIndex: number; + declare public blockNumber: number; + declare public blockTimestamp: Date; + declare public transactionHash: string; public static initialize(sequelize: Sequelize): void { this.init( { key: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, value: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, accountId: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, ...COMMON_EVENT_INIT_ATTRIBUTES, }, { sequelize, schema: getSchema(), - tableName: 'AccountMetadataEmittedEvents', + tableName: 'account_metadata_emitted_events', indexes: [ { fields: ['accountId'], - name: `IX_AccountMetadataEmittedEvents_accountId`, - unique: false, + name: 'idx_account_metadata_emitted_events_accountId', }, ], }, diff --git a/src/models/AddressDriverSplitReceiverModel.ts b/src/models/AddressDriverSplitReceiverModel.ts deleted file mode 100644 index cd67c88..0000000 --- a/src/models/AddressDriverSplitReceiverModel.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { - CreationOptional, - InferAttributes, - InferCreationAttributes, - Sequelize, -} from 'sequelize'; -import { DataTypes, Model } from 'sequelize'; -import type { AddressLike } from 'ethers'; -import getSchema from '../utils/getSchema'; -import ProjectModel from './ProjectModel'; -import type { - AddressDriverId, - ImmutableSplitsDriverId, - NftDriverId, - RepoDriverId, -} from '../core/types'; -import DripListModel from './DripListModel'; -import EcosystemMainAccountModel from './EcosystemMainAccountModel'; -import SubListModel from './SubListModel'; - -export enum AddressDriverSplitReceiverType { - ProjectMaintainer = 'ProjectMaintainer', - ProjectDependency = 'ProjectDependency', - DripListDependency = 'DripListDependency', - EcosystemDependency = 'EcosystemDependency', -} - -export default class AddressDriverSplitReceiverModel extends Model< - InferAttributes, - InferCreationAttributes -> { - public declare id: CreationOptional; // Primary key - public declare fundeeAccountId: AddressDriverId; - public declare fundeeAccountAddress: AddressLike; - public declare funderProjectId: RepoDriverId | null; // Foreign key - public declare funderDripListId: NftDriverId | null; // Foreign key - public declare funderEcosystemMainAccountId: NftDriverId | null; // Foreign key - public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key - - public declare weight: number; - public declare type: AddressDriverSplitReceiverType; - public declare blockTimestamp: Date; - - public static initialize(sequelize: Sequelize): void { - this.init( - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - fundeeAccountId: { - type: DataTypes.STRING, - allowNull: false, - }, - fundeeAccountAddress: { - type: DataTypes.STRING, - allowNull: false, - }, - funderProjectId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: ProjectModel, - key: 'id', - }, - allowNull: true, - }, - funderDripListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: DripListModel, - key: 'id', - }, - allowNull: true, - }, - funderEcosystemMainAccountId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: EcosystemMainAccountModel, - key: 'id', - }, - allowNull: true, - }, - funderSubListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: SubListModel, - key: 'id', - }, - allowNull: true, - }, - weight: { - type: DataTypes.INTEGER, - allowNull: true, - }, - type: { - type: DataTypes.ENUM( - ...Object.values(AddressDriverSplitReceiverType), - ), - allowNull: false, - }, - blockTimestamp: { - type: DataTypes.DATE, - allowNull: false, - }, - }, - { - sequelize, - schema: getSchema(), - tableName: 'AddressDriverSplitReceivers', - indexes: [ - { - fields: ['fundeeAccountId'], - name: `IX_AddressDriverSplitReceivers_fundeeAccountId`, - unique: false, - }, - { - fields: ['funderDripListId'], - name: `IX_AddressDriverSplitReceivers_funderDripListId`, - where: { - type: AddressDriverSplitReceiverType.DripListDependency, - }, - unique: false, - }, - { - fields: ['funderEcosystemMainAccountId'], - name: `IX_AddressDriverSplitReceivers_funderEcosystemId`, - where: { - type: AddressDriverSplitReceiverType.EcosystemDependency, - }, - unique: false, - }, - ], - }, - ); - } -} diff --git a/src/models/DripListModel.ts b/src/models/DripListModel.ts index 6d2be68..93ea353 100644 --- a/src/models/DripListModel.ts +++ b/src/models/DripListModel.ts @@ -1,90 +1,100 @@ import type { + CreationOptional, InferAttributes, InferCreationAttributes, Sequelize, } from 'sequelize'; import { DataTypes, Model } from 'sequelize'; -import type { AddressLike } from 'ethers'; import type { UUID } from 'crypto'; -import type { AccountId, NftDriverId } from '../core/types'; +import type { AccountId, Address, NftDriverId } from '../core/types'; import getSchema from '../utils/getSchema'; export default class DripListModel extends Model< InferAttributes, InferCreationAttributes > { - public declare id: NftDriverId; - public declare isValid: boolean; - public declare name: string | null; - public declare creator: AddressLike | null; - public declare description: string | null; - public declare ownerAddress: AddressLike | null; - public declare ownerAccountId: AccountId | null; - public declare previousOwnerAddress: AddressLike | null; - public declare latestVotingRoundId: UUID | null; - public declare isVisible: boolean | null; - public declare lastProcessedIpfsHash: string | null; + declare public accountId: NftDriverId; + declare public isValid: boolean; + declare public name: string | null; + declare public creator: Address | null; + declare public description: string | null; + declare public ownerAddress: Address | null; + declare public ownerAccountId: AccountId | null; + declare public previousOwnerAddress: Address | null; + declare public latestVotingRoundId: UUID | null; + declare public isVisible: boolean; + declare public lastProcessedIpfsHash: string; + declare public createdAt: CreationOptional; + declare public updatedAt: CreationOptional; public static initialize(sequelize: Sequelize): void { this.init( { - id: { - type: DataTypes.STRING, + accountId: { primaryKey: true, + type: DataTypes.STRING, }, isValid: { - type: DataTypes.BOOLEAN, allowNull: false, + type: DataTypes.BOOLEAN, }, ownerAddress: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, ownerAccountId: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, name: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, latestVotingRoundId: { - type: DataTypes.UUID, allowNull: true, + type: DataTypes.UUID, }, description: { - type: DataTypes.TEXT, allowNull: true, + type: DataTypes.TEXT, }, creator: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, previousOwnerAddress: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, isVisible: { + allowNull: false, type: DataTypes.BOOLEAN, - allowNull: true, }, lastProcessedIpfsHash: { + allowNull: false, type: DataTypes.TEXT, - allowNull: true, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, }, }, { sequelize, schema: getSchema(), - tableName: 'DripLists', + tableName: 'drip_lists', + timestamps: true, indexes: [ { fields: ['ownerAddress'], - name: `IX_DripLists_ownerAddress`, + name: `idx_drip_lists_owner_address`, where: { isValid: true, }, - unique: false, }, ], }, diff --git a/src/models/DripListSplitReceiverModel.ts b/src/models/DripListSplitReceiverModel.ts deleted file mode 100644 index eace747..0000000 --- a/src/models/DripListSplitReceiverModel.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { - CreationOptional, - InferAttributes, - InferCreationAttributes, - Sequelize, -} from 'sequelize'; -import { DataTypes, Model } from 'sequelize'; -import getSchema from '../utils/getSchema'; -import { DependencyType } from '../core/types'; -import type { - ImmutableSplitsDriverId, - NftDriverId, - RepoDriverId, -} from '../core/types'; -import DripListModel from './DripListModel'; -import ProjectModel from './ProjectModel'; -import EcosystemMainAccountModel from './EcosystemMainAccountModel'; -import SubListModel from './SubListModel'; - -export default class DripListSplitReceiverModel extends Model< - InferAttributes, - InferCreationAttributes -> { - public declare id: CreationOptional; // Primary key - public declare fundeeDripListId: NftDriverId; // Foreign key - public declare funderProjectId: RepoDriverId | null; // Foreign key - public declare funderDripListId: NftDriverId | null; // Foreign key - public declare funderEcosystemMainAccountId: NftDriverId | null; // Foreign key - public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key - - public declare weight: number; - public declare type: DependencyType; - public declare blockTimestamp: Date; - - public static initialize(sequelize: Sequelize): void { - this.init( - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - fundeeDripListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: DripListModel, - key: 'id', - }, - allowNull: false, - }, - funderProjectId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: ProjectModel, - key: 'id', - }, - allowNull: true, - }, - funderDripListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: DripListModel, - key: 'id', - }, - allowNull: true, - }, - funderEcosystemMainAccountId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: EcosystemMainAccountModel, - key: 'id', - }, - allowNull: true, - }, - funderSubListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: SubListModel, - key: 'id', - }, - allowNull: true, - }, - weight: { - type: DataTypes.INTEGER, - allowNull: true, - }, - type: { - type: DataTypes.ENUM(...Object.values(DependencyType)), - allowNull: false, - }, - blockTimestamp: { - type: DataTypes.DATE, - allowNull: false, - }, - }, - { - sequelize, - schema: getSchema(), - tableName: 'DripListSplitReceivers', - indexes: [ - { - fields: ['fundeeDripListId'], - name: `IX_DripListSplitReceivers_fundeeDripListId`, - unique: false, - }, - { - fields: ['funderProjectId'], - name: `IX_DripListSplitReceivers_funderProjectId`, - where: { - type: DependencyType.ProjectDependency, - }, - unique: false, - }, - { - fields: ['funderDripListId'], - name: `IX_DripListSplitReceivers_funderDripListId`, - where: { - type: DependencyType.DripListDependency, - }, - unique: false, - }, - { - fields: ['funderEcosystemMainAccountId'], - name: `IX_DripListSplitReceivers_funderEcosystemId`, - where: { - type: DependencyType.EcosystemDependency, - }, - unique: false, - }, - ], - }, - ); - } -} diff --git a/src/models/EcosystemMainAccountModel.ts b/src/models/EcosystemMainAccountModel.ts index 927d0db..498a6db 100644 --- a/src/models/EcosystemMainAccountModel.ts +++ b/src/models/EcosystemMainAccountModel.ts @@ -1,84 +1,94 @@ import type { + CreationOptional, InferAttributes, InferCreationAttributes, Sequelize, } from 'sequelize'; import { DataTypes, Model } from 'sequelize'; -import type { AddressLike } from 'ethers'; -import type { AccountId, NftDriverId } from '../core/types'; +import type { AccountId, Address, NftDriverId } from '../core/types'; import getSchema from '../utils/getSchema'; export default class EcosystemMainAccountModel extends Model< InferAttributes, InferCreationAttributes > { - public declare id: NftDriverId; - public declare isValid: boolean; - public declare name: string | null; - public declare creator: AddressLike | null; - public declare description: string | null; - public declare ownerAddress: AddressLike | null; - public declare ownerAccountId: AccountId | null; - public declare previousOwnerAddress: AddressLike | null; - public declare isVisible: boolean; - public declare lastProcessedIpfsHash: string | null; + declare public accountId: NftDriverId; + declare public isValid: boolean; + declare public name: string | null; + declare public creator: Address | null; + declare public description: string | null; + declare public ownerAddress: Address | null; + declare public ownerAccountId: AccountId | null; + declare public previousOwnerAddress: Address | null; + declare public isVisible: boolean; + declare public lastProcessedIpfsHash: string; + declare public createdAt: CreationOptional; + declare public updatedAt: CreationOptional; public static initialize(sequelize: Sequelize): void { this.init( { - id: { - type: DataTypes.STRING, + accountId: { primaryKey: true, + type: DataTypes.STRING, }, isValid: { - type: DataTypes.BOOLEAN, allowNull: false, + type: DataTypes.BOOLEAN, }, ownerAddress: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, ownerAccountId: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, name: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, description: { - type: DataTypes.TEXT, allowNull: true, + type: DataTypes.TEXT, }, creator: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, previousOwnerAddress: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, isVisible: { + allowNull: false, type: DataTypes.BOOLEAN, - allowNull: true, }, lastProcessedIpfsHash: { + allowNull: false, type: DataTypes.TEXT, - allowNull: true, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, }, }, { sequelize, schema: getSchema(), - tableName: 'EcosystemMainAccounts', + tableName: 'ecosystem_main_accounts', + timestamps: true, indexes: [ { fields: ['ownerAddress'], - name: `IX_Ecosystems_ownerAddress`, + name: `idx_ecosystem_main_accounts_owner_address`, where: { isValid: true, }, - unique: false, }, ], }, diff --git a/src/models/GivenEventModel.ts b/src/models/GivenEventModel.ts index 71e5045..ac762c1 100644 --- a/src/models/GivenEventModel.ts +++ b/src/models/GivenEventModel.ts @@ -16,16 +16,14 @@ export default class GivenEventModel > implements IEventModel { - public declare accountId: AccountId; // Sender of the Give - public declare receiver: AccountId; - public declare erc20: Address; - public declare amt: BigIntString; - - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; + declare public accountId: AccountId; // Sender of the Give + declare public receiver: AccountId; + declare public erc20: Address; + declare public amt: BigIntString; + declare public logIndex: number; + declare public blockNumber: number; + declare public blockTimestamp: Date; + declare public transactionHash: string; public static initialize(sequelize: Sequelize): void { this.init( diff --git a/src/models/OwnerUpdateRequestedEventModel.ts b/src/models/OwnerUpdateRequestedEventModel.ts deleted file mode 100644 index 9c7176d..0000000 --- a/src/models/OwnerUpdateRequestedEventModel.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { - InferAttributes, - InferCreationAttributes, - Sequelize, -} from 'sequelize'; -import { DataTypes, Model } from 'sequelize'; -import type { Forge, RepoDriverId } from '../core/types'; -import getSchema from '../utils/getSchema'; -import { COMMON_EVENT_INIT_ATTRIBUTES, FORGES_MAP } from '../core/constants'; -import type { IEventModel } from '../events/types'; - -export default class OwnerUpdateRequestedEventModel - extends Model< - InferAttributes, - InferCreationAttributes - > - implements IEventModel -{ - // Properties from event output. - public declare forge: Forge; - public declare name: string; - public declare accountId: RepoDriverId; - - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; - - public static initialize(sequelize: Sequelize): void { - this.init( - { - name: { - type: DataTypes.STRING, - allowNull: false, - }, - accountId: { - type: DataTypes.STRING, - allowNull: false, - }, - forge: { - type: DataTypes.ENUM(...Object.values(FORGES_MAP)), - allowNull: false, - }, - ...COMMON_EVENT_INIT_ATTRIBUTES, - }, - { - sequelize, - schema: getSchema(), - tableName: 'OwnerUpdateRequestedEvents', - }, - ); - } -} diff --git a/src/models/ProjectModel.ts b/src/models/ProjectModel.ts index d00b193..18870ec 100644 --- a/src/models/ProjectModel.ts +++ b/src/models/ProjectModel.ts @@ -1,4 +1,5 @@ import type { + CreationOptional, InferAttributes, InferCreationAttributes, Sequelize, @@ -6,157 +7,125 @@ import type { import { DataTypes, Model } from 'sequelize'; import type { AddressLike } from 'ethers'; import getSchema from '../utils/getSchema'; -import type { AccountId, Forge, RepoDriverId } from '../core/types'; -import { FORGES_MAP } from '../core/constants'; +import type { AddressDriverId, RepoDriverId } from '../core/types'; -export enum ProjectVerificationStatus { - Claimed = 'Claimed', - OwnerUpdateRequested = 'OwnerUpdateRequested', - OwnerUpdated = 'OwnerUpdated', - Unclaimed = 'Unclaimed', - PendingOwner = 'PendingOwner', - PendingMetadata = 'PendingMetadata', -} +export const PROJECT_VERIFICATION_STATUSES = [ + 'claimed', + 'owner_update_requested', + 'owner_updated', + 'unclaimed', + 'pending_owner', + 'pending_metadata', +] as const; +export type ProjectVerificationStatus = + (typeof PROJECT_VERIFICATION_STATUSES)[number]; + +export type ProjectName = `${string}/${string}`; + +export const FORGES = ['github', 'gitlab'] as const; +export type Forge = (typeof FORGES)[number]; export default class ProjectModel extends Model< InferAttributes, InferCreationAttributes > { - public declare id: RepoDriverId; // The `accountId` from `OwnerUpdatedRequested` event. - public declare isValid: boolean; - public declare name: string | null; - public declare forge: Forge | null; - public declare ownerAddress: AddressLike | null; - public declare ownerAccountId: AccountId | null; - - public declare url: string | null; - public declare emoji: string | null; - public declare avatarCid: string | null; - public declare color: string | null; - public declare description: string | null; - public declare verificationStatus: ProjectVerificationStatus; - public declare isVisible: boolean; - public declare lastProcessedIpfsHash: string | null; - - public declare claimedAt: Date | null; + declare public accountId: RepoDriverId; + declare public url: string; + declare public forge: Forge; + declare public emoji: string | null; + declare public color: string; + declare public name: ProjectName; + declare public avatarCid: string | null; + declare public verificationStatus: ProjectVerificationStatus; + declare public isVisible: boolean; + declare public lastProcessedIpfsHash: string; + declare public ownerAddress: AddressLike; + declare public ownerAccountId: AddressDriverId; + declare public claimedAt: Date; + declare public createdAt: CreationOptional; + declare public updatedAt: CreationOptional; public static initialize(sequelize: Sequelize): void { this.init( { - id: { - type: DataTypes.STRING, + accountId: { primaryKey: true, - }, - isValid: { - type: DataTypes.BOOLEAN, - allowNull: false, + type: DataTypes.STRING, }, name: { + allowNull: false, type: DataTypes.STRING, - allowNull: true, - validate: { - isValidName(value: string) { - if (!value) { - return; - } - - const components = value?.split('/'); - - if (components.length !== 2) { - throw new Error(`Invalid project name: '${value}'.`); - } - - const ownerName = components[0]; - const repoName = components[1]; - - const validProjectNameRegex: RegExp = /^[\w.-]+$/; - - if ( - !validProjectNameRegex.test(ownerName) || - !validProjectNameRegex.test(repoName) - ) { - throw new Error(`Invalid project name: '${value}'.`); - } - }, - }, }, verificationStatus: { - type: DataTypes.ENUM(...Object.values(ProjectVerificationStatus)), allowNull: false, - }, - claimedAt: { - type: DataTypes.DATE, - allowNull: true, + type: DataTypes.ENUM(...PROJECT_VERIFICATION_STATUSES), }, forge: { - type: DataTypes.ENUM(...Object.values(FORGES_MAP)), - allowNull: true, + allowNull: false, + type: DataTypes.ENUM(...FORGES), }, ownerAddress: { + allowNull: false, type: DataTypes.STRING, - allowNull: true, }, ownerAccountId: { + allowNull: false, type: DataTypes.STRING, - allowNull: true, }, url: { + allowNull: false, type: DataTypes.STRING, - allowNull: true, }, emoji: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, avatarCid: { - type: DataTypes.STRING, allowNull: true, + type: DataTypes.STRING, }, color: { + allowNull: false, type: DataTypes.STRING, - allowNull: true, - }, - description: { - type: DataTypes.TEXT, - allowNull: true, }, isVisible: { - type: DataTypes.BOOLEAN, allowNull: false, + type: DataTypes.BOOLEAN, }, lastProcessedIpfsHash: { + allowNull: false, type: DataTypes.TEXT, - allowNull: true, + }, + claimedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, }, }, { sequelize, schema: getSchema(), - tableName: 'GitProjects', + tableName: 'projects', + timestamps: true, indexes: [ { fields: ['ownerAddress'], - name: `IX_GitProjects_ownerAddress`, - unique: false, - where: { - isValid: true, - }, + name: 'idx_projects_owner_address', }, { fields: ['verificationStatus'], - name: `IX_GitProjects_verificationStatus`, - where: { - isValid: true, - }, - unique: false, + name: 'idx_projects_verification_status', }, { fields: ['url'], - name: `IX_GitProjects_url`, - where: { - isValid: true, - }, - unique: false, + name: 'idx_projects_url', }, ], }, diff --git a/src/models/RepoDriverSplitReceiverModel.ts b/src/models/RepoDriverSplitReceiverModel.ts deleted file mode 100644 index 30c4632..0000000 --- a/src/models/RepoDriverSplitReceiverModel.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { - CreationOptional, - InferAttributes, - InferCreationAttributes, - Sequelize, -} from 'sequelize'; -import { DataTypes, Model } from 'sequelize'; -import getSchema from '../utils/getSchema'; -import ProjectModel from './ProjectModel'; -import { DependencyType } from '../core/types'; -import type { - ImmutableSplitsDriverId, - NftDriverId, - RepoDriverId, -} from '../core/types'; -import DripListModel from './DripListModel'; -import EcosystemMainAccountModel from './EcosystemMainAccountModel'; -import SubListModel from './SubListModel'; - -export default class RepoDriverSplitReceiverModel extends Model< - InferAttributes, - InferCreationAttributes -> { - public declare id: CreationOptional; // Primary key - public declare fundeeProjectId: RepoDriverId; // Foreign key - public declare funderProjectId: RepoDriverId | null; // Foreign key - public declare funderDripListId: NftDriverId | null; // Foreign key - public declare funderEcosystemMainAccountId: NftDriverId | null; // Foreign key - public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key - - public declare weight: number; - public declare type: DependencyType; - public declare blockTimestamp: Date; - - public static initialize(sequelize: Sequelize): void { - this.init( - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - fundeeProjectId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: ProjectModel, - key: 'id', - }, - allowNull: false, - }, - funderProjectId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: ProjectModel, - key: 'id', - }, - allowNull: true, - }, - funderDripListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: DripListModel, - key: 'id', - }, - allowNull: true, - }, - funderEcosystemMainAccountId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: EcosystemMainAccountModel, - key: 'id', - }, - allowNull: true, - }, - funderSubListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: SubListModel, - key: 'id', - }, - allowNull: true, - }, - weight: { - type: DataTypes.INTEGER, - allowNull: true, - }, - type: { - type: DataTypes.ENUM(...Object.values(DependencyType)), - allowNull: false, - }, - blockTimestamp: { - type: DataTypes.DATE, - allowNull: false, - }, - }, - { - sequelize, - schema: getSchema(), - tableName: 'RepoDriverSplitReceivers', - indexes: [ - { - fields: ['fundeeProjectId'], - name: `IX_RepoDriverSplitReceivers_fundeeProjectId`, - unique: false, - }, - { - fields: ['funderProjectId'], - name: `IX_RepoDriverSplitReceivers_funderProjectId`, - where: { - type: DependencyType.ProjectDependency, - }, - unique: false, - }, - { - fields: ['funderDripListId'], - name: `IX_RepoDriverSplitReceivers_funderDripListId`, - where: { - type: DependencyType.DripListDependency, - }, - unique: false, - }, - { - fields: ['funderEcosystemMainAccountId'], - name: `IX_RepoDriverSplitReceivers_funderEcosystemId`, - where: { - type: DependencyType.EcosystemDependency, - }, - unique: false, - }, - ], - }, - ); - } -} diff --git a/src/models/SplitReceiver.ts b/src/models/SplitReceiver.ts new file mode 100644 index 0000000..0f4dc29 --- /dev/null +++ b/src/models/SplitReceiver.ts @@ -0,0 +1,95 @@ +import type { + CreationOptional, + InferAttributes, + InferCreationAttributes, + Sequelize, +} from 'sequelize'; +import { DataTypes, Model } from 'sequelize'; +import getSchema from '../utils/getSchema'; +import type { AccountId } from '../core/types'; +import { + type RelationshipType, + type AccountType, + ACCOUNT_TYPES, + RELATIONSHIP_TYPES, +} from '../core/splitRules'; + +export default class SplitReceiver extends Model< + InferAttributes, + InferCreationAttributes +> { + declare public id: CreationOptional; + declare public receiverAccountId: AccountId; + declare public receiverAccountType: AccountType; + declare public senderAccountId: AccountId; + declare public senderAccountType: AccountType; + declare public relationshipType: RelationshipType; + declare public weight: number; + declare public blockTimestamp: Date; + declare public createdAt: CreationOptional; + declare public updatedAt: CreationOptional; + + public static initialize(sequelize: Sequelize): void { + this.init( + { + id: { + primaryKey: true, + autoIncrement: true, + type: DataTypes.INTEGER, + }, + receiverAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiverAccountType: { + allowNull: false, + type: DataTypes.ENUM(...ACCOUNT_TYPES), + }, + senderAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + senderAccountType: { + allowNull: false, + type: DataTypes.ENUM(...ACCOUNT_TYPES), + }, + relationshipType: { + allowNull: false, + type: DataTypes.ENUM(...RELATIONSHIP_TYPES), + }, + weight: { + type: DataTypes.INTEGER, + allowNull: false, + }, + blockTimestamp: { + type: DataTypes.DATE, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + }, + }, + { + sequelize, + schema: getSchema(), + tableName: 'split_receivers', + timestamps: true, + indexes: [ + { + fields: ['receiverAccountId', 'senderAccountId'], + name: 'idx_split_receivers_receiver_sender', + }, + { + fields: ['senderAccountId', 'receiverAccountId'], + name: 'idx_split_receivers_sender_receiver', + }, + ], + }, + ); + } +} diff --git a/src/models/SubListModel.ts b/src/models/SubListModel.ts index 3bdec02..82eecc4 100644 --- a/src/models/SubListModel.ts +++ b/src/models/SubListModel.ts @@ -1,115 +1,80 @@ import type { + CreationOptional, InferAttributes, InferCreationAttributes, Sequelize, } from 'sequelize'; import { DataTypes, Model } from 'sequelize'; -import type { NftDriverId, ImmutableSplitsDriverId } from '../core/types'; +import type { ImmutableSplitsDriverId, AccountId } from '../core/types'; import getSchema from '../utils/getSchema'; +import type { AccountType } from '../core/splitRules'; export default class SubListModel extends Model< InferAttributes, InferCreationAttributes > { - public declare id: ImmutableSplitsDriverId; - public declare parentDripListId: NftDriverId | null; - public declare parentEcosystemMainAccountId: NftDriverId | null; - public declare parentSubListId: ImmutableSplitsDriverId | null; - public declare rootDripListId: NftDriverId | null; - public declare rootEcosystemMainAccountId: NftDriverId | null; - public declare lastProcessedIpfsHash: string | null; - public declare isValid: boolean; + declare public accountId: ImmutableSplitsDriverId; + declare public parentId: AccountId; + declare public parentAccountType: AccountType; + declare public rootId: AccountId; + declare public rootAccountType: AccountType; + declare public lastProcessedIpfsHash: string; + declare public createdAt: CreationOptional; + declare public updatedAt: CreationOptional; public static initialize(sequelize: Sequelize): void { this.init( { - id: { + accountId: { + primaryKey: false, type: DataTypes.STRING, - primaryKey: true, }, - parentDripListId: { - // Foreign key - type: DataTypes.STRING, - allowNull: true, - references: { - model: 'DripLists', - key: 'id', - }, - }, - parentEcosystemMainAccountId: { - // Foreign key + parentId: { + allowNull: false, type: DataTypes.STRING, - allowNull: true, - references: { - model: 'EcosystemMainAccounts', - key: 'id', - }, }, - parentSubListId: { - // Foreign key + parentAccountType: { + allowNull: false, type: DataTypes.STRING, - allowNull: true, - references: { - model: 'SubLists', - key: 'id', - }, }, - rootDripListId: { - // Foreign key + rootId: { + allowNull: false, type: DataTypes.STRING, - allowNull: true, - references: { - model: 'DripLists', - key: 'id', - }, }, - rootEcosystemMainAccountId: { - // Foreign key + rootAccountType: { type: DataTypes.STRING, - allowNull: true, - references: { - model: 'EcosystemMainAccounts', - key: 'id', - }, + allowNull: false, }, lastProcessedIpfsHash: { type: DataTypes.TEXT, - allowNull: true, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, }, - isValid: { - type: DataTypes.BOOLEAN, + updatedAt: { allowNull: false, + type: DataTypes.DATE, }, }, { sequelize, schema: getSchema(), - tableName: 'SubLists', + tableName: 'sub_lists', + timestamps: true, indexes: [ { - fields: ['parentDripListId'], - name: `IX_SubLists_parentDripListId`, - unique: false, - }, - { - fields: ['parentEcosystemMainAccountId'], - name: `IX_SubLists_parentEcosystemId`, - unique: false, - }, - { - fields: ['parentSubListId'], - name: `IX_SubLists_parentSubListId`, - unique: false, + fields: ['accountId'], + name: 'idx_sub_lists_account_id', }, { - fields: ['rootDripListId'], - name: `IX_SubLists_rootDripListId`, - unique: false, + fields: ['parentId'], + name: 'idx_sub_lists_parent_id', }, { - fields: ['rootEcosystemMainAccountId'], - name: `IX_SubLists_rootEcosystemId`, - unique: false, + fields: ['rootId'], + name: 'idx_sub_lists_root_id', }, ], }, diff --git a/src/models/SubListSplitReceiverModel.ts b/src/models/SubListSplitReceiverModel.ts deleted file mode 100644 index 94f374a..0000000 --- a/src/models/SubListSplitReceiverModel.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { - CreationOptional, - InferAttributes, - InferCreationAttributes, - Sequelize, -} from 'sequelize'; -import { DataTypes, Model } from 'sequelize'; -import getSchema from '../utils/getSchema'; -import { DependencyType } from '../core/types'; -import type { - ImmutableSplitsDriverId, - NftDriverId, - RepoDriverId, -} from '../core/types'; -import DripListModel from './DripListModel'; -import ProjectModel from './ProjectModel'; -import SubListModel from './SubListModel'; -import EcosystemMainAccountModel from './EcosystemMainAccountModel'; - -export default class SubListSplitReceiverModel extends Model< - InferAttributes, - InferCreationAttributes -> { - public declare id: CreationOptional; // Primary key - public declare fundeeSubListId: ImmutableSplitsDriverId; // Foreign key - public declare funderProjectId: RepoDriverId | null; // Foreign key - public declare funderDripListId: NftDriverId | null; // Foreign key - public declare funderEcosystemMainAccountId: NftDriverId | null; // Foreign key - public declare funderSubListId: ImmutableSplitsDriverId | null; // Foreign key - - public declare weight: number; - public declare type: DependencyType; - public declare blockTimestamp: Date; - - public static initialize(sequelize: Sequelize): void { - this.init( - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - fundeeSubListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: SubListModel, - key: 'id', - }, - allowNull: false, - }, - funderProjectId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: ProjectModel, - key: 'id', - }, - allowNull: true, - }, - funderDripListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: DripListModel, - key: 'id', - }, - allowNull: true, - }, - funderEcosystemMainAccountId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: EcosystemMainAccountModel, - key: 'id', - }, - allowNull: true, - }, - funderSubListId: { - // Foreign key - type: DataTypes.STRING, - references: { - model: EcosystemMainAccountModel, - key: 'id', - }, - allowNull: true, - }, - weight: { - type: DataTypes.INTEGER, - allowNull: true, - }, - type: { - type: DataTypes.ENUM(...Object.values(DependencyType)), - allowNull: false, - }, - blockTimestamp: { - type: DataTypes.DATE, - allowNull: false, - }, - }, - { - sequelize, - schema: getSchema(), - tableName: 'SubListSplitReceivers', - indexes: [ - { - fields: ['fundeeSubListId'], - name: `IX_SubListSplitReceivers_fundeeImmutableSplitsId`, - unique: false, - }, - { - fields: ['funderProjectId'], - name: `IX_SubListSplitReceivers_funderProjectId`, - where: { - type: DependencyType.ProjectDependency, - }, - unique: false, - }, - { - fields: ['funderDripListId'], - name: `IX_SubListSplitReceivers_funderDripListId`, - where: { - type: DependencyType.DripListDependency, - }, - unique: false, - }, - { - fields: ['funderEcosystemMainAccountId'], - name: `IX_SubListSplitReceivers_funderEcosystemId`, - where: { - type: DependencyType.EcosystemDependency, - }, - unique: false, - }, - ], - }, - ); - } -} diff --git a/src/models/TransferEventModel.ts b/src/models/TransferEventModel.ts index f20a601..73d5e09 100644 --- a/src/models/TransferEventModel.ts +++ b/src/models/TransferEventModel.ts @@ -1,11 +1,10 @@ -import type { AddressLike } from 'ethers'; import type { InferAttributes, InferCreationAttributes, Sequelize, } from 'sequelize'; import { DataTypes, Model } from 'sequelize'; -import type { NftDriverId } from '../core/types'; +import type { Address, NftDriverId } from '../core/types'; import getSchema from '../utils/getSchema'; import { COMMON_EVENT_INIT_ATTRIBUTES } from '../core/constants'; import type { IEventModel } from '../events/types'; @@ -17,37 +16,35 @@ export default class TransferEventModel > implements IEventModel { - public declare tokenId: NftDriverId; // The `tokenId` from `Transfer` event. - public declare from: AddressLike; - public declare to: AddressLike; - - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; + declare public tokenId: NftDriverId; // The `tokenId` from `Transfer` event. + declare public from: Address; + declare public to: Address; + declare public logIndex: number; + declare public blockNumber: number; + declare public blockTimestamp: Date; + declare public transactionHash: string; public static initialize(sequelize: Sequelize): void { this.init( { tokenId: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, from: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, to: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, ...COMMON_EVENT_INIT_ATTRIBUTES, }, { sequelize, schema: getSchema(), - tableName: 'TransferEvents', + tableName: 'transfer_events', }, ); } diff --git a/src/models/_LastIndexedBlockModel.ts b/src/models/_LastIndexedBlockModel.ts index 144a92f..e002e65 100644 --- a/src/models/_LastIndexedBlockModel.ts +++ b/src/models/_LastIndexedBlockModel.ts @@ -1,4 +1,5 @@ import type { + CreationOptional, InferAttributes, InferCreationAttributes, Sequelize, @@ -10,27 +11,37 @@ export default class _LastIndexedBlockModel extends Model< InferAttributes<_LastIndexedBlockModel>, InferCreationAttributes<_LastIndexedBlockModel> > { - public declare blockNumber: bigint; - public declare id: number; + declare public blockNumber: bigint; + declare public id: number; + declare public createdAt: CreationOptional; + declare public updatedAt: CreationOptional; public static initialize(sequelize: Sequelize): void { this.init( { id: { - type: DataTypes.INTEGER, - autoIncrement: true, primaryKey: true, + autoIncrement: true, + type: DataTypes.INTEGER, }, blockNumber: { + unique: true, + allowNull: false, type: DataTypes.BIGINT, + }, + createdAt: { allowNull: false, - unique: true, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, }, }, { sequelize, schema: getSchema(), - tableName: '_LastIndexedBlock', + tableName: '_last_indexed_block', }, ); } diff --git a/src/models/index.ts b/src/models/index.ts index 42c4b5d..1466390 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,16 +1,13 @@ export { default as SubListModel } from './SubListModel'; +export { default as ProjectModel } from './ProjectModel'; +export { default as SplitReceiver } from './SplitReceiver'; export { default as DripListModel } from './DripListModel'; export { default as GivenEventModel } from './GivenEventModel'; export { default as SplitEventModel } from './SplitEventModel'; -export { default as Project } from './Project'; -export { default as EcosystemMainAccountModel } from './EcosystemMainAccountModel'; export { default as TransferEventModel } from './TransferEventModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; export { default as _LastIndexedBlockModel } from './_LastIndexedBlockModel'; export { default as SqueezedStreamsEventModel } from './SqueezedStreamsEventModel'; -export { default as SubListSplitReceiverModel } from './SubListSplitReceiverModel'; -export { default as DripListSplitReceiverModel } from './DripListSplitReceiverModel'; -export { default as RepoDriverSplitReceiverModel } from './RepoDriverSplitReceiverModel'; +export { default as EcosystemMainAccountModel } from './EcosystemMainAccountModel'; export { default as StreamReceiverSeenEventModel } from './StreamReceiverSeenEventModel'; -export { default as AddressDriverSplitReceiverModel } from './AddressDriverSplitReceiverModel'; export { default as AccountMetadataEmittedEventModel } from './AccountMetadataEmittedEventModel'; diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index 67f86e5..ae2ea67 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -1,13 +1,45 @@ -import type { AddressLike } from 'ethers'; +/* eslint-disable no-bitwise */ +import { ethers, type AddressLike } from 'ethers'; import type { AccountId, AddressDriverId, + DripsContract, ImmutableSplitsDriverId, NftDriverId, RepoDriverId, } from '../core/types'; import { addressDriverContract } from '../core/contractClients'; -import { getContractNameFromAccountId } from './contractUtils'; + +export function getContractNameFromAccountId(id: string): DripsContract { + if (Number.isNaN(Number(id))) { + throw new Error(`Could not get bits: ${id} is not a number.`); + } + + const accountIdAsBigInt = BigInt(id); + + if (accountIdAsBigInt < 0n || accountIdAsBigInt > 2n ** 256n - 1n) { + throw new Error( + `Could not get bits: ${id} is not a valid positive number within the range of a uint256.`, + ); + } + + const mask = 2n ** 32n - 1n; // 32 bits mask + + const bits = (accountIdAsBigInt >> 224n) & mask; // eslint-disable-line no-bitwise + + switch (bits) { + case 0n: + return 'addressDriver'; + case 1n: + return 'nftDriver'; + case 2n: + return 'immutableSplitsDriver'; + case 3n: + return 'repoDriver'; + default: + throw new Error(`Unknown driver for ID ${id}.`); + } +} // RepoDriver export function isRepoDriverId(id: string | bigint): id is RepoDriverId { @@ -33,6 +65,12 @@ export function convertToRepoDriverId(id: bigint | string): RepoDriverId { return repoDriverId as RepoDriverId; } +export function assertIsRepoDriverId(id: string): asserts id is RepoDriverId { + if (!isRepoDriverId(id)) { + throw new Error(`Failed to assert: '${id}' is not a valid RepoDriver ID.`); + } +} + // NftDriver export function isNftDriverId(id: string | bigint): id is NftDriverId { const idStr = typeof id === 'bigint' ? id.toString() : id; @@ -57,6 +95,12 @@ export function convertToNftDriverId(id: bigint | string): NftDriverId { return nftDriverId as NftDriverId; } +export function assertIsNftDriverId(id: string): asserts id is NftDriverId { + if (!isNftDriverId(id)) { + throw new Error(`Failed to assert: '${id}' is not a valid NftDriver ID.`); + } +} + // AddressDriver export function isAddressDriverId( idString: string, @@ -83,7 +127,7 @@ export function convertToAddressDriverId(id: string): AddressDriverId { return id as AddressDriverId; } -export function assertAddressDiverId( +export function assertIsAddressDiverId( id: string, ): asserts id is AddressDriverId { if (!isAddressDriverId(id)) { @@ -123,6 +167,16 @@ export function convertToImmutableSplitsDriverId( return stringId as ImmutableSplitsDriverId; } +export function assertIsImmutableSplitsDriverId( + id: string, +): asserts id is ImmutableSplitsDriverId { + if (!isImmutableSplitsDriverId(id)) { + throw new Error( + `Failed to assert: '${id}' is not a valid ImmutableSplitsDriver ID.`, + ); + } +} + export async function calcAccountId(owner: AddressLike): Promise { return ( await addressDriverContract.calcAccountId(owner as string) @@ -131,15 +185,15 @@ export async function calcAccountId(owner: AddressLike): Promise { // Account ID export function convertToAccountId(id: bigint | string): AccountId { - const accountidString = typeof id === 'bigint' ? id.toString() : id; + const accountIdAsString = typeof id === 'bigint' ? id.toString() : id; if ( - isRepoDriverId(accountidString) || - isNftDriverId(accountidString) || - isAddressDriverId(accountidString) || - isImmutableSplitsDriverId(accountidString) + isRepoDriverId(accountIdAsString) || + isNftDriverId(accountIdAsString) || + isAddressDriverId(accountIdAsString) || + isImmutableSplitsDriverId(accountIdAsString) ) { - return accountidString as AccountId; + return accountIdAsString as AccountId; } throw new Error(`Failed to convert: '${id}' is not a valid account ID.`); @@ -161,3 +215,39 @@ export function assertIsAccountId( ); } } + +export function getAddress(accountId: string): AddressLike { + let accountIdBigInt: bigint; + + try { + accountIdBigInt = BigInt(accountId); + } catch { + throw new Error( + `Failed to get address: '${accountId}' is not a valid bigint string.`, + ); + } + + if (accountIdBigInt < 0n || accountIdBigInt > 2n ** 256n - 1n) { + throw new Error( + `Failed to get address: '${accountId}' is not a valid positive number within the range of a uint256.`, + ); + } + + if (getContractNameFromAccountId(accountId) !== 'addressDriver') { + // Mid 64 bits after first 32 (128-191) must be zero + const mid64Mask = ((1n << 64n) - 1n) << 160n; + + if ((accountIdBigInt & mid64Mask) !== 0n) { + throw new Error( + `Failed to get address: '${accountId}' is not a valid AddressDriver ID. The first 64 (after first 32) bits must be 0.`, + ); + } + } + + const addressMask = (1n << 160n) - 1n; + const addressBigInt = accountIdBigInt & addressMask; + + // Convert to hex, pad to 20 bytes (40 hex chars), and checksum it + const hex = `0x${addressBigInt.toString(16).padStart(40, '0')}`; + return ethers.getAddress(hex); +} diff --git a/src/utils/contractUtils.ts b/src/utils/contractUtils.ts deleted file mode 100644 index dfb293e..0000000 --- a/src/utils/contractUtils.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { DripsContract } from '../core/types'; - -export function getContractNameFromAccountId(id: string): DripsContract { - if (Number.isNaN(Number(id))) { - throw new Error(`Could not get bits: ${id} is not a number.`); - } - - const accountIdAsBigInt = BigInt(id); - - if (accountIdAsBigInt < 0n || accountIdAsBigInt > 2n ** 256n - 1n) { - throw new Error( - `Could not get bits: ${id} is not a valid positive number within the range of a uint256.`, - ); - } - - const mask = 2n ** 32n - 1n; // 32 bits mask - - const bits = (accountIdAsBigInt >> 224n) & mask; // eslint-disable-line no-bitwise - - switch (bits) { - case 0n: - return 'addressDriver'; - case 1n: - return 'nftDriver'; - case 2n: - return 'immutableSplitsDriver'; - case 3n: - return 'repoDriver'; - default: - throw new Error(`Unknown driver for ID ${id}.`); - } -} diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts index f85dbf5..a311ff2 100644 --- a/src/utils/metadataUtils.ts +++ b/src/utils/metadataUtils.ts @@ -1,4 +1,4 @@ -import { ethers } from 'ethers'; +import { toUtf8String } from 'ethers'; import type { AnyVersion } from '@efstajas/versioned-parser'; import type { IpfsHash } from '../core/types'; import { @@ -9,7 +9,7 @@ import { import appSettings from '../config/appSettings'; export function convertToIpfsHash(str: string): IpfsHash { - const ipfsHash = ethers.toUtf8String(str); + const ipfsHash = toUtf8String(str); const isIpfsHash = /^(Qm[a-zA-Z0-9]{44})$/.test(ipfsHash); diff --git a/src/utils/projectUtils.ts b/src/utils/projectUtils.ts index a03b09c..af6f71c 100644 --- a/src/utils/projectUtils.ts +++ b/src/utils/projectUtils.ts @@ -1,76 +1,65 @@ -import type { AddressLike } from 'ethers'; -import { ethers } from 'ethers'; +import { hexlify, toUtf8Bytes, ZeroAddress, type AddressLike } from 'ethers'; import type { z } from 'zod'; -import { FORGES_MAP } from '../core/constants'; -import type { Forge } from '../core/types'; import unreachableError from './unreachableError'; -import { ProjectVerificationStatus } from '../models/ProjectModel'; -import type { sourceSchema } from '../metadata/schemas/common/sources'; +import type { Forge, ProjectVerificationStatus } from '../models/ProjectModel'; +import { repoDriverContract } from '../core/contractClients'; +import type { repoDriverSplitReceiverSchema } from '../metadata/schemas/repo-driver/v2'; -export function toProjectOwnerAddress(address: string): AddressLike { - if (!ethers.isAddress(address)) { - throw new Error(`Invalid owner address: ${address}.`); - } - - return address as AddressLike; -} - -export function toUrl(forge: Forge, projectName: string): string { +export function convertForgeToNumber(forge: Forge) { switch (forge) { - case 'GitHub': - return `https://github.com/${projectName}`; + case 'github': + return 0; + case 'gitlab': + return 1; default: - throw new Error(`Unsupported forge: ${forge}.`); + return unreachableError( + `Failed to convert: '${forge}' is not a valid forge.`, + ); } } -export function toForge(forge: bigint): Forge { - const forgeAsString = FORGES_MAP[Number(forge) as keyof typeof FORGES_MAP]; - - if (!forgeAsString) { - throw new Error(`Invalid forge value: ${forge}.`); +export function calculateProjectStatus( + owner: AddressLike | null, +): ProjectVerificationStatus { + if (!owner || owner === ZeroAddress) { + return 'unclaimed'; } - return forgeAsString; + return 'claimed'; } -export const METADATA_FORGE_MAP: Record< - z.infer['forge'], - Forge -> = { - github: 'GitHub', -}; +export async function verifyProjectSources( + projectReceivers: z.infer[], +): Promise<{ + areProjectsValid: boolean; + message?: string; +}> { + const errors: string[] = []; -export function toReadable(bytes: string): string { - return ethers.toUtf8String(bytes); -} - -export function calculateProjectStatus(project: { - id: string; - color: string | null; - ownerAddress: AddressLike | null; -}): ProjectVerificationStatus { - if (!project.ownerAddress && !project.color) { - return ProjectVerificationStatus.Unclaimed; - } - - if (project.ownerAddress && project.color) { - return ProjectVerificationStatus.Claimed; - } + for (const { + accountId, + source: { forge, ownerName, repoName }, + } of projectReceivers) { + const calculatedAccountId = await repoDriverContract.calcAccountId( + convertForgeToNumber(forge), + hexlify(toUtf8Bytes(`${ownerName}/${repoName}`)), + ); - if (project.ownerAddress && !project.color) { - return ProjectVerificationStatus.PendingMetadata; + if (calculatedAccountId.toString() !== accountId) { + errors.push( + `Mismatch for '${ownerName}/${repoName}' on '${forge}': expected '${accountId}', got '${calculatedAccountId}'.`, + ); + } } - if (!project.ownerAddress && project.color) { - return ProjectVerificationStatus.PendingOwner; + if (errors.length > 0) { + return { + areProjectsValid: false, + message: `Failed to verify project sources:\n${errors.join('\n')}`, + }; } - return unreachableError( - `Project with ID ${project.id} has an invalid status.\n` + - ` Project:\n${JSON.stringify(project, null, 2) - .split('\n') - .map((line) => ` ${line}`) - .join('\n')}`, - ); + return { + areProjectsValid: true, + }; } diff --git a/src/utils/retryOperation.ts b/src/utils/retryOperation.ts deleted file mode 100644 index 1d2456d..0000000 --- a/src/utils/retryOperation.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { Result } from '../core/types'; - -async function sleep(milliseconds: number): Promise { - return new Promise((resolve) => setTimeout(resolve, milliseconds)); // eslint-disable-line no-promise-executor-return -} - -type RetryError = { - message: string; - errors: string[]; - attempts: number; -}; - -export function isRetryError(err: unknown): err is RetryError { - return ( - typeof err === 'object' && - err !== null && - 'message' in err && - 'errors' in err && - 'attempts' in err - ); -} - -export default async function retryOperation( - operation: () => T | Promise, - maxRetries: number = 3, -): Promise> { - let attempts = 0; - const baseDelay = 100; - const errors: string[] = []; - - while (attempts < maxRetries) { - attempts += 1; - - try { - const result = await Promise.resolve(operation()); - return { - ok: true, - value: result, - }; - } catch (error: any) { - errors.push(error.message); - - if (attempts < maxRetries) { - const delay = baseDelay * 2 ** attempts; - await sleep(delay); - } - } - } - - return { - ok: false, - error: { - message: errors[errors.length - 1], - errors, - attempts, - }, - }; -} diff --git a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts index 8753444..1847392 100644 --- a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts +++ b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts @@ -15,7 +15,7 @@ import { } from '../../src/utils/metadataUtils'; import * as handleDripListMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata'; -jest.mock('../../src/models/AccountMetadataEmittedEventModel'); +jest.mock('../../src/models/AccountMetadataEmittedEvent'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); jest.mock('../../src/core/LogManager'); @@ -106,7 +106,7 @@ describe('AccountMetadataEmittedHandler', () => { expect(dbConnection.transaction).not.toHaveBeenCalled(); }); - test('should create a new AccountMetadataEmittedEventModel', async () => { + test('should create a new AccountMetadataEmittedEvent', async () => { // Arrange AccountMetadataEmittedEventModel.create = jest.fn().mockResolvedValue([ { diff --git a/tests/eventHandlers/TransferEventHandler.test.ts b/tests/eventHandlers/TransferEventHandler.test.ts index 748feb6..ad984e7 100644 --- a/tests/eventHandlers/TransferEventHandler.test.ts +++ b/tests/eventHandlers/TransferEventHandler.test.ts @@ -10,7 +10,7 @@ import TransferEventModel from '../../src/models/TransferEventModel'; import DripListModel from '../../src/models/DripListModel'; jest.mock('../../src/models/TransferEventModel'); -jest.mock('../../src/models/DripListModel'); +jest.mock('../../src/models/DripList'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); jest.mock('../../src/events/eventHandlerUtils'); From 751065badcfacf8f30417e9c1a65b0fc9de82a49 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 22 Apr 2025 10:39:38 +0200 Subject: [PATCH 27/60] feat: change naming convention in db --- scripts/run-migrations.ts | 3 + src/core/LogManager.ts | 32 +- src/core/types.ts | 1 - src/db/database.ts | 1 - .../20250414133746-initial_create.ts | 1175 ++++++++++++----- src/db/modelRegistration.ts | 4 +- .../AccountMetadataEmittedEventHandler.ts | 24 +- .../handlers/handleDripListMetadata.ts | 9 +- .../handleEcosystemMainAccountMetadata.ts | 3 +- .../handlers/handleProjectMetadata.ts | 9 +- .../handlers/handleSubListMetadata.ts | 9 +- .../receiversRepository.ts | 10 +- .../StreamReceiverSeenEventHandler.ts | 26 +- src/eventHandlers/TransferEventHandler.ts | 4 +- src/events/EventHandlerBase.ts | 13 +- src/events/poll.ts | 2 +- .../AccountMetadataEmittedEventModel.ts | 2 + src/models/DripListModel.ts | 1 + src/models/EcosystemMainAccountModel.ts | 1 + src/models/GivenEventModel.ts | 24 +- src/models/ProjectModel.ts | 1 + src/models/SplitEventModel.ts | 35 +- ...SplitReceiver.ts => SplitReceiverModel.ts} | 7 +- src/models/SqueezedStreamsEventModel.ts | 35 +- src/models/StreamReceiverSeenEventModel.ts | 28 +- src/models/StreamsSetEventModel.ts | 42 +- src/models/SubListModel.ts | 1 + src/models/TransferEventModel.ts | 2 + src/models/_LastIndexedBlockModel.ts | 1 + src/models/index.ts | 2 +- src/queue/initJobProcessingQueue.ts | 5 +- src/queue/queue.ts | 10 +- src/utils/isLatestEvent.ts | 4 +- 33 files changed, 1030 insertions(+), 496 deletions(-) rename src/models/{SplitReceiver.ts => SplitReceiverModel.ts} (93%) diff --git a/scripts/run-migrations.ts b/scripts/run-migrations.ts index 69bb89f..f6c3661 100644 --- a/scripts/run-migrations.ts +++ b/scripts/run-migrations.ts @@ -15,6 +15,9 @@ export async function runMigrations(): Promise { const sequelize = new Sequelize(connectionString, { dialect: 'postgres', logging: false, + define: { + underscored: true, + }, }); const schema = getSchema(); diff --git a/src/core/LogManager.ts b/src/core/LogManager.ts index 1c3eba6..bd3cc6b 100644 --- a/src/core/LogManager.ts +++ b/src/core/LogManager.ts @@ -74,26 +74,20 @@ export default class LogManager { instance: T, type: { new (): T }, id: string, - wasCreated: boolean, ): this { - const action = wasCreated ? 'Created new' : 'Upserted existing'; - const baseMessage = `${action} ${LogManager.nameOfType(type)} with ID ${id}.`; - - if (wasCreated) { - this._logs.push(baseMessage); - } else { - const changes = LogManager.getChangedProperties(instance); - - const formattedChanges = - changes && Object.keys(changes).length > 0 - ? `\n\tChanged properties:\n${JSON.stringify(changes, null, 2) - .split('\n') - .map((line) => `\t ${line}`) - .join('\n')}` - : `\n\tNo changes detected.`; - - this._logs.push(`${baseMessage}${formattedChanges}`); - } + const changes = LogManager.getChangedProperties(instance); + + const formattedChanges = + changes && Object.keys(changes).length > 0 + ? `\n\tChanged properties:\n${JSON.stringify(changes, null, 2) + .split('\n') + .map((line) => `\t ${line}`) + .join('\n')}` + : `\n\tNo changes detected.`; + + this._logs.push( + `Upserted ${LogManager.nameOfType(type)} with ID ${id}${formattedChanges}`, + ); return this; } diff --git a/src/core/types.ts b/src/core/types.ts index c8a3979..cd3d3eb 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -57,7 +57,6 @@ export type ChainConfig = { export type ModelStaticMembers = { new (): Model; initialize(sequelize: Sequelize): void; - defineAssociations?(): void; }; export type StreamHistoryHashes = string & { diff --git a/src/db/database.ts b/src/db/database.ts index d630904..acdf60e 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -30,7 +30,6 @@ export async function connectToDb(): Promise { async function initializeEntities(): Promise { logger.info('Initializing database schema...'); - getRegisteredModels().map((Model) => Model.defineAssociations?.()); getRegisteredModels().map((Model) => Model.initialize(dbConnection)); logger.info('Database schema initialized.'); diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index 9213190..784d117 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -7,6 +7,10 @@ export async function up({ context: sequelize }: any): Promise { const schema = getSchema(); const queryInterface: QueryInterface = sequelize.getQueryInterface(); + await queryInterface.sequelize.query( + `CREATE SCHEMA IF NOT EXISTS ${schema};`, + ); + await queryInterface.sequelize.query(` CREATE TYPE ${schema}.account_type AS ENUM ( 'project', @@ -24,7 +28,7 @@ export async function up({ context: sequelize }: any): Promise { 'project_maintainer', 'drip_list_receiver', 'ecosystem_receiver', - 'sub_list_link', + 'sub_list_link' ); `); @@ -35,14 +39,14 @@ export async function up({ context: sequelize }: any): Promise { 'owner_updated', 'unclaimed', 'pending_owner', - 'pending_metadata', + 'pending_metadata' ); `); await queryInterface.sequelize.query(` CREATE TYPE ${schema}.forges AS ENUM ( - 'gitHub', - 'gitLab', + 'github', + 'gitlab' ); `); @@ -54,210 +58,612 @@ export async function up({ context: sequelize }: any): Promise { await createTransferEventsTable(queryInterface, schema); await createSubListsEventsTable(queryInterface, schema); await createLastIndexedBlockTable(queryInterface, schema); + await createGivenEventsTable(queryInterface, schema); + await createSplitEventsTable(queryInterface, schema); + await createSqueezedStreamsEventsTable(queryInterface, schema); + await createStreamReceiverSeenEventsTable(queryInterface, schema); + await createStreamsSetEventsTable(queryInterface, schema); } -async function createLastIndexedBlockTable( +async function createStreamsSetEventsTable( queryInterface: QueryInterface, schema: DbSchema, ) { - await queryInterface.createTable(`${schema}._last_indexed_block`, { - id: { - primaryKey: true, - autoIncrement: true, - type: DataTypes.INTEGER, + await queryInterface.createTable( + { + schema, + tableName: `streams_set_events`, }, - blockNumber: { - unique: true, - allowNull: false, - type: DataTypes.BIGINT, + transformFieldNamesToSnakeCase({ + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + erc20: { + allowNull: false, + type: DataTypes.STRING, + }, + receiversHash: { + allowNull: false, + type: DataTypes.STRING, + }, + streamsHistoryHash: { + allowNull: false, + type: DataTypes.STRING, + }, + balance: { + allowNull: false, + type: DataTypes.STRING, + }, + maxEnd: { + allowNull: false, + type: DataTypes.STRING, + }, + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); + + await queryInterface.addIndex( + { + schema, + tableName: `streams_set_events`, }, - createdAt: { - allowNull: false, - type: DataTypes.DATE, + transformFieldArrayToSnakeCase(['receiversHash']), + { + name: 'idx_streams_set_events_receiversHash', }, - updatedAt: { - allowNull: false, - type: DataTypes.DATE, + ); + await queryInterface.addIndex( + { + schema, + tableName: `streams_set_events`, }, - }); + transformFieldArrayToSnakeCase(['accountId']), + { + name: 'idx_streams_set_events_accountId', + }, + ); } -async function createSubListsEventsTable( +async function createStreamReceiverSeenEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: `stream_receiver_seen_events`, + }, + transformFieldNamesToSnakeCase({ + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + config: { + allowNull: false, + type: DataTypes.STRING, + }, + receiversHash: { + allowNull: false, + type: DataTypes.STRING, + }, + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); + + await queryInterface.addIndex( + { + schema, + tableName: `stream_receiver_seen_events`, + }, + transformFieldArrayToSnakeCase(['accountId']), + { + name: 'idx_stream_receiver_seen_events_accountId', + }, + ); +} + +async function createSqueezedStreamsEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: `squeezed_streams_events`, + }, + transformFieldNamesToSnakeCase({ + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + erc20: { + allowNull: false, + type: DataTypes.STRING, + }, + senderId: { + allowNull: false, + type: DataTypes.STRING, + }, + amount: { + allowNull: false, + type: DataTypes.STRING, + }, + streamsHistoryHashes: { + allowNull: false, + type: DataTypes.TEXT, + }, + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); +} + +async function createSplitEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: `split_events`, + }, + transformFieldNamesToSnakeCase({ + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiver: { + allowNull: false, + type: DataTypes.STRING, + }, + erc20: { + allowNull: false, + type: DataTypes.STRING, + }, + amt: { + allowNull: false, + type: DataTypes.STRING, + }, + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); + + await queryInterface.addIndex( + { + schema, + tableName: `split_events`, + }, + transformFieldArrayToSnakeCase(['receiver']), + { + name: 'idx_split_events_receiver', + }, + ); + await queryInterface.addIndex( + { + schema, + tableName: `split_events`, + }, + transformFieldArrayToSnakeCase(['accountId', 'receiver']), + { + name: 'idx_split_events_accountId_receiver', + }, + ); +} + +async function createGivenEventsTable( queryInterface: QueryInterface, schema: DbSchema, ) { - await queryInterface.createTable(`${schema}.sub_lists`, { - accountId: { - primaryKey: true, - type: DataTypes.STRING, + await queryInterface.createTable( + { + schema, + tableName: `given_events`, }, - parentAccountType: { - allowNull: false, - type: literal(`"${schema}".account_type`) as unknown as DataType, + transformFieldNamesToSnakeCase({ + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiver: { + allowNull: false, + type: DataTypes.STRING, + }, + erc20: { + allowNull: false, + type: DataTypes.STRING, + }, + amt: { + allowNull: false, + type: DataTypes.STRING, + }, + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); + + await queryInterface.addIndex( + { + schema, + tableName: `given_events`, }, - parentId: { - allowNull: false, - type: DataTypes.STRING, + transformFieldArrayToSnakeCase(['accountId']), + { + name: 'idx_given_events_accountId', }, - rootAccountType: { - allowNull: false, - type: literal(`"${schema}".account_type`) as unknown as DataType, + ); + await queryInterface.addIndex( + { + schema, + tableName: `given_events`, }, - rootId: { - allowNull: false, - type: DataTypes.STRING, + transformFieldArrayToSnakeCase(['receiver']), + { + name: 'idx_given_events_receiver', }, - lastProcessedIpfsHash: { - allowNull: false, - type: DataTypes.TEXT, + ); + await queryInterface.addIndex( + { + schema, + tableName: `given_events`, }, - createdAt: { - allowNull: false, - type: DataTypes.DATE, + transformFieldArrayToSnakeCase(['erc20']), + { + name: 'idx_given_events_erc20', }, - updatedAt: { - allowNull: false, - type: DataTypes.DATE, + ); + await queryInterface.addIndex( + { + schema, + tableName: `given_events`, }, - }); + transformFieldArrayToSnakeCase(['transactionHash', 'logIndex']), + { + name: 'idx_given_events_transactionHash_logIndex', + }, + ); +} - await queryInterface.addIndex(`${schema}.sub_lists`, ['accountId'], { - name: 'idx_sub_lists_account_id', - }); - await queryInterface.addIndex(`${schema}.sub_lists`, ['parentId'], { - name: 'idx_sub_lists_parent_id', - }); - await queryInterface.addIndex(`${schema}.sub_lists`, ['rootId'], { - name: 'idx_sub_lists_root_id', - }); +async function createLastIndexedBlockTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: `_last_indexed_block`, + }, + transformFieldNamesToSnakeCase({ + id: { + primaryKey: true, + autoIncrement: true, + type: DataTypes.INTEGER, + }, + blockNumber: { + unique: true, + allowNull: false, + type: DataTypes.BIGINT, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); +} + +async function createSubListsEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: `sub_lists`, + }, + transformFieldNamesToSnakeCase({ + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + parentAccountType: { + allowNull: false, + type: literal(`${schema}.account_type`).val as unknown as DataType, + }, + parentId: { + allowNull: false, + type: DataTypes.STRING, + }, + rootAccountType: { + allowNull: false, + type: literal(`${schema}.account_type`).val as unknown as DataType, + }, + rootId: { + allowNull: false, + type: DataTypes.STRING, + }, + lastProcessedIpfsHash: { + allowNull: false, + type: DataTypes.TEXT, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); + + await queryInterface.addIndex( + { + schema, + tableName: `sub_lists`, + }, + transformFieldArrayToSnakeCase(['accountId']), + { + name: 'idx_sub_lists_account_id', + }, + ); + await queryInterface.addIndex( + { + schema, + tableName: `sub_lists`, + }, + transformFieldArrayToSnakeCase(['parentId']), + { + name: 'idx_sub_lists_parent_id', + }, + ); + await queryInterface.addIndex( + { + schema, + tableName: `sub_lists`, + }, + transformFieldArrayToSnakeCase(['rootId']), + { + name: 'idx_sub_lists_root_id', + }, + ); } -async function createDripListsTable( - queryInterface: QueryInterface, - schema: DbSchema, -) { - await queryInterface.createTable(`${schema}.drip_lists`, { - accountId: { - primaryKey: true, - type: DataTypes.STRING, - }, - isValid: { - allowNull: false, - type: DataTypes.BOOLEAN, - }, - ownerAddress: { - allowNull: true, - type: DataTypes.STRING, - }, - ownerAccountId: { - allowNull: true, - type: DataTypes.STRING, - }, - name: { - allowNull: true, - type: DataTypes.STRING, - }, - latestVotingRoundId: { - allowNull: true, - type: DataTypes.UUID, - }, - description: { - allowNull: true, - type: DataTypes.TEXT, - }, - creator: { - allowNull: true, - type: DataTypes.STRING, - }, - previousOwnerAddress: { - allowNull: true, - type: DataTypes.STRING, - }, - isVisible: { - allowNull: false, - type: DataTypes.BOOLEAN, - }, - lastProcessedIpfsHash: { - allowNull: false, - type: DataTypes.TEXT, - }, - createdAt: { - allowNull: false, - type: DataTypes.DATE, +async function createDripListsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: `drip_lists`, + }, + transformFieldNamesToSnakeCase({ + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + isValid: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + ownerAddress: { + allowNull: true, + type: DataTypes.STRING, + }, + ownerAccountId: { + allowNull: true, + type: DataTypes.STRING, + }, + name: { + allowNull: true, + type: DataTypes.STRING, + }, + latestVotingRoundId: { + allowNull: true, + type: DataTypes.UUID, + }, + description: { + allowNull: true, + type: DataTypes.TEXT, + }, + creator: { + allowNull: true, + type: DataTypes.STRING, + }, + previousOwnerAddress: { + allowNull: true, + type: DataTypes.STRING, + }, + isVisible: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + lastProcessedIpfsHash: { + allowNull: false, + type: DataTypes.TEXT, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); + + await queryInterface.addIndex( + { + schema, + tableName: `drip_lists`, }, - updatedAt: { - allowNull: false, - type: DataTypes.DATE, + transformFieldArrayToSnakeCase(['ownerAddress']), + { + name: 'idx_drip_lists_owner_address', }, - }); - - await queryInterface.addIndex(`${schema}.drip_lists`, ['ownerAddress'], { - name: 'idx_drip_lists_owner_address', - }); + ); } async function createEcosystemMainAccountsTable( queryInterface: QueryInterface, schema: DbSchema, ) { - await queryInterface.createTable(`${schema}.ecosystem_main_accounts`, { - accountId: { - primaryKey: true, - type: DataTypes.STRING, - }, - isValid: { - allowNull: false, - type: DataTypes.BOOLEAN, - }, - ownerAddress: { - allowNull: true, - type: DataTypes.STRING, - }, - ownerAccountId: { - allowNull: true, - type: DataTypes.STRING, - }, - name: { - allowNull: true, - type: DataTypes.STRING, - }, - latestVotingRoundId: { - allowNull: true, - type: DataTypes.UUID, - }, - description: { - allowNull: true, - type: DataTypes.TEXT, - }, - creator: { - allowNull: true, - type: DataTypes.STRING, - }, - previousOwnerAddress: { - allowNull: true, - type: DataTypes.STRING, - }, - isVisible: { - allowNull: false, - type: DataTypes.BOOLEAN, - }, - lastProcessedIpfsHash: { - allowNull: false, - type: DataTypes.TEXT, - }, - createdAt: { - allowNull: false, - type: DataTypes.DATE, - }, - updatedAt: { - allowNull: false, - type: DataTypes.DATE, + await queryInterface.createTable( + { + schema, + tableName: `ecosystem_main_accounts`, }, - }); + transformFieldNamesToSnakeCase({ + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + isValid: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + ownerAddress: { + allowNull: true, + type: DataTypes.STRING, + }, + ownerAccountId: { + allowNull: true, + type: DataTypes.STRING, + }, + name: { + allowNull: true, + type: DataTypes.STRING, + }, + latestVotingRoundId: { + allowNull: true, + type: DataTypes.UUID, + }, + description: { + allowNull: true, + type: DataTypes.TEXT, + }, + creator: { + allowNull: true, + type: DataTypes.STRING, + }, + previousOwnerAddress: { + allowNull: true, + type: DataTypes.STRING, + }, + isVisible: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + lastProcessedIpfsHash: { + allowNull: false, + type: DataTypes.TEXT, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); await queryInterface.addIndex( - `${schema}.ecosystem_main_accounts`, - ['ownerAddress'], + { + schema, + tableName: `ecosystem_main_accounts`, + }, + transformFieldArrayToSnakeCase(['ownerAddress']), { name: 'idx_ecosystem_main_accounts_owner_address', }, @@ -268,80 +674,106 @@ async function createProjectsTable( queryInterface: QueryInterface, schema: DbSchema, ) { - await queryInterface.createTable(`${schema}.projects`, { - accountId: { - primaryKey: true, - type: DataTypes.STRING, - }, - name: { - allowNull: false, - type: DataTypes.STRING, - }, - verificationStatus: { - allowNull: false, - type: literal( - `"${schema}".project_verification_status`, - ) as unknown as DataType, - }, - forge: { - allowNull: false, - type: literal(`"${schema}".forges`) as unknown as DataType, - }, - ownerAddress: { - allowNull: false, - type: DataTypes.STRING, - }, - ownerAccountId: { - allowNull: false, - type: DataTypes.STRING, - }, - url: { - allowNull: false, - type: DataTypes.STRING, - }, - emoji: { - allowNull: true, - type: DataTypes.STRING, - }, - avatarCid: { - allowNull: true, - type: DataTypes.STRING, + await queryInterface.createTable( + { + schema, + tableName: `projects`, }, - color: { - allowNull: false, - type: DataTypes.STRING, + transformFieldNamesToSnakeCase({ + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + name: { + allowNull: false, + type: DataTypes.STRING, + }, + verificationStatus: { + allowNull: false, + type: literal(`${schema}.project_verification_status`) + .val as unknown as DataType, + }, + forge: { + allowNull: false, + type: literal(`${schema}.forges`).val as unknown as DataType, + }, + ownerAddress: { + allowNull: false, + type: DataTypes.STRING, + }, + ownerAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + url: { + allowNull: false, + type: DataTypes.STRING, + }, + emoji: { + allowNull: true, + type: DataTypes.STRING, + }, + avatarCid: { + allowNull: true, + type: DataTypes.STRING, + }, + color: { + allowNull: false, + type: DataTypes.STRING, + }, + isVisible: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, + lastProcessedIpfsHash: { + allowNull: false, + type: DataTypes.TEXT, + }, + claimedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); + + await queryInterface.addIndex( + { + schema, + tableName: `projects`, }, - isVisible: { - allowNull: false, - type: DataTypes.BOOLEAN, + transformFieldArrayToSnakeCase(['ownerAddress']), + { + name: 'idx_projects_owner_address', }, - lastProcessedIpfsHash: { - allowNull: false, - type: DataTypes.TEXT, + ); + await queryInterface.addIndex( + { + schema, + tableName: `projects`, }, - claimedAt: { - allowNull: false, - type: DataTypes.DATE, + transformFieldArrayToSnakeCase(['verificationStatus']), + { + name: 'idx_projects_verification_status', }, - createdAt: { - allowNull: false, - type: DataTypes.DATE, + ); + await queryInterface.addIndex( + { + schema, + tableName: `projects`, }, - updatedAt: { - allowNull: false, - type: DataTypes.DATE, + transformFieldArrayToSnakeCase(['url']), + { + name: 'idx_projects_url', }, - }); - - await queryInterface.addIndex(`${schema}.projects`, ['ownerAddress'], { - name: 'idx_projects_owner_address', - }); - await queryInterface.addIndex(`${schema}.projects`, ['verificationStatus'], { - name: 'idx_projects_verification_status', - }); - await queryInterface.addIndex(`${schema}.projects`, ['url'], { - name: 'idx_projects_url', - }); + ); } async function createAccountMetadataEventsTable( @@ -349,8 +781,11 @@ async function createAccountMetadataEventsTable( schema: DbSchema, ) { await queryInterface.createTable( - `${schema}.account_metadata_emitted_events`, { + schema, + tableName: `account_metadata_emitted_events`, + }, + transformFieldNamesToSnakeCase({ key: { allowNull: false, type: DataTypes.STRING, @@ -381,12 +816,15 @@ async function createAccountMetadataEventsTable( allowNull: false, type: DataTypes.INTEGER, }, - }, + }), ); await queryInterface.addIndex( - `${schema}.account_metadata_emitted_events`, - ['accountId'], + { + schema, + tableName: `account_metadata_emitted_events`, + }, + transformFieldArrayToSnakeCase(['accountId']), { name: 'idx_account_metadata_emitted_events_accountId', }, @@ -397,98 +835,116 @@ async function createTransferEventsTable( queryInterface: QueryInterface, schema: DbSchema, ) { - await queryInterface.createTable(`${schema}.transfer_events`, { - tokenId: { - allowNull: false, - type: DataTypes.STRING, - }, - from: { - allowNull: false, - type: DataTypes.STRING, - }, - to: { - allowNull: false, - type: DataTypes.STRING, - }, - transactionHash: { - primaryKey: true, - allowNull: false, - type: DataTypes.STRING, - }, - logIndex: { - primaryKey: true, - allowNull: false, - type: DataTypes.INTEGER, - }, - blockNumber: { - allowNull: false, - type: DataTypes.INTEGER, - }, - blockTimestamp: { - allowNull: false, - type: DataTypes.DATE, + await queryInterface.createTable( + { + schema, + tableName: `transfer_events`, }, - }); + transformFieldNamesToSnakeCase({ + tokenId: { + allowNull: false, + type: DataTypes.STRING, + }, + from: { + allowNull: false, + type: DataTypes.STRING, + }, + to: { + allowNull: false, + type: DataTypes.STRING, + }, + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); } async function createSplitReceiversTable( queryInterface: QueryInterface, schema: DbSchema, ) { - await queryInterface.createTable(`${schema}.split_receivers`, { - id: { - primaryKey: true, - autoIncrement: true, - type: DataTypes.INTEGER, - }, - receiverAccountId: { - allowNull: false, - type: DataTypes.STRING, - }, - receiverAccountType: { - allowNull: false, - type: literal(`"${schema}".account_type`) as unknown as DataType, - }, - senderAccountId: { - allowNull: false, - type: DataTypes.STRING, - }, - senderAccountType: { - allowNull: false, - type: literal(`"${schema}".account_type`) as unknown as DataType, - }, - relationshipType: { - allowNull: false, - type: literal(`"${schema}".relationship_type`) as unknown as DataType, - }, - weight: { - type: DataTypes.INTEGER, - allowNull: false, - }, - blockTimestamp: { - type: DataTypes.DATE, - allowNull: false, - }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, + await queryInterface.createTable( + { + schema, + tableName: `split_receivers`, }, - }); + transformFieldNamesToSnakeCase({ + id: { + primaryKey: true, + autoIncrement: true, + type: DataTypes.INTEGER, + }, + receiverAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiverAccountType: { + allowNull: false, + type: literal(`${schema}.account_type`).val as unknown as DataType, + }, + senderAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + senderAccountType: { + allowNull: false, + type: literal(`${schema}.account_type`).val as unknown as DataType, + }, + relationshipType: { + allowNull: false, + type: literal(`${schema}.relationship_type`).val as unknown as DataType, + }, + weight: { + type: DataTypes.INTEGER, + allowNull: false, + }, + blockTimestamp: { + type: DataTypes.DATE, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + }, + }), + ); await queryInterface.addIndex( - `${schema}.split_receivers`, - ['receiverAccountId', 'senderAccountId'], + { + schema, + tableName: `split_receivers`, + }, + transformFieldArrayToSnakeCase(['receiverAccountId', 'senderAccountId']), { name: 'idx_split_receivers_receiver_sender', }, ); await queryInterface.addIndex( - `${schema}.split_receivers`, - ['senderAccountId', 'receiverAccountId'], + { + schema, + tableName: `split_receivers`, + }, + transformFieldArrayToSnakeCase(['senderAccountId', 'receiverAccountId']), { name: 'idx_split_receivers_sender_receiver', }, @@ -499,14 +955,58 @@ export async function down({ context: sequelize }: any): Promise { const schema = getSchema(); const queryInterface: QueryInterface = sequelize.getQueryInterface(); - await queryInterface.dropTable(`${schema}.split_receivers`); - await queryInterface.dropTable(`${schema}.account_metadata_emitted_events`); - await queryInterface.dropTable(`${schema}.projects`); - await queryInterface.dropTable(`${schema}.drip_lists`); - await queryInterface.dropTable(`${schema}.ecosystem_main_accounts`); - await queryInterface.dropTable(`${schema}.transfer_events`); - await queryInterface.dropTable(`${schema}.sub_lists`); - await queryInterface.dropTable(`${schema}._last_indexed_block`); + await queryInterface.dropTable({ + schema, + tableName: `streams_set_events`, + }); + await queryInterface.dropTable({ + schema, + tableName: `stream_receiver_seen_events`, + }); + await queryInterface.dropTable({ + schema, + tableName: `squeezed_streams_events`, + }); + await queryInterface.dropTable({ + schema, + tableName: `split_events`, + }); + await queryInterface.dropTable({ + schema, + tableName: `given_events`, + }); + await queryInterface.dropTable({ + schema, + tableName: `sub_lists`, + }); + await queryInterface.dropTable({ + schema, + tableName: `drip_lists`, + }); + await queryInterface.dropTable({ + schema, + tableName: `ecosystem_main_accounts`, + }); + await queryInterface.dropTable({ + schema, + tableName: `projects`, + }); + await queryInterface.dropTable({ + schema, + tableName: `account_metadata_emitted_events`, + }); + await queryInterface.dropTable({ + schema, + tableName: `transfer_events`, + }); + await queryInterface.dropTable({ + schema, + tableName: `split_receivers`, + }); + await queryInterface.dropTable({ + schema, + tableName: `_last_indexed_block`, + }); await queryInterface.sequelize.query(` DROP TYPE IF EXISTS ${schema}.account_type; @@ -524,3 +1024,22 @@ export async function down({ context: sequelize }: any): Promise { DROP TYPE IF EXISTS ${schema}.forges; `); } + +export function camelToSnake(str: string): string { + return str + .replace(/([A-Z])/g, '_$1') + .replace(/^_/, '') + .toLowerCase(); +} + +export function transformFieldNamesToSnakeCase>( + fields: T, +): Record { + return Object.fromEntries( + Object.entries(fields).map(([key, value]) => [camelToSnake(key), value]), + ); +} + +export function transformFieldArrayToSnakeCase(fields: string[]): string[] { + return fields.map(camelToSnake); +} diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index 6c42baf..d5cc11b 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -12,6 +12,7 @@ import { SqueezedStreamsEventModel, SubListModel, EcosystemMainAccountModel, + SplitReceiverModel, } from '../models'; const REGISTERED_MODELS: ModelStaticMembers[] = []; @@ -28,11 +29,12 @@ export function registerModels(): void { registerModel(_LastIndexedBlockModel); registerModel(SubListModel); + registerModel(ProjectModel); registerModel(DripListModel); registerModel(GivenEventModel); registerModel(SplitEventModel); - registerModel(ProjectModel); registerModel(TransferEventModel); + registerModel(SplitReceiverModel); registerModel(StreamsSetEventModel); registerModel(EcosystemMainAccountModel); registerModel(SqueezedStreamsEventModel); diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index c4544c3..ba843b0 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -28,6 +28,7 @@ import { getCurrentSplitReceiversBySender } from './receiversRepository'; import { isLatestEvent } from '../../utils/isLatestEvent'; import type { AccountMetadataEmittedEvent } from '../../../contracts/CURRENT_NETWORK/Drips'; import { AccountMetadataEmittedEventModel } from '../../models'; +import logger from '../../core/logger'; export default class AccountMetadataEmittedEventHandler extends EventHandlerBase<'AccountMetadataEmitted(uint256,bytes32,bytes)'> { public readonly eventSignatures = [ @@ -53,8 +54,8 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase LogManager.logRequestInfo( [ `📥 ${this.name} is processing ${eventSignature}:`, - ` - key: ${key} (decoded: ${toUtf8String(key)})`, - ` - value: ${value} (decoded: ${ipfsHash})`, + ` - key: ${toUtf8String(key)} (raw: ${key})`, + ` - value: ${ipfsHash} (raw: ${value})`, ` - accountId: ${accountId}`, ` - logIndex: ${logIndex}`, ` - txHash: ${transactionHash}`, @@ -172,15 +173,28 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase public override async beforeHandle({ event: { args }, + id: requestId, }: EventHandlerRequest<'AccountMetadataEmitted(uint256,bytes32,bytes)'>): Promise<{ accountIdsToInvalidate: AccountId[]; }> { + logger.info( + `[${requestId}] ${this.name} is gathering accountIds to invalidate...`, + ); + const [accountId] = args as AccountMetadataEmittedEvent.OutputTuple; + const accountIdsToInvalidate = await getCurrentSplitReceiversBySender( + convertToAccountId(accountId), + ); + + logger.info( + `[${requestId}] ${this.name} account IDs to invalidate: ${accountIdsToInvalidate.join( + ', ', + )}`, + ); + return { - accountIdsToInvalidate: await getCurrentSplitReceiversBySender( - convertToAccountId(accountId), - ), + accountIdsToInvalidate, }; } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index ac8805c..d1b59f6 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -121,7 +121,7 @@ async function upsertDripList({ transaction: Transaction; metadata: AnyVersion; }) { - const [dripList, isCreated] = await DripListModel.upsert( + const [dripList] = await DripListModel.upsert( { accountId: convertToNftDriverId(metadata.describes.accountId), name: metadata.name ?? null, @@ -144,12 +144,7 @@ async function upsertDripList({ }, ); - logManager.appendUpsertLog( - dripList, - DripListModel, - dripList.accountId, - Boolean(isCreated), - ); + logManager.appendUpsertLog(dripList, DripListModel, dripList.accountId); } async function createNewSplitReceivers({ diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index c23d1c5..c3a93cf 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -107,7 +107,7 @@ async function upsertEcosystemMainAccount({ transaction: Transaction; metadata: AnyVersion; }) { - const [dripList, isCreated] = await EcosystemMainAccountModel.upsert( + const [dripList] = await EcosystemMainAccountModel.upsert( { accountId: convertToNftDriverId(metadata.describes.accountId), name: metadata.name ?? null, @@ -130,7 +130,6 @@ async function upsertEcosystemMainAccount({ dripList, EcosystemMainAccountModel, dripList.accountId, - Boolean(isCreated), ); } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index e9e037f..dcc3caa 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -147,7 +147,7 @@ async function upsertProject({ return null; } - const [project, isCreated] = await ProjectModel.upsert( + const [project] = await ProjectModel.upsert( { accountId: convertToRepoDriverId(accountId), url, @@ -168,12 +168,7 @@ async function upsertProject({ }, ); - logManager.appendUpsertLog( - project, - ProjectModel, - project.accountId, - Boolean(isCreated), - ); + logManager.appendUpsertLog(project, ProjectModel, project.accountId); } async function createNewSplitReceivers({ diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 2e25282..5c1de51 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -119,7 +119,7 @@ async function upsertSubList({ emitterAccountId: ImmutableSplitsDriverId; metadata: AnyVersion; }): Promise { - const [subList, isCreated] = await SubListModel.upsert( + const [subList] = await SubListModel.upsert( { accountId: emitterAccountId, parentAccountType: @@ -135,12 +135,7 @@ async function upsertSubList({ }, ); - logManager.appendUpsertLog( - subList, - SubListModel, - subList.accountId, - Boolean(isCreated), - ); + logManager.appendUpsertLog(subList, SubListModel, subList.accountId); } async function createNewSplitReceivers({ diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts index 9736b53..60b7ffb 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts @@ -2,13 +2,13 @@ import { type Transaction } from 'sequelize'; import type { AccountId } from '../../core/types'; import type LogManager from '../../core/LogManager'; import type { SplitReceiverShape } from '../../core/splitRules'; -import { SplitReceiver, StreamReceiverSeenEventModel } from '../../models'; +import { SplitReceiverModel, StreamReceiverSeenEventModel } from '../../models'; export async function deleteExistingSplitReceivers( senderAccountId: AccountId, transaction: Transaction, ) { - await SplitReceiver.destroy({ + await SplitReceiverModel.destroy({ where: { senderAccountId, }, @@ -26,18 +26,18 @@ export async function createSplitReceiver({ transaction: Transaction; splitReceiverShape: SplitReceiverShape; }) { - const splitReceiver = await SplitReceiver.create( + const splitReceiver = await SplitReceiverModel.create( { ...splitReceiverShape }, { transaction }, ); - logManager.appendCreateLog(SplitReceiver, splitReceiver.id.toString()); + logManager.appendCreateLog(SplitReceiverModel, splitReceiver.id.toString()); } export async function getCurrentSplitReceiversBySender( senderAccountId: AccountId, ): Promise { - const splitReceivers = await SplitReceiver.findAll({ + const splitReceivers = await SplitReceiverModel.findAll({ where: { senderAccountId, }, diff --git a/src/eventHandlers/StreamReceiverSeenEventHandler.ts b/src/eventHandlers/StreamReceiverSeenEventHandler.ts index b19f194..97712e8 100644 --- a/src/eventHandlers/StreamReceiverSeenEventHandler.ts +++ b/src/eventHandlers/StreamReceiverSeenEventHandler.ts @@ -1,4 +1,5 @@ import type { StreamReceiverSeenEvent } from '../../contracts/CURRENT_NETWORK/Drips'; +import logger from '../core/logger'; import LogManager from '../core/LogManager'; import type { AccountId } from '../core/types'; import { dbConnection } from '../db/database'; @@ -64,20 +65,29 @@ export default class StreamReceiverSeenEventHandler extends EventHandlerBase<'St }); } - public override async beforeHandle( - request: EventHandlerRequest<'StreamReceiverSeen(bytes32,uint256,uint256)'>, - ): Promise<{ + public override async beforeHandle({ + event: { args }, + id: requestId, + }: EventHandlerRequest<'StreamReceiverSeen(bytes32,uint256,uint256)'>): Promise<{ accountIdsToInvalidate: AccountId[]; }> { - const { - event: { args }, - } = request; + logger.info( + `[${requestId}] ${this.name} is gathering accountIds to invalidate...`, + ); const [rawReceiversHash] = args as StreamReceiverSeenEvent.OutputTuple; + const accountIdsToInvalidate = + await getCurrentSplitReceiversByReceiversHash(rawReceiversHash); + + logger.info( + `[${requestId}] ${this.name} account IDs to invalidate: ${accountIdsToInvalidate.join( + ', ', + )}`, + ); + return { - accountIdsToInvalidate: - await getCurrentSplitReceiversByReceiversHash(rawReceiversHash), + accountIdsToInvalidate, }; } } diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index 2365f55..288e8a3 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -125,13 +125,15 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add override async afterHandle(context: { args: [from: string, to: string, tokenId: bigint]; blockTimestamp: Date; + requestId: string; }): Promise { - const { args, blockTimestamp } = context; + const { args, blockTimestamp, requestId } = context; const [from, to, tokenId] = args; await super.afterHandle({ args: [tokenId, await calcAccountId(from), await calcAccountId(to)], blockTimestamp, + requestId, }); } } diff --git a/src/events/EventHandlerBase.ts b/src/events/EventHandlerBase.ts index f44479b..0efa47d 100644 --- a/src/events/EventHandlerBase.ts +++ b/src/events/EventHandlerBase.ts @@ -1,5 +1,5 @@ import { isAddress } from 'ethers'; -import { ValidationError } from 'sequelize'; +import { BaseError } from 'sequelize'; import appSettings from '../config/appSettings'; import logger from '../core/logger'; import type { AccountId, Result } from '../core/types'; @@ -32,7 +32,7 @@ export default abstract class EventHandlerBase { const result = await getResult(this._handle.bind(this))(request); if (!result.ok) { - if (result.error instanceof ValidationError) { + if (result.error instanceof BaseError) { logger.error( `[${request.id}] ${this.name} failed to process event: ${result.error.message}`, ); @@ -54,8 +54,9 @@ export default abstract class EventHandlerBase { public async afterHandle(context: { args: any[]; blockTimestamp: Date; + requestId: string; }): Promise { - const { args, blockTimestamp } = context; + const { args, blockTimestamp, requestId } = context; // If the block is older than 15 minutes, we don't invalidate the cache to avoid unnecessary requests while indexing. if (new Date(blockTimestamp).getTime() < Date.now() - 15 * 60000) { @@ -92,14 +93,16 @@ export default abstract class EventHandlerBase { }); logger.info( - `'${ + `[${requestId}]'${ this.name }' invalidated cache entries for accountIds: ${accountIds.join( ', ', )}`, ); } catch (error: any) { - logger.error(`Failed to invalidate cache: ${error.message}`); + logger.error( + `[${requestId}] Failed to invalidate cache: ${error.message}`, + ); } } } diff --git a/src/events/poll.ts b/src/events/poll.ts index fc8481f..67ada0b 100644 --- a/src/events/poll.ts +++ b/src/events/poll.ts @@ -12,7 +12,7 @@ import appSettings from '../config/appSettings'; async function getLatestIndexedBlock() { const record = await _LastIndexedBlockModel.findOne({ - order: [['blockNumber', 'DESC']], + order: [['block_number', 'DESC']], }); return record?.blockNumber ? Number(record.blockNumber) : 0; diff --git a/src/models/AccountMetadataEmittedEventModel.ts b/src/models/AccountMetadataEmittedEventModel.ts index 506d6f4..8ad023f 100644 --- a/src/models/AccountMetadataEmittedEventModel.ts +++ b/src/models/AccountMetadataEmittedEventModel.ts @@ -45,6 +45,8 @@ export default class AccountMetadataEmittedEventModel sequelize, schema: getSchema(), tableName: 'account_metadata_emitted_events', + underscored: true, + timestamps: false, indexes: [ { fields: ['accountId'], diff --git a/src/models/DripListModel.ts b/src/models/DripListModel.ts index 93ea353..131e2e0 100644 --- a/src/models/DripListModel.ts +++ b/src/models/DripListModel.ts @@ -87,6 +87,7 @@ export default class DripListModel extends Model< sequelize, schema: getSchema(), tableName: 'drip_lists', + underscored: true, timestamps: true, indexes: [ { diff --git a/src/models/EcosystemMainAccountModel.ts b/src/models/EcosystemMainAccountModel.ts index 498a6db..bc96749 100644 --- a/src/models/EcosystemMainAccountModel.ts +++ b/src/models/EcosystemMainAccountModel.ts @@ -81,6 +81,7 @@ export default class EcosystemMainAccountModel extends Model< sequelize, schema: getSchema(), tableName: 'ecosystem_main_accounts', + underscored: true, timestamps: true, indexes: [ { diff --git a/src/models/GivenEventModel.ts b/src/models/GivenEventModel.ts index ac762c1..684b7e0 100644 --- a/src/models/GivenEventModel.ts +++ b/src/models/GivenEventModel.ts @@ -29,47 +29,45 @@ export default class GivenEventModel this.init( { accountId: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, receiver: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, erc20: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, amt: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, ...COMMON_EVENT_INIT_ATTRIBUTES, }, { sequelize, schema: getSchema(), - tableName: 'GivenEvents', + tableName: 'given_events', + underscored: true, + timestamps: false, indexes: [ { fields: ['accountId'], - name: `IX_GivenEvents_accountId`, - unique: false, + name: `idx_given_events_accountId`, }, { fields: ['receiver'], - name: `IX_GivenEvents_receiver`, - unique: false, + name: `idx_given_events_receiver`, }, { fields: ['erc20'], - name: `IX_GivenEvents_erc20`, - unique: false, + name: `idx_given_events_erc20`, }, { fields: ['transactionHash', 'logIndex'], - name: `IX_GivenEvents_transactionHash_logIndex`, - unique: false, + name: `idx_given_events_transactionHash_logIndex`, }, ], }, diff --git a/src/models/ProjectModel.ts b/src/models/ProjectModel.ts index 18870ec..93c3eff 100644 --- a/src/models/ProjectModel.ts +++ b/src/models/ProjectModel.ts @@ -113,6 +113,7 @@ export default class ProjectModel extends Model< sequelize, schema: getSchema(), tableName: 'projects', + underscored: true, timestamps: true, indexes: [ { diff --git a/src/models/SplitEventModel.ts b/src/models/SplitEventModel.ts index a8c2a25..aec5210 100644 --- a/src/models/SplitEventModel.ts +++ b/src/models/SplitEventModel.ts @@ -16,52 +16,51 @@ export default class SplitEventModel > implements IEventModel { - public declare accountId: AccountId; - public declare receiver: AccountId; - public declare erc20: Address; - public declare amt: BigIntString; + declare public accountId: AccountId; + declare public receiver: AccountId; + declare public erc20: Address; + declare public amt: BigIntString; - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; + declare public logIndex: number; + declare public blockNumber: number; + declare public blockTimestamp: Date; + declare public transactionHash: string; public static initialize(sequelize: Sequelize): void { this.init( { accountId: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, receiver: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, erc20: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, amt: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, ...COMMON_EVENT_INIT_ATTRIBUTES, }, { sequelize, schema: getSchema(), - tableName: 'SplitEvents', + tableName: 'split_events', + underscored: true, + timestamps: false, indexes: [ { fields: ['receiver'], - name: `IX_SplitEvents_receiver`, - unique: false, + name: `idx_split_events_receiver`, }, { fields: ['accountId', 'receiver'], - name: `IX_SplitEvents_accountId_receiver`, - unique: false, + name: `idx_split_events_accountId_receiver`, }, ], }, diff --git a/src/models/SplitReceiver.ts b/src/models/SplitReceiverModel.ts similarity index 93% rename from src/models/SplitReceiver.ts rename to src/models/SplitReceiverModel.ts index 0f4dc29..475182a 100644 --- a/src/models/SplitReceiver.ts +++ b/src/models/SplitReceiverModel.ts @@ -14,9 +14,9 @@ import { RELATIONSHIP_TYPES, } from '../core/splitRules'; -export default class SplitReceiver extends Model< - InferAttributes, - InferCreationAttributes +export default class SplitReceiverModel extends Model< + InferAttributes, + InferCreationAttributes > { declare public id: CreationOptional; declare public receiverAccountId: AccountId; @@ -78,6 +78,7 @@ export default class SplitReceiver extends Model< sequelize, schema: getSchema(), tableName: 'split_receivers', + underscored: true, timestamps: true, indexes: [ { diff --git a/src/models/SqueezedStreamsEventModel.ts b/src/models/SqueezedStreamsEventModel.ts index 6fc51d4..18399e3 100644 --- a/src/models/SqueezedStreamsEventModel.ts +++ b/src/models/SqueezedStreamsEventModel.ts @@ -21,18 +21,15 @@ export default class SqueezedStreamsEventModel > implements IEventModel { - // Properties from event output. - public declare accountId: AccountId; - public declare erc20: Address; - public declare senderId: AccountId; - public declare amount: BigIntString; - public declare streamsHistoryHashes: StreamHistoryHashes; - - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; + declare public accountId: AccountId; + declare public erc20: Address; + declare public senderId: AccountId; + declare public amount: BigIntString; + declare public streamsHistoryHashes: StreamHistoryHashes; + declare public logIndex: number; + declare public blockNumber: number; + declare public blockTimestamp: Date; + declare public transactionHash: string; public static toStreamHistoryHashes( streamsHistoryHashes: string[], @@ -44,31 +41,33 @@ export default class SqueezedStreamsEventModel this.init( { accountId: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, erc20: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, senderId: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, amount: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, streamsHistoryHashes: { - type: DataTypes.JSON, allowNull: false, + type: DataTypes.JSON, }, ...COMMON_EVENT_INIT_ATTRIBUTES, }, { sequelize, schema: getSchema(), - tableName: 'SqueezedStreamsEvents', + tableName: 'squeezed_streams_events', + underscored: true, + timestamps: false, }, ); } diff --git a/src/models/StreamReceiverSeenEventModel.ts b/src/models/StreamReceiverSeenEventModel.ts index 356f576..83bca86 100644 --- a/src/models/StreamReceiverSeenEventModel.ts +++ b/src/models/StreamReceiverSeenEventModel.ts @@ -16,41 +16,41 @@ export default class StreamReceiverSeenEventModel > implements IEventModel { - public declare receiversHash: string; - public declare accountId: AccountId; - public declare config: BigIntString; - - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; + declare public receiversHash: string; + declare public accountId: AccountId; + declare public config: BigIntString; + declare public logIndex: number; + declare public blockNumber: number; + declare public blockTimestamp: Date; + declare public transactionHash: string; public static initialize(sequelize: Sequelize): void { this.init( { accountId: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, config: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, receiversHash: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, ...COMMON_EVENT_INIT_ATTRIBUTES, }, { sequelize, schema: getSchema(), - tableName: 'StreamReceiverSeenEvents', + tableName: 'stream_receiver_seen_events', + underscored: true, + timestamps: false, indexes: [ { fields: ['accountId'], - name: `IX_StreamReceiverSeenEvents_accountId`, + name: `idx_stream_receiver_seen_events_accountId`, unique: false, }, ], diff --git a/src/models/StreamsSetEventModel.ts b/src/models/StreamsSetEventModel.ts index a5c21ab..cad352b 100644 --- a/src/models/StreamsSetEventModel.ts +++ b/src/models/StreamsSetEventModel.ts @@ -16,61 +16,61 @@ export default class StreamsSetEventModel > implements IEventModel { - public declare accountId: AccountId; - public declare erc20: string; - public declare receiversHash: string; - public declare streamsHistoryHash: string; - public declare balance: BigIntString; - public declare maxEnd: BigIntString; - - // Common event log properties. - public declare logIndex: number; - public declare blockNumber: number; - public declare blockTimestamp: Date; - public declare transactionHash: string; + declare public accountId: AccountId; + declare public erc20: string; + declare public receiversHash: string; + declare public streamsHistoryHash: string; + declare public balance: BigIntString; + declare public maxEnd: BigIntString; + declare public logIndex: number; + declare public blockNumber: number; + declare public blockTimestamp: Date; + declare public transactionHash: string; public static initialize(sequelize: Sequelize): void { this.init( { accountId: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, erc20: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, receiversHash: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, streamsHistoryHash: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, balance: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, maxEnd: { - type: DataTypes.STRING, allowNull: false, + type: DataTypes.STRING, }, ...COMMON_EVENT_INIT_ATTRIBUTES, }, { sequelize, schema: getSchema(), - tableName: 'StreamsSetEvents', + tableName: 'streams_set_events', + underscored: true, + timestamps: false, indexes: [ { fields: ['receiversHash'], - name: `IX_StreamsSetEvents_receiversHash`, + name: `idx_streams_set_events_receiversHash`, unique: false, }, { fields: ['accountId'], - name: `IX_StreamsSetEvents}_accountId`, + name: `idx_streams_set_events_accountId`, unique: false, }, ], diff --git a/src/models/SubListModel.ts b/src/models/SubListModel.ts index 82eecc4..b941e9a 100644 --- a/src/models/SubListModel.ts +++ b/src/models/SubListModel.ts @@ -62,6 +62,7 @@ export default class SubListModel extends Model< sequelize, schema: getSchema(), tableName: 'sub_lists', + underscored: true, timestamps: true, indexes: [ { diff --git a/src/models/TransferEventModel.ts b/src/models/TransferEventModel.ts index 73d5e09..94934b4 100644 --- a/src/models/TransferEventModel.ts +++ b/src/models/TransferEventModel.ts @@ -45,6 +45,8 @@ export default class TransferEventModel sequelize, schema: getSchema(), tableName: 'transfer_events', + underscored: true, + timestamps: false, }, ); } diff --git a/src/models/_LastIndexedBlockModel.ts b/src/models/_LastIndexedBlockModel.ts index e002e65..91fe648 100644 --- a/src/models/_LastIndexedBlockModel.ts +++ b/src/models/_LastIndexedBlockModel.ts @@ -42,6 +42,7 @@ export default class _LastIndexedBlockModel extends Model< sequelize, schema: getSchema(), tableName: '_last_indexed_block', + underscored: true, }, ); } diff --git a/src/models/index.ts b/src/models/index.ts index 1466390..7ba16e6 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,9 +1,9 @@ export { default as SubListModel } from './SubListModel'; export { default as ProjectModel } from './ProjectModel'; -export { default as SplitReceiver } from './SplitReceiver'; export { default as DripListModel } from './DripListModel'; export { default as GivenEventModel } from './GivenEventModel'; export { default as SplitEventModel } from './SplitEventModel'; +export { default as SplitReceiverModel } from './SplitReceiverModel'; export { default as TransferEventModel } from './TransferEventModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; export { default as _LastIndexedBlockModel } from './_LastIndexedBlockModel'; diff --git a/src/queue/initJobProcessingQueue.ts b/src/queue/initJobProcessingQueue.ts index 7f42e05..b488fb2 100644 --- a/src/queue/initJobProcessingQueue.ts +++ b/src/queue/initJobProcessingQueue.ts @@ -47,9 +47,12 @@ export default async function initJobProcessingQueue() { await handler.afterHandle({ args: handleContext.event.args.concat(accountIdsToInvalidate), blockTimestamp: handleContext.event.blockTimestamp, + requestId: handleContext.id, }); } catch (error: any) { - logger.error(`❌ ${handler.name} 'afterHandle' error: ${error.message}.`); + logger.error( + `❌ [${handleContext.id}] ${handler.name} 'afterHandle' error: ${error.message}.`, + ); } }); } diff --git a/src/queue/queue.ts b/src/queue/queue.ts index 4b37c5b..1894496 100644 --- a/src/queue/queue.ts +++ b/src/queue/queue.ts @@ -28,19 +28,15 @@ eventProcessingQueue.on('error', (error: Error) => { }); eventProcessingQueue.on('job succeeded', (job) => { - logger.info(`✅ SUCCESS: Job with ID ${job} completed successfully.`); + logger.info(`✅ [${job}] completed successfully.`); }); eventProcessingQueue.on('job failed', (job, err) => { - logger.error( - `❌ FAILED: Job with ID ${job} failed with error '${err.message}'.`, - ); + logger.error(`❌ [${job}] failed: ${err.message}`); }); eventProcessingQueue.on('job retrying', (job, err) => { - logger.info( - `♻️ Job with ID ${job} failed with error '${err.message}' but is being retried...`, - ); + logger.info(`♻️ [${job}] failed (will be retried): '${err.message}`); }); export default eventProcessingQueue; diff --git a/src/utils/isLatestEvent.ts b/src/utils/isLatestEvent.ts index 71239d0..5e8114c 100644 --- a/src/utils/isLatestEvent.ts +++ b/src/utils/isLatestEvent.ts @@ -17,8 +17,8 @@ export async function isLatestEvent>( transaction, where, order: [ - ['blockNumber', 'DESC'], - ['logIndex', 'DESC'], + ['block_number', 'DESC'], + ['log_index', 'DESC'], ], }); From a24ba28688514190c8f42149a5eb0e438a58b1b8 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 24 Apr 2025 10:47:58 +0200 Subject: [PATCH 28/60] feat: improve logging and small db schema fixes --- src/core/LogManager.ts | 145 ---------------- src/core/ScopedLogger.ts | 163 ++++++++++++++++++ src/core/splitRules.ts | 8 +- .../20250414133746-initial_create.ts | 28 ++- .../AccountMetadataEmittedEventHandler.ts | 46 +++-- .../handlers/handleDripListMetadata.ts | 91 ++++++---- .../handleEcosystemMainAccountMetadata.ts | 90 ++++++---- .../handlers/handleProjectMetadata.ts | 93 +++++----- .../handlers/handleSubListMetadata.ts | 89 ++++++---- .../receiversRepository.ts | 12 +- src/eventHandlers/GivenEventHandler.ts | 21 ++- src/eventHandlers/SplitEventHandler.ts | 21 ++- .../SqueezedStreamsEventHandler.ts | 21 ++- .../StreamReceiverSeenEventHandler.ts | 30 ++-- src/eventHandlers/StreamsSetEventHandler.ts | 21 +-- src/eventHandlers/TransferEventHandler.ts | 28 +-- src/events/EventHandlerBase.ts | 2 +- src/models/ProjectModel.ts | 9 +- src/models/SubListModel.ts | 10 +- src/queue/queue.ts | 2 +- ...AccountMetadataEmittedEventHandler.test.ts | 11 +- tests/eventHandlers/GivenEventHandler.test.ts | 6 +- tests/eventHandlers/SplitEventHandler.test.ts | 6 +- .../SqueezedStreamsEventHandler.test.ts | 6 +- .../StreamReceiverSeenEventHandler.test.ts | 6 +- .../StreamsSetEventHandler.test.ts | 6 +- .../TransferEventHandler.test.ts | 8 +- 27 files changed, 525 insertions(+), 454 deletions(-) delete mode 100644 src/core/LogManager.ts create mode 100644 src/core/ScopedLogger.ts diff --git a/src/core/LogManager.ts b/src/core/LogManager.ts deleted file mode 100644 index bd3cc6b..0000000 --- a/src/core/LogManager.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { UUID } from 'crypto'; -import type { Model } from 'sequelize'; -import logger from './logger'; - -export type ChangedProperties = { - [key: string]: { old: any; new: any }; -}; - -export default class LogManager { - private readonly _logs: string[] = []; - private readonly _requestId: UUID; - - public constructor(requestId: UUID) { - this._requestId = requestId; - } - - public static nameOfType(type: { new (): T }) { - return type.prototype.constructor.name; - } - - public static getChangedProperties( - instance: T, - ): ChangedProperties | null { - const changedKeys = instance.changed(); - - if (!changedKeys) { - return null; - } - - const changedProps: ChangedProperties = {}; - - if (changedKeys && changedKeys.length > 0) { - for (const key of changedKeys) { - changedProps[key] = { - old: instance.previous(key), - new: instance.get(key), - }; - } - } - - return changedProps; - } - - public appendFindOrCreateLog( - type: { new (): T }, - isCreated: boolean, - id: string, - ): this { - this._logs.push( - `${ - isCreated - ? `Created a new ${LogManager.nameOfType(type)} with ID ${id}.` - : `${LogManager.nameOfType( - type, - )} with ID ${id} already exists. Probably it was created by another event. Skipping creation.` - }`, - ); - - return this; - } - - public appendCreateLog( - type: { new (): T }, - id: string, - ): this { - this._logs.push( - `Created a new ${LogManager.nameOfType(type)} with ID ${id}.`, - ); - - return this; - } - - public appendUpsertLog( - instance: T, - type: { new (): T }, - id: string, - ): this { - const changes = LogManager.getChangedProperties(instance); - - const formattedChanges = - changes && Object.keys(changes).length > 0 - ? `\n\tChanged properties:\n${JSON.stringify(changes, null, 2) - .split('\n') - .map((line) => `\t ${line}`) - .join('\n')}` - : `\n\tNo changes detected.`; - - this._logs.push( - `Upserted ${LogManager.nameOfType(type)} with ID ${id}${formattedChanges}`, - ); - - return this; - } - - public appendLog(log: string): this { - this._logs.push(log); - - return this; - } - - public appendUpdateLog( - instance: T, - type: { new (): T }, - id: string, - ): this { - const changes = LogManager.getChangedProperties(instance); - - const formattedChanges = - changes && Object.keys(changes).length > 0 - ? `\n\tChanged properties:\n${JSON.stringify(changes, null, 2) - .split('\n') - .map((line) => `\t ${line}`) - .join('\n')}` - : `\n\tNo changes detected.`; - - this._logs.push( - `Updated ${LogManager.nameOfType(type)} with ID ${id}:${formattedChanges}`, - ); - - return this; - } - - public logAllInfo(handler: string): void { - const formattedLogs = this._logs.map((log) => `\t - ${log}`).join('\n'); - const message = `${handler} completed successfully. The following happened:\n${formattedLogs}`; - - LogManager.logRequestInfo(message, this._requestId); - } - - public static logRequestDebug(message: string, requestId: UUID): void { - logger.debug(`[${requestId}] ${message}`); - } - - public static logRequestInfo(message: string, requestId: UUID): void { - logger.info(`[${requestId}] ${message}`); - } - - public static logRequestWarn(message: string, requestId: UUID): void { - logger.warn(`[${requestId}] ${message}`); - } - - public static logRequestError(message: string, requestId: UUID): void { - logger.error(`[${requestId}] ${message}`); - } -} diff --git a/src/core/ScopedLogger.ts b/src/core/ScopedLogger.ts new file mode 100644 index 0000000..30062f4 --- /dev/null +++ b/src/core/ScopedLogger.ts @@ -0,0 +1,163 @@ +/* eslint-disable no-dupe-class-members */ +import type { UUID } from 'crypto'; +import type { Model } from 'sequelize'; +import logger from './logger'; + +export type ChangedProperties = { + [key: string]: { old: any; new: any }; +}; + +/** + * ScopedLogger buffers context-aware messages for a single handler/request. + */ +export default class ScopedLogger { + private readonly _handler: string; + private readonly _requestId: UUID; + private readonly _buffer: string[] = []; + + constructor(handler: string, requestId: UUID) { + this._handler = handler; + this._requestId = requestId; + } + + /** + * Buffer a plain text message. + */ + public bufferMessage(message: string): this { + this._buffer.push(`[${this._requestId}] ${message}`); + return this; + } + + /** + * Buffer a creation event for a Sequelize model instance. + */ + public bufferCreation(opts: { + input: T; + type: abstract new (...args: any[]) => T; + id: string; + }): this { + const typeName = this._getClassName(opts.type); + this._buffer.push( + `[${this._requestId}] Created new ${typeName} (ID: ${opts.id}).`, + ); + + return this; + } + + /** + * Buffer an update event for a Sequelize model instance, including change details. + */ + public bufferUpdate(opts: { + input: T; + type: abstract new (...args: any[]) => T; + id: string; + }): this { + const changes = this._extractChanges(opts.input) ?? {}; + const changeKeys = Object.keys(changes); + const typeName = this._getClassName(opts.type); + + let summary: string; + if (changeKeys.length > 0) { + summary = `Updated ${typeName} with ${changeKeys.length} change${changeKeys.length > 1 ? 's' : ''}.`; + } else { + summary = `No changes detected on ${typeName}.`; + } + + const formattedChanges = this._formatChanges(changes); + this._buffer.push( + `[${this._requestId}] Processed ${typeName} (ID: ${opts.id}) — ${summary}${formattedChanges}`, + ); + + return this; + } + + /** + * Immediately log a single message at the given level (bypassing buffer). + */ + public log( + message: string, + level: 'info' | 'debug' | 'warn' | 'error' = 'info', + ): void { + const formatted = `[${this._requestId}] ${message}`; + switch (level) { + case 'debug': + logger.debug(formatted); + break; + case 'warn': + logger.warn(formatted); + break; + case 'error': + logger.error(formatted); + break; + case 'info': + default: + logger.info(formatted); + break; + } + } + + /** + * Flush all buffered messages as a single multi-line log entry. + */ + public flush(level: 'info' | 'debug' | 'warn' | 'error' = 'info'): void { + const content = this._buffer.map((line) => `\t - ${line}`).join('\n'); + const message = `[${this._requestId}] ${this._handler} completed successfully:\n${content}`; + + switch (level) { + case 'debug': + logger.debug(message); + break; + case 'warn': + logger.warn(message); + break; + case 'error': + logger.error(message); + break; + case 'info': + default: + logger.info(message); + break; + } + } + + private _formatChanges(changes: ChangedProperties | null): string { + if (!changes || Object.keys(changes).length === 0) { + return ''; + } + + const safe = Object.fromEntries( + Object.entries(changes).map(([key, { old, new: _new }]) => [ + key, + { + old: old === undefined ? null : old, + new: _new === undefined ? null : _new, + }, + ]), + ) as ChangedProperties; + + return `\n\tChanged properties:\n${JSON.stringify(safe, null, 2) + .split('\n') + .map((line) => `\t ${line}`) + .join('\n')}.`; + } + + private _extractChanges( + instance: T, + ): ChangedProperties | null { + const changedKeys = instance.changed(); + if (!changedKeys) return null; + + return changedKeys.reduce((acc, key) => { + acc[key] = { + old: instance.previous(key), + new: instance.get(key), + }; + + return acc; + }, {} as ChangedProperties); + } + + private _getClassName(ctor: abstract new (...args: any[]) => T): string { + return ctor.prototype.constructor.name; + } +} diff --git a/src/core/splitRules.ts b/src/core/splitRules.ts index f5562eb..59db915 100644 --- a/src/core/splitRules.ts +++ b/src/core/splitRules.ts @@ -62,22 +62,22 @@ const SPLIT_RULES = Object.freeze([ { senderAccountType: 'sub_list', receiverAccountType: 'address', - relationshipType: 'sub_list_receiver', + relationshipType: 'sub_list_link', }, { senderAccountType: 'sub_list', receiverAccountType: 'drip_list', - relationshipType: 'sub_list_receiver', + relationshipType: 'sub_list_link', }, { senderAccountType: 'sub_list', receiverAccountType: 'project', - relationshipType: 'sub_list_receiver', + relationshipType: 'sub_list_link', }, { senderAccountType: 'sub_list', receiverAccountType: 'sub_list', - relationshipType: 'sub_list_receiver', + relationshipType: 'sub_list_link', }, ] as const); diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index 784d117..43b1da6 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -35,11 +35,7 @@ export async function up({ context: sequelize }: any): Promise { await queryInterface.sequelize.query(` CREATE TYPE ${schema}.project_verification_status AS ENUM ( 'claimed', - 'owner_update_requested', - 'owner_updated', - 'unclaimed', - 'pending_owner', - 'pending_metadata' + 'unclaimed' ); `); @@ -453,11 +449,15 @@ async function createSubListsEventsTable( primaryKey: true, type: DataTypes.STRING, }, + parentAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, parentAccountType: { allowNull: false, type: literal(`${schema}.account_type`).val as unknown as DataType, }, - parentId: { + rootAccountId: { allowNull: false, type: DataTypes.STRING, }, @@ -465,10 +465,6 @@ async function createSubListsEventsTable( allowNull: false, type: literal(`${schema}.account_type`).val as unknown as DataType, }, - rootId: { - allowNull: false, - type: DataTypes.STRING, - }, lastProcessedIpfsHash: { allowNull: false, type: DataTypes.TEXT, @@ -499,9 +495,9 @@ async function createSubListsEventsTable( schema, tableName: `sub_lists`, }, - transformFieldArrayToSnakeCase(['parentId']), + transformFieldArrayToSnakeCase(['parentAccountId']), { - name: 'idx_sub_lists_parent_id', + name: 'idx_sub_lists_parent_account_id', }, ); await queryInterface.addIndex( @@ -509,9 +505,9 @@ async function createSubListsEventsTable( schema, tableName: `sub_lists`, }, - transformFieldArrayToSnakeCase(['rootId']), + transformFieldArrayToSnakeCase(['rootAccountId']), { - name: 'idx_sub_lists_root_id', + name: 'idx_sub_lists_root_account_id', }, ); } @@ -623,10 +619,6 @@ async function createEcosystemMainAccountsTable( allowNull: true, type: DataTypes.STRING, }, - latestVotingRoundId: { - allowNull: true, - type: DataTypes.UUID, - }, description: { allowNull: true, type: DataTypes.TEXT, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index ba843b0..830f164 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -4,7 +4,6 @@ import type { AccountId } from '../../core/types'; import EventHandlerBase from '../../events/EventHandlerBase'; import { DRIPS_APP_USER_METADATA_KEY } from '../../core/constants'; import handleProjectMetadata from './handlers/handleProjectMetadata'; -import LogManager from '../../core/LogManager'; import { isImmutableSplitsDriverId, isNftDriverId, @@ -28,7 +27,7 @@ import { getCurrentSplitReceiversBySender } from './receiversRepository'; import { isLatestEvent } from '../../utils/isLatestEvent'; import type { AccountMetadataEmittedEvent } from '../../../contracts/CURRENT_NETWORK/Drips'; import { AccountMetadataEmittedEventModel } from '../../models'; -import logger from '../../core/logger'; +import ScopedLogger from '../../core/ScopedLogger'; export default class AccountMetadataEmittedEventHandler extends EventHandlerBase<'AccountMetadataEmitted(uint256,bytes32,bytes)'> { public readonly eventSignatures = [ @@ -51,7 +50,9 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase const ipfsHash = convertToIpfsHash(value); - LogManager.logRequestInfo( + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log( [ `📥 ${this.name} is processing ${eventSignature}:`, ` - key: ${toUtf8String(key)} (raw: ${key})`, @@ -60,30 +61,25 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase ` - logIndex: ${logIndex}`, ` - txHash: ${transactionHash}`, ].join('\n'), - requestId, ); if (!this._isEmittedByTheDripsApp(key)) { - LogManager.logRequestInfo( + scopedLogger.log( `Skipping ${eventSignature} event: key '${key}' not emitted by the Drips App.`, - requestId, ); return; } if (!this._canProcessDriverType(accountId)) { - LogManager.logRequestInfo( + scopedLogger.log( `Skipping ${eventSignature} event: accountId '${accountId}' is not of a Driver that can be processed.`, - requestId, ); return; } await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - const accountMetadataEmittedEvent = await AccountMetadataEmittedEventModel.create( { @@ -100,10 +96,11 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase }, ); - logManager.appendCreateLog( - AccountMetadataEmittedEventModel, - `${accountMetadataEmittedEvent.transactionHash}-${accountMetadataEmittedEvent.logIndex}`, - ); + scopedLogger.bufferCreation({ + type: AccountMetadataEmittedEventModel, + input: accountMetadataEmittedEvent, + id: `${accountMetadataEmittedEvent.transactionHash}-${accountMetadataEmittedEvent.logIndex}`, + }); // Only process metadata if this is the latest event. if ( @@ -122,7 +119,7 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase if (isRepoDriverId(accountId)) { await handleProjectMetadata({ ipfsHash, - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId: convertToRepoDriverId(accountId), @@ -136,7 +133,7 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase await handleDripListMetadata({ ipfsHash, metadata, - logManager, + scopedLogger, transaction, blockNumber, blockTimestamp, @@ -148,7 +145,7 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase await handleEcosystemMainAccountMetadata({ ipfsHash, metadata, - logManager, + scopedLogger, transaction, blockNumber, blockTimestamp, @@ -160,14 +157,14 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase if (isImmutableSplitsDriverId(accountId)) { await handleSubListMetadata({ ipfsHash, - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId: convertToImmutableSplitsDriverId(accountId), }); } - logManager.logAllInfo(this.name); + scopedLogger.flush(); }); } @@ -177,18 +174,17 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase }: EventHandlerRequest<'AccountMetadataEmitted(uint256,bytes32,bytes)'>): Promise<{ accountIdsToInvalidate: AccountId[]; }> { - logger.info( - `[${requestId}] ${this.name} is gathering accountIds to invalidate...`, - ); + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log(`${this.name} is gathering accountIds to invalidate...`); const [accountId] = args as AccountMetadataEmittedEvent.OutputTuple; const accountIdsToInvalidate = await getCurrentSplitReceiversBySender( convertToAccountId(accountId), ); - - logger.info( - `[${requestId}] ${this.name} account IDs to invalidate: ${accountIdsToInvalidate.join( + scopedLogger.log( + `${this.name} account IDs to invalidate: ${accountIdsToInvalidate.join( ', ', )}`, ); diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index d1b59f6..97b5a5e 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -5,7 +5,7 @@ import type { UUID } from 'crypto'; import type { z } from 'zod'; import type { IpfsHash, NftDriverId } from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; -import type LogManager from '../../../core/LogManager'; +import type ScopedLogger from '../../../core/ScopedLogger'; import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; import appSettings from '../../../config/appSettings'; @@ -32,7 +32,7 @@ type Params = { ipfsHash: IpfsHash; blockNumber: number; blockTimestamp: Date; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; emitterAccountId: NftDriverId; metadata: AnyVersion; @@ -41,7 +41,7 @@ type Params = { export default async function handleDripListMetadata({ ipfsHash, metadata, - logManager, + scopedLogger, blockNumber, transaction, blockTimestamp, @@ -50,7 +50,7 @@ export default async function handleDripListMetadata({ validateMetadata(metadata); if (metadata.describes.accountId !== emitterAccountId) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨🕵️‍♂️ Skipped Drip List ${emitterAccountId} metadata processing: metadata describes account ID '${metadata.describes.accountId}' but metadata emitted by '${emitterAccountId}'.`, ); @@ -68,7 +68,7 @@ export default async function handleDripListMetadata({ ); if (!isMatch) { - logManager.appendLog( + scopedLogger.bufferMessage( `Skipped Drip List ${emitterAccountId} metadata processing: on-chain splits hash '${onChainHash}' does not match hash '${actualHash}' calculated from metadata.`, ); @@ -80,7 +80,7 @@ export default async function handleDripListMetadata({ ); if (!areProjectsValid) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨🕵️‍♂️ Skipped Drip List ${emitterAccountId} metadata processing: ${message}`, ); @@ -92,7 +92,7 @@ export default async function handleDripListMetadata({ await upsertDripList({ ipfsHash, metadata, - logManager, + scopedLogger, blockNumber, transaction, }); @@ -100,7 +100,7 @@ export default async function handleDripListMetadata({ deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId, @@ -111,51 +111,70 @@ export default async function handleDripListMetadata({ async function upsertDripList({ ipfsHash, metadata, - logManager, + scopedLogger, blockNumber, transaction, }: { ipfsHash: IpfsHash; blockNumber: number; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; metadata: AnyVersion; }) { - const [dripList] = await DripListModel.upsert( - { - accountId: convertToNftDriverId(metadata.describes.accountId), - name: metadata.name ?? null, - description: - 'description' in metadata ? metadata.description || null : null, - latestVotingRoundId: - 'latestVotingRoundId' in metadata - ? (metadata.latestVotingRoundId as UUID) || null - : null, - lastProcessedIpfsHash: ipfsHash, - isVisible: - blockNumber > appSettings.visibilityThresholdBlockNumber && - 'isVisible' in metadata - ? metadata.isVisible - : true, + const accountId = convertToNftDriverId(metadata.describes.accountId); + + const values = { + accountId, + name: metadata.name ?? null, + description: + 'description' in metadata ? metadata.description || null : null, + latestVotingRoundId: + 'latestVotingRoundId' in metadata + ? (metadata.latestVotingRoundId as UUID) || null + : null, + lastProcessedIpfsHash: ipfsHash, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, + }; + + const [dripList, isCreation] = await DripListModel.findOrCreate({ + where: { accountId }, + defaults: { + ...values, isValid: false, // Until the `Transfer` event is processed. }, - { - transaction, - }, - ); + transaction, + }); - logManager.appendUpsertLog(dripList, DripListModel, dripList.accountId); + if (!isCreation) { + scopedLogger.bufferUpdate({ + id: accountId, + type: DripListModel, + input: dripList, + }); + + await dripList.update(values, { transaction }); + } else { + scopedLogger.bufferCreation({ + id: accountId, + type: DripListModel, + input: dripList, + }); + } } async function createNewSplitReceivers({ splitReceivers, - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId, }: { blockTimestamp: Date; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; emitterAccountId: NftDriverId; splitReceivers: ( @@ -170,7 +189,7 @@ async function createNewSplitReceivers({ case 'repoDriver': assertIsRepoDriverId(receiver.accountId); return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { @@ -187,7 +206,7 @@ async function createNewSplitReceivers({ case 'dripList': assertIsNftDriverId(receiver.accountId); return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { @@ -204,7 +223,7 @@ async function createNewSplitReceivers({ case 'address': assertIsAddressDiverId(receiver.accountId); return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index c3a93cf..b64fcbe 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -1,7 +1,7 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Transaction } from 'sequelize'; import type { z } from 'zod'; -import type LogManager from '../../../core/LogManager'; +import type ScopedLogger from '../../../core/ScopedLogger'; import type { IpfsHash, NftDriverId } from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; import verifySplitsReceivers from '../verifySplitsReceivers'; @@ -25,7 +25,7 @@ type Params = { ipfsHash: IpfsHash; blockNumber: number; blockTimestamp: Date; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; emitterAccountId: NftDriverId; metadata: AnyVersion; @@ -34,7 +34,7 @@ type Params = { export default async function handleEcosystemMainAccountMetadata({ ipfsHash, metadata, - logManager, + scopedLogger, blockNumber, transaction, blockTimestamp, @@ -43,7 +43,7 @@ export default async function handleEcosystemMainAccountMetadata({ validateMetadata(metadata); if (metadata.describes.accountId !== emitterAccountId) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨🕵️‍♂️ Skipped Ecosystem Main Account ${emitterAccountId} metadata processing: metadata describes account ID '${metadata.describes.accountId}' but metadata emitted by '${emitterAccountId}'.`, ); @@ -56,7 +56,7 @@ export default async function handleEcosystemMainAccountMetadata({ ); if (!isMatch) { - logManager.appendLog( + scopedLogger.bufferMessage( `Skipped Ecosystem Main Account ${emitterAccountId} metadata processing: on-chain splits hash '${onChainHash}' does not match hash '${actualHash}' calculated from metadata.`, ); @@ -68,7 +68,7 @@ export default async function handleEcosystemMainAccountMetadata({ ); if (!areProjectsValid) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨🕵️‍♂️ Skipped Ecosystem Main Account ${emitterAccountId} metadata processing: ${message}`, ); } @@ -78,7 +78,7 @@ export default async function handleEcosystemMainAccountMetadata({ await upsertEcosystemMainAccount({ ipfsHash, metadata, - logManager, + scopedLogger, blockNumber, transaction, }); @@ -86,7 +86,7 @@ export default async function handleEcosystemMainAccountMetadata({ deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId, @@ -97,51 +97,67 @@ export default async function handleEcosystemMainAccountMetadata({ async function upsertEcosystemMainAccount({ ipfsHash, metadata, - logManager, + scopedLogger, blockNumber, transaction, }: { ipfsHash: IpfsHash; blockNumber: number; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; metadata: AnyVersion; }) { - const [dripList] = await EcosystemMainAccountModel.upsert( - { - accountId: convertToNftDriverId(metadata.describes.accountId), - name: metadata.name ?? null, - description: - 'description' in metadata ? metadata.description || null : null, - lastProcessedIpfsHash: ipfsHash, - isVisible: - blockNumber > appSettings.visibilityThresholdBlockNumber && - 'isVisible' in metadata - ? metadata.isVisible - : true, - isValid: false, // Until the `Transfer` event is processed. - }, - { + const accountId = convertToNftDriverId(metadata.describes.accountId); + + const values = { + accountId, + name: metadata.name ?? null, + description: + 'description' in metadata ? metadata.description || null : null, + lastProcessedIpfsHash: ipfsHash, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, + }; + + const [ecosystemMainAccount, isCreation] = + await EcosystemMainAccountModel.findOrCreate({ + where: { accountId }, + defaults: { + ...values, + isValid: false, // Until the `Transfer` event is processed. + }, transaction, - }, - ); - - logManager.appendUpsertLog( - dripList, - EcosystemMainAccountModel, - dripList.accountId, - ); + }); + + if (!isCreation) { + scopedLogger.bufferUpdate({ + input: ecosystemMainAccount, + id: ecosystemMainAccount.accountId, + type: EcosystemMainAccountModel, + }); + + await ecosystemMainAccount.update(values, { transaction }); + } else { + scopedLogger.bufferCreation({ + input: ecosystemMainAccount, + id: ecosystemMainAccount.accountId, + type: EcosystemMainAccountModel, + }); + } } async function createNewSplitReceivers({ splitReceivers, - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId, }: { blockTimestamp: Date; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; emitterAccountId: NftDriverId; splitReceivers: ( @@ -154,7 +170,7 @@ async function createNewSplitReceivers({ case 'repoDriver': assertIsRepoDriverId(receiver.accountId); return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { @@ -171,7 +187,7 @@ async function createNewSplitReceivers({ case 'subList': assertIsImmutableSplitsDriverId(receiver.accountId); return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index dcc3caa..2e2c495 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -4,7 +4,7 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import { ZeroAddress } from 'ethers'; import { ProjectModel } from '../../../models'; import type { repoDriverAccountMetadataParser } from '../../../metadata/schemas'; -import type LogManager from '../../../core/LogManager'; +import type ScopedLogger from '../../../core/ScopedLogger'; import { calculateProjectStatus, verifyProjectSources, @@ -32,14 +32,14 @@ import { type Params = { ipfsHash: IpfsHash; blockTimestamp: Date; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; emitterAccountId: RepoDriverId; }; export default async function handleProjectMetadata({ ipfsHash, - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId, @@ -47,7 +47,7 @@ export default async function handleProjectMetadata({ const metadata = await getProjectMetadata(ipfsHash); if (metadata.describes.accountId !== emitterAccountId) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: metadata describes account ID '${metadata.describes.accountId}' but metadata emitted by '${emitterAccountId}'.`, ); @@ -60,7 +60,7 @@ export default async function handleProjectMetadata({ ); if (!isMatch) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨 Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: on-chain splits hash '${onChainHash}' does not match '${actualHash}' calculated from metadata.`, ); @@ -74,7 +74,7 @@ export default async function handleProjectMetadata({ await verifyProjectSources(projectReceivers); if (!areProjectsValid) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: ${message}`, ); @@ -83,7 +83,7 @@ export default async function handleProjectMetadata({ const onChainOwner = await repoDriverContract.ownerOf(emitterAccountId); if (!onChainOwner || onChainOwner === ZeroAddress) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: on-chain owner is not set.`, ); } @@ -93,7 +93,7 @@ export default async function handleProjectMetadata({ await upsertProject({ metadata, ipfsHash, - logManager, + scopedLogger, transaction, blockTimestamp, onChainOwner: onChainOwner as Address, @@ -102,7 +102,7 @@ export default async function handleProjectMetadata({ deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId, @@ -113,14 +113,14 @@ export default async function handleProjectMetadata({ async function upsertProject({ ipfsHash, metadata, - logManager, + scopedLogger, transaction, onChainOwner, blockTimestamp, }: { ipfsHash: IpfsHash; blockTimestamp: Date; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; onChainOwner: Address; metadata: AnyVersion; @@ -128,7 +128,7 @@ async function upsertProject({ const { color, source: { forge, ownerName, repoName, url }, - describes: { accountId }, + describes, } = metadata; function getEmoji(): string | null { @@ -147,39 +147,54 @@ async function upsertProject({ return null; } - const [project] = await ProjectModel.upsert( - { - accountId: convertToRepoDriverId(accountId), - url, - forge, - emoji: getEmoji(), - color, - name: `${ownerName}/${repoName}` as ProjectName, - avatarCid: getAvatarCid(), - verificationStatus: calculateProjectStatus(onChainOwner), - isVisible: 'isVisible' in metadata ? metadata.isVisible : true, // Projects without `isVisible` field (V4 and below) are considered visible by default. - lastProcessedIpfsHash: ipfsHash, - ownerAddress: getAddress(onChainOwner), - ownerAccountId: convertToAddressDriverId(onChainOwner), - claimedAt: blockTimestamp, - }, - { - transaction, - }, - ); + const accountId = convertToRepoDriverId(describes.accountId); + + const values = { + accountId, + url, + forge, + emoji: getEmoji(), + color, + name: `${ownerName}/${repoName}` as ProjectName, + avatarCid: getAvatarCid(), + verificationStatus: calculateProjectStatus(onChainOwner), + isVisible: 'isVisible' in metadata ? metadata.isVisible : true, // Projects without `isVisible` field (V4 and below) are considered visible by default. + lastProcessedIpfsHash: ipfsHash, + ownerAddress: getAddress(onChainOwner), + ownerAccountId: convertToAddressDriverId(onChainOwner), + claimedAt: blockTimestamp, + }; + const [project, isCreation] = await ProjectModel.findOrCreate({ + where: { accountId }, + defaults: values, + }); - logManager.appendUpsertLog(project, ProjectModel, project.accountId); + if (!isCreation) { + scopedLogger.bufferUpdate({ + type: ProjectModel, + input: project, + id: project.accountId, + }); + + await project.update(values, { transaction }); + } else { + scopedLogger.bufferCreation({ + type: ProjectModel, + input: project, + id: project.accountId, + }); + } } async function createNewSplitReceivers({ - logManager, + scopedLogger, transaction, splitReceivers, blockTimestamp, emitterAccountId, }: { blockTimestamp: Date; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; emitterAccountId: RepoDriverId; splitReceivers: AnyVersion['splits']; @@ -190,7 +205,7 @@ async function createNewSplitReceivers({ assertIsAddressDiverId(maintainer.accountId); return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { @@ -208,7 +223,7 @@ async function createNewSplitReceivers({ const dependencyPromises = dependencies.map(async (dependency) => { if (isRepoDriverId(dependency.accountId)) { return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { @@ -225,7 +240,7 @@ async function createNewSplitReceivers({ if (isAddressDriverId(dependency.accountId)) { return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { @@ -242,7 +257,7 @@ async function createNewSplitReceivers({ if (isNftDriverId(dependency.accountId)) { return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 5c1de51..67335a6 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -2,7 +2,7 @@ import type { Transaction } from 'sequelize'; import type { z } from 'zod'; import type { AnyVersion } from '@efstajas/versioned-parser'; -import type LogManager from '../../../core/LogManager'; +import type ScopedLogger from '../../../core/ScopedLogger'; import type { ImmutableSplitsDriverId, IpfsHash } from '../../../core/types'; import { EcosystemMainAccountModel, SubListModel } from '../../../models'; import { getImmutableSpitsDriverMetadata } from '../../../utils/metadataUtils'; @@ -28,19 +28,20 @@ import { assertIsNftDriverId, assertIsRepoDriverId, convertToAccountId, + convertToImmutableSplitsDriverId, } from '../../../utils/accountIdUtils'; type Params = { ipfsHash: IpfsHash; blockTimestamp: Date; - logManager: LogManager; + scopedLogger: ScopedLogger; emitterAccountId: ImmutableSplitsDriverId; transaction: Transaction; }; export default async function handleSubListMetadata({ ipfsHash, - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId, @@ -51,7 +52,7 @@ export default async function handleSubListMetadata({ metadata.parent.type !== 'ecosystem' || metadata.root.type !== 'ecosystem' ) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨 Skipped Sub-List metadata processing: parent and root must be of type 'ecosystem'.`, ); @@ -64,7 +65,7 @@ export default async function handleSubListMetadata({ ); if (!isMatch) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨 Skipped Sub-List metadata processing: on-chain splits hash '${onChainHash}' does not match '${actualHash}' calculated from metadata.`, ); @@ -76,7 +77,7 @@ export default async function handleSubListMetadata({ ); if (!areProjectsValid) { - logManager.appendLog( + scopedLogger.bufferMessage( `🚨🕵️‍♂️ Skipped Sub-List metadata processing: ${message}`, ); @@ -90,7 +91,7 @@ export default async function handleSubListMetadata({ await upsertSubList({ metadata, ipfsHash, - logManager, + scopedLogger, transaction, emitterAccountId, }); @@ -98,7 +99,7 @@ export default async function handleSubListMetadata({ deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId, @@ -109,44 +110,60 @@ export default async function handleSubListMetadata({ async function upsertSubList({ ipfsHash, metadata, - logManager, + scopedLogger, transaction, emitterAccountId, }: { ipfsHash: IpfsHash; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; emitterAccountId: ImmutableSplitsDriverId; metadata: AnyVersion; }): Promise { - const [subList] = await SubListModel.upsert( - { - accountId: emitterAccountId, - parentAccountType: - METADATA_RECEIVER_TYPE_TO_ACCOUNT_TYPE[metadata.parent.type], - parentId: convertToAccountId(metadata.parent.accountId), - rootAccountType: - METADATA_RECEIVER_TYPE_TO_ACCOUNT_TYPE.ecosystem_main_account, - rootId: convertToAccountId(metadata.root.accountId), - lastProcessedIpfsHash: ipfsHash, - }, - { - transaction, - }, - ); + const values = { + accountId: emitterAccountId, + parentAccountType: + METADATA_RECEIVER_TYPE_TO_ACCOUNT_TYPE[metadata.parent.type], + parentAccountId: convertToAccountId(metadata.parent.accountId), + rootAccountType: METADATA_RECEIVER_TYPE_TO_ACCOUNT_TYPE[metadata.root.type], + rootAccountId: convertToAccountId(metadata.root.accountId), + lastProcessedIpfsHash: ipfsHash, + }; - logManager.appendUpsertLog(subList, SubListModel, subList.accountId); + const accountId = convertToImmutableSplitsDriverId(emitterAccountId); + + const [subList, isCreation] = await SubListModel.findOrCreate({ + where: { accountId }, + transaction, + defaults: values, + }); + + if (!isCreation) { + scopedLogger.bufferUpdate({ + input: subList, + type: SubListModel, + id: subList.accountId, + }); + + await subList.update(values, { transaction }); + } else { + scopedLogger.bufferCreation({ + input: subList, + type: SubListModel, + id: subList.accountId, + }); + } } async function createNewSplitReceivers({ receivers, - logManager, + scopedLogger, transaction, blockTimestamp, emitterAccountId, }: { blockTimestamp: Date; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; emitterAccountId: ImmutableSplitsDriverId; receivers: ( @@ -161,7 +178,7 @@ async function createNewSplitReceivers({ case 'repoDriver': assertIsRepoDriverId(receiver.accountId); return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { @@ -169,7 +186,7 @@ async function createNewSplitReceivers({ senderAccountType: 'sub_list', receiverAccountId: receiver.accountId, receiverAccountType: 'project', - relationshipType: 'sub_list_receiver', + relationshipType: 'sub_list_link', weight: receiver.weight, blockTimestamp, }, @@ -178,7 +195,7 @@ async function createNewSplitReceivers({ case 'subList': assertIsImmutableSplitsDriverId(receiver.accountId); return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { @@ -186,7 +203,7 @@ async function createNewSplitReceivers({ senderAccountType: 'sub_list', receiverAccountId: receiver.accountId, receiverAccountType: 'sub_list', - relationshipType: 'sub_list_receiver', + relationshipType: 'sub_list_link', weight: receiver.weight, blockTimestamp, }, @@ -195,7 +212,7 @@ async function createNewSplitReceivers({ case 'dripList': assertIsNftDriverId(receiver.accountId); return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { @@ -203,7 +220,7 @@ async function createNewSplitReceivers({ senderAccountType: 'sub_list', receiverAccountId: receiver.accountId, receiverAccountType: 'drip_list', - relationshipType: 'sub_list_receiver', + relationshipType: 'sub_list_link', weight: receiver.weight, blockTimestamp, }, @@ -212,7 +229,7 @@ async function createNewSplitReceivers({ case 'address': assertIsAddressDiverId(receiver.accountId); return createSplitReceiver({ - logManager, + scopedLogger, transaction, blockTimestamp, splitReceiverShape: { @@ -220,7 +237,7 @@ async function createNewSplitReceivers({ senderAccountType: 'sub_list', receiverAccountId: receiver.accountId, receiverAccountType: 'address', - relationshipType: 'sub_list_receiver', + relationshipType: 'sub_list_link', weight: receiver.weight, blockTimestamp, }, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts index 60b7ffb..7c51bd0 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts @@ -1,6 +1,6 @@ import { type Transaction } from 'sequelize'; import type { AccountId } from '../../core/types'; -import type LogManager from '../../core/LogManager'; +import type ScopedLogger from '../../core/ScopedLogger'; import type { SplitReceiverShape } from '../../core/splitRules'; import { SplitReceiverModel, StreamReceiverSeenEventModel } from '../../models'; @@ -17,12 +17,12 @@ export async function deleteExistingSplitReceivers( } export async function createSplitReceiver({ - logManager, + scopedLogger, transaction, splitReceiverShape, }: { blockTimestamp: Date; - logManager: LogManager; + scopedLogger: ScopedLogger; transaction: Transaction; splitReceiverShape: SplitReceiverShape; }) { @@ -31,7 +31,11 @@ export async function createSplitReceiver({ { transaction }, ); - logManager.appendCreateLog(SplitReceiverModel, splitReceiver.id.toString()); + scopedLogger.bufferCreation({ + input: splitReceiver, + type: SplitReceiverModel, + id: splitReceiver.id.toString(), + }); } export async function getCurrentSplitReceiversBySender( diff --git a/src/eventHandlers/GivenEventHandler.ts b/src/eventHandlers/GivenEventHandler.ts index 0cedffe..f623d33 100644 --- a/src/eventHandlers/GivenEventHandler.ts +++ b/src/eventHandlers/GivenEventHandler.ts @@ -1,5 +1,5 @@ import EventHandlerBase from '../events/EventHandlerBase'; -import LogManager from '../core/LogManager'; +import ScopedLogger from '../core/ScopedLogger'; import { convertToAccountId } from '../utils/accountIdUtils'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import { GivenEventModel } from '../models'; @@ -27,7 +27,9 @@ export default class GivenEventHandler extends EventHandlerBase<'Given(uint256,u const erc20 = toAddress(rawErc20); const amt = toBigIntString(rawAmt.toString()); - LogManager.logRequestInfo( + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log( `📥 ${this.name} is processing the following ${request.event.eventSignature}: \r\t - accountId: ${accountId} \r\t - receiver: ${rawReceiver} @@ -35,12 +37,9 @@ export default class GivenEventHandler extends EventHandlerBase<'Given(uint256,u \r\t - amt: ${rawAmt} \r\t - logIndex: ${logIndex} \r\t - tx hash: ${transactionHash}`, - requestId, ); await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - const givenEvent = await GivenEventModel.create( { accountId, @@ -55,13 +54,13 @@ export default class GivenEventHandler extends EventHandlerBase<'Given(uint256,u { transaction }, ); - logManager.appendFindOrCreateLog( - GivenEventModel, - true, - `${givenEvent.transactionHash}-${givenEvent.logIndex}`, - ); + scopedLogger.bufferCreation({ + input: givenEvent, + type: GivenEventModel, + id: `${givenEvent.transactionHash}-${givenEvent.logIndex}`, + }); - logManager.logAllInfo(this.name); + scopedLogger.flush(); }); } } diff --git a/src/eventHandlers/SplitEventHandler.ts b/src/eventHandlers/SplitEventHandler.ts index 65ee216..d505c91 100644 --- a/src/eventHandlers/SplitEventHandler.ts +++ b/src/eventHandlers/SplitEventHandler.ts @@ -1,5 +1,5 @@ import EventHandlerBase from '../events/EventHandlerBase'; -import LogManager from '../core/LogManager'; +import ScopedLogger from '../core/ScopedLogger'; import { convertToAccountId } from '../utils/accountIdUtils'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import { SplitEventModel } from '../models'; @@ -27,7 +27,9 @@ export default class SplitEventHandler extends EventHandlerBase<'Split(uint256,u const erc20 = toAddress(rawErc20); const amt = toBigIntString(rawAmt.toString()); - LogManager.logRequestInfo( + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log( `📥 ${this.name} is processing the following ${request.event.eventSignature}: \r\t - accountId: ${accountId} \r\t - receiver: ${rawReceiver} @@ -35,12 +37,9 @@ export default class SplitEventHandler extends EventHandlerBase<'Split(uint256,u \r\t - amt: ${rawAmt} \r\t - logIndex: ${logIndex} \r\t - tx hash: ${transactionHash}`, - requestId, ); await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - const splitEvent = await SplitEventModel.create( { accountId, @@ -55,13 +54,13 @@ export default class SplitEventHandler extends EventHandlerBase<'Split(uint256,u { transaction }, ); - logManager.appendFindOrCreateLog( - SplitEventModel, - true, - `${splitEvent.transactionHash}-${splitEvent.logIndex}`, - ); + scopedLogger.bufferCreation({ + input: splitEvent, + type: SplitEventModel, + id: `${splitEvent.transactionHash}-${splitEvent.logIndex}`, + }); - logManager.logAllInfo(this.name); + scopedLogger.flush(); }); } } diff --git a/src/eventHandlers/SqueezedStreamsEventHandler.ts b/src/eventHandlers/SqueezedStreamsEventHandler.ts index 6653f18..791e904 100644 --- a/src/eventHandlers/SqueezedStreamsEventHandler.ts +++ b/src/eventHandlers/SqueezedStreamsEventHandler.ts @@ -1,5 +1,5 @@ import EventHandlerBase from '../events/EventHandlerBase'; -import LogManager from '../core/LogManager'; +import ScopedLogger from '../core/ScopedLogger'; import { convertToAccountId } from '../utils/accountIdUtils'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import { dbConnection } from '../db/database'; @@ -36,7 +36,9 @@ export default class SqueezedStreamsEventHandler extends EventHandlerBase<'Squee const streamsHistoryHashes = SqueezedStreamsEventModel.toStreamHistoryHashes(rawStreamsHistoryHashes); - LogManager.logRequestInfo( + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log( `📥 ${this.name} is processing the following ${request.event.eventSignature}: \r\t - accountId: ${accountId} \r\t - erc20: ${erc20} @@ -45,12 +47,9 @@ export default class SqueezedStreamsEventHandler extends EventHandlerBase<'Squee \r\t - streamsHistoryHashes: ${streamsHistoryHashes} \r\t - logIndex: ${logIndex} \r\t - tx hash: ${transactionHash}`, - requestId, ); await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - const transferEvent = await SqueezedStreamsEventModel.create( { accountId, @@ -66,13 +65,13 @@ export default class SqueezedStreamsEventHandler extends EventHandlerBase<'Squee { transaction }, ); - logManager.appendFindOrCreateLog( - SqueezedStreamsEventModel, - true, - `${transferEvent.transactionHash}-${transferEvent.logIndex}`, - ); + scopedLogger.bufferCreation({ + input: transferEvent, + type: SqueezedStreamsEventModel, + id: `${transferEvent.transactionHash}-${transferEvent.logIndex}`, + }); - logManager.logAllInfo(this.name); + scopedLogger.flush(); }); } } diff --git a/src/eventHandlers/StreamReceiverSeenEventHandler.ts b/src/eventHandlers/StreamReceiverSeenEventHandler.ts index 97712e8..d5a29c0 100644 --- a/src/eventHandlers/StreamReceiverSeenEventHandler.ts +++ b/src/eventHandlers/StreamReceiverSeenEventHandler.ts @@ -1,6 +1,5 @@ import type { StreamReceiverSeenEvent } from '../../contracts/CURRENT_NETWORK/Drips'; -import logger from '../core/logger'; -import LogManager from '../core/LogManager'; +import ScopedLogger from '../core/ScopedLogger'; import type { AccountId } from '../core/types'; import { dbConnection } from '../db/database'; import EventHandlerBase from '../events/EventHandlerBase'; @@ -29,19 +28,18 @@ export default class StreamReceiverSeenEventHandler extends EventHandlerBase<'St const accountId = convertToAccountId(rawAccountId); const config = toBigIntString(rawConfig.toString()); - LogManager.logRequestInfo( + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log( `📥 ${this.name} is processing the following ${request.event.eventSignature}: \r\t - receiversHash: ${rawReceiversHash} \r\t - accountId: ${accountId} \r\t - config: ${config} \r\t - logIndex: ${logIndex} \r\t - tx hash: ${transactionHash}`, - requestId, ); await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - const streamReceiverSeenEvent = await StreamReceiverSeenEventModel.create( { receiversHash: rawReceiversHash, @@ -57,11 +55,11 @@ export default class StreamReceiverSeenEventHandler extends EventHandlerBase<'St }, ); - logManager.appendFindOrCreateLog( - StreamReceiverSeenEventModel, - true, - `${streamReceiverSeenEvent.transactionHash}-${streamReceiverSeenEvent.logIndex}`, - ); + scopedLogger.bufferCreation({ + type: StreamReceiverSeenEventModel, + input: streamReceiverSeenEvent, + id: `${streamReceiverSeenEvent.transactionHash}-${streamReceiverSeenEvent.logIndex}`, + }); }); } @@ -71,17 +69,17 @@ export default class StreamReceiverSeenEventHandler extends EventHandlerBase<'St }: EventHandlerRequest<'StreamReceiverSeen(bytes32,uint256,uint256)'>): Promise<{ accountIdsToInvalidate: AccountId[]; }> { - logger.info( - `[${requestId}] ${this.name} is gathering accountIds to invalidate...`, - ); + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log(`${this.name} is gathering accountIds to invalidate...`); const [rawReceiversHash] = args as StreamReceiverSeenEvent.OutputTuple; const accountIdsToInvalidate = await getCurrentSplitReceiversByReceiversHash(rawReceiversHash); - logger.info( - `[${requestId}] ${this.name} account IDs to invalidate: ${accountIdsToInvalidate.join( + scopedLogger.log( + `${this.name} account IDs to invalidate: ${accountIdsToInvalidate.join( ', ', )}`, ); diff --git a/src/eventHandlers/StreamsSetEventHandler.ts b/src/eventHandlers/StreamsSetEventHandler.ts index 47df218..a947c20 100644 --- a/src/eventHandlers/StreamsSetEventHandler.ts +++ b/src/eventHandlers/StreamsSetEventHandler.ts @@ -1,5 +1,5 @@ import type { StreamsSetEvent } from '../../contracts/CURRENT_NETWORK/Drips'; -import LogManager from '../core/LogManager'; +import ScopedLogger from '../core/ScopedLogger'; import { dbConnection } from '../db/database'; import EventHandlerBase from '../events/EventHandlerBase'; import type EventHandlerRequest from '../events/EventHandlerRequest'; @@ -33,7 +33,9 @@ export default class StreamsSetEventHandler extends EventHandlerBase<'StreamsSet const balance = toBigIntString(rawBalance.toString()); const maxEnd = toBigIntString(rawMaxEnd.toString()); - LogManager.logRequestInfo( + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log( `📥 ${this.name} is processing the following ${request.event.eventSignature}: \r\t - accountId: ${accountId} \r\t - erc20: ${rawErc20} @@ -43,12 +45,9 @@ export default class StreamsSetEventHandler extends EventHandlerBase<'StreamsSet \r\t - maxEnd: ${maxEnd} \r\t - logIndex: ${logIndex} \r\t - tx hash: ${transactionHash}`, - requestId, ); await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - const streamsSetEvent = await StreamsSetEventModel.create( { accountId, @@ -67,11 +66,13 @@ export default class StreamsSetEventHandler extends EventHandlerBase<'StreamsSet }, ); - logManager.appendFindOrCreateLog( - StreamsSetEventModel, - true, - `${streamsSetEvent.transactionHash}-${streamsSetEvent.logIndex}`, - ); + scopedLogger.bufferCreation({ + type: StreamsSetEventModel, + input: streamsSetEvent, + id: `${streamsSetEvent.transactionHash}-${streamsSetEvent.logIndex}`, + }); + + scopedLogger.flush(); }); } } diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index 288e8a3..c56fb55 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -1,7 +1,7 @@ import { ZeroAddress } from 'ethers'; import type { TransferEvent } from '../../contracts/CURRENT_NETWORK/NftDriver'; import EventHandlerBase from '../events/EventHandlerBase'; -import LogManager from '../core/LogManager'; +import ScopedLogger from '../core/ScopedLogger'; import { calcAccountId, convertToNftDriverId } from '../utils/accountIdUtils'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import { @@ -31,7 +31,9 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add }: EventHandlerRequest<'Transfer(address,address,uint256)'>): Promise { const [from, to, rawTokenId] = args as TransferEvent.OutputTuple; - LogManager.logRequestInfo( + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log( [ `📥 ${this.name} is processing ${eventSignature}:`, ` - from: ${from}`, @@ -40,12 +42,9 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add ` - logIndex: ${logIndex}`, ` - txHash: ${transactionHash}`, ].join('\n'), - requestId, ); await dbConnection.transaction(async (transaction) => { - const logManager = new LogManager(requestId); - const tokenId = convertToNftDriverId(rawTokenId); const transferEvent = await TransferEventModel.create( @@ -63,10 +62,11 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add }, ); - logManager.appendCreateLog( - TransferEventModel, - `${transferEvent.transactionHash}-${transferEvent.logIndex}`, - ); + scopedLogger.bufferCreation({ + type: TransferEventModel, + input: transferEvent, + id: `${transferEvent.transactionHash}-${transferEvent.logIndex}`, + }); // Only process if this is the latest event. if ( @@ -77,7 +77,7 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add transaction, )) ) { - logManager.logAllInfo(this.name); + scopedLogger.flush(); return; } @@ -114,11 +114,15 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add : true; entity.isValid = true; // The entity is initialized with `false` when created during account metadata processing. - logManager.appendUpdateLog(entity, entityModel, entity.accountId); + scopedLogger.bufferUpdate({ + input: entity, + type: entityModel, + id: entity.accountId, + }); await entity.save({ transaction }); - logManager.logAllInfo(this.name); + scopedLogger.flush(); }); } diff --git a/src/events/EventHandlerBase.ts b/src/events/EventHandlerBase.ts index 0efa47d..ff18da8 100644 --- a/src/events/EventHandlerBase.ts +++ b/src/events/EventHandlerBase.ts @@ -34,7 +34,7 @@ export default abstract class EventHandlerBase { if (!result.ok) { if (result.error instanceof BaseError) { logger.error( - `[${request.id}] ${this.name} failed to process event: ${result.error.message}`, + `[${request.id}] ${this.name} failed to process event: ${JSON.stringify(result.error, null, 2)}`, ); } diff --git a/src/models/ProjectModel.ts b/src/models/ProjectModel.ts index 93c3eff..1f731aa 100644 --- a/src/models/ProjectModel.ts +++ b/src/models/ProjectModel.ts @@ -9,14 +9,7 @@ import type { AddressLike } from 'ethers'; import getSchema from '../utils/getSchema'; import type { AddressDriverId, RepoDriverId } from '../core/types'; -export const PROJECT_VERIFICATION_STATUSES = [ - 'claimed', - 'owner_update_requested', - 'owner_updated', - 'unclaimed', - 'pending_owner', - 'pending_metadata', -] as const; +export const PROJECT_VERIFICATION_STATUSES = ['claimed', 'unclaimed'] as const; export type ProjectVerificationStatus = (typeof PROJECT_VERIFICATION_STATUSES)[number]; diff --git a/src/models/SubListModel.ts b/src/models/SubListModel.ts index b941e9a..8bc83ab 100644 --- a/src/models/SubListModel.ts +++ b/src/models/SubListModel.ts @@ -14,9 +14,9 @@ export default class SubListModel extends Model< InferCreationAttributes > { declare public accountId: ImmutableSplitsDriverId; - declare public parentId: AccountId; + declare public parentAccountId: AccountId; declare public parentAccountType: AccountType; - declare public rootId: AccountId; + declare public rootAccountId: AccountId; declare public rootAccountType: AccountType; declare public lastProcessedIpfsHash: string; declare public createdAt: CreationOptional; @@ -26,10 +26,10 @@ export default class SubListModel extends Model< this.init( { accountId: { - primaryKey: false, + primaryKey: true, type: DataTypes.STRING, }, - parentId: { + parentAccountId: { allowNull: false, type: DataTypes.STRING, }, @@ -37,7 +37,7 @@ export default class SubListModel extends Model< allowNull: false, type: DataTypes.STRING, }, - rootId: { + rootAccountId: { allowNull: false, type: DataTypes.STRING, }, diff --git a/src/queue/queue.ts b/src/queue/queue.ts index 1894496..32a0933 100644 --- a/src/queue/queue.ts +++ b/src/queue/queue.ts @@ -36,7 +36,7 @@ eventProcessingQueue.on('job failed', (job, err) => { }); eventProcessingQueue.on('job retrying', (job, err) => { - logger.info(`♻️ [${job}] failed (will be retried): '${err.message}`); + logger.info(`♻️ [${job}] failed (will be retried): ${err.message}`); }); export default eventProcessingQueue; diff --git a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts index 1847392..52ca22c 100644 --- a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts +++ b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts @@ -1,5 +1,6 @@ /* eslint-disable dot-notation */ import { randomUUID } from 'crypto'; +import { hexlify, toUtf8Bytes } from 'ethers'; import { AccountMetadataEmittedEventHandler } from '../../src/eventHandlers'; import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import type { EventData } from '../../src/events/types'; @@ -15,10 +16,10 @@ import { } from '../../src/utils/metadataUtils'; import * as handleDripListMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata'; -jest.mock('../../src/models/AccountMetadataEmittedEvent'); +jest.mock('../../src/models/AccountMetadataEmittedEventModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); -jest.mock('../../src/core/LogManager'); +jest.mock('../../src/core/ScopedLogger'); jest.mock('../../src/events/eventHandlerUtils'); jest.mock( '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata', @@ -71,7 +72,7 @@ describe('AccountMetadataEmittedHandler', () => { event: { args: [ 80920745289880686872077472087501508459438916877610571750365932290048n, - 'key', + hexlify(toUtf8Bytes('key')), '0x516d65444e625169575257666333395844754d354d69796337725755465156666b706d5a7675723965757767584a', ], logIndex: 1, @@ -166,7 +167,7 @@ describe('AccountMetadataEmittedHandler', () => { // Assert expect(handleProjectMetadata.default).toHaveBeenCalledWith({ - logManager: expect.anything(), + scopedLogger: expect.anything(), emitterAccountId: convertToAccountId(mockRequest.event.args[0]), transaction: mockDbTransaction, ipfsHash: convertToIpfsHash(mockRequest.event.args[2]), @@ -215,7 +216,7 @@ describe('AccountMetadataEmittedHandler', () => { expect(handleDripListMetadata.default).toHaveBeenCalledWith({ ipfsHash: convertToIpfsHash(request.event.args[2]), metadata: mockMetadata, - logManager: expect.anything(), + scopedLogger: expect.anything(), transaction: mockDbTransaction, blockTimestamp: request.event.blockTimestamp, blockNumber: request.event.blockNumber, diff --git a/tests/eventHandlers/GivenEventHandler.test.ts b/tests/eventHandlers/GivenEventHandler.test.ts index fccfdc3..eaa482a 100644 --- a/tests/eventHandlers/GivenEventHandler.test.ts +++ b/tests/eventHandlers/GivenEventHandler.test.ts @@ -4,7 +4,7 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; import GivenEventModel from '../../src/models/GivenEventModel'; -import LogManager from '../../src/core/LogManager'; +import ScopedLogger from '../../src/core/ScopedLogger'; import GivenEventHandler from '../../src/eventHandlers/GivenEventHandler'; import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { toAddress } from '../../src/utils/ethereumAddressUtils'; @@ -13,7 +13,7 @@ import { toBigIntString } from '../../src/utils/bigintUtils'; jest.mock('../../src/models/GivenEventModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); -jest.mock('../../src/core/LogManager'); +jest.mock('../../src/core/ScopedLogger'); describe('GivenEventHandler', () => { let mockDbTransaction: {}; @@ -58,7 +58,7 @@ describe('GivenEventHandler', () => { }, ]); - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); + ScopedLogger.prototype.bufferCreation = jest.fn().mockReturnThis(); // Act await handler['_handle'](mockRequest); diff --git a/tests/eventHandlers/SplitEventHandler.test.ts b/tests/eventHandlers/SplitEventHandler.test.ts index dcb31e8..bd9f5a6 100644 --- a/tests/eventHandlers/SplitEventHandler.test.ts +++ b/tests/eventHandlers/SplitEventHandler.test.ts @@ -4,7 +4,7 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; import SplitEventModel from '../../src/models/SplitEventModel'; -import LogManager from '../../src/core/LogManager'; +import ScopedLogger from '../../src/core/ScopedLogger'; import SplitEventHandler from '../../src/eventHandlers/SplitEventHandler'; import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { toAddress } from '../../src/utils/ethereumAddressUtils'; @@ -13,7 +13,7 @@ import { toBigIntString } from '../../src/utils/bigintUtils'; jest.mock('../../src/models/SplitEventModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); -jest.mock('../../src/core/LogManager'); +jest.mock('../../src/core/ScopedLogger'); describe('SplitEventHandler', () => { let mockDbTransaction: {}; @@ -58,7 +58,7 @@ describe('SplitEventHandler', () => { }, ]); - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); + ScopedLogger.prototype.bufferCreation = jest.fn().mockReturnThis(); // Act await handler['_handle'](mockRequest); diff --git a/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts b/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts index 49825ac..e78619f 100644 --- a/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts +++ b/tests/eventHandlers/SqueezedStreamsEventHandler.test.ts @@ -4,7 +4,7 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; import SqueezedStreamsEventModel from '../../src/models/SqueezedStreamsEventModel'; -import LogManager from '../../src/core/LogManager'; +import ScopedLogger from '../../src/core/ScopedLogger'; import SqueezedStreamsEventHandler from '../../src/eventHandlers/SqueezedStreamsEventHandler'; import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { toAddress } from '../../src/utils/ethereumAddressUtils'; @@ -13,7 +13,7 @@ import { toBigIntString } from '../../src/utils/bigintUtils'; jest.mock('../../src/models/SqueezedStreamsEventModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); -jest.mock('../../src/core/LogManager'); +jest.mock('../../src/core/ScopedLogger'); describe('SqueezedStreamsEventHandler', () => { let mockDbTransaction: {}; @@ -59,7 +59,7 @@ describe('SqueezedStreamsEventHandler', () => { }, ]); - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); + ScopedLogger.prototype.bufferCreation = jest.fn().mockReturnThis(); // Act await handler['_handle'](mockRequest); diff --git a/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts b/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts index e0206cf..7bfd2ed 100644 --- a/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts +++ b/tests/eventHandlers/StreamReceiverSeenEventHandler.test.ts @@ -4,7 +4,7 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; import StreamReceiverSeenEventModel from '../../src/models/StreamReceiverSeenEventModel'; -import LogManager from '../../src/core/LogManager'; +import ScopedLogger from '../../src/core/ScopedLogger'; import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { StreamReceiverSeenEventHandler } from '../../src/eventHandlers'; import { toBigIntString } from '../../src/utils/bigintUtils'; @@ -12,7 +12,7 @@ import { toBigIntString } from '../../src/utils/bigintUtils'; jest.mock('../../src/models/StreamReceiverSeenEventModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); -jest.mock('../../src/core/LogManager'); +jest.mock('../../src/core/ScopedLogger'); describe('StreamReceiverSeenEventHandler', () => { let mockDbTransaction: {}; @@ -56,7 +56,7 @@ describe('StreamReceiverSeenEventHandler', () => { }, ]); - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); + ScopedLogger.prototype.bufferCreation = jest.fn().mockReturnThis(); // Act await handler['_handle'](mockRequest); diff --git a/tests/eventHandlers/StreamsSetEventHandler.test.ts b/tests/eventHandlers/StreamsSetEventHandler.test.ts index f8e570e..8b9d6a8 100644 --- a/tests/eventHandlers/StreamsSetEventHandler.test.ts +++ b/tests/eventHandlers/StreamsSetEventHandler.test.ts @@ -4,7 +4,7 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; import StreamsSetEventModel from '../../src/models/StreamsSetEventModel'; -import LogManager from '../../src/core/LogManager'; +import ScopedLogger from '../../src/core/ScopedLogger'; import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { StreamsSetEventHandler } from '../../src/eventHandlers'; import { toBigIntString } from '../../src/utils/bigintUtils'; @@ -12,7 +12,7 @@ import { toBigIntString } from '../../src/utils/bigintUtils'; jest.mock('../../src/models/StreamsSetEventModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); -jest.mock('../../src/core/LogManager'); +jest.mock('../../src/core/ScopedLogger'); describe('StreamsSetEventHandler', () => { let mockDbTransaction: {}; @@ -60,7 +60,7 @@ describe('StreamsSetEventHandler', () => { true, ]); - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); + ScopedLogger.prototype.bufferCreation = jest.fn().mockReturnThis(); // Act await handler['_handle'](mockRequest); diff --git a/tests/eventHandlers/TransferEventHandler.test.ts b/tests/eventHandlers/TransferEventHandler.test.ts index ad984e7..7ba7621 100644 --- a/tests/eventHandlers/TransferEventHandler.test.ts +++ b/tests/eventHandlers/TransferEventHandler.test.ts @@ -5,17 +5,17 @@ import { TransferEventHandler } from '../../src/eventHandlers'; import { dbConnection } from '../../src/db/database'; import type { EventData } from '../../src/events/types'; import { convertToNftDriverId } from '../../src/utils/accountIdUtils'; -import LogManager from '../../src/core/LogManager'; +import ScopedLogger from '../../src/core/ScopedLogger'; import TransferEventModel from '../../src/models/TransferEventModel'; import DripListModel from '../../src/models/DripListModel'; jest.mock('../../src/models/TransferEventModel'); -jest.mock('../../src/models/DripList'); +jest.mock('../../src/models/DripListModel'); jest.mock('../../src/db/database'); jest.mock('bee-queue'); jest.mock('../../src/events/eventHandlerUtils'); jest.mock('../../src/utils/accountIdUtils'); -jest.mock('../../src/core/LogManager'); +jest.mock('../../src/core/ScopedLogger'); jest.mock('../../src/utils/isLatestEvent'); describe('TransferEventHandler', () => { @@ -64,7 +64,7 @@ describe('TransferEventHandler', () => { .fn() .mockResolvedValue([{ save: jest.fn() }, true]); - LogManager.prototype.appendFindOrCreateLog = jest.fn().mockReturnThis(); + ScopedLogger.prototype.bufferCreation = jest.fn().mockReturnThis(); // Act await handler['_handle'](mockRequest); From 4d2a1e0dbfb5fac892ebda0bc689137a4720fb47 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 28 Apr 2025 13:59:57 +0200 Subject: [PATCH 29/60] feat: (re)introduce SplitsSet event handler to set an entity's isValid flag --- .../20250414133746-initial_create.ts | 53 ++++++ src/db/modelRegistration.ts | 2 + .../AccountMetadataEmittedEventHandler.ts | 9 +- .../handlers/handleDripListMetadata.ts | 2 +- .../handleEcosystemMainAccountMetadata.ts | 2 +- .../handlers/handleProjectMetadata.ts | 9 +- .../handlers/handleSubListMetadata.ts | 5 +- .../SplitsSetEvent/SplitsSetEventHandler.ts | 66 +++++++ .../SplitsSetEvent/setIsValidFlag.ts | 170 ++++++++++++++++++ .../StreamReceiverSeenEventHandler.ts | 8 +- src/eventHandlers/TransferEventHandler.ts | 17 +- src/events/registrations.ts | 5 + src/metadata/schemas/index.ts | 2 + src/metadata/schemas/repo-driver/v6.ts | 6 + src/models/ProjectModel.ts | 5 + src/models/SplitsSetEventModel.ts | 48 +++++ src/models/SubListModel.ts | 5 + src/models/index.ts | 1 + .../SplitsSetEventHandler.test.ts | 92 ++++++++++ .../TransferEventHandler.test.ts | 6 + 20 files changed, 500 insertions(+), 13 deletions(-) create mode 100644 src/eventHandlers/SplitsSetEvent/SplitsSetEventHandler.ts create mode 100644 src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts create mode 100644 src/metadata/schemas/repo-driver/v6.ts create mode 100644 src/models/SplitsSetEventModel.ts create mode 100644 tests/eventHandlers/SplitsSetEventHandler.test.ts diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index 43b1da6..24bc71f 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -59,6 +59,47 @@ export async function up({ context: sequelize }: any): Promise { await createSqueezedStreamsEventsTable(queryInterface, schema); await createStreamReceiverSeenEventsTable(queryInterface, schema); await createStreamsSetEventsTable(queryInterface, schema); + await createSplitsSetEventsTable(queryInterface, schema); +} + +async function createSplitsSetEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: `splits_set_events`, + }, + transformFieldNamesToSnakeCase({ + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiversHash: { + allowNull: false, + type: DataTypes.STRING, + }, + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); } async function createStreamsSetEventsTable( @@ -449,6 +490,10 @@ async function createSubListsEventsTable( primaryKey: true, type: DataTypes.STRING, }, + isValid: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, parentAccountId: { allowNull: false, type: DataTypes.STRING, @@ -676,6 +721,10 @@ async function createProjectsTable( primaryKey: true, type: DataTypes.STRING, }, + isValid: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, name: { allowNull: false, type: DataTypes.STRING, @@ -947,6 +996,10 @@ export async function down({ context: sequelize }: any): Promise { const schema = getSchema(); const queryInterface: QueryInterface = sequelize.getQueryInterface(); + await queryInterface.dropTable({ + schema, + tableName: `split_set_events`, + }); await queryInterface.dropTable({ schema, tableName: `streams_set_events`, diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index d5cc11b..859b78c 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -14,6 +14,7 @@ import { EcosystemMainAccountModel, SplitReceiverModel, } from '../models'; +import SplitsSetEventModel from '../models/SplitsSetEventModel'; const REGISTERED_MODELS: ModelStaticMembers[] = []; @@ -35,6 +36,7 @@ export function registerModels(): void { registerModel(SplitEventModel); registerModel(TransferEventModel); registerModel(SplitReceiverModel); + registerModel(SplitsSetEventModel); registerModel(StreamsSetEventModel); registerModel(EcosystemMainAccountModel); registerModel(SqueezedStreamsEventModel); diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index 830f164..8b28c41 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -183,10 +183,13 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase const accountIdsToInvalidate = await getCurrentSplitReceiversBySender( convertToAccountId(accountId), ); + scopedLogger.log( - `${this.name} account IDs to invalidate: ${accountIdsToInvalidate.join( - ', ', - )}`, + `${this.name} account IDs to invalidate: ${ + accountIdsToInvalidate.length > 0 + ? accountIdsToInvalidate.join(', ') + : 'none' + }`, ); return { diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 97b5a5e..56237d7 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -144,7 +144,7 @@ async function upsertDripList({ where: { accountId }, defaults: { ...values, - isValid: false, // Until the `Transfer` event is processed. + isValid: false, // Until the `SplitsSet` event is processed. }, transaction, }); diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index b64fcbe..f6add64 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -127,7 +127,7 @@ async function upsertEcosystemMainAccount({ where: { accountId }, defaults: { ...values, - isValid: false, // Until the `Transfer` event is processed. + isValid: false, // Until the `SetSplits` event is processed. }, transaction, }); diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index 2e2c495..6f95ec6 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -82,10 +82,13 @@ export default async function handleProjectMetadata({ } const onChainOwner = await repoDriverContract.ownerOf(emitterAccountId); + // If on-chain owner is still unset, it likely means the first `OwnerUpdateRequested` and `OwnerUpdated` events were not emitted (yet?). No need to process the metadata. if (!onChainOwner || onChainOwner === ZeroAddress) { scopedLogger.bufferMessage( `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: on-chain owner is not set.`, ); + + return; } // ✅ All checks passed, we can proceed with the processing. @@ -164,9 +167,13 @@ async function upsertProject({ ownerAccountId: convertToAddressDriverId(onChainOwner), claimedAt: blockTimestamp, }; + const [project, isCreation] = await ProjectModel.findOrCreate({ where: { accountId }, - defaults: values, + defaults: { + ...values, + isValid: false, // Until the `SplitsSet` event is processed. + }, }); if (!isCreation) { diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 67335a6..90b3614 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -135,7 +135,10 @@ async function upsertSubList({ const [subList, isCreation] = await SubListModel.findOrCreate({ where: { accountId }, transaction, - defaults: values, + defaults: { + ...values, + isValid: false, // Until the `SetSplits` event is processed. + }, }); if (!isCreation) { diff --git a/src/eventHandlers/SplitsSetEvent/SplitsSetEventHandler.ts b/src/eventHandlers/SplitsSetEvent/SplitsSetEventHandler.ts new file mode 100644 index 0000000..282601c --- /dev/null +++ b/src/eventHandlers/SplitsSetEvent/SplitsSetEventHandler.ts @@ -0,0 +1,66 @@ +import type { SplitsSetEvent } from '../../../contracts/CURRENT_NETWORK/Drips'; +import ScopedLogger from '../../core/ScopedLogger'; +import { dbConnection } from '../../db/database'; +import EventHandlerBase from '../../events/EventHandlerBase'; +import type EventHandlerRequest from '../../events/EventHandlerRequest'; +import SplitsSetEventModel from '../../models/SplitsSetEventModel'; +import { convertToAccountId } from '../../utils/accountIdUtils'; +import setIsValidFlag from './setIsValidFlag'; + +export default class SplitsSetEventHandler extends EventHandlerBase<'SplitsSet(uint256,bytes32)'> { + public eventSignatures = ['SplitsSet(uint256,bytes32)' as const]; + + protected async _handle({ + id: requestId, + event: { + args, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + eventSignature, + }, + }: EventHandlerRequest<'SplitsSet(uint256,bytes32)'>): Promise { + const [rawAccountId, rawReceiversHash] = args as SplitsSetEvent.OutputTuple; + const accountId = convertToAccountId(rawAccountId); + + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log( + [ + `📥 ${this.name} is processing ${eventSignature}:`, + ` - accountId: ${accountId}`, + ` - receiversHash: ${rawReceiversHash}`, + ` - logIndex: ${logIndex}`, + ` - txHash: ${transactionHash}`, + ].join('\n'), + ); + + await dbConnection.transaction(async (transaction) => { + const splitsSetEvent = await SplitsSetEventModel.create( + { + accountId, + receiversHash: rawReceiversHash, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + { transaction }, + ); + + scopedLogger.bufferCreation({ + type: SplitsSetEventModel, + input: splitsSetEvent, + id: `${splitsSetEvent.transactionHash}-${splitsSetEvent.logIndex}`, + }); + + // Account’s splits are set by `AccountMetadataEmitted` events. + // The `SplitsSet` event only confirms that the split receivers are valid. + + await setIsValidFlag(splitsSetEvent, scopedLogger, transaction); + + scopedLogger.flush(); + }); + } +} diff --git a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts new file mode 100644 index 0000000..929c993 --- /dev/null +++ b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts @@ -0,0 +1,170 @@ +import type { Transaction } from 'sequelize'; +import type { AccountId } from '../../core/types'; +import { + isImmutableSplitsDriverId, + isNftDriverId, + isRepoDriverId, +} from '../../utils/accountIdUtils'; +import { + dripsContract, + nftDriverContract, + repoDriverContract, +} from '../../core/contractClients'; +import { formatSplitReceivers } from '../../utils/formatSplitReceivers'; +import RecoverableError from '../../utils/recoverableError'; +import { + ProjectModel, + DripListModel, + EcosystemMainAccountModel, + SubListModel, + SplitReceiverModel, +} from '../../models'; +import type SplitsSetEventModel from '../../models/SplitsSetEventModel'; +import type ScopedLogger from '../../core/ScopedLogger'; + +export default async function setIsValidFlag( + { accountId, receiversHash: eventReceiversHash }: SplitsSetEventModel, + scopedLogger: ScopedLogger, + transaction: Transaction, +): Promise { + const onChainReceiversHash = await dripsContract.splitsHash(accountId); + + // Only proceed if this event matches the latest on-chain hash. + if (eventReceiversHash !== onChainReceiversHash) { + scopedLogger.bufferMessage( + `Skipped setting 'isValid' flag for ${accountId}: on-chain splits hash '${onChainReceiversHash}' does not match event hash '${eventReceiversHash}'.`, + ); + + return; + } + + if (isRepoDriverId(accountId)) { + const project = await ProjectModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!project) { + throw new RecoverableError( + `Failed to set 'isValid' flag for Project: Project '${accountId}' not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + + const onChainOwner = await repoDriverContract.ownerOf(accountId); + const dbOwner = project.ownerAddress; // populated from metadata. + if (onChainOwner !== dbOwner) { + throw new RecoverableError( + `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for Project '${accountId}'. Likely waiting on latest 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + + const dbReceiversHash = await hashDbSplits(accountId, transaction); + const isValid = dbReceiversHash === onChainReceiversHash; + + project.isValid = isValid; + + scopedLogger.bufferUpdate({ + id: project.accountId, + type: ProjectModel, + input: project, + }); + + await project.save({ transaction }); + + scopedLogger.bufferMessage( + `Set 'isValid' for Project '${accountId}' to '${isValid}'.`, + ); + } else if (isNftDriverId(accountId)) { + const dripList = await DripListModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + const ecosystem = dripList + ? null + : await EcosystemMainAccountModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + const entity = dripList ?? ecosystem; + const Model = dripList ? DripListModel : EcosystemMainAccountModel; + + if (!entity) { + throw new RecoverableError( + `Failed to set 'isValid' flag for ${Model.name}: ${Model.name} '${accountId}' not found.`, + ); + } + + const onChainOwner = await nftDriverContract.ownerOf(accountId); + const dbOwner = entity.ownerAddress; // populated from metadata. + if (onChainOwner !== dbOwner) { + throw new RecoverableError( + `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for ${Model.name} '${accountId}'. Likely waiting on latest 'Transfer' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + + const dbReceiversHash = await hashDbSplits(accountId, transaction); + const isValid = dbReceiversHash === onChainReceiversHash; + + entity.isValid = isValid; + + scopedLogger.bufferUpdate({ + id: entity.accountId, + type: Model, + input: entity, + }); + + await entity.save({ transaction }); + + scopedLogger.bufferMessage( + `Set 'isValid' for ${Model.name} '${accountId}' to '${isValid}'.`, + ); + } else if (isImmutableSplitsDriverId(accountId)) { + const subList = await SubListModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!subList) { + throw new RecoverableError( + `Failed to set 'isValid' flag for SubList: SubList '${accountId}' not found.`, + ); + } + + const dbReceiversHash = await hashDbSplits(accountId, transaction); + const isValid = dbReceiversHash === onChainReceiversHash; + + subList.isValid = isValid; + + scopedLogger.bufferUpdate({ + id: subList.accountId, + type: SubListModel, + input: subList, + }); + + await subList.save({ transaction }); + + scopedLogger.bufferMessage( + `Set 'isValid' for SubList '${accountId}' to '${isValid}'.`, + ); + } +} + +async function hashDbSplits( + accountId: AccountId, + transaction: Transaction, +): Promise { + const rows = await SplitReceiverModel.findAll({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { senderAccountId: accountId }, + }); + + const receivers = rows.map((r) => ({ + accountId: r.receiverAccountId, + weight: r.weight, + })); + + return dripsContract.hashSplits(formatSplitReceivers(receivers)); +} diff --git a/src/eventHandlers/StreamReceiverSeenEventHandler.ts b/src/eventHandlers/StreamReceiverSeenEventHandler.ts index d5a29c0..4f82cf1 100644 --- a/src/eventHandlers/StreamReceiverSeenEventHandler.ts +++ b/src/eventHandlers/StreamReceiverSeenEventHandler.ts @@ -79,9 +79,11 @@ export default class StreamReceiverSeenEventHandler extends EventHandlerBase<'St await getCurrentSplitReceiversByReceiversHash(rawReceiversHash); scopedLogger.log( - `${this.name} account IDs to invalidate: ${accountIdsToInvalidate.join( - ', ', - )}`, + `${this.name} account IDs to invalidate: ${ + accountIdsToInvalidate.length + ? accountIdsToInvalidate.join(', ') + : 'none' + }`, ); return { diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index c56fb55..862f363 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -14,6 +14,7 @@ import { isLatestEvent } from '../utils/isLatestEvent'; import appSettings from '../config/appSettings'; import RecoverableError from '../utils/recoverableError'; import type { Address } from '../core/types'; +import { nftDriverContract } from '../core/contractClients'; export default class TransferEventHandler extends EventHandlerBase<'Transfer(address,address,uint256)'> { public eventSignatures = ['Transfer(address,address,uint256)' as const]; @@ -44,9 +45,20 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add ].join('\n'), ); - await dbConnection.transaction(async (transaction) => { - const tokenId = convertToNftDriverId(rawTokenId); + const tokenId = convertToNftDriverId(rawTokenId); + + const onChainOwner = await nftDriverContract.ownerOf(tokenId); + if (to !== onChainOwner) { + scopedLogger.bufferMessage( + `🚨🕵️‍♂️ Skipped Drip List or Ecosystem Main Account ${tokenId} TransferEvent processing: on-chain owner '${onChainOwner}' does not match event 'to' '${to}'.`, + ); + scopedLogger.flush(); + + return; + } + + await dbConnection.transaction(async (transaction) => { const transferEvent = await TransferEventModel.create( { tokenId, @@ -112,7 +124,6 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add blockNumber > appSettings.visibilityThresholdBlockNumber ? from === ZeroAddress || from === appSettings.ecosystemDeployer : true; - entity.isValid = true; // The entity is initialized with `false` when created during account metadata processing. scopedLogger.bufferUpdate({ input: entity, diff --git a/src/events/registrations.ts b/src/events/registrations.ts index 38ec273..672174a 100644 --- a/src/events/registrations.ts +++ b/src/events/registrations.ts @@ -7,6 +7,7 @@ import { StreamsSetEventHandler, SqueezedStreamsEventHandler, } from '../eventHandlers'; +import SplitsSetEventHandler from '../eventHandlers/SplitsSetEvent/SplitsSetEventHandler'; import { registerEventHandler } from './eventHandlerUtils'; export function registerEventHandlers(): void { @@ -30,6 +31,10 @@ export function registerEventHandlers(): void { 'StreamsSet(uint256,address,bytes32,bytes32,uint128,uint32)', StreamsSetEventHandler, ); + registerEventHandler<'SplitsSet(uint256,bytes32)'>( + 'SplitsSet(uint256,bytes32)', + SplitsSetEventHandler, + ); registerEventHandler<'StreamReceiverSeen(bytes32,uint256,uint256)'>( 'StreamReceiverSeen(bytes32,uint256,uint256)', StreamReceiverSeenEventHandler, diff --git a/src/metadata/schemas/index.ts b/src/metadata/schemas/index.ts index 1f3c0fa..9a95b65 100644 --- a/src/metadata/schemas/index.ts +++ b/src/metadata/schemas/index.ts @@ -12,6 +12,7 @@ import { repoDriverAccountMetadataSchemaV5 } from './repo-driver/v5'; import { nftDriverAccountMetadataSchemaV5 } from './nft-driver/v5'; import { subListMetadataSchemaV1 } from './immutable-splits-driver/v1'; import { nftDriverAccountMetadataSchemaV6 } from './nft-driver/v6'; +import { repoDriverAccountMetadataSchemaV6 } from './repo-driver/v6'; export const nftDriverAccountMetadataParser = createVersionedParser([ nftDriverAccountMetadataSchemaV6.parse, @@ -27,6 +28,7 @@ export const addressDriverAccountMetadataParser = createVersionedParser([ ]); export const repoDriverAccountMetadataParser = createVersionedParser([ + repoDriverAccountMetadataSchemaV6.parse, repoDriverAccountMetadataSchemaV5.parse, repoDriverAccountMetadataSchemaV4.parse, repoDriverAccountMetadataSchemaV3.parse, diff --git a/src/metadata/schemas/repo-driver/v6.ts b/src/metadata/schemas/repo-driver/v6.ts new file mode 100644 index 0000000..ba75203 --- /dev/null +++ b/src/metadata/schemas/repo-driver/v6.ts @@ -0,0 +1,6 @@ +import { repoDriverAccountMetadataSchemaV5 } from './v5'; + +export const repoDriverAccountMetadataSchemaV6 = + repoDriverAccountMetadataSchemaV5.omit({ + description: true, + }); diff --git a/src/models/ProjectModel.ts b/src/models/ProjectModel.ts index 1f731aa..746e4b1 100644 --- a/src/models/ProjectModel.ts +++ b/src/models/ProjectModel.ts @@ -30,6 +30,7 @@ export default class ProjectModel extends Model< declare public name: ProjectName; declare public avatarCid: string | null; declare public verificationStatus: ProjectVerificationStatus; + declare public isValid: boolean; declare public isVisible: boolean; declare public lastProcessedIpfsHash: string; declare public ownerAddress: AddressLike; @@ -45,6 +46,10 @@ export default class ProjectModel extends Model< primaryKey: true, type: DataTypes.STRING, }, + isValid: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, name: { allowNull: false, type: DataTypes.STRING, diff --git a/src/models/SplitsSetEventModel.ts b/src/models/SplitsSetEventModel.ts new file mode 100644 index 0000000..07cfaf7 --- /dev/null +++ b/src/models/SplitsSetEventModel.ts @@ -0,0 +1,48 @@ +import type { + InferAttributes, + InferCreationAttributes, + Sequelize, +} from 'sequelize'; +import { DataTypes, Model } from 'sequelize'; +import type { AccountId } from '../core/types'; +import getSchema from '../utils/getSchema'; +import { COMMON_EVENT_INIT_ATTRIBUTES } from '../core/constants'; +import type { IEventModel } from '../events/types'; + +export default class SplitsSetEventModel + extends Model< + InferAttributes, + InferCreationAttributes + > + implements IEventModel +{ + declare public accountId: AccountId; + declare public receiversHash: string; + declare public logIndex: number; + declare public blockNumber: number; + declare public blockTimestamp: Date; + declare public transactionHash: string; + + public static initialize(sequelize: Sequelize): void { + this.init( + { + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiversHash: { + allowNull: false, + type: DataTypes.STRING, + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + { + sequelize, + timestamps: false, + underscored: true, + schema: getSchema(), + tableName: 'splits_set_events', + }, + ); + } +} diff --git a/src/models/SubListModel.ts b/src/models/SubListModel.ts index 8bc83ab..626cca8 100644 --- a/src/models/SubListModel.ts +++ b/src/models/SubListModel.ts @@ -19,6 +19,7 @@ export default class SubListModel extends Model< declare public rootAccountId: AccountId; declare public rootAccountType: AccountType; declare public lastProcessedIpfsHash: string; + declare public isValid: boolean; declare public createdAt: CreationOptional; declare public updatedAt: CreationOptional; @@ -29,6 +30,10 @@ export default class SubListModel extends Model< primaryKey: true, type: DataTypes.STRING, }, + isValid: { + allowNull: false, + type: DataTypes.BOOLEAN, + }, parentAccountId: { allowNull: false, type: DataTypes.STRING, diff --git a/src/models/index.ts b/src/models/index.ts index 7ba16e6..5b4db78 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -5,6 +5,7 @@ export { default as GivenEventModel } from './GivenEventModel'; export { default as SplitEventModel } from './SplitEventModel'; export { default as SplitReceiverModel } from './SplitReceiverModel'; export { default as TransferEventModel } from './TransferEventModel'; +export { default as SplitsSetEventModel } from './SplitsSetEventModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; export { default as _LastIndexedBlockModel } from './_LastIndexedBlockModel'; export { default as SqueezedStreamsEventModel } from './SqueezedStreamsEventModel'; diff --git a/tests/eventHandlers/SplitsSetEventHandler.test.ts b/tests/eventHandlers/SplitsSetEventHandler.test.ts new file mode 100644 index 0000000..3aa91f2 --- /dev/null +++ b/tests/eventHandlers/SplitsSetEventHandler.test.ts @@ -0,0 +1,92 @@ +/* eslint-disable dot-notation */ +import { randomUUID } from 'crypto'; +import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; +import { dbConnection } from '../../src/db/database'; +import type { EventData } from '../../src/events/types'; +import SplitsSetEventModel from '../../src/models/SplitsSetEventModel'; +import { convertToAccountId } from '../../src/utils/accountIdUtils'; +import SplitsSetEventHandler from '../../src/eventHandlers/SplitsSetEvent/SplitsSetEventHandler'; +import ScopedLogger from '../../src/core/ScopedLogger'; +import setIsValidFlag from '../../src/eventHandlers/SplitsSetEvent/setIsValidFlag'; + +jest.mock('../../src/models/SplitsSetEventModel'); +jest.mock('../../src/db/database'); +jest.mock('bee-queue'); +jest.mock('../../src/core/ScopedLogger'); +jest.mock('../../src/eventHandlers/SplitsSetEvent/setIsValidFlag'); + +describe('SplitsSetEventHandler', () => { + let mockDbTransaction: {}; + let handler: SplitsSetEventHandler; + let mockRequest: EventHandlerRequest<'SplitsSet(uint256,bytes32)'>; + + beforeAll(() => { + jest.clearAllMocks(); + + handler = new SplitsSetEventHandler(); + + mockRequest = { + id: randomUUID(), + event: { + args: [ + 80920745289880686872077472087501508459438916877610571750365932290048n, + 'receiversHash', + ], + logIndex: 1, + blockNumber: 1, + blockTimestamp: new Date(), + transactionHash: 'requestTransactionHash', + } as EventData<'SplitsSet(uint256,bytes32)'>, + }; + + mockDbTransaction = {}; + + dbConnection.transaction = jest + .fn() + .mockImplementation((callback) => callback(mockDbTransaction)); + }); + + describe('_handle', () => { + test('should create a new SplitsSetEventModel', async () => { + // Arrange + SplitsSetEventModel.create = jest.fn().mockResolvedValue([ + { + transactionHash: 'SplitsSetTransactionHash', + logIndex: 1, + }, + ]); + + ScopedLogger.prototype.bufferCreation = jest.fn().mockReturnThis(); + + // Act + await handler['_handle'](mockRequest); + + // Assert + const { + event: { + args: [rawAccountId, rawReceiversHash], + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + } = mockRequest; + + expect(SplitsSetEventModel.create).toHaveBeenCalledWith( + { + accountId: convertToAccountId(rawAccountId), + receiversHash: rawReceiversHash, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + { + transaction: mockDbTransaction, + }, + ); + + expect(setIsValidFlag).toHaveBeenCalled(); + }); + }); +}); diff --git a/tests/eventHandlers/TransferEventHandler.test.ts b/tests/eventHandlers/TransferEventHandler.test.ts index 7ba7621..95dbe53 100644 --- a/tests/eventHandlers/TransferEventHandler.test.ts +++ b/tests/eventHandlers/TransferEventHandler.test.ts @@ -8,6 +8,7 @@ import { convertToNftDriverId } from '../../src/utils/accountIdUtils'; import ScopedLogger from '../../src/core/ScopedLogger'; import TransferEventModel from '../../src/models/TransferEventModel'; import DripListModel from '../../src/models/DripListModel'; +import * as contractClients from '../../src/core/contractClients'; jest.mock('../../src/models/TransferEventModel'); jest.mock('../../src/models/DripListModel'); @@ -17,6 +18,7 @@ jest.mock('../../src/events/eventHandlerUtils'); jest.mock('../../src/utils/accountIdUtils'); jest.mock('../../src/core/ScopedLogger'); jest.mock('../../src/utils/isLatestEvent'); +jest.mock('../../src/core/contractClients'); describe('TransferEventHandler', () => { let mockDbTransaction: {}; @@ -66,6 +68,10 @@ describe('TransferEventHandler', () => { ScopedLogger.prototype.bufferCreation = jest.fn().mockReturnThis(); + contractClients.nftDriverContract.ownerOf = jest + .fn() + .mockResolvedValue(mockRequest.event.args[1]) as any; + // Act await handler['_handle'](mockRequest); From 0679594d0009c8afd72751a368b1a315174f151a Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 30 Apr 2025 14:47:44 +0200 Subject: [PATCH 30/60] refactor: remove RepoDriver metadata v6 --- src/metadata/schemas/index.ts | 2 -- src/metadata/schemas/nft-driver/v1.ts | 1 - src/metadata/schemas/repo-driver/v6.ts | 6 ------ 3 files changed, 9 deletions(-) delete mode 100644 src/metadata/schemas/repo-driver/v6.ts diff --git a/src/metadata/schemas/index.ts b/src/metadata/schemas/index.ts index 9a95b65..1f3c0fa 100644 --- a/src/metadata/schemas/index.ts +++ b/src/metadata/schemas/index.ts @@ -12,7 +12,6 @@ import { repoDriverAccountMetadataSchemaV5 } from './repo-driver/v5'; import { nftDriverAccountMetadataSchemaV5 } from './nft-driver/v5'; import { subListMetadataSchemaV1 } from './immutable-splits-driver/v1'; import { nftDriverAccountMetadataSchemaV6 } from './nft-driver/v6'; -import { repoDriverAccountMetadataSchemaV6 } from './repo-driver/v6'; export const nftDriverAccountMetadataParser = createVersionedParser([ nftDriverAccountMetadataSchemaV6.parse, @@ -28,7 +27,6 @@ export const addressDriverAccountMetadataParser = createVersionedParser([ ]); export const repoDriverAccountMetadataParser = createVersionedParser([ - repoDriverAccountMetadataSchemaV6.parse, repoDriverAccountMetadataSchemaV5.parse, repoDriverAccountMetadataSchemaV4.parse, repoDriverAccountMetadataSchemaV3.parse, diff --git a/src/metadata/schemas/nft-driver/v1.ts b/src/metadata/schemas/nft-driver/v1.ts index ee826df..0834562 100644 --- a/src/metadata/schemas/nft-driver/v1.ts +++ b/src/metadata/schemas/nft-driver/v1.ts @@ -12,7 +12,6 @@ const repoDriverSplitReceiverSchema = z.object({ source: sourceSchema, }); -// eslint-disable-next-line import/prefer-default-export export const nftDriverAccountMetadataSchemaV1 = z.object({ driver: z.literal('nft'), describes: z.object({ diff --git a/src/metadata/schemas/repo-driver/v6.ts b/src/metadata/schemas/repo-driver/v6.ts deleted file mode 100644 index ba75203..0000000 --- a/src/metadata/schemas/repo-driver/v6.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { repoDriverAccountMetadataSchemaV5 } from './v5'; - -export const repoDriverAccountMetadataSchemaV6 = - repoDriverAccountMetadataSchemaV5.omit({ - description: true, - }); From 4534c722904ff48ada9b970e43da61fde887f704 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 30 Apr 2025 14:49:03 +0200 Subject: [PATCH 31/60] refactor: sync concurrency and db connection pool --- src/db/database.ts | 5 +++++ src/queue/initJobProcessingQueue.ts | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/db/database.ts b/src/db/database.ts index acdf60e..3f409a9 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -9,6 +9,11 @@ export const dbConnection = new Sequelize( dialect: 'postgres', logging: false, timezone: 'UTC', + pool: { + max: 15, + acquire: 30000, + idle: 10000, + }, }, ); diff --git a/src/queue/initJobProcessingQueue.ts b/src/queue/initJobProcessingQueue.ts index b488fb2..4a02d3d 100644 --- a/src/queue/initJobProcessingQueue.ts +++ b/src/queue/initJobProcessingQueue.ts @@ -1,12 +1,11 @@ import { getEventHandler } from '../events/eventHandlerUtils'; import type { KnownAny } from '../core/types'; import eventProcessingQueue from './queue'; -import { assertRequestId } from '../utils/assert'; import EventHandlerRequest from '../events/EventHandlerRequest'; import logger from '../core/logger'; export default async function initJobProcessingQueue() { - eventProcessingQueue.process(100, async (job) => { + eventProcessingQueue.process(10, async (job) => { const handler = getEventHandler(job.data.eventSignature); const { @@ -18,8 +17,6 @@ export default async function initJobProcessingQueue() { blockTimestamp, } = job.data; - assertRequestId(job.id); - const handleContext = new EventHandlerRequest( { args: JSON.parse(args, (_, value) => { From ecaa92bd11d31ee47278f03ec35697044eedeb52 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 30 Apr 2025 14:50:58 +0200 Subject: [PATCH 32/60] feat: make job IDs unique but deterministic to ensure no duplicate job is enqueued --- src/core/ScopedLogger.ts | 6 ++---- src/events/EventHandlerBase.ts | 2 +- src/events/EventHandlerRequest.ts | 6 ++---- src/events/poll.ts | 21 +++++++++++++-------- src/index.ts | 1 - 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/core/ScopedLogger.ts b/src/core/ScopedLogger.ts index 30062f4..75ee23b 100644 --- a/src/core/ScopedLogger.ts +++ b/src/core/ScopedLogger.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-dupe-class-members */ -import type { UUID } from 'crypto'; import type { Model } from 'sequelize'; import logger from './logger'; @@ -12,10 +10,10 @@ export type ChangedProperties = { */ export default class ScopedLogger { private readonly _handler: string; - private readonly _requestId: UUID; + private readonly _requestId: string; private readonly _buffer: string[] = []; - constructor(handler: string, requestId: UUID) { + constructor(handler: string, requestId: string) { this._handler = handler; this._requestId = requestId; } diff --git a/src/events/EventHandlerBase.ts b/src/events/EventHandlerBase.ts index ff18da8..03b3df4 100644 --- a/src/events/EventHandlerBase.ts +++ b/src/events/EventHandlerBase.ts @@ -20,7 +20,7 @@ export default abstract class EventHandlerBase { protected abstract _handle(request: EventHandlerRequest): Promise; public async createJob(request: EventHandlerRequest): Promise { - await saveEventProcessingJob(request, request.event.eventSignature); + await saveEventProcessingJob(request); } /** diff --git a/src/events/EventHandlerRequest.ts b/src/events/EventHandlerRequest.ts index 7ef6ea2..b10a00c 100644 --- a/src/events/EventHandlerRequest.ts +++ b/src/events/EventHandlerRequest.ts @@ -1,12 +1,10 @@ -import type { UUID } from 'crypto'; -import { randomUUID } from 'crypto'; import type { EventData, EventSignature } from './types'; export default class EventHandlerRequest { - public readonly id: UUID; + public readonly id: string; public readonly event: EventData; - constructor(event: EventData, id: UUID = randomUUID()) { + constructor(event: EventData, id: string) { this.id = id; this.event = event; } diff --git a/src/events/poll.ts b/src/events/poll.ts index 67ada0b..1e730e3 100644 --- a/src/events/poll.ts +++ b/src/events/poll.ts @@ -86,14 +86,19 @@ export default async function poll( ); await handler?.createJob( - new EventHandlerRequest({ - logIndex: log.index, - blockNumber: log.blockNumber, - blockTimestamp: new Date((await log.getBlock()).timestamp * 1000), - transactionHash: log.transactionHash, - args: parsedLog.args as KnownAny, - eventSignature: signature, - }), + new EventHandlerRequest( + { + logIndex: log.index, + blockNumber: log.blockNumber, + blockTimestamp: new Date( + (await log.getBlock()).timestamp * 1000, + ), + transactionHash: log.transactionHash, + args: parsedLog.args as KnownAny, + eventSignature: signature, + }, + `${log.blockNumber}-${log.transactionHash}-${log.index}`, + ), ); } }), diff --git a/src/index.ts b/src/index.ts index 3a5f590..ef0db74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -73,7 +73,6 @@ async function init() { ], getHandlers(), getProvider(), - startBlock, ); From b24f2be7553982e3fe52201bcd3372279d998bc8 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 30 Apr 2025 14:52:29 +0200 Subject: [PATCH 33/60] refactor: handle all metadata versions when creating new Drip List splits --- .../handlers/handleDripListMetadata.ts | 79 ++++++++++--------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 56237d7..5638e3b 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -2,19 +2,12 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Transaction } from 'sequelize'; import type { UUID } from 'crypto'; -import type { z } from 'zod'; import type { IpfsHash, NftDriverId } from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; import type ScopedLogger from '../../../core/ScopedLogger'; import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; import appSettings from '../../../config/appSettings'; -import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; -import type { - repoDriverSplitReceiverSchema, - addressDriverSplitReceiverSchema, -} from '../../../metadata/schemas/repo-driver/v2'; -import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immutable-splits-driver/v1'; import { createSplitReceiver, deleteExistingSplitReceivers, @@ -76,7 +69,7 @@ export default async function handleDripListMetadata({ } const { areProjectsValid, message } = await verifyProjectSources( - splitReceivers.filter((r) => r.type === 'repoDriver'), + splitReceivers.filter((splitReceiver) => 'source' in splitReceiver), ); if (!areProjectsValid) { @@ -104,7 +97,7 @@ export default async function handleDripListMetadata({ transaction, blockTimestamp, emitterAccountId, - splitReceivers, + metadata, }); } @@ -167,7 +160,7 @@ async function upsertDripList({ } async function createNewSplitReceivers({ - splitReceivers, + metadata, scopedLogger, transaction, blockTimestamp, @@ -177,13 +170,40 @@ async function createNewSplitReceivers({ scopedLogger: ScopedLogger; transaction: Transaction; emitterAccountId: NftDriverId; - splitReceivers: ( - | z.infer - | z.infer - | z.infer - | z.infer - )[]; + metadata: AnyVersion; }) { + const rawReceivers = + // eslint-disable-next-line no-nested-ternary + 'recipients' in metadata + ? (metadata.recipients ?? []) + : 'projects' in metadata + ? (metadata.projects ?? []) + : []; + + // 2. Upgrade legacy payloads so that *every* receiver object has a `type`. + // – v2+ entries already expose `type`. + // – v1 repo receivers carry a `source` property. + const splitReceivers = rawReceivers.map((receiver: any) => { + if ('type' in receiver) { + return receiver; // v6 or v2–v5. + } + + // v1 without `type`. + if ('source' in receiver) { + // Legacy repo driver receiver. + return { ...receiver, type: 'repoDriver' } as const; + } + + // Legacy address receiver. + return { ...receiver, type: 'address' } as const; + }); + + // Nothing to persist. + if (splitReceivers.length === 0) { + return; + } + + // 3. Persist receivers. const receiverPromises = splitReceivers.map(async (receiver) => { switch (receiver.type) { case 'repoDriver': @@ -236,6 +256,7 @@ async function createNewSplitReceivers({ blockTimestamp, }, }); + default: return unreachableError( `Unhandled Drip List Split Receiver type: ${(receiver as any).type}`, @@ -248,29 +269,11 @@ async function createNewSplitReceivers({ function validateMetadata( metadata: AnyVersion, -): asserts metadata is Extract< - typeof metadata, - | { - type: 'dripList'; - recipients: ( - | z.infer - | z.infer - | z.infer - | z.infer - )[]; - } - | { - projects: ( - | z.infer - | z.infer - | z.infer - )[]; - } -> { - const isCurrent = 'recipients' in metadata && metadata.type === 'dripList'; - const isLegacy = 'projects' in metadata; +) { + const isV6 = 'recipients' in metadata && metadata.type === 'dripList'; + const isV5AndBelow = 'projects' in metadata; - if (!isCurrent && !isLegacy) { + if (!isV6 && !isV5AndBelow) { throw new Error('Invalid Drip List metadata schema.'); } } From 802b54feedb4877ea6c7875a94b4f7e6a63fde61 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 30 Apr 2025 14:53:56 +0200 Subject: [PATCH 34/60] fix: wrong project owner account ID calculation --- .../handlers/handleProjectMetadata.ts | 18 +++++---- src/utils/accountIdUtils.ts | 40 +------------------ src/utils/assert.ts | 10 ----- src/utils/getAccountAddress.ts | 36 ----------------- src/utils/projectUtils.ts | 10 ++++- 5 files changed, 21 insertions(+), 93 deletions(-) delete mode 100644 src/utils/getAccountAddress.ts diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index 6f95ec6..6cac4c6 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -14,7 +14,6 @@ import { assertIsAddressDiverId, convertToAddressDriverId, convertToRepoDriverId, - getAddress, isAddressDriverId, isNftDriverId, isRepoDriverId, @@ -22,7 +21,10 @@ import { import unreachableError from '../../../utils/unreachableError'; import { getProjectMetadata } from '../../../utils/metadataUtils'; import verifySplitsReceivers from '../verifySplitsReceivers'; -import { repoDriverContract } from '../../../core/contractClients'; +import { + addressDriverContract, + repoDriverContract, +} from '../../../core/contractClients'; import type { ProjectName } from '../../../models/ProjectModel'; import { createSplitReceiver, @@ -67,9 +69,9 @@ export default async function handleProjectMetadata({ return; } - const projectReceivers = metadata.splits.dependencies - .flatMap((s) => ('type' in s && s.type === 'repoDriver' ? [s] : [])) - .filter((dep) => dep.type === 'repoDriver'); + const projectReceivers = metadata.splits.dependencies.flatMap((s) => + 'source' in s ? [s] : [], + ); const { areProjectsValid, message } = await verifyProjectSources(projectReceivers); @@ -163,8 +165,10 @@ async function upsertProject({ verificationStatus: calculateProjectStatus(onChainOwner), isVisible: 'isVisible' in metadata ? metadata.isVisible : true, // Projects without `isVisible` field (V4 and below) are considered visible by default. lastProcessedIpfsHash: ipfsHash, - ownerAddress: getAddress(onChainOwner), - ownerAccountId: convertToAddressDriverId(onChainOwner), + ownerAddress: onChainOwner, + ownerAccountId: convertToAddressDriverId( + (await addressDriverContract.calcAccountId(onChainOwner)).toString(), + ), claimedAt: blockTimestamp, }; diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index ae2ea67..b988ab8 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -1,7 +1,7 @@ /* eslint-disable no-bitwise */ -import { ethers, type AddressLike } from 'ethers'; import type { AccountId, + Address, AddressDriverId, DripsContract, ImmutableSplitsDriverId, @@ -177,7 +177,7 @@ export function assertIsImmutableSplitsDriverId( } } -export async function calcAccountId(owner: AddressLike): Promise { +export async function calcAccountId(owner: Address): Promise { return ( await addressDriverContract.calcAccountId(owner as string) ).toString() as AccountId; @@ -215,39 +215,3 @@ export function assertIsAccountId( ); } } - -export function getAddress(accountId: string): AddressLike { - let accountIdBigInt: bigint; - - try { - accountIdBigInt = BigInt(accountId); - } catch { - throw new Error( - `Failed to get address: '${accountId}' is not a valid bigint string.`, - ); - } - - if (accountIdBigInt < 0n || accountIdBigInt > 2n ** 256n - 1n) { - throw new Error( - `Failed to get address: '${accountId}' is not a valid positive number within the range of a uint256.`, - ); - } - - if (getContractNameFromAccountId(accountId) !== 'addressDriver') { - // Mid 64 bits after first 32 (128-191) must be zero - const mid64Mask = ((1n << 64n) - 1n) << 160n; - - if ((accountIdBigInt & mid64Mask) !== 0n) { - throw new Error( - `Failed to get address: '${accountId}' is not a valid AddressDriver ID. The first 64 (after first 32) bits must be 0.`, - ); - } - } - - const addressMask = (1n << 160n) - 1n; - const addressBigInt = accountIdBigInt & addressMask; - - // Convert to hex, pad to 20 bytes (40 hex chars), and checksum it - const hex = `0x${addressBigInt.toString(16).padStart(40, '0')}`; - return ethers.getAddress(hex); -} diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 2ae481a..59e0a1a 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -1,4 +1,3 @@ -import type { UUID } from 'crypto'; import type { Transaction } from 'sequelize'; import type { EventSignature } from '../events/types'; @@ -10,15 +9,6 @@ export function assertTransaction( } } -export function assertRequestId(requestId: string): asserts requestId is UUID { - const uuidRegExp = - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; - - if (!uuidRegExp.test(requestId)) { - throw new Error(`Request ID ${requestId} is not a valid UUID.`); - } -} - export function assertEventSignature( eventSignature: string, expectedEventSignature: EventSignature, diff --git a/src/utils/getAccountAddress.ts b/src/utils/getAccountAddress.ts deleted file mode 100644 index 5d30adc..0000000 --- a/src/utils/getAccountAddress.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable no-bitwise */ -import { ethers } from 'ethers'; -import { isAddressDriverId } from './accountIdUtils'; - -export default function getUserAddress(accountId: string): string { - if (!accountId) { - throw new Error(`Could not get user address: accountId is missing.`); - } - - const accountIdAsBn = BigInt(accountId); - - if (accountIdAsBn < 0 || accountIdAsBn > ethers.MaxUint256) { - throw new Error( - `Could not get user address: ${accountId} is not a valid positive number within the range of a uint256.`, - ); - } - - if (isAddressDriverId(accountId)) { - const mid64BitsMask = (BigInt(2) ** BigInt(64) - BigInt(1)) << BigInt(160); - - if ((accountIdAsBn & mid64BitsMask) !== BigInt(0)) { - throw new Error( - `Could not get user address: ${accountId} is not a valid user ID. The first 64 (after first 32) bits must be 0.`, - ); - } - } - - const mask = BigInt(2) ** BigInt(160) - BigInt(1); - const address = accountIdAsBn & mask; - - // Convert BigInt to a hex string and pad with zeros - const paddedAddress = address.toString(16).padStart(40, '0').toLowerCase(); - - // You would still use ethers.js to checksum the address - return ethers.getAddress(`0x${paddedAddress}`); -} diff --git a/src/utils/projectUtils.ts b/src/utils/projectUtils.ts index af6f71c..512bab4 100644 --- a/src/utils/projectUtils.ts +++ b/src/utils/projectUtils.ts @@ -3,7 +3,8 @@ import type { z } from 'zod'; import unreachableError from './unreachableError'; import type { Forge, ProjectVerificationStatus } from '../models/ProjectModel'; import { repoDriverContract } from '../core/contractClients'; -import type { repoDriverSplitReceiverSchema } from '../metadata/schemas/repo-driver/v2'; +import type { sourceSchema } from '../metadata/schemas/common/sources'; +import { assertIsRepoDriverId } from './accountIdUtils'; export function convertForgeToNumber(forge: Forge) { switch (forge) { @@ -29,7 +30,10 @@ export function calculateProjectStatus( } export async function verifyProjectSources( - projectReceivers: z.infer[], + projectReceivers: { + accountId: string; + source: z.infer; + }[], ): Promise<{ areProjectsValid: boolean; message?: string; @@ -40,6 +44,8 @@ export async function verifyProjectSources( accountId, source: { forge, ownerName, repoName }, } of projectReceivers) { + assertIsRepoDriverId(accountId); + const calculatedAccountId = await repoDriverContract.calcAccountId( convertForgeToNumber(forge), hexlify(toUtf8Bytes(`${ownerName}/${repoName}`)), From 0f83e4d501dd0a8ba45c7494cd7d9184f4ace7ee Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 30 Apr 2025 14:54:33 +0200 Subject: [PATCH 35/60] refactor: leftover from commit ecaa92bd11d31ee47278f03ec35697044eedeb52 --- src/queue/saveEventProcessingJob.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/queue/saveEventProcessingJob.ts b/src/queue/saveEventProcessingJob.ts index 350992a..22e28c5 100644 --- a/src/queue/saveEventProcessingJob.ts +++ b/src/queue/saveEventProcessingJob.ts @@ -1,4 +1,3 @@ -import { randomUUID } from 'crypto'; import { assertEventSignature } from '../utils/assert'; import eventProcessingQueue from './queue'; import type { EventSignature } from '../events/types'; @@ -6,7 +5,6 @@ import type EventHandlerRequest from '../events/EventHandlerRequest'; export default async function saveEventProcessingJob( request: EventHandlerRequest, - expectedEventSignature: T, ) { const { blockNumber, @@ -17,7 +15,7 @@ export default async function saveEventProcessingJob( eventSignature, } = request.event; - assertEventSignature(eventSignature, expectedEventSignature); + assertEventSignature(eventSignature, eventSignature); return eventProcessingQueue .createJob({ @@ -33,7 +31,7 @@ export default async function saveEventProcessingJob( return value; }), }) - .setId(randomUUID()) + .setId(request.id) .retries(10) .backoff('exponential', 500) .save(); From f8a4bc08e9c63575e26e21a17c8b12c3ee6f68de Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 30 Apr 2025 14:55:03 +0200 Subject: [PATCH 36/60] refactor: minor improvements --- .../SplitsSetEvent/setIsValidFlag.ts | 4 ++-- src/eventHandlers/TransferEventHandler.ts | 23 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts index 929c993..7ddd9c0 100644 --- a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts @@ -46,7 +46,7 @@ export default async function setIsValidFlag( if (!project) { throw new RecoverableError( - `Failed to set 'isValid' flag for Project: Project '${accountId}' not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + `Failed to set 'isValid' flag for Project: Project '${accountId}' not found. Likely waiting on 'AccountMetadataEmitted' event to be processed. Retrying, but if this persists, it is a real error.`, ); } @@ -54,7 +54,7 @@ export default async function setIsValidFlag( const dbOwner = project.ownerAddress; // populated from metadata. if (onChainOwner !== dbOwner) { throw new RecoverableError( - `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for Project '${accountId}'. Likely waiting on latest 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for Project '${accountId}'. Likely waiting on latest 'AccountMetadataEmitted' event to be processed. Retrying, but if this persists, it is a real error.`, ); } diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index 862f363..1ec580e 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -53,8 +53,6 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add `🚨🕵️‍♂️ Skipped Drip List or Ecosystem Main Account ${tokenId} TransferEvent processing: on-chain owner '${onChainOwner}' does not match event 'to' '${to}'.`, ); - scopedLogger.flush(); - return; } @@ -118,12 +116,17 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add entity.ownerAddress = to as Address; entity.previousOwnerAddress = from as Address; - entity.ownerAccountId = await calcAccountId(to); + entity.ownerAccountId = await calcAccountId(to as Address); entity.creator = to as Address; // TODO: https://github.com/drips-network/events-processor/issues/14 - entity.isVisible = - blockNumber > appSettings.visibilityThresholdBlockNumber - ? from === ZeroAddress || from === appSettings.ecosystemDeployer - : true; + + const isAboveThreshold = + blockNumber > appSettings.visibilityThresholdBlockNumber; + const isMint = from === ZeroAddress; + + // Only re-compute vi visibility on real transfers. Mints are handled by metadata. + if (!isMint) { + entity.isVisible = !isAboveThreshold; + } scopedLogger.bufferUpdate({ input: entity, @@ -146,7 +149,11 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add const [from, to, tokenId] = args; await super.afterHandle({ - args: [tokenId, await calcAccountId(from), await calcAccountId(to)], + args: [ + tokenId, + await calcAccountId(from as Address), + await calcAccountId(to as Address), + ], blockTimestamp, requestId, }); From 2ea43d690028124d7ae8fdb9a9d3a31df0a931ad Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 30 Apr 2025 15:22:03 +0200 Subject: [PATCH 37/60] refactor: remove isLatestEvent function --- .../AccountMetadataEmittedEventHandler.ts | 15 -------- src/eventHandlers/TransferEventHandler.ts | 15 -------- src/utils/isLatestEvent.ts | 35 ------------------- ...AccountMetadataEmittedEventHandler.test.ts | 8 ----- .../TransferEventHandler.test.ts | 9 +++-- 5 files changed, 4 insertions(+), 78 deletions(-) delete mode 100644 src/utils/isLatestEvent.ts diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index 8b28c41..9cd9963 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -24,7 +24,6 @@ import handleEcosystemMainAccountMetadata from './handlers/handleEcosystemMainAc import handleSubListMetadata from './handlers/handleSubListMetadata'; import type { nftDriverAccountMetadataParser } from '../../metadata/schemas'; import { getCurrentSplitReceiversBySender } from './receiversRepository'; -import { isLatestEvent } from '../../utils/isLatestEvent'; import type { AccountMetadataEmittedEvent } from '../../../contracts/CURRENT_NETWORK/Drips'; import { AccountMetadataEmittedEventModel } from '../../models'; import ScopedLogger from '../../core/ScopedLogger'; @@ -102,20 +101,6 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase id: `${accountMetadataEmittedEvent.transactionHash}-${accountMetadataEmittedEvent.logIndex}`, }); - // Only process metadata if this is the latest event. - if ( - !(await isLatestEvent( - accountMetadataEmittedEvent, - AccountMetadataEmittedEventModel, - { - accountId: convertToAccountId(accountId), - }, - transaction, - )) - ) { - return; - } - if (isRepoDriverId(accountId)) { await handleProjectMetadata({ ipfsHash, diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index 1ec580e..366d688 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -10,7 +10,6 @@ import { TransferEventModel, } from '../models'; import { dbConnection } from '../db/database'; -import { isLatestEvent } from '../utils/isLatestEvent'; import appSettings from '../config/appSettings'; import RecoverableError from '../utils/recoverableError'; import type { Address } from '../core/types'; @@ -78,20 +77,6 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add id: `${transferEvent.transactionHash}-${transferEvent.logIndex}`, }); - // Only process if this is the latest event. - if ( - !(await isLatestEvent( - transferEvent, - TransferEventModel, - { tokenId }, - transaction, - )) - ) { - scopedLogger.flush(); - - return; - } - const dripList = await DripListModel.findByPk(tokenId, { transaction, lock: transaction.LOCK.UPDATE, diff --git a/src/utils/isLatestEvent.ts b/src/utils/isLatestEvent.ts deleted file mode 100644 index 5e8114c..0000000 --- a/src/utils/isLatestEvent.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - type FindOptions, - type Model, - type Transaction, - type WhereOptions, -} from 'sequelize'; -import type { IEventModel } from '../events/types'; - -export async function isLatestEvent>( - incomingEvent: T, - model: { findOne(options: FindOptions): Promise }, - where: WhereOptions, - transaction: Transaction, -): Promise { - const latest = await model.findOne({ - lock: transaction.LOCK.UPDATE, - transaction, - where, - order: [ - ['block_number', 'DESC'], - ['log_index', 'DESC'], - ], - }); - - if (!latest) { - return true; - } - - const isNewerBlock = latest.blockNumber < incomingEvent.blockNumber; - const isSameBlockNewerLog = - latest.blockNumber === incomingEvent.blockNumber && - latest.logIndex <= incomingEvent.logIndex; - - return isNewerBlock || isSameBlockNewerLog; -} diff --git a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts index 52ca22c..347276f 100644 --- a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts +++ b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts @@ -6,7 +6,6 @@ import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; import type { EventData } from '../../src/events/types'; import { dbConnection } from '../../src/db/database'; import AccountMetadataEmittedEventModel from '../../src/models/AccountMetadataEmittedEventModel'; -import { isLatestEvent } from '../../src/utils/isLatestEvent'; import { convertToAccountId } from '../../src/utils/accountIdUtils'; import { DRIPS_APP_USER_METADATA_KEY } from '../../src/core/constants'; import * as handleProjectMetadata from '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata'; @@ -27,7 +26,6 @@ jest.mock( jest.mock( '../../src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata', ); -jest.mock('../../src/utils/isLatestEvent'); jest.mock('../../src/utils/metadataUtils'); describe('AccountMetadataEmittedHandler', () => { @@ -116,8 +114,6 @@ describe('AccountMetadataEmittedHandler', () => { }, ]); - (isLatestEvent as jest.Mock).mockResolvedValue(false); - // Act await handler['_handle'](mockRequest); @@ -158,8 +154,6 @@ describe('AccountMetadataEmittedHandler', () => { true, ]); - (isLatestEvent as jest.Mock).mockResolvedValue(true); - (handleProjectMetadata.default as jest.Mock) = jest.fn(); // Act @@ -185,8 +179,6 @@ describe('AccountMetadataEmittedHandler', () => { true, ]); - (isLatestEvent as jest.Mock).mockResolvedValue(true); - (handleDripListMetadata.default as jest.Mock) = jest.fn(); const request = { diff --git a/tests/eventHandlers/TransferEventHandler.test.ts b/tests/eventHandlers/TransferEventHandler.test.ts index 95dbe53..374ea7a 100644 --- a/tests/eventHandlers/TransferEventHandler.test.ts +++ b/tests/eventHandlers/TransferEventHandler.test.ts @@ -17,7 +17,6 @@ jest.mock('bee-queue'); jest.mock('../../src/events/eventHandlerUtils'); jest.mock('../../src/utils/accountIdUtils'); jest.mock('../../src/core/ScopedLogger'); -jest.mock('../../src/utils/isLatestEvent'); jest.mock('../../src/core/contractClients'); describe('TransferEventHandler', () => { @@ -45,7 +44,9 @@ describe('TransferEventHandler', () => { } as EventData<'Transfer(address,address,uint256)'>, }; - mockDbTransaction = {}; + mockDbTransaction = { + LOCK: { UPDATE: jest.fn() }, + }; dbConnection.transaction = jest .fn() @@ -62,9 +63,7 @@ describe('TransferEventHandler', () => { }, ]); - DripListModel.findOne = jest - .fn() - .mockResolvedValue([{ save: jest.fn() }, true]); + DripListModel.findByPk = jest.fn().mockResolvedValue({ save: jest.fn() }); ScopedLogger.prototype.bufferCreation = jest.fn().mockReturnThis(); From 7487067e1c1ceb4ae2140fbe765ce25b36bdf332 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 1 May 2025 11:49:11 +0200 Subject: [PATCH 38/60] feat: process latest Drip List data --- .../20250414133746-initial_create.ts | 14 +- .../AccountMetadataEmittedEventHandler.ts | 1 + .../handlers/handleDripListMetadata.ts | 36 +++- .../SplitsSetEvent/setIsValidFlag.ts | 23 +-- src/eventHandlers/TransferEventHandler.ts | 157 ++++++++++++------ src/models/DripListModel.ts | 17 +- src/utils/lastProcessedVersion.ts | 23 +++ ...AccountMetadataEmittedEventHandler.test.ts | 1 + .../TransferEventHandler.test.ts | 4 +- 9 files changed, 203 insertions(+), 73 deletions(-) create mode 100644 src/utils/lastProcessedVersion.ts diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index 24bc71f..bad9e02 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -576,11 +576,11 @@ async function createDripListsTable( type: DataTypes.BOOLEAN, }, ownerAddress: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, ownerAccountId: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, name: { @@ -600,7 +600,7 @@ async function createDripListsTable( type: DataTypes.STRING, }, previousOwnerAddress: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, isVisible: { @@ -611,6 +611,10 @@ async function createDripListsTable( allowNull: false, type: DataTypes.TEXT, }, + lastProcessedVersion: { + allowNull: false, + type: DataTypes.BIGINT, + }, createdAt: { allowNull: false, type: DataTypes.DATE, @@ -684,6 +688,10 @@ async function createEcosystemMainAccountsTable( allowNull: false, type: DataTypes.TEXT, }, + lastProcessedVersion: { + allowNull: false, + type: DataTypes.BIGINT, + }, createdAt: { allowNull: false, type: DataTypes.DATE, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index 9cd9963..a470844 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -117,6 +117,7 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase if (this._isDripListMetadata(metadata)) { await handleDripListMetadata({ ipfsHash, + logIndex, metadata, scopedLogger, transaction, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 5638e3b..263eb36 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -2,7 +2,8 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Transaction } from 'sequelize'; import type { UUID } from 'crypto'; -import type { IpfsHash, NftDriverId } from '../../../core/types'; +import { ZeroAddress } from 'ethers'; +import type { Address, IpfsHash, NftDriverId } from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; import type ScopedLogger from '../../../core/ScopedLogger'; import unreachableError from '../../../utils/unreachableError'; @@ -18,11 +19,18 @@ import { assertIsAddressDiverId, assertIsNftDriverId, assertIsRepoDriverId, + calcAccountId, convertToNftDriverId, } from '../../../utils/accountIdUtils'; +import { + decodeVersion, + makeVersion, +} from '../../../utils/lastProcessedVersion'; +import { nftDriverContract } from '../../../core/contractClients'; type Params = { ipfsHash: IpfsHash; + logIndex: number; blockNumber: number; blockTimestamp: Date; scopedLogger: ScopedLogger; @@ -33,6 +41,7 @@ type Params = { export default async function handleDripListMetadata({ ipfsHash, + logIndex, metadata, scopedLogger, blockNumber, @@ -84,6 +93,7 @@ export default async function handleDripListMetadata({ await upsertDripList({ ipfsHash, + logIndex, metadata, scopedLogger, blockNumber, @@ -93,21 +103,23 @@ export default async function handleDripListMetadata({ deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ - scopedLogger, + metadata, transaction, + scopedLogger, blockTimestamp, emitterAccountId, - metadata, }); } async function upsertDripList({ ipfsHash, + logIndex, metadata, scopedLogger, blockNumber, transaction, }: { + logIndex: number; ipfsHash: IpfsHash; blockNumber: number; scopedLogger: ScopedLogger; @@ -116,11 +128,15 @@ async function upsertDripList({ }) { const accountId = convertToNftDriverId(metadata.describes.accountId); + const onChainOwner = (await nftDriverContract.ownerOf(accountId)) as Address; + const values = { accountId, name: metadata.name ?? null, description: 'description' in metadata ? metadata.description || null : null, + ownerAddress: onChainOwner, + ownerAccountId: await calcAccountId(onChainOwner), latestVotingRoundId: 'latestVotingRoundId' in metadata ? (metadata.latestVotingRoundId as UUID) || null @@ -131,6 +147,7 @@ async function upsertDripList({ 'isVisible' in metadata ? metadata.isVisible : true, + lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), }; const [dripList, isCreation] = await DripListModel.findOrCreate({ @@ -138,11 +155,24 @@ async function upsertDripList({ defaults: { ...values, isValid: false, // Until the `SplitsSet` event is processed. + previousOwnerAddress: ZeroAddress as Address, }, transaction, }); if (!isCreation) { + const newVersion = makeVersion(blockNumber, logIndex); + const storedVersion = BigInt(dripList.lastProcessedVersion); + const { blockNumber: sb, logIndex: sl } = decodeVersion(storedVersion); + + if (newVersion <= storedVersion) { + scopedLogger.log( + `Skipped Drip List ${accountId} stale 'AccountMetadata' event (${blockNumber}:${logIndex} ≤ lastProcessed ${sb}:${sl}).`, + ); + + return; + } + scopedLogger.bufferUpdate({ id: accountId, type: DripListModel, diff --git a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts index 7ddd9c0..ee23673 100644 --- a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts @@ -21,6 +21,7 @@ import { } from '../../models'; import type SplitsSetEventModel from '../../models/SplitsSetEventModel'; import type ScopedLogger from '../../core/ScopedLogger'; +import unreachableError from '../../utils/unreachableError'; export default async function setIsValidFlag( { accountId, receiversHash: eventReceiversHash }: SplitsSetEventModel, @@ -79,15 +80,12 @@ export default async function setIsValidFlag( transaction, lock: transaction.LOCK.UPDATE, }); + const ecosystemMain = await EcosystemMainAccountModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); - const ecosystem = dripList - ? null - : await EcosystemMainAccountModel.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - const entity = dripList ?? ecosystem; + const entity = dripList ?? ecosystemMain!; const Model = dripList ? DripListModel : EcosystemMainAccountModel; if (!entity) { @@ -95,12 +93,17 @@ export default async function setIsValidFlag( `Failed to set 'isValid' flag for ${Model.name}: ${Model.name} '${accountId}' not found.`, ); } + if (dripList && ecosystemMain) { + unreachableError( + `Invariant violation: both Drip List and Ecosystem Main Account found for token '${accountId}'.`, + ); + } const onChainOwner = await nftDriverContract.ownerOf(accountId); - const dbOwner = entity.ownerAddress; // populated from metadata. + const dbOwner = entity.ownerAddress; if (onChainOwner !== dbOwner) { throw new RecoverableError( - `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for ${Model.name} '${accountId}'. Likely waiting on latest 'Transfer' event to be processed. Retrying, but if this persists, it is a real error.`, + `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for ${Model.name} '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, ); } diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index 366d688..a44bb7f 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -10,10 +10,12 @@ import { TransferEventModel, } from '../models'; import { dbConnection } from '../db/database'; -import appSettings from '../config/appSettings'; import RecoverableError from '../utils/recoverableError'; import type { Address } from '../core/types'; import { nftDriverContract } from '../core/contractClients'; +import { decodeVersion, makeVersion } from '../utils/lastProcessedVersion'; +import unreachableError from '../utils/unreachableError'; +import appSettings from '../config/appSettings'; export default class TransferEventHandler extends EventHandlerBase<'Transfer(address,address,uint256)'> { public eventSignatures = ['Transfer(address,address,uint256)' as const]; @@ -30,7 +32,7 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add }, }: EventHandlerRequest<'Transfer(address,address,uint256)'>): Promise { const [from, to, rawTokenId] = args as TransferEvent.OutputTuple; - + const tokenId = convertToNftDriverId(rawTokenId); const scopedLogger = new ScopedLogger(this.name, requestId); scopedLogger.log( @@ -44,18 +46,100 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add ].join('\n'), ); - const tokenId = convertToNftDriverId(rawTokenId); - - const onChainOwner = await nftDriverContract.ownerOf(tokenId); + const onChainOwner = (await nftDriverContract.ownerOf(tokenId)) as Address; if (to !== onChainOwner) { - scopedLogger.bufferMessage( - `🚨🕵️‍♂️ Skipped Drip List or Ecosystem Main Account ${tokenId} TransferEvent processing: on-chain owner '${onChainOwner}' does not match event 'to' '${to}'.`, + scopedLogger.log( + `Skipped Drip List or Ecosystem Main Account ${tokenId} 'Transfer' event processing: on-chain owner '${onChainOwner}' does not match event 'to' '${to}'.`, ); return; } await dbConnection.transaction(async (transaction) => { + const dripList = await DripListModel.findByPk(tokenId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + const ecosystemMain = await EcosystemMainAccountModel.findByPk(tokenId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + const entity = dripList ?? ecosystemMain!; + const Model = dripList ? DripListModel : EcosystemMainAccountModel; + + if (!entity) { + throw new RecoverableError( + `Drip List or Ecosystem Main Account '${tokenId}' not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + if (dripList && ecosystemMain) { + unreachableError( + `Invariant violation: both Drip List and Ecosystem Main Account found for token '${tokenId}'.`, + ); + } + + const newVersion = makeVersion(blockNumber, logIndex); + const storedVersion = BigInt(entity.lastProcessedVersion); + const { blockNumber: sb, logIndex: sl } = decodeVersion(storedVersion); + const isMint = from === ZeroAddress; + + // Always set the `creator` from a mint event. + if (isMint) { + if (entity.creator) { + unreachableError( + `Invariant violation: mint for ${Model.name} ${tokenId} but 'creator' already set to '${entity.creator}'.`, + ); + } + + const transferEvent = await TransferEventModel.create( + { + tokenId, + to: to as Address, + from: from as Address, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + { transaction }, + ); + + scopedLogger.bufferCreation({ + input: transferEvent, + type: TransferEventModel, + id: `${transactionHash}-${logIndex}`, + }); + + entity.creator = onChainOwner; // Equal to `to`. + entity.ownerAddress = onChainOwner; // Equal to `to`. + entity.ownerAccountId = await calcAccountId(onChainOwner); + entity.previousOwnerAddress = ZeroAddress as Address; + + scopedLogger.bufferUpdate({ + type: Model, + id: entity.accountId, + input: entity, + }); + + await entity.save({ transaction }); + + scopedLogger.flush(); + + return; + } + + // Staleness guard: skip if not strictly newer. + if (newVersion <= storedVersion) { + scopedLogger.log( + `Skipped Drip List or Ecosystem Main Account ${tokenId} stale 'Transfer' event (${blockNumber}:${logIndex} ≤ lastProcessed ${sb}:${sl}).`, + ); + + scopedLogger.flush(); + + return; + } + const transferEvent = await TransferEventModel.create( { tokenId, @@ -66,57 +150,32 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add blockTimestamp, transactionHash, }, - { - transaction, - }, + { transaction }, ); scopedLogger.bufferCreation({ type: TransferEventModel, + id: `${transactionHash}-${logIndex}`, input: transferEvent, - id: `${transferEvent.transactionHash}-${transferEvent.logIndex}`, }); - const dripList = await DripListModel.findByPk(tokenId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); + // Normal update. + const actualPrev = entity.ownerAddress ?? (from as Address); + entity.previousOwnerAddress = actualPrev; - const ecosystemMainAccount = await EcosystemMainAccountModel.findByPk( - tokenId, - { - transaction, - lock: transaction.LOCK.UPDATE, - }, - ); - - if (!dripList && !ecosystemMainAccount) { - throw new RecoverableError( - `Drip List or Ecosystem Main Account '${tokenId}' not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } - - const entity = (dripList ?? ecosystemMainAccount)!; - const entityModel = dripList ? DripListModel : EcosystemMainAccountModel; - - entity.ownerAddress = to as Address; - entity.previousOwnerAddress = from as Address; - entity.ownerAccountId = await calcAccountId(to as Address); - entity.creator = to as Address; // TODO: https://github.com/drips-network/events-processor/issues/14 + entity.ownerAddress = onChainOwner; // Equal to `to`. + entity.ownerAccountId = await calcAccountId(onChainOwner); // Equal to `to`. - const isAboveThreshold = - blockNumber > appSettings.visibilityThresholdBlockNumber; - const isMint = from === ZeroAddress; + entity.isVisible = !( + blockNumber > appSettings.visibilityThresholdBlockNumber + ); - // Only re-compute vi visibility on real transfers. Mints are handled by metadata. - if (!isMint) { - entity.isVisible = !isAboveThreshold; - } + entity.lastProcessedVersion = newVersion.toString(); scopedLogger.bufferUpdate({ - input: entity, - type: entityModel, + type: Model, id: entity.accountId, + input: entity, }); await entity.save({ transaction }); @@ -130,17 +189,15 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add blockTimestamp: Date; requestId: string; }): Promise { - const { args, blockTimestamp, requestId } = context; - const [from, to, tokenId] = args; - + const [from, to, tokenId] = context.args; await super.afterHandle({ args: [ tokenId, await calcAccountId(from as Address), await calcAccountId(to as Address), ], - blockTimestamp, - requestId, + blockTimestamp: context.blockTimestamp, + requestId: context.requestId, }); } } diff --git a/src/models/DripListModel.ts b/src/models/DripListModel.ts index 131e2e0..467713f 100644 --- a/src/models/DripListModel.ts +++ b/src/models/DripListModel.ts @@ -18,12 +18,13 @@ export default class DripListModel extends Model< declare public name: string | null; declare public creator: Address | null; declare public description: string | null; - declare public ownerAddress: Address | null; - declare public ownerAccountId: AccountId | null; - declare public previousOwnerAddress: Address | null; + declare public ownerAddress: Address; + declare public ownerAccountId: AccountId; + declare public previousOwnerAddress: Address; declare public latestVotingRoundId: UUID | null; declare public isVisible: boolean; declare public lastProcessedIpfsHash: string; + declare public lastProcessedVersion: string; declare public createdAt: CreationOptional; declare public updatedAt: CreationOptional; @@ -39,11 +40,11 @@ export default class DripListModel extends Model< type: DataTypes.BOOLEAN, }, ownerAddress: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, ownerAccountId: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, name: { @@ -63,7 +64,7 @@ export default class DripListModel extends Model< type: DataTypes.STRING, }, previousOwnerAddress: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, isVisible: { @@ -74,6 +75,10 @@ export default class DripListModel extends Model< allowNull: false, type: DataTypes.TEXT, }, + lastProcessedVersion: { + allowNull: false, + type: DataTypes.BIGINT, + }, createdAt: { allowNull: false, type: DataTypes.DATE, diff --git a/src/utils/lastProcessedVersion.ts b/src/utils/lastProcessedVersion.ts new file mode 100644 index 0000000..4b794cf --- /dev/null +++ b/src/utils/lastProcessedVersion.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-bitwise */ + +/** + * Packs a blockNumber and logIndex into a single BigInt “version.” + */ +export function makeVersion(blockNumber: number, logIndex: number): bigint { + // shift `blockNumber` into the high‐32 bits, OR in the low‐32‐bit `logIndex`. + return (BigInt(blockNumber) << 32n) | BigInt(logIndex); +} + +/** + * Unpacks a BigInt “version” back into its `blockNumber` and `logIndex` parts. + */ +export function decodeVersion(version: bigint): { + blockNumber: number; + logIndex: number; +} { + // High 32 bits contain the blockNumber. + const blockNumber = Number(version >> 32n); + // Low 32 bits contain the logIndex. + const logIndex = Number(version & ((1n << 32n) - 1n)); + return { blockNumber, logIndex }; +} diff --git a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts index 347276f..e7fed9e 100644 --- a/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts +++ b/tests/eventHandlers/AccountMetadataEmittedEventHandler.test.ts @@ -209,6 +209,7 @@ describe('AccountMetadataEmittedHandler', () => { ipfsHash: convertToIpfsHash(request.event.args[2]), metadata: mockMetadata, scopedLogger: expect.anything(), + logIndex: request.event.logIndex, transaction: mockDbTransaction, blockTimestamp: request.event.blockTimestamp, blockNumber: request.event.blockNumber, diff --git a/tests/eventHandlers/TransferEventHandler.test.ts b/tests/eventHandlers/TransferEventHandler.test.ts index 374ea7a..6ed1688 100644 --- a/tests/eventHandlers/TransferEventHandler.test.ts +++ b/tests/eventHandlers/TransferEventHandler.test.ts @@ -63,7 +63,9 @@ describe('TransferEventHandler', () => { }, ]); - DripListModel.findByPk = jest.fn().mockResolvedValue({ save: jest.fn() }); + DripListModel.findByPk = jest + .fn() + .mockResolvedValue({ save: jest.fn(), lastProcessedVersion: '0' }); ScopedLogger.prototype.bufferCreation = jest.fn().mockReturnThis(); From e188926c376beeafc4d4eefa27a4df3767dbaf29 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 1 May 2025 12:01:53 +0200 Subject: [PATCH 39/60] feat: process latest Ecosystem data --- .../20250414133746-initial_create.ts | 6 ++-- .../AccountMetadataEmittedEventHandler.ts | 1 + .../handleEcosystemMainAccountMetadata.ts | 32 ++++++++++++++++++- src/models/EcosystemMainAccountModel.ts | 17 ++++++---- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index bad9e02..be6b34c 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -657,11 +657,11 @@ async function createEcosystemMainAccountsTable( type: DataTypes.BOOLEAN, }, ownerAddress: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, ownerAccountId: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, name: { @@ -677,7 +677,7 @@ async function createEcosystemMainAccountsTable( type: DataTypes.STRING, }, previousOwnerAddress: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, isVisible: { diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index a470844..c860abc 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -130,6 +130,7 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase if (this._isEcosystemMainAccountMetadata(metadata)) { await handleEcosystemMainAccountMetadata({ ipfsHash, + logIndex, metadata, scopedLogger, transaction, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index f6add64..3c7a514 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -1,8 +1,9 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Transaction } from 'sequelize'; import type { z } from 'zod'; +import { ZeroAddress } from 'ethers'; import type ScopedLogger from '../../../core/ScopedLogger'; -import type { IpfsHash, NftDriverId } from '../../../core/types'; +import type { Address, IpfsHash, NftDriverId } from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; import verifySplitsReceivers from '../verifySplitsReceivers'; import type { repoDriverSplitReceiverSchema } from '../../../metadata/schemas/repo-driver/v2'; @@ -11,6 +12,7 @@ import { verifyProjectSources } from '../../../utils/projectUtils'; import { assertIsImmutableSplitsDriverId, assertIsRepoDriverId, + calcAccountId, convertToNftDriverId, } from '../../../utils/accountIdUtils'; import { EcosystemMainAccountModel } from '../../../models'; @@ -20,9 +22,15 @@ import { deleteExistingSplitReceivers, } from '../receiversRepository'; import unreachableError from '../../../utils/unreachableError'; +import { nftDriverContract } from '../../../core/contractClients'; +import { + decodeVersion, + makeVersion, +} from '../../../utils/lastProcessedVersion'; type Params = { ipfsHash: IpfsHash; + logIndex: number; blockNumber: number; blockTimestamp: Date; scopedLogger: ScopedLogger; @@ -33,6 +41,7 @@ type Params = { export default async function handleEcosystemMainAccountMetadata({ ipfsHash, + logIndex, metadata, scopedLogger, blockNumber, @@ -77,6 +86,7 @@ export default async function handleEcosystemMainAccountMetadata({ await upsertEcosystemMainAccount({ ipfsHash, + logIndex, metadata, scopedLogger, blockNumber, @@ -96,11 +106,13 @@ export default async function handleEcosystemMainAccountMetadata({ async function upsertEcosystemMainAccount({ ipfsHash, + logIndex, metadata, scopedLogger, blockNumber, transaction, }: { + logIndex: number; ipfsHash: IpfsHash; blockNumber: number; scopedLogger: ScopedLogger; @@ -109,17 +121,22 @@ async function upsertEcosystemMainAccount({ }) { const accountId = convertToNftDriverId(metadata.describes.accountId); + const onChainOwner = (await nftDriverContract.ownerOf(accountId)) as Address; + const values = { accountId, name: metadata.name ?? null, description: 'description' in metadata ? metadata.description || null : null, + ownerAddress: onChainOwner, + ownerAccountId: await calcAccountId(onChainOwner), lastProcessedIpfsHash: ipfsHash, isVisible: blockNumber > appSettings.visibilityThresholdBlockNumber && 'isVisible' in metadata ? metadata.isVisible : true, + lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), }; const [ecosystemMainAccount, isCreation] = @@ -128,11 +145,24 @@ async function upsertEcosystemMainAccount({ defaults: { ...values, isValid: false, // Until the `SetSplits` event is processed. + previousOwnerAddress: ZeroAddress as Address, }, transaction, }); if (!isCreation) { + const newVersion = makeVersion(blockNumber, logIndex); + const storedVersion = BigInt(ecosystemMainAccount.lastProcessedVersion); + const { blockNumber: sb, logIndex: sl } = decodeVersion(storedVersion); + + if (newVersion <= storedVersion) { + scopedLogger.log( + `Skipped Drip List ${accountId} stale 'AccountMetadata' event (${blockNumber}:${logIndex} ≤ lastProcessed ${sb}:${sl}).`, + ); + + return; + } + scopedLogger.bufferUpdate({ input: ecosystemMainAccount, id: ecosystemMainAccount.accountId, diff --git a/src/models/EcosystemMainAccountModel.ts b/src/models/EcosystemMainAccountModel.ts index bc96749..b523dd0 100644 --- a/src/models/EcosystemMainAccountModel.ts +++ b/src/models/EcosystemMainAccountModel.ts @@ -17,11 +17,12 @@ export default class EcosystemMainAccountModel extends Model< declare public name: string | null; declare public creator: Address | null; declare public description: string | null; - declare public ownerAddress: Address | null; - declare public ownerAccountId: AccountId | null; - declare public previousOwnerAddress: Address | null; + declare public ownerAddress: Address; + declare public ownerAccountId: AccountId; + declare public previousOwnerAddress: Address; declare public isVisible: boolean; declare public lastProcessedIpfsHash: string; + declare public lastProcessedVersion: string; declare public createdAt: CreationOptional; declare public updatedAt: CreationOptional; @@ -37,11 +38,11 @@ export default class EcosystemMainAccountModel extends Model< type: DataTypes.BOOLEAN, }, ownerAddress: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, ownerAccountId: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, name: { @@ -57,7 +58,7 @@ export default class EcosystemMainAccountModel extends Model< type: DataTypes.STRING, }, previousOwnerAddress: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, isVisible: { @@ -68,6 +69,10 @@ export default class EcosystemMainAccountModel extends Model< allowNull: false, type: DataTypes.TEXT, }, + lastProcessedVersion: { + allowNull: false, + type: DataTypes.BIGINT, + }, createdAt: { allowNull: false, type: DataTypes.DATE, From 61369a3deecf0c45d85ad9a9ee40d6b3aeb34693 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 1 May 2025 12:18:55 +0200 Subject: [PATCH 40/60] fix: wrong table name when reverting migrations --- DEVELOPMENT.md | 2 +- scripts/revert-migration.ts | 6 +++++- scripts/run-migrations.ts | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 9d787ed..a8933c9 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -69,7 +69,7 @@ This runs all the above steps in one go. To undo the most recently applied migration: ```bash -npm run db:revert-last-migration +npm run db:revert-migration ``` > ⚠️ Make sure the project is built before running this script, as it uses the compiled .js files under dist. diff --git a/scripts/revert-migration.ts b/scripts/revert-migration.ts index 0228797..bcda50a 100644 --- a/scripts/revert-migration.ts +++ b/scripts/revert-migration.ts @@ -26,7 +26,11 @@ export async function revertLastMigration(): Promise { glob: './dist/src/db/migrations/*.js', // Migrations must be built before running. }, context: sequelize, - storage: new SequelizeStorage({ sequelize, schema }), + storage: new SequelizeStorage({ + sequelize, + schema, + tableName: 'sequelize_meta', + }), logger: console, }); diff --git a/scripts/run-migrations.ts b/scripts/run-migrations.ts index f6c3661..8f03857 100644 --- a/scripts/run-migrations.ts +++ b/scripts/run-migrations.ts @@ -29,7 +29,11 @@ export async function runMigrations(): Promise { glob: './dist/src/db/migrations/*.js', // Migrations must be built before running. }, context: sequelize, - storage: new SequelizeStorage({ sequelize, schema }), + storage: new SequelizeStorage({ + sequelize, + schema, + tableName: 'sequelize_meta', + }), logger: console, }); From ef4244e0b2b68d10227b6130504fe0497f9e249a Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 1 May 2025 16:50:00 +0200 Subject: [PATCH 41/60] refactor: remove util function for calculating AddressDriver IDs --- .../handlers/handleDripListMetadata.ts | 17 +++++++++--- .../handleEcosystemMainAccountMetadata.ts | 17 +++++++++--- src/eventHandlers/TransferEventHandler.ts | 27 +++++++++++++------ src/utils/accountIdUtils.ts | 8 ------ 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 263eb36..247a175 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -3,7 +3,12 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Transaction } from 'sequelize'; import type { UUID } from 'crypto'; import { ZeroAddress } from 'ethers'; -import type { Address, IpfsHash, NftDriverId } from '../../../core/types'; +import type { + Address, + AddressDriverId, + IpfsHash, + NftDriverId, +} from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; import type ScopedLogger from '../../../core/ScopedLogger'; import unreachableError from '../../../utils/unreachableError'; @@ -19,14 +24,16 @@ import { assertIsAddressDiverId, assertIsNftDriverId, assertIsRepoDriverId, - calcAccountId, convertToNftDriverId, } from '../../../utils/accountIdUtils'; import { decodeVersion, makeVersion, } from '../../../utils/lastProcessedVersion'; -import { nftDriverContract } from '../../../core/contractClients'; +import { + addressDriverContract, + nftDriverContract, +} from '../../../core/contractClients'; type Params = { ipfsHash: IpfsHash; @@ -136,7 +143,9 @@ async function upsertDripList({ description: 'description' in metadata ? metadata.description || null : null, ownerAddress: onChainOwner, - ownerAccountId: await calcAccountId(onChainOwner), + ownerAccountId: ( + await addressDriverContract.calcAccountId(onChainOwner) + ).toString() as AddressDriverId, latestVotingRoundId: 'latestVotingRoundId' in metadata ? (metadata.latestVotingRoundId as UUID) || null diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index 3c7a514..d7c6b73 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -3,7 +3,12 @@ import type { Transaction } from 'sequelize'; import type { z } from 'zod'; import { ZeroAddress } from 'ethers'; import type ScopedLogger from '../../../core/ScopedLogger'; -import type { Address, IpfsHash, NftDriverId } from '../../../core/types'; +import type { + Address, + AddressDriverId, + IpfsHash, + NftDriverId, +} from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; import verifySplitsReceivers from '../verifySplitsReceivers'; import type { repoDriverSplitReceiverSchema } from '../../../metadata/schemas/repo-driver/v2'; @@ -12,7 +17,6 @@ import { verifyProjectSources } from '../../../utils/projectUtils'; import { assertIsImmutableSplitsDriverId, assertIsRepoDriverId, - calcAccountId, convertToNftDriverId, } from '../../../utils/accountIdUtils'; import { EcosystemMainAccountModel } from '../../../models'; @@ -22,7 +26,10 @@ import { deleteExistingSplitReceivers, } from '../receiversRepository'; import unreachableError from '../../../utils/unreachableError'; -import { nftDriverContract } from '../../../core/contractClients'; +import { + addressDriverContract, + nftDriverContract, +} from '../../../core/contractClients'; import { decodeVersion, makeVersion, @@ -129,7 +136,9 @@ async function upsertEcosystemMainAccount({ description: 'description' in metadata ? metadata.description || null : null, ownerAddress: onChainOwner, - ownerAccountId: await calcAccountId(onChainOwner), + ownerAccountId: ( + await addressDriverContract.calcAccountId(onChainOwner) + ).toString() as AddressDriverId, lastProcessedIpfsHash: ipfsHash, isVisible: blockNumber > appSettings.visibilityThresholdBlockNumber && diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index a44bb7f..204a4f6 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -2,7 +2,7 @@ import { ZeroAddress } from 'ethers'; import type { TransferEvent } from '../../contracts/CURRENT_NETWORK/NftDriver'; import EventHandlerBase from '../events/EventHandlerBase'; import ScopedLogger from '../core/ScopedLogger'; -import { calcAccountId, convertToNftDriverId } from '../utils/accountIdUtils'; +import { convertToNftDriverId } from '../utils/accountIdUtils'; import type EventHandlerRequest from '../events/EventHandlerRequest'; import { DripListModel, @@ -11,8 +11,11 @@ import { } from '../models'; import { dbConnection } from '../db/database'; import RecoverableError from '../utils/recoverableError'; -import type { Address } from '../core/types'; -import { nftDriverContract } from '../core/contractClients'; +import type { Address, AddressDriverId } from '../core/types'; +import { + addressDriverContract, + nftDriverContract, +} from '../core/contractClients'; import { decodeVersion, makeVersion } from '../utils/lastProcessedVersion'; import unreachableError from '../utils/unreachableError'; import appSettings from '../config/appSettings'; @@ -70,7 +73,7 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add if (!entity) { throw new RecoverableError( - `Drip List or Ecosystem Main Account '${tokenId}' not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + `Cannot process 'Transfer' event for Drip List or Ecosystem Main Account ${tokenId}: entity not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, ); } if (dripList && ecosystemMain) { @@ -113,7 +116,9 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add entity.creator = onChainOwner; // Equal to `to`. entity.ownerAddress = onChainOwner; // Equal to `to`. - entity.ownerAccountId = await calcAccountId(onChainOwner); + entity.ownerAccountId = ( + await addressDriverContract.calcAccountId(onChainOwner) + ).toString() as AddressDriverId; entity.previousOwnerAddress = ZeroAddress as Address; scopedLogger.bufferUpdate({ @@ -164,7 +169,9 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add entity.previousOwnerAddress = actualPrev; entity.ownerAddress = onChainOwner; // Equal to `to`. - entity.ownerAccountId = await calcAccountId(onChainOwner); // Equal to `to`. + entity.ownerAccountId = ( + await addressDriverContract.calcAccountId(onChainOwner) + ).toString() as AddressDriverId; // Equal to `to`. entity.isVisible = !( blockNumber > appSettings.visibilityThresholdBlockNumber @@ -193,8 +200,12 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add await super.afterHandle({ args: [ tokenId, - await calcAccountId(from as Address), - await calcAccountId(to as Address), + ( + await addressDriverContract.calcAccountId(from) + ).toString() as AddressDriverId, + ( + await addressDriverContract.calcAccountId(to) + ).toString() as AddressDriverId, ], blockTimestamp: context.blockTimestamp, requestId: context.requestId, diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index b988ab8..139967e 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -1,14 +1,12 @@ /* eslint-disable no-bitwise */ import type { AccountId, - Address, AddressDriverId, DripsContract, ImmutableSplitsDriverId, NftDriverId, RepoDriverId, } from '../core/types'; -import { addressDriverContract } from '../core/contractClients'; export function getContractNameFromAccountId(id: string): DripsContract { if (Number.isNaN(Number(id))) { @@ -177,12 +175,6 @@ export function assertIsImmutableSplitsDriverId( } } -export async function calcAccountId(owner: Address): Promise { - return ( - await addressDriverContract.calcAccountId(owner as string) - ).toString() as AccountId; -} - // Account ID export function convertToAccountId(id: bigint | string): AccountId { const accountIdAsString = typeof id === 'bigint' ? id.toString() : id; From 134d1ba76d5d6392b6596e9a76a039d2c58ee337 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Fri, 2 May 2025 11:49:42 +0200 Subject: [PATCH 42/60] refactor: lock when handling metadata --- .../handlers/handleDripListMetadata.ts | 3 ++- .../handlers/handleEcosystemMainAccountMetadata.ts | 3 ++- .../handlers/handleSubListMetadata.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 247a175..7256cbd 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -160,13 +160,14 @@ async function upsertDripList({ }; const [dripList, isCreation] = await DripListModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, where: { accountId }, defaults: { ...values, isValid: false, // Until the `SplitsSet` event is processed. previousOwnerAddress: ZeroAddress as Address, }, - transaction, }); if (!isCreation) { diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index d7c6b73..e3f6f88 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -150,13 +150,14 @@ async function upsertEcosystemMainAccount({ const [ecosystemMainAccount, isCreation] = await EcosystemMainAccountModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, where: { accountId }, defaults: { ...values, isValid: false, // Until the `SetSplits` event is processed. previousOwnerAddress: ZeroAddress as Address, }, - transaction, }); if (!isCreation) { diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 90b3614..ff32f92 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -133,8 +133,9 @@ async function upsertSubList({ const accountId = convertToImmutableSplitsDriverId(emitterAccountId); const [subList, isCreation] = await SubListModel.findOrCreate({ - where: { accountId }, transaction, + lock: transaction.LOCK.UPDATE, + where: { accountId }, defaults: { ...values, isValid: false, // Until the `SetSplits` event is processed. From 6c076a173848e6bb30e8ccd69b56724e4b693a51 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Fri, 2 May 2025 11:50:40 +0200 Subject: [PATCH 43/60] refactor: fine tune retry strategy --- src/queue/saveEventProcessingJob.ts | 4 ++-- src/utils/projectUtils.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/queue/saveEventProcessingJob.ts b/src/queue/saveEventProcessingJob.ts index 22e28c5..0a8e7d5 100644 --- a/src/queue/saveEventProcessingJob.ts +++ b/src/queue/saveEventProcessingJob.ts @@ -32,7 +32,7 @@ export default async function saveEventProcessingJob( }), }) .setId(request.id) - .retries(10) - .backoff('exponential', 500) + .retries(15) + .backoff('fixed', 30000) .save(); } diff --git a/src/utils/projectUtils.ts b/src/utils/projectUtils.ts index 512bab4..c9eaf80 100644 --- a/src/utils/projectUtils.ts +++ b/src/utils/projectUtils.ts @@ -1,10 +1,11 @@ -import { hexlify, toUtf8Bytes, ZeroAddress, type AddressLike } from 'ethers'; +import { hexlify, toUtf8Bytes, ZeroAddress } from 'ethers'; import type { z } from 'zod'; import unreachableError from './unreachableError'; import type { Forge, ProjectVerificationStatus } from '../models/ProjectModel'; import { repoDriverContract } from '../core/contractClients'; import type { sourceSchema } from '../metadata/schemas/common/sources'; import { assertIsRepoDriverId } from './accountIdUtils'; +import type { Address } from '../core/types'; export function convertForgeToNumber(forge: Forge) { switch (forge) { @@ -20,7 +21,7 @@ export function convertForgeToNumber(forge: Forge) { } export function calculateProjectStatus( - owner: AddressLike | null, + owner: Address | null, ): ProjectVerificationStatus { if (!owner || owner === ZeroAddress) { return 'unclaimed'; From 54bece763a88209168a765ab8636306be9eaf647 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Fri, 2 May 2025 11:52:05 +0200 Subject: [PATCH 44/60] feat: index OwnerUpdated events --- .../20250414133746-initial_create.ts | 51 ++++++- src/db/modelRegistration.ts | 2 + .../AccountMetadataEmittedEventHandler.ts | 6 +- src/eventHandlers/OwnerUpdatedEventHandler.ts | 139 ++++++++++++++++++ src/models/OwnerUpdatedEventModel.ts | 49 ++++++ src/models/index.ts | 1 + 6 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 src/eventHandlers/OwnerUpdatedEventHandler.ts create mode 100644 src/models/OwnerUpdatedEventModel.ts diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index be6b34c..2c516d7 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -60,6 +60,47 @@ export async function up({ context: sequelize }: any): Promise { await createStreamReceiverSeenEventsTable(queryInterface, schema); await createStreamsSetEventsTable(queryInterface, schema); await createSplitsSetEventsTable(queryInterface, schema); + await createOwnerUpdatedEventsTable(queryInterface, schema); +} + +async function createOwnerUpdatedEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: `owner_updated_events`, + }, + transformFieldNamesToSnakeCase({ + owner: { + allowNull: false, + type: DataTypes.STRING, + }, + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); } async function createSplitsSetEventsTable( @@ -782,6 +823,10 @@ async function createProjectsTable( allowNull: false, type: DataTypes.DATE, }, + lastProcessedVersion: { + allowNull: false, + type: DataTypes.BIGINT, + }, createdAt: { allowNull: false, type: DataTypes.DATE, @@ -1006,7 +1051,11 @@ export async function down({ context: sequelize }: any): Promise { await queryInterface.dropTable({ schema, - tableName: `split_set_events`, + tableName: `owner_updated_events`, + }); + await queryInterface.dropTable({ + schema, + tableName: `splits_set_events`, }); await queryInterface.dropTable({ schema, diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index 859b78c..c6cc0f7 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -13,6 +13,7 @@ import { SubListModel, EcosystemMainAccountModel, SplitReceiverModel, + OwnerUpdatedEventModel, } from '../models'; import SplitsSetEventModel from '../models/SplitsSetEventModel'; @@ -38,6 +39,7 @@ export function registerModels(): void { registerModel(SplitReceiverModel); registerModel(SplitsSetEventModel); registerModel(StreamsSetEventModel); + registerModel(OwnerUpdatedEventModel); registerModel(EcosystemMainAccountModel); registerModel(SqueezedStreamsEventModel); registerModel(StreamReceiverSeenEventModel); diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index c860abc..de84161 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -103,7 +103,9 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase if (isRepoDriverId(accountId)) { await handleProjectMetadata({ + logIndex, ipfsHash, + blockNumber, scopedLogger, transaction, blockTimestamp, @@ -125,9 +127,7 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase blockTimestamp, emitterAccountId: convertToNftDriverId(accountId), }); - } - - if (this._isEcosystemMainAccountMetadata(metadata)) { + } else if (this._isEcosystemMainAccountMetadata(metadata)) { await handleEcosystemMainAccountMetadata({ ipfsHash, logIndex, diff --git a/src/eventHandlers/OwnerUpdatedEventHandler.ts b/src/eventHandlers/OwnerUpdatedEventHandler.ts new file mode 100644 index 0000000..8b5ccd6 --- /dev/null +++ b/src/eventHandlers/OwnerUpdatedEventHandler.ts @@ -0,0 +1,139 @@ +import type { OwnerUpdatedEvent } from '../../contracts/CURRENT_NETWORK/RepoDriver'; +import { + addressDriverContract, + repoDriverContract, +} from '../core/contractClients'; +import ScopedLogger from '../core/ScopedLogger'; +import type { Address, AddressDriverId } from '../core/types'; +import { dbConnection } from '../db/database'; +import EventHandlerBase from '../events/EventHandlerBase'; +import type EventHandlerRequest from '../events/EventHandlerRequest'; +import { ProjectModel } from '../models'; +import OwnerUpdatedEventModel from '../models/OwnerUpdatedEventModel'; +import { convertToRepoDriverId } from '../utils/accountIdUtils'; +import { makeVersion, decodeVersion } from '../utils/lastProcessedVersion'; +import RecoverableError from '../utils/recoverableError'; + +export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpdated(uint256,address)'> { + public readonly eventSignatures = ['OwnerUpdated(uint256,address)' as const]; + + protected async _handle({ + id: requestId, + event: { + args, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + eventSignature, + }, + }: EventHandlerRequest<'OwnerUpdated(uint256,address)'>): Promise { + const [accountId, owner] = args as OwnerUpdatedEvent.OutputTuple; + + const scopedLogger = new ScopedLogger(this.name, requestId); + + scopedLogger.log( + [ + `📥 ${this.name} is processing ${eventSignature}:`, + ` - owner: ${owner}`, + ` - accountId: ${accountId}`, + ` - logIndex: ${logIndex}`, + ` - txHash: ${transactionHash}`, + ].join('\n'), + ); + + const onChainOwner = (await repoDriverContract.ownerOf( + accountId, + )) as Address; + if (owner !== onChainOwner) { + scopedLogger.log( + `Skipped Project ${accountId} 'OwnerUpdated' event processing: on-chain owner '${onChainOwner}' does not match event 'owner' '${owner}'.`, + ); + + return; + } + + await dbConnection.transaction(async (transaction) => { + const ownerUpdatedEvent = await OwnerUpdatedEventModel.create( + { + owner, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + accountId: convertToRepoDriverId(accountId), + }, + { transaction }, + ); + + scopedLogger.bufferCreation({ + input: ownerUpdatedEvent, + type: OwnerUpdatedEventModel, + id: `${transactionHash}-${logIndex}`, + }); + + const project = await ProjectModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!project) { + throw new RecoverableError( + `Cannot process 'OwnerUpdated' event for Project ${accountId}: project not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + + const newVersion = makeVersion(blockNumber, logIndex); + const storedVersion = BigInt(project.lastProcessedVersion); + const { blockNumber: sb, logIndex: sl } = decodeVersion(storedVersion); + + // Staleness guard: skip if not strictly newer. + if (newVersion <= storedVersion) { + scopedLogger.log( + `Skipped Project ${accountId} stale 'OwnerUpdated' event (${blockNumber}:${logIndex} ≤ lastProcessed ${sb}:${sl}).`, + ); + + scopedLogger.flush(); + + return; + } + + project.ownerAddress = owner; + project.claimedAt = blockTimestamp; + project.ownerAccountId = + (( + await addressDriverContract.calcAccountId(onChainOwner) + ).toString() as AddressDriverId) ?? null; + project.verificationStatus = 'claimed'; + + project.lastProcessedVersion = newVersion.toString(); + + scopedLogger.bufferUpdate({ + type: ProjectModel, + id: project.accountId, + input: project, + }); + + await project.save({ transaction }); + + scopedLogger.flush(); + }); + } + + override async afterHandle(context: { + args: [accountId: bigint, owner: string]; + blockTimestamp: Date; + requestId: string; + }): Promise { + const { args, blockTimestamp } = context; + const [accountId, owner] = args; + + const ownerAccountId = await addressDriverContract.calcAccountId(owner); + + super.afterHandle({ + args: [accountId, ownerAccountId], + blockTimestamp, + requestId: context.requestId, + }); + } +} diff --git a/src/models/OwnerUpdatedEventModel.ts b/src/models/OwnerUpdatedEventModel.ts new file mode 100644 index 0000000..d1b2f5d --- /dev/null +++ b/src/models/OwnerUpdatedEventModel.ts @@ -0,0 +1,49 @@ +import type { + InferAttributes, + InferCreationAttributes, + Sequelize, +} from 'sequelize'; +import { DataTypes, Model } from 'sequelize'; +import type { AddressLike } from 'ethers'; +import { COMMON_EVENT_INIT_ATTRIBUTES } from '../core/constants'; +import type { RepoDriverId } from '../core/types'; +import getSchema from '../utils/getSchema'; +import type { IEventModel } from '../events/types'; + +export default class OwnerUpdatedEventModel + extends Model< + InferAttributes, + InferCreationAttributes + > + implements IEventModel +{ + declare public owner: AddressLike; + declare public accountId: RepoDriverId; + declare public logIndex: number; + declare public blockNumber: number; + declare public blockTimestamp: Date; + declare public transactionHash: string; + + public static initialize(sequelize: Sequelize): void { + this.init( + { + owner: { + allowNull: false, + type: DataTypes.STRING, + }, + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + { + sequelize, + underscored: true, + timestamps: false, + schema: getSchema(), + tableName: 'owner_updated_events', + }, + ); + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 5b4db78..6767a1b 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -8,6 +8,7 @@ export { default as TransferEventModel } from './TransferEventModel'; export { default as SplitsSetEventModel } from './SplitsSetEventModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; export { default as _LastIndexedBlockModel } from './_LastIndexedBlockModel'; +export { default as OwnerUpdatedEventModel } from './OwnerUpdatedEventModel'; export { default as SqueezedStreamsEventModel } from './SqueezedStreamsEventModel'; export { default as EcosystemMainAccountModel } from './EcosystemMainAccountModel'; export { default as StreamReceiverSeenEventModel } from './StreamReceiverSeenEventModel'; From f03d1d7d6dc3c3a83c0836adf49ed9fce1503e73 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Fri, 2 May 2025 11:53:03 +0200 Subject: [PATCH 45/60] feat: process latest Project data --- .../handlers/handleProjectMetadata.ts | 55 +++++++++++++------ src/events/poll.ts | 2 +- src/events/registrations.ts | 5 ++ src/models/ProjectModel.ts | 5 ++ 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index 6cac4c6..f1611b8 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -1,7 +1,6 @@ /* eslint-disable no-param-reassign */ // Mutating Sequelize model instance is intentional and safe here. import { type Transaction } from 'sequelize'; import type { AnyVersion } from '@efstajas/versioned-parser'; -import { ZeroAddress } from 'ethers'; import { ProjectModel } from '../../../models'; import type { repoDriverAccountMetadataParser } from '../../../metadata/schemas'; import type ScopedLogger from '../../../core/ScopedLogger'; @@ -9,7 +8,12 @@ import { calculateProjectStatus, verifyProjectSources, } from '../../../utils/projectUtils'; -import type { Address, IpfsHash, RepoDriverId } from '../../../core/types'; +import type { + Address, + AddressDriverId, + IpfsHash, + RepoDriverId, +} from '../../../core/types'; import { assertIsAddressDiverId, convertToAddressDriverId, @@ -30,9 +34,15 @@ import { createSplitReceiver, deleteExistingSplitReceivers, } from '../receiversRepository'; +import { + decodeVersion, + makeVersion, +} from '../../../utils/lastProcessedVersion'; type Params = { + logIndex: number; ipfsHash: IpfsHash; + blockNumber: number; blockTimestamp: Date; scopedLogger: ScopedLogger; transaction: Transaction; @@ -41,6 +51,8 @@ type Params = { export default async function handleProjectMetadata({ ipfsHash, + logIndex, + blockNumber, scopedLogger, transaction, blockTimestamp, @@ -83,25 +95,16 @@ export default async function handleProjectMetadata({ return; } - const onChainOwner = await repoDriverContract.ownerOf(emitterAccountId); - // If on-chain owner is still unset, it likely means the first `OwnerUpdateRequested` and `OwnerUpdated` events were not emitted (yet?). No need to process the metadata. - if (!onChainOwner || onChainOwner === ZeroAddress) { - scopedLogger.bufferMessage( - `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: on-chain owner is not set.`, - ); - - return; - } - // ✅ All checks passed, we can proceed with the processing. await upsertProject({ + logIndex, metadata, ipfsHash, + blockNumber, scopedLogger, transaction, blockTimestamp, - onChainOwner: onChainOwner as Address, }); deleteExistingSplitReceivers(emitterAccountId, transaction); @@ -117,17 +120,19 @@ export default async function handleProjectMetadata({ async function upsertProject({ ipfsHash, + logIndex, metadata, + blockNumber, scopedLogger, transaction, - onChainOwner, blockTimestamp, }: { + logIndex: number; ipfsHash: IpfsHash; + blockNumber: number; blockTimestamp: Date; - scopedLogger: ScopedLogger; transaction: Transaction; - onChainOwner: Address; + scopedLogger: ScopedLogger; metadata: AnyVersion; }): Promise { const { @@ -153,6 +158,7 @@ async function upsertProject({ } const accountId = convertToRepoDriverId(describes.accountId); + const onChainOwner = (await repoDriverContract.ownerOf(accountId)) as Address; const values = { accountId, @@ -168,11 +174,14 @@ async function upsertProject({ ownerAddress: onChainOwner, ownerAccountId: convertToAddressDriverId( (await addressDriverContract.calcAccountId(onChainOwner)).toString(), - ), + ) as AddressDriverId, claimedAt: blockTimestamp, + lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), }; const [project, isCreation] = await ProjectModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, where: { accountId }, defaults: { ...values, @@ -181,6 +190,18 @@ async function upsertProject({ }); if (!isCreation) { + const newVersion = makeVersion(blockNumber, logIndex); + const storedVersion = BigInt(project.lastProcessedVersion); + const { blockNumber: sb, logIndex: sl } = decodeVersion(storedVersion); + + if (newVersion <= storedVersion) { + scopedLogger.log( + `Skipped Project ${accountId} stale 'AccountMetadata' event (${blockNumber}:${logIndex} ≤ lastProcessed ${sb}:${sl}).`, + ); + + return; + } + scopedLogger.bufferUpdate({ type: ProjectModel, input: project, diff --git a/src/events/poll.ts b/src/events/poll.ts index 1e730e3..1a80cac 100644 --- a/src/events/poll.ts +++ b/src/events/poll.ts @@ -97,7 +97,7 @@ export default async function poll( args: parsedLog.args as KnownAny, eventSignature: signature, }, - `${log.blockNumber}-${log.transactionHash}-${log.index}`, + `${log.blockNumber}:${log.transactionHash}:${log.index}`, ), ); } diff --git a/src/events/registrations.ts b/src/events/registrations.ts index 672174a..7f45011 100644 --- a/src/events/registrations.ts +++ b/src/events/registrations.ts @@ -7,6 +7,7 @@ import { StreamsSetEventHandler, SqueezedStreamsEventHandler, } from '../eventHandlers'; +import OwnerUpdatedEventHandler from '../eventHandlers/OwnerUpdatedEventHandler'; import SplitsSetEventHandler from '../eventHandlers/SplitsSetEvent/SplitsSetEventHandler'; import { registerEventHandler } from './eventHandlerUtils'; @@ -43,4 +44,8 @@ export function registerEventHandlers(): void { 'SqueezedStreams(uint256,address,uint256,uint128,bytes32[])', SqueezedStreamsEventHandler, ); + registerEventHandler<'OwnerUpdated(uint256,address)'>( + 'OwnerUpdated(uint256,address)', + OwnerUpdatedEventHandler, + ); } diff --git a/src/models/ProjectModel.ts b/src/models/ProjectModel.ts index 746e4b1..7dab9d4 100644 --- a/src/models/ProjectModel.ts +++ b/src/models/ProjectModel.ts @@ -36,6 +36,7 @@ export default class ProjectModel extends Model< declare public ownerAddress: AddressLike; declare public ownerAccountId: AddressDriverId; declare public claimedAt: Date; + declare public lastProcessedVersion: string; declare public createdAt: CreationOptional; declare public updatedAt: CreationOptional; @@ -94,6 +95,10 @@ export default class ProjectModel extends Model< allowNull: false, type: DataTypes.TEXT, }, + lastProcessedVersion: { + allowNull: false, + type: DataTypes.STRING, + }, claimedAt: { allowNull: false, type: DataTypes.DATE, From 7131be40beed508aa3b369f7d737484b3b6f511c Mon Sep 17 00:00:00 2001 From: jtourkos Date: Sat, 3 May 2025 08:42:14 +0200 Subject: [PATCH 46/60] refactor: remove underscore prefix from last indexed block model name --- src/db/modelRegistration.ts | 4 ++-- src/events/poll.ts | 6 +++--- src/health.ts | 4 ++-- ..._LastIndexedBlockModel.ts => LastIndexedBlockModel.ts} | 8 ++++---- src/models/index.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) rename src/models/{_LastIndexedBlockModel.ts => LastIndexedBlockModel.ts} (83%) diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index c6cc0f7..73a6841 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -7,7 +7,7 @@ import { GivenEventModel, StreamsSetEventModel, StreamReceiverSeenEventModel, - _LastIndexedBlockModel, + LastIndexedBlockModel, SplitEventModel, SqueezedStreamsEventModel, SubListModel, @@ -28,7 +28,7 @@ export function getRegisteredModels(): ModelStaticMembers[] { } export function registerModels(): void { - registerModel(_LastIndexedBlockModel); + registerModel(LastIndexedBlockModel); registerModel(SubListModel); registerModel(ProjectModel); diff --git a/src/events/poll.ts b/src/events/poll.ts index 1a80cac..ca080bc 100644 --- a/src/events/poll.ts +++ b/src/events/poll.ts @@ -6,12 +6,12 @@ import type { Address, KnownAny } from '../core/types'; import EventHandlerRequest from './EventHandlerRequest'; import type EventHandlerBase from './EventHandlerBase'; import type { EventSignature } from './types'; -import _LastIndexedBlockModel from '../models/_LastIndexedBlockModel'; +import LastIndexedBlockModel from '../models/LastIndexedBlockModel'; import logger from '../core/logger'; import appSettings from '../config/appSettings'; async function getLatestIndexedBlock() { - const record = await _LastIndexedBlockModel.findOne({ + const record = await LastIndexedBlockModel.findOne({ order: [['block_number', 'DESC']], }); @@ -19,7 +19,7 @@ async function getLatestIndexedBlock() { } function setLatestIndexedBlock(blockNumber: number) { - return _LastIndexedBlockModel.upsert({ + return LastIndexedBlockModel.upsert({ id: 1, blockNumber: BigInt(blockNumber), }); diff --git a/src/health.ts b/src/health.ts index 73d5154..af9f7be 100644 --- a/src/health.ts +++ b/src/health.ts @@ -1,7 +1,7 @@ import type { RequestHandler } from 'express'; import logger from './core/logger'; import getProvider from './core/getProvider'; -import _LastIndexedBlockModel from './models/_LastIndexedBlockModel'; +import LastIndexedBlockModel from './models/LastIndexedBlockModel'; export const healthEndpoint: RequestHandler = async (req, res) => { const HEALTH_THRESHOLD = 10; @@ -10,7 +10,7 @@ export const healthEndpoint: RequestHandler = async (req, res) => { const provider = getProvider(); const latestChainBlock = await provider.getBlockNumber(); - const lastIndexedBlockRecord = await _LastIndexedBlockModel.findOne({ + const lastIndexedBlockRecord = await LastIndexedBlockModel.findOne({ order: [['blockNumber', 'DESC']], }); diff --git a/src/models/_LastIndexedBlockModel.ts b/src/models/LastIndexedBlockModel.ts similarity index 83% rename from src/models/_LastIndexedBlockModel.ts rename to src/models/LastIndexedBlockModel.ts index 91fe648..bdf2896 100644 --- a/src/models/_LastIndexedBlockModel.ts +++ b/src/models/LastIndexedBlockModel.ts @@ -7,9 +7,9 @@ import type { import { DataTypes, Model } from 'sequelize'; import getSchema from '../utils/getSchema'; -export default class _LastIndexedBlockModel extends Model< - InferAttributes<_LastIndexedBlockModel>, - InferCreationAttributes<_LastIndexedBlockModel> +export default class LastIndexedBlockModel extends Model< + InferAttributes, + InferCreationAttributes > { declare public blockNumber: bigint; declare public id: number; @@ -41,7 +41,7 @@ export default class _LastIndexedBlockModel extends Model< { sequelize, schema: getSchema(), - tableName: '_last_indexed_block', + tableName: 'last_indexed_block', underscored: true, }, ); diff --git a/src/models/index.ts b/src/models/index.ts index 6767a1b..e29d58a 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -7,7 +7,7 @@ export { default as SplitReceiverModel } from './SplitReceiverModel'; export { default as TransferEventModel } from './TransferEventModel'; export { default as SplitsSetEventModel } from './SplitsSetEventModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; -export { default as _LastIndexedBlockModel } from './_LastIndexedBlockModel'; +export { default as LastIndexedBlockModel } from './LastIndexedBlockModel'; export { default as OwnerUpdatedEventModel } from './OwnerUpdatedEventModel'; export { default as SqueezedStreamsEventModel } from './SqueezedStreamsEventModel'; export { default as EcosystemMainAccountModel } from './EcosystemMainAccountModel'; From 1cae034fd96ac980f5d31d400a78efbdb8ed7b65 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 5 May 2025 11:36:23 +0200 Subject: [PATCH 47/60] refactor: fine tune retry strategy --- src/queue/saveEventProcessingJob.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/queue/saveEventProcessingJob.ts b/src/queue/saveEventProcessingJob.ts index 0a8e7d5..43ec301 100644 --- a/src/queue/saveEventProcessingJob.ts +++ b/src/queue/saveEventProcessingJob.ts @@ -32,7 +32,7 @@ export default async function saveEventProcessingJob( }), }) .setId(request.id) - .retries(15) + .retries(20) .backoff('fixed', 30000) .save(); } From 9aa654ee6bcc6182ee44d4f0debbae44a50cdb64 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 5 May 2025 11:37:14 +0200 Subject: [PATCH 48/60] refactor: properly handle mint --- src/eventHandlers/TransferEventHandler.ts | 137 +++++++++------------- 1 file changed, 53 insertions(+), 84 deletions(-) diff --git a/src/eventHandlers/TransferEventHandler.ts b/src/eventHandlers/TransferEventHandler.ts index 204a4f6..00afc90 100644 --- a/src/eventHandlers/TransferEventHandler.ts +++ b/src/eventHandlers/TransferEventHandler.ts @@ -16,9 +16,9 @@ import { addressDriverContract, nftDriverContract, } from '../core/contractClients'; -import { decodeVersion, makeVersion } from '../utils/lastProcessedVersion'; import unreachableError from '../utils/unreachableError'; import appSettings from '../config/appSettings'; +import { makeVersion } from '../utils/lastProcessedVersion'; export default class TransferEventHandler extends EventHandlerBase<'Transfer(address,address,uint256)'> { public eventSignatures = ['Transfer(address,address,uint256)' as const]; @@ -36,6 +36,9 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add }: EventHandlerRequest<'Transfer(address,address,uint256)'>): Promise { const [from, to, rawTokenId] = args as TransferEvent.OutputTuple; const tokenId = convertToNftDriverId(rawTokenId); + + const isMint = from === ZeroAddress; + const scopedLogger = new ScopedLogger(this.name, requestId); scopedLogger.log( @@ -49,34 +52,51 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add ].join('\n'), ); - const onChainOwner = (await nftDriverContract.ownerOf(tokenId)) as Address; - if (to !== onChainOwner) { - scopedLogger.log( - `Skipped Drip List or Ecosystem Main Account ${tokenId} 'Transfer' event processing: on-chain owner '${onChainOwner}' does not match event 'to' '${to}'.`, + await dbConnection.transaction(async (transaction) => { + const transferEvent = await TransferEventModel.create( + { + tokenId, + to: to as Address, + from: from as Address, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + { transaction }, ); - return; - } + scopedLogger.bufferCreation({ + type: TransferEventModel, + id: `${transactionHash}-${logIndex}`, + input: transferEvent, + }); - await dbConnection.transaction(async (transaction) => { const dripList = await DripListModel.findByPk(tokenId, { transaction, lock: transaction.LOCK.UPDATE, }); - const ecosystemMain = await EcosystemMainAccountModel.findByPk(tokenId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - const entity = dripList ?? ecosystemMain!; + const ecosystemMainAccount = await EcosystemMainAccountModel.findByPk( + tokenId, + { + transaction, + lock: transaction.LOCK.UPDATE, + }, + ); + + const entity = dripList ?? ecosystemMainAccount; const Model = dripList ? DripListModel : EcosystemMainAccountModel; if (!entity) { + scopedLogger.flush(); + throw new RecoverableError( - `Cannot process 'Transfer' event for Drip List or Ecosystem Main Account ${tokenId}: entity not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, + `Cannot process '${eventSignature}' event for Drip List or Ecosystem Main Account ${tokenId}: entity not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, ); } - if (dripList && ecosystemMain) { + + if (dripList && ecosystemMainAccount) { unreachableError( `Invariant violation: both Drip List and Ecosystem Main Account found for token '${tokenId}'.`, ); @@ -84,42 +104,9 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add const newVersion = makeVersion(blockNumber, logIndex); const storedVersion = BigInt(entity.lastProcessedVersion); - const { blockNumber: sb, logIndex: sl } = decodeVersion(storedVersion); - const isMint = from === ZeroAddress; - // Always set the `creator` from a mint event. if (isMint) { - if (entity.creator) { - unreachableError( - `Invariant violation: mint for ${Model.name} ${tokenId} but 'creator' already set to '${entity.creator}'.`, - ); - } - - const transferEvent = await TransferEventModel.create( - { - tokenId, - to: to as Address, - from: from as Address, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - { transaction }, - ); - - scopedLogger.bufferCreation({ - input: transferEvent, - type: TransferEventModel, - id: `${transactionHash}-${logIndex}`, - }); - - entity.creator = onChainOwner; // Equal to `to`. - entity.ownerAddress = onChainOwner; // Equal to `to`. - entity.ownerAccountId = ( - await addressDriverContract.calcAccountId(onChainOwner) - ).toString() as AddressDriverId; - entity.previousOwnerAddress = ZeroAddress as Address; + entity.creator = to as Address; scopedLogger.bufferUpdate({ type: Model, @@ -128,16 +115,15 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add }); await entity.save({ transaction }); - - scopedLogger.flush(); - - return; } - // Staleness guard: skip if not strictly newer. - if (newVersion <= storedVersion) { - scopedLogger.log( - `Skipped Drip List or Ecosystem Main Account ${tokenId} stale 'Transfer' event (${blockNumber}:${logIndex} ≤ lastProcessed ${sb}:${sl}).`, + const onChainOwner = (await nftDriverContract.ownerOf( + tokenId, + )) as Address; + + if (to !== onChainOwner) { + scopedLogger.bufferMessage( + `Skipped Drip List or Ecosystem Main Account ${tokenId} '${eventSignature}' event processing: event is not the latest (on-chain owner '${onChainOwner}' does not match 'to' '${to}').`, ); scopedLogger.flush(); @@ -145,37 +131,20 @@ export default class TransferEventHandler extends EventHandlerBase<'Transfer(add return; } - const transferEvent = await TransferEventModel.create( - { - tokenId, - to: to as Address, - from: from as Address, - logIndex, - blockNumber, - blockTimestamp, - transactionHash, - }, - { transaction }, - ); - - scopedLogger.bufferCreation({ - type: TransferEventModel, - id: `${transactionHash}-${logIndex}`, - input: transferEvent, - }); - - // Normal update. - const actualPrev = entity.ownerAddress ?? (from as Address); - entity.previousOwnerAddress = actualPrev; - + // Update to the latest on-chain state. entity.ownerAddress = onChainOwner; // Equal to `to`. entity.ownerAccountId = ( await addressDriverContract.calcAccountId(onChainOwner) ).toString() as AddressDriverId; // Equal to `to`. - - entity.isVisible = !( - blockNumber > appSettings.visibilityThresholdBlockNumber - ); + entity.previousOwnerAddress = from as Address; + + // Safely update fields that another event handler could also modify. + if (newVersion > storedVersion) { + entity.isVisible = + blockNumber > appSettings.visibilityThresholdBlockNumber + ? from === ZeroAddress // If it's a mint, then the Drip List will be visible. If it's a real transfer, then it's not. + : true; // If the block number is less than the visibility threshold, then the Drip List is visible by default. + } entity.lastProcessedVersion = newVersion.toString(); From 7bb81eeb7be75f8c39edd3df1a712e7a14759c6f Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 5 May 2025 11:38:39 +0200 Subject: [PATCH 49/60] feat: process Projects --- .../20250414133746-initial_create.ts | 49 ++-- .../AccountMetadataEmittedEventHandler.ts | 20 ++ .../handlers/handleDripListMetadata.ts | 79 +++--- .../handleEcosystemMainAccountMetadata.ts | 37 ++- .../handlers/handleProjectMetadata.ts | 228 +++++++++--------- .../handlers/handleSubListMetadata.ts | 40 ++- .../receiversRepository.ts | 1 - src/eventHandlers/OwnerUpdatedEventHandler.ts | 91 +++---- .../SplitsSetEvent/setIsValidFlag.ts | 31 ++- src/models/DripListModel.ts | 4 +- src/models/ProjectModel.ts | 60 +++-- src/utils/isLatestEvent.ts | 33 +++ src/utils/projectUtils.ts | 27 ++- 13 files changed, 437 insertions(+), 263 deletions(-) create mode 100644 src/utils/isLatestEvent.ts diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index 2c516d7..9f90184 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -35,7 +35,8 @@ export async function up({ context: sequelize }: any): Promise { await queryInterface.sequelize.query(` CREATE TYPE ${schema}.project_verification_status AS ENUM ( 'claimed', - 'unclaimed' + 'unclaimed', + 'pending_metadata' ); `); @@ -492,7 +493,7 @@ async function createLastIndexedBlockTable( await queryInterface.createTable( { schema, - tableName: `_last_indexed_block`, + tableName: `last_indexed_block`, }, transformFieldNamesToSnakeCase({ id: { @@ -641,7 +642,7 @@ async function createDripListsTable( type: DataTypes.STRING, }, previousOwnerAddress: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, isVisible: { @@ -718,7 +719,7 @@ async function createEcosystemMainAccountsTable( type: DataTypes.STRING, }, previousOwnerAddress: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, isVisible: { @@ -774,8 +775,12 @@ async function createProjectsTable( allowNull: false, type: DataTypes.BOOLEAN, }, - name: { + isVisible: { allowNull: false, + type: DataTypes.BOOLEAN, + }, + name: { + allowNull: true, type: DataTypes.STRING, }, verificationStatus: { @@ -783,20 +788,20 @@ async function createProjectsTable( type: literal(`${schema}.project_verification_status`) .val as unknown as DataType, }, - forge: { - allowNull: false, - type: literal(`${schema}.forges`).val as unknown as DataType, - }, ownerAddress: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, ownerAccountId: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, + forge: { + allowNull: true, + type: literal(`${schema}.forges`).val as unknown as DataType, + }, url: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, emoji: { @@ -808,24 +813,20 @@ async function createProjectsTable( type: DataTypes.STRING, }, color: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, - isVisible: { - allowNull: false, - type: DataTypes.BOOLEAN, - }, lastProcessedIpfsHash: { - allowNull: false, + allowNull: true, type: DataTypes.TEXT, }, - claimedAt: { - allowNull: false, - type: DataTypes.DATE, - }, lastProcessedVersion: { allowNull: false, - type: DataTypes.BIGINT, + type: DataTypes.STRING, + }, + claimedAt: { + allowNull: true, + type: DataTypes.DATE, }, createdAt: { allowNull: false, @@ -1107,7 +1108,7 @@ export async function down({ context: sequelize }: any): Promise { }); await queryInterface.dropTable({ schema, - tableName: `_last_indexed_block`, + tableName: `last_indexed_block`, }); await queryInterface.sequelize.query(` diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index de84161..2abfcc1 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -27,6 +27,7 @@ import { getCurrentSplitReceiversBySender } from './receiversRepository'; import type { AccountMetadataEmittedEvent } from '../../../contracts/CURRENT_NETWORK/Drips'; import { AccountMetadataEmittedEventModel } from '../../models'; import ScopedLogger from '../../core/ScopedLogger'; +import { isLatestEvent } from '../../utils/isLatestEvent'; export default class AccountMetadataEmittedEventHandler extends EventHandlerBase<'AccountMetadataEmitted(uint256,bytes32,bytes)'> { public readonly eventSignatures = [ @@ -101,6 +102,23 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase id: `${accountMetadataEmittedEvent.transactionHash}-${accountMetadataEmittedEvent.logIndex}`, }); + if ( + !(await isLatestEvent( + accountMetadataEmittedEvent, + AccountMetadataEmittedEventModel, + { + accountId, + logIndex, + transactionHash, + }, + transaction, + )) + ) { + scopedLogger.flush(); + + return; + } + if (isRepoDriverId(accountId)) { await handleProjectMetadata({ logIndex, @@ -144,6 +162,8 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase if (isImmutableSplitsDriverId(accountId)) { await handleSubListMetadata({ ipfsHash, + logIndex, + blockNumber, scopedLogger, transaction, blockTimestamp, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 7256cbd..198ae28 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -2,7 +2,6 @@ import type { AnyVersion } from '@efstajas/versioned-parser'; import type { Transaction } from 'sequelize'; import type { UUID } from 'crypto'; -import { ZeroAddress } from 'ethers'; import type { Address, AddressDriverId, @@ -26,14 +25,12 @@ import { assertIsRepoDriverId, convertToNftDriverId, } from '../../../utils/accountIdUtils'; -import { - decodeVersion, - makeVersion, -} from '../../../utils/lastProcessedVersion'; +import { makeVersion } from '../../../utils/lastProcessedVersion'; import { addressDriverContract, nftDriverContract, } from '../../../core/contractClients'; +import { ProjectModel } from '../../../models'; type Params = { ipfsHash: IpfsHash; @@ -111,6 +108,8 @@ export default async function handleDripListMetadata({ await createNewSplitReceivers({ metadata, + logIndex, + blockNumber, transaction, scopedLogger, blockTimestamp, @@ -151,11 +150,6 @@ async function upsertDripList({ ? (metadata.latestVotingRoundId as UUID) || null : null, lastProcessedIpfsHash: ipfsHash, - isVisible: - blockNumber > appSettings.visibilityThresholdBlockNumber && - 'isVisible' in metadata - ? metadata.isVisible - : true, lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), }; @@ -166,21 +160,31 @@ async function upsertDripList({ defaults: { ...values, isValid: false, // Until the `SplitsSet` event is processed. - previousOwnerAddress: ZeroAddress as Address, + isVisible: + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true, }, }); - if (!isCreation) { + if (isCreation) { + scopedLogger.bufferCreation({ + id: accountId, + type: DripListModel, + input: dripList, + }); + } else { const newVersion = makeVersion(blockNumber, logIndex); const storedVersion = BigInt(dripList.lastProcessedVersion); - const { blockNumber: sb, logIndex: sl } = decodeVersion(storedVersion); - if (newVersion <= storedVersion) { - scopedLogger.log( - `Skipped Drip List ${accountId} stale 'AccountMetadata' event (${blockNumber}:${logIndex} ≤ lastProcessed ${sb}:${sl}).`, - ); - - return; + // Safely update fields that another event handler could also modify. + if (newVersion > storedVersion) { + dripList.isVisible = + blockNumber > appSettings.visibilityThresholdBlockNumber && + 'isVisible' in metadata + ? metadata.isVisible + : true; } scopedLogger.bufferUpdate({ @@ -189,23 +193,24 @@ async function upsertDripList({ input: dripList, }); - await dripList.update(values, { transaction }); - } else { - scopedLogger.bufferCreation({ - id: accountId, - type: DripListModel, - input: dripList, - }); + await dripList.update( + { ...values, isVisible: dripList.isVisible }, + { transaction }, + ); } } async function createNewSplitReceivers({ metadata, + logIndex, + blockNumber, scopedLogger, transaction, blockTimestamp, emitterAccountId, }: { + logIndex: number; + blockNumber: number; blockTimestamp: Date; scopedLogger: ScopedLogger; transaction: Transaction; @@ -248,10 +253,28 @@ async function createNewSplitReceivers({ switch (receiver.type) { case 'repoDriver': assertIsRepoDriverId(receiver.accountId); + + await ProjectModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + accountId: receiver.accountId, + }, + defaults: { + accountId: receiver.accountId, + verificationStatus: 'unclaimed', + isVisible: true, // Visible by default. Account metadata will set the final visibility. + isValid: true, // There are no receivers yet. Consider the project valid. + url: receiver.source.url, + forge: receiver.source.forge, + name: `${receiver.source.ownerName}/${receiver.source.repoName}`, + lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), + }, + }); + return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'drip_list', @@ -268,7 +291,6 @@ async function createNewSplitReceivers({ return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'drip_list', @@ -285,7 +307,6 @@ async function createNewSplitReceivers({ return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'drip_list', diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index e3f6f88..aa3b596 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -19,7 +19,7 @@ import { assertIsRepoDriverId, convertToNftDriverId, } from '../../../utils/accountIdUtils'; -import { EcosystemMainAccountModel } from '../../../models'; +import { EcosystemMainAccountModel, ProjectModel } from '../../../models'; import appSettings from '../../../config/appSettings'; import { createSplitReceiver, @@ -103,8 +103,10 @@ export default async function handleEcosystemMainAccountMetadata({ deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ - scopedLogger, + logIndex, + blockNumber, transaction, + scopedLogger, blockTimestamp, emitterAccountId, splitReceivers: metadata.recipients, @@ -165,7 +167,7 @@ async function upsertEcosystemMainAccount({ const storedVersion = BigInt(ecosystemMainAccount.lastProcessedVersion); const { blockNumber: sb, logIndex: sl } = decodeVersion(storedVersion); - if (newVersion <= storedVersion) { + if (newVersion < storedVersion) { scopedLogger.log( `Skipped Drip List ${accountId} stale 'AccountMetadata' event (${blockNumber}:${logIndex} ≤ lastProcessed ${sb}:${sl}).`, ); @@ -190,12 +192,16 @@ async function upsertEcosystemMainAccount({ } async function createNewSplitReceivers({ - splitReceivers, - scopedLogger, + logIndex, + blockNumber, transaction, + scopedLogger, + splitReceivers, blockTimestamp, emitterAccountId, }: { + logIndex: number; + blockNumber: number; blockTimestamp: Date; scopedLogger: ScopedLogger; transaction: Transaction; @@ -209,10 +215,28 @@ async function createNewSplitReceivers({ switch (receiver.type) { case 'repoDriver': assertIsRepoDriverId(receiver.accountId); + + await ProjectModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + accountId: receiver.accountId, + }, + defaults: { + accountId: receiver.accountId, + verificationStatus: 'unclaimed', + isVisible: true, // Visible by default. Account metadata will set the final visibility. + isValid: true, // There are no receivers yet. Consider the project valid. + url: receiver.source.url, + forge: receiver.source.forge, + name: `${receiver.source.ownerName}/${receiver.source.repoName}`, + lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), + }, + }); + return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'ecosystem_main_account', @@ -229,7 +253,6 @@ async function createNewSplitReceivers({ return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'ecosystem_main_account', diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index f1611b8..229e867 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -8,16 +8,9 @@ import { calculateProjectStatus, verifyProjectSources, } from '../../../utils/projectUtils'; -import type { - Address, - AddressDriverId, - IpfsHash, - RepoDriverId, -} from '../../../core/types'; +import type { IpfsHash, RepoDriverId } from '../../../core/types'; import { assertIsAddressDiverId, - convertToAddressDriverId, - convertToRepoDriverId, isAddressDriverId, isNftDriverId, isRepoDriverId, @@ -25,19 +18,12 @@ import { import unreachableError from '../../../utils/unreachableError'; import { getProjectMetadata } from '../../../utils/metadataUtils'; import verifySplitsReceivers from '../verifySplitsReceivers'; -import { - addressDriverContract, - repoDriverContract, -} from '../../../core/contractClients'; -import type { ProjectName } from '../../../models/ProjectModel'; import { createSplitReceiver, deleteExistingSplitReceivers, } from '../receiversRepository'; -import { - decodeVersion, - makeVersion, -} from '../../../utils/lastProcessedVersion'; +import { makeVersion } from '../../../utils/lastProcessedVersion'; +import RecoverableError from '../../../utils/recoverableError'; type Params = { logIndex: number; @@ -81,11 +67,33 @@ export default async function handleProjectMetadata({ return; } + const project = await ProjectModel.findByPk(emitterAccountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!project) { + scopedLogger.bufferMessage( + `Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: project not found. Likely waiting on 'OwnerUpdated' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + + scopedLogger.flush(); + + throw new RecoverableError( + `Cannot process metadata for Project ${emitterAccountId}: entity not found. Likely waiting on 'OwnerUpdated' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + const projectReceivers = metadata.splits.dependencies.flatMap((s) => 'source' in s ? [s] : [], ); - const { areProjectsValid, message } = - await verifyProjectSources(projectReceivers); + const { areProjectsValid, message } = await verifyProjectSources([ + ...projectReceivers, + { + accountId: emitterAccountId, + source: metadata.source, + }, + ]); if (!areProjectsValid) { scopedLogger.bufferMessage( @@ -95,21 +103,41 @@ export default async function handleProjectMetadata({ return; } + // We'll store `source` information the metadata, not from the 'OwnerUpdatedRequested' event. + // Therefore, it's necessary to also verify the project's source directly. + const { areProjectsValid: isProjectSourceValid } = await verifyProjectSources( + [ + { + accountId: emitterAccountId, + source: metadata.source, + }, + ], + ); + + if (!isProjectSourceValid) { + scopedLogger.bufferMessage( + `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: ${message}`, + ); + return; + } + // ✅ All checks passed, we can proceed with the processing. - await upsertProject({ - logIndex, - metadata, + await updateProject({ + project, ipfsHash, + metadata, + logIndex, blockNumber, - scopedLogger, transaction, - blockTimestamp, + scopedLogger, }); deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ + logIndex, + blockNumber, scopedLogger, transaction, blockTimestamp, @@ -118,113 +146,71 @@ export default async function handleProjectMetadata({ }); } -async function upsertProject({ - ipfsHash, +async function updateProject({ + project, logIndex, metadata, + ipfsHash, + transaction, blockNumber, scopedLogger, - transaction, - blockTimestamp, }: { logIndex: number; - ipfsHash: IpfsHash; blockNumber: number; - blockTimestamp: Date; + project: ProjectModel; + ipfsHash: IpfsHash; transaction: Transaction; scopedLogger: ScopedLogger; metadata: AnyVersion; }): Promise { - const { - color, - source: { forge, ownerName, repoName, url }, - describes, - } = metadata; - - function getEmoji(): string | null { - if ('avatar' in metadata) { - return metadata.avatar.type === 'emoji' ? metadata.avatar.emoji : null; - } - - return metadata.emoji ?? null; - } - - function getAvatarCid(): string | null { - if ('avatar' in metadata && metadata.avatar.type === 'image') { - return metadata.avatar.cid; + const { color, source } = metadata; + + project.url = source.url; + project.forge = source.forge; + project.name = `${source.ownerName}/${source.repoName}`; + project.color = color; + + project.lastProcessedIpfsHash = ipfsHash; + project.lastProcessedVersion = makeVersion(blockNumber, logIndex).toString(); + project.verificationStatus = calculateProjectStatus(project); + project.isVisible = 'isVisible' in metadata ? metadata.isVisible : true; // Projects without `isVisible` field (V4 and below) are considered visible by default. + + if ('avatar' in metadata) { + // Metadata V4 + + if (metadata.avatar.type === 'emoji') { + project.emoji = metadata.avatar.emoji; + project.avatarCid = null; + } else if (metadata.avatar.type === 'image') { + project.avatarCid = metadata.avatar.cid; + project.emoji = null; } + } else { + // Metadata V3 - return null; + project.emoji = metadata.emoji; } - const accountId = convertToRepoDriverId(describes.accountId); - const onChainOwner = (await repoDriverContract.ownerOf(accountId)) as Address; - - const values = { - accountId, - url, - forge, - emoji: getEmoji(), - color, - name: `${ownerName}/${repoName}` as ProjectName, - avatarCid: getAvatarCid(), - verificationStatus: calculateProjectStatus(onChainOwner), - isVisible: 'isVisible' in metadata ? metadata.isVisible : true, // Projects without `isVisible` field (V4 and below) are considered visible by default. - lastProcessedIpfsHash: ipfsHash, - ownerAddress: onChainOwner, - ownerAccountId: convertToAddressDriverId( - (await addressDriverContract.calcAccountId(onChainOwner)).toString(), - ) as AddressDriverId, - claimedAt: blockTimestamp, - lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), - }; - - const [project, isCreation] = await ProjectModel.findOrCreate({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { accountId }, - defaults: { - ...values, - isValid: false, // Until the `SplitsSet` event is processed. - }, + scopedLogger.bufferUpdate({ + type: ProjectModel, + input: project, + id: project.accountId, }); - if (!isCreation) { - const newVersion = makeVersion(blockNumber, logIndex); - const storedVersion = BigInt(project.lastProcessedVersion); - const { blockNumber: sb, logIndex: sl } = decodeVersion(storedVersion); - - if (newVersion <= storedVersion) { - scopedLogger.log( - `Skipped Project ${accountId} stale 'AccountMetadata' event (${blockNumber}:${logIndex} ≤ lastProcessed ${sb}:${sl}).`, - ); - - return; - } - - scopedLogger.bufferUpdate({ - type: ProjectModel, - input: project, - id: project.accountId, - }); - - await project.update(values, { transaction }); - } else { - scopedLogger.bufferCreation({ - type: ProjectModel, - input: project, - id: project.accountId, - }); - } + await project.save({ transaction }); } async function createNewSplitReceivers({ - scopedLogger, + logIndex, + blockNumber, transaction, - splitReceivers, + scopedLogger, blockTimestamp, + splitReceivers, emitterAccountId, }: { + logIndex: number; + blockNumber: number; blockTimestamp: Date; scopedLogger: ScopedLogger; transaction: Transaction; @@ -239,7 +225,6 @@ async function createNewSplitReceivers({ return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'project', @@ -254,10 +239,33 @@ async function createNewSplitReceivers({ const dependencyPromises = dependencies.map(async (dependency) => { if (isRepoDriverId(dependency.accountId)) { + if (!('source' in dependency)) { + throw new Error( + `Project dependency ${dependency.accountId} is missing source information.`, + ); + } + + await ProjectModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + accountId: dependency.accountId, + }, + defaults: { + accountId: dependency.accountId, + verificationStatus: 'unclaimed', + isVisible: true, // Visible by default. Account metadata will set the final visibility. + isValid: true, // There are no receivers yet. Consider the project valid. + url: dependency.source.url, + forge: dependency.source.forge, + name: `${dependency.source.ownerName}/${dependency.source.repoName}`, + lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), + }, + }); + return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'project', @@ -274,7 +282,6 @@ async function createNewSplitReceivers({ return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'project', @@ -291,7 +298,6 @@ async function createNewSplitReceivers({ return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'project', diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index ff32f92..4ec8c18 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -4,7 +4,11 @@ import type { z } from 'zod'; import type { AnyVersion } from '@efstajas/versioned-parser'; import type ScopedLogger from '../../../core/ScopedLogger'; import type { ImmutableSplitsDriverId, IpfsHash } from '../../../core/types'; -import { EcosystemMainAccountModel, SubListModel } from '../../../models'; +import { + EcosystemMainAccountModel, + ProjectModel, + SubListModel, +} from '../../../models'; import { getImmutableSpitsDriverMetadata } from '../../../utils/metadataUtils'; import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; @@ -30,8 +34,11 @@ import { convertToAccountId, convertToImmutableSplitsDriverId, } from '../../../utils/accountIdUtils'; +import { makeVersion } from '../../../utils/lastProcessedVersion'; type Params = { + logIndex: number; + blockNumber: number; ipfsHash: IpfsHash; blockTimestamp: Date; scopedLogger: ScopedLogger; @@ -41,6 +48,8 @@ type Params = { export default async function handleSubListMetadata({ ipfsHash, + logIndex, + blockNumber, scopedLogger, transaction, blockTimestamp, @@ -99,6 +108,8 @@ export default async function handleSubListMetadata({ deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ + logIndex, + blockNumber, scopedLogger, transaction, blockTimestamp, @@ -160,12 +171,16 @@ async function upsertSubList({ } async function createNewSplitReceivers({ + logIndex, receivers, + blockNumber, scopedLogger, transaction, blockTimestamp, emitterAccountId, }: { + logIndex: number; + blockNumber: number; blockTimestamp: Date; scopedLogger: ScopedLogger; transaction: Transaction; @@ -181,10 +196,28 @@ async function createNewSplitReceivers({ switch (receiver.type) { case 'repoDriver': assertIsRepoDriverId(receiver.accountId); + + await ProjectModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + accountId: receiver.accountId, + }, + defaults: { + accountId: receiver.accountId, + verificationStatus: 'unclaimed', + isVisible: true, // Visible by default. Account metadata will set the final visibility. + isValid: true, // There are no receivers yet. Consider the project valid. + url: receiver.source.url, + forge: receiver.source.forge, + name: `${receiver.source.ownerName}/${receiver.source.repoName}`, + lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), + }, + }); + return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'sub_list', @@ -201,7 +234,6 @@ async function createNewSplitReceivers({ return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'sub_list', @@ -218,7 +250,6 @@ async function createNewSplitReceivers({ return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'sub_list', @@ -235,7 +266,6 @@ async function createNewSplitReceivers({ return createSplitReceiver({ scopedLogger, transaction, - blockTimestamp, splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'sub_list', diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts index 7c51bd0..20a12eb 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts @@ -21,7 +21,6 @@ export async function createSplitReceiver({ transaction, splitReceiverShape, }: { - blockTimestamp: Date; scopedLogger: ScopedLogger; transaction: Transaction; splitReceiverShape: SplitReceiverShape; diff --git a/src/eventHandlers/OwnerUpdatedEventHandler.ts b/src/eventHandlers/OwnerUpdatedEventHandler.ts index 8b5ccd6..32a1002 100644 --- a/src/eventHandlers/OwnerUpdatedEventHandler.ts +++ b/src/eventHandlers/OwnerUpdatedEventHandler.ts @@ -11,8 +11,8 @@ import type EventHandlerRequest from '../events/EventHandlerRequest'; import { ProjectModel } from '../models'; import OwnerUpdatedEventModel from '../models/OwnerUpdatedEventModel'; import { convertToRepoDriverId } from '../utils/accountIdUtils'; -import { makeVersion, decodeVersion } from '../utils/lastProcessedVersion'; -import RecoverableError from '../utils/recoverableError'; +import { makeVersion } from '../utils/lastProcessedVersion'; +import { calculateProjectStatus } from '../utils/projectUtils'; export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpdated(uint256,address)'> { public readonly eventSignatures = ['OwnerUpdated(uint256,address)' as const]; @@ -28,15 +28,16 @@ export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpd eventSignature, }, }: EventHandlerRequest<'OwnerUpdated(uint256,address)'>): Promise { - const [accountId, owner] = args as OwnerUpdatedEvent.OutputTuple; + const [rawAccountId, owner] = args as OwnerUpdatedEvent.OutputTuple; + const accountId = convertToRepoDriverId(rawAccountId); const scopedLogger = new ScopedLogger(this.name, requestId); scopedLogger.log( [ `📥 ${this.name} is processing ${eventSignature}:`, - ` - owner: ${owner}`, - ` - accountId: ${accountId}`, + ` - owner: ${owner}`, + ` - accountId: ${accountId}`, ` - logIndex: ${logIndex}`, ` - txHash: ${transactionHash}`, ].join('\n'), @@ -61,7 +62,7 @@ export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpd blockNumber, blockTimestamp, transactionHash, - accountId: convertToRepoDriverId(accountId), + accountId, }, { transaction }, ); @@ -72,49 +73,57 @@ export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpd id: `${transactionHash}-${logIndex}`, }); - const project = await ProjectModel.findByPk(accountId, { + const [project, isCreation] = await ProjectModel.findOrCreate({ transaction, lock: transaction.LOCK.UPDATE, + where: { + accountId, + }, + defaults: { + accountId, + ownerAddress: onChainOwner, + ownerAccountId: ( + await addressDriverContract.calcAccountId(onChainOwner) + ).toString() as AddressDriverId, + claimedAt: blockTimestamp, + verificationStatus: 'pending_metadata', + isVisible: true, // Visible by default. Account metadata will set the final visibility. + isValid: true, // There are no receivers yet. Consider the project valid. + lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), + }, }); - if (!project) { - throw new RecoverableError( - `Cannot process 'OwnerUpdated' event for Project ${accountId}: project not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } - - const newVersion = makeVersion(blockNumber, logIndex); - const storedVersion = BigInt(project.lastProcessedVersion); - const { blockNumber: sb, logIndex: sl } = decodeVersion(storedVersion); - - // Staleness guard: skip if not strictly newer. - if (newVersion <= storedVersion) { - scopedLogger.log( - `Skipped Project ${accountId} stale 'OwnerUpdated' event (${blockNumber}:${logIndex} ≤ lastProcessed ${sb}:${sl}).`, - ); - - scopedLogger.flush(); - - return; - } - - project.ownerAddress = owner; - project.claimedAt = blockTimestamp; - project.ownerAccountId = - (( + if (isCreation) { + scopedLogger.bufferCreation({ + type: ProjectModel, + input: project, + id: project.accountId, + }); + } else { + const newVersion = makeVersion(blockNumber, logIndex); + const storedVersion = BigInt(project.lastProcessedVersion); + + // Safely update fields that another event handler could also modify. + if (newVersion > storedVersion) { + project.verificationStatus = calculateProjectStatus(project); + } + + project.ownerAccountId = ( await addressDriverContract.calcAccountId(onChainOwner) - ).toString() as AddressDriverId) ?? null; - project.verificationStatus = 'claimed'; + ).toString() as AddressDriverId; + project.ownerAddress = onChainOwner; + project.claimedAt = blockTimestamp; - project.lastProcessedVersion = newVersion.toString(); + project.lastProcessedVersion = newVersion.toString(); - scopedLogger.bufferUpdate({ - type: ProjectModel, - id: project.accountId, - input: project, - }); + scopedLogger.bufferUpdate({ + type: ProjectModel, + id: project.accountId, + input: project, + }); - await project.save({ transaction }); + await project.save({ transaction }); + } scopedLogger.flush(); }); diff --git a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts index ee23673..6af508c 100644 --- a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts @@ -55,7 +55,7 @@ export default async function setIsValidFlag( const dbOwner = project.ownerAddress; // populated from metadata. if (onChainOwner !== dbOwner) { throw new RecoverableError( - `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for Project '${accountId}'. Likely waiting on latest 'AccountMetadataEmitted' event to be processed. Retrying, but if this persists, it is a real error.`, + `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for Project '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, ); } @@ -72,14 +72,18 @@ export default async function setIsValidFlag( await project.save({ transaction }); - scopedLogger.bufferMessage( - `Set 'isValid' for Project '${accountId}' to '${isValid}'.`, - ); + if (!isValid) { + // Rethrow the error to trigger a retry. Eventually, the on-chain hash should match the DB hash. + throw new RecoverableError( + `On-chain splits hash '${onChainReceiversHash}' does not match DB splits hash '${dbReceiversHash}' for Project '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } } else if (isNftDriverId(accountId)) { const dripList = await DripListModel.findByPk(accountId, { transaction, lock: transaction.LOCK.UPDATE, }); + const ecosystemMain = await EcosystemMainAccountModel.findByPk(accountId, { transaction, lock: transaction.LOCK.UPDATE, @@ -93,6 +97,7 @@ export default async function setIsValidFlag( `Failed to set 'isValid' flag for ${Model.name}: ${Model.name} '${accountId}' not found.`, ); } + if (dripList && ecosystemMain) { unreachableError( `Invariant violation: both Drip List and Ecosystem Main Account found for token '${accountId}'.`, @@ -120,9 +125,12 @@ export default async function setIsValidFlag( await entity.save({ transaction }); - scopedLogger.bufferMessage( - `Set 'isValid' for ${Model.name} '${accountId}' to '${isValid}'.`, - ); + if (!isValid) { + // Rethrow the error to trigger a retry. Eventually, the on-chain hash should match the DB hash. + throw new RecoverableError( + `On-chain splits hash '${onChainReceiversHash}' does not match DB splits hash '${dbReceiversHash}' for ${Model.name} '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } } else if (isImmutableSplitsDriverId(accountId)) { const subList = await SubListModel.findByPk(accountId, { transaction, @@ -148,9 +156,12 @@ export default async function setIsValidFlag( await subList.save({ transaction }); - scopedLogger.bufferMessage( - `Set 'isValid' for SubList '${accountId}' to '${isValid}'.`, - ); + if (!isValid) { + // Rethrow the error to trigger a retry. Eventually, the on-chain hash should match the DB hash. + throw new RecoverableError( + `On-chain splits hash '${onChainReceiversHash}' does not match DB splits hash '${dbReceiversHash}' for SubList '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } } } diff --git a/src/models/DripListModel.ts b/src/models/DripListModel.ts index 467713f..559b9e8 100644 --- a/src/models/DripListModel.ts +++ b/src/models/DripListModel.ts @@ -20,7 +20,7 @@ export default class DripListModel extends Model< declare public description: string | null; declare public ownerAddress: Address; declare public ownerAccountId: AccountId; - declare public previousOwnerAddress: Address; + declare public previousOwnerAddress: Address | null; declare public latestVotingRoundId: UUID | null; declare public isVisible: boolean; declare public lastProcessedIpfsHash: string; @@ -64,7 +64,7 @@ export default class DripListModel extends Model< type: DataTypes.STRING, }, previousOwnerAddress: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, isVisible: { diff --git a/src/models/ProjectModel.ts b/src/models/ProjectModel.ts index 7dab9d4..797ce07 100644 --- a/src/models/ProjectModel.ts +++ b/src/models/ProjectModel.ts @@ -5,11 +5,14 @@ import type { Sequelize, } from 'sequelize'; import { DataTypes, Model } from 'sequelize'; -import type { AddressLike } from 'ethers'; import getSchema from '../utils/getSchema'; -import type { AddressDriverId, RepoDriverId } from '../core/types'; +import type { Address, AddressDriverId, RepoDriverId } from '../core/types'; -export const PROJECT_VERIFICATION_STATUSES = ['claimed', 'unclaimed'] as const; +export const PROJECT_VERIFICATION_STATUSES = [ + 'claimed', + 'unclaimed', + 'pending_metadata', +] as const; export type ProjectVerificationStatus = (typeof PROJECT_VERIFICATION_STATUSES)[number]; @@ -22,20 +25,25 @@ export default class ProjectModel extends Model< InferAttributes, InferCreationAttributes > { + // Populated by `OwnerUpdated` declare public accountId: RepoDriverId; - declare public url: string; - declare public forge: Forge; + declare public ownerAddress: Address | null; + declare public ownerAccountId: AddressDriverId | null; + declare public claimedAt: Date | null; + + // Populated by `AccountMetadataEmitted` + declare public url: string | null; + declare public forge: Forge | null; + declare public name: ProjectName | null; declare public emoji: string | null; - declare public color: string; - declare public name: ProjectName; + declare public color: string | null; declare public avatarCid: string | null; + declare public lastProcessedIpfsHash: string | null; + + // Common declare public verificationStatus: ProjectVerificationStatus; declare public isValid: boolean; declare public isVisible: boolean; - declare public lastProcessedIpfsHash: string; - declare public ownerAddress: AddressLike; - declare public ownerAccountId: AddressDriverId; - declare public claimedAt: Date; declare public lastProcessedVersion: string; declare public createdAt: CreationOptional; declare public updatedAt: CreationOptional; @@ -51,28 +59,32 @@ export default class ProjectModel extends Model< allowNull: false, type: DataTypes.BOOLEAN, }, - name: { + isVisible: { allowNull: false, + type: DataTypes.BOOLEAN, + }, + name: { + allowNull: true, type: DataTypes.STRING, }, verificationStatus: { allowNull: false, type: DataTypes.ENUM(...PROJECT_VERIFICATION_STATUSES), }, - forge: { - allowNull: false, - type: DataTypes.ENUM(...FORGES), - }, ownerAddress: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, ownerAccountId: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, + forge: { + allowNull: true, + type: DataTypes.ENUM(...FORGES), + }, url: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, emoji: { @@ -84,15 +96,11 @@ export default class ProjectModel extends Model< type: DataTypes.STRING, }, color: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, - isVisible: { - allowNull: false, - type: DataTypes.BOOLEAN, - }, lastProcessedIpfsHash: { - allowNull: false, + allowNull: true, type: DataTypes.TEXT, }, lastProcessedVersion: { @@ -100,7 +108,7 @@ export default class ProjectModel extends Model< type: DataTypes.STRING, }, claimedAt: { - allowNull: false, + allowNull: true, type: DataTypes.DATE, }, createdAt: { diff --git a/src/utils/isLatestEvent.ts b/src/utils/isLatestEvent.ts new file mode 100644 index 0000000..8105832 --- /dev/null +++ b/src/utils/isLatestEvent.ts @@ -0,0 +1,33 @@ +import type { FindOptions, Model, Transaction, WhereOptions } from 'sequelize'; +import type { IEventModel } from '../events/types'; + +export async function isLatestEvent>( + incomingEvent: T, + model: { findOne(options: FindOptions): Promise }, + where: WhereOptions, + transaction: Transaction, +): Promise { + const latestEventInDb = await model.findOne({ + lock: true, + transaction, + where, + order: [ + ['blockNumber', 'DESC'], + ['logIndex', 'DESC'], + ], + }); + + if (!latestEventInDb) { + return true; + } + + if ( + latestEventInDb.blockNumber > incomingEvent.blockNumber || + (latestEventInDb.blockNumber === incomingEvent.blockNumber && + latestEventInDb.logIndex > incomingEvent.logIndex) + ) { + return false; + } + + return true; +} diff --git a/src/utils/projectUtils.ts b/src/utils/projectUtils.ts index c9eaf80..e796b9c 100644 --- a/src/utils/projectUtils.ts +++ b/src/utils/projectUtils.ts @@ -1,11 +1,11 @@ -import { hexlify, toUtf8Bytes, ZeroAddress } from 'ethers'; +import { hexlify, toUtf8Bytes } from 'ethers'; import type { z } from 'zod'; import unreachableError from './unreachableError'; +import type ProjectModel from '../models/ProjectModel'; import type { Forge, ProjectVerificationStatus } from '../models/ProjectModel'; import { repoDriverContract } from '../core/contractClients'; import type { sourceSchema } from '../metadata/schemas/common/sources'; import { assertIsRepoDriverId } from './accountIdUtils'; -import type { Address } from '../core/types'; export function convertForgeToNumber(forge: Forge) { switch (forge) { @@ -21,17 +21,30 @@ export function convertForgeToNumber(forge: Forge) { } export function calculateProjectStatus( - owner: Address | null, + project: ProjectModel, ): ProjectVerificationStatus { - if (!owner || owner === ZeroAddress) { + const hasOwner = Boolean(project.ownerAddress); + const hasMetadata = Boolean(project.lastProcessedIpfsHash); + + if (hasOwner && hasMetadata) { + return 'claimed'; + } + + if (!hasOwner && !hasMetadata) { return 'unclaimed'; } - return 'claimed'; + if (hasOwner) { + return 'pending_metadata'; + } + + return unreachableError( + `Invalid project status: hasOwner=${hasOwner}, hasMetadata=${hasMetadata}`, + ); } export async function verifyProjectSources( - projectReceivers: { + projects: { accountId: string; source: z.infer; }[], @@ -44,7 +57,7 @@ export async function verifyProjectSources( for (const { accountId, source: { forge, ownerName, repoName }, - } of projectReceivers) { + } of projects) { assertIsRepoDriverId(accountId); const calculatedAccountId = await repoDriverContract.calcAccountId( From 7297de33176484656a4c6f417b94cb87e6fa8060 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 5 May 2025 12:27:09 +0200 Subject: [PATCH 50/60] refactor: simplify common timestamps column creation --- src/core/constants.ts | 8 + .../20250414133746-initial_create.ts | 172 +----------------- .../AccountMetadataEmittedEventModel.ts | 2 +- src/models/GivenEventModel.ts | 2 +- src/models/OwnerUpdatedEventModel.ts | 2 +- src/models/SplitEventModel.ts | 2 +- src/models/SplitsSetEventModel.ts | 2 +- src/models/SqueezedStreamsEventModel.ts | 2 +- src/models/StreamReceiverSeenEventModel.ts | 2 +- src/models/StreamsSetEventModel.ts | 2 +- src/models/TransferEventModel.ts | 2 +- 11 files changed, 27 insertions(+), 171 deletions(-) diff --git a/src/core/constants.ts b/src/core/constants.ts index cdf613f..3a2fca4 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -41,4 +41,12 @@ export const COMMON_EVENT_INIT_ATTRIBUTES = { allowNull: false, type: DataTypes.INTEGER, }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, } as const; diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index 9f90184..110c03f 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -2,6 +2,7 @@ import { DataTypes, literal } from 'sequelize'; import type { DataType, QueryInterface } from 'sequelize'; import getSchema from '../../utils/getSchema'; import type { DbSchema } from '../../core/types'; +import { COMMON_EVENT_INIT_ATTRIBUTES } from '../../core/constants'; export async function up({ context: sequelize }: any): Promise { const schema = getSchema(); @@ -82,24 +83,7 @@ async function createOwnerUpdatedEventsTable( allowNull: false, type: DataTypes.STRING, }, - transactionHash: { - primaryKey: true, - allowNull: false, - type: DataTypes.STRING, - }, - logIndex: { - primaryKey: true, - allowNull: false, - type: DataTypes.INTEGER, - }, - blockNumber: { - allowNull: false, - type: DataTypes.INTEGER, - }, - blockTimestamp: { - allowNull: false, - type: DataTypes.DATE, - }, + ...COMMON_EVENT_INIT_ATTRIBUTES, }), ); } @@ -122,24 +106,7 @@ async function createSplitsSetEventsTable( allowNull: false, type: DataTypes.STRING, }, - transactionHash: { - primaryKey: true, - allowNull: false, - type: DataTypes.STRING, - }, - logIndex: { - primaryKey: true, - allowNull: false, - type: DataTypes.INTEGER, - }, - blockNumber: { - allowNull: false, - type: DataTypes.INTEGER, - }, - blockTimestamp: { - allowNull: false, - type: DataTypes.DATE, - }, + ...COMMON_EVENT_INIT_ATTRIBUTES, }), ); } @@ -178,24 +145,7 @@ async function createStreamsSetEventsTable( allowNull: false, type: DataTypes.STRING, }, - transactionHash: { - primaryKey: true, - allowNull: false, - type: DataTypes.STRING, - }, - logIndex: { - primaryKey: true, - allowNull: false, - type: DataTypes.INTEGER, - }, - blockNumber: { - allowNull: false, - type: DataTypes.INTEGER, - }, - blockTimestamp: { - allowNull: false, - type: DataTypes.DATE, - }, + ...COMMON_EVENT_INIT_ATTRIBUTES, }), ); @@ -243,24 +193,7 @@ async function createStreamReceiverSeenEventsTable( allowNull: false, type: DataTypes.STRING, }, - transactionHash: { - primaryKey: true, - allowNull: false, - type: DataTypes.STRING, - }, - logIndex: { - primaryKey: true, - allowNull: false, - type: DataTypes.INTEGER, - }, - blockNumber: { - allowNull: false, - type: DataTypes.INTEGER, - }, - blockTimestamp: { - allowNull: false, - type: DataTypes.DATE, - }, + ...COMMON_EVENT_INIT_ATTRIBUTES, }), ); @@ -306,24 +239,7 @@ async function createSqueezedStreamsEventsTable( allowNull: false, type: DataTypes.TEXT, }, - transactionHash: { - primaryKey: true, - allowNull: false, - type: DataTypes.STRING, - }, - logIndex: { - primaryKey: true, - allowNull: false, - type: DataTypes.INTEGER, - }, - blockNumber: { - allowNull: false, - type: DataTypes.INTEGER, - }, - blockTimestamp: { - allowNull: false, - type: DataTypes.DATE, - }, + ...COMMON_EVENT_INIT_ATTRIBUTES, }), ); } @@ -354,24 +270,7 @@ async function createSplitEventsTable( allowNull: false, type: DataTypes.STRING, }, - transactionHash: { - primaryKey: true, - allowNull: false, - type: DataTypes.STRING, - }, - logIndex: { - primaryKey: true, - allowNull: false, - type: DataTypes.INTEGER, - }, - blockNumber: { - allowNull: false, - type: DataTypes.INTEGER, - }, - blockTimestamp: { - allowNull: false, - type: DataTypes.DATE, - }, + ...COMMON_EVENT_INIT_ATTRIBUTES, }), ); @@ -423,24 +322,7 @@ async function createGivenEventsTable( allowNull: false, type: DataTypes.STRING, }, - transactionHash: { - primaryKey: true, - allowNull: false, - type: DataTypes.STRING, - }, - logIndex: { - primaryKey: true, - allowNull: false, - type: DataTypes.INTEGER, - }, - blockNumber: { - allowNull: false, - type: DataTypes.INTEGER, - }, - blockTimestamp: { - allowNull: false, - type: DataTypes.DATE, - }, + ...COMMON_EVENT_INIT_ATTRIBUTES, }), ); @@ -893,24 +775,7 @@ async function createAccountMetadataEventsTable( allowNull: false, type: DataTypes.STRING, }, - transactionHash: { - primaryKey: true, - allowNull: false, - type: DataTypes.STRING, - }, - logIndex: { - primaryKey: true, - allowNull: false, - type: DataTypes.INTEGER, - }, - blockTimestamp: { - allowNull: false, - type: DataTypes.DATE, - }, - blockNumber: { - allowNull: false, - type: DataTypes.INTEGER, - }, + ...COMMON_EVENT_INIT_ATTRIBUTES, }), ); @@ -948,24 +813,7 @@ async function createTransferEventsTable( allowNull: false, type: DataTypes.STRING, }, - transactionHash: { - primaryKey: true, - allowNull: false, - type: DataTypes.STRING, - }, - logIndex: { - primaryKey: true, - allowNull: false, - type: DataTypes.INTEGER, - }, - blockNumber: { - allowNull: false, - type: DataTypes.INTEGER, - }, - blockTimestamp: { - allowNull: false, - type: DataTypes.DATE, - }, + ...COMMON_EVENT_INIT_ATTRIBUTES, }), ); } diff --git a/src/models/AccountMetadataEmittedEventModel.ts b/src/models/AccountMetadataEmittedEventModel.ts index 8ad023f..c67054e 100644 --- a/src/models/AccountMetadataEmittedEventModel.ts +++ b/src/models/AccountMetadataEmittedEventModel.ts @@ -46,7 +46,7 @@ export default class AccountMetadataEmittedEventModel schema: getSchema(), tableName: 'account_metadata_emitted_events', underscored: true, - timestamps: false, + timestamps: true, indexes: [ { fields: ['accountId'], diff --git a/src/models/GivenEventModel.ts b/src/models/GivenEventModel.ts index 684b7e0..2650521 100644 --- a/src/models/GivenEventModel.ts +++ b/src/models/GivenEventModel.ts @@ -51,7 +51,7 @@ export default class GivenEventModel schema: getSchema(), tableName: 'given_events', underscored: true, - timestamps: false, + timestamps: true, indexes: [ { fields: ['accountId'], diff --git a/src/models/OwnerUpdatedEventModel.ts b/src/models/OwnerUpdatedEventModel.ts index d1b2f5d..4ff4af1 100644 --- a/src/models/OwnerUpdatedEventModel.ts +++ b/src/models/OwnerUpdatedEventModel.ts @@ -40,7 +40,7 @@ export default class OwnerUpdatedEventModel { sequelize, underscored: true, - timestamps: false, + timestamps: true, schema: getSchema(), tableName: 'owner_updated_events', }, diff --git a/src/models/SplitEventModel.ts b/src/models/SplitEventModel.ts index aec5210..956bb54 100644 --- a/src/models/SplitEventModel.ts +++ b/src/models/SplitEventModel.ts @@ -52,7 +52,7 @@ export default class SplitEventModel schema: getSchema(), tableName: 'split_events', underscored: true, - timestamps: false, + timestamps: true, indexes: [ { fields: ['receiver'], diff --git a/src/models/SplitsSetEventModel.ts b/src/models/SplitsSetEventModel.ts index 07cfaf7..5809437 100644 --- a/src/models/SplitsSetEventModel.ts +++ b/src/models/SplitsSetEventModel.ts @@ -38,7 +38,7 @@ export default class SplitsSetEventModel }, { sequelize, - timestamps: false, + timestamps: true, underscored: true, schema: getSchema(), tableName: 'splits_set_events', diff --git a/src/models/SqueezedStreamsEventModel.ts b/src/models/SqueezedStreamsEventModel.ts index 18399e3..0fe58ca 100644 --- a/src/models/SqueezedStreamsEventModel.ts +++ b/src/models/SqueezedStreamsEventModel.ts @@ -67,7 +67,7 @@ export default class SqueezedStreamsEventModel schema: getSchema(), tableName: 'squeezed_streams_events', underscored: true, - timestamps: false, + timestamps: true, }, ); } diff --git a/src/models/StreamReceiverSeenEventModel.ts b/src/models/StreamReceiverSeenEventModel.ts index 83bca86..3f45b5a 100644 --- a/src/models/StreamReceiverSeenEventModel.ts +++ b/src/models/StreamReceiverSeenEventModel.ts @@ -46,7 +46,7 @@ export default class StreamReceiverSeenEventModel schema: getSchema(), tableName: 'stream_receiver_seen_events', underscored: true, - timestamps: false, + timestamps: true, indexes: [ { fields: ['accountId'], diff --git a/src/models/StreamsSetEventModel.ts b/src/models/StreamsSetEventModel.ts index cad352b..1e64e70 100644 --- a/src/models/StreamsSetEventModel.ts +++ b/src/models/StreamsSetEventModel.ts @@ -61,7 +61,7 @@ export default class StreamsSetEventModel schema: getSchema(), tableName: 'streams_set_events', underscored: true, - timestamps: false, + timestamps: true, indexes: [ { fields: ['receiversHash'], diff --git a/src/models/TransferEventModel.ts b/src/models/TransferEventModel.ts index 94934b4..3a3841e 100644 --- a/src/models/TransferEventModel.ts +++ b/src/models/TransferEventModel.ts @@ -46,7 +46,7 @@ export default class TransferEventModel schema: getSchema(), tableName: 'transfer_events', underscored: true, - timestamps: false, + timestamps: true, }, ); } From 08e68ec46ea2c00632c922f55f55ad52e4dd5ca0 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 5 May 2025 13:38:15 +0200 Subject: [PATCH 51/60] refactor: rename split receivers table --- src/db/migrations/20250414133746-initial_create.ts | 12 ++++++------ src/db/modelRegistration.ts | 4 ++-- .../receiversRepository.ts | 13 ++++++++----- src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts | 4 ++-- ...SplitReceiverModel.ts => SplitsReceiverModel.ts} | 12 ++++++------ src/models/index.ts | 2 +- 6 files changed, 25 insertions(+), 22 deletions(-) rename src/models/{SplitReceiverModel.ts => SplitsReceiverModel.ts} (88%) diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index 110c03f..919944b 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -825,7 +825,7 @@ async function createSplitReceiversTable( await queryInterface.createTable( { schema, - tableName: `split_receivers`, + tableName: `splits_receivers`, }, transformFieldNamesToSnakeCase({ id: { @@ -875,21 +875,21 @@ async function createSplitReceiversTable( await queryInterface.addIndex( { schema, - tableName: `split_receivers`, + tableName: `splits_receivers`, }, transformFieldArrayToSnakeCase(['receiverAccountId', 'senderAccountId']), { - name: 'idx_split_receivers_receiver_sender', + name: 'idx_splits_receivers_receiver_sender', }, ); await queryInterface.addIndex( { schema, - tableName: `split_receivers`, + tableName: `splits_receivers`, }, transformFieldArrayToSnakeCase(['senderAccountId', 'receiverAccountId']), { - name: 'idx_split_receivers_sender_receiver', + name: 'idx_splits_receivers_sender_receiver', }, ); } @@ -952,7 +952,7 @@ export async function down({ context: sequelize }: any): Promise { }); await queryInterface.dropTable({ schema, - tableName: `split_receivers`, + tableName: `splits_receivers`, }); await queryInterface.dropTable({ schema, diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index 73a6841..5dc3768 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -12,7 +12,7 @@ import { SqueezedStreamsEventModel, SubListModel, EcosystemMainAccountModel, - SplitReceiverModel, + SplitsReceiverModel, OwnerUpdatedEventModel, } from '../models'; import SplitsSetEventModel from '../models/SplitsSetEventModel'; @@ -36,7 +36,7 @@ export function registerModels(): void { registerModel(GivenEventModel); registerModel(SplitEventModel); registerModel(TransferEventModel); - registerModel(SplitReceiverModel); + registerModel(SplitsReceiverModel); registerModel(SplitsSetEventModel); registerModel(StreamsSetEventModel); registerModel(OwnerUpdatedEventModel); diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts index 20a12eb..8a08dd7 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/receiversRepository.ts @@ -2,13 +2,16 @@ import { type Transaction } from 'sequelize'; import type { AccountId } from '../../core/types'; import type ScopedLogger from '../../core/ScopedLogger'; import type { SplitReceiverShape } from '../../core/splitRules'; -import { SplitReceiverModel, StreamReceiverSeenEventModel } from '../../models'; +import { + SplitsReceiverModel, + StreamReceiverSeenEventModel, +} from '../../models'; export async function deleteExistingSplitReceivers( senderAccountId: AccountId, transaction: Transaction, ) { - await SplitReceiverModel.destroy({ + await SplitsReceiverModel.destroy({ where: { senderAccountId, }, @@ -25,14 +28,14 @@ export async function createSplitReceiver({ transaction: Transaction; splitReceiverShape: SplitReceiverShape; }) { - const splitReceiver = await SplitReceiverModel.create( + const splitReceiver = await SplitsReceiverModel.create( { ...splitReceiverShape }, { transaction }, ); scopedLogger.bufferCreation({ input: splitReceiver, - type: SplitReceiverModel, + type: SplitsReceiverModel, id: splitReceiver.id.toString(), }); } @@ -40,7 +43,7 @@ export async function createSplitReceiver({ export async function getCurrentSplitReceiversBySender( senderAccountId: AccountId, ): Promise { - const splitReceivers = await SplitReceiverModel.findAll({ + const splitReceivers = await SplitsReceiverModel.findAll({ where: { senderAccountId, }, diff --git a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts index 6af508c..d401628 100644 --- a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts @@ -17,7 +17,7 @@ import { DripListModel, EcosystemMainAccountModel, SubListModel, - SplitReceiverModel, + SplitsReceiverModel, } from '../../models'; import type SplitsSetEventModel from '../../models/SplitsSetEventModel'; import type ScopedLogger from '../../core/ScopedLogger'; @@ -169,7 +169,7 @@ async function hashDbSplits( accountId: AccountId, transaction: Transaction, ): Promise { - const rows = await SplitReceiverModel.findAll({ + const rows = await SplitsReceiverModel.findAll({ transaction, lock: transaction.LOCK.UPDATE, where: { senderAccountId: accountId }, diff --git a/src/models/SplitReceiverModel.ts b/src/models/SplitsReceiverModel.ts similarity index 88% rename from src/models/SplitReceiverModel.ts rename to src/models/SplitsReceiverModel.ts index 475182a..05fad36 100644 --- a/src/models/SplitReceiverModel.ts +++ b/src/models/SplitsReceiverModel.ts @@ -14,9 +14,9 @@ import { RELATIONSHIP_TYPES, } from '../core/splitRules'; -export default class SplitReceiverModel extends Model< - InferAttributes, - InferCreationAttributes +export default class SplitsReceiverModel extends Model< + InferAttributes, + InferCreationAttributes > { declare public id: CreationOptional; declare public receiverAccountId: AccountId; @@ -77,17 +77,17 @@ export default class SplitReceiverModel extends Model< { sequelize, schema: getSchema(), - tableName: 'split_receivers', + tableName: 'splits_receivers', underscored: true, timestamps: true, indexes: [ { fields: ['receiverAccountId', 'senderAccountId'], - name: 'idx_split_receivers_receiver_sender', + name: 'idx_splits_receivers_receiver_sender', }, { fields: ['senderAccountId', 'receiverAccountId'], - name: 'idx_split_receivers_sender_receiver', + name: 'idx_splits_receivers_sender_receiver', }, ], }, diff --git a/src/models/index.ts b/src/models/index.ts index e29d58a..d0a1513 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -3,7 +3,7 @@ export { default as ProjectModel } from './ProjectModel'; export { default as DripListModel } from './DripListModel'; export { default as GivenEventModel } from './GivenEventModel'; export { default as SplitEventModel } from './SplitEventModel'; -export { default as SplitReceiverModel } from './SplitReceiverModel'; +export { default as SplitsReceiverModel } from './SplitsReceiverModel'; export { default as TransferEventModel } from './TransferEventModel'; export { default as SplitsSetEventModel } from './SplitsSetEventModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; From c0c0fd1b71f6c894914fd666bde13407ed8709cb Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 13 May 2025 13:38:23 +0200 Subject: [PATCH 52/60] feat: integrate with RepoSubAccountDriver --- scripts/codegen-any-chain-types.ts | 10 +- src/abi/filecoin/RepoSubAccountDriver.json | 447 ++++++++++++++++++ src/abi/goerli/RepoSubAccountDriver.json | 447 ++++++++++++++++++ .../localtestnet/RepoSubAccountDriver.json | 447 ++++++++++++++++++ src/abi/mainnet/RepoSubAccountDriver.json | 447 ++++++++++++++++++ src/abi/metis/RepoSubAccountDriver.json | 447 ++++++++++++++++++ src/abi/optimism/RepoSubAccountDriver.json | 447 ++++++++++++++++++ .../RepoSubAccountDriver.json | 447 ++++++++++++++++++ .../polygon_amoy/RepoSubAccountDriver.json | 447 ++++++++++++++++++ src/abi/sepolia/RepoSubAccountDriver.json | 447 ++++++++++++++++++ src/config/chainConfigs/localtestnet.json | 3 + src/config/chainConfigs/optimism_sepolia.json | 3 + src/core/constants.ts | 1 + src/core/contractClients.ts | 9 +- src/core/splitRules.ts | 1 + src/core/types.ts | 6 +- .../20250414133746-initial_create.ts | 28 +- .../handleEcosystemMainAccountMetadata.ts | 20 +- .../handlers/handleSubListMetadata.ts | 12 +- .../SplitsSetEvent/setIsValidFlag.ts | 18 +- ...repoSubAccountDriverSplitReceiverSchema.ts | 10 + .../schemas/immutable-splits-driver/v1.ts | 8 +- src/metadata/schemas/nft-driver/v6.ts | 6 +- src/models/SplitsReceiverModel.ts | 19 + src/utils/accountIdUtils.ts | 76 +++ src/utils/metadataUtils.ts | 10 +- src/utils/projectUtils.ts | 4 +- .../handlers/handleProjectMetadata.test.ts | 0 28 files changed, 4232 insertions(+), 35 deletions(-) create mode 100644 src/abi/filecoin/RepoSubAccountDriver.json create mode 100644 src/abi/goerli/RepoSubAccountDriver.json create mode 100644 src/abi/localtestnet/RepoSubAccountDriver.json create mode 100644 src/abi/mainnet/RepoSubAccountDriver.json create mode 100644 src/abi/metis/RepoSubAccountDriver.json create mode 100644 src/abi/optimism/RepoSubAccountDriver.json create mode 100644 src/abi/optimism_sepolia/RepoSubAccountDriver.json create mode 100644 src/abi/polygon_amoy/RepoSubAccountDriver.json create mode 100644 src/abi/sepolia/RepoSubAccountDriver.json create mode 100644 src/metadata/schemas/common/repoSubAccountDriverSplitReceiverSchema.ts create mode 100644 tests/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.test.ts diff --git a/scripts/codegen-any-chain-types.ts b/scripts/codegen-any-chain-types.ts index 33ed3f4..08a43be 100644 --- a/scripts/codegen-any-chain-types.ts +++ b/scripts/codegen-any-chain-types.ts @@ -40,6 +40,7 @@ function generateTypeImports(chainNames: string[]) { import { Drips as ${chainName}Drips } from './${chainName}/'; import { NftDriver as ${chainName}NftDriver } from './${chainName}/'; import { RepoDriver as ${chainName}RepoDriver } from './${chainName}/'; +import { RepoSubAccountDriver as ${chainName}RepoSubAccountDriver } from './${chainName}/'; import { AddressDriver as ${chainName}AddressDriver } from './${chainName}/'; import { ImmutableSplitsDriver as ${chainName}ImmutableSplitsDriver } from './${chainName}/'; import { TypedContractEvent as ${chainName}TypedContractEvent } from './${chainName}/common'; @@ -54,12 +55,14 @@ function generateAnyChainTypes(chainNames: string[]) { export type AnyChainDrips = ${chainNames.map((name) => `${name}Drips`).join(' | ')}; export type AnyChainNftDriver = ${chainNames.map((name) => `${name}NftDriver`).join(' | ')}; export type AnyChainRepoDriver = ${chainNames.map((name) => `${name}RepoDriver`).join(' | ')}; +export type AnyChainRepoSubAccountDriver = ${chainNames.map((name) => `${name}RepoSubAccountDriver`).join(' | ')}; export type AnyChainAddressDriver = ${chainNames.map((name) => `${name}AddressDriver`).join(' | ')}; export type AnyChainImmutableSplitsDriver = ${chainNames.map((name) => `${name}ImmutableSplitsDriver`).join(' | ')}; export type AnyChainDripsFilters = ${chainNames.map((name) => `${name}Drips['filters']`).join(' & ')}; export type AnyChainNftDriverFilters = ${chainNames.map((name) => `${name}NftDriver['filters']`).join(' & ')}; export type AnyChainRepoDriverFilters = ${chainNames.map((name) => `${name}RepoDriver['filters']`).join(' & ')}; +export type AnyChainRepoSubAccountDriverFilters = ${chainNames.map((name) => `${name}RepoSubAccountDriver['filters']`).join(' & ')}; export type AnyChainAddressDriverFilters = ${chainNames.map((name) => `${name}AddressDriver['filters']`).join(' & ')}; export type AnyChainImmutableSplitsDriverFilters = ${chainNames.map((name) => `${name}ImmutableSplitsDriver['filters']`).join(' & ')}; @@ -73,7 +76,7 @@ function generateContractGetters() { return ` import type { Provider } from 'ethers'; -import { Drips__factory, NftDriver__factory, RepoDriver__factory, AddressDriver__factory, ImmutableSplitsDriver__factory } from './${process.env.NETWORK}'; +import { Drips__factory, NftDriver__factory, RepoDriver__factory, AddressDriver__factory, ImmutableSplitsDriver__factory, RepoSubAccountDriver__factory } from './${process.env.NETWORK}'; export const getDripsContract: (contractAddress: string, provider: Provider) => AnyChainDrips = (contractAddress, provider) => Drips__factory.connect( contractAddress, @@ -90,6 +93,11 @@ export const getRepoDriverContract: (contractAddress: string, provider: Provider provider ); +export const getRepoSubAccountDriverContract: (contractAddress: string, provider: Provider) => AnyChainRepoSubAccountDriver = (contractAddress, provider) => RepoSubAccountDriver__factory.connect( + contractAddress, + provider +); + export const getAddressDriverContract: (contractAddress: string, provider: Provider) => AnyChainAddressDriver = (contractAddress, provider) => AddressDriver__factory.connect( contractAddress, provider diff --git a/src/abi/filecoin/RepoSubAccountDriver.json b/src/abi/filecoin/RepoSubAccountDriver.json new file mode 100644 index 0000000..05e3034 --- /dev/null +++ b/src/abi/filecoin/RepoSubAccountDriver.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "resultAccountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/goerli/RepoSubAccountDriver.json b/src/abi/goerli/RepoSubAccountDriver.json new file mode 100644 index 0000000..05e3034 --- /dev/null +++ b/src/abi/goerli/RepoSubAccountDriver.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "resultAccountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/localtestnet/RepoSubAccountDriver.json b/src/abi/localtestnet/RepoSubAccountDriver.json new file mode 100644 index 0000000..05e3034 --- /dev/null +++ b/src/abi/localtestnet/RepoSubAccountDriver.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "resultAccountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/mainnet/RepoSubAccountDriver.json b/src/abi/mainnet/RepoSubAccountDriver.json new file mode 100644 index 0000000..05e3034 --- /dev/null +++ b/src/abi/mainnet/RepoSubAccountDriver.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "resultAccountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/metis/RepoSubAccountDriver.json b/src/abi/metis/RepoSubAccountDriver.json new file mode 100644 index 0000000..05e3034 --- /dev/null +++ b/src/abi/metis/RepoSubAccountDriver.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "resultAccountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/optimism/RepoSubAccountDriver.json b/src/abi/optimism/RepoSubAccountDriver.json new file mode 100644 index 0000000..05e3034 --- /dev/null +++ b/src/abi/optimism/RepoSubAccountDriver.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "resultAccountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/optimism_sepolia/RepoSubAccountDriver.json b/src/abi/optimism_sepolia/RepoSubAccountDriver.json new file mode 100644 index 0000000..05e3034 --- /dev/null +++ b/src/abi/optimism_sepolia/RepoSubAccountDriver.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "resultAccountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/polygon_amoy/RepoSubAccountDriver.json b/src/abi/polygon_amoy/RepoSubAccountDriver.json new file mode 100644 index 0000000..05e3034 --- /dev/null +++ b/src/abi/polygon_amoy/RepoSubAccountDriver.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "resultAccountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/sepolia/RepoSubAccountDriver.json b/src/abi/sepolia/RepoSubAccountDriver.json new file mode 100644 index 0000000..05e3034 --- /dev/null +++ b/src/abi/sepolia/RepoSubAccountDriver.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "resultAccountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/config/chainConfigs/localtestnet.json b/src/config/chainConfigs/localtestnet.json index beb8a16..12087ab 100644 --- a/src/config/chainConfigs/localtestnet.json +++ b/src/config/chainConfigs/localtestnet.json @@ -8,6 +8,9 @@ "repoDriver": { "address": "0x971e08fc533d2A5f228c7944E511611dA3B56B24" }, + "repoSubAccountDriver": { + "address": "0xB8743C2bB8DF7399273aa7EE4cE8d4109Bec327F" + }, "drips": { "address": "0x7CBbD3FdF9E5eb359E6D9B12848c5Faa81629944" }, diff --git a/src/config/chainConfigs/optimism_sepolia.json b/src/config/chainConfigs/optimism_sepolia.json index 341db77..9cc9523 100644 --- a/src/config/chainConfigs/optimism_sepolia.json +++ b/src/config/chainConfigs/optimism_sepolia.json @@ -14,6 +14,9 @@ "repoDriver": { "address": "0xa71bdf410D48d4AA9aE1517A69D7E1Ef0c179b2B" }, + "repoSubAccountDriver": { + "address": "0x5cEB4E59A1f91caC75017163B4D0663F155e9B77" + }, "addressDriver": { "address": "0x70E1E1437AeFe8024B6780C94490662b45C3B567" } diff --git a/src/core/constants.ts b/src/core/constants.ts index 3a2fca4..419eba2 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -16,6 +16,7 @@ export const DRIPS_CONTRACTS = [ 'drips', 'nftDriver', 'repoDriver', + 'repoSubAccountDriver', 'addressDriver', 'immutableSplitsDriver', ] as const; diff --git a/src/core/contractClients.ts b/src/core/contractClients.ts index b5f1cc8..1e0ff55 100644 --- a/src/core/contractClients.ts +++ b/src/core/contractClients.ts @@ -3,12 +3,14 @@ import { getAddressDriverContract, getNftDriverContract, getRepoDriverContract, + getRepoSubAccountDriverContract, } from '../../contracts/contract-types'; import loadChainConfig from '../config/loadChainConfig'; import getProvider from './getProvider'; const { contracts } = loadChainConfig(); -const { drips, addressDriver, nftDriver, repoDriver } = contracts; +const { drips, addressDriver, nftDriver, repoDriver, repoSubAccountDriver } = + contracts; const provider = getProvider(); @@ -28,3 +30,8 @@ export const repoDriverContract = getRepoDriverContract( repoDriver.address, provider, ); + +export const repoSubAccountDriverContract = getRepoSubAccountDriverContract( + repoSubAccountDriver.address, + provider, +); diff --git a/src/core/splitRules.ts b/src/core/splitRules.ts index 59db915..8ea7320 100644 --- a/src/core/splitRules.ts +++ b/src/core/splitRules.ts @@ -114,6 +114,7 @@ export type SplitReceiverShape = SplitRuleFromRaw< > & { weight: number; blockTimestamp: Date; + splitsToRepoDriverSubAccount?: boolean; }; export const ACCOUNT_TYPES = Array.from( diff --git a/src/core/types.ts b/src/core/types.ts index cd3d3eb..35b1f2e 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -16,12 +16,16 @@ export type ImmutableSplitsDriverId = string & { export type RepoDeadlineDriverId = string & { __brand: 'RepoDeadlineDriverId'; }; +export type RepoSubAccountDriverId = string & { + __brand: 'RepoSubAccountDriverId'; +}; export type AccountId = | AddressDriverId | NftDriverId | RepoDriverId | ImmutableSplitsDriverId - | RepoDeadlineDriverId; + | RepoDeadlineDriverId + | RepoSubAccountDriverId; export type Address = string & { __brand: 'Address' }; diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index 919944b..da5c39b 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -1,4 +1,4 @@ -import { DataTypes, literal } from 'sequelize'; +import { DataTypes, literal, Op } from 'sequelize'; import type { DataType, QueryInterface } from 'sequelize'; import getSchema from '../../utils/getSchema'; import type { DbSchema } from '../../core/types'; @@ -853,6 +853,10 @@ async function createSplitReceiversTable( allowNull: false, type: literal(`${schema}.relationship_type`).val as unknown as DataType, }, + splitsToRepoDriverSubAccount: { + type: DataTypes.BOOLEAN, + allowNull: true, + }, weight: { type: DataTypes.INTEGER, allowNull: false, @@ -892,6 +896,28 @@ async function createSplitReceiversTable( name: 'idx_splits_receivers_sender_receiver', }, ); + + await queryInterface.addConstraint(`${getSchema()}.splits_receivers`, { + type: 'check', + name: 'chk_splits_receivers_project_splits_to_repo_driver_sub_account', + fields: ['receiver_account_type', 'splits_to_repo_driver_sub_account'], + where: { + [Op.or]: [ + { + receiver_account_type: 'project', + splits_to_repo_driver_sub_account: { + [Op.not]: null, + }, + }, + { + receiver_account_type: { + [Op.ne]: 'project', + }, + splits_to_repo_driver_sub_account: null, + }, + ], + }, + }); } export async function down({ context: sequelize }: any): Promise { diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index aa3b596..9b2ca10 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -11,12 +11,11 @@ import type { } from '../../../core/types'; import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; import verifySplitsReceivers from '../verifySplitsReceivers'; -import type { repoDriverSplitReceiverSchema } from '../../../metadata/schemas/repo-driver/v2'; import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immutable-splits-driver/v1'; import { verifyProjectSources } from '../../../utils/projectUtils'; import { assertIsImmutableSplitsDriverId, - assertIsRepoDriverId, + calcParentRepoDriverId, convertToNftDriverId, } from '../../../utils/accountIdUtils'; import { EcosystemMainAccountModel, ProjectModel } from '../../../models'; @@ -34,6 +33,7 @@ import { decodeVersion, makeVersion, } from '../../../utils/lastProcessedVersion'; +import type { repoSubAccountDriverSplitReceiverSchema } from '../../../metadata/schemas/common/repoSubAccountDriverSplitReceiverSchema'; type Params = { ipfsHash: IpfsHash; @@ -80,7 +80,7 @@ export default async function handleEcosystemMainAccountMetadata({ } const { areProjectsValid, message } = await verifyProjectSources( - metadata.recipients.filter((r) => r.type === 'repoDriver'), + metadata.recipients.filter((r) => r.type === 'repoSubAccountDriver'), ); if (!areProjectsValid) { @@ -207,14 +207,15 @@ async function createNewSplitReceivers({ transaction: Transaction; emitterAccountId: NftDriverId; splitReceivers: ( - | z.infer + | z.infer | z.infer )[]; }) { const receiverPromises = splitReceivers.map(async (receiver) => { switch (receiver.type) { - case 'repoDriver': - assertIsRepoDriverId(receiver.accountId); + case 'repoSubAccountDriver': + // eslint-disable-next-line no-case-declarations + const repoDriverId = await calcParentRepoDriverId(receiver.accountId); await ProjectModel.findOrCreate({ transaction, @@ -223,7 +224,7 @@ async function createNewSplitReceivers({ accountId: receiver.accountId, }, defaults: { - accountId: receiver.accountId, + accountId: repoDriverId, verificationStatus: 'unclaimed', isVisible: true, // Visible by default. Account metadata will set the final visibility. isValid: true, // There are no receivers yet. Consider the project valid. @@ -240,11 +241,12 @@ async function createNewSplitReceivers({ splitReceiverShape: { senderAccountId: emitterAccountId, senderAccountType: 'ecosystem_main_account', - receiverAccountId: receiver.accountId, + receiverAccountId: repoDriverId, receiverAccountType: 'project', relationshipType: 'ecosystem_receiver', weight: receiver.weight, blockTimestamp, + splitsToRepoDriverSubAccount: true, }, }); @@ -281,7 +283,7 @@ function validateMetadata( { type: 'ecosystem'; recipients: ( - | z.infer + | z.infer | z.infer )[]; } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 4ec8c18..69c28bd 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -12,10 +12,7 @@ import { import { getImmutableSpitsDriverMetadata } from '../../../utils/metadataUtils'; import unreachableError from '../../../utils/unreachableError'; import verifySplitsReceivers from '../verifySplitsReceivers'; -import type { - addressDriverSplitReceiverSchema, - repoDriverSplitReceiverSchema, -} from '../../../metadata/schemas/repo-driver/v2'; +import type { addressDriverSplitReceiverSchema } from '../../../metadata/schemas/repo-driver/v2'; import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immutable-splits-driver/v1'; import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; import RecoverableError from '../../../utils/recoverableError'; @@ -35,6 +32,7 @@ import { convertToImmutableSplitsDriverId, } from '../../../utils/accountIdUtils'; import { makeVersion } from '../../../utils/lastProcessedVersion'; +import type { repoSubAccountDriverSplitReceiverSchema } from '../../../metadata/schemas/common/repoSubAccountDriverSplitReceiverSchema'; type Params = { logIndex: number; @@ -82,7 +80,7 @@ export default async function handleSubListMetadata({ } const { areProjectsValid, message } = await verifyProjectSources( - metadata.recipients.filter((r) => r.type === 'repoDriver'), + metadata.recipients.filter((r) => r.type === 'repoSubAccountDriver'), ); if (!areProjectsValid) { @@ -186,7 +184,7 @@ async function createNewSplitReceivers({ transaction: Transaction; emitterAccountId: ImmutableSplitsDriverId; receivers: ( - | z.infer + | z.infer | z.infer | z.infer | z.infer @@ -194,7 +192,7 @@ async function createNewSplitReceivers({ }) { const receiverPromises = receivers.map(async (receiver) => { switch (receiver.type) { - case 'repoDriver': + case 'repoSubAccountDriver': assertIsRepoDriverId(receiver.accountId); await ProjectModel.findOrCreate({ diff --git a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts index d401628..a216b0f 100644 --- a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts @@ -1,6 +1,7 @@ import type { Transaction } from 'sequelize'; import type { AccountId } from '../../core/types'; import { + calcSubRepoDriverId, isImmutableSplitsDriverId, isNftDriverId, isRepoDriverId, @@ -175,10 +176,19 @@ async function hashDbSplits( where: { senderAccountId: accountId }, }); - const receivers = rows.map((r) => ({ - accountId: r.receiverAccountId, - weight: r.weight, - })); + const receiverPromises = rows.map(async (s) => { + let receiverId = s.receiverAccountId; + if (s.splitsToRepoDriverSubAccount) { + receiverId = await calcSubRepoDriverId(s.receiverAccountId); + } + + return { + accountId: receiverId, + weight: s.weight, + }; + }); + + const receivers = await Promise.all(receiverPromises); return dripsContract.hashSplits(formatSplitReceivers(receivers)); } diff --git a/src/metadata/schemas/common/repoSubAccountDriverSplitReceiverSchema.ts b/src/metadata/schemas/common/repoSubAccountDriverSplitReceiverSchema.ts new file mode 100644 index 0000000..7deb5f5 --- /dev/null +++ b/src/metadata/schemas/common/repoSubAccountDriverSplitReceiverSchema.ts @@ -0,0 +1,10 @@ +import z from 'zod'; +import { sourceSchema } from './sources'; + +// Here, until there is a need to create specific `RepoSubAccountDriver` metadata. +export const repoSubAccountDriverSplitReceiverSchema = z.object({ + type: z.literal('repoSubAccountDriver'), + weight: z.number(), + accountId: z.string(), + source: sourceSchema, +}); diff --git a/src/metadata/schemas/immutable-splits-driver/v1.ts b/src/metadata/schemas/immutable-splits-driver/v1.ts index 18c5ee8..171668f 100644 --- a/src/metadata/schemas/immutable-splits-driver/v1.ts +++ b/src/metadata/schemas/immutable-splits-driver/v1.ts @@ -1,9 +1,7 @@ import z from 'zod'; -import { - addressDriverSplitReceiverSchema, - repoDriverSplitReceiverSchema, -} from '../repo-driver/v2'; +import { addressDriverSplitReceiverSchema } from '../repo-driver/v2'; import { dripListSplitReceiverSchema } from '../nft-driver/v2'; +import { repoSubAccountDriverSplitReceiverSchema } from '../common/repoSubAccountDriverSplitReceiverSchema'; export const subListSplitReceiverSchema = z.object({ type: z.literal('subList'), @@ -18,7 +16,7 @@ export const subListMetadataSchemaV1 = z.object({ z.union([ addressDriverSplitReceiverSchema, dripListSplitReceiverSchema, - repoDriverSplitReceiverSchema, + repoSubAccountDriverSplitReceiverSchema, subListSplitReceiverSchema, ]), ), diff --git a/src/metadata/schemas/nft-driver/v6.ts b/src/metadata/schemas/nft-driver/v6.ts index e8a875e..7df2903 100644 --- a/src/metadata/schemas/nft-driver/v6.ts +++ b/src/metadata/schemas/nft-driver/v6.ts @@ -6,6 +6,7 @@ import { } from '../repo-driver/v2'; import { subListSplitReceiverSchema } from '../immutable-splits-driver/v1'; import { dripListSplitReceiverSchema } from './v2'; +import { repoSubAccountDriverSplitReceiverSchema } from '../common/repoSubAccountDriverSplitReceiverSchema'; const base = nftDriverAccountMetadataSchemaV5 .omit({ @@ -20,7 +21,10 @@ const base = nftDriverAccountMetadataSchemaV5 const ecosystemVariant = base.extend({ type: z.literal('ecosystem'), recipients: z.array( - z.union([repoDriverSplitReceiverSchema, subListSplitReceiverSchema]), + z.union([ + repoSubAccountDriverSplitReceiverSchema, + subListSplitReceiverSchema, + ]), ), }); diff --git a/src/models/SplitsReceiverModel.ts b/src/models/SplitsReceiverModel.ts index 05fad36..8d9965b 100644 --- a/src/models/SplitsReceiverModel.ts +++ b/src/models/SplitsReceiverModel.ts @@ -26,6 +26,7 @@ export default class SplitsReceiverModel extends Model< declare public relationshipType: RelationshipType; declare public weight: number; declare public blockTimestamp: Date; + declare public splitsToRepoDriverSubAccount: boolean | undefined; declare public createdAt: CreationOptional; declare public updatedAt: CreationOptional; @@ -57,6 +58,24 @@ export default class SplitsReceiverModel extends Model< allowNull: false, type: DataTypes.ENUM(...RELATIONSHIP_TYPES), }, + splitsToRepoDriverSubAccount: { + type: DataTypes.BOOLEAN, + allowNull: true, + validate: { + splitsToRepoDriverSubAccountValidation(value: boolean | null) { + if (this.receiverAccountType === 'project' && value === null) { + throw new Error( + `'splitsToRepoDriverSubAccount' must have a value when 'receiverAccountType' is 'project'.`, + ); + } + if (this.receiverAccountType !== 'project' && value !== null) { + throw new Error( + `'splitsToRepoDriverSubAccount' must be 'null' when 'receiverAccountType' is not 'project'.`, + ); + } + }, + }, + }, weight: { type: DataTypes.INTEGER, allowNull: false, diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index 139967e..74ac35c 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -1,4 +1,5 @@ /* eslint-disable no-bitwise */ +import { repoSubAccountDriverContract } from '../core/contractClients'; import type { AccountId, AddressDriverId, @@ -6,7 +7,9 @@ import type { ImmutableSplitsDriverId, NftDriverId, RepoDriverId, + RepoSubAccountDriverId, } from '../core/types'; +import unreachableError from './unreachableError'; export function getContractNameFromAccountId(id: string): DripsContract { if (Number.isNaN(Number(id))) { @@ -34,6 +37,8 @@ export function getContractNameFromAccountId(id: string): DripsContract { return 'immutableSplitsDriver'; case 3n: return 'repoDriver'; + case 4n: + return 'repoSubAccountDriver'; default: throw new Error(`Unknown driver for ID ${id}.`); } @@ -175,6 +180,77 @@ export function assertIsImmutableSplitsDriverId( } } +// RepoSubAccountDriver +export function isRepoSubAccountDriverId( + id: string | bigint, +): id is RepoSubAccountDriverId { + const idString = typeof id === 'bigint' ? id.toString() : id; + const isNaN = Number.isNaN(Number(idString)); + const isAccountIdOfRepoSubAccountDriver = + getContractNameFromAccountId(idString) === 'repoSubAccountDriver'; + + if (isNaN || !isAccountIdOfRepoSubAccountDriver) { + return false; + } + + return true; +} + +export function assertIsRepoSubAccountDriverId( + id: string, +): asserts id is RepoSubAccountDriverId { + if (!isRepoSubAccountDriverId(id)) { + throw new Error( + `Failed to assert: '${id}' is not a valid RepoSubAccountDriverId ID.`, + ); + } +} + +export async function transformRepoDriverId( + id: string, + direction: 'toParent' | 'toSub', +): Promise { + if (direction === 'toParent') { + assertIsRepoSubAccountDriverId(id); + } else { + assertIsRepoDriverId(id); + } + + const transformedId = ( + await repoSubAccountDriverContract.calcAccountId(id) + ).toString(); + + if (direction === 'toParent') { + assertIsRepoDriverId(transformedId); + } else { + assertIsRepoSubAccountDriverId(transformedId); + } + + const recalculatedId = ( + await repoSubAccountDriverContract.calcAccountId(transformedId) + ).toString(); + + if (recalculatedId !== id) { + unreachableError( + `Failed to transform RepoDriver ID: '${id}' does not match the recalculated ID '${recalculatedId}'.`, + ); + } + + return transformedId as RepoDriverId; +} + +export async function calcParentRepoDriverId( + subAccountId: string, +): Promise { + return transformRepoDriverId(subAccountId, 'toParent'); +} + +export async function calcSubRepoDriverId( + parentId: string, +): Promise { + return transformRepoDriverId(parentId, 'toSub'); +} + // Account ID export function convertToAccountId(id: bigint | string): AccountId { const accountIdAsString = typeof id === 'bigint' ? id.toString() : id; diff --git a/src/utils/metadataUtils.ts b/src/utils/metadataUtils.ts index a311ff2..25ce1d5 100644 --- a/src/utils/metadataUtils.ts +++ b/src/utils/metadataUtils.ts @@ -36,10 +36,14 @@ async function getIpfsFile(hash: IpfsHash): Promise { export async function getNftDriverMetadata( ipfsHash: IpfsHash, ): Promise> { - const ipfsFile = await (await getIpfsFile(ipfsHash)).json(); - const metadata = nftDriverAccountMetadataParser.parseAny(ipfsFile); + try { + const ipfsFile = await (await getIpfsFile(ipfsHash)).json(); + const metadata = nftDriverAccountMetadataParser.parseAny(ipfsFile); - return metadata; + return metadata; + } catch (error) { + throw new Error(`Failed to fetch NFT driver metadata from IPFS: ${error}`); + } } export async function getImmutableSpitsDriverMetadata( diff --git a/src/utils/projectUtils.ts b/src/utils/projectUtils.ts index e796b9c..9e0e7e7 100644 --- a/src/utils/projectUtils.ts +++ b/src/utils/projectUtils.ts @@ -5,7 +5,7 @@ import type ProjectModel from '../models/ProjectModel'; import type { Forge, ProjectVerificationStatus } from '../models/ProjectModel'; import { repoDriverContract } from '../core/contractClients'; import type { sourceSchema } from '../metadata/schemas/common/sources'; -import { assertIsRepoDriverId } from './accountIdUtils'; +import { assertIsRepoSubAccountDriverId } from './accountIdUtils'; export function convertForgeToNumber(forge: Forge) { switch (forge) { @@ -58,7 +58,7 @@ export async function verifyProjectSources( accountId, source: { forge, ownerName, repoName }, } of projects) { - assertIsRepoDriverId(accountId); + assertIsRepoSubAccountDriverId(accountId); const calculatedAccountId = await repoDriverContract.calcAccountId( convertForgeToNumber(forge), diff --git a/tests/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.test.ts b/tests/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.test.ts new file mode 100644 index 0000000..e69de29 From 9943fa351e64d7ba3dfd00f75cbe3d4604b7169d Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 13 May 2025 14:03:17 +0200 Subject: [PATCH 53/60] fix: missing RepoSubAccountDriver ABI --- .../RepoSubAccountDriver.json | 447 ++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 src/abi/zksync_era_sepolia/RepoSubAccountDriver.json diff --git a/src/abi/zksync_era_sepolia/RepoSubAccountDriver.json b/src/abi/zksync_era_sepolia/RepoSubAccountDriver.json new file mode 100644 index 0000000..05e3034 --- /dev/null +++ b/src/abi/zksync_era_sepolia/RepoSubAccountDriver.json @@ -0,0 +1,447 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "address", "name": "forwarder", "type": "address" }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "calcAccountId", + "outputs": [ + { + "internalType": "uint256", + "name": "resultAccountId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "collect", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes32", "name": "key", "type": "bytes32" }, + { "internalType": "bytes", "name": "value", "type": "bytes" } + ], + "internalType": "struct AccountMetadata[]", + "name": "accountMetadata", + "type": "tuple[]" + } + ], + "name": "emitAccountMetadata", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint256", "name": "receiver", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "name": "give", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "forwarder", "type": "address" } + ], + "name": "isTrustedForwarder", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "name": "ownerOf", + "outputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "uint32", "name": "weight", "type": "uint32" } + ], + "internalType": "struct SplitsReceiver[]", + "name": "receivers", + "type": "tuple[]" + } + ], + "name": "setSplits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "currReceivers", + "type": "tuple[]" + }, + { "internalType": "int128", "name": "balanceDelta", "type": "int128" }, + { + "components": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" }, + { + "internalType": "StreamConfig", + "name": "config", + "type": "uint256" + } + ], + "internalType": "struct StreamReceiver[]", + "name": "newReceivers", + "type": "tuple[]" + }, + { "internalType": "uint32", "name": "maxEndHint1", "type": "uint32" }, + { "internalType": "uint32", "name": "maxEndHint2", "type": "uint32" }, + { "internalType": "address", "name": "transferTo", "type": "address" } + ], + "name": "setStreams", + "outputs": [ + { "internalType": "int128", "name": "realBalanceDelta", "type": "int128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] From 4fc8a888abc44099c9e0e064b14d223bb784d369 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 13 May 2025 18:00:11 +0200 Subject: [PATCH 54/60] fix: wrong account id in FindOrCreate --- .../handlers/handleEcosystemMainAccountMetadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index 9b2ca10..f2dbf0b 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -221,7 +221,7 @@ async function createNewSplitReceivers({ transaction, lock: transaction.LOCK.UPDATE, where: { - accountId: receiver.accountId, + accountId: repoDriverId, }, defaults: { accountId: repoDriverId, From c42585e3fd004e102cb0a6d0537a551684a4f5bd Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 13 May 2025 18:28:08 +0200 Subject: [PATCH 55/60] fix: wrong logic when verifying project sources --- src/utils/projectUtils.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/utils/projectUtils.ts b/src/utils/projectUtils.ts index 9e0e7e7..48c9b72 100644 --- a/src/utils/projectUtils.ts +++ b/src/utils/projectUtils.ts @@ -5,7 +5,10 @@ import type ProjectModel from '../models/ProjectModel'; import type { Forge, ProjectVerificationStatus } from '../models/ProjectModel'; import { repoDriverContract } from '../core/contractClients'; import type { sourceSchema } from '../metadata/schemas/common/sources'; -import { assertIsRepoSubAccountDriverId } from './accountIdUtils'; +import { + assertIsRepoSubAccountDriverId, + calcParentRepoDriverId, +} from './accountIdUtils'; export function convertForgeToNumber(forge: Forge) { switch (forge) { @@ -60,14 +63,16 @@ export async function verifyProjectSources( } of projects) { assertIsRepoSubAccountDriverId(accountId); - const calculatedAccountId = await repoDriverContract.calcAccountId( + const parentAccountId = await calcParentRepoDriverId(accountId); + + const calculatedParentAccountId = await repoDriverContract.calcAccountId( convertForgeToNumber(forge), hexlify(toUtf8Bytes(`${ownerName}/${repoName}`)), ); - if (calculatedAccountId.toString() !== accountId) { + if (calculatedParentAccountId.toString() !== parentAccountId) { errors.push( - `Mismatch for '${ownerName}/${repoName}' on '${forge}': expected '${accountId}', got '${calculatedAccountId}'.`, + `Mismatch for '${ownerName}/${repoName}' on '${forge}': expected '${parentAccountId}', got '${calculatedParentAccountId}'.`, ); } } From 4d00bc89ad7be5d6dff7e1dd722c395120a78dd6 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 13 May 2025 20:45:15 +0200 Subject: [PATCH 56/60] fix: wrong project verification logic --- src/utils/projectUtils.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/utils/projectUtils.ts b/src/utils/projectUtils.ts index 48c9b72..b2516e4 100644 --- a/src/utils/projectUtils.ts +++ b/src/utils/projectUtils.ts @@ -6,8 +6,9 @@ import type { Forge, ProjectVerificationStatus } from '../models/ProjectModel'; import { repoDriverContract } from '../core/contractClients'; import type { sourceSchema } from '../metadata/schemas/common/sources'; import { - assertIsRepoSubAccountDriverId, calcParentRepoDriverId, + isRepoDriverId, + isRepoSubAccountDriverId, } from './accountIdUtils'; export function convertForgeToNumber(forge: Forge) { @@ -56,34 +57,43 @@ export async function verifyProjectSources( message?: string; }> { const errors: string[] = []; - for (const { accountId, source: { forge, ownerName, repoName }, } of projects) { - assertIsRepoSubAccountDriverId(accountId); - - const parentAccountId = await calcParentRepoDriverId(accountId); + const isSubAccount = isRepoSubAccountDriverId(accountId.toString()); + const isParentAccount = isRepoDriverId(accountId.toString()); + if (!isSubAccount && !isParentAccount) { + unreachableError( + `Invalid account ID: '${accountId}' is not a valid RepoDriver or RepoSubAccount ID.`, + ); + } const calculatedParentAccountId = await repoDriverContract.calcAccountId( convertForgeToNumber(forge), hexlify(toUtf8Bytes(`${ownerName}/${repoName}`)), ); - if (calculatedParentAccountId.toString() !== parentAccountId) { + if (isSubAccount) { + const parentId = await calcParentRepoDriverId(accountId); + + if (parentId !== calculatedParentAccountId.toString()) { + errors.push( + `Mismatch for '${ownerName}/${repoName}' on '${forge}': for sub account '${accountId}', expected parent '${calculatedParentAccountId}', got '${parentId}'.`, + ); + } + } else if (accountId !== calculatedParentAccountId.toString()) { errors.push( - `Mismatch for '${ownerName}/${repoName}' on '${forge}': expected '${parentAccountId}', got '${calculatedParentAccountId}'.`, + `Mismatch for '${ownerName}/${repoName}' on '${forge}': expected parent account '${calculatedParentAccountId}', got '${accountId}'.`, ); } } - if (errors.length > 0) { return { areProjectsValid: false, message: `Failed to verify project sources:\n${errors.join('\n')}`, }; } - return { areProjectsValid: true, }; From 476f68c6daff0466763bc4b80a2d3664b3a078ba Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 14 May 2025 11:29:31 +0200 Subject: [PATCH 57/60] feat: index ecosystem avatar and color --- src/db/migrations/20250414133746-initial_create.ts | 8 ++++++++ .../handlers/handleEcosystemMainAccountMetadata.ts | 2 ++ src/metadata/schemas/nft-driver/v6.ts | 3 +++ src/metadata/schemas/repo-driver/v4.ts | 2 +- src/models/EcosystemMainAccountModel.ts | 10 ++++++++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index da5c39b..db63fd1 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -616,6 +616,14 @@ async function createEcosystemMainAccountsTable( allowNull: false, type: DataTypes.BIGINT, }, + avatar: { + allowNull: true, + type: DataTypes.STRING, + }, + color: { + allowNull: true, + type: DataTypes.STRING, + }, createdAt: { allowNull: false, type: DataTypes.DATE, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index f2dbf0b..39566c7 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -148,6 +148,8 @@ async function upsertEcosystemMainAccount({ ? metadata.isVisible : true, lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), + color: 'color' in metadata ? metadata.color : null, + avatar: 'avatar' in metadata ? metadata.avatar.emoji : null, }; const [ecosystemMainAccount, isCreation] = diff --git a/src/metadata/schemas/nft-driver/v6.ts b/src/metadata/schemas/nft-driver/v6.ts index 7df2903..e534e0d 100644 --- a/src/metadata/schemas/nft-driver/v6.ts +++ b/src/metadata/schemas/nft-driver/v6.ts @@ -7,6 +7,7 @@ import { import { subListSplitReceiverSchema } from '../immutable-splits-driver/v1'; import { dripListSplitReceiverSchema } from './v2'; import { repoSubAccountDriverSplitReceiverSchema } from '../common/repoSubAccountDriverSplitReceiverSchema'; +import { emojiAvatarSchema } from '../repo-driver/v4'; const base = nftDriverAccountMetadataSchemaV5 .omit({ @@ -16,6 +17,8 @@ const base = nftDriverAccountMetadataSchemaV5 .extend({ isDripList: z.undefined().optional(), projects: z.undefined().optional(), + color: z.string(), + avatar: emojiAvatarSchema, }); const ecosystemVariant = base.extend({ diff --git a/src/metadata/schemas/repo-driver/v4.ts b/src/metadata/schemas/repo-driver/v4.ts index b8b2f68..3a2eb53 100644 --- a/src/metadata/schemas/repo-driver/v4.ts +++ b/src/metadata/schemas/repo-driver/v4.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { repoDriverAccountMetadataSchemaV3 } from './v3'; -const emojiAvatarSchema = z.object({ +export const emojiAvatarSchema = z.object({ type: z.literal('emoji'), emoji: z.string(), }); diff --git a/src/models/EcosystemMainAccountModel.ts b/src/models/EcosystemMainAccountModel.ts index b523dd0..99bc04a 100644 --- a/src/models/EcosystemMainAccountModel.ts +++ b/src/models/EcosystemMainAccountModel.ts @@ -23,6 +23,8 @@ export default class EcosystemMainAccountModel extends Model< declare public isVisible: boolean; declare public lastProcessedIpfsHash: string; declare public lastProcessedVersion: string; + declare public avatar: string | null; + declare public color: string | null; declare public createdAt: CreationOptional; declare public updatedAt: CreationOptional; @@ -73,6 +75,14 @@ export default class EcosystemMainAccountModel extends Model< allowNull: false, type: DataTypes.BIGINT, }, + avatar: { + allowNull: true, + type: DataTypes.STRING, + }, + color: { + allowNull: true, + type: DataTypes.STRING, + }, createdAt: { allowNull: false, type: DataTypes.DATE, From 2b5effbc803949c7765809bab1ba027e0e26262a Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 14 May 2025 11:46:05 +0200 Subject: [PATCH 58/60] refactor: make color and avatar required in ecosystems --- src/db/migrations/20250414133746-initial_create.ts | 4 ++-- .../handlers/handleEcosystemMainAccountMetadata.ts | 10 ++++++++-- src/metadata/schemas/nft-driver/v6.ts | 4 ++-- src/models/EcosystemMainAccountModel.ts | 8 ++++---- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/db/migrations/20250414133746-initial_create.ts b/src/db/migrations/20250414133746-initial_create.ts index db63fd1..6edc89e 100644 --- a/src/db/migrations/20250414133746-initial_create.ts +++ b/src/db/migrations/20250414133746-initial_create.ts @@ -617,11 +617,11 @@ async function createEcosystemMainAccountsTable( type: DataTypes.BIGINT, }, avatar: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, color: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, createdAt: { diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index 39566c7..57b0188 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -148,8 +148,14 @@ async function upsertEcosystemMainAccount({ ? metadata.isVisible : true, lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), - color: 'color' in metadata ? metadata.color : null, - avatar: 'avatar' in metadata ? metadata.avatar.emoji : null, + color: + 'color' in metadata + ? metadata.color + : unreachableError('Missing color in Ecosystem Main Account metadata'), + avatar: + 'avatar' in metadata + ? metadata.avatar.emoji + : unreachableError('Missing avatar in Ecosystem Main Account metadata'), }; const [ecosystemMainAccount, isCreation] = diff --git a/src/metadata/schemas/nft-driver/v6.ts b/src/metadata/schemas/nft-driver/v6.ts index e534e0d..2d6a3ed 100644 --- a/src/metadata/schemas/nft-driver/v6.ts +++ b/src/metadata/schemas/nft-driver/v6.ts @@ -17,8 +17,6 @@ const base = nftDriverAccountMetadataSchemaV5 .extend({ isDripList: z.undefined().optional(), projects: z.undefined().optional(), - color: z.string(), - avatar: emojiAvatarSchema, }); const ecosystemVariant = base.extend({ @@ -29,6 +27,8 @@ const ecosystemVariant = base.extend({ subListSplitReceiverSchema, ]), ), + color: z.string(), + avatar: emojiAvatarSchema, }); const dripListVariant = base.extend({ diff --git a/src/models/EcosystemMainAccountModel.ts b/src/models/EcosystemMainAccountModel.ts index 99bc04a..c1d3e71 100644 --- a/src/models/EcosystemMainAccountModel.ts +++ b/src/models/EcosystemMainAccountModel.ts @@ -23,8 +23,8 @@ export default class EcosystemMainAccountModel extends Model< declare public isVisible: boolean; declare public lastProcessedIpfsHash: string; declare public lastProcessedVersion: string; - declare public avatar: string | null; - declare public color: string | null; + declare public avatar: string; + declare public color: string; declare public createdAt: CreationOptional; declare public updatedAt: CreationOptional; @@ -76,11 +76,11 @@ export default class EcosystemMainAccountModel extends Model< type: DataTypes.BIGINT, }, avatar: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, color: { - allowNull: true, + allowNull: false, type: DataTypes.STRING, }, createdAt: { From 1d71617401f304ea051163a5d070d49cce839420 Mon Sep 17 00:00:00 2001 From: Georgios Jason Efstathiou Date: Thu, 15 May 2025 11:42:51 +0200 Subject: [PATCH 59/60] fix? --- src/config/chainConfigs/zksync_era_sepolia.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config/chainConfigs/zksync_era_sepolia.json b/src/config/chainConfigs/zksync_era_sepolia.json index fa0bd6e..93a1520 100644 --- a/src/config/chainConfigs/zksync_era_sepolia.json +++ b/src/config/chainConfigs/zksync_era_sepolia.json @@ -8,6 +8,9 @@ "repoDriver": { "address": "0x8bDC23877A23Ce59fEF1712A1486810d9A6E2B94" }, + "repoSubAccountDriver": { + "address": "0x0000000000000000000000000000000000000000" + }, "addressDriver": { "address": "0x0557b6BA791A24df0Fa6167E1Dc304F403ee777A" }, From c6c51560586767ce3386fd2ab2c3d68849f54fc7 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 15 May 2025 12:40:53 +0200 Subject: [PATCH 60/60] fix: wrong `splitsToRepoDriverSubAccount` validator logic --- src/models/SplitsReceiverModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/SplitsReceiverModel.ts b/src/models/SplitsReceiverModel.ts index 8d9965b..2940c32 100644 --- a/src/models/SplitsReceiverModel.ts +++ b/src/models/SplitsReceiverModel.ts @@ -63,12 +63,12 @@ export default class SplitsReceiverModel extends Model< allowNull: true, validate: { splitsToRepoDriverSubAccountValidation(value: boolean | null) { - if (this.receiverAccountType === 'project' && value === null) { + if (this.receiverAccountType === 'project' && !value) { throw new Error( `'splitsToRepoDriverSubAccount' must have a value when 'receiverAccountType' is 'project'.`, ); } - if (this.receiverAccountType !== 'project' && value !== null) { + if (this.receiverAccountType !== 'project' && value) { throw new Error( `'splitsToRepoDriverSubAccount' must be 'null' when 'receiverAccountType' is not 'project'.`, );