diff --git a/.changeset/crisp-buckets-agree.md b/.changeset/crisp-buckets-agree.md new file mode 100644 index 0000000000..3915fea605 --- /dev/null +++ b/.changeset/crisp-buckets-agree.md @@ -0,0 +1,5 @@ +--- +"@human-protocol/sdk": minor +--- + +Added typed subgraph errors (SubgraphRequestError, SubgraphBadIndexerError) and wrapped subgraph request failures with these classes diff --git a/.changeset/shaggy-months-post.md b/.changeset/shaggy-months-post.md new file mode 100644 index 0000000000..183d64c44e --- /dev/null +++ b/.changeset/shaggy-months-post.md @@ -0,0 +1,5 @@ +--- +"@human-protocol/sdk": patch +--- + +Split combined domain files into module folders with explicit files per responsibility. diff --git a/.github/workflows/cd-packages.yaml b/.github/workflows/cd-packages.yaml index 8aea8c284a..583e54b4f8 100644 --- a/.github/workflows/cd-packages.yaml +++ b/.github/workflows/cd-packages.yaml @@ -34,7 +34,7 @@ jobs: steps: - uses: actions/checkout@v6 with: - token: ${{ secrets.GH_GITBOOK_TOKEN }} + token: ${{ secrets.RELEASE_GH_TOKEN }} - name: Setup git identity run: | git config --global user.name "github-actions[bot]" diff --git a/.github/workflows/cd-python-sdk.yaml b/.github/workflows/cd-python-sdk.yaml index 30b69eb072..e323231a1c 100644 --- a/.github/workflows/cd-python-sdk.yaml +++ b/.github/workflows/cd-python-sdk.yaml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@v6 with: - token: ${{ secrets.GH_GITBOOK_TOKEN }} + token: ${{ secrets.RELEASE_GH_TOKEN }} - name: Setup git identity run: | git config --global user.name "github-actions[bot]" diff --git a/packages/apps/dashboard/client/eslint.config.mjs b/packages/apps/dashboard/client/eslint.config.mjs index 6605a1e56a..d41d2254a2 100644 --- a/packages/apps/dashboard/client/eslint.config.mjs +++ b/packages/apps/dashboard/client/eslint.config.mjs @@ -46,6 +46,8 @@ export default tseslint.config( }, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', 'react/prop-types': 'off', 'react/display-name': 'off', 'react/react-in-jsx-scope': 'off', diff --git a/packages/apps/dashboard/client/package.json b/packages/apps/dashboard/client/package.json index 0e0346a378..43a99df21d 100644 --- a/packages/apps/dashboard/client/package.json +++ b/packages/apps/dashboard/client/package.json @@ -20,7 +20,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@human-protocol/sdk": "workspace:*", - "@mui/icons-material": "^7.2.0", + "@mui/icons-material": "^7.3.8", "@mui/material": "^7.2.0", "@mui/styled-engine-sc": "7.2.0", "@mui/system": "^7.2.0", @@ -35,7 +35,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-number-format": "^5.4.3", - "react-router-dom": "^6.23.1", + "react-router-dom": "^7.13.0", "recharts": "^2.13.0-alpha.4", "simplebar-react": "^3.3.2", "styled-components": "^6.1.11", @@ -46,7 +46,7 @@ "zustand": "^5.0.10" }, "devDependencies": { - "@eslint/js": "^9.27.0", + "@eslint/js": "^10.0.1", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@typescript-eslint/eslint-plugin": "^7.2.0", @@ -59,7 +59,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.11", "globals": "^16.2.0", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "sass": "^1.89.2", "terser": "^5.36.0", "typescript": "^5.6.3", diff --git a/packages/apps/dashboard/server/eslint.config.mjs b/packages/apps/dashboard/server/eslint.config.mjs index 0d8f27e5ea..12c294c9ab 100644 --- a/packages/apps/dashboard/server/eslint.config.mjs +++ b/packages/apps/dashboard/server/eslint.config.mjs @@ -30,6 +30,8 @@ export default tseslint.config( jest: jestPlugin, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/apps/dashboard/server/package.json b/packages/apps/dashboard/server/package.json index 1aa0bbc590..88c324eb9a 100644 --- a/packages/apps/dashboard/server/package.json +++ b/packages/apps/dashboard/server/package.json @@ -62,7 +62,7 @@ "eslint-plugin-jest": "^28.9.0", "eslint-plugin-prettier": "^5.5.5", "jest": "^29.7.0", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "source-map-support": "^0.5.20", "ts-jest": "29.2.5", "ts-node": "^10.0.0", diff --git a/packages/apps/dashboard/server/src/common/filters/exception.filter.ts b/packages/apps/dashboard/server/src/common/filters/exception.filter.ts index fe958fdd00..5e5e80ba4d 100644 --- a/packages/apps/dashboard/server/src/common/filters/exception.filter.ts +++ b/packages/apps/dashboard/server/src/common/filters/exception.filter.ts @@ -6,6 +6,7 @@ import { HttpException, } from '@nestjs/common'; import { Request, Response } from 'express'; +import { SubgraphRequestError } from '@human-protocol/sdk'; import logger from '../../logger'; @@ -23,7 +24,15 @@ export class ExceptionFilter implements IExceptionFilter { message: 'Internal server error', }; - if (exception instanceof HttpException) { + if (exception instanceof SubgraphRequestError) { + status = HttpStatus.BAD_GATEWAY; + responseBody.message = exception.message; + + this.logger.error('Subgraph request failed', { + error: exception, + path: request.url, + }); + } else if (exception instanceof HttpException) { status = exception.getStatus(); const exceptionResponse = exception.getResponse(); if (typeof exceptionResponse === 'string') { diff --git a/packages/apps/faucet/client/eslint.config.mjs b/packages/apps/faucet/client/eslint.config.mjs index e6ac5e722b..a20306124a 100644 --- a/packages/apps/faucet/client/eslint.config.mjs +++ b/packages/apps/faucet/client/eslint.config.mjs @@ -25,6 +25,8 @@ export default tseslint.config( }, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/apps/faucet/client/package.json b/packages/apps/faucet/client/package.json index 0e0aad48dd..b821969172 100644 --- a/packages/apps/faucet/client/package.json +++ b/packages/apps/faucet/client/package.json @@ -21,12 +21,12 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@human-protocol/sdk": "workspace:*", - "@mui/icons-material": "^7.0.1", + "@mui/icons-material": "^7.3.8", "@mui/material": "^5.16.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-loading-skeleton": "^3.3.1", - "react-router-dom": "^6.4.3", + "react-router-dom": "^7.13.0", "serve": "^14.2.4", "viem": "2.x" }, @@ -41,7 +41,7 @@ "eslint-plugin-import": "^2.29.0", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^5.1.0", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "typescript": "^5.8.3", "vite": "^6.2.4", "vite-plugin-node-polyfills": "^0.25.0" diff --git a/packages/apps/faucet/server/eslint.config.mjs b/packages/apps/faucet/server/eslint.config.mjs index 1c908b5aea..c7c9b424f2 100644 --- a/packages/apps/faucet/server/eslint.config.mjs +++ b/packages/apps/faucet/server/eslint.config.mjs @@ -24,6 +24,8 @@ export default tseslint.config( }, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', 'no-console': 'warn', 'prettier/prettier': 'error', '@/quotes': [ diff --git a/packages/apps/fortune/exchange-oracle/client/Dockerfile b/packages/apps/fortune/exchange-oracle/client/Dockerfile index 1e5800c6bb..93e9380e77 100644 --- a/packages/apps/fortune/exchange-oracle/client/Dockerfile +++ b/packages/apps/fortune/exchange-oracle/client/Dockerfile @@ -23,7 +23,7 @@ RUN yarn workspaces focus @apps/fortune-exchange-oracle-client # Copy base TS config that is required to build packages COPY tsconfig.base.json ./ # Build libs (scoped) -RUN yarn workspaces foreach -Rpt --from @apps/fortune-exchange-oracle-client run build +RUN yarn workspaces foreach -Rpt --from @apps/fortune-exchange-oracle-client --exclude @apps/fortune-exchange-oracle-client run build # Copy everything else COPY ${APP_PATH} ./${APP_PATH} @@ -32,4 +32,4 @@ WORKDIR ./${APP_PATH} RUN yarn build # Start the server using the build -CMD [ "yarn", "start:prod" ] \ No newline at end of file +CMD [ "yarn", "start:prod" ] diff --git a/packages/apps/fortune/exchange-oracle/client/eslint.config.mjs b/packages/apps/fortune/exchange-oracle/client/eslint.config.mjs index 3649fc8f9a..ee368a0556 100644 --- a/packages/apps/fortune/exchange-oracle/client/eslint.config.mjs +++ b/packages/apps/fortune/exchange-oracle/client/eslint.config.mjs @@ -35,6 +35,8 @@ const config = tseslint.config( 'react-refresh': reactRefreshPlugin, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', 'react-refresh/only-export-components': [ diff --git a/packages/apps/fortune/exchange-oracle/client/package.json b/packages/apps/fortune/exchange-oracle/client/package.json index 7ffddc7d88..9e2a7f1309 100644 --- a/packages/apps/fortune/exchange-oracle/client/package.json +++ b/packages/apps/fortune/exchange-oracle/client/package.json @@ -30,7 +30,7 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@human-protocol/sdk": "workspace:^", - "@mui/icons-material": "^7.0.1", + "@mui/icons-material": "^7.3.8", "@mui/material": "^5.16.7", "@tanstack/query-sync-storage-persister": "^5.68.0", "@tanstack/react-query": "^5.67.2", @@ -39,7 +39,7 @@ "ethers": "^6.15.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.24.1", + "react-router-dom": "^7.13.0", "serve": "^14.2.4", "viem": "2.x", "wagmi": "^2.14.6" @@ -54,7 +54,7 @@ "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.11", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "typescript": "^5.6.3", "vite": "^6.2.4", "vite-plugin-node-polyfills": "^0.25.0" diff --git a/packages/apps/fortune/exchange-oracle/server/Dockerfile b/packages/apps/fortune/exchange-oracle/server/Dockerfile index 969544f4b1..14690d1feb 100644 --- a/packages/apps/fortune/exchange-oracle/server/Dockerfile +++ b/packages/apps/fortune/exchange-oracle/server/Dockerfile @@ -23,7 +23,7 @@ RUN yarn workspaces focus @apps/fortune-exchange-oracle-server # Copy base TS config that is required to build packages COPY tsconfig.base.json ./ # Build libs (scoped) -RUN yarn workspaces foreach -Rpt --from @apps/fortune-exchange-oracle-server run build +RUN yarn workspaces foreach -Rpt --from @apps/fortune-exchange-oracle-server --exclude @apps/fortune-exchange-oracle-server run build # Copy everything else COPY ${APP_PATH} ./${APP_PATH} diff --git a/packages/apps/fortune/exchange-oracle/server/eslint.config.mjs b/packages/apps/fortune/exchange-oracle/server/eslint.config.mjs index 471eb358d4..3c398c43c0 100644 --- a/packages/apps/fortune/exchange-oracle/server/eslint.config.mjs +++ b/packages/apps/fortune/exchange-oracle/server/eslint.config.mjs @@ -29,6 +29,8 @@ const config = tseslint.config( jest: jestPlugin, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/apps/fortune/exchange-oracle/server/package.json b/packages/apps/fortune/exchange-oracle/server/package.json index 6a3d223834..d18b95a9f6 100644 --- a/packages/apps/fortune/exchange-oracle/server/package.json +++ b/packages/apps/fortune/exchange-oracle/server/package.json @@ -79,7 +79,7 @@ "eslint-plugin-jest": "^28.9.0", "eslint-plugin-prettier": "^5.5.5", "jest": "^29.7.0", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "source-map-support": "^0.5.20", "ts-jest": "29.2.5", "ts-node": "^10.9.2", diff --git a/packages/apps/fortune/exchange-oracle/server/src/common/exceptions/exception.filter.ts b/packages/apps/fortune/exchange-oracle/server/src/common/exceptions/exception.filter.ts index 96e7b88236..5bfc921d06 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/common/exceptions/exception.filter.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/common/exceptions/exception.filter.ts @@ -5,6 +5,7 @@ import { HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; +import { SubgraphRequestError } from '@human-protocol/sdk'; import logger from '../../logger'; import { @@ -36,6 +37,8 @@ export class ExceptionFilter implements IExceptionFilter { return HttpStatus.UNPROCESSABLE_ENTITY; } else if (exception instanceof DatabaseError) { return HttpStatus.UNPROCESSABLE_ENTITY; + } else if (exception instanceof SubgraphRequestError) { + return HttpStatus.BAD_GATEWAY; } const exceptionStatusCode = exception.statusCode || exception.status; @@ -51,7 +54,12 @@ export class ExceptionFilter implements IExceptionFilter { const status = this.getStatus(exception); const message = exception.message || 'Internal server error'; - if (status === HttpStatus.INTERNAL_SERVER_ERROR) { + if (exception instanceof SubgraphRequestError) { + this.logger.error('Subgraph request failed', { + error: exception, + path: request.url, + }); + } else if (status === HttpStatus.INTERNAL_SERVER_ERROR) { this.logger.error('Unhandled exception', { error: exception, path: request.url, diff --git a/packages/apps/fortune/recording-oracle/Dockerfile b/packages/apps/fortune/recording-oracle/Dockerfile index b6569d2c63..c25c15dda0 100644 --- a/packages/apps/fortune/recording-oracle/Dockerfile +++ b/packages/apps/fortune/recording-oracle/Dockerfile @@ -23,7 +23,7 @@ RUN yarn workspaces focus @apps/fortune-recording-oracle # Copy base TS config that is required to build packages COPY tsconfig.base.json ./ # Build libs (scoped) -RUN yarn workspaces foreach -Rpt --from @apps/fortune-recording-oracle run build +RUN yarn workspaces foreach -Rpt --from @apps/fortune-recording-oracle --exclude @apps/fortune-recording-oracle run build # Copy everything else COPY ${APP_PATH} ./${APP_PATH} diff --git a/packages/apps/fortune/recording-oracle/eslint.config.mjs b/packages/apps/fortune/recording-oracle/eslint.config.mjs index c131251039..e2042f3b2f 100644 --- a/packages/apps/fortune/recording-oracle/eslint.config.mjs +++ b/packages/apps/fortune/recording-oracle/eslint.config.mjs @@ -29,6 +29,8 @@ export default tseslint.config( jest: jestPlugin, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', 'no-console': 'warn', 'prettier/prettier': 'error', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/apps/fortune/recording-oracle/package.json b/packages/apps/fortune/recording-oracle/package.json index 7ae73fc159..88717fac85 100644 --- a/packages/apps/fortune/recording-oracle/package.json +++ b/packages/apps/fortune/recording-oracle/package.json @@ -53,7 +53,7 @@ "eslint-plugin-jest": "^28.9.0", "eslint-plugin-prettier": "^5.5.5", "jest": "^29.7.0", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "ts-jest": "29.2.5", "ts-node": "^10.9.2", "typescript": "^5.8.3" diff --git a/packages/apps/fortune/recording-oracle/src/common/config/env-schema.ts b/packages/apps/fortune/recording-oracle/src/common/config/env-schema.ts index 7a1a4d6144..865b9f747d 100644 --- a/packages/apps/fortune/recording-oracle/src/common/config/env-schema.ts +++ b/packages/apps/fortune/recording-oracle/src/common/config/env-schema.ts @@ -6,6 +6,7 @@ export const envValidator = Joi.object({ PORT: Joi.string(), // Web3 WEB3_PRIVATE_KEY: Joi.string().required(), + SDK_TX_TIMEOUT_MS: Joi.number(), RPC_URL_POLYGON: Joi.string(), RPC_URL_BSC: Joi.string(), RPC_URL_POLYGON_AMOY: Joi.string(), diff --git a/packages/apps/fortune/recording-oracle/src/common/config/web3-config.service.ts b/packages/apps/fortune/recording-oracle/src/common/config/web3-config.service.ts index 619e9a0444..e740cf11ca 100644 --- a/packages/apps/fortune/recording-oracle/src/common/config/web3-config.service.ts +++ b/packages/apps/fortune/recording-oracle/src/common/config/web3-config.service.ts @@ -12,4 +12,12 @@ export class Web3ConfigService { get privateKey(): string { return this.configService.getOrThrow('WEB3_PRIVATE_KEY'); } + + /** + * Timeout for web3 transactions in milliseconds. + * Default: 60000 (60 seconds) + */ + get txTimeoutMs(): number { + return +this.configService.get('SDK_TX_TIMEOUT_MS', 60000); + } } diff --git a/packages/apps/fortune/recording-oracle/src/common/exceptions/exception.filter.ts b/packages/apps/fortune/recording-oracle/src/common/exceptions/exception.filter.ts index 558cff998a..d9d7940c69 100644 --- a/packages/apps/fortune/recording-oracle/src/common/exceptions/exception.filter.ts +++ b/packages/apps/fortune/recording-oracle/src/common/exceptions/exception.filter.ts @@ -5,6 +5,7 @@ import { HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; +import { SubgraphRequestError } from '@human-protocol/sdk'; import logger from '../../logger'; import { @@ -33,6 +34,8 @@ export class ExceptionFilter implements IExceptionFilter { return HttpStatus.CONFLICT; } else if (exception instanceof ServerError) { return HttpStatus.UNPROCESSABLE_ENTITY; + } else if (exception instanceof SubgraphRequestError) { + return HttpStatus.BAD_GATEWAY; } const exceptionStatusCode = exception.statusCode || exception.status; @@ -48,7 +51,12 @@ export class ExceptionFilter implements IExceptionFilter { const status = this.getStatus(exception); const message = exception.message || 'Internal server error'; - if (status === HttpStatus.INTERNAL_SERVER_ERROR) { + if (exception instanceof SubgraphRequestError) { + this.logger.error('Subgraph request failed', { + error: exception, + path: request.url, + }); + } else if (status === HttpStatus.INTERNAL_SERVER_ERROR) { this.logger.error('Unhandled exception', { error: exception, path: request.url, diff --git a/packages/apps/fortune/recording-oracle/src/modules/job/job.service.ts b/packages/apps/fortune/recording-oracle/src/modules/job/job.service.ts index 09210cec12..9ce3e2b726 100644 --- a/packages/apps/fortune/recording-oracle/src/modules/job/job.service.ts +++ b/packages/apps/fortune/recording-oracle/src/modules/job/job.service.ts @@ -202,6 +202,7 @@ export class JobService { jobSolutionUploaded.url, jobSolutionUploaded.hash, !lastProcessedSolution?.error ? amountToReserve : 0n, + { timeoutMs: this.web3ConfigService.txTimeoutMs }, ); if ( @@ -307,6 +308,7 @@ export class JobService { intermediateResultsURL, intermediateResultsHash, 0n, + { timeoutMs: this.web3ConfigService.txTimeoutMs }, ); let reputationOracleWebhook: string | null = null; diff --git a/packages/apps/human-app/frontend/Dockerfile b/packages/apps/human-app/frontend/Dockerfile index a2371a5cec..d60ea4451d 100644 --- a/packages/apps/human-app/frontend/Dockerfile +++ b/packages/apps/human-app/frontend/Dockerfile @@ -23,7 +23,7 @@ RUN yarn workspaces focus @apps/human-app-frontend # Copy base TS config that is required to build packages COPY tsconfig.base.json ./ # Build libs (scoped) -RUN yarn workspaces foreach -Rpt --from @apps/human-app-frontend run build +RUN yarn workspaces foreach -Rpt --from @apps/human-app-frontend --exclude @apps/human-app-frontend run build # Copy everything else COPY ${APP_PATH} ./${APP_PATH} @@ -32,4 +32,4 @@ WORKDIR ./${APP_PATH} RUN yarn build # Start the server using the build -CMD [ "yarn", "start:prod" ] \ No newline at end of file +CMD [ "yarn", "start:prod" ] diff --git a/packages/apps/human-app/frontend/eslint.config.mjs b/packages/apps/human-app/frontend/eslint.config.mjs index fb7986dd00..21c512d7f1 100644 --- a/packages/apps/human-app/frontend/eslint.config.mjs +++ b/packages/apps/human-app/frontend/eslint.config.mjs @@ -34,6 +34,8 @@ export default tseslint.config( import: eslintPluginImport, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/apps/human-app/frontend/package.json b/packages/apps/human-app/frontend/package.json index c43bff58fb..7e778a016f 100644 --- a/packages/apps/human-app/frontend/package.json +++ b/packages/apps/human-app/frontend/package.json @@ -25,7 +25,7 @@ "@hcaptcha/react-hcaptcha": "^1.14.0", "@hookform/resolvers": "^5.1.0", "@human-protocol/sdk": "workspace:*", - "@mui/icons-material": "^7.0.1", + "@mui/icons-material": "^7.3.8", "@mui/material": "^5.16.7", "@mui/system": "^5.15.14", "@mui/x-date-pickers": "^8.26.0", @@ -50,7 +50,7 @@ "react-i18next": "^15.1.0", "react-imask": "^7.4.0", "react-number-format": "^5.4.3", - "react-router-dom": "^6.22.0", + "react-router-dom": "^7.13.0", "serve": "^14.2.4", "viem": "^2.31.4", "vite-plugin-svgr": "^4.2.0", @@ -77,7 +77,7 @@ "eslint-plugin-react-refresh": "^0.4.11", "husky": "^9.1.6", "jsdom": "^25.0.1", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "typescript": "^5.6.3", "vite": "^6.2.4", "vitest": "^3.1.1" diff --git a/packages/apps/human-app/server/Dockerfile b/packages/apps/human-app/server/Dockerfile index 36d786f20f..dac8c581e6 100644 --- a/packages/apps/human-app/server/Dockerfile +++ b/packages/apps/human-app/server/Dockerfile @@ -23,7 +23,7 @@ RUN yarn workspaces focus @apps/human-app-server # Copy base TS config that is required to build packages COPY tsconfig.base.json ./ # Build libs (scoped) -RUN yarn workspaces foreach -Rpt --from @apps/human-app-server run build +RUN yarn workspaces foreach -Rpt --from @apps/human-app-server --exclude @apps/human-app-server run build # Copy everything else COPY ${APP_PATH} ./${APP_PATH} @@ -32,4 +32,4 @@ WORKDIR ./${APP_PATH} RUN yarn build # Start the server using the build -CMD [ "yarn", "start:prod" ] \ No newline at end of file +CMD [ "yarn", "start:prod" ] diff --git a/packages/apps/human-app/server/eslint.config.mjs b/packages/apps/human-app/server/eslint.config.mjs index 2d61b62292..600fdc911e 100644 --- a/packages/apps/human-app/server/eslint.config.mjs +++ b/packages/apps/human-app/server/eslint.config.mjs @@ -30,6 +30,8 @@ const config = tseslint.config( jest: jestPlugin, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', 'no-console': 'warn', 'prettier/prettier': 'error', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/apps/human-app/server/package.json b/packages/apps/human-app/server/package.json index 34289c7932..c454439078 100644 --- a/packages/apps/human-app/server/package.json +++ b/packages/apps/human-app/server/package.json @@ -71,8 +71,8 @@ "eslint-plugin-jest": "^28.9.0", "eslint-plugin-prettier": "^5.5.5", "jest": "^29.7.0", - "nock": "^13.5.1", - "prettier": "^3.7.4", + "nock": "^14.0.11", + "prettier": "^3.8.1", "source-map-support": "^0.5.20", "ts-jest": "29.2.5", "ts-node": "^10.9.2", diff --git a/packages/apps/human-app/server/src/common/filter/exceptions.filter.ts b/packages/apps/human-app/server/src/common/filter/exceptions.filter.ts index 61eb3a6c4d..cf8bea9b16 100644 --- a/packages/apps/human-app/server/src/common/filter/exceptions.filter.ts +++ b/packages/apps/human-app/server/src/common/filter/exceptions.filter.ts @@ -5,6 +5,7 @@ import { HttpException, HttpStatus, } from '@nestjs/common'; +import { SubgraphRequestError } from '@human-protocol/sdk'; import logger from '../../logger'; import { AxiosError } from 'axios'; import * as errorUtils from '../utils/error'; @@ -21,7 +22,15 @@ export class ExceptionFilter implements IExceptionFilter { let status = HttpStatus.INTERNAL_SERVER_ERROR; let message: any = 'Internal Server Error'; - if (exception instanceof HttpException) { + if (exception instanceof SubgraphRequestError) { + status = HttpStatus.BAD_GATEWAY; + message = exception.message; + + this.logger.error('Subgraph request failed', { + error: errorUtils.formatError(exception), + path: request.url, + }); + } else if (exception instanceof HttpException) { status = exception.getStatus(); message = exception.getResponse(); } else if (exception instanceof AxiosError) { diff --git a/packages/apps/job-launcher/client/Dockerfile b/packages/apps/job-launcher/client/Dockerfile index ec0bea13d1..b0d2180584 100644 --- a/packages/apps/job-launcher/client/Dockerfile +++ b/packages/apps/job-launcher/client/Dockerfile @@ -23,7 +23,7 @@ RUN yarn workspaces focus @apps/job-launcher-client # Copy base TS config that is required to build packages COPY tsconfig.base.json ./ # Build libs (scoped) -RUN yarn workspaces foreach -Rpt --from @apps/job-launcher-client run build +RUN yarn workspaces foreach -Rpt --from @apps/job-launcher-client --exclude @apps/job-launcher-client run build # Copy everything else COPY ${APP_PATH} ./${APP_PATH} @@ -32,4 +32,4 @@ WORKDIR ./${APP_PATH} RUN yarn build # Start the server using the build -CMD [ "yarn", "start:prod" ] \ No newline at end of file +CMD [ "yarn", "start:prod" ] diff --git a/packages/apps/job-launcher/client/eslint.config.mjs b/packages/apps/job-launcher/client/eslint.config.mjs index 349d15df68..0016de8981 100644 --- a/packages/apps/job-launcher/client/eslint.config.mjs +++ b/packages/apps/job-launcher/client/eslint.config.mjs @@ -33,6 +33,8 @@ export default tseslint.config( import: eslintPluginImport, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/apps/job-launcher/client/package.json b/packages/apps/job-launcher/client/package.json index 1add81d388..9cf9b95fcd 100644 --- a/packages/apps/job-launcher/client/package.json +++ b/packages/apps/job-launcher/client/package.json @@ -8,7 +8,7 @@ "@emotion/styled": "^11.10.5", "@hcaptcha/react-hcaptcha": "^1.14.0", "@human-protocol/sdk": "workspace:*", - "@mui/icons-material": "^7.0.1", + "@mui/icons-material": "^7.3.8", "@mui/lab": "^6.0.0-dev.240424162023-9968b4889d", "@mui/material": "^5.16.7", "@mui/system": "^5.15.14", @@ -30,7 +30,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.1.0", - "react-router-dom": "^6.14.1", + "react-router-dom": "^7.13.0", "recharts": "^2.7.2", "serve": "^14.2.4", "swr": "^2.2.4", @@ -76,7 +76,7 @@ "eslint-plugin-import": "^2.29.0", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^5.1.0", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "resize-observer-polyfill": "^1.5.1", "vite": "^6.2.4", "vite-plugin-node-polyfills": "^0.25.0" diff --git a/packages/apps/job-launcher/server/Dockerfile b/packages/apps/job-launcher/server/Dockerfile index ddeb087e3b..3c5ff227c7 100644 --- a/packages/apps/job-launcher/server/Dockerfile +++ b/packages/apps/job-launcher/server/Dockerfile @@ -23,7 +23,7 @@ RUN yarn workspaces focus @apps/job-launcher-server # Copy base TS config that is required to build packages COPY tsconfig.base.json ./ # Build libs (scoped) -RUN yarn workspaces foreach -Rpt --from @apps/job-launcher-server run build +RUN yarn workspaces foreach -Rpt --from @apps/job-launcher-server --exclude @apps/job-launcher-server run build # Copy everything else COPY ${APP_PATH} ./${APP_PATH} @@ -32,4 +32,4 @@ WORKDIR ./${APP_PATH} RUN yarn build # Start the server using the build -CMD [ "yarn", "start:prod" ] \ No newline at end of file +CMD [ "yarn", "start:prod" ] diff --git a/packages/apps/job-launcher/server/eslint.config.mjs b/packages/apps/job-launcher/server/eslint.config.mjs index 4f8ea39dc5..8dd55ac3e3 100644 --- a/packages/apps/job-launcher/server/eslint.config.mjs +++ b/packages/apps/job-launcher/server/eslint.config.mjs @@ -30,6 +30,8 @@ const config = tseslint.config( jest: jestPlugin, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/apps/job-launcher/server/package.json b/packages/apps/job-launcher/server/package.json index c41032dea4..0acccff919 100644 --- a/packages/apps/job-launcher/server/package.json +++ b/packages/apps/job-launcher/server/package.json @@ -93,7 +93,7 @@ "eslint-plugin-jest": "^28.9.0", "eslint-plugin-prettier": "^5.5.5", "jest": "^29.7.0", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "source-map-support": "^0.5.20", "ts-jest": "29.2.5", "ts-node": "^10.9.2", diff --git a/packages/apps/job-launcher/server/src/common/config/env-schema.ts b/packages/apps/job-launcher/server/src/common/config/env-schema.ts index 2dca07e7b4..195f9becb1 100644 --- a/packages/apps/job-launcher/server/src/common/config/env-schema.ts +++ b/packages/apps/job-launcher/server/src/common/config/env-schema.ts @@ -26,6 +26,7 @@ export const envValidator = Joi.object({ // Web3 WEB3_ENV: Joi.string(), WEB3_PRIVATE_KEY: Joi.string().required(), + SDK_TX_TIMEOUT_MS: Joi.number(), GAS_PRICE_MULTIPLIER: Joi.number(), APPROVE_AMOUNT_USD: Joi.number(), REPUTATION_ORACLE_ADDRESS: Joi.string().required(), diff --git a/packages/apps/job-launcher/server/src/common/config/web3-config.service.ts b/packages/apps/job-launcher/server/src/common/config/web3-config.service.ts index b2f0f8a5d0..9c5806f0f4 100644 --- a/packages/apps/job-launcher/server/src/common/config/web3-config.service.ts +++ b/packages/apps/job-launcher/server/src/common/config/web3-config.service.ts @@ -96,4 +96,12 @@ export class Web3ConfigService { get approveAmountUsd(): number { return this.configService.get('APPROVE_AMOUNT_USD', 0); } + + /** + * Timeout for web3 transactions in milliseconds. + * Default: 60000 (60 seconds) + */ + get txTimeoutMs(): number { + return +this.configService.get('SDK_TX_TIMEOUT_MS', 60000); + } } diff --git a/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts b/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts index c1b6f3362a..de67ef3283 100644 --- a/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts +++ b/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts @@ -5,6 +5,7 @@ import { HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; +import { SubgraphRequestError } from '@human-protocol/sdk'; import { ValidationError, @@ -36,6 +37,8 @@ export class ExceptionFilter implements IExceptionFilter { return HttpStatus.UNPROCESSABLE_ENTITY; } else if (exception instanceof DatabaseError) { return HttpStatus.UNPROCESSABLE_ENTITY; + } else if (exception instanceof SubgraphRequestError) { + return HttpStatus.BAD_GATEWAY; } const exceptionStatusCode = exception.statusCode || exception.status; @@ -51,7 +54,12 @@ export class ExceptionFilter implements IExceptionFilter { const status = this.getStatus(exception); const message = exception.message || 'Internal server error'; - if (status === HttpStatus.INTERNAL_SERVER_ERROR) { + if (exception instanceof SubgraphRequestError) { + this.logger.error('Subgraph request failed', { + error: exception, + path: request.url, + }); + } else if (status === HttpStatus.INTERNAL_SERVER_ERROR) { this.logger.error('Unhandled exception', { error: exception, path: request.url, diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts index 42f7b763b0..cdf7d3a25a 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts @@ -15,6 +15,7 @@ import { Test } from '@nestjs/testing'; import { ethers, ZeroAddress } from 'ethers'; import { createSignerMock } from '../../../test/fixtures/web3'; import { ServerConfigService } from '../../common/config/server-config.service'; +import { Web3ConfigService } from '../../common/config/web3-config.service'; import { ErrorEscrow, ErrorJob } from '../../common/constants/errors'; import { TOKEN_ADDRESSES } from '../../common/constants/tokens'; import { @@ -82,6 +83,9 @@ const mockRateService = createMock(); const mockRoutingProtocolService = createMock(); const mockManifestService = createMock(); const mockWhitelistService = createMock(); +const mockWeb3ConfigService = { + txTimeoutMs: faker.number.int({ min: 30000, max: 120000 }), +}; const mockedEscrowClient = jest.mocked(EscrowClient); const mockedEscrowUtils = jest.mocked(EscrowUtils); @@ -129,6 +133,7 @@ describe('JobService', () => { provide: ManifestService, useValue: mockManifestService, }, + { provide: Web3ConfigService, useValue: mockWeb3ConfigService }, ], }).compile(); @@ -754,7 +759,10 @@ describe('JobService', () => { } as unknown as EscrowClient); mockWeb3Service.ensureEscrowAllowance.mockResolvedValueOnce(undefined); - mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(1n); + mockWeb3Service.calculateTxFees.mockResolvedValueOnce({ + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + }); const token = (TOKEN_ADDRESSES[jobEntity.chainId as ChainId] ?? {})[ jobEntity.token as EscrowFundToken @@ -774,7 +782,7 @@ describe('JobService', () => { expectedWeiAmount, NETWORKS[jobEntity.chainId as ChainId]!.factoryAddress, ); - expect(mockWeb3Service.calculateGasPrice).toHaveBeenCalledWith( + expect(mockWeb3Service.calculateTxFees).toHaveBeenCalledWith( jobEntity.chainId, ); expect(createFundAndSetupEscrowMock).toHaveBeenCalledWith( @@ -791,7 +799,11 @@ describe('JobService', () => { manifest: jobEntity.manifestUrl, manifestHash: jobEntity.manifestHash, }), - { gasPrice: 1n }, + { + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + timeoutMs: mockWeb3ConfigService.txTimeoutMs, + }, ); expect(result.status).toBe(JobStatus.LAUNCHED); expect(result.escrowAddress).toBe(escrowAddress); @@ -837,7 +849,10 @@ describe('JobService', () => { } as unknown as EscrowClient); mockWeb3Service.ensureEscrowAllowance.mockResolvedValueOnce(undefined); - mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(1n); + mockWeb3Service.calculateTxFees.mockResolvedValueOnce({ + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + }); const token = (TOKEN_ADDRESSES[jobEntity.chainId as ChainId] ?? {})[ jobEntity.token as EscrowFundToken @@ -864,7 +879,11 @@ describe('JobService', () => { expectedWeiAmount, jobEntity.userId.toString(), expect.any(Object), - { gasPrice: 1n }, + { + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + timeoutMs: mockWeb3ConfigService.txTimeoutMs, + }, ); expect(mockJobRepository.updateOne).not.toHaveBeenCalled(); @@ -1262,7 +1281,10 @@ describe('JobService', () => { describe('processEscrowCancellation', () => { it('should process escrow cancellation', async () => { const jobEntity = createJobEntity(); - mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(1n); + mockWeb3Service.calculateTxFees.mockResolvedValueOnce({ + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + }); const getStatusMock = jest.fn().mockResolvedValueOnce('Active'); const requestCancellationMock = jest .fn() @@ -1283,7 +1305,10 @@ describe('JobService', () => { it('should throw if escrow status is not Active', async () => { const jobEntity = createJobEntity(); - mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(1n); + mockWeb3Service.calculateTxFees.mockResolvedValueOnce({ + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + }); mockedEscrowClient.build.mockResolvedValueOnce({ getStatus: jest.fn().mockResolvedValueOnce(EscrowStatus.Complete), requestCancellation: jest.fn(), @@ -1299,7 +1324,7 @@ describe('JobService', () => { // TODO: Re-enable when cancellation is removed from processEscrowCancellation // it('should throw if requestCancellation throws an error', async () => { // const jobEntity = createJobEntity(); - // mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(1n); + // mockWeb3Service.calculateTxFees.mockResolvedValueOnce({ maxFeePerGas: 1n, maxPriorityFeePerGas: 1n }); // mockedEscrowClient.build.mockResolvedValueOnce({ // getStatus: jest.fn().mockResolvedValueOnce(EscrowStatus.Pending), // requestCancellation: jest diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index 5984349463..efe70a86f7 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -14,6 +14,7 @@ import { validate, } from 'class-validator'; import { ethers } from 'ethers'; +import { Web3ConfigService } from '../../common/config/web3-config.service'; import { ServerConfigService } from '../../common/config/server-config.service'; import { CANCEL_JOB_STATUSES } from '../../common/constants'; import { @@ -87,6 +88,7 @@ export class JobService { constructor( @Inject(Web3Service) private readonly web3Service: Web3Service, + private readonly web3ConfigService: Web3ConfigService, private readonly jobRepository: JobRepository, private readonly webhookRepository: WebhookRepository, private readonly paymentService: PaymentService, @@ -346,7 +348,8 @@ export class JobService { jobEntity.userId.toString(), escrowConfig, { - gasPrice: await this.web3Service.calculateGasPrice(jobEntity.chainId), + ...(await this.web3Service.calculateTxFees(jobEntity.chainId)), + timeoutMs: this.web3ConfigService.txTimeoutMs, }, ); @@ -610,7 +613,8 @@ export class JobService { // TODO: Remove try-catch when requestCancellation is fully supported by all escrows try { await (escrowClient as any).requestCancellation(escrowAddress!, { - gasPrice: await this.web3Service.calculateGasPrice(chainId), + ...(await this.web3Service.calculateTxFees(chainId)), + timeoutMs: this.web3ConfigService.txTimeoutMs, }); } catch (error: any) { this.logger.warn( @@ -623,7 +627,8 @@ export class JobService { }, ); await (escrowClient as any).cancel(escrowAddress!, { - gasPrice: await this.web3Service.calculateGasPrice(chainId), + ...(await this.web3Service.calculateTxFees(chainId)), + timeoutMs: this.web3ConfigService.txTimeoutMs, }); } } diff --git a/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts b/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts index a08edebae1..dcaaadd94b 100644 --- a/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts @@ -36,6 +36,7 @@ jest.mock('@human-protocol/sdk', () => { describe('Web3Service', () => { let configService: ConfigService; let web3Service: Web3Service; + let web3ConfigService: Web3ConfigService; const mockRateService = { getRate: jest.fn(), }; @@ -66,6 +67,7 @@ describe('Web3Service', () => { }).compile(); web3Service = moduleRef.get(Web3Service); + web3ConfigService = moduleRef.get(Web3ConfigService); configService = moduleRef.get(ConfigService); }); @@ -115,41 +117,77 @@ describe('Web3Service', () => { }); }); - describe('calculateGasPrice', () => { - it('should return gas price multiplied by the multiplier', async () => { + describe('calculateTxFees', () => { + it('should return transaction fees multiplied by the multiplier', async () => { jest.spyOn(configService, 'get').mockImplementation((key: string) => { if (key === 'GAS_PRICE_MULTIPLIER') return 1; return mockConfig[key]; }); - const mockGasPrice = BigInt(1000000000); + const mockMaxFeePerGas = faker.number.bigInt(); + const mockMaxPriorityFeePerGas = faker.number.bigInt(); web3Service.getSigner = jest.fn().mockReturnValue({ address: MOCK_ADDRESS, getNetwork: jest.fn().mockResolvedValue({ chainId: 1 }), provider: { - getFeeData: jest - .fn() - .mockResolvedValueOnce({ gasPrice: mockGasPrice }), + getFeeData: jest.fn().mockResolvedValueOnce({ + maxFeePerGas: mockMaxFeePerGas, + maxPriorityFeePerGas: mockMaxPriorityFeePerGas, + }), }, }); - const result = await web3Service.calculateGasPrice(ChainId.POLYGON_AMOY); - expect(result).toBe(mockGasPrice * BigInt(1)); + const result = await web3Service.calculateTxFees(ChainId.POLYGON_AMOY); + expect(result).toEqual({ + maxFeePerGas: + mockMaxFeePerGas * BigInt(web3ConfigService.gasPriceMultiplier), + maxPriorityFeePerGas: + mockMaxPriorityFeePerGas * + BigInt(web3ConfigService.gasPriceMultiplier), + }); }); - it('should throw an error if gasPrice is undefined', async () => { + it('should throw an error if transaction fees are missing', async () => { web3Service.getSigner = jest.fn().mockReturnValue({ address: MOCK_ADDRESS, getNetwork: jest.fn().mockResolvedValue({ chainId: 1 }), provider: { - getFeeData: jest.fn().mockResolvedValueOnce({ gasPrice: undefined }), + getFeeData: jest.fn().mockResolvedValueOnce({ + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + }), }, }); await expect( - web3Service.calculateGasPrice(ChainId.POLYGON_AMOY), + web3Service.calculateTxFees(ChainId.POLYGON_AMOY), ).rejects.toThrow(new ConflictError(ErrorWeb3.GasPriceError)); }); + + it('should fallback to legacy gasPrice data', async () => { + const mockGasPrice = faker.number.bigInt(); + + web3Service.getSigner = jest.fn().mockReturnValue({ + address: MOCK_ADDRESS, + getNetwork: jest.fn().mockResolvedValue({ chainId: 1 }), + provider: { + getFeeData: jest.fn().mockResolvedValueOnce({ + gasPrice: mockGasPrice, + maxFeePerGas: null, + maxPriorityFeePerGas: null, + }), + }, + }); + + await expect( + web3Service.calculateTxFees(ChainId.POLYGON_AMOY), + ).resolves.toEqual({ + maxFeePerGas: + mockGasPrice * BigInt(web3ConfigService.gasPriceMultiplier), + maxPriorityFeePerGas: + mockGasPrice * BigInt(web3ConfigService.gasPriceMultiplier), + }); + }); }); describe('validateChainId', () => { diff --git a/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts b/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts index a288dc2be3..10bb5fdd40 100644 --- a/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts +++ b/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts @@ -45,14 +45,29 @@ export class Web3Service { } } - public async calculateGasPrice(chainId: number): Promise { + public async calculateTxFees(chainId: number): Promise<{ + maxFeePerGas: bigint; + maxPriorityFeePerGas: bigint; + }> { const signer = this.getSigner(chainId); - const multiplier = this.web3ConfigService.gasPriceMultiplier; + const multiplier = BigInt(this.web3ConfigService.gasPriceMultiplier); + const feeData = await signer.provider?.getFeeData(); - const gasPrice = (await signer.provider?.getFeeData())?.gasPrice; - if (gasPrice) { - return gasPrice * BigInt(multiplier); + if (!feeData) { + throw new ConflictError(ErrorWeb3.GasPriceError); } + + const maxFeePerGas = feeData.maxFeePerGas ?? feeData.gasPrice; + const maxPriorityFeePerGas = + feeData.maxPriorityFeePerGas ?? feeData.gasPrice; + + if (maxFeePerGas && maxPriorityFeePerGas) { + return { + maxFeePerGas: maxFeePerGas * multiplier, + maxPriorityFeePerGas: maxPriorityFeePerGas * multiplier, + }; + } + throw new ConflictError(ErrorWeb3.GasPriceError); } diff --git a/packages/apps/reputation-oracle/server/Dockerfile b/packages/apps/reputation-oracle/server/Dockerfile index c8e947c145..425086d324 100644 --- a/packages/apps/reputation-oracle/server/Dockerfile +++ b/packages/apps/reputation-oracle/server/Dockerfile @@ -22,8 +22,9 @@ RUN yarn workspaces focus @apps/reputation-oracle # Copy base TS config that is required to build packages COPY tsconfig.base.json ./ -# Build libs (scoped) -RUN yarn workspaces foreach -Rpt --from @apps/reputation-oracle run build +# Build only dependency workspaces; the app itself is built later +# after its full source (including tsconfig.json) is copied. +RUN yarn workspaces foreach -Rpt --from @apps/reputation-oracle --exclude @apps/reputation-oracle run build # Copy everything else COPY ${APP_PATH} ./${APP_PATH} @@ -32,4 +33,4 @@ WORKDIR ./${APP_PATH} RUN yarn build # Start the server using the build -CMD [ "yarn", "start:prod" ] \ No newline at end of file +CMD [ "yarn", "start:prod" ] diff --git a/packages/apps/reputation-oracle/server/eslint.config.mjs b/packages/apps/reputation-oracle/server/eslint.config.mjs index 19d77d0394..6275a10188 100644 --- a/packages/apps/reputation-oracle/server/eslint.config.mjs +++ b/packages/apps/reputation-oracle/server/eslint.config.mjs @@ -28,6 +28,8 @@ export default tseslint.config( 'import': importPlugin, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/no-unsafe-argument': 'warn', diff --git a/packages/apps/reputation-oracle/server/package.json b/packages/apps/reputation-oracle/server/package.json index 83b3a75c43..3aa650e7e6 100644 --- a/packages/apps/reputation-oracle/server/package.json +++ b/packages/apps/reputation-oracle/server/package.json @@ -73,7 +73,7 @@ "zxcvbn": "^4.4.2" }, "devDependencies": { - "@eslint/js": "^9.33.0", + "@eslint/js": "^10.0.1", "@faker-js/faker": "^9.8.0", "@golevelup/ts-jest": "^0.6.1", "@nestjs/cli": "^11.0.16", @@ -93,8 +93,8 @@ "eslint-plugin-prettier": "^5.5.5", "globals": "^16.3.0", "jest": "^29.7.0", - "nock": "^14.0.3", - "prettier": "^3.7.4", + "nock": "^14.0.11", + "prettier": "^3.8.1", "ts-jest": "29.2.5", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", diff --git a/packages/apps/reputation-oracle/server/src/common/filters/exception.filter.ts b/packages/apps/reputation-oracle/server/src/common/filters/exception.filter.ts index f15ee7315c..eac86fcbea 100644 --- a/packages/apps/reputation-oracle/server/src/common/filters/exception.filter.ts +++ b/packages/apps/reputation-oracle/server/src/common/filters/exception.filter.ts @@ -1,3 +1,4 @@ +import { SubgraphRequestError } from '@human-protocol/sdk'; import { ArgumentsHost, Catch, @@ -31,6 +32,13 @@ export class ExceptionFilter implements IExceptionFilter { responseBody.message = 'Unprocessable entity'; } this.logger.error('Database error', exception); + } else if (exception instanceof SubgraphRequestError) { + status = HttpStatus.BAD_GATEWAY; + responseBody.message = exception.message; + this.logger.error('Subgraph request failed', { + error: exception, + path: request.url, + }); } else if (exception instanceof HttpException) { status = exception.getStatus(); const exceptionResponse = exception.getResponse(); diff --git a/packages/apps/reputation-oracle/server/src/config/env-schema.ts b/packages/apps/reputation-oracle/server/src/config/env-schema.ts index 78876e7ec8..c2a55a98d8 100644 --- a/packages/apps/reputation-oracle/server/src/config/env-schema.ts +++ b/packages/apps/reputation-oracle/server/src/config/env-schema.ts @@ -47,6 +47,7 @@ export const envValidator = Joi.object({ // Web3 WEB3_ENV: Joi.string().valid(...Object.values(Web3Network)), WEB3_PRIVATE_KEY: Joi.string().required(), + SDK_TX_TIMEOUT_MS: Joi.number().integer(), GAS_PRICE_MULTIPLIER: Joi.number().positive(), RPC_URL_SEPOLIA: Joi.string().uri({ scheme: ['http', 'https'] }), RPC_URL_POLYGON: Joi.string().uri({ scheme: ['http', 'https'] }), diff --git a/packages/apps/reputation-oracle/server/src/config/web3-config.service.ts b/packages/apps/reputation-oracle/server/src/config/web3-config.service.ts index dbb40d55f4..98cc7b4a0e 100644 --- a/packages/apps/reputation-oracle/server/src/config/web3-config.service.ts +++ b/packages/apps/reputation-oracle/server/src/config/web3-config.service.ts @@ -56,6 +56,14 @@ export class Web3ConfigService { return Number(this.configService.get('GAS_PRICE_MULTIPLIER')) || 1; } + /** + * Timeout for web3 transactions in milliseconds. + * Default: 60000 (60 seconds) + */ + get txTimeoutMs(): number { + return +this.configService.get('SDK_TX_TIMEOUT_MS', 60000); + } + getRpcUrlByChainId(chainId: number): string | undefined { const rpcUrlsByChainId: Record = { [ChainId.POLYGON]: this.configService.get('RPC_URL_POLYGON'), diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts index e13aa05392..d8a951f98f 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.spec.ts @@ -26,11 +26,14 @@ import stringify from 'json-stable-stringify'; import _ from 'lodash'; import { CvatJobType, FortuneJobType } from '@/common/enums'; -import { ServerConfigService } from '@/config'; +import { ServerConfigService, Web3ConfigService } from '@/config'; import { ReputationService } from '@/modules/reputation'; import { StorageService } from '@/modules/storage'; import { WalletWithProvider, Web3Service } from '@/modules/web3'; -import { generateTestnetChainId } from '@/modules/web3/fixtures'; +import { + generateTestnetChainId, + mockWeb3ConfigService, +} from '@/modules/web3/fixtures'; import { OutgoingWebhookService } from '@/modules/webhook'; import { createSignerMock, type SignerMock } from '~/test/fixtures/web3'; @@ -99,6 +102,10 @@ describe('EscrowCompletionService', () => { provide: StorageService, useValue: mockStorageService, }, + { + provide: Web3ConfigService, + useValue: mockWeb3ConfigService, + }, { provide: OutgoingWebhookService, useValue: mockOutgoingWebhookService, @@ -998,8 +1005,11 @@ describe('EscrowCompletionService', () => { recordingOracle: recordingOracleAddress, } as unknown as IEscrow); mockGetEscrowStatus.mockResolvedValueOnce(escrowStatus); - const mockGasPrice = faker.number.bigInt(); - mockWeb3Service.calculateGasPrice.mockResolvedValueOnce(mockGasPrice); + const mockFees = { + maxFeePerGas: faker.number.bigInt(), + maxPriorityFeePerGas: faker.number.bigInt(), + }; + mockWeb3Service.calculateTxFees.mockResolvedValueOnce(mockFees); const paidPayoutsRecord = generateEscrowCompletion( EscrowCompletionStatus.PAID, @@ -1042,7 +1052,8 @@ describe('EscrowCompletionService', () => { expect(mockCompleteEscrow).toHaveBeenCalledWith( paidPayoutsRecord.escrowAddress, { - gasPrice: mockGasPrice, + ...mockFees, + timeoutMs: mockWeb3ConfigService.txTimeoutMs, }, ); expect(mockReputationService.assessEscrowParties).toHaveBeenCalledTimes( diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts index 205e915fcc..cf119c2515 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts @@ -16,7 +16,7 @@ import { v4 as uuidv4 } from 'uuid'; import { BACKOFF_INTERVAL_SECONDS } from '@/common/constants'; import { JobManifest, JobRequestType } from '@/common/types'; -import { ServerConfigService } from '@/config'; +import { ServerConfigService, Web3ConfigService } from '@/config'; import { isDuplicatedError } from '@/database'; import logger from '@/logger'; import { ReputationService } from '@/modules/reputation'; @@ -58,6 +58,7 @@ export class EscrowCompletionService { private readonly escrowCompletionRepository: EscrowCompletionRepository, private readonly escrowPayoutsBatchRepository: EscrowPayoutsBatchRepository, private readonly web3Service: Web3Service, + private readonly web3ConfigService: Web3ConfigService, private readonly storageService: StorageService, private readonly outgoingWebhookService: OutgoingWebhookService, private readonly reputationService: ReputationService, @@ -240,13 +241,19 @@ export class EscrowCompletionService { EscrowStatus.ToCancel, ].includes(escrowStatus) ) { - const gasPrice = await this.web3Service.calculateGasPrice(chainId); + const feeOverrides = await this.web3Service.calculateTxFees(chainId); if (escrowStatus === EscrowStatus.ToCancel) { - await escrowClient.cancel(escrowAddress, { gasPrice }); + await escrowClient.cancel(escrowAddress, { + ...feeOverrides, + timeoutMs: this.web3ConfigService.txTimeoutMs, + }); escrowStatus = EscrowStatus.Cancelled; } else { - await escrowClient.complete(escrowAddress, { gasPrice }); + await escrowClient.complete(escrowAddress, { + ...feeOverrides, + timeoutMs: this.web3ConfigService.txTimeoutMs, + }); escrowStatus = EscrowStatus.Complete; } @@ -439,9 +446,9 @@ export class EscrowCompletionService { uuidv4(), // TODO obtain it from intermediate results false, { - gasPrice: await this.web3Service.calculateGasPrice( + ...(await this.web3Service.calculateTxFees( escrowCompletionEntity.chainId, - ), + )), nonce: payoutsBatch.txNonce, }, ); @@ -453,7 +460,10 @@ export class EscrowCompletionService { try { const transactionResponse = await signer.sendTransaction(rawTransaction); - await transactionResponse.wait(); + await transactionResponse.wait( + undefined, + this.web3ConfigService.txTimeoutMs, + ); await this.escrowPayoutsBatchRepository.deleteOne(payoutsBatch); } catch (error) { diff --git a/packages/apps/reputation-oracle/server/src/modules/user/user.service.spec.ts b/packages/apps/reputation-oracle/server/src/modules/user/user.service.spec.ts index be2744f64a..c81002a821 100644 --- a/packages/apps/reputation-oracle/server/src/modules/user/user.service.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/user/user.service.spec.ts @@ -30,7 +30,6 @@ import { } from './user.error'; import { UserRepository } from './user.repository'; import { UserService, OperatorStatus } from './user.service'; - const mockUserRepository = createMock(); const mockSiteKeyRepository = createMock(); const mockHCaptchaService = createMock(); @@ -472,6 +471,7 @@ describe('UserService', () => { expect(mockedKVStoreSet).toHaveBeenCalledWith( user.evmAddress, OperatorStatus.ACTIVE, + { timeoutMs: mockWeb3ConfigService.txTimeoutMs }, ); }); }); @@ -554,6 +554,7 @@ describe('UserService', () => { expect(mockedKVStoreSet).toHaveBeenCalledWith( user.evmAddress, OperatorStatus.INACTIVE, + { timeoutMs: mockWeb3ConfigService.txTimeoutMs }, ); }); }); diff --git a/packages/apps/reputation-oracle/server/src/modules/user/user.service.ts b/packages/apps/reputation-oracle/server/src/modules/user/user.service.ts index 331a6003c0..9c18211859 100644 --- a/packages/apps/reputation-oracle/server/src/modules/user/user.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/user/user.service.ts @@ -222,7 +222,9 @@ export class UserService { ); } - await kvstore.set(operatorUser.evmAddress, OperatorStatus.ACTIVE); + await kvstore.set(operatorUser.evmAddress, OperatorStatus.ACTIVE, { + timeoutMs: this.web3ConfigService.txTimeoutMs, + }); } async disableOperator(userId: number, signature: string): Promise { @@ -266,7 +268,9 @@ export class UserService { ); } - await kvstore.set(operatorUser.evmAddress, OperatorStatus.INACTIVE); + await kvstore.set(operatorUser.evmAddress, OperatorStatus.INACTIVE, { + timeoutMs: this.web3ConfigService.txTimeoutMs, + }); } async registrationInExchangeOracle( diff --git a/packages/apps/reputation-oracle/server/src/modules/web3/fixtures/index.ts b/packages/apps/reputation-oracle/server/src/modules/web3/fixtures/index.ts index fde77f55ed..fe1eff7c1f 100644 --- a/packages/apps/reputation-oracle/server/src/modules/web3/fixtures/index.ts +++ b/packages/apps/reputation-oracle/server/src/modules/web3/fixtures/index.ts @@ -18,6 +18,7 @@ export const mockWeb3ConfigService: Omit = { operatorAddress: testWallet.address, network: Web3Network.TESTNET, gasPriceMultiplier: faker.number.int({ min: 1, max: 42 }), + txTimeoutMs: faker.number.int({ min: 30000, max: 120000 }), reputationNetworkChainId: generateTestnetChainId(), getRpcUrlByChainId: () => faker.internet.url(), }; diff --git a/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.spec.ts b/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.spec.ts index adef07f9e6..f3b58e84d0 100644 --- a/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.spec.ts +++ b/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.spec.ts @@ -54,7 +54,7 @@ describe('Web3Service', () => { }); }); - describe('calculateGasPrice', () => { + describe('calculateTxFees', () => { const mockProvider = createMock(); let spyOnGetSigner: jest.SpyInstance; @@ -75,32 +75,61 @@ describe('Web3Service', () => { mockProvider.getFeeData.mockReset(); }); - it('should use multiplier for gas price', async () => { + it('should use multiplier for transaction fees', async () => { const testChainId = generateTestnetChainId(); - const randomGasPrice = faker.number.bigInt({ min: 1n }); + const randomMaxFeePerGas = faker.number.bigInt(); + const randomMaxPriorityFeePerGas = faker.number.bigInt(); mockProvider.getFeeData.mockResolvedValueOnce({ - gasPrice: randomGasPrice, + maxFeePerGas: randomMaxFeePerGas, + maxPriorityFeePerGas: randomMaxPriorityFeePerGas, } as FeeData); - const gasPrice = await web3Service.calculateGasPrice(testChainId); - - const expectedGasPrice = - randomGasPrice * BigInt(mockWeb3ConfigService.gasPriceMultiplier); - expect(gasPrice).toEqual(expectedGasPrice); + const fees = await web3Service.calculateTxFees(testChainId); + + const expectedMaxFeePerGas = + randomMaxFeePerGas * BigInt(mockWeb3ConfigService.gasPriceMultiplier); + const expectedMaxPriorityFeePerGas = + randomMaxPriorityFeePerGas * + BigInt(mockWeb3ConfigService.gasPriceMultiplier); + expect(fees).toEqual({ + maxFeePerGas: expectedMaxFeePerGas, + maxPriorityFeePerGas: expectedMaxPriorityFeePerGas, + }); }); - it('should throw if no gas price from provider', async () => { + it('should throw if transaction fees are missing', async () => { const testChainId = generateTestnetChainId(); mockProvider.getFeeData.mockResolvedValueOnce({ - gasPrice: null, + maxFeePerGas: null, + maxPriorityFeePerGas: null, } as FeeData); - await expect(web3Service.calculateGasPrice(testChainId)).rejects.toThrow( - `No gas price data for chain id: ${testChainId}`, + await expect(web3Service.calculateTxFees(testChainId)).rejects.toThrow( + `No transaction fee data for chain id: ${testChainId}`, ); }); + + it('should fallback to legacy gasPrice data', async () => { + const testChainId = generateTestnetChainId(); + const randomGasPrice = faker.number.bigInt(); + + mockProvider.getFeeData.mockResolvedValueOnce({ + gasPrice: randomGasPrice, + maxFeePerGas: null, + maxPriorityFeePerGas: null, + } as FeeData); + + const fees = await web3Service.calculateTxFees(testChainId); + const expectedFee = + randomGasPrice * BigInt(mockWeb3ConfigService.gasPriceMultiplier); + + expect(fees).toEqual({ + maxFeePerGas: expectedFee, + maxPriorityFeePerGas: expectedFee, + }); + }); }); }); diff --git a/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.ts b/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.ts index 768196b62a..5b81ab2c6f 100644 --- a/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/web3/web3.service.ts @@ -85,15 +85,26 @@ export class Web3Service { throw new Error(`No signer for provided chain id: ${chainId}`); } - async calculateGasPrice(chainId: number): Promise { + async calculateTxFees(chainId: number): Promise<{ + maxFeePerGas: bigint; + maxPriorityFeePerGas: bigint; + }> { const signer = this.getSigner(chainId); - const { gasPrice } = await signer.provider.getFeeData(); - - if (gasPrice) { - return gasPrice * BigInt(this.web3ConfigService.gasPriceMultiplier); + const feeData = await signer.provider.getFeeData(); + const multiplier = BigInt(this.web3ConfigService.gasPriceMultiplier); + + const maxFeePerGas = feeData.maxFeePerGas ?? feeData.gasPrice; + const maxPriorityFeePerGas = + feeData.maxPriorityFeePerGas ?? feeData.gasPrice; + + if (maxFeePerGas && maxPriorityFeePerGas) { + return { + maxFeePerGas: maxFeePerGas * multiplier, + maxPriorityFeePerGas: maxPriorityFeePerGas * multiplier, + }; } - throw new Error(`No gas price data for chain id: ${chainId}`); + throw new Error(`No transaction fee data for chain id: ${chainId}`); } async getTokenDecimals( diff --git a/packages/apps/staking/eslint.config.mjs b/packages/apps/staking/eslint.config.mjs index 6515b17715..72bf9bf8bd 100644 --- a/packages/apps/staking/eslint.config.mjs +++ b/packages/apps/staking/eslint.config.mjs @@ -29,6 +29,8 @@ export default tseslint.config( 'react-refresh': reactRefreshPlugin, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', 'react-refresh/only-export-components': [ diff --git a/packages/apps/staking/package.json b/packages/apps/staking/package.json index efd250ad55..0401dffed8 100644 --- a/packages/apps/staking/package.json +++ b/packages/apps/staking/package.json @@ -31,7 +31,7 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@human-protocol/sdk": "*", - "@mui/icons-material": "^7.0.1", + "@mui/icons-material": "^7.3.8", "@mui/material": "^5.16.7", "@mui/x-data-grid": "^8.7.0", "@tanstack/query-sync-storage-persister": "^5.68.0", @@ -41,7 +41,7 @@ "ethers": "^6.15.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.24.1", + "react-router-dom": "^7.13.0", "serve": "^14.2.4", "simplebar-react": "^3.3.2", "viem": "2.x", @@ -57,7 +57,7 @@ "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.11", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "sass": "^1.89.2", "typescript": "^5.6.3", "vite": "^6.2.4", diff --git a/packages/core/eslint.config.mjs b/packages/core/eslint.config.mjs index 35f4c1108c..77a2c72261 100644 --- a/packages/core/eslint.config.mjs +++ b/packages/core/eslint.config.mjs @@ -26,6 +26,8 @@ export default tseslint.config( }, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', 'no-console': 'warn', 'prefer-const': 'warn', 'no-extra-semi': 'off', diff --git a/packages/core/package.json b/packages/core/package.json index d23371ccf0..e9db16341a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -80,7 +80,7 @@ "hardhat-dependency-compiler": "^1.2.1", "hardhat-gas-reporter": "^2.0.2", "openpgp": "6.2.2", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "prettier-plugin-solidity": "^1.3.1", "solidity-coverage": "^0.8.17", "tenderly": "^0.9.1", diff --git a/packages/examples/gcv/eslint.config.mjs b/packages/examples/gcv/eslint.config.mjs index df1b4ffb4b..c127f82b7f 100644 --- a/packages/examples/gcv/eslint.config.mjs +++ b/packages/examples/gcv/eslint.config.mjs @@ -30,6 +30,8 @@ const config = tseslint.config( jest: jestPlugin, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/libs/logger/eslint.config.mjs b/packages/libs/logger/eslint.config.mjs index 3dda18fe0c..0e913843d1 100644 --- a/packages/libs/logger/eslint.config.mjs +++ b/packages/libs/logger/eslint.config.mjs @@ -33,6 +33,8 @@ export default tseslint.config( }, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/no-unsafe-argument': 'warn', diff --git a/packages/libs/logger/package.json b/packages/libs/logger/package.json index ca74a9a0ff..89e1b97f86 100644 --- a/packages/libs/logger/package.json +++ b/packages/libs/logger/package.json @@ -20,7 +20,7 @@ "pino-pretty": "^13.1.3" }, "devDependencies": { - "@eslint/js": "^9.30.1", + "@eslint/js": "^10.0.1", "@types/node": "^22.10.5", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.5", @@ -28,7 +28,7 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.5", "globals": "^16.3.0", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "ts-node": "^10.9.2", "typescript": "^5.8.3", "typescript-eslint": "^8.35.1" diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/cancel.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/cancel.py index 223b88aafb..6d5ddf36db 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/cancel.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/cancel.py @@ -70,4 +70,6 @@ def get_cancellation_refund_by_escrow_query(): }} }} {cancellation_refund_fragment} -""".format(cancellation_refund_fragment=cancellation_refund_fragment) +""".format( + cancellation_refund_fragment=cancellation_refund_fragment + ) diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/escrow.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/escrow.py index 5d1b01f6e6..38a0ca8919 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/escrow.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/escrow.py @@ -97,7 +97,9 @@ def get_escrow_query(): }} }} {escrow_fragment} -""".format(escrow_fragment=escrow_fragment) +""".format( + escrow_fragment=escrow_fragment + ) def get_status_query( diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/reward.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/reward.py index 09c06278ae..3f43bbff9e 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/reward.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/reward.py @@ -14,4 +14,6 @@ }} }} {reward_added_event_fragment} -""".format(reward_added_event_fragment=reward_added_event_fragment) +""".format( + reward_added_event_fragment=reward_added_event_fragment +) diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/transaction.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/transaction.py index f17cc61876..ef637424b2 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/transaction.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/transaction.py @@ -103,4 +103,6 @@ def get_transaction_query() -> str: }} }} {transaction_fragment} -""".format(transaction_fragment=transaction_fragment) +""".format( + transaction_fragment=transaction_fragment + ) diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/worker.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/worker.py index ee21af1ef7..9788272612 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/worker.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/worker.py @@ -18,7 +18,9 @@ def get_worker_query() -> str: }} }} {worker_fragment} -""".format(worker_fragment=worker_fragment) +""".format( + worker_fragment=worker_fragment + ) def get_workers_query(filter: WorkerFilter) -> str: diff --git a/packages/sdk/python/human-protocol-sdk/scripts/build-contracts.sh b/packages/sdk/python/human-protocol-sdk/scripts/build-contracts.sh index c83fbb9a4d..448f4bb849 100755 --- a/packages/sdk/python/human-protocol-sdk/scripts/build-contracts.sh +++ b/packages/sdk/python/human-protocol-sdk/scripts/build-contracts.sh @@ -6,5 +6,7 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../../.." && pwd)" yarn --cwd "$REPO_ROOT" workspaces focus @human-protocol/python-sdk +yarn --cwd "$REPO_ROOT" workspaces foreach -Rpt --from @human-protocol/python-sdk run build + rm -rf artifacts cp -r "${REPO_ROOT}/node_modules/@human-protocol/core/artifacts" . diff --git a/packages/sdk/typescript/human-protocol-sdk/eslint.config.mjs b/packages/sdk/typescript/human-protocol-sdk/eslint.config.mjs index 0bf91c4406..ea3fd32317 100644 --- a/packages/sdk/typescript/human-protocol-sdk/eslint.config.mjs +++ b/packages/sdk/typescript/human-protocol-sdk/eslint.config.mjs @@ -31,6 +31,8 @@ export default tseslint.config( jest: jestPlugin, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', 'no-console': 'warn', '@/quotes': [ 'error', diff --git a/packages/sdk/typescript/human-protocol-sdk/package.json b/packages/sdk/typescript/human-protocol-sdk/package.json index 963489d7c0..cc4e18a41c 100644 --- a/packages/sdk/typescript/human-protocol-sdk/package.json +++ b/packages/sdk/typescript/human-protocol-sdk/package.json @@ -55,7 +55,7 @@ "eslint-plugin-jest": "^28.9.0", "eslint-plugin-prettier": "^5.5.5", "glob": "^13.0.0", - "prettier": "^3.7.4", + "prettier": "^3.8.1", "ts-node": "^10.9.2", "typedoc": "^0.28.15", "typedoc-plugin-markdown": "^4.9.0", diff --git a/packages/sdk/typescript/human-protocol-sdk/src/encryption.ts b/packages/sdk/typescript/human-protocol-sdk/src/encryption/encryption.ts similarity index 50% rename from packages/sdk/typescript/human-protocol-sdk/src/encryption.ts rename to packages/sdk/typescript/human-protocol-sdk/src/encryption/encryption.ts index 208f3b9513..0329c1dd25 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/encryption.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/encryption/encryption.ts @@ -1,21 +1,5 @@ import * as openpgp from 'openpgp'; -import { IKeyPair } from './interfaces'; - -/** - * Type representing the data type of a message. - * It can be either a string or a Uint8Array. - * - * @public - */ -export type MessageDataType = string | Uint8Array; - -function makeMessageDataBinary(message: MessageDataType): Uint8Array { - if (typeof message === 'string') { - return Buffer.from(message); - } - - return message; -} +import { makeMessageDataBinary, MessageDataType } from './types'; /** * Class for signing and decrypting messages. @@ -191,179 +175,3 @@ export class Encryption { return cleartextMessage; } } - -/** - * Utility class for encryption-related operations. - */ -export class EncryptionUtils { - /** - * This function verifies the signature of a signed message using the public key. - * - * @param message - Message to verify. - * @param publicKey - Public key to verify that the message was signed by a specific source. - * @returns True if verified. False if not verified. - * - * @example - * ```ts - * import { EncryptionUtils } from '@human-protocol/sdk'; - * - * const publicKey = '-----BEGIN PGP PUBLIC KEY BLOCK-----...'; - * const result = await EncryptionUtils.verify('message', publicKey); - * console.log('Verification result:', result); - * ``` - */ - public static async verify( - message: string, - publicKey: string - ): Promise { - const pgpPublicKey = await openpgp.readKey({ armoredKey: publicKey }); - const signedMessage = await openpgp.readCleartextMessage({ - cleartextMessage: message, - }); - - const verificationResult = await signedMessage.verify([pgpPublicKey]); - const { verified } = verificationResult[0]; - - try { - return await verified; - } catch { - return false; - } - } - - /** - * This function gets signed data from a signed message. - * - * @param message - Message. - * @returns Signed data. - * @throws Error If data could not be extracted from the message - * - * @example - * ```ts - * import { EncryptionUtils } from '@human-protocol/sdk'; - * - * const signedData = await EncryptionUtils.getSignedData('message'); - * console.log('Signed data:', signedData); - * ``` - */ - public static async getSignedData(message: string): Promise { - const signedMessage = await openpgp.readCleartextMessage({ - cleartextMessage: message, - }); - - try { - return signedMessage.getText(); - } catch (e) { - throw new Error('Could not get data: ' + e.message); - } - } - - /** - * This function generates a key pair for encryption and decryption. - * - * @param name - Name for the key pair. - * @param email - Email for the key pair. - * @param passphrase - Passphrase to encrypt the private key (optional, defaults to empty string). - * @returns Key pair generated. - * - * @example - * ```ts - * import { EncryptionUtils } from '@human-protocol/sdk'; - * - * const name = 'YOUR_NAME'; - * const email = 'YOUR_EMAIL'; - * const passphrase = 'YOUR_PASSPHRASE'; - * const keyPair = await EncryptionUtils.generateKeyPair(name, email, passphrase); - * console.log('Public key:', keyPair.publicKey); - * ``` - */ - public static async generateKeyPair( - name: string, - email: string, - passphrase = '' - ): Promise { - const { privateKey, publicKey, revocationCertificate } = - await openpgp.generateKey({ - type: 'ecc', - curve: 'ed25519Legacy', - userIDs: [{ name: name, email: email }], - passphrase: passphrase, - format: 'armored', - }); - - return { - passphrase: passphrase, - privateKey, - publicKey, - revocationCertificate, - }; - } - - /** - * This function encrypts a message using the specified public keys. - * - * @param message - Message to encrypt. - * @param publicKeys - Array of public keys to use for encryption. - * @returns Message encrypted. - * - * @example - * ```ts - * import { EncryptionUtils } from '@human-protocol/sdk'; - * - * const publicKey1 = '-----BEGIN PGP PUBLIC KEY BLOCK-----...'; - * const publicKey2 = '-----BEGIN PGP PUBLIC KEY BLOCK-----...'; - * const publicKeys = [publicKey1, publicKey2]; - * const encryptedMessage = await EncryptionUtils.encrypt('message', publicKeys); - * console.log('Encrypted message:', encryptedMessage); - * ``` - */ - public static async encrypt( - message: MessageDataType, - publicKeys: string[] - ): Promise { - const pgpPublicKeys = await Promise.all( - publicKeys.map((armoredKey) => openpgp.readKey({ armoredKey })) - ); - - const pgpMessage = await openpgp.createMessage({ - binary: makeMessageDataBinary(message), - }); - const encrypted = await openpgp.encrypt({ - message: pgpMessage, - encryptionKeys: pgpPublicKeys, - format: 'armored', - }); - - return encrypted as string; - } - - /** - * Verifies if a message appears to be encrypted with OpenPGP. - * - * @param message - Message to verify. - * @returns `true` if the message appears to be encrypted, `false` if not. - * - * @example - * ```ts - * import { EncryptionUtils } from '@human-protocol/sdk'; - * - * const message = '-----BEGIN PGP MESSAGE-----...'; - * const isEncrypted = EncryptionUtils.isEncrypted(message); - * - * if (isEncrypted) { - * console.log('The message is encrypted with OpenPGP.'); - * } else { - * console.log('The message is not encrypted with OpenPGP.'); - * } - * ``` - */ - public static isEncrypted(message: string): boolean { - const startMarker = '-----BEGIN PGP MESSAGE-----'; - const endMarker = '-----END PGP MESSAGE-----'; - - const hasStartMarker = message.includes(startMarker); - const hasEndMarker = message.includes(endMarker); - - return hasStartMarker && hasEndMarker; - } -} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/encryption/encryption_utils.ts b/packages/sdk/typescript/human-protocol-sdk/src/encryption/encryption_utils.ts new file mode 100644 index 0000000000..0d75eaf028 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/encryption/encryption_utils.ts @@ -0,0 +1,179 @@ +import * as openpgp from 'openpgp'; +import { IKeyPair } from '../interfaces'; +import { makeMessageDataBinary, MessageDataType } from './types'; + +/** + * Utility class for encryption-related operations. + */ +export class EncryptionUtils { + /** + * This function verifies the signature of a signed message using the public key. + * + * @param message - Message to verify. + * @param publicKey - Public key to verify that the message was signed by a specific source. + * @returns True if verified. False if not verified. + * + * @example + * ```ts + * import { EncryptionUtils } from '@human-protocol/sdk'; + * + * const publicKey = '-----BEGIN PGP PUBLIC KEY BLOCK-----...'; + * const result = await EncryptionUtils.verify('message', publicKey); + * console.log('Verification result:', result); + * ``` + */ + public static async verify( + message: string, + publicKey: string + ): Promise { + const pgpPublicKey = await openpgp.readKey({ armoredKey: publicKey }); + const signedMessage = await openpgp.readCleartextMessage({ + cleartextMessage: message, + }); + + const verificationResult = await signedMessage.verify([pgpPublicKey]); + const { verified } = verificationResult[0]; + + try { + return await verified; + } catch { + return false; + } + } + + /** + * This function gets signed data from a signed message. + * + * @param message - Message. + * @returns Signed data. + * @throws Error If data could not be extracted from the message + * + * @example + * ```ts + * import { EncryptionUtils } from '@human-protocol/sdk'; + * + * const signedData = await EncryptionUtils.getSignedData('message'); + * console.log('Signed data:', signedData); + * ``` + */ + public static async getSignedData(message: string): Promise { + const signedMessage = await openpgp.readCleartextMessage({ + cleartextMessage: message, + }); + + try { + return signedMessage.getText(); + } catch (e) { + throw new Error('Could not get data: ' + e.message); + } + } + + /** + * This function generates a key pair for encryption and decryption. + * + * @param name - Name for the key pair. + * @param email - Email for the key pair. + * @param passphrase - Passphrase to encrypt the private key (optional, defaults to empty string). + * @returns Key pair generated. + * + * @example + * ```ts + * import { EncryptionUtils } from '@human-protocol/sdk'; + * + * const name = 'YOUR_NAME'; + * const email = 'YOUR_EMAIL'; + * const passphrase = 'YOUR_PASSPHRASE'; + * const keyPair = await EncryptionUtils.generateKeyPair(name, email, passphrase); + * console.log('Public key:', keyPair.publicKey); + * ``` + */ + public static async generateKeyPair( + name: string, + email: string, + passphrase = '' + ): Promise { + const { privateKey, publicKey, revocationCertificate } = + await openpgp.generateKey({ + type: 'ecc', + curve: 'ed25519Legacy', + userIDs: [{ name: name, email: email }], + passphrase: passphrase, + format: 'armored', + }); + + return { + passphrase: passphrase, + privateKey, + publicKey, + revocationCertificate, + }; + } + + /** + * This function encrypts a message using the specified public keys. + * + * @param message - Message to encrypt. + * @param publicKeys - Array of public keys to use for encryption. + * @returns Message encrypted. + * + * @example + * ```ts + * import { EncryptionUtils } from '@human-protocol/sdk'; + * + * const publicKey1 = '-----BEGIN PGP PUBLIC KEY BLOCK-----...'; + * const publicKey2 = '-----BEGIN PGP PUBLIC KEY BLOCK-----...'; + * const publicKeys = [publicKey1, publicKey2]; + * const encryptedMessage = await EncryptionUtils.encrypt('message', publicKeys); + * console.log('Encrypted message:', encryptedMessage); + * ``` + */ + public static async encrypt( + message: MessageDataType, + publicKeys: string[] + ): Promise { + const pgpPublicKeys = await Promise.all( + publicKeys.map((armoredKey) => openpgp.readKey({ armoredKey })) + ); + + const pgpMessage = await openpgp.createMessage({ + binary: makeMessageDataBinary(message), + }); + const encrypted = await openpgp.encrypt({ + message: pgpMessage, + encryptionKeys: pgpPublicKeys, + format: 'armored', + }); + + return encrypted as string; + } + + /** + * Verifies if a message appears to be encrypted with OpenPGP. + * + * @param message - Message to verify. + * @returns `true` if the message appears to be encrypted, `false` if not. + * + * @example + * ```ts + * import { EncryptionUtils } from '@human-protocol/sdk'; + * + * const message = '-----BEGIN PGP MESSAGE-----...'; + * const isEncrypted = EncryptionUtils.isEncrypted(message); + * + * if (isEncrypted) { + * console.log('The message is encrypted with OpenPGP.'); + * } else { + * console.log('The message is not encrypted with OpenPGP.'); + * } + * ``` + */ + public static isEncrypted(message: string): boolean { + const startMarker = '-----BEGIN PGP MESSAGE-----'; + const endMarker = '-----END PGP MESSAGE-----'; + + const hasStartMarker = message.includes(startMarker); + const hasEndMarker = message.includes(endMarker); + + return hasStartMarker && hasEndMarker; + } +} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/encryption/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/encryption/index.ts new file mode 100644 index 0000000000..78d80c0783 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/encryption/index.ts @@ -0,0 +1,3 @@ +export { Encryption } from './encryption'; +export { EncryptionUtils } from './encryption_utils'; +export type { MessageDataType } from './types'; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/encryption/types.ts b/packages/sdk/typescript/human-protocol-sdk/src/encryption/types.ts new file mode 100644 index 0000000000..7d7ee473c6 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/encryption/types.ts @@ -0,0 +1,15 @@ +/** + * Type representing the data type of a message. + * It can be either a string or a Uint8Array. + * + * @public + */ +export type MessageDataType = string | Uint8Array; + +export function makeMessageDataBinary(message: MessageDataType): Uint8Array { + if (typeof message === 'string') { + return Buffer.from(message); + } + + return message; +} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/error.ts b/packages/sdk/typescript/human-protocol-sdk/src/error.ts index f5cbe8fd4c..7371a6baa3 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/error.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/error.ts @@ -349,3 +349,17 @@ export class InvalidKeyError extends Error { super(`Key "${key}" not found for address ${address}`); } } + +export class SubgraphRequestError extends Error { + public readonly statusCode?: number; + public readonly url: string; + + constructor(message: string, url: string, statusCode?: number) { + super(message); + this.name = this.constructor.name; + this.url = url; + this.statusCode = statusCode; + } +} + +export class SubgraphBadIndexerError extends SubgraphRequestError {} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/escrow.ts b/packages/sdk/typescript/human-protocol-sdk/src/escrow/escrow_client.ts similarity index 77% rename from packages/sdk/typescript/human-protocol-sdk/src/escrow.ts rename to packages/sdk/typescript/human-protocol-sdk/src/escrow/escrow_client.ts index 8b535a74ac..63e0eb72fc 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/escrow.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/escrow/escrow_client.ts @@ -10,10 +10,10 @@ import { HMToken__factory, } from '@human-protocol/core/typechain-types'; import { ContractRunner, EventLog, Overrides, Signer, ethers } from 'ethers'; -import { BaseEthersClient } from './base'; -import { ESCROW_BULK_PAYOUT_MAX_ITEMS, NETWORKS } from './constants'; -import { requiresSigner } from './decorators'; -import { ChainId, OrderDirection } from './enums'; +import { BaseEthersClient } from '../base'; +import { ESCROW_BULK_PAYOUT_MAX_ITEMS, NETWORKS } from '../constants'; +import { requiresSigner } from '../decorators'; +import { ChainId } from '../enums'; import { ErrorAmountMustBeGreaterThanZero, ErrorAmountsCannotBeEmptyArray, @@ -21,7 +21,6 @@ import { ErrorEscrowAddressIsNotProvidedByFactory, ErrorEscrowDoesNotHaveEnoughBalance, ErrorHashIsEmptyString, - ErrorInvalidAddress, ErrorInvalidEscrowAddressProvided, ErrorInvalidExchangeOracleAddressProvided, ErrorInvalidManifest, @@ -40,46 +39,15 @@ import { ErrorUnsupportedChainID, InvalidEthereumAddressError, WarnVersionMismatch, -} from './error'; -import { - CancellationRefundData, - EscrowData, - GET_CANCELLATION_REFUNDS_QUERY, - GET_CANCELLATION_REFUND_BY_ADDRESS_QUERY, - GET_ESCROWS_QUERY, - GET_ESCROW_BY_ADDRESS_QUERY, - GET_PAYOUTS_QUERY, - GET_STATUS_UPDATES_QUERY, - PayoutData, - StatusEvent, -} from './graphql'; -import { - IEscrow, - IEscrowConfig, - IEscrowsFilter, - IPayoutFilter, - IStatusEventFilter, - IStatusEvent, - ICancellationRefund, - ICancellationRefundFilter, - IPayout, - IEscrowWithdraw, - SubgraphOptions, -} from './interfaces'; +} from '../error'; +import { IEscrowConfig, IEscrowWithdraw } from '../interfaces'; import { EscrowStatus, NetworkData, TransactionLikeWithNonce, TransactionOverrides, -} from './types'; -import { - getSubgraphUrl, - getUnixTimestamp, - customGqlFetch, - isValidJson, - isValidUrl, - throwError, -} from './utils'; +} from '../types'; +import { isValidJson, isValidUrl, throwError } from '../utils'; /** * Client to perform actions on Escrow contracts and obtain information from the contracts. @@ -1682,480 +1650,3 @@ export class EscrowClient extends BaseEthersClient { } } } -/** - * Utility helpers for escrow-related queries. - * - * @example - * ```ts - * import { ChainId, EscrowUtils } from '@human-protocol/sdk'; - * - * const escrows = await EscrowUtils.getEscrows({ - * chainId: ChainId.POLYGON_AMOY - * }); - * console.log('Escrows:', escrows); - * ``` - */ -export class EscrowUtils { - /** - * This function returns an array of escrows based on the specified filter parameters. - * - * @param filter - Filter parameters. - * @param options - Optional configuration for subgraph requests. - * @returns List of escrows that match the filter. - * @throws ErrorInvalidAddress If any filter address is invalid - * @throws ErrorUnsupportedChainID If the chain ID is not supported - * - * @example - * ```ts - * import { ChainId, EscrowStatus } from '@human-protocol/sdk'; - * - * const filters = { - * status: EscrowStatus.Pending, - * from: new Date(2023, 4, 8), - * to: new Date(2023, 5, 8), - * chainId: ChainId.POLYGON_AMOY - * }; - * const escrows = await EscrowUtils.getEscrows(filters); - * console.log('Found escrows:', escrows.length); - * ``` - */ - public static async getEscrows( - filter: IEscrowsFilter, - options?: SubgraphOptions - ): Promise { - if (filter.launcher && !ethers.isAddress(filter.launcher)) { - throw ErrorInvalidAddress; - } - - if (filter.recordingOracle && !ethers.isAddress(filter.recordingOracle)) { - throw ErrorInvalidAddress; - } - - if (filter.reputationOracle && !ethers.isAddress(filter.reputationOracle)) { - throw ErrorInvalidAddress; - } - - if (filter.exchangeOracle && !ethers.isAddress(filter.exchangeOracle)) { - throw ErrorInvalidAddress; - } - - const first = - filter.first !== undefined ? Math.min(filter.first, 1000) : 10; - const skip = filter.skip || 0; - const orderDirection = filter.orderDirection || OrderDirection.DESC; - - const networkData = NETWORKS[filter.chainId]; - - if (!networkData) { - throw ErrorUnsupportedChainID; - } - - let statuses; - if (filter.status !== undefined) { - statuses = Array.isArray(filter.status) ? filter.status : [filter.status]; - statuses = statuses.map((status) => EscrowStatus[status]); - } - const { escrows } = await customGqlFetch<{ escrows: EscrowData[] }>( - getSubgraphUrl(networkData), - GET_ESCROWS_QUERY(filter), - { - ...filter, - launcher: filter.launcher?.toLowerCase(), - reputationOracle: filter.reputationOracle?.toLowerCase(), - recordingOracle: filter.recordingOracle?.toLowerCase(), - exchangeOracle: filter.exchangeOracle?.toLowerCase(), - status: statuses, - from: filter.from ? getUnixTimestamp(filter.from) : undefined, - to: filter.to ? getUnixTimestamp(filter.to) : undefined, - orderDirection: orderDirection, - first: first, - skip: skip, - }, - options - ); - return (escrows || []).map((e) => mapEscrow(e, networkData.chainId)); - } - - /** - * This function returns the escrow data for a given address. - * - * > This uses Subgraph - * - * @param chainId - Network in which the escrow has been deployed - * @param escrowAddress - Address of the escrow - * @param options - Optional configuration for subgraph requests. - * @returns Escrow data or null if not found. - * @throws ErrorUnsupportedChainID If the chain ID is not supported - * @throws ErrorInvalidAddress If the escrow address is invalid - * - * @example - * ```ts - * import { ChainId } from '@human-protocol/sdk'; - * - * const escrow = await EscrowUtils.getEscrow( - * ChainId.POLYGON_AMOY, - * "0x1234567890123456789012345678901234567890" - * ); - * if (escrow) { - * console.log('Escrow status:', escrow.status); - * } - * ``` - */ - public static async getEscrow( - chainId: ChainId, - escrowAddress: string, - options?: SubgraphOptions - ): Promise { - const networkData = NETWORKS[chainId]; - - if (!networkData) { - throw ErrorUnsupportedChainID; - } - - if (escrowAddress && !ethers.isAddress(escrowAddress)) { - throw ErrorInvalidAddress; - } - - const { escrow } = await customGqlFetch<{ escrow: EscrowData | null }>( - getSubgraphUrl(networkData), - GET_ESCROW_BY_ADDRESS_QUERY(), - { escrowAddress: escrowAddress.toLowerCase() }, - options - ); - if (!escrow) return null; - - return mapEscrow(escrow, networkData.chainId); - } - - /** - * This function returns the status events for a given set of networks within an optional date range. - * - * > This uses Subgraph - * - * @param filter - Filter parameters. - * @param options - Optional configuration for subgraph requests. - * @returns Array of status events with their corresponding statuses. - * @throws ErrorInvalidAddress If the launcher address is invalid - * @throws ErrorUnsupportedChainID If the chain ID is not supported - * - * @example - * ```ts - * import { ChainId, EscrowStatus } from '@human-protocol/sdk'; - * - * const fromDate = new Date('2023-01-01'); - * const toDate = new Date('2023-12-31'); - * const statusEvents = await EscrowUtils.getStatusEvents({ - * chainId: ChainId.POLYGON, - * statuses: [EscrowStatus.Pending, EscrowStatus.Complete], - * from: fromDate, - * to: toDate - * }); - * console.log('Status events:', statusEvents.length); - * ``` - */ - public static async getStatusEvents( - filter: IStatusEventFilter, - options?: SubgraphOptions - ): Promise { - const { - chainId, - statuses, - from, - to, - launcher, - first = 10, - skip = 0, - orderDirection = OrderDirection.DESC, - } = filter; - - if (launcher && !ethers.isAddress(launcher)) { - throw ErrorInvalidAddress; - } - - const networkData = NETWORKS[chainId]; - if (!networkData) { - throw ErrorUnsupportedChainID; - } - - // If statuses are not provided, use all statuses except Launched - const effectiveStatuses = statuses ?? [ - EscrowStatus.Launched, - EscrowStatus.Pending, - EscrowStatus.Partial, - EscrowStatus.Paid, - EscrowStatus.Complete, - EscrowStatus.Cancelled, - ]; - - const statusNames = effectiveStatuses.map((status) => EscrowStatus[status]); - - const data = await customGqlFetch<{ - escrowStatusEvents: StatusEvent[]; - }>( - getSubgraphUrl(networkData), - GET_STATUS_UPDATES_QUERY(from, to, launcher), - { - status: statusNames, - from: from ? getUnixTimestamp(from) : undefined, - to: to ? getUnixTimestamp(to) : undefined, - launcher: launcher || undefined, - orderDirection, - first: Math.min(first, 1000), - skip, - }, - options - ); - - if (!data || !data['escrowStatusEvents']) { - return []; - } - - return data['escrowStatusEvents'].map((event) => ({ - timestamp: Number(event.timestamp) * 1000, - escrowAddress: event.escrowAddress, - status: EscrowStatus[event.status as keyof typeof EscrowStatus], - chainId, - })); - } - - /** - * This function returns the payouts for a given set of networks. - * - * > This uses Subgraph - * - * @param filter - Filter parameters. - * @param options - Optional configuration for subgraph requests. - * @returns List of payouts matching the filters. - * @throws ErrorUnsupportedChainID If the chain ID is not supported - * @throws ErrorInvalidAddress If any filter address is invalid - * - * @example - * ```ts - * import { ChainId } from '@human-protocol/sdk'; - * - * const payouts = await EscrowUtils.getPayouts({ - * chainId: ChainId.POLYGON, - * escrowAddress: '0x1234567890123456789012345678901234567890', - * recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef', - * from: new Date('2023-01-01'), - * to: new Date('2023-12-31') - * }); - * console.log('Payouts:', payouts.length); - * ``` - */ - public static async getPayouts( - filter: IPayoutFilter, - options?: SubgraphOptions - ): Promise { - const networkData = NETWORKS[filter.chainId]; - if (!networkData) { - throw ErrorUnsupportedChainID; - } - if (filter.escrowAddress && !ethers.isAddress(filter.escrowAddress)) { - throw ErrorInvalidAddress; - } - if (filter.recipient && !ethers.isAddress(filter.recipient)) { - throw ErrorInvalidAddress; - } - - const first = - filter.first !== undefined ? Math.min(filter.first, 1000) : 10; - const skip = filter.skip || 0; - const orderDirection = filter.orderDirection || OrderDirection.DESC; - - const { payouts } = await customGqlFetch<{ payouts: PayoutData[] }>( - getSubgraphUrl(networkData), - GET_PAYOUTS_QUERY(filter), - { - escrowAddress: filter.escrowAddress?.toLowerCase(), - recipient: filter.recipient?.toLowerCase(), - from: filter.from ? getUnixTimestamp(filter.from) : undefined, - to: filter.to ? getUnixTimestamp(filter.to) : undefined, - first: Math.min(first, 1000), - skip, - orderDirection, - }, - options - ); - if (!payouts) { - return []; - } - - return payouts.map((payout) => ({ - id: payout.id, - escrowAddress: payout.escrowAddress, - recipient: payout.recipient, - amount: BigInt(payout.amount), - createdAt: Number(payout.createdAt) * 1000, - })); - } - - /** - * This function returns the cancellation refunds for a given set of networks. - * - * > This uses Subgraph - * - * @param filter - Filter parameters. - * @param options - Optional configuration for subgraph requests. - * @returns List of cancellation refunds matching the filters. - * @throws ErrorUnsupportedChainID If the chain ID is not supported - * @throws ErrorInvalidEscrowAddressProvided If the escrow address is invalid - * @throws ErrorInvalidAddress If the receiver address is invalid - * - * @example - * ```ts - * import { ChainId } from '@human-protocol/sdk'; - * - * const cancellationRefunds = await EscrowUtils.getCancellationRefunds({ - * chainId: ChainId.POLYGON_AMOY, - * escrowAddress: '0x1234567890123456789012345678901234567890', - * }); - * console.log('Cancellation refunds:', cancellationRefunds.length); - * ``` - */ - public static async getCancellationRefunds( - filter: ICancellationRefundFilter, - options?: SubgraphOptions - ): Promise { - const networkData = NETWORKS[filter.chainId]; - if (!networkData) throw ErrorUnsupportedChainID; - if (filter.escrowAddress && !ethers.isAddress(filter.escrowAddress)) { - throw ErrorInvalidEscrowAddressProvided; - } - if (filter.receiver && !ethers.isAddress(filter.receiver)) { - throw ErrorInvalidAddress; - } - - const first = - filter.first !== undefined ? Math.min(filter.first, 1000) : 10; - const skip = filter.skip || 0; - const orderDirection = filter.orderDirection || OrderDirection.DESC; - - const { cancellationRefundEvents } = await customGqlFetch<{ - cancellationRefundEvents: CancellationRefundData[]; - }>( - getSubgraphUrl(networkData), - GET_CANCELLATION_REFUNDS_QUERY(filter), - { - escrowAddress: filter.escrowAddress?.toLowerCase(), - receiver: filter.receiver?.toLowerCase(), - from: filter.from ? getUnixTimestamp(filter.from) : undefined, - to: filter.to ? getUnixTimestamp(filter.to) : undefined, - first, - skip, - orderDirection, - }, - options - ); - - if (!cancellationRefundEvents || cancellationRefundEvents.length === 0) { - return []; - } - - return cancellationRefundEvents.map((event) => ({ - id: event.id, - escrowAddress: event.escrowAddress, - receiver: event.receiver, - amount: BigInt(event.amount), - block: Number(event.block), - timestamp: Number(event.timestamp) * 1000, - txHash: event.txHash, - })); - } - - /** - * This function returns the cancellation refund for a given escrow address. - * - * > This uses Subgraph - * - * @param chainId - Network in which the escrow has been deployed - * @param escrowAddress - Address of the escrow - * @param options - Optional configuration for subgraph requests. - * @returns Cancellation refund data or null if not found. - * @throws ErrorUnsupportedChainID If the chain ID is not supported - * @throws ErrorInvalidEscrowAddressProvided If the escrow address is invalid - * - * @example - * ```ts - * import { ChainId } from '@human-protocol/sdk'; - * - * - * const cancellationRefund = await EscrowUtils.getCancellationRefund( - * ChainId.POLYGON_AMOY, - * "0x1234567890123456789012345678901234567890" - * ); - * if (cancellationRefund) { - * console.log('Refund amount:', cancellationRefund.amount); - * } - * ``` - */ - public static async getCancellationRefund( - chainId: ChainId, - escrowAddress: string, - options?: SubgraphOptions - ): Promise { - const networkData = NETWORKS[chainId]; - if (!networkData) throw ErrorUnsupportedChainID; - - if (!ethers.isAddress(escrowAddress)) { - throw ErrorInvalidEscrowAddressProvided; - } - - const { cancellationRefundEvents } = await customGqlFetch<{ - cancellationRefundEvents: CancellationRefundData[]; - }>( - getSubgraphUrl(networkData), - GET_CANCELLATION_REFUND_BY_ADDRESS_QUERY(), - { escrowAddress: escrowAddress.toLowerCase() }, - options - ); - - if (!cancellationRefundEvents || cancellationRefundEvents.length === 0) { - return null; - } - - return { - id: cancellationRefundEvents[0].id, - escrowAddress: cancellationRefundEvents[0].escrowAddress, - receiver: cancellationRefundEvents[0].receiver, - amount: BigInt(cancellationRefundEvents[0].amount), - block: Number(cancellationRefundEvents[0].block), - timestamp: Number(cancellationRefundEvents[0].timestamp) * 1000, - txHash: cancellationRefundEvents[0].txHash, - }; - } -} - -function mapEscrow(e: EscrowData, chainId: ChainId | number): IEscrow { - return { - id: e.id, - address: e.address, - amountPaid: BigInt(e.amountPaid), - balance: BigInt(e.balance), - count: Number(e.count), - factoryAddress: e.factoryAddress, - finalResultsUrl: e.finalResultsUrl, - finalResultsHash: e.finalResultsHash, - intermediateResultsUrl: e.intermediateResultsUrl, - intermediateResultsHash: e.intermediateResultsHash, - launcher: e.launcher, - jobRequesterId: e.jobRequesterId, - manifestHash: e.manifestHash, - manifest: e.manifest, - recordingOracle: e.recordingOracle, - reputationOracle: e.reputationOracle, - exchangeOracle: e.exchangeOracle, - recordingOracleFee: e.recordingOracleFee - ? Number(e.recordingOracleFee) - : null, - reputationOracleFee: e.reputationOracleFee - ? Number(e.reputationOracleFee) - : null, - exchangeOracleFee: e.exchangeOracleFee ? Number(e.exchangeOracleFee) : null, - status: e.status, - token: e.token, - totalFundedAmount: BigInt(e.totalFundedAmount), - createdAt: Number(e.createdAt) * 1000, - chainId: Number(chainId), - }; -} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/escrow/escrow_utils.ts b/packages/sdk/typescript/human-protocol-sdk/src/escrow/escrow_utils.ts new file mode 100644 index 0000000000..7d3639cdfa --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/escrow/escrow_utils.ts @@ -0,0 +1,510 @@ +import { ethers } from 'ethers'; +import { NETWORKS } from '../constants'; +import { ChainId, OrderDirection } from '../enums'; +import { + ErrorInvalidAddress, + ErrorInvalidEscrowAddressProvided, + ErrorUnsupportedChainID, +} from '../error'; +import { + CancellationRefundData, + EscrowData, + GET_CANCELLATION_REFUNDS_QUERY, + GET_CANCELLATION_REFUND_BY_ADDRESS_QUERY, + GET_ESCROWS_QUERY, + GET_ESCROW_BY_ADDRESS_QUERY, + GET_PAYOUTS_QUERY, + GET_STATUS_UPDATES_QUERY, + PayoutData, + StatusEvent, +} from '../graphql'; +import { + ICancellationRefund, + ICancellationRefundFilter, + IEscrow, + IEscrowsFilter, + IPayout, + IPayoutFilter, + IStatusEvent, + IStatusEventFilter, + SubgraphOptions, +} from '../interfaces'; +import { EscrowStatus } from '../types'; +import { customGqlFetch, getSubgraphUrl, getUnixTimestamp } from '../utils'; +/** + * Utility helpers for escrow-related queries. + * + * @example + * ```ts + * import { ChainId, EscrowUtils } from '@human-protocol/sdk'; + * + * const escrows = await EscrowUtils.getEscrows({ + * chainId: ChainId.POLYGON_AMOY + * }); + * console.log('Escrows:', escrows); + * ``` + */ +export class EscrowUtils { + /** + * This function returns an array of escrows based on the specified filter parameters. + * + * @param filter - Filter parameters. + * @param options - Optional configuration for subgraph requests. + * @returns List of escrows that match the filter. + * @throws ErrorInvalidAddress If any filter address is invalid + * @throws ErrorUnsupportedChainID If the chain ID is not supported + * + * @example + * ```ts + * import { ChainId, EscrowStatus } from '@human-protocol/sdk'; + * + * const filters = { + * status: EscrowStatus.Pending, + * from: new Date(2023, 4, 8), + * to: new Date(2023, 5, 8), + * chainId: ChainId.POLYGON_AMOY + * }; + * const escrows = await EscrowUtils.getEscrows(filters); + * console.log('Found escrows:', escrows.length); + * ``` + */ + public static async getEscrows( + filter: IEscrowsFilter, + options?: SubgraphOptions + ): Promise { + if (filter.launcher && !ethers.isAddress(filter.launcher)) { + throw ErrorInvalidAddress; + } + + if (filter.recordingOracle && !ethers.isAddress(filter.recordingOracle)) { + throw ErrorInvalidAddress; + } + + if (filter.reputationOracle && !ethers.isAddress(filter.reputationOracle)) { + throw ErrorInvalidAddress; + } + + if (filter.exchangeOracle && !ethers.isAddress(filter.exchangeOracle)) { + throw ErrorInvalidAddress; + } + + const first = + filter.first !== undefined ? Math.min(filter.first, 1000) : 10; + const skip = filter.skip || 0; + const orderDirection = filter.orderDirection || OrderDirection.DESC; + + const networkData = NETWORKS[filter.chainId]; + + if (!networkData) { + throw ErrorUnsupportedChainID; + } + + let statuses; + if (filter.status !== undefined) { + statuses = Array.isArray(filter.status) ? filter.status : [filter.status]; + statuses = statuses.map((status) => EscrowStatus[status]); + } + const { escrows } = await customGqlFetch<{ escrows: EscrowData[] }>( + getSubgraphUrl(networkData), + GET_ESCROWS_QUERY(filter), + { + ...filter, + launcher: filter.launcher?.toLowerCase(), + reputationOracle: filter.reputationOracle?.toLowerCase(), + recordingOracle: filter.recordingOracle?.toLowerCase(), + exchangeOracle: filter.exchangeOracle?.toLowerCase(), + status: statuses, + from: filter.from ? getUnixTimestamp(filter.from) : undefined, + to: filter.to ? getUnixTimestamp(filter.to) : undefined, + orderDirection: orderDirection, + first: first, + skip: skip, + }, + options + ); + return (escrows || []).map((e) => mapEscrow(e, networkData.chainId)); + } + + /** + * This function returns the escrow data for a given address. + * + * > This uses Subgraph + * + * @param chainId - Network in which the escrow has been deployed + * @param escrowAddress - Address of the escrow + * @param options - Optional configuration for subgraph requests. + * @returns Escrow data or null if not found. + * @throws ErrorUnsupportedChainID If the chain ID is not supported + * @throws ErrorInvalidAddress If the escrow address is invalid + * + * @example + * ```ts + * import { ChainId } from '@human-protocol/sdk'; + * + * const escrow = await EscrowUtils.getEscrow( + * ChainId.POLYGON_AMOY, + * "0x1234567890123456789012345678901234567890" + * ); + * if (escrow) { + * console.log('Escrow status:', escrow.status); + * } + * ``` + */ + public static async getEscrow( + chainId: ChainId, + escrowAddress: string, + options?: SubgraphOptions + ): Promise { + const networkData = NETWORKS[chainId]; + + if (!networkData) { + throw ErrorUnsupportedChainID; + } + + if (escrowAddress && !ethers.isAddress(escrowAddress)) { + throw ErrorInvalidAddress; + } + + const { escrow } = await customGqlFetch<{ escrow: EscrowData | null }>( + getSubgraphUrl(networkData), + GET_ESCROW_BY_ADDRESS_QUERY(), + { escrowAddress: escrowAddress.toLowerCase() }, + options + ); + if (!escrow) return null; + + return mapEscrow(escrow, networkData.chainId); + } + + /** + * This function returns the status events for a given set of networks within an optional date range. + * + * > This uses Subgraph + * + * @param filter - Filter parameters. + * @param options - Optional configuration for subgraph requests. + * @returns Array of status events with their corresponding statuses. + * @throws ErrorInvalidAddress If the launcher address is invalid + * @throws ErrorUnsupportedChainID If the chain ID is not supported + * + * @example + * ```ts + * import { ChainId, EscrowStatus } from '@human-protocol/sdk'; + * + * const fromDate = new Date('2023-01-01'); + * const toDate = new Date('2023-12-31'); + * const statusEvents = await EscrowUtils.getStatusEvents({ + * chainId: ChainId.POLYGON, + * statuses: [EscrowStatus.Pending, EscrowStatus.Complete], + * from: fromDate, + * to: toDate + * }); + * console.log('Status events:', statusEvents.length); + * ``` + */ + public static async getStatusEvents( + filter: IStatusEventFilter, + options?: SubgraphOptions + ): Promise { + const { + chainId, + statuses, + from, + to, + launcher, + first = 10, + skip = 0, + orderDirection = OrderDirection.DESC, + } = filter; + + if (launcher && !ethers.isAddress(launcher)) { + throw ErrorInvalidAddress; + } + + const networkData = NETWORKS[chainId]; + if (!networkData) { + throw ErrorUnsupportedChainID; + } + + // If statuses are not provided, use all statuses except Launched + const effectiveStatuses = statuses ?? [ + EscrowStatus.Launched, + EscrowStatus.Pending, + EscrowStatus.Partial, + EscrowStatus.Paid, + EscrowStatus.Complete, + EscrowStatus.Cancelled, + ]; + + const statusNames = effectiveStatuses.map((status) => EscrowStatus[status]); + + const data = await customGqlFetch<{ + escrowStatusEvents: StatusEvent[]; + }>( + getSubgraphUrl(networkData), + GET_STATUS_UPDATES_QUERY(from, to, launcher), + { + status: statusNames, + from: from ? getUnixTimestamp(from) : undefined, + to: to ? getUnixTimestamp(to) : undefined, + launcher: launcher || undefined, + orderDirection, + first: Math.min(first, 1000), + skip, + }, + options + ); + + if (!data || !data['escrowStatusEvents']) { + return []; + } + + return data['escrowStatusEvents'].map((event) => ({ + timestamp: Number(event.timestamp) * 1000, + escrowAddress: event.escrowAddress, + status: EscrowStatus[event.status as keyof typeof EscrowStatus], + chainId, + })); + } + + /** + * This function returns the payouts for a given set of networks. + * + * > This uses Subgraph + * + * @param filter - Filter parameters. + * @param options - Optional configuration for subgraph requests. + * @returns List of payouts matching the filters. + * @throws ErrorUnsupportedChainID If the chain ID is not supported + * @throws ErrorInvalidAddress If any filter address is invalid + * + * @example + * ```ts + * import { ChainId } from '@human-protocol/sdk'; + * + * const payouts = await EscrowUtils.getPayouts({ + * chainId: ChainId.POLYGON, + * escrowAddress: '0x1234567890123456789012345678901234567890', + * recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef', + * from: new Date('2023-01-01'), + * to: new Date('2023-12-31') + * }); + * console.log('Payouts:', payouts.length); + * ``` + */ + public static async getPayouts( + filter: IPayoutFilter, + options?: SubgraphOptions + ): Promise { + const networkData = NETWORKS[filter.chainId]; + if (!networkData) { + throw ErrorUnsupportedChainID; + } + if (filter.escrowAddress && !ethers.isAddress(filter.escrowAddress)) { + throw ErrorInvalidAddress; + } + if (filter.recipient && !ethers.isAddress(filter.recipient)) { + throw ErrorInvalidAddress; + } + + const first = + filter.first !== undefined ? Math.min(filter.first, 1000) : 10; + const skip = filter.skip || 0; + const orderDirection = filter.orderDirection || OrderDirection.DESC; + + const { payouts } = await customGqlFetch<{ payouts: PayoutData[] }>( + getSubgraphUrl(networkData), + GET_PAYOUTS_QUERY(filter), + { + escrowAddress: filter.escrowAddress?.toLowerCase(), + recipient: filter.recipient?.toLowerCase(), + from: filter.from ? getUnixTimestamp(filter.from) : undefined, + to: filter.to ? getUnixTimestamp(filter.to) : undefined, + first: Math.min(first, 1000), + skip, + orderDirection, + }, + options + ); + if (!payouts) { + return []; + } + + return payouts.map((payout) => ({ + id: payout.id, + escrowAddress: payout.escrowAddress, + recipient: payout.recipient, + amount: BigInt(payout.amount), + createdAt: Number(payout.createdAt) * 1000, + })); + } + + /** + * This function returns the cancellation refunds for a given set of networks. + * + * > This uses Subgraph + * + * @param filter - Filter parameters. + * @param options - Optional configuration for subgraph requests. + * @returns List of cancellation refunds matching the filters. + * @throws ErrorUnsupportedChainID If the chain ID is not supported + * @throws ErrorInvalidEscrowAddressProvided If the escrow address is invalid + * @throws ErrorInvalidAddress If the receiver address is invalid + * + * @example + * ```ts + * import { ChainId } from '@human-protocol/sdk'; + * + * const cancellationRefunds = await EscrowUtils.getCancellationRefunds({ + * chainId: ChainId.POLYGON_AMOY, + * escrowAddress: '0x1234567890123456789012345678901234567890', + * }); + * console.log('Cancellation refunds:', cancellationRefunds.length); + * ``` + */ + public static async getCancellationRefunds( + filter: ICancellationRefundFilter, + options?: SubgraphOptions + ): Promise { + const networkData = NETWORKS[filter.chainId]; + if (!networkData) throw ErrorUnsupportedChainID; + if (filter.escrowAddress && !ethers.isAddress(filter.escrowAddress)) { + throw ErrorInvalidEscrowAddressProvided; + } + if (filter.receiver && !ethers.isAddress(filter.receiver)) { + throw ErrorInvalidAddress; + } + + const first = + filter.first !== undefined ? Math.min(filter.first, 1000) : 10; + const skip = filter.skip || 0; + const orderDirection = filter.orderDirection || OrderDirection.DESC; + + const { cancellationRefundEvents } = await customGqlFetch<{ + cancellationRefundEvents: CancellationRefundData[]; + }>( + getSubgraphUrl(networkData), + GET_CANCELLATION_REFUNDS_QUERY(filter), + { + escrowAddress: filter.escrowAddress?.toLowerCase(), + receiver: filter.receiver?.toLowerCase(), + from: filter.from ? getUnixTimestamp(filter.from) : undefined, + to: filter.to ? getUnixTimestamp(filter.to) : undefined, + first, + skip, + orderDirection, + }, + options + ); + + if (!cancellationRefundEvents || cancellationRefundEvents.length === 0) { + return []; + } + + return cancellationRefundEvents.map((event) => ({ + id: event.id, + escrowAddress: event.escrowAddress, + receiver: event.receiver, + amount: BigInt(event.amount), + block: Number(event.block), + timestamp: Number(event.timestamp) * 1000, + txHash: event.txHash, + })); + } + + /** + * This function returns the cancellation refund for a given escrow address. + * + * > This uses Subgraph + * + * @param chainId - Network in which the escrow has been deployed + * @param escrowAddress - Address of the escrow + * @param options - Optional configuration for subgraph requests. + * @returns Cancellation refund data or null if not found. + * @throws ErrorUnsupportedChainID If the chain ID is not supported + * @throws ErrorInvalidEscrowAddressProvided If the escrow address is invalid + * + * @example + * ```ts + * import { ChainId } from '@human-protocol/sdk'; + * + * + * const cancellationRefund = await EscrowUtils.getCancellationRefund( + * ChainId.POLYGON_AMOY, + * "0x1234567890123456789012345678901234567890" + * ); + * if (cancellationRefund) { + * console.log('Refund amount:', cancellationRefund.amount); + * } + * ``` + */ + public static async getCancellationRefund( + chainId: ChainId, + escrowAddress: string, + options?: SubgraphOptions + ): Promise { + const networkData = NETWORKS[chainId]; + if (!networkData) throw ErrorUnsupportedChainID; + + if (!ethers.isAddress(escrowAddress)) { + throw ErrorInvalidEscrowAddressProvided; + } + + const { cancellationRefundEvents } = await customGqlFetch<{ + cancellationRefundEvents: CancellationRefundData[]; + }>( + getSubgraphUrl(networkData), + GET_CANCELLATION_REFUND_BY_ADDRESS_QUERY(), + { escrowAddress: escrowAddress.toLowerCase() }, + options + ); + + if (!cancellationRefundEvents || cancellationRefundEvents.length === 0) { + return null; + } + + return { + id: cancellationRefundEvents[0].id, + escrowAddress: cancellationRefundEvents[0].escrowAddress, + receiver: cancellationRefundEvents[0].receiver, + amount: BigInt(cancellationRefundEvents[0].amount), + block: Number(cancellationRefundEvents[0].block), + timestamp: Number(cancellationRefundEvents[0].timestamp) * 1000, + txHash: cancellationRefundEvents[0].txHash, + }; + } +} + +function mapEscrow(e: EscrowData, chainId: ChainId | number): IEscrow { + return { + id: e.id, + address: e.address, + amountPaid: BigInt(e.amountPaid), + balance: BigInt(e.balance), + count: Number(e.count), + factoryAddress: e.factoryAddress, + finalResultsUrl: e.finalResultsUrl, + finalResultsHash: e.finalResultsHash, + intermediateResultsUrl: e.intermediateResultsUrl, + intermediateResultsHash: e.intermediateResultsHash, + launcher: e.launcher, + jobRequesterId: e.jobRequesterId, + manifestHash: e.manifestHash, + manifest: e.manifest, + recordingOracle: e.recordingOracle, + reputationOracle: e.reputationOracle, + exchangeOracle: e.exchangeOracle, + recordingOracleFee: e.recordingOracleFee + ? Number(e.recordingOracleFee) + : null, + reputationOracleFee: e.reputationOracleFee + ? Number(e.reputationOracleFee) + : null, + exchangeOracleFee: e.exchangeOracleFee ? Number(e.exchangeOracleFee) : null, + status: e.status, + token: e.token, + totalFundedAmount: BigInt(e.totalFundedAmount), + createdAt: Number(e.createdAt) * 1000, + chainId: Number(chainId), + }; +} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/escrow/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/escrow/index.ts new file mode 100644 index 0000000000..4424ee3911 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/escrow/index.ts @@ -0,0 +1,2 @@ +export { EscrowClient } from './escrow_client'; +export { EscrowUtils } from './escrow_utils'; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/index.ts index 0e07f51949..12d1ceff49 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/index.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/index.ts @@ -1,12 +1,3 @@ -import { StakingClient, StakingUtils } from './staking'; -import { KVStoreClient, KVStoreUtils } from './kvstore'; -import { EscrowClient, EscrowUtils } from './escrow'; -import { StatisticsUtils } from './statistics'; -import { Encryption, EncryptionUtils, MessageDataType } from './encryption'; -import { OperatorUtils } from './operator'; -import { TransactionUtils } from './transaction'; -import { WorkerUtils } from './worker'; - export * from './constants'; export * from './types'; export * from './enums'; @@ -22,20 +13,16 @@ export { ContractExecutionError, InvalidEthereumAddressError, InvalidKeyError, + SubgraphBadIndexerError, + SubgraphRequestError, } from './error'; -export { - StakingClient, - KVStoreClient, - KVStoreUtils, - EscrowClient, - EscrowUtils, - StatisticsUtils, - Encryption, - EncryptionUtils, - OperatorUtils, - TransactionUtils, - WorkerUtils, - StakingUtils, - MessageDataType, -}; +export { StakingClient, StakingUtils } from './staking'; +export { KVStoreClient, KVStoreUtils } from './kvstore'; +export { EscrowClient, EscrowUtils } from './escrow'; +export { StatisticsUtils } from './statistics'; +export { Encryption, EncryptionUtils } from './encryption'; +export type { MessageDataType } from './encryption'; +export { OperatorUtils } from './operator'; +export { TransactionUtils } from './transaction'; +export { WorkerUtils } from './worker'; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/kvstore/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/kvstore/index.ts new file mode 100644 index 0000000000..507a0f6d62 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/kvstore/index.ts @@ -0,0 +1,2 @@ +export { KVStoreClient } from './kvstore_client'; +export { KVStoreUtils } from './kvstore_utils'; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/kvstore.ts b/packages/sdk/typescript/human-protocol-sdk/src/kvstore/kvstore_client.ts similarity index 53% rename from packages/sdk/typescript/human-protocol-sdk/src/kvstore.ts rename to packages/sdk/typescript/human-protocol-sdk/src/kvstore/kvstore_client.ts index 22635167d3..763028c38f 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/kvstore.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/kvstore/kvstore_client.ts @@ -3,28 +3,20 @@ import { KVStore__factory, } from '@human-protocol/core/typechain-types'; import { ContractRunner, ethers } from 'ethers'; -import { BaseEthersClient } from './base'; -import { KVStoreKeys, NETWORKS } from './constants'; -import { requiresSigner } from './decorators'; -import { ChainId } from './enums'; +import { BaseEthersClient } from '../base'; +import { NETWORKS } from '../constants'; +import { requiresSigner } from '../decorators'; +import { ChainId } from '../enums'; import { ErrorInvalidAddress, - ErrorInvalidHash, ErrorInvalidUrl, ErrorKVStoreArrayLength, ErrorKVStoreEmptyKey, ErrorProviderDoesNotExist, ErrorUnsupportedChainID, - InvalidKeyError, -} from './error'; -import { NetworkData, TransactionOverrides } from './types'; -import { getSubgraphUrl, customGqlFetch, isValidUrl } from './utils'; -import { - GET_KVSTORE_BY_ADDRESS_AND_KEY_QUERY, - GET_KVSTORE_BY_ADDRESS_QUERY, -} from './graphql/queries/kvstore'; -import { KVStoreData } from './graphql'; -import { IKVStore, SubgraphOptions } from './interfaces'; +} from '../error'; +import { NetworkData, TransactionOverrides } from '../types'; +import { isValidUrl } from '../utils'; /** * Client for interacting with the KVStore contract. * @@ -282,224 +274,3 @@ export class KVStoreClient extends BaseEthersClient { } } } - -/** - * Utility helpers for KVStore-related queries. - * - * @example - * ```ts - * import { ChainId, KVStoreUtils } from '@human-protocol/sdk'; - * - * const kvStoreData = await KVStoreUtils.getKVStoreData( - * ChainId.POLYGON_AMOY, - * "0x1234567890123456789012345678901234567890" - * ); - * console.log('KVStore data:', kvStoreData); - * ``` - */ -export class KVStoreUtils { - /** - * This function returns the KVStore data for a given address. - * - * @param chainId - Network in which the KVStore is deployed - * @param address - Address of the KVStore - * @param options - Optional configuration for subgraph requests. - * @returns KVStore data - * @throws ErrorUnsupportedChainID If the network's chainId is not supported - * @throws ErrorInvalidAddress If the address is invalid - * - * @example - * ```ts - * const kvStoreData = await KVStoreUtils.getKVStoreData( - * ChainId.POLYGON_AMOY, - * "0x1234567890123456789012345678901234567890" - * ); - * console.log('KVStore data:', kvStoreData); - * ``` - */ - public static async getKVStoreData( - chainId: ChainId, - address: string, - options?: SubgraphOptions - ): Promise { - const networkData = NETWORKS[chainId]; - - if (!networkData) { - throw ErrorUnsupportedChainID; - } - - if (address && !ethers.isAddress(address)) { - throw ErrorInvalidAddress; - } - - const { kvstores } = await customGqlFetch<{ kvstores: KVStoreData[] }>( - getSubgraphUrl(networkData), - GET_KVSTORE_BY_ADDRESS_QUERY(), - { address: address.toLowerCase() }, - options - ); - - const kvStoreData = kvstores.map((item) => ({ - key: item.key, - value: item.value, - })); - - return kvStoreData || []; - } - - /** - * Gets the value of a key-value pair in the KVStore using the subgraph. - * - * @param chainId - Network in which the KVStore is deployed - * @param address - Address from which to get the key value. - * @param key - Key to obtain the value. - * @param options - Optional configuration for subgraph requests. - * @returns Value of the key. - * @throws ErrorUnsupportedChainID If the network's chainId is not supported - * @throws ErrorInvalidAddress If the address is invalid - * @throws ErrorKVStoreEmptyKey If the key is empty - * @throws InvalidKeyError If the key is not found - * - * @example - * ```ts - * const value = await KVStoreUtils.get( - * ChainId.POLYGON_AMOY, - * '0x1234567890123456789012345678901234567890', - * 'role' - * ); - * console.log('Value:', value); - * ``` - */ - public static async get( - chainId: ChainId, - address: string, - key: string, - options?: SubgraphOptions - ): Promise { - if (key === '') throw ErrorKVStoreEmptyKey; - if (!ethers.isAddress(address)) throw ErrorInvalidAddress; - - const networkData = NETWORKS[chainId]; - - if (!networkData) { - throw ErrorUnsupportedChainID; - } - - const { kvstores } = await customGqlFetch<{ kvstores: KVStoreData[] }>( - getSubgraphUrl(networkData), - GET_KVSTORE_BY_ADDRESS_AND_KEY_QUERY(), - { address: address.toLowerCase(), key }, - options - ); - - if (!kvstores || kvstores.length === 0) { - throw new InvalidKeyError(key, address); - } - - return kvstores[0].value; - } - - /** - * Gets the URL value of the given entity, and verifies its hash. - * - * @param chainId - Network in which the KVStore is deployed - * @param address - Address from which to get the URL value. - * @param urlKey - Configurable URL key. `url` by default. - * @param options - Optional configuration for subgraph requests. - * @returns URL value for the given address if it exists, and the content is valid - * @throws ErrorInvalidAddress If the address is invalid - * @throws ErrorInvalidHash If the hash verification fails - * @throws Error If fetching URL or hash fails - * - * @example - * ```ts - * const url = await KVStoreUtils.getFileUrlAndVerifyHash( - * ChainId.POLYGON_AMOY, - * '0x1234567890123456789012345678901234567890' - * ); - * console.log('Verified URL:', url); - * ``` - */ - public static async getFileUrlAndVerifyHash( - chainId: ChainId, - address: string, - urlKey = 'url', - options?: SubgraphOptions - ): Promise { - if (!ethers.isAddress(address)) throw ErrorInvalidAddress; - const hashKey = urlKey + '_hash'; - - let url = '', - hash = ''; - - try { - url = await this.get(chainId, address, urlKey, options); - } catch (e) { - if (e instanceof Error) throw Error(`Failed to get URL: ${e.message}`); - } - - // Return empty string - if (!url?.length) { - return ''; - } - - try { - hash = await this.get(chainId, address, hashKey); - } catch (e) { - if (e instanceof Error) throw Error(`Failed to get Hash: ${e.message}`); - } - - const content = await fetch(url).then((res) => res.text()); - const contentHash = ethers.keccak256(ethers.toUtf8Bytes(content)); - - const formattedHash = hash?.replace(/^0x/, ''); - const formattedContentHash = contentHash?.replace(/^0x/, ''); - - if (formattedHash !== formattedContentHash) { - throw ErrorInvalidHash; - } - - return url; - } - - /** - * Gets the public key of the given entity, and verifies its hash. - * - * @param chainId - Network in which the KVStore is deployed - * @param address - Address from which to get the public key. - * @param options - Optional configuration for subgraph requests. - * @returns Public key for the given address if it exists, and the content is valid - * @throws ErrorInvalidAddress If the address is invalid - * @throws ErrorInvalidHash If the hash verification fails - * @throws Error If fetching the public key fails - * - * @example - * ```ts - * const publicKey = await KVStoreUtils.getPublicKey( - * ChainId.POLYGON_AMOY, - * '0x1234567890123456789012345678901234567890' - * ); - * console.log('Public key:', publicKey); - * ``` - */ - public static async getPublicKey( - chainId: ChainId, - address: string, - options?: SubgraphOptions - ): Promise { - const publicKeyUrl = await this.getFileUrlAndVerifyHash( - chainId, - address, - KVStoreKeys.publicKey, - options - ); - - if (publicKeyUrl === '') { - return ''; - } - - const publicKey = await fetch(publicKeyUrl).then((res) => res.text()); - - return publicKey; - } -} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/kvstore/kvstore_utils.ts b/packages/sdk/typescript/human-protocol-sdk/src/kvstore/kvstore_utils.ts new file mode 100644 index 0000000000..ecc9097a40 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/kvstore/kvstore_utils.ts @@ -0,0 +1,237 @@ +import { ethers } from 'ethers'; +import { KVStoreKeys, NETWORKS } from '../constants'; +import { ChainId } from '../enums'; +import { + ErrorInvalidAddress, + ErrorInvalidHash, + ErrorKVStoreEmptyKey, + ErrorUnsupportedChainID, + InvalidKeyError, +} from '../error'; +import { KVStoreData } from '../graphql'; +import { + GET_KVSTORE_BY_ADDRESS_AND_KEY_QUERY, + GET_KVSTORE_BY_ADDRESS_QUERY, +} from '../graphql/queries/kvstore'; +import { IKVStore, SubgraphOptions } from '../interfaces'; +import { customGqlFetch, getSubgraphUrl } from '../utils'; +/** + * Utility helpers for KVStore-related queries. + * + * @example + * ```ts + * import { ChainId, KVStoreUtils } from '@human-protocol/sdk'; + * + * const kvStoreData = await KVStoreUtils.getKVStoreData( + * ChainId.POLYGON_AMOY, + * "0x1234567890123456789012345678901234567890" + * ); + * console.log('KVStore data:', kvStoreData); + * ``` + */ +export class KVStoreUtils { + /** + * This function returns the KVStore data for a given address. + * + * @param chainId - Network in which the KVStore is deployed + * @param address - Address of the KVStore + * @param options - Optional configuration for subgraph requests. + * @returns KVStore data + * @throws ErrorUnsupportedChainID If the network's chainId is not supported + * @throws ErrorInvalidAddress If the address is invalid + * + * @example + * ```ts + * const kvStoreData = await KVStoreUtils.getKVStoreData( + * ChainId.POLYGON_AMOY, + * "0x1234567890123456789012345678901234567890" + * ); + * console.log('KVStore data:', kvStoreData); + * ``` + */ + public static async getKVStoreData( + chainId: ChainId, + address: string, + options?: SubgraphOptions + ): Promise { + const networkData = NETWORKS[chainId]; + + if (!networkData) { + throw ErrorUnsupportedChainID; + } + + if (address && !ethers.isAddress(address)) { + throw ErrorInvalidAddress; + } + + const { kvstores } = await customGqlFetch<{ kvstores: KVStoreData[] }>( + getSubgraphUrl(networkData), + GET_KVSTORE_BY_ADDRESS_QUERY(), + { address: address.toLowerCase() }, + options + ); + + const kvStoreData = kvstores.map((item) => ({ + key: item.key, + value: item.value, + })); + + return kvStoreData || []; + } + + /** + * Gets the value of a key-value pair in the KVStore using the subgraph. + * + * @param chainId - Network in which the KVStore is deployed + * @param address - Address from which to get the key value. + * @param key - Key to obtain the value. + * @param options - Optional configuration for subgraph requests. + * @returns Value of the key. + * @throws ErrorUnsupportedChainID If the network's chainId is not supported + * @throws ErrorInvalidAddress If the address is invalid + * @throws ErrorKVStoreEmptyKey If the key is empty + * @throws InvalidKeyError If the key is not found + * + * @example + * ```ts + * const value = await KVStoreUtils.get( + * ChainId.POLYGON_AMOY, + * '0x1234567890123456789012345678901234567890', + * 'role' + * ); + * console.log('Value:', value); + * ``` + */ + public static async get( + chainId: ChainId, + address: string, + key: string, + options?: SubgraphOptions + ): Promise { + if (key === '') throw ErrorKVStoreEmptyKey; + if (!ethers.isAddress(address)) throw ErrorInvalidAddress; + + const networkData = NETWORKS[chainId]; + + if (!networkData) { + throw ErrorUnsupportedChainID; + } + + const { kvstores } = await customGqlFetch<{ kvstores: KVStoreData[] }>( + getSubgraphUrl(networkData), + GET_KVSTORE_BY_ADDRESS_AND_KEY_QUERY(), + { address: address.toLowerCase(), key }, + options + ); + + if (!kvstores || kvstores.length === 0) { + throw new InvalidKeyError(key, address); + } + + return kvstores[0].value; + } + + /** + * Gets the URL value of the given entity, and verifies its hash. + * + * @param chainId - Network in which the KVStore is deployed + * @param address - Address from which to get the URL value. + * @param urlKey - Configurable URL key. `url` by default. + * @param options - Optional configuration for subgraph requests. + * @returns URL value for the given address if it exists, and the content is valid + * @throws ErrorInvalidAddress If the address is invalid + * @throws ErrorInvalidHash If the hash verification fails + * @throws Error If fetching URL or hash fails + * + * @example + * ```ts + * const url = await KVStoreUtils.getFileUrlAndVerifyHash( + * ChainId.POLYGON_AMOY, + * '0x1234567890123456789012345678901234567890' + * ); + * console.log('Verified URL:', url); + * ``` + */ + public static async getFileUrlAndVerifyHash( + chainId: ChainId, + address: string, + urlKey = 'url', + options?: SubgraphOptions + ): Promise { + if (!ethers.isAddress(address)) throw ErrorInvalidAddress; + const hashKey = urlKey + '_hash'; + + let url = '', + hash = ''; + + try { + url = await this.get(chainId, address, urlKey, options); + } catch (e) { + if (e instanceof Error) throw Error(`Failed to get URL: ${e.message}`); + } + + // Return empty string + if (!url?.length) { + return ''; + } + + try { + hash = await this.get(chainId, address, hashKey); + } catch (e) { + if (e instanceof Error) throw Error(`Failed to get Hash: ${e.message}`); + } + + const content = await fetch(url).then((res) => res.text()); + const contentHash = ethers.keccak256(ethers.toUtf8Bytes(content)); + + const formattedHash = hash?.replace(/^0x/, ''); + const formattedContentHash = contentHash?.replace(/^0x/, ''); + + if (formattedHash !== formattedContentHash) { + throw ErrorInvalidHash; + } + + return url; + } + + /** + * Gets the public key of the given entity, and verifies its hash. + * + * @param chainId - Network in which the KVStore is deployed + * @param address - Address from which to get the public key. + * @param options - Optional configuration for subgraph requests. + * @returns Public key for the given address if it exists, and the content is valid + * @throws ErrorInvalidAddress If the address is invalid + * @throws ErrorInvalidHash If the hash verification fails + * @throws Error If fetching the public key fails + * + * @example + * ```ts + * const publicKey = await KVStoreUtils.getPublicKey( + * ChainId.POLYGON_AMOY, + * '0x1234567890123456789012345678901234567890' + * ); + * console.log('Public key:', publicKey); + * ``` + */ + public static async getPublicKey( + chainId: ChainId, + address: string, + options?: SubgraphOptions + ): Promise { + const publicKeyUrl = await this.getFileUrlAndVerifyHash( + chainId, + address, + KVStoreKeys.publicKey, + options + ); + + if (publicKeyUrl === '') { + return ''; + } + + const publicKey = await fetch(publicKeyUrl).then((res) => res.text()); + + return publicKey; + } +} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/operator/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/operator/index.ts new file mode 100644 index 0000000000..9cb7c0df41 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/operator/index.ts @@ -0,0 +1 @@ +export { OperatorUtils } from './operator_utils'; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/operator.ts b/packages/sdk/typescript/human-protocol-sdk/src/operator/operator_utils.ts similarity index 96% rename from packages/sdk/typescript/human-protocol-sdk/src/operator.ts rename to packages/sdk/typescript/human-protocol-sdk/src/operator/operator_utils.ts index ad0dd150d7..b7a84b9668 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/operator.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/operator/operator_utils.ts @@ -4,27 +4,27 @@ import { IOperatorsFilter, IReward, SubgraphOptions, -} from './interfaces'; -import { GET_REWARD_ADDED_EVENTS_QUERY } from './graphql/queries/reward'; +} from '../interfaces'; +import { GET_REWARD_ADDED_EVENTS_QUERY } from '../graphql/queries/reward'; import { IOperatorSubgraph, IReputationNetworkSubgraph, RewardAddedEventData, -} from './graphql'; +} from '../graphql'; import { GET_LEADER_QUERY, GET_LEADERS_QUERY, GET_REPUTATION_NETWORK_QUERY, -} from './graphql/queries/operator'; +} from '../graphql/queries/operator'; import { ethers } from 'ethers'; import { ErrorInvalidSlasherAddressProvided, ErrorInvalidStakerAddressProvided, ErrorUnsupportedChainID, -} from './error'; -import { getSubgraphUrl, customGqlFetch } from './utils'; -import { ChainId, OrderDirection } from './enums'; -import { NETWORKS } from './constants'; +} from '../error'; +import { getSubgraphUrl, customGqlFetch } from '../utils'; +import { ChainId, OrderDirection } from '../enums'; +import { NETWORKS } from '../constants'; /** * Utility helpers for operator-related queries. diff --git a/packages/sdk/typescript/human-protocol-sdk/src/staking/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/staking/index.ts new file mode 100644 index 0000000000..714d1b4561 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/staking/index.ts @@ -0,0 +1,2 @@ +export { StakingClient } from './staking_client'; +export { StakingUtils } from './staking_utils'; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/staking.ts b/packages/sdk/typescript/human-protocol-sdk/src/staking/staking_client.ts similarity index 71% rename from packages/sdk/typescript/human-protocol-sdk/src/staking.ts rename to packages/sdk/typescript/human-protocol-sdk/src/staking/staking_client.ts index b0f0284db6..65c747140a 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/staking.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/staking/staking_client.ts @@ -7,10 +7,10 @@ import { Staking__factory, } from '@human-protocol/core/typechain-types'; import { ContractRunner, ethers } from 'ethers'; -import { BaseEthersClient } from './base'; -import { NETWORKS } from './constants'; -import { requiresSigner } from './decorators'; -import { ChainId, OrderDirection } from './enums'; +import { BaseEthersClient } from '../base'; +import { NETWORKS } from '../constants'; +import { requiresSigner } from '../decorators'; +import { ChainId } from '../enums'; import { ErrorEscrowAddressIsNotProvidedByFactory, ErrorInvalidEscrowAddressProvided, @@ -19,22 +19,11 @@ import { ErrorInvalidStakingValueSign, ErrorInvalidStakingValueType, ErrorProviderDoesNotExist, - ErrorStakerNotFound, ErrorUnsupportedChainID, -} from './error'; -import { - IStaker, - IStakersFilter, - StakerInfo, - SubgraphOptions, -} from './interfaces'; -import { StakerData } from './graphql'; -import { NetworkData, TransactionOverrides } from './types'; -import { getSubgraphUrl, customGqlFetch, throwError } from './utils'; -import { - GET_STAKER_BY_ADDRESS_QUERY, - GET_STAKERS_QUERY, -} from './graphql/queries/staking'; +} from '../error'; +import { StakerInfo } from '../interfaces'; +import { NetworkData, TransactionOverrides } from '../types'; +import { throwError } from '../utils'; /** * Client for staking actions on HUMAN Protocol. @@ -456,158 +445,3 @@ export class StakingClient extends BaseEthersClient { } } } - -/** - * Utility helpers for Staking-related queries. - * - * @example - * ```ts - * import { StakingUtils, ChainId } from '@human-protocol/sdk'; - * - * const staker = await StakingUtils.getStaker( - * ChainId.POLYGON_AMOY, - * '0xYourStakerAddress' - * ); - * console.log('Staked amount:', staker.stakedAmount); - * ``` - */ -export class StakingUtils { - /** - * Gets staking info for a staker from the subgraph. - * - * @param chainId - Network in which the staking contract is deployed - * @param stakerAddress - Address of the staker - * @param options - Optional configuration for subgraph requests. - * @returns Staker info from subgraph - * @throws ErrorInvalidStakerAddressProvided If the staker address is invalid - * @throws ErrorUnsupportedChainID If the chain ID is not supported - * @throws ErrorStakerNotFound If the staker is not found - * - * @example - * ```ts - * import { StakingUtils, ChainId } from '@human-protocol/sdk'; - * - * const staker = await StakingUtils.getStaker( - * ChainId.POLYGON_AMOY, - * '0xYourStakerAddress' - * ); - * console.log('Staked amount:', staker.stakedAmount); - * ``` - */ - public static async getStaker( - chainId: ChainId, - stakerAddress: string, - options?: SubgraphOptions - ): Promise { - if (!ethers.isAddress(stakerAddress)) { - throw ErrorInvalidStakerAddressProvided; - } - - const networkData: NetworkData | undefined = NETWORKS[chainId]; - if (!networkData) { - throw ErrorUnsupportedChainID; - } - - const { staker } = await customGqlFetch<{ staker: StakerData }>( - getSubgraphUrl(networkData), - GET_STAKER_BY_ADDRESS_QUERY, - { id: stakerAddress.toLowerCase() }, - options - ); - - if (!staker) { - throw ErrorStakerNotFound; - } - - return mapStaker(staker); - } - - /** - * Gets all stakers from the subgraph with filters, pagination and ordering. - * - * @param filter - Stakers filter with pagination and ordering - * @param options - Optional configuration for subgraph requests. - * @returns Array of stakers - * @throws ErrorUnsupportedChainID If the chain ID is not supported - * - * @example - * ```ts - * import { ChainId } from '@human-protocol/sdk'; - * - * const filter = { - * chainId: ChainId.POLYGON_AMOY, - * minStakedAmount: '1000000000000000000', // 1 token in WEI - * }; - * const stakers = await StakingUtils.getStakers(filter); - * console.log('Stakers:', stakers.length); - * ``` - */ - public static async getStakers( - filter: IStakersFilter, - options?: SubgraphOptions - ): Promise { - const first = - filter.first !== undefined ? Math.min(filter.first, 1000) : 10; - const skip = filter.skip || 0; - const orderDirection = filter.orderDirection || OrderDirection.DESC; - const orderBy = filter.orderBy || 'lastDepositTimestamp'; - - const networkData = NETWORKS[filter.chainId]; - if (!networkData) { - throw ErrorUnsupportedChainID; - } - - const { stakers } = await customGqlFetch<{ stakers: StakerData[] }>( - getSubgraphUrl(networkData), - GET_STAKERS_QUERY(filter), - { - minStakedAmount: filter.minStakedAmount - ? filter.minStakedAmount - : undefined, - maxStakedAmount: filter.maxStakedAmount - ? filter.maxStakedAmount - : undefined, - minLockedAmount: filter.minLockedAmount - ? filter.minLockedAmount - : undefined, - maxLockedAmount: filter.maxLockedAmount - ? filter.maxLockedAmount - : undefined, - minWithdrawnAmount: filter.minWithdrawnAmount - ? filter.minWithdrawnAmount - : undefined, - maxWithdrawnAmount: filter.maxWithdrawnAmount - ? filter.maxWithdrawnAmount - : undefined, - minSlashedAmount: filter.minSlashedAmount - ? filter.minSlashedAmount - : undefined, - maxSlashedAmount: filter.maxSlashedAmount - ? filter.maxSlashedAmount - : undefined, - orderBy: orderBy, - orderDirection: orderDirection, - first: first, - skip: skip, - }, - options - ); - if (!stakers) { - return []; - } - - return stakers.map((s) => mapStaker(s)); - } -} - -function mapStaker(s: StakerData): IStaker { - return { - address: s.address, - stakedAmount: BigInt(s.stakedAmount), - lockedAmount: BigInt(s.lockedAmount), - withdrawableAmount: BigInt(s.withdrawnAmount), - slashedAmount: BigInt(s.slashedAmount), - lockedUntil: Number(s.lockedUntilTimestamp) * 1000, - lastDepositTimestamp: Number(s.lastDepositTimestamp) * 1000, - }; -} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/staking/staking_utils.ts b/packages/sdk/typescript/human-protocol-sdk/src/staking/staking_utils.ts new file mode 100644 index 0000000000..3c17984bd7 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/staking/staking_utils.ts @@ -0,0 +1,170 @@ +import { ethers } from 'ethers'; +import { NETWORKS } from '../constants'; +import { ChainId, OrderDirection } from '../enums'; +import { + ErrorInvalidStakerAddressProvided, + ErrorStakerNotFound, + ErrorUnsupportedChainID, +} from '../error'; +import { StakerData } from '../graphql'; +import { + GET_STAKER_BY_ADDRESS_QUERY, + GET_STAKERS_QUERY, +} from '../graphql/queries/staking'; +import { IStaker, IStakersFilter, SubgraphOptions } from '../interfaces'; +import { NetworkData } from '../types'; +import { customGqlFetch, getSubgraphUrl } from '../utils'; +/** + * Utility helpers for Staking-related queries. + * + * @example + * ```ts + * import { StakingUtils, ChainId } from '@human-protocol/sdk'; + * + * const staker = await StakingUtils.getStaker( + * ChainId.POLYGON_AMOY, + * '0xYourStakerAddress' + * ); + * console.log('Staked amount:', staker.stakedAmount); + * ``` + */ +export class StakingUtils { + /** + * Gets staking info for a staker from the subgraph. + * + * @param chainId - Network in which the staking contract is deployed + * @param stakerAddress - Address of the staker + * @param options - Optional configuration for subgraph requests. + * @returns Staker info from subgraph + * @throws ErrorInvalidStakerAddressProvided If the staker address is invalid + * @throws ErrorUnsupportedChainID If the chain ID is not supported + * @throws ErrorStakerNotFound If the staker is not found + * + * @example + * ```ts + * import { StakingUtils, ChainId } from '@human-protocol/sdk'; + * + * const staker = await StakingUtils.getStaker( + * ChainId.POLYGON_AMOY, + * '0xYourStakerAddress' + * ); + * console.log('Staked amount:', staker.stakedAmount); + * ``` + */ + public static async getStaker( + chainId: ChainId, + stakerAddress: string, + options?: SubgraphOptions + ): Promise { + if (!ethers.isAddress(stakerAddress)) { + throw ErrorInvalidStakerAddressProvided; + } + + const networkData: NetworkData | undefined = NETWORKS[chainId]; + if (!networkData) { + throw ErrorUnsupportedChainID; + } + + const { staker } = await customGqlFetch<{ staker: StakerData }>( + getSubgraphUrl(networkData), + GET_STAKER_BY_ADDRESS_QUERY, + { id: stakerAddress.toLowerCase() }, + options + ); + + if (!staker) { + throw ErrorStakerNotFound; + } + + return mapStaker(staker); + } + + /** + * Gets all stakers from the subgraph with filters, pagination and ordering. + * + * @param filter - Stakers filter with pagination and ordering + * @param options - Optional configuration for subgraph requests. + * @returns Array of stakers + * @throws ErrorUnsupportedChainID If the chain ID is not supported + * + * @example + * ```ts + * import { ChainId } from '@human-protocol/sdk'; + * + * const filter = { + * chainId: ChainId.POLYGON_AMOY, + * minStakedAmount: '1000000000000000000', // 1 token in WEI + * }; + * const stakers = await StakingUtils.getStakers(filter); + * console.log('Stakers:', stakers.length); + * ``` + */ + public static async getStakers( + filter: IStakersFilter, + options?: SubgraphOptions + ): Promise { + const first = + filter.first !== undefined ? Math.min(filter.first, 1000) : 10; + const skip = filter.skip || 0; + const orderDirection = filter.orderDirection || OrderDirection.DESC; + const orderBy = filter.orderBy || 'lastDepositTimestamp'; + + const networkData = NETWORKS[filter.chainId]; + if (!networkData) { + throw ErrorUnsupportedChainID; + } + + const { stakers } = await customGqlFetch<{ stakers: StakerData[] }>( + getSubgraphUrl(networkData), + GET_STAKERS_QUERY(filter), + { + minStakedAmount: filter.minStakedAmount + ? filter.minStakedAmount + : undefined, + maxStakedAmount: filter.maxStakedAmount + ? filter.maxStakedAmount + : undefined, + minLockedAmount: filter.minLockedAmount + ? filter.minLockedAmount + : undefined, + maxLockedAmount: filter.maxLockedAmount + ? filter.maxLockedAmount + : undefined, + minWithdrawnAmount: filter.minWithdrawnAmount + ? filter.minWithdrawnAmount + : undefined, + maxWithdrawnAmount: filter.maxWithdrawnAmount + ? filter.maxWithdrawnAmount + : undefined, + minSlashedAmount: filter.minSlashedAmount + ? filter.minSlashedAmount + : undefined, + maxSlashedAmount: filter.maxSlashedAmount + ? filter.maxSlashedAmount + : undefined, + orderBy: orderBy, + orderDirection: orderDirection, + first: first, + skip: skip, + }, + options + ); + if (!stakers) { + return []; + } + + return stakers.map((s) => mapStaker(s)); + } +} + +function mapStaker(s: StakerData): IStaker { + return { + address: s.address, + stakedAmount: BigInt(s.stakedAmount), + lockedAmount: BigInt(s.lockedAmount), + withdrawableAmount: BigInt(s.withdrawnAmount), + slashedAmount: BigInt(s.slashedAmount), + lockedUntil: Number(s.lockedUntilTimestamp) * 1000, + lastDepositTimestamp: Number(s.lastDepositTimestamp) * 1000, + }; +} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/statistics/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/statistics/index.ts new file mode 100644 index 0000000000..9e5d6b2493 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/statistics/index.ts @@ -0,0 +1 @@ +export { StatisticsUtils } from './statistics_utils'; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/statistics.ts b/packages/sdk/typescript/human-protocol-sdk/src/statistics/statistics_utils.ts similarity index 99% rename from packages/sdk/typescript/human-protocol-sdk/src/statistics.ts rename to packages/sdk/typescript/human-protocol-sdk/src/statistics/statistics_utils.ts index fd1192a449..1cf4b4422c 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/statistics.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/statistics/statistics_utils.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { OrderDirection } from './enums'; +import { OrderDirection } from '../enums'; import { EscrowStatisticsData, EventDayData, @@ -9,7 +9,7 @@ import { GET_HOLDERS_QUERY, HMTHolderData, HMTStatisticsData, -} from './graphql'; +} from '../graphql'; import { IDailyHMT, IEscrowStatistics, @@ -20,14 +20,14 @@ import { IStatisticsFilter, IWorkerStatistics, SubgraphOptions, -} from './interfaces'; -import { NetworkData } from './types'; +} from '../interfaces'; +import { NetworkData } from '../types'; import { getSubgraphUrl, getUnixTimestamp, customGqlFetch, throwError, -} from './utils'; +} from '../utils'; /** * Utility class for statistics-related queries. diff --git a/packages/sdk/typescript/human-protocol-sdk/src/transaction/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/transaction/index.ts new file mode 100644 index 0000000000..d4461b9f3f --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/transaction/index.ts @@ -0,0 +1 @@ +export { TransactionUtils } from './transaction_utils'; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/transaction.ts b/packages/sdk/typescript/human-protocol-sdk/src/transaction/transaction_utils.ts similarity index 97% rename from packages/sdk/typescript/human-protocol-sdk/src/transaction.ts rename to packages/sdk/typescript/human-protocol-sdk/src/transaction/transaction_utils.ts index d2802feff7..d055ef27c4 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/transaction.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/transaction/transaction_utils.ts @@ -1,23 +1,23 @@ import { ethers } from 'ethers'; -import { NETWORKS } from './constants'; -import { ChainId, OrderDirection } from './enums'; +import { NETWORKS } from '../constants'; +import { ChainId, OrderDirection } from '../enums'; import { ErrorCannotUseDateAndBlockSimultaneously, ErrorInvalidHashProvided, ErrorUnsupportedChainID, -} from './error'; -import { TransactionData } from './graphql'; +} from '../error'; +import { TransactionData } from '../graphql'; import { GET_TRANSACTION_QUERY, GET_TRANSACTIONS_QUERY, -} from './graphql/queries/transaction'; +} from '../graphql/queries/transaction'; import { InternalTransaction, ITransaction, ITransactionsFilter, SubgraphOptions, -} from './interfaces'; -import { getSubgraphUrl, getUnixTimestamp, customGqlFetch } from './utils'; +} from '../interfaces'; +import { getSubgraphUrl, getUnixTimestamp, customGqlFetch } from '../utils'; /** * Utility class for transaction-related queries. diff --git a/packages/sdk/typescript/human-protocol-sdk/src/utils.ts b/packages/sdk/typescript/human-protocol-sdk/src/utils.ts index 02ed1bd9aa..5d28485d4e 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/utils.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/utils.ts @@ -13,6 +13,8 @@ import { NonceExpired, NumericFault, ReplacementUnderpriced, + SubgraphBadIndexerError, + SubgraphRequestError, TransactionReplaced, WarnSubgraphApiKeyNotProvided, } from './error'; @@ -117,6 +119,38 @@ export const isIndexerError = (error: any): boolean => { return errorMessage.toLowerCase().includes('bad indexers'); }; +const getSubgraphErrorMessage = (error: any): string => { + return ( + error?.response?.errors?.[0]?.message || + error?.message || + error?.toString?.() || + 'Subgraph request failed' + ); +}; + +const getSubgraphStatusCode = (error: any): number | undefined => { + if (typeof error?.response?.status === 'number') { + return error.response.status; + } + + if (typeof error?.status === 'number') { + return error.status; + } + + return undefined; +}; + +const toSubgraphError = (error: any, url: string): Error => { + const message = getSubgraphErrorMessage(error); + const statusCode = getSubgraphStatusCode(error); + + if (isIndexerError(error)) { + return new SubgraphBadIndexerError(message, url, statusCode); + } + + return new SubgraphRequestError(message, url, statusCode); +}; + const sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); }; @@ -154,7 +188,11 @@ export const customGqlFetch = async ( : undefined; if (!options) { - return await gqlFetch(url, query, variables, headers); + try { + return await gqlFetch(url, query, variables, headers); + } catch (error) { + throw toSubgraphError(error, url); + } } const hasMaxRetries = options.maxRetries !== undefined; @@ -177,10 +215,11 @@ export const customGqlFetch = async ( try { return await gqlFetch(targetUrl, query, variables, headers); } catch (error) { - lastError = error; + const wrappedError = toSubgraphError(error, targetUrl); + lastError = wrappedError; if (attempt === maxRetries || !isIndexerError(error)) { - throw error; + throw wrappedError; } const delay = baseDelay * attempt; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/worker/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/worker/index.ts new file mode 100644 index 0000000000..d6dfa1ca7b --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/worker/index.ts @@ -0,0 +1 @@ +export { WorkerUtils } from './worker_utils'; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/worker.ts b/packages/sdk/typescript/human-protocol-sdk/src/worker/worker_utils.ts similarity index 91% rename from packages/sdk/typescript/human-protocol-sdk/src/worker.ts rename to packages/sdk/typescript/human-protocol-sdk/src/worker/worker_utils.ts index e76278bd3b..d26c847f8d 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/worker.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/worker/worker_utils.ts @@ -1,11 +1,11 @@ import { ethers } from 'ethers'; -import { NETWORKS } from './constants'; -import { ChainId, OrderDirection } from './enums'; -import { ErrorInvalidAddress, ErrorUnsupportedChainID } from './error'; -import { WorkerData } from './graphql'; -import { GET_WORKER_QUERY, GET_WORKERS_QUERY } from './graphql/queries/worker'; -import { IWorker, IWorkersFilter, SubgraphOptions } from './interfaces'; -import { getSubgraphUrl, customGqlFetch } from './utils'; +import { NETWORKS } from '../constants'; +import { ChainId, OrderDirection } from '../enums'; +import { ErrorInvalidAddress, ErrorUnsupportedChainID } from '../error'; +import { WorkerData } from '../graphql'; +import { GET_WORKER_QUERY, GET_WORKERS_QUERY } from '../graphql/queries/worker'; +import { IWorker, IWorkersFilter, SubgraphOptions } from '../interfaces'; +import { getSubgraphUrl, customGqlFetch } from '../utils'; /** * Utility class for worker-related operations. diff --git a/packages/sdk/typescript/human-protocol-sdk/test/utils.test.ts b/packages/sdk/typescript/human-protocol-sdk/test/utils.test.ts index ac9d4cb0cf..d2f05e34c8 100644 --- a/packages/sdk/typescript/human-protocol-sdk/test/utils.test.ts +++ b/packages/sdk/typescript/human-protocol-sdk/test/utils.test.ts @@ -19,6 +19,8 @@ import { WarnSubgraphApiKeyNotProvided, ErrorRetryParametersMissing, ErrorRoutingRequestsToIndexerRequiresApiKey, + SubgraphBadIndexerError, + SubgraphRequestError, } from '../src/error'; import { getSubgraphUrl, @@ -340,7 +342,7 @@ describe('customGqlFetch', () => { maxRetries: 3, baseDelay: 10, }) - ).rejects.toThrow('Regular GraphQL error'); + ).rejects.toThrow(SubgraphRequestError); expect(gqlFetchSpy).toHaveBeenCalledTimes(1); }); @@ -360,7 +362,7 @@ describe('customGqlFetch', () => { maxRetries: 2, baseDelay: 10, }) - ).rejects.toEqual(badIndexerError); + ).rejects.toThrow(SubgraphBadIndexerError); expect(gqlFetchSpy).toHaveBeenCalledTimes(3); }); @@ -400,8 +402,20 @@ describe('customGqlFetch', () => { maxRetries: 1, baseDelay: 10, }) - ).rejects.toEqual(badIndexerError); + ).rejects.toThrow(SubgraphBadIndexerError); expect(gqlFetchSpy).toHaveBeenCalledTimes(2); }); + + test('wraps subgraph request errors even when no retry config is provided', async () => { + const gqlFetchSpy = vi + .spyOn(gqlFetch, 'default') + .mockRejectedValue(new Error('fetch failed')); + + await expect( + customGqlFetch(mockUrl, mockQuery, mockVariables) + ).rejects.toThrow(SubgraphRequestError); + + expect(gqlFetchSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/sdk/typescript/subgraph/eslint.config.mjs b/packages/sdk/typescript/subgraph/eslint.config.mjs index e3b3ba3aa9..666059c3d0 100644 --- a/packages/sdk/typescript/subgraph/eslint.config.mjs +++ b/packages/sdk/typescript/subgraph/eslint.config.mjs @@ -27,6 +27,8 @@ export default tseslint.config( }, }, rules: { + 'no-useless-assignment': 'off', + 'preserve-caught-error': 'off', 'no-console': 'warn', '@/quotes': [ 'error', diff --git a/packages/sdk/typescript/subgraph/package.json b/packages/sdk/typescript/subgraph/package.json index 50e13ffc6b..3a84c97f38 100644 --- a/packages/sdk/typescript/subgraph/package.json +++ b/packages/sdk/typescript/subgraph/package.json @@ -46,7 +46,7 @@ "graphql": "^16.6.0", "matchstick-as": "^0.6.0", "mustache": "^4.2.0", - "prettier": "^3.7.4" + "prettier": "^3.8.1" }, "lint-staged": { "*.{ts,graphql}": [ diff --git a/yarn.lock b/yarn.lock index 0db0c9afc7..e09fa5ba8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -105,9 +105,9 @@ __metadata: dependencies: "@emotion/react": "npm:^11.11.4" "@emotion/styled": "npm:^11.11.5" - "@eslint/js": "npm:^9.27.0" + "@eslint/js": "npm:^10.0.1" "@human-protocol/sdk": "workspace:*" - "@mui/icons-material": "npm:^7.2.0" + "@mui/icons-material": "npm:^7.3.8" "@mui/material": "npm:^7.2.0" "@mui/styled-engine-sc": "npm:7.2.0" "@mui/system": "npm:^7.2.0" @@ -131,11 +131,11 @@ __metadata: eslint-plugin-react-hooks: "npm:^5.2.0" eslint-plugin-react-refresh: "npm:^0.4.11" globals: "npm:^16.2.0" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" react-number-format: "npm:^5.4.3" - react-router-dom: "npm:^6.23.1" + react-router-dom: "npm:^7.13.0" recharts: "npm:^2.13.0-alpha.4" sass: "npm:^1.89.2" simplebar-react: "npm:^3.3.2" @@ -193,7 +193,7 @@ __metadata: keyv: "npm:^5.5.5" lodash: "npm:^4.17.21" minio: "npm:8.0.6" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" reflect-metadata: "npm:^0.2.2" rxjs: "npm:^7.2.0" source-map-support: "npm:^0.5.20" @@ -211,7 +211,7 @@ __metadata: "@emotion/react": "npm:^11.14.0" "@emotion/styled": "npm:^11.14.1" "@human-protocol/sdk": "workspace:*" - "@mui/icons-material": "npm:^7.0.1" + "@mui/icons-material": "npm:^7.3.8" "@mui/material": "npm:^5.16.7" "@types/react": "npm:^18.3.12" "@types/react-dom": "npm:^18.3.1" @@ -223,11 +223,11 @@ __metadata: eslint-plugin-import: "npm:^2.29.0" eslint-plugin-react: "npm:^7.34.3" eslint-plugin-react-hooks: "npm:^5.1.0" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" react-loading-skeleton: "npm:^3.3.1" - react-router-dom: "npm:^6.4.3" + react-router-dom: "npm:^7.13.0" serve: "npm:^14.2.4" typescript: "npm:^5.8.3" viem: "npm:2.x" @@ -269,7 +269,7 @@ __metadata: "@emotion/react": "npm:^11.11.3" "@emotion/styled": "npm:^11.11.0" "@human-protocol/sdk": "workspace:^" - "@mui/icons-material": "npm:^7.0.1" + "@mui/icons-material": "npm:^7.3.8" "@mui/material": "npm:^5.16.7" "@tanstack/query-sync-storage-persister": "npm:^5.68.0" "@tanstack/react-query": "npm:^5.67.2" @@ -285,10 +285,10 @@ __metadata: eslint-plugin-react-hooks: "npm:^5.1.0" eslint-plugin-react-refresh: "npm:^0.4.11" ethers: "npm:^6.15.0" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" - react-router-dom: "npm:^6.24.1" + react-router-dom: "npm:^7.13.0" serve: "npm:^14.2.4" typescript: "npm:^5.6.3" viem: "npm:2.x" @@ -345,7 +345,7 @@ __metadata: passport: "npm:^0.7.0" passport-jwt: "npm:^4.0.1" pg: "npm:8.13.1" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" reflect-metadata: "npm:^0.2.2" rxjs: "npm:^7.2.0" source-map-support: "npm:^0.5.20" @@ -388,7 +388,7 @@ __metadata: jest: "npm:^29.7.0" joi: "npm:^17.13.3" minio: "npm:8.0.6" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" reflect-metadata: "npm:^0.2.2" rxjs: "npm:^7.2.0" ts-jest: "npm:29.2.5" @@ -426,7 +426,7 @@ __metadata: "@hcaptcha/react-hcaptcha": "npm:^1.14.0" "@hookform/resolvers": "npm:^5.1.0" "@human-protocol/sdk": "workspace:*" - "@mui/icons-material": "npm:^7.0.1" + "@mui/icons-material": "npm:^7.3.8" "@mui/material": "npm:^5.16.7" "@mui/system": "npm:^5.15.14" "@mui/x-date-pickers": "npm:^8.26.0" @@ -461,7 +461,7 @@ __metadata: material-react-table: "npm:3.0.1" mui-image: "npm:^1.0.7" notistack: "npm:^3.0.1" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" prop-types: "npm:^15.8.1" query-string: "npm:^9.0.0" react: "npm:^18.3.1" @@ -470,7 +470,7 @@ __metadata: react-i18next: "npm:^15.1.0" react-imask: "npm:^7.4.0" react-number-format: "npm:^5.4.3" - react-router-dom: "npm:^6.22.0" + react-router-dom: "npm:^7.13.0" serve: "npm:^14.2.4" typescript: "npm:^5.6.3" viem: "npm:^2.31.4" @@ -531,10 +531,10 @@ __metadata: jwt-decode: "npm:^4.0.0" keyv: "npm:^5.5.5" lodash: "npm:^4.17.21" - nock: "npm:^13.5.1" + nock: "npm:^14.0.11" passport: "npm:^0.7.0" passport-jwt: "npm:^4.0.1" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" reflect-metadata: "npm:^0.2.2" rxjs: "npm:^7.2.0" source-map-support: "npm:^0.5.20" @@ -553,7 +553,7 @@ __metadata: "@emotion/styled": "npm:^11.10.5" "@hcaptcha/react-hcaptcha": "npm:^1.14.0" "@human-protocol/sdk": "workspace:*" - "@mui/icons-material": "npm:^7.0.1" + "@mui/icons-material": "npm:^7.3.8" "@mui/lab": "npm:^6.0.0-dev.240424162023-9968b4889d" "@mui/material": "npm:^5.16.7" "@mui/system": "npm:^5.15.14" @@ -583,11 +583,11 @@ __metadata: file-saver: "npm:^2.0.5" formik: "npm:^2.4.2" jwt-decode: "npm:^4.0.0" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" react-redux: "npm:^9.1.0" - react-router-dom: "npm:^6.14.1" + react-router-dom: "npm:^7.13.0" recharts: "npm:^2.7.2" resize-observer-polyfill: "npm:^1.5.1" serve: "npm:^14.2.4" @@ -660,7 +660,7 @@ __metadata: passport: "npm:^0.7.0" passport-jwt: "npm:^4.0.1" pg: "npm:8.13.1" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" reflect-metadata: "npm:^0.2.2" rxjs: "npm:^7.2.0" source-map-support: "npm:^0.5.20" @@ -680,7 +680,7 @@ __metadata: version: 0.0.0-use.local resolution: "@apps/reputation-oracle@workspace:packages/apps/reputation-oracle/server" dependencies: - "@eslint/js": "npm:^9.33.0" + "@eslint/js": "npm:^10.0.1" "@faker-js/faker": "npm:^9.8.0" "@golevelup/ts-jest": "npm:^0.6.1" "@human-protocol/core": "workspace:*" @@ -731,11 +731,11 @@ __metadata: json-stable-stringify: "npm:^1.2.1" lodash: "npm:^4.17.21" minio: "npm:8.0.6" - nock: "npm:^14.0.3" + nock: "npm:^14.0.11" passport: "npm:^0.7.0" passport-jwt: "npm:^4.0.1" pg: "npm:8.13.1" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" reflect-metadata: "npm:^0.2.2" rxjs: "npm:^7.2.0" ts-jest: "npm:29.2.5" @@ -759,7 +759,7 @@ __metadata: "@emotion/react": "npm:^11.14.0" "@emotion/styled": "npm:^11.14.1" "@human-protocol/sdk": "npm:*" - "@mui/icons-material": "npm:^7.0.1" + "@mui/icons-material": "npm:^7.3.8" "@mui/material": "npm:^5.16.7" "@mui/x-data-grid": "npm:^8.7.0" "@tanstack/query-sync-storage-persister": "npm:^5.68.0" @@ -776,10 +776,10 @@ __metadata: eslint-plugin-react-hooks: "npm:^5.1.0" eslint-plugin-react-refresh: "npm:^0.4.11" ethers: "npm:^6.15.0" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" - react-router-dom: "npm:^6.24.1" + react-router-dom: "npm:^7.13.0" sass: "npm:^1.89.2" serve: "npm:^14.2.4" simplebar-react: "npm:^3.3.2" @@ -2893,7 +2893,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.24.4": +"@babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.28.6": version: 7.28.6 resolution: "@babel/runtime@npm:7.28.6" checksum: 10c0/358cf2429992ac1c466df1a21c1601d595c46930a13c1d4662fde908d44ee78ec3c183aaff513ecb01ef8c55c3624afe0309eeeb34715672dbfadb7feedb2c0d @@ -3808,10 +3808,15 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:^9.27.0, @eslint/js@npm:^9.30.1, @eslint/js@npm:^9.33.0": - version: 9.39.0 - resolution: "@eslint/js@npm:9.39.0" - checksum: 10c0/f0ac65784932f1a5d3b9c0db12eb1ff9dcb480dbd03da1045e5da820bd97a35875fb7790f1fbe652763270b1327b770c79a9ba0396e2ad91fbd97822493e67eb +"@eslint/js@npm:^10.0.1": + version: 10.0.1 + resolution: "@eslint/js@npm:10.0.1" + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + checksum: 10c0/9f3fcaf71ba7fdf65d82e8faad6ecfe97e11801cc3c362b306a88ea1ed1344ae0d35330dddb0e8ad18f010f6687a70b75491b9e01c8af57acd7987cee6b3ec6c languageName: node linkType: hard @@ -4957,7 +4962,7 @@ __metadata: hardhat-dependency-compiler: "npm:^1.2.1" hardhat-gas-reporter: "npm:^2.0.2" openpgp: "npm:6.2.2" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" prettier-plugin-solidity: "npm:^1.3.1" solidity-coverage: "npm:^0.8.17" tenderly: "npm:^0.9.1" @@ -4974,7 +4979,7 @@ __metadata: version: 0.0.0-use.local resolution: "@human-protocol/logger@workspace:packages/libs/logger" dependencies: - "@eslint/js": "npm:^9.30.1" + "@eslint/js": "npm:^10.0.1" "@types/node": "npm:^22.10.5" eslint: "npm:^9.39.1" eslint-config-prettier: "npm:^10.1.5" @@ -4984,7 +4989,7 @@ __metadata: globals: "npm:^16.3.0" pino: "npm:^10.1.0" pino-pretty: "npm:^13.1.3" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" ts-node: "npm:^10.9.2" typescript: "npm:^5.8.3" typescript-eslint: "npm:^8.35.1" @@ -5036,7 +5041,7 @@ __metadata: graphql-request: "npm:^7.3.4" graphql-tag: "npm:^2.12.6" openpgp: "npm:^6.2.2" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" secp256k1: "npm:^5.0.1" ts-node: "npm:^10.9.2" typedoc: "npm:^0.28.15" @@ -6505,9 +6510,9 @@ __metadata: languageName: node linkType: hard -"@mswjs/interceptors@npm:^0.39.5": - version: 0.39.8 - resolution: "@mswjs/interceptors@npm:0.39.8" +"@mswjs/interceptors@npm:^0.41.0": + version: 0.41.3 + resolution: "@mswjs/interceptors@npm:0.41.3" dependencies: "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/logger": "npm:^0.3.0" @@ -6515,7 +6520,7 @@ __metadata: is-node-process: "npm:^1.2.0" outvariant: "npm:^1.4.3" strict-event-emitter: "npm:^0.5.1" - checksum: 10c0/0d07625ff1bbbf4b5ea702e164914490e26cc3754323f544429f843f5ad8b157194f6a650259cfd1bbdeb6003e1354f63be1d9bf483816aaa17d93ef4b321aaf + checksum: 10c0/a259bbfc3bb4caada7a9a3529cc830159818e838c152df89ac890e7203df615a5e070ca63aa1e70a39868322ff5c1441ab74bbadb4081ca55b0c3a462e2903c0 languageName: node linkType: hard @@ -6555,19 +6560,19 @@ __metadata: languageName: node linkType: hard -"@mui/icons-material@npm:^7.0.1, @mui/icons-material@npm:^7.2.0": - version: 7.3.4 - resolution: "@mui/icons-material@npm:7.3.4" +"@mui/icons-material@npm:^7.3.8": + version: 7.3.8 + resolution: "@mui/icons-material@npm:7.3.8" dependencies: - "@babel/runtime": "npm:^7.28.4" + "@babel/runtime": "npm:^7.28.6" peerDependencies: - "@mui/material": ^7.3.4 + "@mui/material": ^7.3.8 "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: "@types/react": optional: true - checksum: 10c0/09c5708f0a96979dafeefdfbaef4950463e987bdc283874831d67ae0ce32cbc946bf408ba5084bd7a8f57af0cb87c3fdfddcf4c21e0946bb5e17c34abfd49d80 + checksum: 10c0/3c972ef066ddd0fbfc9ed3c26afa7ad769126ba19add9e89d50007671865238581d23d3c45fec901527642e086f8887404de38444d9b0bf1524d8fc5ffce2f6e languageName: node linkType: hard @@ -8626,13 +8631,6 @@ __metadata: languageName: node linkType: hard -"@remix-run/router@npm:1.23.0": - version: 1.23.0 - resolution: "@remix-run/router@npm:1.23.0" - checksum: 10c0/eaef5cb46a1e413f7d1019a75990808307e08e53a39d4cf69c339432ddc03143d725decef3d6b9b5071b898da07f72a4a57c4e73f787005fcf10162973d8d7d7 - languageName: node - linkType: hard - "@reown/appkit-adapter-wagmi@npm:^1.7.11": version: 1.8.12 resolution: "@reown/appkit-adapter-wagmi@npm:1.8.12" @@ -11177,7 +11175,7 @@ __metadata: graphql: "npm:^16.6.0" matchstick-as: "npm:^0.6.0" mustache: "npm:^4.2.0" - prettier: "npm:^3.7.4" + prettier: "npm:^3.8.1" languageName: unknown linkType: soft @@ -16541,6 +16539,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^1.0.1": + version: 1.1.1 + resolution: "cookie@npm:1.1.1" + checksum: 10c0/79c4ddc0fcad9c4f045f826f42edf54bcc921a29586a4558b0898277fa89fb47be95bc384c2253f493af7b29500c830da28341274527328f18eba9f58afa112c + languageName: node + linkType: hard + "copy-to-clipboard@npm:^3.3.3": version: 3.3.3 resolution: "copy-to-clipboard@npm:3.3.3" @@ -24570,25 +24575,14 @@ __metadata: languageName: node linkType: hard -"nock@npm:^13.5.1": - version: 13.5.6 - resolution: "nock@npm:13.5.6" - dependencies: - debug: "npm:^4.1.0" - json-stringify-safe: "npm:^5.0.1" - propagate: "npm:^2.0.0" - checksum: 10c0/94249a294176a6e521bbb763c214de4aa6b6ab63dff1e299aaaf455886a465d38906891d7f24570d94a43b1e376c239c54d89ff7697124bc57ef188006acc25e - languageName: node - linkType: hard - -"nock@npm:^14.0.3": - version: 14.0.10 - resolution: "nock@npm:14.0.10" +"nock@npm:^14.0.11": + version: 14.0.11 + resolution: "nock@npm:14.0.11" dependencies: - "@mswjs/interceptors": "npm:^0.39.5" + "@mswjs/interceptors": "npm:^0.41.0" json-stringify-safe: "npm:^5.0.1" propagate: "npm:^2.0.0" - checksum: 10c0/4868ce7c3e6a51ee83b496a1305eb821ad89427eb9e09c3c431344d91fd49974717e214fe97548be7d5f9a8039fefc3602ffbaad036f3508dd2c143726e3cfb8 + checksum: 10c0/154fde5d582ad8078b328dba850cd08d4d2084ebc387e032b3f24ae56498a8a2309f5718b4a6b81eda9c4683a4de7f545fe653f263a6e84a70317f30f55b175b languageName: node linkType: hard @@ -26321,12 +26315,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.7.4": - version: 3.7.4 - resolution: "prettier@npm:3.7.4" +"prettier@npm:^3.8.1": + version: 3.8.1 + resolution: "prettier@npm:3.8.1" bin: prettier: bin/prettier.cjs - checksum: 10c0/9675d2cd08eacb1faf1d1a2dbfe24bfab6a912b059fc9defdb380a408893d88213e794a40a2700bd29b140eb3172e0b07c852853f6e22f16f3374659a1a13389 + checksum: 10c0/33169b594009e48f570471271be7eac7cdcf88a209eed39ac3b8d6d78984039bfa9132f82b7e6ba3b06711f3bfe0222a62a1bfb87c43f50c25a83df1b78a2c42 languageName: node linkType: hard @@ -26928,27 +26922,31 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:^6.14.1, react-router-dom@npm:^6.22.0, react-router-dom@npm:^6.23.1, react-router-dom@npm:^6.24.1, react-router-dom@npm:^6.4.3": - version: 6.30.1 - resolution: "react-router-dom@npm:6.30.1" +"react-router-dom@npm:^7.13.0": + version: 7.13.0 + resolution: "react-router-dom@npm:7.13.0" dependencies: - "@remix-run/router": "npm:1.23.0" - react-router: "npm:6.30.1" + react-router: "npm:7.13.0" peerDependencies: - react: ">=16.8" - react-dom: ">=16.8" - checksum: 10c0/e9e1297236b0faa864424ad7d51c392fc6e118595d4dad4cd542fd217c479a81601a81c6266d5801f04f9e154de02d3b094fc22ccb544e755c2eb448fab4ec6b + react: ">=18" + react-dom: ">=18" + checksum: 10c0/759bd5e7fe7b5baba50a0264724188707682d217cad8eac702a55e0b1abebf295be014dd3bfaff8e3c2def9dfaa23e6ded3f908feab84df766e9b82cc3774e98 languageName: node linkType: hard -"react-router@npm:6.30.1": - version: 6.30.1 - resolution: "react-router@npm:6.30.1" +"react-router@npm:7.13.0": + version: 7.13.0 + resolution: "react-router@npm:7.13.0" dependencies: - "@remix-run/router": "npm:1.23.0" + cookie: "npm:^1.0.1" + set-cookie-parser: "npm:^2.6.0" peerDependencies: - react: ">=16.8" - checksum: 10c0/0414326f2d8e0c107fb4603cf4066dacba6a1f6f025c6e273f003e177b2f18888aca3de06d9b5522908f0e41de93be1754c37e82aa97b3a269c4742c08e82539 + react: ">=18" + react-dom: ">=18" + peerDependenciesMeta: + react-dom: + optional: true + checksum: 10c0/397cb009bc83d071269c8f9323bbfe1f856721fde75e39b29fe0ddfe7564ebdc3b8bbb85768321cae92ec28b406e8fac7eab7e232d0738b3b1c092e2764e4307 languageName: node linkType: hard @@ -28074,6 +28072,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.6.0": + version: 2.7.2 + resolution: "set-cookie-parser@npm:2.7.2" + checksum: 10c0/4381a9eb7ee951dfe393fe7aacf76b9a3b4e93a684d2162ab35594fa4053cc82a4d7d7582bf397718012c9adcf839b8cd8f57c6c42901ea9effe33c752da4a45 + languageName: node + linkType: hard + "set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2"