diff --git a/.env.test b/.env.test index d2acedfb2..5ab9791db 100644 --- a/.env.test +++ b/.env.test @@ -7,11 +7,11 @@ ENABLE_HTTPS="true" REDIS_URL="redis://127.0.0.1:6379/0" THIRDWEB_API_SECRET_KEY="my-thirdweb-secret-key" -TEST_AWS_KMS_KEY_ID="" -TEST_AWS_KMS_ACCESS_KEY_ID="" -TEST_AWS_KMS_SECRET_ACCESS_KEY="" -TEST_AWS_KMS_REGION="" +TEST_AWS_KMS_KEY_ID="UNIMPLEMENTED" +TEST_AWS_KMS_ACCESS_KEY_ID="UNIMPLEMENTED" +TEST_AWS_KMS_SECRET_ACCESS_KEY="UNIMPLEMENTED" +TEST_AWS_KMS_REGION="UNIMPLEMENTED" -TEST_GCP_KMS_RESOURCE_PATH="" -TEST_GCP_KMS_EMAIL="" -TEST_GCP_KMS_PK="" \ No newline at end of file +TEST_GCP_KMS_RESOURCE_PATH="UNIMPLEMENTED" +TEST_GCP_KMS_EMAIL="UNIMPLEMENTED" +TEST_GCP_KMS_PK="UNIMPLEMENTED" \ No newline at end of file diff --git a/.github/workflows/build-image-tag.yml b/.github/workflows/build-image-tag.yml index fbf5d46e4..46ef2324a 100644 --- a/.github/workflows/build-image-tag.yml +++ b/.github/workflows/build-image-tag.yml @@ -24,21 +24,21 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.release.target_commitish }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 #v3.10.0 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and Push Docker Image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 with: context: . target: prod @@ -58,10 +58,10 @@ jobs: LATEST_TAG: ${{ github.event.release.target_commitish == 'main' && 'thirdweb/engine:latest' || '' }} steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 #v3.10.0 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c96789b9e..c61d654e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,12 +7,12 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.ref }} - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: "18" cache: "yarn" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8f889b22a..52be2d444 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,12 +7,12 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.ref }} - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: "18" cache: "yarn" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6bceec97c..e01e0a8ca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,20 +22,20 @@ jobs: arch: arm64 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 #v3.10.0 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push image id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 with: context: . target: prod @@ -51,10 +51,10 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 #v3.10.0 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 6e447e67f..e2e6b633c 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -8,12 +8,12 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.ref }} - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: "18" cache: "yarn" diff --git a/README.md b/README.md index dc52c03c9..aa5b4148b 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Engine is an open-source, backend HTTP server that provides a production-ready i - [Get Engine hosted by thirdweb](https://thirdweb.com/dashboard/engine?requestCloudHosted) - [Documentation](https://portal.thirdweb.com/engine) -- [Self-host instructions](https://portal.thirdweb.com/engine/self-host) +- [Self-host instructions](https://portal.thirdweb.com/engine/v2/self-host) ## Features diff --git a/package.json b/package.json index 69359687f..d29f8da32 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dependencies": { "@aws-sdk/client-kms": "^3.679.0", "@bull-board/fastify": "^5.23.0", + "@circle-fin/developer-controlled-wallets": "^7.0.0", "@cloud-cryptographic-wallet/cloud-kms-signer": "^0.1.2", "@cloud-cryptographic-wallet/signer": "^0.0.5", "@ethersproject/json-wallets": "^5.7.0", @@ -47,6 +48,7 @@ "aws-kms-signer": "^0.5.3", "base-64": "^1.0.0", "bullmq": "^5.11.0", + "cron": "^4.3.0", "cron-parser": "^4.9.0", "crypto": "^1.0.1", "crypto-js": "^4.2.0", @@ -62,14 +64,15 @@ "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", "mnemonist": "^0.39.8", - "node-cron": "^3.0.2", + "ox": "0.6.9", "pg": "^8.11.3", "prisma": "^5.14.0", "prom-client": "^15.1.3", "superjson": "^2.2.1", - "thirdweb": "^5.83.0", + "thirdweb": "^5.105.42", + "undici": "^6.20.1", "uuid": "^9.0.1", - "viem": "^2.21.54", + "viem": "2.22.17", "winston": "^3.14.1", "zod": "^3.23.8" }, @@ -79,7 +82,6 @@ "@types/crypto-js": "^4.2.2", "@types/jsonwebtoken": "^9.0.6", "@types/node": "^18.15.4", - "@types/node-cron": "^3.0.8", "@types/pg": "^8.6.6", "@types/uuid": "^9.0.1", "@types/ws": "^8.5.5", @@ -100,7 +102,9 @@ "cookie": ">=0.7.0", "elliptic": ">=6.6.0", "micromatch": ">=4.0.8", + "ox": "0.6.9", "secp256k1": ">=4.0.4", + "viem": "2.22.17", "ws": ">=8.17.1", "cross-spawn": ">=7.0.6" }, diff --git a/sdk/package.json b/sdk/package.json index 19a522e9d..5cc6fa596 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@thirdweb-dev/engine", - "version": "0.0.18", + "version": "0.0.19", "main": "dist/thirdweb-dev-engine.cjs.js", "module": "dist/thirdweb-dev-engine.esm.js", "files": [ diff --git a/sdk/src/Engine.ts b/sdk/src/Engine.ts index 8a8e397a2..dba4af76a 100644 --- a/sdk/src/Engine.ts +++ b/sdk/src/Engine.ts @@ -30,6 +30,8 @@ import { MarketplaceOffersService } from './services/MarketplaceOffersService'; import { PermissionsService } from './services/PermissionsService'; import { RelayerService } from './services/RelayerService'; import { TransactionService } from './services/TransactionService'; +import { WalletCredentialsService } from './services/WalletCredentialsService'; +import { WalletSubscriptionsService } from './services/WalletSubscriptionsService'; import { WebhooksService } from './services/WebhooksService'; type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest; @@ -60,6 +62,8 @@ class EngineLogic { public readonly permissions: PermissionsService; public readonly relayer: RelayerService; public readonly transaction: TransactionService; + public readonly walletCredentials: WalletCredentialsService; + public readonly walletSubscriptions: WalletSubscriptionsService; public readonly webhooks: WebhooksService; public readonly request: BaseHttpRequest; @@ -101,6 +105,8 @@ class EngineLogic { this.permissions = new PermissionsService(this.request); this.relayer = new RelayerService(this.request); this.transaction = new TransactionService(this.request); + this.walletCredentials = new WalletCredentialsService(this.request); + this.walletSubscriptions = new WalletSubscriptionsService(this.request); this.webhooks = new WebhooksService(this.request); } } diff --git a/sdk/src/index.ts b/sdk/src/index.ts index bb6705eab..992231be9 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -34,4 +34,6 @@ export { MarketplaceOffersService } from './services/MarketplaceOffersService'; export { PermissionsService } from './services/PermissionsService'; export { RelayerService } from './services/RelayerService'; export { TransactionService } from './services/TransactionService'; +export { WalletCredentialsService } from './services/WalletCredentialsService'; +export { WalletSubscriptionsService } from './services/WalletSubscriptionsService'; export { WebhooksService } from './services/WebhooksService'; diff --git a/sdk/src/services/BackendWalletService.ts b/sdk/src/services/BackendWalletService.ts index 7c25b097f..de88eba66 100644 --- a/sdk/src/services/BackendWalletService.ts +++ b/sdk/src/services/BackendWalletService.ts @@ -17,13 +17,19 @@ export class BackendWalletService { * @throws ApiError */ public create( - requestBody?: { + requestBody?: ({ + label?: string; + type?: ('local' | 'aws-kms' | 'gcp-kms' | 'smart:aws-kms' | 'smart:gcp-kms' | 'smart:local'); + } | { label?: string; + type: ('circle' | 'smart:circle'); /** - * Type of new wallet to create. It is recommended to always provide this value. If not provided, the default wallet type will be used. + * If your engine is configured with a testnet API Key for Circle, you can only create testnet wallets and send testnet transactions. Enable this field for testnet wallets. NOTE: A production API Key cannot be used for testnet transactions, and a testnet API Key cannot be used for production transactions. See: https://developers.circle.com/w3s/sandbox-vs-production */ - type?: ('local' | 'aws-kms' | 'gcp-kms' | 'smart:aws-kms' | 'smart:gcp-kms' | 'smart:local'); - }, + isTestnet?: boolean; + credentialId: string; + walletSetId?: string; + }), ): CancelablePromise<{ result: { /** @@ -31,7 +37,7 @@ export class BackendWalletService { */ walletAddress: string; status: string; - type: ('local' | 'aws-kms' | 'gcp-kms' | 'smart:aws-kms' | 'smart:gcp-kms' | 'smart:local'); + type: ('local' | 'aws-kms' | 'gcp-kms' | 'smart:aws-kms' | 'smart:gcp-kms' | 'smart:local' | 'circle' | 'smart:circle'); }; }> { return this.httpRequest.request({ @@ -489,6 +495,17 @@ export class BackendWalletService { toAddress?: string; data: string; value: string; + authorizationList?: Array<{ + /** + * A contract or wallet address + */ + address: string; + chainId: number; + nonce: string; + 'r': string; + 's': string; + yParity: number; + }>; txOverrides?: { /** * Gas limit for the transaction @@ -721,7 +738,7 @@ export class BackendWalletService { gasPrice?: string; data?: string; value?: string; - chainId?: number; + chainId: number; type?: number; accessList?: any; maxFeePerGas?: string; @@ -808,6 +825,8 @@ export class BackendWalletService { domain: Record; types: Record; value: Record; + primaryType?: string; + chainId?: number; }, xIdempotencyKey?: string, xTransactionMode?: 'sponsored', @@ -1032,7 +1051,7 @@ export class BackendWalletService { * @throws ApiError */ public resetNonces( - requestBody?: { + requestBody: { /** * The chain ID to reset nonces for. */ @@ -1041,6 +1060,10 @@ export class BackendWalletService { * The backend wallet address to reset nonces for. Omit to reset all backend wallets. */ walletAddress?: string; + /** + * Resync nonces to match the onchain transaction count for your backend wallets. (Default: true) + */ + syncOnchainNonces: boolean; }, ): CancelablePromise<{ result: { diff --git a/sdk/src/services/ConfigurationService.ts b/sdk/src/services/ConfigurationService.ts index 57b32942e..40de38b91 100644 --- a/sdk/src/services/ConfigurationService.ts +++ b/sdk/src/services/ConfigurationService.ts @@ -17,7 +17,7 @@ export class ConfigurationService { */ public getWalletsConfiguration(): CancelablePromise<{ result: { - type: ('local' | 'aws-kms' | 'gcp-kms' | 'smart:aws-kms' | 'smart:gcp-kms' | 'smart:local'); + type: ('local' | 'aws-kms' | 'gcp-kms' | 'smart:aws-kms' | 'smart:gcp-kms' | 'smart:local' | 'circle' | 'smart:circle'); awsAccessKeyId: (string | null); awsRegion: (string | null); gcpApplicationProjectId: (string | null); @@ -55,10 +55,12 @@ export class ConfigurationService { gcpKmsKeyRingId: string; gcpApplicationCredentialEmail: string; gcpApplicationCredentialPrivateKey: string; + } | { + circleApiKey: string; }), ): CancelablePromise<{ result: { - type: ('local' | 'aws-kms' | 'gcp-kms' | 'smart:aws-kms' | 'smart:gcp-kms' | 'smart:local'); + type: ('local' | 'aws-kms' | 'gcp-kms' | 'smart:aws-kms' | 'smart:gcp-kms' | 'smart:local' | 'circle' | 'smart:circle'); awsAccessKeyId: (string | null); awsRegion: (string | null); gcpApplicationProjectId: (string | null); @@ -329,7 +331,8 @@ export class ConfigurationService { */ public getAuthConfiguration(): CancelablePromise<{ result: { - domain: string; + authDomain: string; + mtlsCertificate: (string | null); }; }> { return this.httpRequest.request({ @@ -351,12 +354,21 @@ export class ConfigurationService { * @throws ApiError */ public updateAuthConfiguration( - requestBody: { - domain: string; + requestBody?: { + authDomain?: string; + /** + * Engine certificate used for outbound mTLS requests. Must provide the full certificate chain. + */ + mtlsCertificate?: string; + /** + * Engine private key used for outbound mTLS requests. + */ + mtlsPrivateKey?: string; }, ): CancelablePromise<{ result: { - domain: string; + authDomain: string; + mtlsCertificate: (string | null); }; }> { return this.httpRequest.request({ diff --git a/sdk/src/services/ContractEventsService.ts b/sdk/src/services/ContractEventsService.ts index a15c9eab8..d799dd2ec 100644 --- a/sdk/src/services/ContractEventsService.ts +++ b/sdk/src/services/ContractEventsService.ts @@ -23,8 +23,8 @@ export class ContractEventsService { public getAllEvents( chain: string, contractAddress: string, - fromBlock?: (number | string), - toBlock?: (number | string), + fromBlock?: (number | 'latest' | 'earliest' | 'pending' | 'safe' | 'finalized'), + toBlock?: (number | 'latest' | 'earliest' | 'pending' | 'safe' | 'finalized'), order?: ('asc' | 'desc'), ): CancelablePromise<{ result: Array>; @@ -63,8 +63,8 @@ export class ContractEventsService { contractAddress: string, requestBody: { eventName: string; - fromBlock?: (number | string); - toBlock?: (number | string); + fromBlock?: (number | 'latest' | 'earliest' | 'pending' | 'safe' | 'finalized'); + toBlock?: (number | 'latest' | 'earliest' | 'pending' | 'safe' | 'finalized'); order?: ('asc' | 'desc'); filters?: any; }, diff --git a/sdk/src/services/ContractService.ts b/sdk/src/services/ContractService.ts index caef13274..7f3334048 100644 --- a/sdk/src/services/ContractService.ts +++ b/sdk/src/services/ContractService.ts @@ -12,10 +12,11 @@ export class ContractService { /** * Read from contract * Call a read function on a contract. - * @param functionName Name of the function to call on Contract + * @param functionName The function to call on the contract. It is highly recommended to provide a full function signature, such as 'function balanceOf(address owner) view returns (uint256)', to avoid ambiguity and to skip ABI resolution * @param chain A chain ID ("137") or slug ("polygon-amoy-testnet"). Chain ID is preferred. * @param contractAddress Contract address * @param args Arguments for the function. Comma Separated + * @param abi * @returns any Default Response * @throws ApiError */ @@ -24,6 +25,33 @@ export class ContractService { chain: string, contractAddress: string, args?: string, + abi?: Array<{ + type: string; + name?: string; + inputs?: Array<{ + type?: string; + name?: string; + internalType?: string; + stateMutability?: string; + components?: Array<{ + type?: string; + name?: string; + internalType?: string; + }>; + }>; + outputs?: Array<{ + type?: string; + name?: string; + internalType?: string; + stateMutability?: string; + components?: Array<{ + type?: string; + name?: string; + internalType?: string; + }>; + }>; + stateMutability?: string; + }>, ): CancelablePromise<{ result: any; }> { @@ -37,6 +65,7 @@ export class ContractService { query: { 'functionName': functionName, 'args': args, + 'abi': abi, }, errors: { 400: `Bad Request`, diff --git a/sdk/src/services/ContractSubscriptionsService.ts b/sdk/src/services/ContractSubscriptionsService.ts index 2bfbfcca1..5b1c2e9ef 100644 --- a/sdk/src/services/ContractSubscriptionsService.ts +++ b/sdk/src/services/ContractSubscriptionsService.ts @@ -68,7 +68,11 @@ export class ContractSubscriptionsService { */ contractAddress: string; /** - * Webhook URL + * The ID of an existing webhook to use for this contract subscription. Either `webhookId` or `webhookUrl` must be provided. + */ + webhookId?: number; + /** + * Creates a new webhook to call when new onchain data is detected. Either `webhookId` or `webhookUrl` must be provided. */ webhookUrl?: string; /** diff --git a/sdk/src/services/WalletCredentialsService.ts b/sdk/src/services/WalletCredentialsService.ts new file mode 100644 index 000000000..bb973fc73 --- /dev/null +++ b/sdk/src/services/WalletCredentialsService.ts @@ -0,0 +1,172 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CancelablePromise } from '../core/CancelablePromise'; +import type { BaseHttpRequest } from '../core/BaseHttpRequest'; + +export class WalletCredentialsService { + + constructor(public readonly httpRequest: BaseHttpRequest) {} + + /** + * Create wallet credentials + * Create a new set of wallet credentials. + * @param requestBody + * @returns any Default Response + * @throws ApiError + */ + public createWalletCredential( + requestBody: { + label: string; + type: 'circle'; + /** + * 32-byte hex string. Consult https://developers.circle.com/w3s/entity-secret-management to create and register an entity secret. + */ + entitySecret: string; + /** + * Whether this credential should be set as the default for its type. Only one credential can be default per type. + */ + isDefault?: boolean; + }, + ): CancelablePromise<{ + result: { + id: string; + type: string; + label: string; + isDefault: (boolean | null); + createdAt: string; + updatedAt: string; + }; + }> { + return this.httpRequest.request({ + method: 'POST', + url: '/wallet-credentials', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Bad Request`, + 404: `Not Found`, + 500: `Internal Server Error`, + }, + }); + } + + /** + * Get all wallet credentials + * Get all wallet credentials with pagination. + * @param page Specify the page number. + * @param limit Specify the number of results to return per page. + * @returns any Default Response + * @throws ApiError + */ + public getAllWalletCredentials( + page: number = 1, + limit: number = 100, + ): CancelablePromise<{ + result: Array<{ + id: string; + type: string; + label: (string | null); + isDefault: (boolean | null); + createdAt: string; + updatedAt: string; + }>; + }> { + return this.httpRequest.request({ + method: 'GET', + url: '/wallet-credentials', + query: { + 'page': page, + 'limit': limit, + }, + errors: { + 400: `Bad Request`, + 404: `Not Found`, + 500: `Internal Server Error`, + }, + }); + } + + /** + * Get wallet credential + * Get a wallet credential by ID. + * @param id The ID of the wallet credential to get. + * @returns any Default Response + * @throws ApiError + */ + public getWalletCredential( + id: string, + ): CancelablePromise<{ + result: { + id: string; + type: string; + label: (string | null); + isDefault: (boolean | null); + createdAt: string; + updatedAt: string; + deletedAt: (string | null); + }; + }> { + return this.httpRequest.request({ + method: 'GET', + url: '/wallet-credentials/{id}', + path: { + 'id': id, + }, + errors: { + 400: `Bad Request`, + 404: `Not Found`, + 500: `Internal Server Error`, + }, + }); + } + + /** + * Update wallet credential + * Update a wallet credential's label, default status, and entity secret. + * @param id The ID of the wallet credential to update. + * @param requestBody + * @returns any Default Response + * @throws ApiError + */ + public updateWalletCredential( + id: string, + requestBody?: { + label?: string; + /** + * Whether this credential should be set as the default for its type. Only one credential can be default per type. + */ + isDefault?: boolean; + /** + * 32-byte hex string. Consult https://developers.circle.com/w3s/entity-secret-management to create and register an entity secret. + */ + entitySecret?: string; + }, + ): CancelablePromise<{ + result: { + id: string; + type: string; + label: (string | null); + isDefault: (boolean | null); + createdAt: string; + updatedAt: string; + }; + }> { + return this.httpRequest.request({ + method: 'PUT', + url: '/wallet-credentials/{id}', + path: { + 'id': id, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Bad Request`, + 404: `Not Found`, + 500: `Internal Server Error`, + }, + }); + } + +} diff --git a/sdk/src/services/WalletSubscriptionsService.ts b/sdk/src/services/WalletSubscriptionsService.ts new file mode 100644 index 000000000..b699eb887 --- /dev/null +++ b/sdk/src/services/WalletSubscriptionsService.ts @@ -0,0 +1,323 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CancelablePromise } from '../core/CancelablePromise'; +import type { BaseHttpRequest } from '../core/BaseHttpRequest'; + +export class WalletSubscriptionsService { + + constructor(public readonly httpRequest: BaseHttpRequest) {} + + /** + * Get wallet subscriptions + * Get all wallet subscriptions. + * @param page Specify the page number. + * @param limit Specify the number of results to return per page. + * @returns any Default Response + * @throws ApiError + */ + public getAllWalletSubscriptions( + page: number = 1, + limit: number = 100, + ): CancelablePromise<{ + result: Array<{ + id: string; + /** + * The chain ID of the subscription. + */ + chainId: string; + /** + * A contract or wallet address + */ + walletAddress: string; + /** + * Array of conditions to monitor for this wallet + */ + conditions: Array<({ + type: 'token_balance_lt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + } | { + type: 'token_balance_gt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + })>; + webhook?: { + url: string; + }; + createdAt: string; + updatedAt: string; + }>; + }> { + return this.httpRequest.request({ + method: 'GET', + url: '/wallet-subscriptions/get-all', + path: { + 'page': page, + 'limit': limit, + }, + errors: { + 400: `Bad Request`, + 404: `Not Found`, + 500: `Internal Server Error`, + }, + }); + } + + /** + * Add wallet subscription + * Subscribe to wallet conditions. + * @param requestBody + * @returns any Default Response + * @throws ApiError + */ + public addWalletSubscription( + requestBody?: ({ + /** + * A chain ID ("137") or slug ("polygon-amoy-testnet"). Chain ID is preferred. + */ + chain: string; + /** + * A contract or wallet address + */ + walletAddress: string; + /** + * Array of conditions to monitor for this wallet + */ + conditions: Array<({ + type: 'token_balance_lt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + } | { + type: 'token_balance_gt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + })>; + } & ({ + /** + * Webhook URL to create a new webhook + */ + webhookUrl: string; + /** + * Optional label for the webhook when creating a new one + */ + webhookLabel?: string; + } | { + /** + * ID of an existing webhook to use + */ + webhookId: number; + })), + ): CancelablePromise<{ + result: { + id: string; + /** + * The chain ID of the subscription. + */ + chainId: string; + /** + * A contract or wallet address + */ + walletAddress: string; + /** + * Array of conditions to monitor for this wallet + */ + conditions: Array<({ + type: 'token_balance_lt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + } | { + type: 'token_balance_gt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + })>; + webhook?: { + url: string; + }; + createdAt: string; + updatedAt: string; + }; + }> { + return this.httpRequest.request({ + method: 'POST', + url: '/wallet-subscriptions', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Bad Request`, + 404: `Not Found`, + 500: `Internal Server Error`, + }, + }); + } + + /** + * Update wallet subscription + * Update an existing wallet subscription. + * @param subscriptionId The ID of the wallet subscription to update. + * @param requestBody + * @returns any Default Response + * @throws ApiError + */ + public updateWalletSubscription( + subscriptionId: string, + requestBody?: { + /** + * A chain ID ("137") or slug ("polygon-amoy-testnet"). Chain ID is preferred. + */ + chain?: string; + /** + * A contract or wallet address + */ + walletAddress?: string; + /** + * Array of conditions to monitor for this wallet + */ + conditions?: Array<({ + type: 'token_balance_lt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + } | { + type: 'token_balance_gt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + })>; + webhookId?: (number | null); + }, + ): CancelablePromise<{ + result: { + id: string; + /** + * The chain ID of the subscription. + */ + chainId: string; + /** + * A contract or wallet address + */ + walletAddress: string; + /** + * Array of conditions to monitor for this wallet + */ + conditions: Array<({ + type: 'token_balance_lt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + } | { + type: 'token_balance_gt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + })>; + webhook?: { + url: string; + }; + createdAt: string; + updatedAt: string; + }; + }> { + return this.httpRequest.request({ + method: 'POST', + url: '/wallet-subscriptions/{subscriptionId}', + path: { + 'subscriptionId': subscriptionId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Bad Request`, + 404: `Not Found`, + 500: `Internal Server Error`, + }, + }); + } + + /** + * Delete wallet subscription + * Delete an existing wallet subscription. + * @param subscriptionId The ID of the wallet subscription to update. + * @returns any Default Response + * @throws ApiError + */ + public deleteWalletSubscription( + subscriptionId: string, + ): CancelablePromise<{ + result: { + id: string; + /** + * The chain ID of the subscription. + */ + chainId: string; + /** + * A contract or wallet address + */ + walletAddress: string; + /** + * Array of conditions to monitor for this wallet + */ + conditions: Array<({ + type: 'token_balance_lt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + } | { + type: 'token_balance_gt'; + tokenAddress: (string | 'native'); + /** + * The threshold value in wei + */ + value: string; + })>; + webhook?: { + url: string; + }; + createdAt: string; + updatedAt: string; + }; + }> { + return this.httpRequest.request({ + method: 'DELETE', + url: '/wallet-subscriptions/{subscriptionId}', + path: { + 'subscriptionId': subscriptionId, + }, + errors: { + 400: `Bad Request`, + 404: `Not Found`, + 500: `Internal Server Error`, + }, + }); + } + +} diff --git a/sdk/src/services/WebhooksService.ts b/sdk/src/services/WebhooksService.ts index 88604c53b..d06835dbd 100644 --- a/sdk/src/services/WebhooksService.ts +++ b/sdk/src/services/WebhooksService.ts @@ -38,7 +38,7 @@ export class WebhooksService { } /** - * Create a webhook + * Create webhook * Create a webhook to call when a specific Engine event occurs. * @param requestBody * @returns any Default Response @@ -51,7 +51,7 @@ export class WebhooksService { */ url: string; name?: string; - eventType: ('queued_transaction' | 'sent_transaction' | 'mined_transaction' | 'errored_transaction' | 'cancelled_transaction' | 'all_transactions' | 'backend_wallet_balance' | 'auth' | 'contract_subscription'); + eventType: ('queued_transaction' | 'sent_transaction' | 'mined_transaction' | 'errored_transaction' | 'cancelled_transaction' | 'all_transactions' | 'backend_wallet_balance' | 'auth' | 'contract_subscription' | 'wallet_subscription'); }, ): CancelablePromise<{ result: { @@ -113,7 +113,7 @@ export class WebhooksService { * @throws ApiError */ public getEventTypes(): CancelablePromise<{ - result: Array<('queued_transaction' | 'sent_transaction' | 'mined_transaction' | 'errored_transaction' | 'cancelled_transaction' | 'all_transactions' | 'backend_wallet_balance' | 'auth' | 'contract_subscription')>; + result: Array<('queued_transaction' | 'sent_transaction' | 'mined_transaction' | 'errored_transaction' | 'cancelled_transaction' | 'all_transactions' | 'backend_wallet_balance' | 'auth' | 'contract_subscription' | 'wallet_subscription')>; }> { return this.httpRequest.request({ method: 'GET', diff --git a/src/abitype.d.ts b/src/abitype.d.ts new file mode 100644 index 000000000..3d8ad85f0 --- /dev/null +++ b/src/abitype.d.ts @@ -0,0 +1,5 @@ +declare module "abitype" { + export interface Config { + AddressType: string; + } +} diff --git a/src/prisma/migrations/20241031010103_add_mtls_configuration/migration.sql b/src/prisma/migrations/20241031010103_add_mtls_configuration/migration.sql new file mode 100644 index 000000000..9e7a54aac --- /dev/null +++ b/src/prisma/migrations/20241031010103_add_mtls_configuration/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "configuration" ADD COLUMN "mtlsCertificateEncrypted" TEXT, +ADD COLUMN "mtlsPrivateKeyEncrypted" TEXT; diff --git a/src/prisma/migrations/20250204201526_add_wallet_credentials/migration.sql b/src/prisma/migrations/20250204201526_add_wallet_credentials/migration.sql new file mode 100644 index 000000000..9776a0c96 --- /dev/null +++ b/src/prisma/migrations/20250204201526_add_wallet_credentials/migration.sql @@ -0,0 +1,29 @@ +-- AlterTable +ALTER TABLE "configuration" ADD COLUMN "walletProviderConfigs" JSONB NOT NULL DEFAULT '{}'; + +-- AlterTable +ALTER TABLE "wallet_details" ADD COLUMN "credentialId" TEXT, +ADD COLUMN "platformIdentifiers" JSONB; + +-- CreateTable +CREATE TABLE "wallet_credentials" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "label" TEXT NOT NULL, + "data" JSONB NOT NULL, + "isDefault" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "wallet_credentials_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "wallet_credentials_type_idx" ON "wallet_credentials"("type"); + +-- CreateIndex +CREATE UNIQUE INDEX "wallet_credentials_type_is_default_key" ON "wallet_credentials"("type", "isDefault"); + +-- AddForeignKey +ALTER TABLE "wallet_details" ADD CONSTRAINT "wallet_details_credentialId_fkey" FOREIGN KEY ("credentialId") REFERENCES "wallet_credentials"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20250207135644_nullable_is_default/migration.sql b/src/prisma/migrations/20250207135644_nullable_is_default/migration.sql new file mode 100644 index 000000000..3712fa67e --- /dev/null +++ b/src/prisma/migrations/20250207135644_nullable_is_default/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "wallet_credentials" ALTER COLUMN "isDefault" DROP NOT NULL; diff --git a/src/prisma/migrations/20250212235511_wallet_subscriptions/migration.sql b/src/prisma/migrations/20250212235511_wallet_subscriptions/migration.sql new file mode 100644 index 000000000..92fdb940e --- /dev/null +++ b/src/prisma/migrations/20250212235511_wallet_subscriptions/migration.sql @@ -0,0 +1,25 @@ +-- AlterTable +ALTER TABLE "configuration" ADD COLUMN "walletSubscriptionsCronSchedule" TEXT; + +-- CreateTable +CREATE TABLE "wallet_subscriptions" ( + "id" TEXT NOT NULL, + "chainId" TEXT NOT NULL, + "walletAddress" TEXT NOT NULL, + "conditions" JSONB[], + "webhookId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "deletedAt" TIMESTAMP(3), + + CONSTRAINT "wallet_subscriptions_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "wallet_subscriptions_chainId_idx" ON "wallet_subscriptions"("chainId"); + +-- CreateIndex +CREATE INDEX "wallet_subscriptions_walletAddress_idx" ON "wallet_subscriptions"("walletAddress"); + +-- AddForeignKey +ALTER TABLE "wallet_subscriptions" ADD CONSTRAINT "wallet_subscriptions_webhookId_fkey" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 2e04b2d8b..602edf6a4 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -29,6 +29,13 @@ model Configuration { cursorDelaySeconds Int @default(2) @map("cursorDelaySeconds") contractSubscriptionsRetryDelaySeconds String @default("10") @map("contractSubscriptionsRetryDelaySeconds") + walletSubscriptionsCronSchedule String? @map("walletSubscriptionsCronSchedule") + + // Wallet provider specific configurations, non-credential + walletProviderConfigs Json @default("{}") @map("walletProviderConfigs") /// Eg: { "aws": { "defaultAwsRegion": "us-east-1" }, "gcp": { "defaultGcpKmsLocationId": "us-east1-b" } } + + // Legacy wallet provider credentials + // Use default credentials instead, and store non-credential wallet provider configuration in walletProviderConfig // AWS awsAccessKeyId String? @map("awsAccessKeyId") /// global config, precedence goes to WalletDetails awsSecretAccessKey String? @map("awsSecretAccessKey") /// global config, precedence goes to WalletDetails @@ -50,6 +57,9 @@ model Configuration { accessControlAllowOrigin String @default("https://thirdweb.com,https://embed.ipfscdn.io") @map("accessControlAllowOrigin") ipAllowlist String[] @default([]) @map("ipAllowlist") clearCacheCronSchedule String @default("*/30 * * * * *") @map("clearCacheCronSchedule") + // mTLS support + mtlsCertificateEncrypted String? + mtlsPrivateKeyEncrypted String? @@map("configuration") } @@ -76,11 +86,19 @@ model Tokens { } model WalletDetails { - address String @id @map("address") - type String @map("type") - label String? @map("label") + address String @id @map("address") + type String @map("type") + label String? @map("label") + // Local - encryptedJson String? @map("encryptedJson") + encryptedJson String? @map("encryptedJson") + + // New approach: platform identifiers + wallet credentials + platformIdentifiers Json? @map("platformIdentifiers") /// Eg: { "awsKmsArn": "..." } or { "gcpKmsResourcePath": "..." } + credentialId String? @map("credentialId") + credential WalletCredentials? @relation(fields: [credentialId], references: [id]) + + // Legacy AWS KMS fields - use platformIdentifiers + WalletCredentials for new wallets // KMS awsKmsKeyId String? @map("awsKmsKeyId") /// deprecated and unused, todo: remove with next breaking change. Use awsKmsArn awsKmsArn String? @map("awsKmsArn") @@ -94,14 +112,34 @@ model WalletDetails { gcpKmsResourcePath String? @map("gcpKmsResourcePath") @db.Text gcpApplicationCredentialEmail String? @map("gcpApplicationCredentialEmail") /// if not available, default to: Configuration.gcpApplicationCredentialEmail gcpApplicationCredentialPrivateKey String? @map("gcpApplicationCredentialPrivateKey") /// if not available, default to: Configuration.gcpApplicationCredentialPrivateKey + // Smart Backend Wallet - accountSignerAddress String? @map("accountSignerAddress") /// this, and either local, aws or gcp encryptedJson, are required for smart wallet - accountFactoryAddress String? @map("accountFactoryAddress") /// optional even for smart wallet, if not available default factory will be used - entrypointAddress String? @map("entrypointAddress") /// optional even for smart wallet, if not available SDK will use default entrypoint + accountSignerAddress String? @map("accountSignerAddress") /// this, and either local, aws or gcp encryptedJson, are required for smart wallet + accountFactoryAddress String? @map("accountFactoryAddress") /// optional even for smart wallet, if not available default factory will be used + entrypointAddress String? @map("entrypointAddress") /// optional even for smart wallet, if not available SDK will use default entrypoint @@map("wallet_details") } +model WalletCredentials { + id String @id @default(uuid()) + type String + label String + data Json + isDefault Boolean? @default(false) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + wallets WalletDetails[] + + // A maximum of one default credential per type + @@unique([type, isDefault], name: "unique_default_per_type", map: "wallet_credentials_type_is_default_key") + @@index([type]) + @@map("wallet_credentials") +} + model WalletNonce { address String @map("address") chainId String @map("chainId") @@ -185,6 +223,7 @@ model Webhooks { updatedAt DateTime @updatedAt @map("updatedAt") revokedAt DateTime? @map("revokedAt") ContractSubscriptions ContractSubscriptions[] + WalletSubscriptions WalletSubscriptions[] @@map("webhooks") } @@ -250,6 +289,25 @@ model ContractEventLogs { @@map("contract_event_logs") } +model WalletSubscriptions { + id String @id @default(uuid()) + chainId String + walletAddress String + + conditions Json[] // Array of condition objects with discriminated union type + + webhookId Int? + webhook Webhooks? @relation(fields: [webhookId], references: [id], onDelete: SetNull) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + @@index([chainId]) + @@index([walletAddress]) + @@map("wallet_subscriptions") +} + model ContractTransactionReceipts { chainId String blockNumber Int diff --git a/src/scripts/generate-sdk.ts b/src/scripts/generate-sdk.ts index 759dd8f00..13c3ca5fb 100644 --- a/src/scripts/generate-sdk.ts +++ b/src/scripts/generate-sdk.ts @@ -142,7 +142,9 @@ export class Engine extends EngineLogic { const ercServices: string[] = ["erc20", "erc721", "erc1155"]; for (const tag of ercServices) { - const fileName = `${tag.charAt(0).toUpperCase() + tag.slice(1)}Service.ts`; + const fileName = `${ + tag.charAt(0).toUpperCase() + tag.slice(1) + }Service.ts`; const filePath = path.join(servicesDir, fileName); const originalCode = fs.readFileSync(filePath, "utf-8"); diff --git a/src/server/index.ts b/src/server/index.ts index 2628edbae..74509ddaa 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -8,7 +8,6 @@ import { env } from "../shared/utils/env"; import { logger } from "../shared/utils/logger"; import { metricsServer } from "../shared/utils/prometheus"; import { withServerUsageReporting } from "../shared/utils/usage"; -import { updateTxListener } from "./listeners/update-tx-listener"; import { withAdminRoutes } from "./middleware/admin-routes"; import { withAuth } from "./middleware/auth"; import { withCors } from "./middleware/cors"; @@ -19,7 +18,6 @@ import { withOpenApi } from "./middleware/open-api"; import { withPrometheus } from "./middleware/prometheus"; import { withRateLimit } from "./middleware/rate-limit"; import { withSecurityHeaders } from "./middleware/security-headers"; -import { withWebSocket } from "./middleware/websocket"; import { withRoutes } from "./routes"; import { writeOpenApiToFile } from "./utils/openapi"; @@ -82,7 +80,6 @@ export const initServer = async () => { withPrometheus(server); // Register routes - await withWebSocket(server); await withAuth(server); await withOpenApi(server); await withRoutes(server); @@ -132,6 +129,5 @@ export const initServer = async () => { }); writeOpenApiToFile(server); - await updateTxListener(); - await clearCacheCron("server"); + await clearCacheCron(); }; diff --git a/src/server/listeners/update-tx-listener.ts b/src/server/listeners/update-tx-listener.ts deleted file mode 100644 index e7302fd0e..000000000 --- a/src/server/listeners/update-tx-listener.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { knex } from "../../shared/db/client"; -import { TransactionDB } from "../../shared/db/transactions/db"; -import { logger } from "../../shared/utils/logger"; -import { toTransactionSchema } from "../schemas/transaction"; -import { subscriptionsData } from "../schemas/websocket"; -import { - formatSocketMessage, - getStatusMessageAndConnectionStatus, -} from "../utils/websocket"; - -export const updateTxListener = async (): Promise => { - logger({ - service: "server", - level: "info", - message: "Listening for updated transactions", - }); - - const connection = await knex.client.acquireConnection(); - connection.query("LISTEN updated_transaction_data"); - - connection.on( - "notification", - async (msg: { channel: string; payload: string }) => { - const parsedPayload = JSON.parse(msg.payload); - - // Send websocket message - const index = subscriptionsData.findIndex( - (sub) => sub.requestId === parsedPayload.id, - ); - - if (index === -1) { - return; - } - - const userSubscription = subscriptionsData[index]; - const transaction = await TransactionDB.get(parsedPayload.id); - const returnData = transaction ? toTransactionSchema(transaction) : null; - - logger({ - service: "server", - level: "info", - message: `[updateTxListener] Sending websocket update for queueId: ${parsedPayload.id}, status ${returnData?.status}.`, - }); - - const { message, closeConnection } = - await getStatusMessageAndConnectionStatus(returnData); - userSubscription.socket.send( - await formatSocketMessage(returnData, message), - ); - closeConnection ? userSubscription.socket.close() : null; - }, - ); - - connection.on("end", async () => { - logger({ - service: "server", - level: "info", - message: "[updateTxListener] Connection database ended", - }); - - knex.client.releaseConnection(connection); - await knex.destroy(); - - logger({ - service: "server", - level: "info", - message: "[updateTxListener] Released database connection on end", - }); - }); - - connection.on("error", async (err: unknown) => { - logger({ - service: "server", - level: "error", - message: "[updateTxListener] Database connection error", - error: err, - }); - - knex.client.releaseConnection(connection); - await knex.destroy(); - - logger({ - service: "worker", - level: "info", - message: "[updateTxListener] Released database connection on error", - }); - }); -}; diff --git a/src/server/middleware/admin-routes.ts b/src/server/middleware/admin-routes.ts index f503a5b9b..4fd700ab2 100644 --- a/src/server/middleware/admin-routes.ts +++ b/src/server/middleware/admin-routes.ts @@ -15,6 +15,7 @@ import { ProcessTransactionReceiptsQueue } from "../../worker/queues/process-tra import { PruneTransactionsQueue } from "../../worker/queues/prune-transactions-queue"; import { SendTransactionQueue } from "../../worker/queues/send-transaction-queue"; import { SendWebhookQueue } from "../../worker/queues/send-webhook-queue"; +import { WalletSubscriptionQueue } from "../../worker/queues/wallet-subscription-queue"; export const ADMIN_QUEUES_BASEPATH = "/admin/queues"; const ADMIN_ROUTES_USERNAME = "admin"; @@ -31,6 +32,7 @@ const QUEUES: Queue[] = [ PruneTransactionsQueue.q, NonceResyncQueue.q, NonceHealthCheckQueue.q, + WalletSubscriptionQueue.q, ]; export const withAdminRoutes = async (fastify: FastifyInstance) => { diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts index f29804e70..781b617c8 100644 --- a/src/server/middleware/auth.ts +++ b/src/server/middleware/auth.ts @@ -5,10 +5,10 @@ import { type ThirdwebAuthUser, } from "@thirdweb-dev/auth/fastify"; import { AsyncWallet } from "@thirdweb-dev/wallets/evm/wallets/async"; -import { createHash } from "node:crypto"; import type { FastifyInstance } from "fastify"; import type { FastifyRequest } from "fastify/types/request"; import jsonwebtoken, { type JwtPayload } from "jsonwebtoken"; +import { createHash } from "node:crypto"; import { validate as uuidValidate } from "uuid"; import { getPermissions } from "../../shared/db/permissions/get-permissions"; import { createToken } from "../../shared/db/tokens/create-token"; @@ -123,7 +123,8 @@ export async function withAuth(server: FastifyInstance) { } // Allow this request to proceed. return; - }if (error) { + } + if (error) { message = error; } } catch (err: unknown) { @@ -172,10 +173,11 @@ export const onRequest = async ({ const authWallet = await getAuthWallet(); if (publicKey === (await authWallet.getAddress())) { return await handleAccessToken(jwt, req, getUser); - }if (publicKey === THIRDWEB_DASHBOARD_ISSUER) { + } + if (publicKey === THIRDWEB_DASHBOARD_ISSUER) { return await handleDashboardAuth(jwt); } - return await handleKeypairAuth({ jwt, req, publicKey }); + return await handleKeypairAuth({ jwt, req, publicKey }); } // Get the public key hash from the `kid` header. @@ -329,14 +331,12 @@ const handleKeypairAuth = async (args: { }) as jsonwebtoken.JwtPayload; // If `bodyHash` is provided, it must match a hash of the POST request body. - if ( - req.method === "POST" && - payload?.bodyHash && - payload.bodyHash !== hashRequestBody(req) - ) { - error = - "The request body does not match the hash in the access token. See: https://portal.thirdweb.com/engine/features/keypair-authentication"; - throw error; + if (req.method === "POST" && payload?.bodyHash) { + const computedBodyHash = hashRequestBody(req); + if (computedBodyHash !== payload.bodyHash) { + error = `The request body does not match the hash in the access token. See: https://portal.thirdweb.com/engine/v2/features/keypair-authentication. [hash in access token: ${payload.bodyHash}, hash computed from request: ${computedBodyHash}]`; + throw error; + } } const { isAllowed, ip } = await checkIpInAllowlist(req); diff --git a/src/server/middleware/error.ts b/src/server/middleware/error.ts index 92dbf8200..7b61b4ca5 100644 --- a/src/server/middleware/error.ts +++ b/src/server/middleware/error.ts @@ -42,6 +42,13 @@ export const badChainError = (chain: string | number): CustomError => "INVALID_CHAIN", ); +export const badBigIntError = (variableName: string): CustomError => + createCustomError( + `Invalid BigInt: ${variableName}`, + StatusCodes.BAD_REQUEST, + "INVALID_BIGINT", + ); + const flipObject = (data: object) => Object.fromEntries(Object.entries(data).map(([key, value]) => [value, key])); diff --git a/src/server/routes/backend-wallet/create.ts b/src/server/routes/backend-wallet/create.ts index 707616b47..107fde4a6 100644 --- a/src/server/routes/backend-wallet/create.ts +++ b/src/server/routes/backend-wallet/create.ts @@ -6,7 +6,11 @@ import { DEFAULT_ACCOUNT_FACTORY_V0_7, ENTRYPOINT_ADDRESS_v0_7, } from "thirdweb/wallets/smart"; -import { WalletType } from "../../../shared/schemas/wallet"; +import { + LegacyWalletType, + WalletType, + CircleWalletType, +} from "../../../shared/schemas/wallet"; import { getConfig } from "../../../shared/utils/cache/get-config"; import { createCustomError } from "../../middleware/error"; import { AddressSchema } from "../../schemas/address"; @@ -25,16 +29,33 @@ import { createSmartGcpWalletDetails, createSmartLocalWalletDetails, } from "../../utils/wallets/create-smart-wallet"; +import { + CircleWalletError, + createCircleWalletDetails, +} from "../../utils/wallets/circle"; +import assert from "node:assert"; -const requestBodySchema = Type.Object({ - label: Type.Optional(Type.String()), - type: Type.Optional( - Type.Enum(WalletType, { - description: - "Type of new wallet to create. It is recommended to always provide this value. If not provided, the default wallet type will be used.", - }), - ), -}); +const requestBodySchema = Type.Union([ + // Base schema for non-circle wallet types + Type.Object({ + label: Type.Optional(Type.String()), + type: Type.Optional(Type.Union([Type.Enum(LegacyWalletType)])), + }), + + // Schema for circle and smart:circle wallet types + Type.Object({ + label: Type.Optional(Type.String()), + type: Type.Union([Type.Enum(CircleWalletType)]), + isTestnet: Type.Optional( + Type.Boolean({ + description: + "If your engine is configured with a testnet API Key for Circle, you can only create testnet wallets and send testnet transactions. Enable this field for testnet wallets. NOTE: A production API Key cannot be used for testnet transactions, and a testnet API Key cannot be used for production transactions. See: https://developers.circle.com/w3s/sandbox-vs-production", + }), + ), + credentialId: Type.String(), + walletSetId: Type.Optional(Type.String()), + }), +]); const responseSchema = Type.Object({ result: Type.Object({ @@ -112,6 +133,64 @@ export const createBackendWallet = async (fastify: FastifyInstance) => { throw e; } break; + case CircleWalletType.circle: + { + // we need this if here for typescript to statically type the credentialId and walletSetId + assert(req.body.type === "circle", "Expected circle wallet type"); + const { credentialId, walletSetId, isTestnet } = req.body; + + try { + const wallet = await createCircleWalletDetails({ + label, + isSmart: false, + credentialId, + walletSetId, + isTestnet: isTestnet, + }); + + walletAddress = getAddress(wallet.address); + } catch (e) { + if (e instanceof CircleWalletError) { + throw createCustomError( + e.message, + StatusCodes.BAD_REQUEST, + "CREATE_CIRCLE_WALLET_ERROR", + ); + } + throw e; + } + } + break; + + case CircleWalletType.smartCircle: + { + // we need this if here for typescript to statically type the credentialId and walletSetId + assert(req.body.type === "smart:circle", "Expected circle wallet type"); + const { credentialId, walletSetId, isTestnet } = req.body; + + try { + const wallet = await createCircleWalletDetails({ + label, + isSmart: true, + credentialId, + walletSetId, + isTestnet: isTestnet, + }); + + walletAddress = getAddress(wallet.address); + } catch (e) { + if (e instanceof CircleWalletError) { + throw createCustomError( + e.message, + StatusCodes.BAD_REQUEST, + "CREATE_CIRCLE_WALLET_ERROR", + ); + } + throw e; + } + } + break; + case WalletType.smartAwsKms: try { const smartAwsWallet = await createSmartAwsWalletDetails({ @@ -161,12 +240,18 @@ export const createBackendWallet = async (fastify: FastifyInstance) => { walletAddress = getAddress(smartLocalWallet.address); } break; + default: + throw createCustomError( + "Unkown wallet type", + StatusCodes.BAD_REQUEST, + "CREATE_WALLET_ERROR", + ); } reply.status(StatusCodes.OK).send({ result: { walletAddress, - type: walletType, + type: walletType as WalletType, status: "success", }, }); diff --git a/src/server/routes/backend-wallet/reset-nonces.ts b/src/server/routes/backend-wallet/reset-nonces.ts index f94be5b69..74f2d237a 100644 --- a/src/server/routes/backend-wallet/reset-nonces.ts +++ b/src/server/routes/backend-wallet/reset-nonces.ts @@ -21,6 +21,11 @@ const requestBodySchema = Type.Object({ description: "The backend wallet address to reset nonces for. Omit to reset all backend wallets.", }), + syncOnchainNonces: Type.Boolean({ + description: + "Resync nonces to match the onchain transaction count for your backend wallets. (Default: true)", + default: true, + }), }); const responseSchema = Type.Object({ @@ -61,7 +66,11 @@ export const resetBackendWalletNoncesRoute = async ( }, }, handler: async (req, reply) => { - const { chainId, walletAddress: _walletAddress } = req.body; + const { + chainId, + walletAddress: _walletAddress, + syncOnchainNonces, + } = req.body; // If chain+wallet are provided, only process that wallet. // Otherwise process all used wallets that has nonce state. @@ -70,19 +79,21 @@ export const resetBackendWalletNoncesRoute = async ( ? [{ chainId, walletAddress: getAddress(_walletAddress) }] : await getUsedBackendWallets(); - const RESYNC_BATCH_SIZE = 50; - for (let i = 0; i < backendWallets.length; i += RESYNC_BATCH_SIZE) { - const batch = backendWallets.slice(i, i + RESYNC_BATCH_SIZE); + const BATCH_SIZE = 50; + for (let i = 0; i < backendWallets.length; i += BATCH_SIZE) { + const batch = backendWallets.slice(i, i + BATCH_SIZE); // Delete nonce state for these backend wallets. await deleteNoncesForBackendWallets(backendWallets); - // Resync nonces for these backend wallets. - await Promise.allSettled( - batch.map(({ chainId, walletAddress }) => - syncLatestNonceFromOnchain(chainId, walletAddress), - ), - ); + if (syncOnchainNonces) { + // Resync nonces for these backend wallets. + await Promise.allSettled( + batch.map(({ chainId, walletAddress }) => + syncLatestNonceFromOnchain(chainId, walletAddress), + ), + ); + } } reply.status(StatusCodes.OK).send({ diff --git a/src/server/routes/backend-wallet/send-transaction-batch-atomic.ts b/src/server/routes/backend-wallet/send-transaction-batch-atomic.ts index 80506b6e8..5aa29c22f 100644 --- a/src/server/routes/backend-wallet/send-transaction-batch-atomic.ts +++ b/src/server/routes/backend-wallet/send-transaction-batch-atomic.ts @@ -1,7 +1,7 @@ import { Type, type Static } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; -import type { Address, Hex } from "thirdweb"; +import { getAddress, type Address, type Hex } from "thirdweb"; import { insertTransaction } from "../../../shared/utils/transaction/insert-transaction"; import { requestQuerystringSchema, @@ -29,7 +29,9 @@ const requestBodySchema = Type.Object({ }), }); -export async function sendTransactionBatchAtomicRoute(fastify: FastifyInstance) { +export async function sendTransactionBatchAtomicRoute( + fastify: FastifyInstance, +) { fastify.route<{ Params: Static; Body: Static; @@ -113,8 +115,10 @@ export async function sendTransactionBatchAtomicRoute(fastify: FastifyInstance) const queueId = await insertTransaction({ insertedTransaction: { + ...(hasSmartHeaders + ? { isUserOp: true, signerAddress: getAddress(fromAddress) } + : { isUserOp: false }), transactionMode: undefined, - isUserOp: false, chainId, from: fromAddress as Address, accountAddress: maybeAddress(accountAddress, "x-account-address"), diff --git a/src/server/routes/backend-wallet/send-transaction.ts b/src/server/routes/backend-wallet/send-transaction.ts index 13e2a8f3a..84bb80651 100644 --- a/src/server/routes/backend-wallet/send-transaction.ts +++ b/src/server/routes/backend-wallet/send-transaction.ts @@ -1,17 +1,24 @@ -import { Type, type Static } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; -import type { Address, Hex } from "thirdweb"; -import { insertTransaction } from "../../../shared/utils/transaction/insert-transaction"; +import { type Hex, prepareTransaction } from "thirdweb"; +import { getChain } from "../../../shared/utils/chain"; +import { thirdwebClient } from "../../../shared/utils/sdk"; +import { queueTransaction } from "../../../shared/utils/transaction/queue-transation"; import { AddressSchema } from "../../schemas/address"; import { requestQuerystringSchema, standardResponseSchema, transactionWritesResponseSchema, } from "../../schemas/shared-api-schemas"; +import { + authorizationListSchema, + toParsedAuthorization, +} from "../../schemas/transaction/authorization"; import { txOverridesSchema } from "../../schemas/tx-overrides"; import { maybeAddress, + requiredAddress, walletChainParamSchema, walletWithAAHeaderSchema, } from "../../schemas/wallet"; @@ -26,6 +33,7 @@ const requestBodySchema = Type.Object({ value: Type.String({ examples: ["10000000"], }), + authorizationList: authorizationListSchema, ...txOverridesSchema.properties, }); @@ -65,57 +73,47 @@ export async function sendTransaction(fastify: FastifyInstance) { }, handler: async (request, reply) => { const { chain } = request.params; - const { toAddress, data, value, txOverrides } = request.body; + const { toAddress, data, value, txOverrides, authorizationList } = + request.body; const { simulateTx } = request.query; const { "x-backend-wallet-address": fromAddress, "x-idempotency-key": idempotencyKey, "x-account-address": accountAddress, "x-account-factory-address": accountFactoryAddress, + "x-account-salt": accountSalt, "x-transaction-mode": transactionMode, } = request.headers as Static; const chainId = await getChainIdFromChain(chain); + const chainObject = await getChain(chainId); + const { value: valueOverride, overrides } = + parseTransactionOverrides(txOverrides); + const transaction = prepareTransaction({ + client: thirdwebClient, + chain: chainObject, + to: toAddress, + data: data as Hex, + value: BigInt(value) || valueOverride, + authorizationList: authorizationList?.map(toParsedAuthorization), + ...overrides, + }); - let queueId: string; - if (accountAddress) { - queueId = await insertTransaction({ - insertedTransaction: { - isUserOp: true, - chainId, - from: fromAddress as Address, - to: toAddress as Address | undefined, - data: data as Hex, - value: BigInt(value), - accountAddress: accountAddress as Address, - signerAddress: fromAddress as Address, - target: toAddress as Address | undefined, - transactionMode: undefined, - accountFactoryAddress: maybeAddress( - accountFactoryAddress, - "x-account-factory-address", - ), - ...parseTransactionOverrides(txOverrides), - }, - shouldSimulate: simulateTx, - idempotencyKey, - }); - } else { - queueId = await insertTransaction({ - insertedTransaction: { - isUserOp: false, - chainId, - from: fromAddress as Address, - to: toAddress as Address | undefined, - data: data as Hex, - transactionMode: transactionMode, - value: BigInt(value), - ...parseTransactionOverrides(txOverrides), - }, - shouldSimulate: simulateTx, - idempotencyKey, - }); - } + const queueId = await queueTransaction({ + transaction, + fromAddress: requiredAddress(fromAddress, "x-backend-wallet-address"), + toAddress: maybeAddress(toAddress, "to"), + accountAddress: maybeAddress(accountAddress, "x-account-address"), + accountFactoryAddress: maybeAddress( + accountFactoryAddress, + "x-account-factory-address", + ), + accountSalt, + txOverrides, + idempotencyKey, + transactionMode, + shouldSimulate: simulateTx, + }); reply.status(StatusCodes.OK).send({ result: { diff --git a/src/server/routes/backend-wallet/sign-transaction.ts b/src/server/routes/backend-wallet/sign-transaction.ts index 3cab7e0a7..79d62a7c5 100644 --- a/src/server/routes/backend-wallet/sign-transaction.ts +++ b/src/server/routes/backend-wallet/sign-transaction.ts @@ -1,7 +1,13 @@ -import { Type, type Static } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; +import { + type Hex, + prepareTransaction, + toSerializableTransaction, +} from "thirdweb"; import { getAccount } from "../../../shared/utils/account"; +import { getChain } from "../../../shared/utils/chain"; import { getChecksumAddress, maybeBigInt, @@ -11,12 +17,6 @@ import { thirdwebClient } from "../../../shared/utils/sdk"; import { createCustomError } from "../../middleware/error"; import { standardResponseSchema } from "../../schemas/shared-api-schemas"; import { walletHeaderSchema } from "../../schemas/wallet"; -import { - prepareTransaction, - toSerializableTransaction, - type Hex, -} from "thirdweb"; -import { getChain } from "../../../shared/utils/chain"; const requestBodySchema = Type.Object({ transaction: Type.Object({ @@ -90,6 +90,11 @@ export async function signTransaction(fastify: FastifyInstance) { gasPrice: maybeBigInt(transaction.gasPrice), maxFeePerGas: maybeBigInt(transaction.maxFeePerGas), maxPriorityFeePerGas: maybeBigInt(transaction.maxPriorityFeePerGas), + type: transaction.type + ? transaction.type === 2 + ? ("eip1559" as const) + : ("legacy" as const) + : undefined, }; const preparedTransaction = prepareTransaction(prepareTransactionOptions); diff --git a/src/server/routes/backend-wallet/sign-typed-data.ts b/src/server/routes/backend-wallet/sign-typed-data.ts index d668f60ed..42eeb6520 100644 --- a/src/server/routes/backend-wallet/sign-typed-data.ts +++ b/src/server/routes/backend-wallet/sign-typed-data.ts @@ -1,8 +1,13 @@ -import type { TypedDataSigner } from "@ethersproject/abstract-signer"; import { type Static, Type } from "@sinclair/typebox"; +import { ethers } from "ethers"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; -import { getWallet } from "../../../shared/utils/cache/get-wallet"; +import { arbitrumSepolia } from "thirdweb/chains"; +import { isSmartBackendWallet } from "../../../shared/db/wallets/get-wallet-details"; +import { getWalletDetails } from "../../../shared/db/wallets/get-wallet-details"; +import { walletDetailsToAccount } from "../../../shared/utils/account"; +import { getChain } from "../../../shared/utils/chain"; +import { createCustomError } from "../../middleware/error"; import { standardResponseSchema } from "../../schemas/shared-api-schemas"; import { walletHeaderSchema } from "../../schemas/wallet"; @@ -10,6 +15,8 @@ const requestBodySchema = Type.Object({ domain: Type.Object({}, { additionalProperties: true }), types: Type.Object({}, { additionalProperties: true }), value: Type.Object({}, { additionalProperties: true }), + primaryType: Type.Optional(Type.String()), + chainId: Type.Optional(Type.Number()), }); const responseBodySchema = Type.Object({ @@ -36,14 +43,44 @@ export async function signTypedData(fastify: FastifyInstance) { }, }, handler: async (request, reply) => { - const { domain, value, types } = request.body; + const { domain, value, types, chainId, primaryType } = request.body; const { "x-backend-wallet-address": walletAddress } = request.headers as Static; - const wallet = await getWallet({ chainId: 1, walletAddress }); + const walletDetails = await getWalletDetails({ + address: walletAddress, + }); + + if (isSmartBackendWallet(walletDetails) && !chainId) { + throw createCustomError( + "Chain ID is required for signing messages with smart wallets.", + StatusCodes.BAD_REQUEST, + "CHAIN_ID_REQUIRED", + ); + } + + const chain = chainId ? await getChain(chainId) : arbitrumSepolia; + + let parsedPrimaryType = primaryType; + if (!parsedPrimaryType) { + // try to detect the primary type, which requires removing the EIP712Domain type + // biome-ignore lint/performance/noDelete: need to delete explicitely + delete (types as unknown as Record).EIP712Domain; + parsedPrimaryType = + ethers.utils._TypedDataEncoder.getPrimaryType(types); + } + + const { account } = await walletDetailsToAccount({ + walletDetails, + chain, + }); - const signer = (await wallet.getSigner()) as unknown as TypedDataSigner; - const result = await signer._signTypedData(domain, types, value); + const result = await account.signTypedData({ + domain, + types, + primaryType: parsedPrimaryType, + message: value, + } as never); reply.status(StatusCodes.OK).send({ result: result, diff --git a/src/server/routes/backend-wallet/transfer.ts b/src/server/routes/backend-wallet/transfer.ts index 2c3c7d765..c83d30a5e 100644 --- a/src/server/routes/backend-wallet/transfer.ts +++ b/src/server/routes/backend-wallet/transfer.ts @@ -9,11 +9,15 @@ import { type Address, } from "thirdweb"; import { transfer as transferERC20 } from "thirdweb/extensions/erc20"; -import { isContractDeployed, resolvePromisedValue } from "thirdweb/utils"; +import { isContractDeployed } from "thirdweb/utils"; import { getChain } from "../../../shared/utils/chain"; -import { normalizeAddress } from "../../../shared/utils/primitive-types"; +import { + getChecksumAddress, + normalizeAddress, +} from "../../../shared/utils/primitive-types"; import { thirdwebClient } from "../../../shared/utils/sdk"; import { insertTransaction } from "../../../shared/utils/transaction/insert-transaction"; +import { queueTransaction } from "../../../shared/utils/transaction/queue-transation"; import type { InsertedTransaction } from "../../../shared/utils/transaction/types"; import { createCustomError } from "../../middleware/error"; import { AddressSchema } from "../../schemas/address"; @@ -25,7 +29,7 @@ import { } from "../../schemas/shared-api-schemas"; import { txOverridesWithValueSchema } from "../../schemas/tx-overrides"; import { - walletHeaderSchema, + walletWithAAHeaderSchema, walletWithAddressParamSchema, } from "../../schemas/wallet"; import { getChainIdFromChain } from "../../utils/chain"; @@ -70,7 +74,7 @@ export async function transfer(fastify: FastifyInstance) { operationId: "transfer", params: requestSchema, body: requestBodySchema, - headers: walletHeaderSchema, + headers: walletWithAAHeaderSchema, querystring: requestQuerystringSchema, response: { ...standardResponseSchema, @@ -88,31 +92,50 @@ export async function transfer(fastify: FastifyInstance) { const { "x-backend-wallet-address": walletAddress, "x-idempotency-key": idempotencyKey, + "x-account-address": accountAddress, + "x-account-factory-address": accountFactoryAddress, + "x-account-salt": accountSalt, "x-transaction-mode": transactionMode, - } = request.headers as Static; + } = request.headers as Static; const { simulateTx: shouldSimulate } = request.query; // Resolve inputs. const currencyAddress = normalizeAddress(_currencyAddress); const chainId = await getChainIdFromChain(chain); - let insertedTransaction: InsertedTransaction; + let queueId: string; if ( currencyAddress === ZERO_ADDRESS || currencyAddress === NATIVE_TOKEN_ADDRESS ) { - insertedTransaction = { - isUserOp: false, + // Native token transfer - use insertTransaction directly + const insertedTransaction: InsertedTransaction = { chainId, from: walletAddress as Address, to: to as Address, data: "0x", value: toWei(amount), - extension: "none", - functionName: "transfer", transactionMode, ...parseTransactionOverrides(txOverrides), + ...(accountAddress + ? { + isUserOp: true, + accountAddress: getChecksumAddress(accountAddress), + signerAddress: getChecksumAddress(walletAddress), + target: getChecksumAddress(to), + accountFactoryAddress: getChecksumAddress( + accountFactoryAddress, + ), + accountSalt, + } + : { isUserOp: false }), }; + + queueId = await insertTransaction({ + insertedTransaction, + idempotencyKey, + shouldSimulate, + }); } else { const contract = getContract({ client: thirdwebClient, @@ -131,31 +154,25 @@ export async function transfer(fastify: FastifyInstance) { ); } + // ERC20 token transfer - use queueTransaction with PreparedTransaction const transaction = transferERC20({ contract, to, amount }); - insertedTransaction = { - isUserOp: false, - chainId, - from: walletAddress as Address, - to: (await resolvePromisedValue(transaction.to)) as - | Address - | undefined, - data: await resolvePromisedValue(transaction.data), - value: 0n, - extension: "erc20", + queueId = await queueTransaction({ + transaction, + fromAddress: getChecksumAddress(walletAddress), + toAddress: getChecksumAddress(transaction.to), + accountAddress: getChecksumAddress(accountAddress), + accountFactoryAddress: getChecksumAddress(accountFactoryAddress), + accountSalt, + txOverrides, + idempotencyKey, + shouldSimulate, functionName: "transfer", - functionArgs: [to, amount, currencyAddress], + extension: "erc20", transactionMode, - ...parseTransactionOverrides(txOverrides), - }; + }); } - const queueId = await insertTransaction({ - insertedTransaction, - idempotencyKey, - shouldSimulate, - }); - reply.status(StatusCodes.OK).send({ result: { queueId, diff --git a/src/server/routes/configuration/auth/get.ts b/src/server/routes/configuration/auth/get.ts index 0b97d971e..115964dba 100644 --- a/src/server/routes/configuration/auth/get.ts +++ b/src/server/routes/configuration/auth/get.ts @@ -6,7 +6,9 @@ import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; export const responseBodySchema = Type.Object({ result: Type.Object({ - domain: Type.String(), + authDomain: Type.String(), + mtlsCertificate: Type.Union([Type.String(), Type.Null()]), + // Do not return mtlsPrivateKey. }), }); @@ -27,10 +29,12 @@ export async function getAuthConfiguration(fastify: FastifyInstance) { }, }, handler: async (_req, res) => { - const config = await getConfig(); + const { authDomain, mtlsCertificate } = await getConfig(); + res.status(StatusCodes.OK).send({ result: { - domain: config.authDomain, + authDomain, + mtlsCertificate, }, }); }, diff --git a/src/server/routes/configuration/auth/update.ts b/src/server/routes/configuration/auth/update.ts index 50d88a49d..3a1243679 100644 --- a/src/server/routes/configuration/auth/update.ts +++ b/src/server/routes/configuration/auth/update.ts @@ -5,10 +5,21 @@ import { updateConfiguration } from "../../../../shared/db/configuration/update- import { getConfig } from "../../../../shared/utils/cache/get-config"; import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; import { responseBodySchema } from "./get"; +import { createCustomError } from "../../../middleware/error"; +import { encrypt } from "../../../../shared/utils/crypto"; -export const requestBodySchema = Type.Object({ - domain: Type.String(), -}); +export const requestBodySchema = Type.Partial( + Type.Object({ + authDomain: Type.String(), + mtlsCertificate: Type.String({ + description: + "Engine certificate used for outbound mTLS requests. Must provide the full certificate chain.", + }), + mtlsPrivateKey: Type.String({ + description: "Engine private key used for outbound mTLS requests.", + }), + }), +); export async function updateAuthConfiguration(fastify: FastifyInstance) { fastify.route<{ @@ -29,15 +40,49 @@ export async function updateAuthConfiguration(fastify: FastifyInstance) { }, }, handler: async (req, res) => { + const { authDomain, mtlsCertificate, mtlsPrivateKey } = req.body; + + if (mtlsCertificate) { + if ( + !mtlsCertificate.includes("-----BEGIN CERTIFICATE-----\n") || + !mtlsCertificate.includes("\n-----END CERTIFICATE-----") + ) { + throw createCustomError( + "Invalid mtlsCertificate.", + StatusCodes.BAD_REQUEST, + "INVALID_MTLS_CERTIFICATE", + ); + } + } + if (mtlsPrivateKey) { + if ( + !mtlsPrivateKey.startsWith("-----BEGIN PRIVATE KEY-----\n") || + !mtlsPrivateKey.endsWith("\n-----END PRIVATE KEY-----") + ) { + throw createCustomError( + "Invalid mtlsPrivateKey.", + StatusCodes.BAD_REQUEST, + "INVALID_MTLS_PRIVATE_KEY", + ); + } + } + await updateConfiguration({ - authDomain: req.body.domain, + authDomain, + mtlsCertificateEncrypted: mtlsCertificate + ? encrypt(mtlsCertificate) + : undefined, + mtlsPrivateKeyEncrypted: mtlsPrivateKey + ? encrypt(mtlsPrivateKey) + : undefined, }); const config = await getConfig(false); res.status(StatusCodes.OK).send({ result: { - domain: config.authDomain, + authDomain: config.authDomain, + mtlsCertificate: config.mtlsCertificate, }, }); }, diff --git a/src/server/routes/configuration/cache/update.ts b/src/server/routes/configuration/cache/update.ts index 5ea2c035e..c1f88211d 100644 --- a/src/server/routes/configuration/cache/update.ts +++ b/src/server/routes/configuration/cache/update.ts @@ -47,7 +47,7 @@ export async function updateCacheConfiguration(fastify: FastifyInstance) { await updateConfiguration({ ...req.body }); const config = await getConfig(false); // restarting cache cron with updated cron schedule - await clearCacheCron("server"); + await clearCacheCron(); res.status(StatusCodes.OK).send({ result: { clearCacheCronSchedule: config.clearCacheCronSchedule, diff --git a/src/server/routes/configuration/wallet-subscriptions/get.ts b/src/server/routes/configuration/wallet-subscriptions/get.ts new file mode 100644 index 000000000..19d1932a5 --- /dev/null +++ b/src/server/routes/configuration/wallet-subscriptions/get.ts @@ -0,0 +1,42 @@ +import { type Static, Type } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { getConfig } from "../../../../shared/utils/cache/get-config"; +import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; + +const responseBodySchema = Type.Object({ + result: Type.Object({ + walletSubscriptionsCronSchedule: Type.String(), + }), +}); + +export async function getWalletSubscriptionsConfiguration( + fastify: FastifyInstance, +) { + fastify.route<{ + Reply: Static; + }>({ + method: "GET", + url: "/configuration/wallet-subscriptions", + schema: { + summary: "Get wallet subscriptions configuration", + description: + "Get wallet subscriptions configuration including cron schedule", + tags: ["Configuration"], + operationId: "getWalletSubscriptionsConfiguration", + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseBodySchema, + }, + }, + handler: async (_req, res) => { + const config = await getConfig(false); + res.status(StatusCodes.OK).send({ + result: { + walletSubscriptionsCronSchedule: + config.walletSubscriptionsCronSchedule || "*/30 * * * * *", + }, + }); + }, + }); +} diff --git a/src/server/routes/configuration/wallet-subscriptions/update.ts b/src/server/routes/configuration/wallet-subscriptions/update.ts new file mode 100644 index 000000000..94f358130 --- /dev/null +++ b/src/server/routes/configuration/wallet-subscriptions/update.ts @@ -0,0 +1,64 @@ +import { type Static, Type } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { updateConfiguration } from "../../../../shared/db/configuration/update-configuration"; +import { getConfig } from "../../../../shared/utils/cache/get-config"; +import { isValidCron } from "../../../../shared/utils/cron/is-valid-cron"; +import { createCustomError } from "../../../middleware/error"; +import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; + +const requestBodySchema = Type.Object({ + walletSubscriptionsCronSchedule: Type.String({ + description: + "Cron expression for wallet subscription checks. It should be in the format of 'ss mm hh * * *' where ss is seconds, mm is minutes and hh is hours. Seconds should not be '*' or less than 10", + default: "*/30 * * * * *", + }), +}); + +const responseBodySchema = Type.Object({ + result: Type.Object({ + walletSubscriptionsCronSchedule: Type.String(), + }), +}); + +export async function updateWalletSubscriptionsConfiguration( + fastify: FastifyInstance, +) { + fastify.route<{ + Body: Static; + }>({ + method: "POST", + url: "/configuration/wallet-subscriptions", + schema: { + summary: "Update wallet subscriptions configuration", + description: + "Update wallet subscriptions configuration including cron schedule", + tags: ["Configuration"], + operationId: "updateWalletSubscriptionsConfiguration", + body: requestBodySchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseBodySchema, + }, + }, + handler: async (req, res) => { + const { walletSubscriptionsCronSchedule } = req.body; + if (isValidCron(walletSubscriptionsCronSchedule) === false) { + throw createCustomError( + "Invalid cron expression.", + StatusCodes.BAD_REQUEST, + "INVALID_CRON", + ); + } + + await updateConfiguration({ walletSubscriptionsCronSchedule }); + const config = await getConfig(false); + res.status(StatusCodes.OK).send({ + result: { + walletSubscriptionsCronSchedule: + config.walletSubscriptionsCronSchedule, + }, + }); + }, + }); +} diff --git a/src/server/routes/configuration/wallets/update.ts b/src/server/routes/configuration/wallets/update.ts index dd4ed2b96..b807a9604 100644 --- a/src/server/routes/configuration/wallets/update.ts +++ b/src/server/routes/configuration/wallets/update.ts @@ -21,6 +21,9 @@ const requestBodySchema = Type.Union([ gcpApplicationCredentialEmail: Type.String(), gcpApplicationCredentialPrivateKey: Type.String(), }), + Type.Object({ + circleApiKey: Type.String(), + }), ]); requestBodySchema.examples = [ @@ -107,6 +110,16 @@ export async function updateWalletsConfiguration(fastify: FastifyInstance) { }); } + if ("circleApiKey" in req.body) { + await updateConfiguration({ + walletProviderConfigs: { + circle: { + apiKey: req.body.circleApiKey, + }, + }, + }); + } + const config = await getConfig(false); const { legacyWalletType_removeInNextBreakingChange, aws, gcp } = diff --git a/src/server/routes/contract/events/get-all-events.ts b/src/server/routes/contract/events/get-all-events.ts index f0df96e84..517b3079c 100644 --- a/src/server/routes/contract/events/get-all-events.ts +++ b/src/server/routes/contract/events/get-all-events.ts @@ -1,20 +1,26 @@ import { type Static, Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; +import { + type GetContractEventsResult, + type PreparedEvent, + getContract, + getContractEvents, +} from "thirdweb"; +import { getChain } from "../../../../shared/utils/chain"; +import { prettifyError } from "../../../../shared/utils/error"; +import { thirdwebClient } from "../../../../shared/utils/sdk"; +import { createCustomError } from "../../../middleware/error"; import { contractEventSchema, eventsQuerystringSchema, } from "../../../schemas/contract"; +import { toContractEventV4Schema } from "../../../schemas/event"; import { contractParamSchema, standardResponseSchema, } from "../../../schemas/shared-api-schemas"; -import { thirdwebClient } from "../../../../shared/utils/sdk"; -import { getChain } from "../../../../shared/utils/chain"; import { getChainIdFromChain } from "../../../utils/chain"; -import { getContract, getContractEvents } from "thirdweb"; -import { maybeBigInt } from "../../../../shared/utils/primitive-types"; -import { toContractEventV4Schema } from "../../../schemas/event"; const requestSchema = contractParamSchema; @@ -93,11 +99,21 @@ export async function getAllEvents(fastify: FastifyInstance) { chain: await getChain(chainId), }); - const eventsV5 = await getContractEvents({ - contract: contract, - fromBlock: maybeBigInt(fromBlock?.toString()), - toBlock: maybeBigInt(toBlock?.toString()), - }); + let eventsV5: GetContractEventsResult[], true>; + try { + eventsV5 = await getContractEvents({ + contract: contract, + fromBlock: + typeof fromBlock === "number" ? BigInt(fromBlock) : fromBlock, + toBlock: typeof toBlock === "number" ? BigInt(toBlock) : toBlock, + }); + } catch (e) { + throw createCustomError( + `Failed to get events: ${prettifyError(e)}`, + StatusCodes.BAD_REQUEST, + "BAD_REQUEST", + ); + } reply.status(StatusCodes.OK).send({ result: eventsV5.map(toContractEventV4Schema).sort((a, b) => { diff --git a/src/server/routes/contract/extensions/erc1155/read/signature-generate.ts b/src/server/routes/contract/extensions/erc1155/read/signature-generate.ts index ab2c14ebd..abf8aaccc 100644 --- a/src/server/routes/contract/extensions/erc1155/read/signature-generate.ts +++ b/src/server/routes/contract/extensions/erc1155/read/signature-generate.ts @@ -55,6 +55,7 @@ const requestBodySchemaV5 = Type.Intersect([ currency: Type.Optional(Type.String()), validityStartTimestamp: Type.Integer({ minimum: 0 }), validityEndTimestamp: Type.Optional(Type.Integer({ minimum: 0 })), + tokenId: Type.Optional(Type.String()), uid: Type.Optional(Type.String()), }), Type.Union([ @@ -143,8 +144,8 @@ export async function erc1155SignatureGenerate(fastify: FastifyInstance) { pricePerToken, pricePerTokenWei, currency, - validityStartTimestamp, validityEndTimestamp, + tokenId, uid, } = args; @@ -183,9 +184,9 @@ export async function erc1155SignatureGenerate(fastify: FastifyInstance) { royaltyBps, primarySaleRecipient, pricePerToken, + tokenId: maybeBigInt(tokenId), pricePerTokenWei: maybeBigInt(pricePerTokenWei), currency, - validityStartTimestamp: new Date(validityStartTimestamp * 1000), validityEndTimestamp: validityEndTimestamp ? new Date(validityEndTimestamp * 1000) : undefined, @@ -223,6 +224,7 @@ export async function erc1155SignatureGenerate(fastify: FastifyInstance) { royaltyBps, royaltyRecipient, uid, + tokenId, } = request.body as Static; const contract = await getContractV4({ @@ -247,9 +249,17 @@ export async function erc1155SignatureGenerate(fastify: FastifyInstance) { royaltyBps, royaltyRecipient, uid, + tokenId, }); - const signedPayload = await contract.erc1155.signature.generate(payload); + const signedPayload = tokenId + ? await contract.erc1155.signature.generateFromTokenId( + { ...payload, tokenId: BigInt(tokenId) }, + ) + : await contract.erc1155.signature.generate(payload); + + console.log("signedPayload", signedPayload); + reply.status(StatusCodes.OK).send({ result: { ...signedPayload, diff --git a/src/server/routes/contract/read/read-batch.ts b/src/server/routes/contract/read/read-batch.ts index 0a026bacd..256b6f55c 100644 --- a/src/server/routes/contract/read/read-batch.ts +++ b/src/server/routes/contract/read/read-batch.ts @@ -1,4 +1,4 @@ -import { Type, type Static } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; import SuperJSON from "superjson"; @@ -10,14 +10,14 @@ import { resolveMethod, } from "thirdweb"; import { prepareMethod } from "thirdweb/contract"; -import { decodeAbiParameters } from "viem/utils"; import type { AbiFunction } from "viem"; -import { createCustomError } from "../../../middleware/error"; -import { getChainIdFromChain } from "../../../utils/chain"; -import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; +import { decodeAbiParameters } from "viem/utils"; import { getChain } from "../../../../shared/utils/chain"; -import { thirdwebClient } from "../../../../shared/utils/sdk"; import { prettifyError } from "../../../../shared/utils/error"; +import { thirdwebClient } from "../../../../shared/utils/sdk"; +import { createCustomError } from "../../../middleware/error"; +import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; +import { getChainIdFromChain } from "../../../utils/chain"; const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"; @@ -118,7 +118,7 @@ export async function readBatchRoute(fastify: FastifyInstance) { ); // Get Multicall3 contract - const multicall = await getContract({ + const multicall = getContract({ chain, address: multicallAddress, client: thirdwebClient, @@ -132,7 +132,7 @@ export async function readBatchRoute(fastify: FastifyInstance) { }); // Process results - const processedResults = results.map((result: unknown, i) => { + const processedResults = results.map((result: unknown, i: number) => { const { success, returnData } = result as { success: boolean; returnData: unknown; diff --git a/src/server/routes/contract/read/read.ts b/src/server/routes/contract/read/read.ts index c84610df8..d44c8b33f 100644 --- a/src/server/routes/contract/read/read.ts +++ b/src/server/routes/contract/read/read.ts @@ -1,7 +1,11 @@ import { Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; -import { getContract } from "../../../../shared/utils/cache/get-contract"; +import type { AbiParameters } from "ox"; +import { readContract as readContractV5, resolveMethod } from "thirdweb"; +import { parseAbiParams, stringify } from "thirdweb/utils"; +import type { AbiFunction } from "thirdweb/utils"; +import { getContractV5 } from "../../../../shared/utils/cache/get-contractv5"; import { prettifyError } from "../../../../shared/utils/error"; import { createCustomError } from "../../../middleware/error"; import { @@ -12,6 +16,8 @@ import { partialRouteSchema, standardResponseSchema, } from "../../../schemas/shared-api-schemas"; +import { sanitizeFunctionName } from "../../../utils/abi"; +import { sanitizeAbi } from "../../../utils/abi"; import { getChainIdFromChain } from "../../../utils/chain"; import { bigNumberReplacer } from "../../../utils/convertor"; @@ -37,12 +43,13 @@ export async function readContract(fastify: FastifyInstance) { }, handler: async (request, reply) => { const { chain, contractAddress } = request.params; - const { functionName, args } = request.query; + const { functionName, args, abi } = request.query; const chainId = await getChainIdFromChain(chain); - const contract = await getContract({ + const contract = await getContractV5({ chainId, contractAddress, + abi: sanitizeAbi(abi), }); let parsedArgs: unknown[] | undefined; @@ -54,19 +61,33 @@ export async function readContract(fastify: FastifyInstance) { // fallback to string split } - parsedArgs ??= args?.split(",").map((arg) => { - if (arg === "true") { - return true; - } - if (arg === "false") { - return false; - } - return arg; - }); + parsedArgs ??= args?.split(","); + + // 3 possible ways to get function from abi: + // 1. functionName passed as solidity signature + // 2. functionName passed as function name + passed in ABI + // 3. functionName passed as function name + inferred ABI (fetched at encode time) + // this is all handled inside the `resolveMethod` function + let method: AbiFunction; + let params: Array; + try { + const functionNameOrSignature = sanitizeFunctionName(functionName); + method = await resolveMethod(functionNameOrSignature)(contract); + params = parseAbiParams( + method.inputs.map((i: AbiParameters.Parameter) => i.type), + parsedArgs ?? [], + ); + } catch (e) { + throw createCustomError( + prettifyError(e), + StatusCodes.BAD_REQUEST, + "BAD_REQUEST", + ); + } let returnData: unknown; try { - returnData = await contract.call(functionName, parsedArgs ?? []); + returnData = await readContractV5({ contract, method, params }); } catch (e) { throw createCustomError( prettifyError(e), @@ -79,7 +100,7 @@ export async function readContract(fastify: FastifyInstance) { reply.status(StatusCodes.OK).send({ // biome-ignore lint/suspicious/noExplicitAny: data from chain - result: returnData as any, + result: JSON.parse(stringify(returnData)) as any, }); }, }); diff --git a/src/server/routes/contract/subscriptions/add-contract-subscription.ts b/src/server/routes/contract/subscriptions/add-contract-subscription.ts index 57545eda0..e46c5c429 100644 --- a/src/server/routes/contract/subscriptions/add-contract-subscription.ts +++ b/src/server/routes/contract/subscriptions/add-contract-subscription.ts @@ -2,7 +2,7 @@ import { type Static, Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; import { getContract } from "thirdweb"; -import { isContractDeployed } from "thirdweb/utils"; +import { isContractDeployed, shortenAddress } from "thirdweb/utils"; import { upsertChainIndexer } from "../../../../shared/db/chain-indexers/upsert-chain-indexer"; import { createContractSubscription } from "../../../../shared/db/contract-subscriptions/create-contract-subscription"; import { getContractSubscriptionsUniqueChainIds } from "../../../../shared/db/contract-subscriptions/get-contract-subscriptions"; @@ -21,6 +21,8 @@ import { import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; import { getChainIdFromChain } from "../../../utils/chain"; import { isValidWebhookUrl } from "../../../utils/validator"; +import { getWebhook } from "../../../../shared/db/webhooks/get-webhook"; +import type { Webhooks } from "@prisma/client"; const bodySchema = Type.Object({ chain: chainIdOrSlugSchema, @@ -28,9 +30,17 @@ const bodySchema = Type.Object({ ...AddressSchema, description: "The address for the contract.", }, + webhookId: Type.Optional( + Type.Number({ + description: + "The ID of an existing webhook to use for this contract subscription. Either `webhookId` or `webhookUrl` must be provided.", + examples: [1], + }), + ), webhookUrl: Type.Optional( Type.String({ - description: "Webhook URL", + description: + "Creates a new webhook to call when new onchain data is detected. Either `webhookId` or `webhookUrl` must be provided.", examples: ["https://example.com/webhook"], }), ), @@ -91,6 +101,7 @@ export async function addContractSubscription(fastify: FastifyInstance) { const { chain, contractAddress, + webhookId, webhookUrl, processEventLogs, filterEvents = [], @@ -124,6 +135,25 @@ export async function addContractSubscription(fastify: FastifyInstance) { ); } + // Get an existing webhook or create a new one. + let webhook: Webhooks | null = null; + if (webhookId) { + webhook = await getWebhook(webhookId); + } else if (webhookUrl && isValidWebhookUrl(webhookUrl)) { + webhook = await insertWebhook({ + eventType: WebhooksEventTypes.CONTRACT_SUBSCRIPTION, + name: `(Generated) Subscription for ${shortenAddress(contractAddress)}`, + url: webhookUrl, + }); + } + if (!webhook) { + throw createCustomError( + 'Failed to get or create webhook for contract subscription. Make sure you provide an valid "webhookId" or "webhookUrl".', + StatusCodes.BAD_REQUEST, + "INVALID_WEBHOOK", + ); + } + // If not currently indexed, upsert the latest block number. const subscribedChainIds = await getContractSubscriptionsUniqueChainIds(); if (!subscribedChainIds.includes(chainId)) { @@ -137,30 +167,11 @@ export async function addContractSubscription(fastify: FastifyInstance) { } } - // Create the webhook (if provided). - let webhookId: number | undefined; - if (webhookUrl) { - if (!isValidWebhookUrl(webhookUrl)) { - throw createCustomError( - "Invalid webhook URL. Make sure it starts with 'https://'.", - StatusCodes.BAD_REQUEST, - "BAD_REQUEST", - ); - } - - const webhook = await insertWebhook({ - eventType: WebhooksEventTypes.CONTRACT_SUBSCRIPTION, - name: "(Auto-generated)", - url: webhookUrl, - }); - webhookId = webhook.id; - } - // Create the contract subscription. const contractSubscription = await createContractSubscription({ chainId, contractAddress: contractAddress.toLowerCase(), - webhookId, + webhookId: webhook.id, processEventLogs, filterEvents, processTransactionReceipts, diff --git a/src/server/routes/contract/subscriptions/remove-contract-subscription.ts b/src/server/routes/contract/subscriptions/remove-contract-subscription.ts index ef1c4d06c..a91fe69fd 100644 --- a/src/server/routes/contract/subscriptions/remove-contract-subscription.ts +++ b/src/server/routes/contract/subscriptions/remove-contract-subscription.ts @@ -2,7 +2,6 @@ import { type Static, Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; import { deleteContractSubscription } from "../../../../shared/db/contract-subscriptions/delete-contract-subscription"; -import { deleteWebhook } from "../../../../shared/db/webhooks/revoke-webhook"; import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; const bodySchema = Type.Object({ @@ -44,12 +43,7 @@ export async function removeContractSubscription(fastify: FastifyInstance) { handler: async (request, reply) => { const { contractSubscriptionId } = request.body; - const contractSubscription = await deleteContractSubscription( - contractSubscriptionId, - ); - if (contractSubscription.webhookId) { - await deleteWebhook(contractSubscription.webhookId); - } + await deleteContractSubscription(contractSubscriptionId); reply.status(StatusCodes.OK).send({ result: { diff --git a/src/server/routes/contract/write/write.ts b/src/server/routes/contract/write/write.ts index 8b9d6abc3..46c0aea2d 100644 --- a/src/server/routes/contract/write/write.ts +++ b/src/server/routes/contract/write/write.ts @@ -1,8 +1,9 @@ -import { Type, type Static } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; +import type { AbiParameters } from "ox"; import { prepareContractCall, resolveMethod } from "thirdweb"; -import { parseAbiParams, type AbiFunction } from "thirdweb/utils"; +import { type AbiFunction, parseAbiParams } from "thirdweb/utils"; import { getContractV5 } from "../../../../shared/utils/cache/get-contractv5"; import { prettifyError } from "../../../../shared/utils/error"; import { queueTransaction } from "../../../../shared/utils/transaction/queue-transation"; @@ -98,7 +99,7 @@ export async function writeToContract(fastify: FastifyInstance) { const functionNameOrSignature = sanitizeFunctionName(functionName); method = await resolveMethod(functionNameOrSignature)(contract); params = parseAbiParams( - method.inputs.map((i) => i.type), + method.inputs.map((i: AbiParameters.Parameter) => i.type), args, ); } catch (e) { @@ -109,11 +110,13 @@ export async function writeToContract(fastify: FastifyInstance) { ); } + const { value, overrides } = parseTransactionOverrides(txOverrides); const transaction = prepareContractCall({ contract, method, params, - ...parseTransactionOverrides(txOverrides), + value, + ...overrides, }); const queueId = await queueTransaction({ diff --git a/src/server/routes/deploy/prebuilt.ts b/src/server/routes/deploy/prebuilt.ts index 883768b6d..3b18466fa 100644 --- a/src/server/routes/deploy/prebuilt.ts +++ b/src/server/routes/deploy/prebuilt.ts @@ -13,6 +13,12 @@ import { import { txOverridesWithValueSchema } from "../../schemas/tx-overrides"; import { walletWithAAHeaderSchema } from "../../schemas/wallet"; import { getChainIdFromChain } from "../../utils/chain"; +import { getDeployArguments, PREBUILT_CONTRACTS_MAP } from "@thirdweb-dev/sdk"; + +const TW_CLONE_FACTORY_ADDRESS = + "0x25548Ba29a0071F30E4bDCd98Ea72F79341b07a1" as const; +const NFT_COLLECTION_AMEX_IMPLEMENTATION_ADDRESS = + "0x7311B46D12219f21080A84BaA1505E0683E9De99" as const; // INPUTS const requestSchema = prebuiltDeployParamSchema; @@ -85,6 +91,51 @@ export async function deployPrebuilt(fastify: FastifyInstance) { } = request.headers as Static; const sdk = await getSdk({ chainId, walletAddress, accountAddress }); + + if (contractType === "nft-collection-amex") { + const parsedContractMetadata = await PREBUILT_CONTRACTS_MAP[ + "nft-collection" + ].schema.deploy.parseAsync(contractMetadata); + const contractURI = await sdk.storage.upload(parsedContractMetadata); + + const signer = await sdk.getSigner(); + + if (!signer) { + throw new Error("No signer found, please use a wallet address"); + } + + const initializerParams = await getDeployArguments( + "nft-collection", + contractMetadata, + contractURI, + signer, + ); + + const tx = await sdk.deployer.deployViaFactory.prepare( + TW_CLONE_FACTORY_ADDRESS, + NFT_COLLECTION_AMEX_IMPLEMENTATION_ADDRESS, + NFT_COLLECTION_AMEX_ABI, + "initialize", + initializerParams, + saltForProxyDeploy, + ); + + const deployedAddress = (await tx.simulate()) as Address; + + const queueId = await queueTx({ + tx, + chainId, + extension: "deploy-prebuilt", + idempotencyKey, + txOverrides, + }); + + return reply.status(StatusCodes.OK).send({ + deployedAddress, + queueId, + }); + } + const tx = await sdk.deployer.deployBuiltInContract.prepare( contractType, contractMetadata, @@ -112,3 +163,1597 @@ export async function deployPrebuilt(fastify: FastifyInstance) { }, }); } + +const NFT_COLLECTION_AMEX_ABI = [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "CurrencyTransferLibFailedNativeTransfer", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "NFTMetadataFrozen", + type: "error", + }, + { + inputs: [], + name: "NFTMetadataInvalidUrl", + type: "error", + }, + { + inputs: [], + name: "NFTMetadataUnauthorized", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "approved", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: false, + internalType: "bool", + name: "approved", + type: "bool", + }, + ], + name: "ApprovalForAll", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "_fromTokenId", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "_toTokenId", + type: "uint256", + }, + ], + name: "BatchMetadataUpdate", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newRoyaltyRecipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "newRoyaltyBps", + type: "uint256", + }, + ], + name: "DefaultRoyalty", + type: "event", + }, + { + anonymous: false, + inputs: [], + name: "EIP712DomainChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "platformFeeRecipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "flatFee", + type: "uint256", + }, + ], + name: "FlatPlatformFeeUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint8", + name: "version", + type: "uint8", + }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [], + name: "MetadataFrozen", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "_tokenId", + type: "uint256", + }, + ], + name: "MetadataUpdate", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "prevOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnerUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "platformFeeRecipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "platformFeeBps", + type: "uint256", + }, + ], + name: "PlatformFeeInfoUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "enum IPlatformFee.PlatformFeeType", + name: "feeType", + type: "uint8", + }, + ], + name: "PlatformFeeTypeUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipient", + type: "address", + }, + ], + name: "PrimarySaleRecipientUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + indexed: true, + internalType: "bytes32", + name: "previousAdminRole", + type: "bytes32", + }, + { + indexed: true, + internalType: "bytes32", + name: "newAdminRole", + type: "bytes32", + }, + ], + name: "RoleAdminChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleGranted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleRevoked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "royaltyRecipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "royaltyBps", + type: "uint256", + }, + ], + name: "RoyaltyForToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "mintedTo", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenIdMinted", + type: "uint256", + }, + { + indexed: false, + internalType: "string", + name: "uri", + type: "string", + }, + ], + name: "TokensMinted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "signer", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "mintedTo", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenIdMinted", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "address", + name: "royaltyRecipient", + type: "address", + }, + { + internalType: "uint256", + name: "royaltyBps", + type: "uint256", + }, + { + internalType: "address", + name: "primarySaleRecipient", + type: "address", + }, + { + internalType: "string", + name: "uri", + type: "string", + }, + { + internalType: "uint256", + name: "price", + type: "uint256", + }, + { + internalType: "address", + name: "currency", + type: "address", + }, + { + internalType: "uint128", + name: "validityStartTimestamp", + type: "uint128", + }, + { + internalType: "uint128", + name: "validityEndTimestamp", + type: "uint128", + }, + { + internalType: "bytes32", + name: "uid", + type: "bytes32", + }, + ], + indexed: false, + internalType: "struct ITokenERC721.MintRequest", + name: "mintRequest", + type: "tuple", + }, + ], + name: "TokensMintedWithSignature", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + { + inputs: [], + name: "DEFAULT_ADMIN_ROLE", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DEFAULT_FEE_RECIPIENT", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "approve", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "burn", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "contractType", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [], + name: "contractURI", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "contractVersion", + outputs: [ + { + internalType: "uint8", + name: "", + type: "uint8", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [], + name: "eip712Domain", + outputs: [ + { + internalType: "bytes1", + name: "fields", + type: "bytes1", + }, + { + internalType: "string", + name: "name", + type: "string", + }, + { + internalType: "string", + name: "version", + type: "string", + }, + { + internalType: "uint256", + name: "chainId", + type: "uint256", + }, + { + internalType: "address", + name: "verifyingContract", + type: "address", + }, + { + internalType: "bytes32", + name: "salt", + type: "bytes32", + }, + { + internalType: "uint256[]", + name: "extensions", + type: "uint256[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "freezeMetadata", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "getApproved", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getDefaultRoyaltyInfo", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPlatformFeeInfo", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + ], + name: "getRoleAdmin", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + ], + name: "getRoleMember", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + ], + name: "getRoleMemberCount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_tokenId", + type: "uint256", + }, + ], + name: "getRoyaltyInfoForToken", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "uint16", + name: "", + type: "uint16", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "grantRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "hasRole", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_defaultAdmin", + type: "address", + }, + { + internalType: "string", + name: "_name", + type: "string", + }, + { + internalType: "string", + name: "_symbol", + type: "string", + }, + { + internalType: "string", + name: "_contractURI", + type: "string", + }, + { + internalType: "address[]", + name: "_trustedForwarders", + type: "address[]", + }, + { + internalType: "address", + name: "_saleRecipient", + type: "address", + }, + { + internalType: "address", + name: "_royaltyRecipient", + type: "address", + }, + { + internalType: "uint128", + name: "_royaltyBps", + type: "uint128", + }, + { + internalType: "uint128", + name: "_platformFeeBps", + type: "uint128", + }, + { + internalType: "address", + name: "_platformFeeRecipient", + type: "address", + }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "address", + name: "operator", + type: "address", + }, + ], + name: "isApprovedForAll", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_to", + type: "address", + }, + { + internalType: "string", + name: "_uri", + type: "string", + }, + ], + name: "mintTo", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "address", + name: "royaltyRecipient", + type: "address", + }, + { + internalType: "uint256", + name: "royaltyBps", + type: "uint256", + }, + { + internalType: "address", + name: "primarySaleRecipient", + type: "address", + }, + { + internalType: "string", + name: "uri", + type: "string", + }, + { + internalType: "uint256", + name: "price", + type: "uint256", + }, + { + internalType: "address", + name: "currency", + type: "address", + }, + { + internalType: "uint128", + name: "validityStartTimestamp", + type: "uint128", + }, + { + internalType: "uint128", + name: "validityEndTimestamp", + type: "uint128", + }, + { + internalType: "bytes32", + name: "uid", + type: "bytes32", + }, + ], + internalType: "struct ITokenERC721.MintRequest", + name: "_req", + type: "tuple", + }, + { + internalType: "bytes", + name: "_signature", + type: "bytes", + }, + ], + name: "mintWithSignature", + outputs: [ + { + internalType: "uint256", + name: "tokenIdMinted", + type: "uint256", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes[]", + name: "data", + type: "bytes[]", + }, + ], + name: "multicall", + outputs: [ + { + internalType: "bytes[]", + name: "results", + type: "bytes[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "nextTokenIdToMint", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "ownerOf", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "platformFeeRecipient", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "primarySaleRecipient", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "renounceRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "revokeRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + internalType: "uint256", + name: "salePrice", + type: "uint256", + }, + ], + name: "royaltyInfo", + outputs: [ + { + internalType: "address", + name: "receiver", + type: "address", + }, + { + internalType: "uint256", + name: "royaltyAmount", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "operator", + type: "address", + }, + { + internalType: "bool", + name: "approved", + type: "bool", + }, + ], + name: "setApprovalForAll", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "_uri", + type: "string", + }, + ], + name: "setContractURI", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_royaltyRecipient", + type: "address", + }, + { + internalType: "uint256", + name: "_royaltyBps", + type: "uint256", + }, + ], + name: "setDefaultRoyaltyInfo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_newOwner", + type: "address", + }, + ], + name: "setOwner", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_platformFeeRecipient", + type: "address", + }, + { + internalType: "uint256", + name: "_platformFeeBps", + type: "uint256", + }, + ], + name: "setPlatformFeeInfo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_saleRecipient", + type: "address", + }, + ], + name: "setPrimarySaleRecipient", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_tokenId", + type: "uint256", + }, + { + internalType: "address", + name: "_recipient", + type: "address", + }, + { + internalType: "uint256", + name: "_bps", + type: "uint256", + }, + ], + name: "setRoyaltyInfoForToken", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_tokenId", + type: "uint256", + }, + { + internalType: "string", + name: "_uri", + type: "string", + }, + ], + name: "setTokenURI", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceId", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + ], + name: "tokenByIndex", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + ], + name: "tokenOfOwnerByIndex", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_tokenId", + type: "uint256", + }, + ], + name: "tokenURI", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalSupply", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "from", + type: "address", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "uriFrozen", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "address", + name: "royaltyRecipient", + type: "address", + }, + { + internalType: "uint256", + name: "royaltyBps", + type: "uint256", + }, + { + internalType: "address", + name: "primarySaleRecipient", + type: "address", + }, + { + internalType: "string", + name: "uri", + type: "string", + }, + { + internalType: "uint256", + name: "price", + type: "uint256", + }, + { + internalType: "address", + name: "currency", + type: "address", + }, + { + internalType: "uint128", + name: "validityStartTimestamp", + type: "uint128", + }, + { + internalType: "uint128", + name: "validityEndTimestamp", + type: "uint128", + }, + { + internalType: "bytes32", + name: "uid", + type: "bytes32", + }, + ], + internalType: "struct ITokenERC721.MintRequest", + name: "_req", + type: "tuple", + }, + { + internalType: "bytes", + name: "_signature", + type: "bytes", + }, + ], + name: "verify", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 1606debcc..1150458dc 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -104,7 +104,10 @@ import { getAllTransactions } from "./transaction/get-all"; import { getAllDeployedContracts } from "./transaction/get-all-deployed-contracts"; import { retryTransaction } from "./transaction/retry"; import { retryFailedTransactionRoute } from "./transaction/retry-failed"; -import { checkTxStatus } from "./transaction/status"; +import { + getTransactionStatusQueryParamRoute, + getTransactionStatusRoute, +} from "./transaction/status"; import { syncRetryTransactionRoute } from "./transaction/sync-retry"; import { createWebhookRoute } from "./webhooks/create"; import { getWebhooksEventTypes } from "./webhooks/events"; @@ -113,6 +116,14 @@ import { revokeWebhook } from "./webhooks/revoke"; import { testWebhookRoute } from "./webhooks/test"; import { readBatchRoute } from "./contract/read/read-batch"; import { sendTransactionBatchAtomicRoute } from "./backend-wallet/send-transaction-batch-atomic"; +import { createWalletCredentialRoute } from "./wallet-credentials/create"; +import { getWalletCredentialRoute } from "./wallet-credentials/get"; +import { getAllWalletCredentialsRoute } from "./wallet-credentials/get-all"; +import { updateWalletCredentialRoute } from "./wallet-credentials/update"; +import { getAllWalletSubscriptionsRoute } from "./wallet-subscriptions/get-all"; +import { addWalletSubscriptionRoute } from "./wallet-subscriptions/add"; +import { updateWalletSubscriptionRoute } from "./wallet-subscriptions/update"; +import { deleteWalletSubscriptionRoute } from "./wallet-subscriptions/delete"; export async function withRoutes(fastify: FastifyInstance) { // Backend Wallets @@ -137,6 +148,12 @@ export async function withRoutes(fastify: FastifyInstance) { await fastify.register(getBackendWalletNonce); await fastify.register(simulateTransaction); + // Credentials + await fastify.register(createWalletCredentialRoute); + await fastify.register(getWalletCredentialRoute); + await fastify.register(getAllWalletCredentialsRoute); + await fastify.register(updateWalletCredentialRoute); + // Configuration await fastify.register(getWalletsConfiguration); await fastify.register(updateWalletsConfiguration); @@ -225,7 +242,8 @@ export async function withRoutes(fastify: FastifyInstance) { // Transactions await fastify.register(getAllTransactions); - await fastify.register(checkTxStatus); + await fastify.register(getTransactionStatusRoute); + await fastify.register(getTransactionStatusQueryParamRoute); await fastify.register(getAllDeployedContracts); await fastify.register(retryTransaction); await fastify.register(syncRetryTransactionRoute); @@ -258,6 +276,12 @@ export async function withRoutes(fastify: FastifyInstance) { await fastify.register(getContractIndexedBlockRange); await fastify.register(getLatestBlock); + // Wallet Subscriptions + await fastify.register(getAllWalletSubscriptionsRoute); + await fastify.register(addWalletSubscriptionRoute); + await fastify.register(updateWalletSubscriptionRoute); + await fastify.register(deleteWalletSubscriptionRoute); + // Contract Transactions // @deprecated await fastify.register(getContractTransactionReceipts); diff --git a/src/server/routes/system/health.ts b/src/server/routes/system/health.ts index 23672c92f..3613c64de 100644 --- a/src/server/routes/system/health.ts +++ b/src/server/routes/system/health.ts @@ -5,17 +5,20 @@ import { isDatabaseReachable } from "../../../shared/db/client"; import { env } from "../../../shared/utils/env"; import { isRedisReachable } from "../../../shared/utils/redis/redis"; import { thirdwebClientId } from "../../../shared/utils/sdk"; -import { createCustomError } from "../../middleware/error"; type EngineFeature = | "KEYPAIR_AUTH" | "CONTRACT_SUBSCRIPTIONS" | "IP_ALLOWLIST" | "HETEROGENEOUS_WALLET_TYPES" - | "SMART_BACKEND_WALLETS"; + | "SMART_BACKEND_WALLETS" + | "WALLET_CREDENTIALS" + | "BALANCE_SUBSCRIPTIONS"; -const ReplySchemaOk = Type.Object({ - status: Type.String(), +const ReplySchema = Type.Object({ + db: Type.Boolean(), + redis: Type.Boolean(), + auth: Type.Boolean(), engineVersion: Type.Optional(Type.String()), engineTier: Type.Optional(Type.String()), features: Type.Array( @@ -25,20 +28,16 @@ const ReplySchemaOk = Type.Object({ Type.Literal("IP_ALLOWLIST"), Type.Literal("HETEROGENEOUS_WALLET_TYPES"), Type.Literal("SMART_BACKEND_WALLETS"), + Type.Literal("WALLET_CREDENTIALS"), + Type.Literal("BALANCE_SUBSCRIPTIONS"), ]), ), clientId: Type.String(), }); -const ReplySchemaError = Type.Object({ - error: Type.String(), -}); - -const responseBodySchema = Type.Union([ReplySchemaOk, ReplySchemaError]); - export async function healthCheck(fastify: FastifyInstance) { fastify.route<{ - Reply: Static; + Reply: Static; }>({ method: "GET", url: "/system/health", @@ -49,34 +48,27 @@ export async function healthCheck(fastify: FastifyInstance) { tags: ["System"], operationId: "checkHealth", response: { - [StatusCodes.OK]: ReplySchemaOk, - [StatusCodes.SERVICE_UNAVAILABLE]: ReplySchemaError, + [StatusCodes.OK]: ReplySchema, + [StatusCodes.SERVICE_UNAVAILABLE]: ReplySchema, }, }, handler: async (_, res) => { - if (!(await isDatabaseReachable())) { - throw createCustomError( - "The database is unreachable.", - StatusCodes.SERVICE_UNAVAILABLE, - "FAILED_HEALTHCHECK", - ); - } - - if (!(await isRedisReachable())) { - throw createCustomError( - "Redis is unreachable.", - StatusCodes.SERVICE_UNAVAILABLE, - "FAILED_HEALTHCHECK", - ); - } + const db = await isDatabaseReachable(); + const redis = await isRedisReachable(); + const auth = await isAuthValid(); + const isHealthy = db && redis && auth; - res.status(StatusCodes.OK).send({ - status: "OK", - engineVersion: env.ENGINE_VERSION, - engineTier: env.ENGINE_TIER ?? "SELF_HOSTED", - features: getFeatures(), - clientId: thirdwebClientId, - }); + res + .status(isHealthy ? StatusCodes.OK : StatusCodes.SERVICE_UNAVAILABLE) + .send({ + db, + redis, + auth, + engineVersion: env.ENGINE_VERSION, + engineTier: env.ENGINE_TIER ?? "SELF_HOSTED", + features: getFeatures(), + clientId: thirdwebClientId, + }); }, }); } @@ -89,9 +81,24 @@ const getFeatures = (): EngineFeature[] => { "HETEROGENEOUS_WALLET_TYPES", "CONTRACT_SUBSCRIPTIONS", "SMART_BACKEND_WALLETS", + "WALLET_CREDENTIALS", + "BALANCE_SUBSCRIPTIONS", ]; if (env.ENABLE_KEYPAIR_AUTH) features.push("KEYPAIR_AUTH"); return features; }; + +async function isAuthValid() { + try { + const resp = await fetch("https://api.thirdweb.com/v2/keys/use", { + headers: { + "x-secret-key": env.THIRDWEB_API_SECRET_KEY, + }, + }); + return resp.ok; + } catch { + return false; + } +} diff --git a/src/server/routes/transaction/blockchain/get-logs.ts b/src/server/routes/transaction/blockchain/get-logs.ts index 171e1262d..7f4610018 100644 --- a/src/server/routes/transaction/blockchain/get-logs.ts +++ b/src/server/routes/transaction/blockchain/get-logs.ts @@ -1,15 +1,15 @@ -import { Type, type Static } from "@sinclair/typebox"; -import type { AbiEvent } from "abitype"; +import { type Static, Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; +import type { AbiEvent } from "ox"; import superjson from "superjson"; import { + type Hex, eth_getTransactionReceipt, getContract, getRpcClient, parseEventLogs, prepareEvent, - type Hex, } from "thirdweb"; import { resolveContractAbi } from "thirdweb/contract"; import type { TransactionReceipt } from "thirdweb/transaction"; @@ -198,15 +198,33 @@ export async function getTransactionLogs(fastify: FastifyInstance) { ); } - const contract = getContract({ - address: transactionReceipt.to, - chain, - client: thirdwebClient, - }); + const contracts = new Set(); + contracts.add(transactionReceipt.to); + for (const log of transactionReceipt.logs) { + if (log.address) { + contracts.add(log.address); + } + } + + const eventSignaturePromises = Array.from(contracts).map( + async (address) => { + const contract = getContract({ + address, + chain, + client: thirdwebClient, + }); + + const abi: AbiEvent.AbiEvent[] = await resolveContractAbi(contract); + const eventSignatures = abi.filter((item) => item.type === "event"); + return eventSignatures; + }, + ); + + const combinedEventSignatures: AbiEvent.AbiEvent[] = ( + await Promise.all(eventSignaturePromises) + ).flat(); - const abi: AbiEvent[] = await resolveContractAbi(contract); - const eventSignatures = abi.filter((item) => item.type === "event"); - if (eventSignatures.length === 0) { + if (combinedEventSignatures.length === 0) { throw createCustomError( "No events found in contract or could not resolve contract ABI", StatusCodes.BAD_REQUEST, @@ -214,12 +232,14 @@ export async function getTransactionLogs(fastify: FastifyInstance) { ); } - const preparedEvents = eventSignatures.map((signature) => + const preparedEvents = combinedEventSignatures.map((signature) => prepareEvent({ signature }), ); + const parsedLogs = parseEventLogs({ events: preparedEvents, logs: transactionReceipt.logs, + strict: false, }); reply.status(StatusCodes.OK).send({ diff --git a/src/server/routes/transaction/blockchain/send-signed-tx.ts b/src/server/routes/transaction/blockchain/send-signed-tx.ts index 862fc261b..ae1791227 100644 --- a/src/server/routes/transaction/blockchain/send-signed-tx.ts +++ b/src/server/routes/transaction/blockchain/send-signed-tx.ts @@ -1,7 +1,7 @@ import { Type, type Static } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; -import { eth_sendRawTransaction, getRpcClient, isHex } from "thirdweb"; +import { eth_sendRawTransaction, getRpcClient, type Hex, isHex } from "thirdweb"; import { getChain } from "../../../../shared/utils/chain"; import { thirdwebClient } from "../../../../shared/utils/sdk"; import { createCustomError } from "../../../middleware/error"; @@ -9,6 +9,7 @@ import { TransactionHashSchema } from "../../../schemas/address"; import { standardResponseSchema } from "../../../schemas/shared-api-schemas"; import { walletChainParamSchema } from "../../../schemas/wallet"; import { getChainIdFromChain } from "../../../utils/chain"; +import { isInsufficientFundsError, prettifyError } from "../../../../shared/utils/error"; const requestBodySchema = Type.Object({ signedTransaction: Type.String(), @@ -57,10 +58,25 @@ export async function sendSignedTransaction(fastify: FastifyInstance) { client: thirdwebClient, chain: await getChain(chainId), }); - const transactionHash = await eth_sendRawTransaction( - rpcRequest, - signedTransaction, - ); + + let transactionHash: Hex; + try { + transactionHash = await eth_sendRawTransaction( + rpcRequest, + signedTransaction, + ); + } catch (error) { + // Return 400 for client errors. + const isClientError = isInsufficientFundsError(error); + if (isClientError) { + throw createCustomError( + prettifyError(error), + StatusCodes.BAD_REQUEST, + "CLIENT_RPC_ERROR", + ); + } + throw error; + } res.status(StatusCodes.OK).send({ result: { diff --git a/src/server/routes/transaction/cancel.ts b/src/server/routes/transaction/cancel.ts index 866e207eb..0addb6298 100644 --- a/src/server/routes/transaction/cancel.ts +++ b/src/server/routes/transaction/cancel.ts @@ -82,33 +82,39 @@ export async function cancelTransaction(fastify: FastifyInstance) { const message = "Transaction successfully cancelled."; let cancelledTransaction: CancelledTransaction | null = null; - if (!transaction.isUserOp) { - if (transaction.status === "queued") { - // Remove all retries from the SEND_TRANSACTION queue. - const config = await getConfig(); - for ( - let resendCount = 0; - resendCount < config.maxRetriesPerTx; - resendCount++ - ) { - await SendTransactionQueue.remove({ queueId, resendCount }); - } + if (transaction.status === "queued") { + // Remove all retries from the SEND_TRANSACTION queue. + const config = await getConfig(); + for ( + let resendCount = 0; + resendCount < config.maxRetriesPerTx; + resendCount++ + ) { + await SendTransactionQueue.remove({ queueId, resendCount }); + } - cancelledTransaction = { - ...transaction, - status: "cancelled", - cancelledAt: new Date(), + cancelledTransaction = { + ...transaction, + status: "cancelled", + cancelledAt: new Date(), - // Dummy data since the transaction was never sent. - sentAt: new Date(), - sentAtBlock: await getBlockNumberish(transaction.chainId), + // Dummy data since the transaction was never sent. + sentAt: new Date(), + sentAtBlock: await getBlockNumberish(transaction.chainId), - isUserOp: false, - gas: 0n, - nonce: -1, - sentTransactionHashes: [], - }; - } else if (transaction.status === "sent") { + // isUserOp: false, + gas: 0n, + ...(transaction.isUserOp + ? { + userOpHash: + "0x0000000000000000000000000000000000000000000000000000000000000000", + nonce: "cancelled", + isUserOp: true, + } + : { nonce: -1, sentTransactionHashes: [], isUserOp: false }), + }; + } else if (transaction.status === "sent") { + if (!transaction.isUserOp) { // Cancel a sent transaction with the same nonce. const { chainId, from, nonce } = transaction; const transactionHash = await sendCancellationTransaction({ @@ -143,7 +149,10 @@ export async function cancelTransaction(fastify: FastifyInstance) { queueId, status: "success", message, - transactionHash: cancelledTransaction.sentTransactionHashes.at(-1), + transactionHash: + "sentTransactionHashes" in cancelledTransaction + ? cancelledTransaction.sentTransactionHashes.at(-1) + : cancelledTransaction.userOpHash, }, }); }, diff --git a/src/server/routes/transaction/status.ts b/src/server/routes/transaction/status.ts index 60dcb18ba..d18fa0333 100644 --- a/src/server/routes/transaction/status.ts +++ b/src/server/routes/transaction/status.ts @@ -1,23 +1,13 @@ -import type { SocketStream } from "@fastify/websocket"; import { type Static, Type } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; import { TransactionDB } from "../../../shared/db/transactions/db"; -import { logger } from "../../../shared/utils/logger"; import { createCustomError } from "../../middleware/error"; import { standardResponseSchema } from "../../schemas/shared-api-schemas"; import { TransactionSchema, toTransactionSchema, } from "../../schemas/transaction"; -import { - findOrAddWSConnectionInSharedState, - formatSocketMessage, - getStatusMessageAndConnectionStatus, - onClose, - onError, - onMessage, -} from "../../utils/websocket"; // INPUT const requestSchema = Type.Object({ @@ -62,7 +52,7 @@ responseBodySchema.example = { }, }; -export async function checkTxStatus(fastify: FastifyInstance) { +export async function getTransactionStatusRoute(fastify: FastifyInstance) { fastify.route<{ Params: Static; Reply: Static; @@ -96,41 +86,51 @@ export async function checkTxStatus(fastify: FastifyInstance) { result: toTransactionSchema(transaction), }); }, - wsHandler: async (connection: SocketStream, request) => { - const { queueId } = request.params; + }); +} - findOrAddWSConnectionInSharedState(connection, queueId, request); +// An alterate route that accepts the queueId as a query param. +export async function getTransactionStatusQueryParamRoute( + fastify: FastifyInstance, +) { + fastify.route<{ + Querystring: Static; + Reply: Static; + }>({ + method: "GET", + url: "/transaction/status", + schema: { + summary: "Get transaction status", + description: "Get the status for a transaction request.", + tags: ["Transaction"], + operationId: "status", + querystring: requestSchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseBodySchema, + }, + }, + handler: async (request, reply) => { + const { queueId } = request.query; + if (!queueId) { + throw createCustomError( + "Queue ID is required.", + StatusCodes.BAD_REQUEST, + "QUEUE_ID_REQUIRED", + ); + } const transaction = await TransactionDB.get(queueId); - const returnData = transaction ? toTransactionSchema(transaction) : null; - - const { message, closeConnection } = - await getStatusMessageAndConnectionStatus(returnData); - - connection.socket.send(await formatSocketMessage(returnData, message)); - - if (closeConnection) { - connection.socket.close(); - return; + if (!transaction) { + throw createCustomError( + "Transaction not found.", + StatusCodes.BAD_REQUEST, + "TRANSACTION_NOT_FOUND", + ); } - connection.socket.on("error", (error) => { - logger({ - service: "websocket", - level: "error", - message: "Websocket error", - error, - }); - - onError(error, connection, request); - }); - - connection.socket.on("message", async (_message, _isBinary) => { - onMessage(connection, request); - }); - - connection.socket.on("close", () => { - onClose(connection, request); + reply.status(StatusCodes.OK).send({ + result: toTransactionSchema(transaction), }); }, }); diff --git a/src/server/routes/wallet-credentials/create.ts b/src/server/routes/wallet-credentials/create.ts new file mode 100644 index 000000000..5afbffc93 --- /dev/null +++ b/src/server/routes/wallet-credentials/create.ts @@ -0,0 +1,103 @@ +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { createWalletCredential } from "../../../shared/db/wallet-credentials/create-wallet-credential"; +import { standardResponseSchema } from "../../schemas/shared-api-schemas"; +import { type Static, Type } from "@sinclair/typebox"; +import { WalletCredentialsError } from "../../../shared/db/wallet-credentials/get-wallet-credential"; +import { createCustomError } from "../../middleware/error"; + +const requestBodySchema = Type.Object({ + label: Type.String(), + type: Type.Literal("circle"), + entitySecret: Type.String({ + description: + "32-byte hex string. Consult https://developers.circle.com/w3s/entity-secret-management to create and register an entity secret.", + pattern: "^[0-9a-fA-F]{64}$", + }), + isDefault: Type.Optional( + Type.Boolean({ + description: + "Whether this credential should be set as the default for its type. Only one credential can be default per type.", + default: false, + }), + ), +}); + +const responseSchema = Type.Object({ + result: Type.Object({ + id: Type.String(), + type: Type.String(), + label: Type.String(), + isDefault: Type.Union([Type.Boolean(), Type.Null()]), + createdAt: Type.String(), + updatedAt: Type.String(), + }), +}); + +responseSchema.example = { + result: { + id: "123e4567-e89b-12d3-a456-426614174000", + type: "circle", + label: "My Circle Credential", + isDefault: false, + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, +}; + +export const createWalletCredentialRoute = async (fastify: FastifyInstance) => { + fastify.withTypeProvider().route<{ + Body: Static; + Reply: Static; + }>({ + method: "POST", + url: "/wallet-credentials", + schema: { + summary: "Create wallet credentials", + description: "Create a new set of wallet credentials.", + tags: ["Wallet Credentials"], + operationId: "createWalletCredential", + body: requestBodySchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseSchema, + }, + }, + handler: async (req, reply) => { + const { label, type, entitySecret, isDefault } = req.body; + + let createdWalletCredential: Awaited< + ReturnType + > | null = null; + + try { + createdWalletCredential = await createWalletCredential({ + type, + label, + entitySecret, + isDefault, + }); + + reply.status(StatusCodes.OK).send({ + result: { + id: createdWalletCredential.id, + type: createdWalletCredential.type, + label: createdWalletCredential.label, + isDefault: createdWalletCredential.isDefault, + createdAt: createdWalletCredential.createdAt.toISOString(), + updatedAt: createdWalletCredential.updatedAt.toISOString(), + }, + }); + } catch (e: unknown) { + if (e instanceof WalletCredentialsError) { + throw createCustomError( + e.message, + StatusCodes.BAD_REQUEST, + "WALLET_CREDENTIAL_ERROR", + ); + } + throw e; + } + }, + }); +}; diff --git a/src/server/routes/wallet-credentials/get-all.ts b/src/server/routes/wallet-credentials/get-all.ts new file mode 100644 index 000000000..f7a48a654 --- /dev/null +++ b/src/server/routes/wallet-credentials/get-all.ts @@ -0,0 +1,70 @@ +import { Type, type Static } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { getAllWalletCredentials } from "../../../shared/db/wallet-credentials/get-all-wallet-credentials"; +import { standardResponseSchema } from "../../schemas/shared-api-schemas"; +import { PaginationSchema } from "../../schemas/pagination"; + +const QuerySchema = PaginationSchema; + +const responseSchema = Type.Object({ + result: Type.Array( + Type.Object({ + id: Type.String(), + type: Type.String(), + label: Type.Union([Type.String(), Type.Null()]), + isDefault: Type.Union([Type.Boolean(), Type.Null()]), + createdAt: Type.String(), + updatedAt: Type.String(), + }), + ), +}); + +responseSchema.example = { + result: [ + { + id: "123e4567-e89b-12d3-a456-426614174000", + type: "circle", + label: "My Circle Credential", + isDefault: false, + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + deletedAt: null, + }, + ], +}; + +export async function getAllWalletCredentialsRoute(fastify: FastifyInstance) { + fastify.route<{ + Querystring: Static; + Reply: Static; + }>({ + method: "GET", + url: "/wallet-credentials", + schema: { + summary: "Get all wallet credentials", + description: "Get all wallet credentials with pagination.", + tags: ["Wallet Credentials"], + operationId: "getAllWalletCredentials", + querystring: QuerySchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseSchema, + }, + }, + handler: async (req, res) => { + const credentials = await getAllWalletCredentials({ + page: req.query.page, + limit: req.query.limit, + }); + + res.status(StatusCodes.OK).send({ + result: credentials.map((cred) => ({ + ...cred, + createdAt: cred.createdAt.toISOString(), + updatedAt: cred.updatedAt.toISOString(), + })), + }); + }, + }); +} diff --git a/src/server/routes/wallet-credentials/get.ts b/src/server/routes/wallet-credentials/get.ts new file mode 100644 index 000000000..f132c08a7 --- /dev/null +++ b/src/server/routes/wallet-credentials/get.ts @@ -0,0 +1,88 @@ +import { Type, type Static } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { + getWalletCredential, + WalletCredentialsError, +} from "../../../shared/db/wallet-credentials/get-wallet-credential"; +import { createCustomError } from "../../middleware/error"; +import { standardResponseSchema } from "../../schemas/shared-api-schemas"; + +const ParamsSchema = Type.Object({ + id: Type.String({ + description: "The ID of the wallet credential to get.", + }), +}); + +const responseSchema = Type.Object({ + result: Type.Object({ + id: Type.String(), + type: Type.String(), + label: Type.Union([Type.String(), Type.Null()]), + isDefault: Type.Union([Type.Boolean(), Type.Null()]), + createdAt: Type.String(), + updatedAt: Type.String(), + deletedAt: Type.Union([Type.String(), Type.Null()]), + }), +}); + +responseSchema.example = { + result: { + id: "123e4567-e89b-12d3-a456-426614174000", + type: "circle", + label: "My Circle Credential", + isDefault: false, + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + deletedAt: null, + }, +}; + +export async function getWalletCredentialRoute(fastify: FastifyInstance) { + fastify.route<{ + Params: Static; + Reply: Static; + }>({ + method: "GET", + url: "/wallet-credentials/:id", + schema: { + summary: "Get wallet credential", + description: "Get a wallet credential by ID.", + tags: ["Wallet Credentials"], + operationId: "getWalletCredential", + params: ParamsSchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseSchema, + }, + }, + handler: async (req, reply) => { + try { + const credential = await getWalletCredential({ + id: req.params.id, + }); + + reply.status(StatusCodes.OK).send({ + result: { + id: credential.id, + type: credential.type, + label: credential.label, + isDefault: credential.isDefault, + createdAt: credential.createdAt.toISOString(), + updatedAt: credential.updatedAt.toISOString(), + deletedAt: credential.deletedAt?.toISOString() || null, + }, + }); + } catch (e) { + if (e instanceof WalletCredentialsError) { + throw createCustomError( + e.message, + StatusCodes.NOT_FOUND, + "WALLET_CREDENTIAL_NOT_FOUND", + ); + } + throw e; + } + }, + }); +} diff --git a/src/server/routes/wallet-credentials/update.ts b/src/server/routes/wallet-credentials/update.ts new file mode 100644 index 000000000..e7a4eb500 --- /dev/null +++ b/src/server/routes/wallet-credentials/update.ts @@ -0,0 +1,106 @@ +import { Type, type Static } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { updateWalletCredential } from "../../../shared/db/wallet-credentials/update-wallet-credential"; +import { WalletCredentialsError } from "../../../shared/db/wallet-credentials/get-wallet-credential"; +import { createCustomError } from "../../middleware/error"; +import { standardResponseSchema } from "../../schemas/shared-api-schemas"; + +const ParamsSchema = Type.Object({ + id: Type.String({ + description: "The ID of the wallet credential to update.", + }), +}); + +const requestBodySchema = Type.Object({ + label: Type.Optional(Type.String()), + isDefault: Type.Optional( + Type.Boolean({ + description: + "Whether this credential should be set as the default for its type. Only one credential can be default per type.", + }), + ), + entitySecret: Type.Optional( + Type.String({ + description: + "32-byte hex string. Consult https://developers.circle.com/w3s/entity-secret-management to create and register an entity secret.", + pattern: "^[0-9a-fA-F]{64}$", + }), + ), +}); + +const responseSchema = Type.Object({ + result: Type.Object({ + id: Type.String(), + type: Type.String(), + label: Type.Union([Type.String(), Type.Null()]), + isDefault: Type.Union([Type.Boolean(), Type.Null()]), + createdAt: Type.String(), + updatedAt: Type.String(), + }), +}); + +responseSchema.example = { + result: { + id: "123e4567-e89b-12d3-a456-426614174000", + type: "circle", + label: "My Updated Circle Credential", + isDefault: true, + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, +}; + +export async function updateWalletCredentialRoute(fastify: FastifyInstance) { + fastify.route<{ + Params: Static; + Body: Static; + Reply: Static; + }>({ + method: "PUT", + url: "/wallet-credentials/:id", + schema: { + summary: "Update wallet credential", + description: + "Update a wallet credential's label, default status, and entity secret.", + tags: ["Wallet Credentials"], + operationId: "updateWalletCredential", + params: ParamsSchema, + body: requestBodySchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseSchema, + }, + }, + handler: async (req, reply) => { + try { + const credential = await updateWalletCredential({ + id: req.params.id, + label: req.body.label, + isDefault: req.body.isDefault, + entitySecret: req.body.entitySecret, + }); + + reply.status(StatusCodes.OK).send({ + result: { + id: credential.id, + type: credential.type, + label: credential.label, + isDefault: credential.isDefault, + createdAt: credential.createdAt.toISOString(), + updatedAt: credential.updatedAt.toISOString(), + }, + }); + } catch (e) { + if (e instanceof WalletCredentialsError) { + throw createCustomError( + e.message, + StatusCodes.NOT_FOUND, + "WALLET_CREDENTIAL_NOT_FOUND", + ); + } + throw e; + } + }, + }); +} diff --git a/src/server/routes/wallet-subscriptions/add.ts b/src/server/routes/wallet-subscriptions/add.ts new file mode 100644 index 000000000..9239011cf --- /dev/null +++ b/src/server/routes/wallet-subscriptions/add.ts @@ -0,0 +1,122 @@ +import { type Static, Type } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { createWalletSubscription } from "../../../shared/db/wallet-subscriptions/create-wallet-subscription"; +import { insertWebhook } from "../../../shared/db/webhooks/create-webhook"; +import { getWebhook } from "../../../shared/db/webhooks/get-webhook"; +import { WebhooksEventTypes } from "../../../shared/schemas/webhooks"; +import { createCustomError } from "../../middleware/error"; +import { AddressSchema } from "../../schemas/address"; +import { chainIdOrSlugSchema } from "../../schemas/chain"; +import { standardResponseSchema } from "../../schemas/shared-api-schemas"; +import { getChainIdFromChain } from "../../utils/chain"; +import { isValidWebhookUrl } from "../../utils/validator"; +import { + walletSubscriptionSchema, + toWalletSubscriptionSchema, +} from "../../schemas/wallet-subscription"; +import { WalletConditionsSchema } from "../../../shared/schemas/wallet-subscription-conditions"; + +const webhookUrlSchema = Type.Object({ + webhookUrl: Type.String({ + description: "Webhook URL to create a new webhook", + examples: ["https://example.com/webhook"], + }), + webhookLabel: Type.Optional( + Type.String({ + description: "Optional label for the webhook when creating a new one", + examples: ["My Wallet Subscription Webhook"], + minLength: 3, + }), + ), +}); + +const webhookIdSchema = Type.Object({ + webhookId: Type.Integer({ + description: "ID of an existing webhook to use", + }), +}); + +const requestBodySchema = Type.Intersect([ + Type.Object({ + chain: chainIdOrSlugSchema, + walletAddress: AddressSchema, + conditions: WalletConditionsSchema, + }), + Type.Optional(Type.Union([webhookUrlSchema, webhookIdSchema])), +]); + +const responseSchema = Type.Object({ + result: walletSubscriptionSchema, +}); + +export async function addWalletSubscriptionRoute(fastify: FastifyInstance) { + fastify.route<{ + Body: Static; + Reply: Static; + }>({ + method: "POST", + url: "/wallet-subscriptions", + schema: { + summary: "Add wallet subscription", + description: "Subscribe to wallet conditions.", + tags: ["Wallet-Subscriptions"], + operationId: "addWalletSubscription", + body: requestBodySchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseSchema, + }, + }, + handler: async (request, reply) => { + const { chain, walletAddress, conditions } = request.body; + const chainId = await getChainIdFromChain(chain); + + let finalWebhookId: number | undefined; + + if ("webhookUrl" in request.body) { + const { webhookUrl, webhookLabel } = request.body; + + if (!isValidWebhookUrl(webhookUrl)) { + throw createCustomError( + "Invalid webhook URL. Make sure it starts with 'https://'.", + StatusCodes.BAD_REQUEST, + "BAD_REQUEST", + ); + } + + const webhook = await insertWebhook({ + url: webhookUrl, + name: webhookLabel, + eventType: WebhooksEventTypes.WALLET_SUBSCRIPTION, + }); + + finalWebhookId = webhook.id; + } else { + const { webhookId } = request.body; + const webhook = await getWebhook(webhookId); + + if (!webhook || webhook.revokedAt) { + throw createCustomError( + "Invalid webhook ID or webhook has been revoked.", + StatusCodes.BAD_REQUEST, + "BAD_REQUEST", + ); + } + + finalWebhookId = webhookId; + } + + const subscription = await createWalletSubscription({ + chainId: chainId.toString(), + walletAddress, + conditions, + webhookId: finalWebhookId, + }); + + reply.status(StatusCodes.OK).send({ + result: toWalletSubscriptionSchema(subscription), + }); + }, + }); +} diff --git a/src/server/routes/wallet-subscriptions/delete.ts b/src/server/routes/wallet-subscriptions/delete.ts new file mode 100644 index 000000000..947087107 --- /dev/null +++ b/src/server/routes/wallet-subscriptions/delete.ts @@ -0,0 +1,50 @@ +import { type Static, Type } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { deleteWalletSubscription } from "../../../shared/db/wallet-subscriptions/delete-wallet-subscription"; +import { + walletSubscriptionSchema, + toWalletSubscriptionSchema, +} from "../../schemas/wallet-subscription"; +import { standardResponseSchema } from "../../schemas/shared-api-schemas"; + +const responseSchema = Type.Object({ + result: walletSubscriptionSchema, +}); + +const paramsSchema = Type.Object({ + subscriptionId: Type.String({ + description: "The ID of the wallet subscription to update.", + }), +}); + + +export async function deleteWalletSubscriptionRoute(fastify: FastifyInstance) { + fastify.route<{ + Reply: Static; + Params: Static; + }>({ + method: "DELETE", + url: "/wallet-subscriptions/:subscriptionId", + schema: { + summary: "Delete wallet subscription", + description: "Delete an existing wallet subscription.", + tags: ["Wallet-Subscriptions"], + operationId: "deleteWalletSubscription", + params: paramsSchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseSchema, + }, + }, + handler: async (request, reply) => { + const { subscriptionId } = request.params; + + const subscription = await deleteWalletSubscription(subscriptionId); + + reply.status(StatusCodes.OK).send({ + result: toWalletSubscriptionSchema(subscription), + }); + }, + }); +} diff --git a/src/server/routes/wallet-subscriptions/get-all.ts b/src/server/routes/wallet-subscriptions/get-all.ts new file mode 100644 index 000000000..9f5621635 --- /dev/null +++ b/src/server/routes/wallet-subscriptions/get-all.ts @@ -0,0 +1,47 @@ +import { type Static, Type } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { getAllWalletSubscriptions } from "../../../shared/db/wallet-subscriptions/get-all-wallet-subscriptions"; +import { + walletSubscriptionSchema, + toWalletSubscriptionSchema, +} from "../../schemas/wallet-subscription"; +import { standardResponseSchema } from "../../schemas/shared-api-schemas"; +import { PaginationSchema } from "../../schemas/pagination"; + +const responseSchema = Type.Object({ + result: Type.Array(walletSubscriptionSchema), +}); + +export async function getAllWalletSubscriptionsRoute(fastify: FastifyInstance) { + fastify.route<{ + Reply: Static; + Params: Static; + }>({ + method: "GET", + url: "/wallet-subscriptions/get-all", + schema: { + params: PaginationSchema, + summary: "Get wallet subscriptions", + description: "Get all wallet subscriptions.", + tags: ["Wallet-Subscriptions"], + operationId: "getAllWalletSubscriptions", + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseSchema, + }, + }, + handler: async (request, reply) => { + const { page, limit } = request.params; + + const subscriptions = await getAllWalletSubscriptions({ + page, + limit, + }); + + reply.status(StatusCodes.OK).send({ + result: subscriptions.map(toWalletSubscriptionSchema), + }); + }, + }); +} diff --git a/src/server/routes/wallet-subscriptions/update.ts b/src/server/routes/wallet-subscriptions/update.ts new file mode 100644 index 000000000..c4030dcbd --- /dev/null +++ b/src/server/routes/wallet-subscriptions/update.ts @@ -0,0 +1,81 @@ +import { type Static, Type } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { updateWalletSubscription } from "../../../shared/db/wallet-subscriptions/update-wallet-subscription"; +import { WalletConditionsSchema } from "../../../shared/schemas/wallet-subscription-conditions"; +import { AddressSchema } from "../../schemas/address"; +import { chainIdOrSlugSchema } from "../../schemas/chain"; +import { + walletSubscriptionSchema, + toWalletSubscriptionSchema, +} from "../../schemas/wallet-subscription"; +import { standardResponseSchema } from "../../schemas/shared-api-schemas"; +import { getChainIdFromChain } from "../../utils/chain"; + +const requestBodySchema = Type.Object({ + chain: Type.Optional(chainIdOrSlugSchema), + walletAddress: Type.Optional(AddressSchema), + conditions: Type.Optional(WalletConditionsSchema), + webhookId: Type.Optional( + Type.Union([ + Type.Integer({ + description: "The ID of an existing webhook to use.", + }), + Type.Null(), + ]), + ), +}); + +const paramsSchema = Type.Object({ + subscriptionId: Type.String({ + description: "The ID of the wallet subscription to update.", + }), +}); + +const responseSchema = Type.Object({ + result: walletSubscriptionSchema, +}); + +export async function updateWalletSubscriptionRoute(fastify: FastifyInstance) { + fastify.route<{ + Body: Static; + Reply: Static; + Params: Static; + }>({ + method: "POST", + url: "/wallet-subscriptions/:subscriptionId", + schema: { + params: paramsSchema, + summary: "Update wallet subscription", + description: "Update an existing wallet subscription.", + tags: ["Wallet-Subscriptions"], + operationId: "updateWalletSubscription", + body: requestBodySchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseSchema, + }, + }, + handler: async (request, reply) => { + const { subscriptionId } = request.params; + + const { chain, walletAddress, conditions, webhookId } = request.body; + + // Get chainId if chain is provided + const chainId = chain ? await getChainIdFromChain(chain) : undefined; + + // Update the subscription + const subscription = await updateWalletSubscription({ + id: subscriptionId, + chainId: chainId?.toString(), + walletAddress, + conditions, + webhookId, + }); + + reply.status(StatusCodes.OK).send({ + result: toWalletSubscriptionSchema(subscription), + }); + }, + }); +} diff --git a/src/server/routes/webhooks/create.ts b/src/server/routes/webhooks/create.ts index 3d18768f8..a45e66a00 100644 --- a/src/server/routes/webhooks/create.ts +++ b/src/server/routes/webhooks/create.ts @@ -45,7 +45,7 @@ export async function createWebhookRoute(fastify: FastifyInstance) { method: "POST", url: "/webhooks/create", schema: { - summary: "Create a webhook", + summary: "Create webhook", description: "Create a webhook to call when a specific Engine event occurs.", tags: ["Webhooks"], diff --git a/src/server/schemas/contract/index.ts b/src/server/schemas/contract/index.ts index d6abd42eb..8c525c3f5 100644 --- a/src/server/schemas/contract/index.ts +++ b/src/server/schemas/contract/index.ts @@ -1,27 +1,7 @@ -import { Type, type Static } from "@sinclair/typebox"; +import { type Static, Type } from "@sinclair/typebox"; import { AddressSchema } from "../address"; import type { contractSchemaTypes } from "../shared-api-schemas"; -/** - * Basic schema for all Request Query String - */ -export const readRequestQuerySchema = Type.Object({ - functionName: Type.String({ - description: "Name of the function to call on Contract", - examples: ["balanceOf"], - }), - args: Type.Optional( - Type.String({ - description: "Arguments for the function. Comma Separated", - examples: [""], - }), - ), -}); - -export interface readSchema extends contractSchemaTypes { - Querystring: Static; -} - const abiTypeSchema = Type.Object({ type: Type.Optional(Type.String()), name: Type.Optional(Type.String()), @@ -65,6 +45,31 @@ export const abiSchema = Type.Object({ export const abiArraySchema = Type.Array(abiSchema); export type AbiSchemaType = Static; +/** + * Basic schema for all Request Query String + */ +export const readRequestQuerySchema = Type.Object({ + functionName: Type.String({ + description: + "The function to call on the contract. It is highly recommended to provide a full function signature, such as 'function balanceOf(address owner) view returns (uint256)', to avoid ambiguity and to skip ABI resolution", + examples: [ + "function balanceOf(address owner) view returns (uint256)", + "balanceOf", + ], + }), + args: Type.Optional( + Type.String({ + description: "Arguments for the function. Comma Separated", + examples: [""], + }), + ), + abi: Type.Optional(abiArraySchema), +}); + +export interface readSchema extends contractSchemaTypes { + Querystring: Static; +} + export const contractEventSchema = Type.Record(Type.String(), Type.Any()); export const rolesResponseSchema = Type.Object({ @@ -79,18 +84,25 @@ export const rolesResponseSchema = Type.Object({ signer: Type.Array(Type.String()), }); +export const blockNumberOrTagSchema = Type.Union([ + Type.Integer({ minimum: 0 }), + Type.Literal("latest"), + Type.Literal("earliest"), + Type.Literal("pending"), + Type.Literal("safe"), + Type.Literal("finalized"), +]); + export const eventsQuerystringSchema = Type.Object( { - fromBlock: Type.Optional( - Type.Union([Type.Integer({ minimum: 0 }), Type.String()], { - default: "0", - }), - ), - toBlock: Type.Optional( - Type.Union([Type.Integer({ minimum: 0 }), Type.String()], { - default: "latest", - }), - ), + fromBlock: Type.Optional({ + ...blockNumberOrTagSchema, + default: "0", + }), + toBlock: Type.Optional({ + ...blockNumberOrTagSchema, + default: "latest", + }), order: Type.Optional( Type.Union([Type.Literal("asc"), Type.Literal("desc")], { default: "desc", diff --git a/src/server/schemas/event.ts b/src/server/schemas/event.ts index 0860726ea..20d263b94 100644 --- a/src/server/schemas/event.ts +++ b/src/server/schemas/event.ts @@ -1,4 +1,6 @@ import { BigNumber } from "ethers"; +import type { AbiEvent } from "ox"; +import type { GetContractEventsResult, PreparedEvent } from "thirdweb"; export type ContractEventV4 = { eventName: string; @@ -18,32 +20,22 @@ export type ContractEventV4 = { }; }; -export type ContractEventV5 = { - eventName: string; - args: Record; - address: string; - topic: string[]; - data: string; - blockNumber: bigint; - transactionHash: string; - transactionIndex: number; - blockHash: string; - logIndex: number; - removed: boolean; -}; - /** * Mapping of events v5 response to v4 for backward compatiblity. * Clients may be using this api and dont want to break things. */ export function toContractEventV4Schema( - eventV5: ContractEventV5, + eventV5: GetContractEventsResult< + PreparedEvent[], + true + >[number], ): ContractEventV4 { const eventName = eventV5.eventName; // backwards compatibility of BigInt(v5) to BigNumber(v4) const data: Record = {}; for (const key of Object.keys(eventV5.args)) { + // @ts-expect-error - FIXME this is kinda hacky let value = eventV5.args[key]; if (typeof value === "bigint") { value = BigNumber.from(value.toString()); @@ -61,7 +53,7 @@ export function toContractEventV4Schema( removed: eventV5.removed, address: eventV5.address, data: eventV5.data, - topic: eventV5.topic, + topic: eventV5.topics, transactionHash: eventV5.transactionHash, logIndex: eventV5.logIndex, event: eventV5.eventName, diff --git a/src/server/schemas/nft/index.ts b/src/server/schemas/nft/index.ts index f1d646bce..e52e035ae 100644 --- a/src/server/schemas/nft/index.ts +++ b/src/server/schemas/nft/index.ts @@ -302,6 +302,12 @@ export const signature1155InputSchema = Type.Object({ Type.Integer({ minimum: 0 }), ]), ), + tokenId: Type.Optional( + Type.String({ + description: + "The token id to mint. If not provided, a new token id will be generated.", + }), + ), }); export const signature1155OutputSchema = Type.Object({ diff --git a/src/server/schemas/transaction/authorization.ts b/src/server/schemas/transaction/authorization.ts new file mode 100644 index 000000000..3f6e99537 --- /dev/null +++ b/src/server/schemas/transaction/authorization.ts @@ -0,0 +1,30 @@ +import { type Static, Type } from "@sinclair/typebox"; +import { AddressSchema } from "../address"; +import { requiredAddress } from "../wallet"; +import { requiredBigInt } from "../../../shared/utils/primitive-types"; + +export const authorizationSchema = Type.Object({ + address: AddressSchema, + chainId: Type.Integer(), + nonce: Type.String(), + r: Type.String(), + s: Type.String(), + yParity: Type.Number(), +}); + +export const authorizationListSchema = Type.Optional( + Type.Array(authorizationSchema), +); + +export const toParsedAuthorization = ( + authorization: Static, +) => { + return { + address: requiredAddress(authorization.address, "[Authorization List]"), + chainId: authorization.chainId, + nonce: requiredBigInt(authorization.nonce, "[Authorization List] -> nonce"), + r: requiredBigInt(authorization.r, "[Authorization List] -> r"), + s: requiredBigInt(authorization.s, "[Authorization List] -> s"), + yParity: authorization.yParity, + }; +}; diff --git a/src/server/schemas/tx-overrides.ts b/src/server/schemas/tx-overrides.ts index cef4a1574..cb4c88171 100644 --- a/src/server/schemas/tx-overrides.ts +++ b/src/server/schemas/tx-overrides.ts @@ -23,6 +23,13 @@ export const txOverridesSchema = Type.Object({ ...WeiAmountStringSchema, description: "Maximum priority fee per gas", }), + + gasFeeCeiling: Type.Optional({ + ...WeiAmountStringSchema, + description: + "Maximum gas fee for the transaction. This is the total maximum gas fee you are willing to pay for the transaction. If the chain gas conditions are worse than this, the transaction will be delayed until the gas conditions are better. If chain gas conditions are better than this, the transaction will be sent immediately. This value is only used to determine if the transaction should be delayed or sent immediately, and is not used to calculate the actual gas fee for the transaction.", + }), + timeoutSeconds: Type.Optional( Type.Integer({ examples: ["7200"], diff --git a/src/server/schemas/wallet-subscription.ts b/src/server/schemas/wallet-subscription.ts new file mode 100644 index 000000000..8930c01d0 --- /dev/null +++ b/src/server/schemas/wallet-subscription.ts @@ -0,0 +1,48 @@ +import { Type } from "@sinclair/typebox"; +import type { WalletSubscriptions, Webhooks } from "@prisma/client"; +import { AddressSchema } from "./address"; +import { + WalletConditionsSchema, + validateConditions, +} from "../../shared/schemas/wallet-subscription-conditions"; + +type WalletSubscriptionWithWebhook = WalletSubscriptions & { + webhook: Webhooks | null; +}; + +export const walletSubscriptionSchema = Type.Object({ + id: Type.String(), + chainId: Type.String({ + description: "The chain ID of the subscription.", + }), + walletAddress: AddressSchema, + conditions: WalletConditionsSchema, + webhook: Type.Optional( + Type.Object({ + url: Type.String(), + }), + ), + createdAt: Type.String(), + updatedAt: Type.String(), +}); + +export type WalletSubscriptionSchema = typeof walletSubscriptionSchema; + +export function toWalletSubscriptionSchema( + subscription: WalletSubscriptionWithWebhook, +) { + return { + id: subscription.id, + chainId: subscription.chainId, + walletAddress: subscription.walletAddress, + conditions: validateConditions(subscription.conditions), + webhook: + subscription.webhookId && subscription.webhook + ? { + url: subscription.webhook.url, + } + : undefined, + createdAt: subscription.createdAt.toISOString(), + updatedAt: subscription.updatedAt.toISOString(), + }; +} diff --git a/src/server/utils/convertor.ts b/src/server/utils/convertor.ts index 5f0334679..6348d387b 100644 --- a/src/server/utils/convertor.ts +++ b/src/server/utils/convertor.ts @@ -3,8 +3,8 @@ import { BigNumber } from "ethers"; const isHexBigNumber = (value: unknown) => { const isNonNullObject = typeof value === "object" && value !== null; const hasType = isNonNullObject && "type" in value; - return hasType && value.type === "BigNumber" && "hex" in value -} + return hasType && value.type === "BigNumber" && "hex" in value; +}; export const bigNumberReplacer = (value: unknown): unknown => { // if we find a BigNumber then make it into a string (since that is safe) if (BigNumber.isBigNumber(value) || isHexBigNumber(value)) { @@ -15,5 +15,9 @@ export const bigNumberReplacer = (value: unknown): unknown => { return value.map(bigNumberReplacer); } + if (typeof value === "bigint") { + return value.toString(); + } + return value; }; diff --git a/src/server/utils/transaction-overrides.ts b/src/server/utils/transaction-overrides.ts index 443758ef0..8292abb56 100644 --- a/src/server/utils/transaction-overrides.ts +++ b/src/server/utils/transaction-overrides.ts @@ -1,6 +1,5 @@ import type { Static } from "@sinclair/typebox"; import { maybeBigInt } from "../../shared/utils/primitive-types"; -import type { InsertedTransaction } from "../../shared/utils/transaction/types"; import type { txOverridesSchema, txOverridesWithValueSchema, @@ -10,7 +9,7 @@ export const parseTransactionOverrides = ( overrides: | Static["txOverrides"] | Static["txOverrides"], -): Partial => { +) => { if (!overrides) { return {}; } @@ -21,6 +20,7 @@ export const parseTransactionOverrides = ( gasPrice: maybeBigInt(overrides.gasPrice), maxFeePerGas: maybeBigInt(overrides.maxFeePerGas), maxPriorityFeePerGas: maybeBigInt(overrides.maxPriorityFeePerGas), + gasFeeCeiling: maybeBigInt(overrides.gasFeeCeiling), }, timeoutSeconds: overrides.timeoutSeconds, // `value` may not be in the overrides object. diff --git a/src/server/utils/wallets/circle/index.ts b/src/server/utils/wallets/circle/index.ts new file mode 100644 index 000000000..5f1759530 --- /dev/null +++ b/src/server/utils/wallets/circle/index.ts @@ -0,0 +1,338 @@ +import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets"; +import { getConfig } from "../../../../shared/utils/cache/get-config"; +import { getWalletCredential } from "../../../../shared/db/wallet-credentials/get-wallet-credential"; +import { + type Address, + eth_sendRawTransaction, + getRpcClient, + type Hex, + serializeTransaction, + type ThirdwebClient, + toHex, + type toSerializableTransaction, +} from "thirdweb"; +import { getChain } from "../../../../shared/utils/chain"; +import { + parseSignature, + type SignableMessage, + type TypedData, + type TypedDataDefinition, +} from "viem"; +import type { Account } from "thirdweb/wallets"; +import { thirdwebClient } from "../../../../shared/utils/sdk"; +import { prisma } from "../../../../shared/db/client"; +import { getConnectedSmartWallet } from "../create-smart-wallet"; +import { + DEFAULT_ACCOUNT_FACTORY_V0_7, + ENTRYPOINT_ADDRESS_v0_7, +} from "thirdweb/wallets/smart"; +import { stringify } from "thirdweb/utils"; + +export class CircleWalletError extends Error { + constructor(message: string) { + super(message); + this.name = "CircleWalletError"; + } +} + +export async function provisionCircleWallet({ + entitySecret, + apiKey, + walletSetId, + client, + isTestnet, +}: { + entitySecret: string; + apiKey: string; + walletSetId?: string; + client: ThirdwebClient; + isTestnet?: boolean; +}) { + const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ + apiKey: apiKey, + entitySecret: entitySecret, + }); + + if (!walletSetId) { + const walletSet = await circleDeveloperSdk + .createWalletSet({ + name: `Engine WalletSet ${new Date().toISOString()}`, + }) + .catch((e) => { + throw new CircleWalletError( + `[Circle] Could not create walletset:\n${JSON.stringify( + e?.response?.data, + )}`, + ); + }); + + walletSetId = walletSet.data?.walletSet.id; + if (!walletSetId) + throw new CircleWalletError( + "Did not receive walletSetId, and failed to create one automatically", + ); + } + + const provisionWalletResponse = await circleDeveloperSdk + .createWallets({ + accountType: "EOA", + blockchains: [isTestnet ? "EVM-TESTNET" : "EVM"], + count: 1, + walletSetId: walletSetId, + }) + .catch((e) => { + throw new CircleWalletError( + `[Circle] Could not provision wallet:\n${JSON.stringify( + e?.response?.data, + )}`, + ); + }); + + const provisionedWallet = provisionWalletResponse.data?.wallets?.[0]; + + if (!provisionedWallet) + throw new CircleWalletError("Did not receive provisioned wallet"); + + const circleAccount = await getCircleAccount({ + walletId: provisionedWallet.id, + apiKey: apiKey, + entitySecret: entitySecret, + client, + }); + + return { + walletSetId, + provisionedWallet: provisionedWallet, + account: circleAccount, + }; +} + +type SerializableTransaction = Awaited< + ReturnType +>; + +type SendTransactionOptions = SerializableTransaction & { + chainId: number; +}; + +type SendTransactionResult = { + transactionHash: Hex; +}; + +type CircleAccount = Account; + +export async function getCircleAccount({ + walletId, + apiKey, + entitySecret, + client, +}: { + walletId: string; + apiKey: string; + entitySecret: string; + client: ThirdwebClient; +}) { + const circleDeveloperSdk = initiateDeveloperControlledWalletsClient({ + apiKey, + entitySecret, + }); + + const walletResponse = await circleDeveloperSdk + .getWallet({ id: walletId }) + .catch((e) => { + throw new CircleWalletError( + `[Circle] Could not get wallet with id:${walletId}:\n${JSON.stringify( + e?.response?.data, + )}`, + ); + }); + + if (!walletResponse) { + throw new CircleWalletError( + `Unable to get circle wallet with id:${walletId}`, + ); + } + const wallet = walletResponse.data?.wallet; + const address = wallet?.address as Address; + + async function signTransaction(tx: SerializableTransaction) { + const signature = await circleDeveloperSdk + .signTransaction({ + walletId, + transaction: stringify(tx), + }) + .catch((e) => { + throw new CircleWalletError( + `[Circle] Could not get transaction signature:\n${JSON.stringify( + e?.response?.data, + )}`, + ); + }); + + if (!signature.data?.signature) { + throw new CircleWalletError("Unable to sign transaction"); + } + + return signature.data.signature as Hex; + } + + async function sendTransaction( + tx: SendTransactionOptions, + ): Promise { + const rpcRequest = getRpcClient({ + client: client, + chain: await getChain(tx.chainId), + }); + + const signature = await signTransaction(tx); + const splittedSignature = parseSignature(signature); + + const signedTransaction = serializeTransaction({ + transaction: tx, + signature: splittedSignature, + }); + + const transactionHash = await eth_sendRawTransaction( + rpcRequest, + signedTransaction, + ); + return { transactionHash }; + } + + async function signTypedData< + const typedData extends TypedData | Record, + primaryType extends keyof typedData | "EIP712Domain" = keyof typedData, + >(_typedData: TypedDataDefinition): Promise { + const signatureResponse = await circleDeveloperSdk + .signTypedData({ + data: stringify(_typedData), + walletId, + }) + .catch((e) => { + throw new CircleWalletError( + `[Circle] Could not get signature:\n${JSON.stringify( + e?.response?.data, + )}`, + ); + }); + + if (!signatureResponse.data?.signature) { + throw new CircleWalletError("Could not sign typed data"); + } + + return signatureResponse.data?.signature as Hex; + } + + async function signMessage({ + message, + }: { + message: SignableMessage; + }): Promise { + const isRawMessage = typeof message === "object" && "raw" in message; + let messageToSign = isRawMessage ? message.raw : message; + + if (typeof messageToSign !== "string") { + messageToSign = toHex(messageToSign); + } + + const signatureResponse = await circleDeveloperSdk + .signMessage({ + walletId, + message: messageToSign, + encodedByHex: isRawMessage, + }) + .catch((e) => { + throw new CircleWalletError( + `[Circle] Could not get signature:\n${JSON.stringify( + e?.response?.data, + )}`, + ); + }); + + if (!signatureResponse.data?.signature) + throw new CircleWalletError("Could not get signature"); + return signatureResponse.data?.signature as Hex; + } + + return { + address, + sendTransaction, + signMessage, + signTypedData, + signTransaction, + } as CircleAccount satisfies Account; +} + +export async function createCircleWalletDetails({ + credentialId, + walletSetId, + label, + isSmart, + isTestnet, +}: { + credentialId: string; + walletSetId?: string; + label?: string; + isSmart: boolean; + isTestnet?: boolean; +}) { + const { + walletConfiguration: { circle }, + } = await getConfig(); + + if (!circle) { + throw new CircleWalletError( + "Circle wallet configuration not found. Please check your configuration.", + ); + } + + const credential = await getWalletCredential({ + id: credentialId, + }); + + if (credential.type !== "circle") { + throw new CircleWalletError( + `Invalid Credential: not valid type, expected circle received ${credential.type}`, + ); + } + + const provisionedDetails = await provisionCircleWallet({ + entitySecret: credential.data.entitySecret, + apiKey: circle.apiKey, + client: thirdwebClient, + walletSetId, + isTestnet, + }); + + let address = provisionedDetails.account.address; + + const sbwDetails = { + accountFactoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7, + entrypointAddress: ENTRYPOINT_ADDRESS_v0_7, + accountSignerAddress: address, + } as const; + + if (isSmart) { + const smartAccount = await getConnectedSmartWallet({ + adminAccount: provisionedDetails.account, + ...sbwDetails, + }); + + address = smartAccount.address; + } + + return await prisma.walletDetails.create({ + data: { + address: address.toLowerCase(), + type: isSmart ? "smart:circle" : "circle", + label: label, + credentialId, + platformIdentifiers: { + circleWalletId: provisionedDetails.provisionedWallet.id, + walletSetId: provisionedDetails.walletSetId, + isTestnet: isTestnet ?? false, + }, + ...(isSmart ? sbwDetails : {}), + }, + }); +} diff --git a/src/server/utils/websocket.ts b/src/server/utils/websocket.ts deleted file mode 100644 index e01f00715..000000000 --- a/src/server/utils/websocket.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { SocketStream } from "@fastify/websocket"; -import type { Static } from "@sinclair/typebox"; -import type { FastifyRequest } from "fastify"; -import { logger } from "../../shared/utils/logger"; -import type { TransactionSchema } from "../schemas/transaction"; -import { type UserSubscription, subscriptionsData } from "../schemas/websocket"; - -// websocket timeout, i.e., ws connection closed after 10 seconds -const timeoutDuration = 10 * 60 * 1000; - -export const findWSConnectionInSharedState = async ( - connection: SocketStream, - _request: FastifyRequest, -): Promise => { - const index = subscriptionsData.findIndex( - (sub) => sub.socket === connection.socket, - ); - return index; -}; - -export const removeWSFromSharedState = async ( - connection: SocketStream, - request: FastifyRequest, -): Promise => { - const index = await findWSConnectionInSharedState(connection, request); - if (index === -1) { - return -1; - } - subscriptionsData.splice(index, 1); - return index; -}; - -export const onError = async ( - error: Error, - connection: SocketStream, - request: FastifyRequest, -): Promise => { - logger({ - service: "server", - level: "error", - message: "Websocket error", - error, - }); - - const index = await findWSConnectionInSharedState(connection, request); - if (index === -1) { - return; - } - - const userSubscription = subscriptionsData[index]; - subscriptionsData.splice(index, 1); - userSubscription.socket.send( - JSON.stringify({ - result: null, - requestId: userSubscription.requestId, - status: "error", - message: error.message, - }), - ); - - connection.socket.close(); -}; - -export const onMessage = async ( - connection: SocketStream, - request: FastifyRequest, -): Promise => { - const index = await findWSConnectionInSharedState(connection, request); - const userSubscription = subscriptionsData[index]; - subscriptionsData.splice(index, 1); - userSubscription.socket.send( - JSON.stringify({ - result: null, - requestId: userSubscription.requestId, - status: "error", - message: "Do not send any message. Closing Socket... Reconnect again.", - }), - ); - userSubscription.socket.close(); -}; - -export const onClose = async ( - connection: SocketStream, - request: FastifyRequest, -): Promise => { - const index = await findWSConnectionInSharedState(connection, request); - if (index === -1) { - return; - } - subscriptionsData.splice(index, 1); -}; - -export const wsTimeout = async ( - connection: SocketStream, - queueId: string, - request: FastifyRequest, -): Promise => { - return setTimeout(() => { - connection.socket.send("Timeout exceeded. Closing connection..."); - removeWSFromSharedState(connection, request); - connection.socket.close(1000, "Session timeout"); // 1000 is a normal closure status code - - logger({ - service: "server", - level: "info", - message: `Websocket connection for ${queueId} closed due to timeout.`, - }); - }, timeoutDuration); -}; - -export const findOrAddWSConnectionInSharedState = async ( - connection: SocketStream, - queueId: string, - request: FastifyRequest, -) => { - let userSubscription: UserSubscription | undefined = undefined; - const index = await findWSConnectionInSharedState(connection, request); - if (index > -1) { - userSubscription = subscriptionsData[index]; - } else { - userSubscription = { - socket: connection.socket, - requestId: queueId, - }; - - subscriptionsData.push(userSubscription); - } -}; - -type CustomStatusAndConnectionType = { - message: string; - closeConnection: boolean; -}; - -export const getStatusMessageAndConnectionStatus = async ( - data: Static | null, -): Promise => { - let message = - "Request is queued. Waiting for transaction to be picked up by worker."; - let closeConnection = false; - - if (!data) { - message = "Transaction not found. Make sure the provided ID is correct."; - closeConnection = true; - } else if (data.status === "mined") { - message = "Transaction mined. Closing connection."; - closeConnection = true; - } else if (data.status === "errored") { - message = data.errorMessage || "Transaction errored. Closing connection."; - closeConnection = true; - } else if (data.status === "sent") { - message = - "Transaction submitted to blockchain. Waiting for transaction to be mined..."; - } - - return { message, closeConnection }; -}; - -export const formatSocketMessage = async ( - data: Static | null, - message: string, -): Promise => { - const returnData = JSON.stringify({ - result: data ? JSON.stringify(data) : undefined, - queueId: data?.queueId, - message, - }); - return returnData; -}; diff --git a/src/shared/db/configuration/get-configuration.ts b/src/shared/db/configuration/get-configuration.ts index ee46e3a3b..d765fafbc 100644 --- a/src/shared/db/configuration/get-configuration.ts +++ b/src/shared/db/configuration/get-configuration.ts @@ -5,6 +5,7 @@ import { ethers } from "ethers"; import type { Chain } from "thirdweb"; import type { AwsWalletConfiguration, + CircleWalletConfiguration, GcpWalletConfiguration, ParsedConfig, } from "../../schemas/config"; @@ -16,6 +17,15 @@ import { env } from "../../utils/env"; import { logger } from "../../utils/logger"; import { prisma } from "../client"; import { updateConfiguration } from "./update-configuration"; +import * as z from "zod"; + +export const walletProviderConfigsSchema = z.object({ + circle: z + .object({ + apiKey: z.string(), + }) + .optional(), +}); const toParsedConfig = async (config: Configuration): Promise => { // We destructure the config to omit wallet related fields to prevent direct access @@ -29,6 +39,7 @@ const toParsedConfig = async (config: Configuration): Promise => { gcpApplicationCredentialEmail, gcpApplicationCredentialPrivateKey, contractSubscriptionsRetryDelaySeconds, + walletProviderConfigs, ...restConfig } = config; @@ -162,6 +173,33 @@ const toParsedConfig = async (config: Configuration): Promise => { legacyWalletType_removeInNextBreakingChange = WalletType.gcpKms; } + let circleWalletConfiguration: CircleWalletConfiguration | null = null; + + const { + data: parsedWalletProviderConfigs, + success, + error: walletProviderConfigsParseError, + } = walletProviderConfigsSchema.safeParse(walletProviderConfigs?.valueOf()); + + // TODO: fail loudly if walletProviderConfigs is not valid + if (!success) { + logger({ + level: "error", + message: "Invalid wallet provider configs", + service: "server", + error: walletProviderConfigsParseError, + }); + } + + if (parsedWalletProviderConfigs?.circle) { + circleWalletConfiguration = { + apiKey: decrypt( + parsedWalletProviderConfigs.circle.apiKey, + env.ENCRYPTION_PASSWORD, + ), + }; + } + return { ...restConfig, contractSubscriptionsRequeryDelaySeconds: @@ -170,8 +208,15 @@ const toParsedConfig = async (config: Configuration): Promise => { walletConfiguration: { aws: awsWalletConfiguration, gcp: gcpWalletConfiguration, + circle: circleWalletConfiguration, legacyWalletType_removeInNextBreakingChange, }, + mtlsCertificate: config.mtlsCertificateEncrypted + ? decrypt(config.mtlsCertificateEncrypted, env.ENCRYPTION_PASSWORD) + : null, + mtlsPrivateKey: config.mtlsPrivateKeyEncrypted + ? decrypt(config.mtlsPrivateKeyEncrypted, env.ENCRYPTION_PASSWORD) + : null, }; }; diff --git a/src/shared/db/configuration/update-configuration.ts b/src/shared/db/configuration/update-configuration.ts index f6d14fd47..41e49df01 100644 --- a/src/shared/db/configuration/update-configuration.ts +++ b/src/shared/db/configuration/update-configuration.ts @@ -1,26 +1,53 @@ import type { Prisma } from "@prisma/client"; import { encrypt } from "../../utils/crypto"; import { prisma } from "../client"; +import { walletProviderConfigsSchema } from "./get-configuration"; +import { logger } from "../../utils/logger"; export const updateConfiguration = async ( - data: Prisma.ConfigurationUpdateArgs["data"], + data: Prisma.ConfigurationUpdateInput, ) => { + if (typeof data.awsSecretAccessKey === "string") { + data.awsSecretAccessKey = encrypt(data.awsSecretAccessKey); + } + + if (typeof data.gcpApplicationCredentialPrivateKey === "string") { + data.gcpApplicationCredentialPrivateKey = encrypt( + data.gcpApplicationCredentialPrivateKey, + ); + } + + // allow undefined (for no updates to field), but do not allow any other values than object + if (typeof data.walletProviderConfigs === "object") { + const { data: parsedWalletProviderConfigs, error } = + walletProviderConfigsSchema.safeParse(data.walletProviderConfigs); + + if (error) { + logger({ + level: "error", + message: "Invalid walletProviderConfigs", + error: error, + service: "server", + }); + // it's okay to throw a raw error here, any HTTP call that uses this should validate the input + throw new Error("Invalid walletProviderConfigs"); + } + + if (parsedWalletProviderConfigs?.circle?.apiKey) { + parsedWalletProviderConfigs.circle.apiKey = encrypt( + parsedWalletProviderConfigs.circle.apiKey, + ); + } + + data.walletProviderConfigs = parsedWalletProviderConfigs; + } else if (typeof data.walletProviderConfigs !== "undefined") { + throw new Error("Invalid walletProviderConfigs"); + } + return prisma.configuration.update({ where: { id: "default", }, - data: { - ...data, - ...(typeof data.awsSecretAccessKey === "string" - ? { awsSecretAccessKey: encrypt(data.awsSecretAccessKey) } - : {}), - ...(typeof data.gcpApplicationCredentialPrivateKey === "string" - ? { - gcpApplicationCredentialPrivateKey: encrypt( - data.gcpApplicationCredentialPrivateKey, - ), - } - : {}), - }, + data, }); }; diff --git a/src/shared/db/transactions/queue-tx.ts b/src/shared/db/transactions/queue-tx.ts index 508e5207e..50e95d2c8 100644 --- a/src/shared/db/transactions/queue-tx.ts +++ b/src/shared/db/transactions/queue-tx.ts @@ -23,6 +23,7 @@ interface QueueTxParams { gas?: string; maxFeePerGas?: string; maxPriorityFeePerGas?: string; + gasFeeCeiling?: string; value?: string; }; } diff --git a/src/shared/db/wallet-credentials/create-wallet-credential.ts b/src/shared/db/wallet-credentials/create-wallet-credential.ts new file mode 100644 index 000000000..51b9cae76 --- /dev/null +++ b/src/shared/db/wallet-credentials/create-wallet-credential.ts @@ -0,0 +1,41 @@ +import { encrypt } from "../../utils/crypto"; +import { prisma } from "../client"; +import { getConfig } from "../../utils/cache/get-config"; +import { WalletCredentialsError } from "./get-wallet-credential"; + +// will be expanded to be a discriminated union of all supported wallet types +export type CreateWalletCredentialsParams = { + type: "circle"; + label: string; + entitySecret: string; + isDefault?: boolean; +}; + +export const createWalletCredential = async ({ + type, + label, + entitySecret, + isDefault, +}: CreateWalletCredentialsParams) => { + const { walletConfiguration } = await getConfig(); + switch (type) { + case "circle": { + const circleApiKey = walletConfiguration.circle?.apiKey; + if (!circleApiKey) { + throw new WalletCredentialsError("No Circle API Key Configured"); + } + // Create the wallet credentials + const walletCredentials = await prisma.walletCredentials.create({ + data: { + type, + label, + isDefault: isDefault || null, + data: { + entitySecret: encrypt(entitySecret), + }, + }, + }); + return walletCredentials; + } + } +}; diff --git a/src/shared/db/wallet-credentials/get-all-wallet-credentials.ts b/src/shared/db/wallet-credentials/get-all-wallet-credentials.ts new file mode 100644 index 000000000..828cbdb6a --- /dev/null +++ b/src/shared/db/wallet-credentials/get-all-wallet-credentials.ts @@ -0,0 +1,34 @@ +import { prisma } from "../client"; +import type { PrismaTransaction } from "../../schemas/prisma"; + +interface GetAllWalletCredentialsParams { + pgtx?: PrismaTransaction; + page?: number; + limit?: number; +} + +export const getAllWalletCredentials = async ({ + page = 1, + limit = 10, +}: GetAllWalletCredentialsParams) => { + const credentials = await prisma.walletCredentials.findMany({ + where: { + deletedAt: null, + }, + skip: (page - 1) * limit, + take: limit, + select: { + id: true, + type: true, + label: true, + isDefault: true, + createdAt: true, + updatedAt: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + + return credentials; +}; diff --git a/src/shared/db/wallet-credentials/get-wallet-credential.ts b/src/shared/db/wallet-credentials/get-wallet-credential.ts new file mode 100644 index 000000000..eacbfb136 --- /dev/null +++ b/src/shared/db/wallet-credentials/get-wallet-credential.ts @@ -0,0 +1,81 @@ +import LRUMap from "mnemonist/lru-map"; +import { z } from "zod"; +import { decrypt } from "../../utils/crypto"; +import { env } from "../../utils/env"; +import { prisma } from "../client"; + +export class WalletCredentialsError extends Error { + constructor(message: string) { + super(message); + this.name = "WalletCredentialsError"; + } +} + +const walletCredentialsSchema = z.object({ + id: z.string(), + type: z.literal("circle"), + label: z.string().nullable(), + data: z.object({ + entitySecret: z.string(), + }), + isDefault: z.boolean().nullable(), + createdAt: z.date(), + updatedAt: z.date(), + deletedAt: z.date().nullable(), +}); + +export type ParsedWalletCredential = z.infer; + +export const walletCredentialsCache = new LRUMap< + string, + ParsedWalletCredential +>(env.ACCOUNT_CACHE_SIZE); + +interface GetWalletCredentialParams { + id: string; +} + +/** + * Return the wallet credentials for the given id. + * The entitySecret will be decrypted. + * If the credentials are not found, an error is thrown. + */ +export const getWalletCredential = async ({ + id, +}: GetWalletCredentialParams) => { + const cachedCredentials = walletCredentialsCache.get(id); + if (cachedCredentials) { + return cachedCredentials; + } + + const credential = await prisma.walletCredentials.findUnique({ + where: { + id, + }, + }); + + if (!credential) { + throw new WalletCredentialsError( + `No wallet credentials found for id ${id}`, + ); + } + + const { data: parsedCredential, error: parseError } = + walletCredentialsSchema.safeParse(credential); + + if (parseError) { + throw new WalletCredentialsError( + `Invalid Credential found for ${id}:\n${parseError.errors + .map((error) => error.message) + .join(", ")}`, + ); + } + + parsedCredential.data.entitySecret = decrypt( + parsedCredential.data.entitySecret, + env.ENCRYPTION_PASSWORD, + ); + + walletCredentialsCache.set(id, parsedCredential); + return parsedCredential; +}; diff --git a/src/shared/db/wallet-credentials/update-wallet-credential.ts b/src/shared/db/wallet-credentials/update-wallet-credential.ts new file mode 100644 index 000000000..018eb5627 --- /dev/null +++ b/src/shared/db/wallet-credentials/update-wallet-credential.ts @@ -0,0 +1,55 @@ +import { getWalletCredential } from "./get-wallet-credential"; +import { encrypt } from "../../utils/crypto"; +import { prisma } from "../client"; +import { cirlceEntitySecretZodSchema } from "../../schemas/wallet"; + +interface UpdateWalletCredentialParams { + id: string; + label?: string; + isDefault?: boolean; + entitySecret?: string; +} + +type UpdateData = { + label?: string; + isDefault: boolean | null; + data?: { + entitySecret: string; + }; +}; + +export const updateWalletCredential = async ({ + id, + label, + isDefault, + entitySecret, +}: UpdateWalletCredentialParams) => { + // First check if credential exists + await getWalletCredential({ id }); + + // If entitySecret is provided, validate and encrypt it + const data: UpdateData = { + label, + isDefault: isDefault || null, + }; + + if (entitySecret) { + // Validate the entity secret + cirlceEntitySecretZodSchema.parse(entitySecret); + + // Only update data field if entitySecret is provided + data.data = { + entitySecret: encrypt(entitySecret), + }; + } + + // Update the credential + const updatedCredential = await prisma.walletCredentials.update({ + where: { + id, + }, + data, + }); + + return updatedCredential; +}; diff --git a/src/shared/db/wallet-subscriptions/create-wallet-subscription.ts b/src/shared/db/wallet-subscriptions/create-wallet-subscription.ts new file mode 100644 index 000000000..437833a4f --- /dev/null +++ b/src/shared/db/wallet-subscriptions/create-wallet-subscription.ts @@ -0,0 +1,55 @@ +import type { Prisma } from "@prisma/client"; +import { prisma } from "../client"; +import type { WalletConditions } from "../../schemas/wallet-subscription-conditions"; +import { validateConditions } from "../../schemas/wallet-subscription-conditions"; +import { getWebhook } from "../webhooks/get-webhook"; +import { WebhooksEventTypes } from "../../schemas/webhooks"; + +interface CreateWalletSubscriptionParams { + chainId: string; + walletAddress: string; + conditions: WalletConditions; + webhookId?: number; +} + +export async function createWalletSubscription({ + chainId, + walletAddress, + conditions, + webhookId, +}: CreateWalletSubscriptionParams) { + // Validate conditions + const validatedConditions = validateConditions(conditions); + + if (webhookId) { + const webhook = await getWebhook(webhookId); + if (!webhook) { + throw new Error("Webhook not found"); + } + if (webhook.revokedAt) { + throw new Error("Webhook has been revoked"); + } + if (webhook.eventType !== WebhooksEventTypes.WALLET_SUBSCRIPTION) { + throw new Error("Webhook is not a wallet subscription webhook"); + } + } + + const existingSubscriptionsCount = await prisma.walletSubscriptions.count({}); + + if (existingSubscriptionsCount >= 1000) { + throw new Error("Maximum number of wallet subscriptions reached"); + } + + // Create a new subscription + return await prisma.walletSubscriptions.create({ + data: { + chainId, + walletAddress: walletAddress.toLowerCase(), + conditions: validatedConditions as Prisma.InputJsonValue[], + webhookId, + }, + include: { + webhook: true, + }, + }); +} diff --git a/src/shared/db/wallet-subscriptions/delete-wallet-subscription.ts b/src/shared/db/wallet-subscriptions/delete-wallet-subscription.ts new file mode 100644 index 000000000..65e3ecd49 --- /dev/null +++ b/src/shared/db/wallet-subscriptions/delete-wallet-subscription.ts @@ -0,0 +1,16 @@ +import { prisma } from "../client"; + +export async function deleteWalletSubscription(id: string) { + return await prisma.walletSubscriptions.update({ + where: { + id, + deletedAt: null, + }, + data: { + deletedAt: new Date(), + }, + include: { + webhook: true, + }, + }); +} \ No newline at end of file diff --git a/src/shared/db/wallet-subscriptions/get-all-wallet-subscriptions.ts b/src/shared/db/wallet-subscriptions/get-all-wallet-subscriptions.ts new file mode 100644 index 000000000..631254375 --- /dev/null +++ b/src/shared/db/wallet-subscriptions/get-all-wallet-subscriptions.ts @@ -0,0 +1,27 @@ +import { validateConditions } from "../../schemas/wallet-subscription-conditions"; +import { prisma } from "../client"; + +export async function getAllWalletSubscriptions(args?: { + page?: number; + limit?: number; +}) { + const { page, limit } = args || {}; + const subscriptions = await prisma.walletSubscriptions.findMany({ + where: { + deletedAt: null, + }, + include: { + webhook: true, + }, + skip: page && limit ? (page - 1) * limit : undefined, + take: limit, + orderBy: { + updatedAt: "desc", + }, + }); + + return subscriptions.map((subscription) => ({ + ...subscription, + conditions: validateConditions(subscription.conditions), + })); +} diff --git a/src/shared/db/wallet-subscriptions/update-wallet-subscription.ts b/src/shared/db/wallet-subscriptions/update-wallet-subscription.ts new file mode 100644 index 000000000..2082470d7 --- /dev/null +++ b/src/shared/db/wallet-subscriptions/update-wallet-subscription.ts @@ -0,0 +1,53 @@ +import type { Prisma } from "@prisma/client"; +import { prisma } from "../client"; +import type { WalletConditions } from "../../schemas/wallet-subscription-conditions"; +import { validateConditions } from "../../schemas/wallet-subscription-conditions"; +import { WebhooksEventTypes } from "../../schemas/webhooks"; +import { getWebhook } from "../webhooks/get-webhook"; + +interface UpdateWalletSubscriptionParams { + id: string; + chainId?: string; + walletAddress?: string; + conditions?: WalletConditions; + webhookId?: number | null; +} + +export async function updateWalletSubscription({ + id, + chainId, + walletAddress, + conditions, + webhookId, +}: UpdateWalletSubscriptionParams) { + if (webhookId) { + const webhook = await getWebhook(webhookId); + if (!webhook) { + throw new Error("Webhook not found"); + } + if (webhook.revokedAt) { + throw new Error("Webhook has been revoked"); + } + if (webhook.eventType !== WebhooksEventTypes.WALLET_SUBSCRIPTION) { + throw new Error("Webhook is not a wallet subscription webhook"); + } + } + + return await prisma.walletSubscriptions.update({ + where: { + id, + deletedAt: null, + }, + data: { + ...(chainId && { chainId }), + ...(walletAddress && { walletAddress: walletAddress.toLowerCase() }), + ...(conditions && { + conditions: validateConditions(conditions) as Prisma.InputJsonValue[], + }), + ...(webhookId !== undefined && { webhookId }), + }, + include: { + webhook: true, + }, + }); +} diff --git a/src/shared/db/wallets/get-wallet-details.ts b/src/shared/db/wallets/get-wallet-details.ts index c6612d628..5e28a2219 100644 --- a/src/shared/db/wallets/get-wallet-details.ts +++ b/src/shared/db/wallets/get-wallet-details.ts @@ -57,8 +57,7 @@ const smartLocalWalletSchema = localWalletSchema .extend({ type: z.literal("smart:local"), }) - .merge(smartWalletPartialSchema) - .merge(baseWalletPartialSchema); + .merge(smartWalletPartialSchema); const awsKmsWalletSchema = z .object({ @@ -73,8 +72,7 @@ const smartAwsKmsWalletSchema = awsKmsWalletSchema .extend({ type: z.literal("smart:aws-kms"), }) - .merge(smartWalletPartialSchema) - .merge(baseWalletPartialSchema); + .merge(smartWalletPartialSchema); const gcpKmsWalletSchema = z .object({ @@ -89,9 +87,26 @@ const smartGcpKmsWalletSchema = gcpKmsWalletSchema .extend({ type: z.literal("smart:gcp-kms"), }) - .merge(smartWalletPartialSchema) + .merge(smartWalletPartialSchema); + +const circleWalletSchema = z + .object({ + type: z.literal("circle"), + platformIdentifiers: z.object({ + circleWalletId: z.string(), + walletSetId: z.string(), + isTestnet: z.boolean(), + }), + credentialId: z.string(), + }) .merge(baseWalletPartialSchema); +const smartCircleWalletSchema = circleWalletSchema + .extend({ + type: z.literal("smart:circle"), + }) + .merge(smartWalletPartialSchema); + const walletDetailsSchema = z.discriminatedUnion("type", [ localWalletSchema, smartLocalWalletSchema, @@ -99,12 +114,15 @@ const walletDetailsSchema = z.discriminatedUnion("type", [ smartAwsKmsWalletSchema, gcpKmsWalletSchema, smartGcpKmsWalletSchema, + circleWalletSchema, + smartCircleWalletSchema, ]); export type SmartBackendWalletDetails = | z.infer | z.infer - | z.infer; + | z.infer + | z.infer; export function isSmartBackendWallet( wallet: ParsedWalletDetails, @@ -118,12 +136,14 @@ export const SmartBackendWalletTypes = [ "smart:local", "smart:aws-kms", "smart:gcp-kms", + "smart:circle", ] as const; export const BackendWalletTypes = [ "local", "aws-kms", "gcp-kms", + "circle", ...SmartBackendWalletTypes, ] as const; @@ -131,7 +151,7 @@ export type SmartBackendWalletType = (typeof SmartBackendWalletTypes)[number]; export type BackendWalletType = (typeof BackendWalletTypes)[number]; export type ParsedWalletDetails = z.infer; -export const walletDetailsCache = new LRUMap(2048); +export const walletDetailsCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); /** * Return the wallet details for the given address. * @@ -181,7 +201,7 @@ export const getWalletDetails = async ({ walletDetails.awsKmsSecretAccessKey = walletDetails.awsKmsSecretAccessKey ? decrypt(walletDetails.awsKmsSecretAccessKey, env.ENCRYPTION_PASSWORD) - : (config.walletConfiguration.aws?.awsSecretAccessKey ?? null); + : config.walletConfiguration.aws?.awsSecretAccessKey ?? null; walletDetails.awsKmsAccessKeyId = walletDetails.awsKmsAccessKeyId ?? @@ -206,8 +226,8 @@ export const getWalletDetails = async ({ walletDetails.gcpApplicationCredentialPrivateKey, env.ENCRYPTION_PASSWORD, ) - : (config.walletConfiguration.gcp?.gcpApplicationCredentialPrivateKey ?? - null); + : config.walletConfiguration.gcp?.gcpApplicationCredentialPrivateKey ?? + null; walletDetails.gcpApplicationCredentialEmail = walletDetails.gcpApplicationCredentialEmail ?? diff --git a/src/shared/db/wallets/wallet-nonce.ts b/src/shared/db/wallets/wallet-nonce.ts index c730533ce..c7c0ea148 100644 --- a/src/shared/db/wallets/wallet-nonce.ts +++ b/src/shared/db/wallets/wallet-nonce.ts @@ -10,6 +10,7 @@ import { normalizeAddress } from "../../utils/primitive-types"; import { redis } from "../../utils/redis/redis"; import { thirdwebClient } from "../../utils/sdk"; import { updateNonceMap } from "./nonce-map"; +import { nonceHistoryKey } from "../../../worker/tasks/nonce-health-check-worker"; /** * Get all used backend wallets. @@ -45,7 +46,7 @@ export const getUsedBackendWallets = async ( /** * The "last used nonce" stores the last nonce submitted onchain. - * Example: "25" + * Example: 25 -> nonce 25 is onchain, nonce 26 is unused or inflight. */ export const lastUsedNonceKey = (chainId: number, walletAddress: Address) => `nonce:${chainId}:${normalizeAddress(walletAddress)}`; @@ -260,6 +261,7 @@ export async function deleteNoncesForBackendWallets( lastUsedNonceKey(chainId, walletAddress), recycledNoncesKey(chainId, walletAddress), sentNoncesKey(chainId, walletAddress), + nonceHistoryKey(chainId, walletAddress), ]); await redis.del(keys); } diff --git a/src/shared/db/webhooks/create-webhook.ts b/src/shared/db/webhooks/create-webhook.ts index 0b3ebb151..ee07d770f 100644 --- a/src/shared/db/webhooks/create-webhook.ts +++ b/src/shared/db/webhooks/create-webhook.ts @@ -1,5 +1,5 @@ import type { Webhooks } from "@prisma/client"; -import { createHash, randomBytes } from "crypto"; +import { createHash, randomBytes } from "node:crypto"; import type { WebhooksEventTypes } from "../../schemas/webhooks"; import { prisma } from "../client"; diff --git a/src/shared/schemas/config.ts b/src/shared/schemas/config.ts index fc3ce2d62..97d7c6d38 100644 --- a/src/shared/schemas/config.ts +++ b/src/shared/schemas/config.ts @@ -21,6 +21,10 @@ export type GcpWalletConfiguration = { defaultGcpApplicationProjectId: string; }; +export type CircleWalletConfiguration = { + apiKey: string; +}; + export interface ParsedConfig extends Omit< Configuration, @@ -33,12 +37,18 @@ export interface ParsedConfig | "gcpApplicationCredentialEmail" | "gcpApplicationCredentialPrivateKey" | "contractSubscriptionsRetryDelaySeconds" + | "mtlsCertificateEncrypted" + | "mtlsPrivateKeyEncrypted" + | "walletProviderConfigs" > { walletConfiguration: { aws: AwsWalletConfiguration | null; gcp: GcpWalletConfiguration | null; + circle: CircleWalletConfiguration | null; legacyWalletType_removeInNextBreakingChange: WalletType; }; contractSubscriptionsRequeryDelaySeconds: string; chainOverridesParsed: Chain[]; + mtlsCertificate: string | null; + mtlsPrivateKey: string | null; } diff --git a/src/shared/schemas/wallet-subscription-conditions.ts b/src/shared/schemas/wallet-subscription-conditions.ts new file mode 100644 index 000000000..72e27f040 --- /dev/null +++ b/src/shared/schemas/wallet-subscription-conditions.ts @@ -0,0 +1,53 @@ +import { Type } from "@sinclair/typebox"; +import { z } from "zod"; +import { AddressSchema } from "../../server/schemas/address"; + +// TypeBox schemas for API validation +export const WalletConditionSchema = Type.Union([ + Type.Object({ + type: Type.Literal('token_balance_lt'), + tokenAddress: Type.Union([AddressSchema, Type.Literal('native')]), + value: Type.String({ + description: "The threshold value in wei", + examples: ["1000000000000000000"] // 1 ETH + }) + }), + Type.Object({ + type: Type.Literal('token_balance_gt'), + tokenAddress: Type.Union([AddressSchema, Type.Literal('native')]), + value: Type.String({ + description: "The threshold value in wei", + examples: ["1000000000000000000"] // 1 ETH + }) + }) +]); + +export const WalletConditionsSchema = Type.Array(WalletConditionSchema, { + maxItems: 100, + description: "Array of conditions to monitor for this wallet" +}); + +// Zod schemas for internal validation +export const WalletConditionZ = z.discriminatedUnion('type', [ + z.object({ + type: z.literal('token_balance_lt'), + tokenAddress: z.union([z.string(), z.literal('native')]), + value: z.string() + }), + z.object({ + type: z.literal('token_balance_gt'), + tokenAddress: z.union([z.string(), z.literal('native')]), + value: z.string() + }) +]); + +export const WalletConditionsZ = z.array(WalletConditionZ).max(100); + +// Type exports +export type WalletCondition = z.infer; +export type WalletConditions = z.infer; + +// Helper to validate conditions +export function validateConditions(conditions: unknown): WalletConditions { + return WalletConditionsZ.parse(conditions); +} \ No newline at end of file diff --git a/src/shared/schemas/wallet.ts b/src/shared/schemas/wallet.ts index 4200e1eb6..6c856e86b 100644 --- a/src/shared/schemas/wallet.ts +++ b/src/shared/schemas/wallet.ts @@ -1,4 +1,25 @@ +import * as z from "zod"; + +export enum CircleWalletType { + circle = "circle", + + // Smart wallets + smartCircle = "smart:circle", +} + +export enum LegacyWalletType { + local = "local", + awsKms = "aws-kms", + gcpKms = "gcp-kms", + + // Smart wallets + smartAwsKms = "smart:aws-kms", + smartGcpKms = "smart:gcp-kms", + smartLocal = "smart:local", +} + export enum WalletType { + // Legacy wallet types local = "local", awsKms = "aws-kms", gcpKms = "gcp-kms", @@ -7,4 +28,14 @@ export enum WalletType { smartAwsKms = "smart:aws-kms", smartGcpKms = "smart:gcp-kms", smartLocal = "smart:local", + + // New credential based wallet types + circle = "circle", + + // Smart wallets + smartCircle = "smart:circle", } + +export const cirlceEntitySecretZodSchema = z.string().regex(/^[0-9a-fA-F]{64}$/, { + message: "entitySecret must be a 32-byte hex string", +}); diff --git a/src/shared/schemas/webhooks.ts b/src/shared/schemas/webhooks.ts index 57279d378..478168430 100644 --- a/src/shared/schemas/webhooks.ts +++ b/src/shared/schemas/webhooks.ts @@ -1,3 +1,5 @@ +import type { WalletCondition } from "./wallet-subscription-conditions"; + export enum WebhooksEventTypes { QUEUED_TX = "queued_transaction", SENT_TX = "sent_transaction", @@ -8,6 +10,7 @@ export enum WebhooksEventTypes { BACKEND_WALLET_BALANCE = "backend_wallet_balance", AUTH = "auth", CONTRACT_SUBSCRIPTION = "contract_subscription", + WALLET_SUBSCRIPTION = "wallet_subscription", } export type BackendWalletBalanceWebhookParams = { @@ -17,3 +20,10 @@ export type BackendWalletBalanceWebhookParams = { chainId: number; message: string; }; +export interface WalletSubscriptionWebhookParams { + subscriptionId: string; + chainId: string; + walletAddress: string; + condition: WalletCondition; + currentValue: string; +} diff --git a/src/shared/utils/account.ts b/src/shared/utils/account.ts index 496071e3d..074ce55d3 100644 --- a/src/shared/utils/account.ts +++ b/src/shared/utils/account.ts @@ -18,8 +18,12 @@ import { import { getSmartWalletV5 } from "./cache/get-smart-wallet-v5"; import { getChain } from "./chain"; import { thirdwebClient } from "./sdk"; +import { getWalletCredential } from "../db/wallet-credentials/get-wallet-credential"; +import { getCircleAccount } from "../../server/utils/wallets/circle"; +import { getConfig } from "./cache/get-config"; +import { env } from "./env"; -export const _accountsCache = new LRUMap(2048); +export const _accountsCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); export const getAccount = async (args: { chainId: number; @@ -152,12 +156,64 @@ export const walletDetailsToAccount = async ({ return { account: connectedWallet, adminAccount }; } + + case WalletType.circle: { + const { + walletConfiguration: { circle }, + } = await getConfig(); + + if (!circle) + throw new Error("No configuration found for circle wallet type"); + + const credentials = await getWalletCredential({ + id: walletDetails.credentialId, + }); + + const account = await getCircleAccount({ + apiKey: circle.apiKey, + client: thirdwebClient, + entitySecret: credentials.data.entitySecret, + walletId: walletDetails.platformIdentifiers.circleWalletId, + }); + + return { account }; + } + + case WalletType.smartCircle: { + const { + walletConfiguration: { circle }, + } = await getConfig(); + + if (!circle) + throw new Error("No configuration found for circle wallet type"); + + const credentials = await getWalletCredential({ + id: walletDetails.credentialId, + }); + + const adminAccount = await getCircleAccount({ + apiKey: circle.apiKey, + client: thirdwebClient, + entitySecret: credentials.data.entitySecret, + walletId: walletDetails.platformIdentifiers.circleWalletId, + }); + + const connectedWallet = await getConnectedSmartWallet({ + adminAccount: adminAccount, + accountFactoryAddress: walletDetails.accountFactoryAddress ?? undefined, + entrypointAddress: walletDetails.entrypointAddress ?? undefined, + chain: chain, + }); + + return { account: connectedWallet, adminAccount }; + } + default: throw new Error(`Wallet type not supported: ${walletDetails.type}`); } }; -export const _adminAccountsCache = new LRUMap(2048); +export const _adminAccountsCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); /** * Get the admin account for a smart backend wallet (cached) diff --git a/src/shared/utils/cache/access-token.ts b/src/shared/utils/cache/access-token.ts index 139f16013..0379714f6 100644 --- a/src/shared/utils/cache/access-token.ts +++ b/src/shared/utils/cache/access-token.ts @@ -1,9 +1,10 @@ import type { Tokens } from "@prisma/client"; import LRUMap from "mnemonist/lru-map"; import { getToken } from "../../db/tokens/get-token"; +import { env } from "../env"; // Cache an access token JWT to the token object, or null if not found. -export const accessTokenCache = new LRUMap(2048); +export const accessTokenCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); interface GetAccessTokenParams { jwt: string; diff --git a/src/shared/utils/cache/clear-cache.ts b/src/shared/utils/cache/clear-cache.ts index e70fa38be..10da2d757 100644 --- a/src/shared/utils/cache/clear-cache.ts +++ b/src/shared/utils/cache/clear-cache.ts @@ -1,4 +1,3 @@ -import type { env } from "../env"; import { accessTokenCache } from "./access-token"; import { invalidateConfig } from "./get-config"; import { sdkCache } from "./get-sdk"; @@ -6,9 +5,7 @@ import { walletsCache } from "./get-wallet"; import { webhookCache } from "./get-webhook"; import { keypairCache } from "./keypair"; -export const clearCache = async ( - _service: (typeof env)["LOG_SERVICES"][0], -): Promise => { +export const clearCache = async (): Promise => { invalidateConfig(); webhookCache.clear(); sdkCache.clear(); diff --git a/src/shared/utils/cache/get-contract.ts b/src/shared/utils/cache/get-contract.ts index c590434d0..baa5a49e8 100644 --- a/src/shared/utils/cache/get-contract.ts +++ b/src/shared/utils/cache/get-contract.ts @@ -3,6 +3,7 @@ import { StatusCodes } from "http-status-codes"; import { createCustomError } from "../../../server/middleware/error"; import { abiSchema } from "../../../server/schemas/contract"; import { getSdk } from "./get-sdk"; +import type { ThirdwebSDK } from "@thirdweb-dev/sdk"; const abiArraySchema = Type.Array(abiSchema); @@ -21,7 +22,17 @@ export const getContract = async ({ accountAddress, abi, }: GetContractParams) => { - const sdk = await getSdk({ chainId, walletAddress, accountAddress }); + let sdk: ThirdwebSDK; + + try { + sdk = await getSdk({ chainId, walletAddress, accountAddress }); + } catch (e) { + throw createCustomError( + `Could not get SDK: ${e}`, + StatusCodes.BAD_REQUEST, + "INVALID_CHAIN_OR_WALLET_TYPE_FOR_ROUTE", + ); + } try { if (abi) { diff --git a/src/shared/utils/cache/get-sdk.ts b/src/shared/utils/cache/get-sdk.ts index 92f80472b..9cb18b50b 100644 --- a/src/shared/utils/cache/get-sdk.ts +++ b/src/shared/utils/cache/get-sdk.ts @@ -7,7 +7,7 @@ import { getChain } from "../chain"; import { env } from "../env"; import { getWallet } from "./get-wallet"; -export const sdkCache = new LRUMap(2048); +export const sdkCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); export const networkResponseSchema = Type.Object({ name: Type.String({ diff --git a/src/shared/utils/cache/get-smart-wallet-v5.ts b/src/shared/utils/cache/get-smart-wallet-v5.ts index 7ab01f275..1bedce2f7 100644 --- a/src/shared/utils/cache/get-smart-wallet-v5.ts +++ b/src/shared/utils/cache/get-smart-wallet-v5.ts @@ -3,8 +3,9 @@ import { getContract, readContract, type Address, type Chain } from "thirdweb"; import { smartWallet, type Account } from "thirdweb/wallets"; import { getAccount } from "../account"; import { thirdwebClient } from "../sdk"; +import { env } from "../env"; -export const smartWalletsCache = new LRUMap(2048); +export const smartWalletsCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); interface SmartWalletParams { chain: Chain; diff --git a/src/shared/utils/cache/get-wallet.ts b/src/shared/utils/cache/get-wallet.ts index 4f3533410..0e0e110dd 100644 --- a/src/shared/utils/cache/get-wallet.ts +++ b/src/shared/utils/cache/get-wallet.ts @@ -14,8 +14,9 @@ import { splitAwsKmsArn } from "../../../server/utils/wallets/aws-kms-arn"; import { splitGcpKmsResourcePath } from "../../../server/utils/wallets/gcp-kms-resource-path"; import { getLocalWallet } from "../../../server/utils/wallets/get-local-wallet"; import { getSmartWallet } from "../../../server/utils/wallets/get-smart-wallet"; +import { env } from "../env"; -export const walletsCache = new LRUMap(2048); +export const walletsCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); interface GetWalletParams { pgtx?: PrismaTransaction; @@ -171,7 +172,7 @@ export const getWallet = async ({ default: throw new Error( - `Wallet with address ${walletAddress} was configured with unknown wallet type ${walletDetails.type}`, + `Wallet with address ${walletAddress} of type ${walletDetails.type} is not supported for these routes yet`, ); } diff --git a/src/shared/utils/cache/get-webhook.ts b/src/shared/utils/cache/get-webhook.ts index 51326d569..ae3ac3664 100644 --- a/src/shared/utils/cache/get-webhook.ts +++ b/src/shared/utils/cache/get-webhook.ts @@ -2,8 +2,9 @@ import type { Webhooks } from "@prisma/client"; import LRUMap from "mnemonist/lru-map"; import { getAllWebhooks } from "../../db/webhooks/get-all-webhooks"; import type { WebhooksEventTypes } from "../../schemas/webhooks"; +import { env } from "../env"; -export const webhookCache = new LRUMap(2048); +export const webhookCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); export const getWebhooksByEventType = async ( eventType: WebhooksEventTypes, diff --git a/src/shared/utils/cache/keypair.ts b/src/shared/utils/cache/keypair.ts index 17165ed5f..4df53a2ec 100644 --- a/src/shared/utils/cache/keypair.ts +++ b/src/shared/utils/cache/keypair.ts @@ -1,9 +1,10 @@ import type { Keypairs } from "@prisma/client"; import LRUMap from "mnemonist/lru-map"; import { getKeypairByHash, getKeypairByPublicKey } from "../../db/keypair/get"; +import { env } from "../env"; // Cache a public key to the Keypair object, or null if not found. -export const keypairCache = new LRUMap(2048); +export const keypairCache = new LRUMap(env.ACCOUNT_CACHE_SIZE); /** * Get a keypair by public key or hash. diff --git a/src/shared/utils/cron/clear-cache-cron.ts b/src/shared/utils/cron/clear-cache-cron.ts index 6fed64673..6cb52a545 100644 --- a/src/shared/utils/cron/clear-cache-cron.ts +++ b/src/shared/utils/cron/clear-cache-cron.ts @@ -1,23 +1,23 @@ -import cron from "node-cron"; +import { CronJob } from "cron"; import { clearCache } from "../cache/clear-cache"; import { getConfig } from "../cache/get-config"; -import type { env } from "../env"; -let task: cron.ScheduledTask; -export const clearCacheCron = async ( - service: (typeof env)["LOG_SERVICES"][0], -) => { +let task: CronJob; + +export const clearCacheCron = async () => { const config = await getConfig(); if (!config.clearCacheCronSchedule) { return; } + // Stop the existing task if it exists. if (task) { task.stop(); } - task = cron.schedule(config.clearCacheCronSchedule, async () => { - await clearCache(service); + task = new CronJob(config.clearCacheCronSchedule, async () => { + await clearCache(); }); + task.start(); }; diff --git a/src/shared/utils/crypto.ts b/src/shared/utils/crypto.ts index 4086d4a9e..a6e8c4e8a 100644 --- a/src/shared/utils/crypto.ts +++ b/src/shared/utils/crypto.ts @@ -1,20 +1,20 @@ -import crypto from "crypto"; import CryptoJS from "crypto-js"; +import crypto from "node:crypto"; import { env } from "./env"; -export const encrypt = (data: string): string => { +export function encrypt(data: string): string { return CryptoJS.AES.encrypt(data, env.ENCRYPTION_PASSWORD).toString(); -}; +} -export const decrypt = (data: string, password: string) => { +export function decrypt(data: string, password: string) { return CryptoJS.AES.decrypt(data, password).toString(CryptoJS.enc.Utf8); -}; +} -export const isWellFormedPublicKey = (key: string) => { +export function isWellFormedPublicKey(key: string) { try { crypto.createPublicKey(key); return true; } catch (_e) { return false; } -}; +} diff --git a/src/shared/utils/custom-auth-header.ts b/src/shared/utils/custom-auth-header.ts new file mode 100644 index 000000000..ac5fd54f5 --- /dev/null +++ b/src/shared/utils/custom-auth-header.ts @@ -0,0 +1,63 @@ +import { createHmac } from "node:crypto"; + +/** + * Generates an HMAC-256 secret to set in the "Authorization" header. + * + * @param webhookUrl - The URL to call. + * @param body - The request body. + * @param timestamp - The request timestamp. + * @param nonce - A unique string for this request. Should not be re-used. + * @param clientId - Your application's client id. + * @param clientSecret - Your application's client secret. + * @returns + */ +export const generateSecretHmac256 = (args: { + webhookUrl: string; + body: Record; + timestamp: Date; + nonce: string; + clientId: string; + clientSecret: string; +}): string => { + const { webhookUrl, body, timestamp, nonce, clientId, clientSecret } = args; + + // Create the body hash by hashing the payload. + const bodyHash = createHmac("sha256", clientSecret) + .update(JSON.stringify(body), "utf8") + .digest("base64"); + + // Create the signature hash by hashing the signature. + const ts = timestamp.getTime(); // timestamp expected in milliseconds + const httpMethod = "POST"; + const url = new URL(webhookUrl); + const resourcePath = url.pathname; + const host = url.hostname; + const port = url.port + ? Number.parseInt(url.port) + : url.protocol === "https:" + ? 443 + : 80; + + const signature = [ + ts, + nonce, + httpMethod, + resourcePath, + host, + port, + bodyHash, + "", // to insert a newline at the end + ].join("\n"); + + const signatureHash = createHmac("sha256", clientSecret) + .update(signature, "utf8") + .digest("base64"); + + return [ + `MAC id="${clientId}"`, + `ts="${ts}"`, + `nonce="${nonce}"`, + `bodyhash="${bodyHash}"`, + `mac="${signatureHash}"`, + ].join(","); +}; diff --git a/src/shared/utils/env.ts b/src/shared/utils/env.ts index 3fa84dcc2..3bbbe593d 100644 --- a/src/shared/utils/env.ts +++ b/src/shared/utils/env.ts @@ -6,18 +6,6 @@ import { z } from "zod"; const path = process.env.NODE_ENV === "test" ? ".env.test" : ".env"; dotenv.config({ path }); -export const JsonSchema = z.string().refine( - (value) => { - try { - JSON.parse(value); - return true; - } catch { - return false; - } - }, - { message: "Invalid JSON string" }, -); - const boolEnvSchema = (defaultBool: boolean) => z .string() @@ -68,7 +56,6 @@ export const env = createEnv({ .default("https://c.thirdweb.com/event"), SDK_BATCH_TIME_LIMIT: z.coerce.number().default(0), SDK_BATCH_SIZE_LIMIT: z.coerce.number().default(100), - ENABLE_KEYPAIR_AUTH: boolEnvSchema(false), REDIS_URL: z.string(), SEND_TRANSACTION_QUEUE_CONCURRENCY: z.coerce.number().default(200), CONFIRM_TRANSACTION_QUEUE_CONCURRENCY: z.coerce.number().default(200), @@ -76,6 +63,7 @@ export const env = createEnv({ .enum(["default", "sandbox", "server_only", "worker_only"]) .default("default"), GLOBAL_RATE_LIMIT_PER_MIN: z.coerce.number().default(400 * 60), + ACCOUNT_CACHE_SIZE: z.coerce.number().default(2048), DD_TRACER_ACTIVATED: boolEnvSchema(false), // Prometheus @@ -98,6 +86,17 @@ export const env = createEnv({ QUEUE_FAIL_HISTORY_COUNT: z.coerce.number().default(10_000), // Sets the number of recent nonces to map to queue IDs. NONCE_MAP_COUNT: z.coerce.number().default(10_000), + // Sets the age (in seconds) after which completed or failed jobs are eligible for removal. Default: 7 days. + QUEUE_JOB_RETENTION_AGE_SECONDS: z.coerce.number().default(7 * 24 * 60 * 60), + // Overrides the cron schedule for contract subscription jobs. + CONTRACT_SUBSCRIPTION_CRON_SCHEDULE_OVERRIDE: z.string().optional(), + + ENABLE_KEYPAIR_AUTH: boolEnvSchema(false), + ENABLE_CUSTOM_HMAC_AUTH: boolEnvSchema(false), + CUSTOM_HMAC_AUTH_CLIENT_ID: z.string().optional(), + CUSTOM_HMAC_AUTH_CLIENT_SECRET: z.string().optional(), + + SEND_WEBHOOK_QUEUE_CONCURRENCY: z.coerce.number().default(10), /** * Experimental env vars. These may be renamed or removed in future non-major releases. @@ -108,6 +107,18 @@ export const env = createEnv({ .default(30 * 60), // Sets the max gas price for a transaction attempt. Most RPCs reject transactions above a certain gas price. Default: 10^18 wei. EXPERIMENTAL__MAX_GAS_PRICE_WEI: z.coerce.number().default(10 ** 18), + EXPERIMENTAL__MINE_WORKER_BASE_POLL_INTERVAL_SECONDS: z.coerce + .number() + .default(2), + EXPERIMENTAL__MINE_WORKER_MAX_POLL_INTERVAL_SECONDS: z.coerce + .number() + .default(20), + EXPERIMENTAL__MINE_WORKER_POLL_INTERVAL_SCALING_FACTOR: z.coerce + .number() + .gt(0.0, "scaling factor must be greater than 0") + .default(1.0), + // Retry prepareUserOp errors instead of immediately failing the transaction + EXPERIMENTAL__RETRY_PREPARE_USEROP_ERRORS: boolEnvSchema(false), }, clientPrefix: "NEVER_USED", client: {}, @@ -130,7 +141,6 @@ export const env = createEnv({ CLIENT_ANALYTICS_URL: process.env.CLIENT_ANALYTICS_URL, SDK_BATCH_TIME_LIMIT: process.env.SDK_BATCH_TIME_LIMIT, SDK_BATCH_SIZE_LIMIT: process.env.SDK_BATCH_SIZE_LIMIT, - ENABLE_KEYPAIR_AUTH: process.env.ENABLE_KEYPAIR_AUTH, REDIS_URL: process.env.REDIS_URL, SEND_TRANSACTION_QUEUE_CONCURRENCY: process.env.SEND_TRANSACTION_QUEUE_CONCURRENCY, @@ -143,13 +153,30 @@ export const env = createEnv({ DD_TRACER_ACTIVATED: process.env.DD_TRACER_ACTIVATED, QUEUE_COMPLETE_HISTORY_COUNT: process.env.QUEUE_COMPLETE_HISTORY_COUNT, QUEUE_FAIL_HISTORY_COUNT: process.env.QUEUE_FAIL_HISTORY_COUNT, + QUEUE_JOB_RETENTION_AGE_SECONDS: process.env.QUEUE_JOB_RETENTION_AGE_SECONDS, NONCE_MAP_COUNT: process.env.NONCE_MAP_COUNT, + CONTRACT_SUBSCRIPTION_CRON_SCHEDULE_OVERRIDE: + process.env.CONTRACT_SUBSCRIPTION_CRON_SCHEDULE_OVERRIDE, EXPERIMENTAL__MINE_WORKER_TIMEOUT_SECONDS: process.env.EXPERIMENTAL__MINE_WORKER_TIMEOUT_SECONDS, EXPERIMENTAL__MAX_GAS_PRICE_WEI: process.env.EXPERIMENTAL__MAX_GAS_PRICE_WEI, METRICS_PORT: process.env.METRICS_PORT, METRICS_ENABLED: process.env.METRICS_ENABLED, + ENABLE_KEYPAIR_AUTH: process.env.ENABLE_KEYPAIR_AUTH, + ENABLE_CUSTOM_HMAC_AUTH: process.env.ENABLE_CUSTOM_HMAC_AUTH, + CUSTOM_HMAC_AUTH_CLIENT_ID: process.env.CUSTOM_HMAC_AUTH_CLIENT_ID, + CUSTOM_HMAC_AUTH_CLIENT_SECRET: process.env.CUSTOM_HMAC_AUTH_CLIENT_SECRET, + ACCOUNT_CACHE_SIZE: process.env.ACCOUNT_CAHCE_SIZE, + EXPERIMENTAL__MINE_WORKER_BASE_POLL_INTERVAL_SECONDS: + process.env.EXPERIMENTAL__MINE_WORKER_BASE_POLL_INTERVAL_SECONDS, + EXPERIMENTAL__MINE_WORKER_MAX_POLL_INTERVAL_SECONDS: + process.env.EXPERIMENTAL__MINE_WORKER_MAX_POLL_INTERVAL_SECONDS, + EXPERIMENTAL__MINE_WORKER_POLL_INTERVAL_SCALING_FACTOR: + process.env.EXPERIMENTAL__MINE_WORKER_POLL_INTERVAL_SCALING_FACTOR, + EXPERIMENTAL__RETRY_PREPARE_USEROP_ERRORS: + process.env.EXPERIMENTAL__RETRY_PREPARE_USEROP_ERRORS, + SEND_WEBHOOK_QUEUE_CONCURRENCY: process.env.SEND_WEBHOOK_QUEUE_CONCURRENCY, }, onValidationError: (error: ZodError) => { console.error( diff --git a/src/shared/utils/error.ts b/src/shared/utils/error.ts index 51d4f8f47..33c239b16 100644 --- a/src/shared/utils/error.ts +++ b/src/shared/utils/error.ts @@ -19,7 +19,7 @@ export const isNonceAlreadyUsedError = (error: unknown) => { if (message) { return ( - message.includes("nonce too low") || message.includes("already known") + message.includes("nonce too low") || message.includes("already known") || message.includes("incorrect account sequence") ); } diff --git a/src/shared/utils/primitive-types.ts b/src/shared/utils/primitive-types.ts index 759902bc2..5f7f30cc6 100644 --- a/src/shared/utils/primitive-types.ts +++ b/src/shared/utils/primitive-types.ts @@ -1,10 +1,19 @@ import type { Address } from "thirdweb"; import { checksumAddress } from "thirdweb/utils"; +import { badBigIntError } from "../../server/middleware/error"; export const maybeBigInt = (val?: string) => (val ? BigInt(val) : undefined); export const maybeInt = (val?: string) => val ? Number.parseInt(val) : undefined; +export function requiredBigInt(val: string, variableName: string) { + try { + return BigInt(val); + } catch { + throw badBigIntError(variableName); + } +} + // These overloads hint TS at the response type (ex: Address if `val` is Address). export function normalizeAddress(val: Address): Address; export function normalizeAddress(val?: Address): Address | undefined; diff --git a/src/shared/utils/sdk.ts b/src/shared/utils/sdk.ts index eeccff5d6..bdb95b7a7 100644 --- a/src/shared/utils/sdk.ts +++ b/src/shared/utils/sdk.ts @@ -1,5 +1,5 @@ import { sha256HexSync } from "@thirdweb-dev/crypto"; -import { createThirdwebClient } from "thirdweb"; +import { createThirdwebClient, hexToNumber, isHex } from "thirdweb"; import type { TransactionReceipt } from "thirdweb/transaction"; import { env } from "./env"; @@ -27,14 +27,6 @@ export const fromTransactionType = (type: TransactionReceipt["type"]) => { if (type === "eip2930") return 2; if (type === "eip4844") return 3; if (type === "eip7702") return 4; + if (isHex(type)) return hexToNumber(type); throw new Error(`Unexpected transaction type ${type}`); }; - -export const toTransactionType = (value: number) => { - if (value === 0) return "legacy"; - if (value === 1) return "eip1559"; - if (value === 2) return "eip2930"; - if (value === 3) return "eip4844"; - if (value === 4) return "eip7702"; - throw new Error(`Unexpected transaction type number ${value}`); -}; diff --git a/src/shared/utils/transaction/insert-transaction.ts b/src/shared/utils/transaction/insert-transaction.ts index 25fd99967..19bec2673 100644 --- a/src/shared/utils/transaction/insert-transaction.ts +++ b/src/shared/utils/transaction/insert-transaction.ts @@ -7,7 +7,6 @@ import { WalletDetailsError, type ParsedWalletDetails, } from "../../../shared/db/wallets/get-wallet-details"; -import { doesChainSupportService } from "../../lib/chain/chain-capabilities"; import { createCustomError } from "../../../server/middleware/error"; import { SendTransactionQueue } from "../../../worker/queues/send-transaction-queue"; import { getChecksumAddress } from "../primitive-types"; @@ -82,19 +81,6 @@ export const insertTransaction = async ( ); } - if ( - !(await doesChainSupportService( - queuedTransaction.chainId, - "account-abstraction", - )) - ) { - throw createCustomError( - "Chain does not support smart backend wallets", - StatusCodes.BAD_REQUEST, - "SBW_CHAIN_NOT_SUPPORTED", - ); - } - queuedTransaction = { ...queuedTransaction, isUserOp: true, @@ -109,9 +95,10 @@ export const insertTransaction = async ( } catch (e) { if (e instanceof WalletDetailsError) { // do nothing. The this is a smart backend wallet using a v4 endpoint + } else { + // if other type of error, rethrow + throw e; } - // if other type of error, rethrow - throw e; } if (!walletDetails && queuedTransaction.accountAddress) { @@ -124,19 +111,6 @@ export const insertTransaction = async ( // entrypointAddress is not set // accountFactoryAddress is not set if (walletDetails && isSmartBackendWallet(walletDetails)) { - if ( - !(await doesChainSupportService( - queuedTransaction.chainId, - "account-abstraction", - )) - ) { - throw createCustomError( - "Chain does not support smart backend wallets", - StatusCodes.BAD_REQUEST, - "SBW_CHAIN_NOT_SUPPORTED", - ); - } - queuedTransaction = { ...queuedTransaction, entrypointAddress: walletDetails.entrypointAddress ?? undefined, diff --git a/src/shared/utils/transaction/queue-transation.ts b/src/shared/utils/transaction/queue-transation.ts index 35e391cb3..eb0646f77 100644 --- a/src/shared/utils/transaction/queue-transation.ts +++ b/src/shared/utils/transaction/queue-transation.ts @@ -70,6 +70,9 @@ export async function queueTransaction(args: QueuedTransactionParams) { const insertedTransaction: InsertedTransaction = { chainId: transaction.chain.id, from: fromAddress, + authorizationList: await resolvePromisedValue( + transaction.authorizationList, + ), to: toAddress, data, value: await resolvePromisedValue(transaction.value), diff --git a/src/shared/utils/transaction/types.ts b/src/shared/utils/transaction/types.ts index 6b28b88d3..978882196 100644 --- a/src/shared/utils/transaction/types.ts +++ b/src/shared/utils/transaction/types.ts @@ -1,4 +1,9 @@ -import type { Address, Hex, toSerializableTransaction } from "thirdweb"; +import type { + Address, + Hex, + SignedAuthorization, + toSerializableTransaction, +} from "thirdweb"; import type { TransactionType } from "viem"; // TODO: Replace with thirdweb SDK exported type when available. @@ -30,6 +35,7 @@ export type InsertedTransaction = { value?: bigint; data?: Hex; + authorizationList?: SignedAuthorization[]; functionName?: string; functionArgs?: unknown[]; @@ -39,6 +45,7 @@ export type InsertedTransaction = { gasPrice?: bigint; maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint; + gasFeeCeiling?: bigint; }; timeoutSeconds?: number; diff --git a/src/shared/utils/usage.ts b/src/shared/utils/usage.ts index 64ec568e8..73ab3bed3 100644 --- a/src/shared/utils/usage.ts +++ b/src/shared/utils/usage.ts @@ -40,9 +40,10 @@ export interface ReportUsageParams { const ANALYTICS_DEFAULT_HEADERS = { "Content-Type": "application/json", - "x-sdk-version": process.env.ENGINE_VERSION, + "x-sdk-version": env.ENGINE_VERSION, "x-product-name": "engine", "x-client-id": thirdwebClientId, + "x-secret-key": env.THIRDWEB_API_SECRET_KEY, } as HeadersInit; const SKIP_USAGE_PATHS = new Set([ diff --git a/src/shared/utils/webhook.ts b/src/shared/utils/webhook.ts index c31b3e1af..1997502e6 100644 --- a/src/shared/utils/webhook.ts +++ b/src/shared/utils/webhook.ts @@ -1,43 +1,70 @@ import type { Webhooks } from "@prisma/client"; -import crypto from "node:crypto"; +import assert from "node:assert"; +import crypto, { randomUUID } from "node:crypto"; +import { Agent, fetch } from "undici"; +import { getConfig } from "./cache/get-config"; +import { env } from "./env"; import { prettifyError } from "./error"; +import { generateSecretHmac256 } from "./custom-auth-header"; -export const generateSignature = ( +function generateSignature( body: Record, - timestamp: string, + timestampSeconds: number, secret: string, -): string => { +): string { const _body = JSON.stringify(body); - const payload = `${timestamp}.${_body}`; + const payload = `${timestampSeconds}.${_body}`; return crypto.createHmac("sha256", secret).update(payload).digest("hex"); -}; +} -export const createWebhookRequestHeaders = async ( - webhook: Webhooks, - body: Record, -): Promise => { - const headers: { - Accept: string; - "Content-Type": string; - Authorization?: string; - "x-engine-signature"?: string; - "x-engine-timestamp"?: string; - } = { - Accept: "application/json", - "Content-Type": "application/json", - }; +function generateAuthorization(args: { + webhook: Webhooks; + timestamp: Date; + body: Record; +}): string { + const { webhook, timestamp, body } = args; - if (webhook.secret) { - const timestamp = Math.floor(Date.now() / 1000).toString(); - const signature = generateSignature(body, timestamp, webhook.secret); + if (env.ENABLE_CUSTOM_HMAC_AUTH) { + assert( + env.CUSTOM_HMAC_AUTH_CLIENT_ID, + 'Missing "CUSTOM_HMAC_AUTH_CLIENT_ID".', + ); + assert( + env.CUSTOM_HMAC_AUTH_CLIENT_SECRET, + 'Missing "CUSTOM_HMAC_AUTH_CLIENT_SECRET"', + ); - headers.Authorization = `Bearer ${webhook.secret}`; - headers["x-engine-signature"] = signature; - headers["x-engine-timestamp"] = timestamp; + return generateSecretHmac256({ + webhookUrl: webhook.url, + body, + timestamp, + nonce: randomUUID(), + clientId: env.CUSTOM_HMAC_AUTH_CLIENT_ID, + clientSecret: env.CUSTOM_HMAC_AUTH_CLIENT_SECRET, + }); } - return headers; -}; + return `Bearer ${webhook.secret}`; +} + +export function generateRequestHeaders(args: { + webhook: Webhooks; + body: Record; + timestamp: Date; +}): HeadersInit { + const { webhook, body, timestamp } = args; + + const timestampSeconds = Math.floor(timestamp.getTime() / 1000); + const signature = generateSignature(body, timestampSeconds, webhook.secret); + const authorization = generateAuthorization({ webhook, timestamp, body }); + return { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: authorization, + "x-engine-signature": signature, + "x-engine-timestamp": timestampSeconds.toString(), + }; +} export interface WebhookResponse { ok: boolean; @@ -50,11 +77,32 @@ export const sendWebhookRequest = async ( body: Record, ): Promise => { try { - const headers = await createWebhookRequestHeaders(webhook, body); + const config = await getConfig(); + + // If mTLS is enabled, provide the certificate with this request. + const dispatcher = + config.mtlsCertificate && config.mtlsPrivateKey + ? new Agent({ + connect: { + cert: config.mtlsCertificate, + key: config.mtlsPrivateKey, + // Validate the server's certificate. + rejectUnauthorized: true, + }, + }) + : undefined; + + const headers = generateRequestHeaders({ + webhook, + body, + timestamp: new Date(), + }); + const resp = await fetch(webhook.url, { method: "POST", headers: headers, body: JSON.stringify(body), + dispatcher, }); return { diff --git a/src/worker/index.ts b/src/worker/index.ts index dce96767f..251c427ca 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -1,12 +1,4 @@ import { chainIndexerListener } from "./listeners/chain-indexer-listener"; -import { - newConfigurationListener, - updatedConfigurationListener, -} from "./listeners/config-listener"; -import { - newWebhooksListener, - updatedWebhooksListener, -} from "./listeners/webhook-listener"; import { initCancelRecycledNoncesWorker } from "./tasks/cancel-recycled-nonces-worker"; import { initMineTransactionWorker } from "./tasks/mine-transaction-worker"; import { initNonceHealthCheckWorker } from "./tasks/nonce-health-check-worker"; @@ -16,6 +8,7 @@ import { initProcessTransactionReceiptsWorker } from "./tasks/process-transactio import { initPruneTransactionsWorker } from "./tasks/prune-transactions-worker"; import { initSendTransactionWorker } from "./tasks/send-transaction-worker"; import { initSendWebhookWorker } from "./tasks/send-webhook-worker"; +import { initWalletSubscriptionWorker } from "./tasks/wallet-subscription-worker"; export const initWorker = async () => { initCancelRecycledNoncesWorker(); @@ -25,18 +18,10 @@ export const initWorker = async () => { initSendTransactionWorker(); initMineTransactionWorker(); initSendWebhookWorker(); - initNonceHealthCheckWorker(); await initNonceResyncWorker(); - - // Listen for new & updated configuration data. - await newConfigurationListener(); - await updatedConfigurationListener(); - - // Listen for new & updated webhooks data. - await newWebhooksListener(); - await updatedWebhooksListener(); + await initWalletSubscriptionWorker(); // Contract subscriptions. await chainIndexerListener(); diff --git a/src/worker/indexers/chain-indexer-registry.ts b/src/worker/indexers/chain-indexer-registry.ts index ed091e288..dae0cdf1e 100644 --- a/src/worker/indexers/chain-indexer-registry.ts +++ b/src/worker/indexers/chain-indexer-registry.ts @@ -1,32 +1,34 @@ -import cron from "node-cron"; import { getBlockTimeSeconds } from "../../shared/utils/indexer/get-block-time"; import { logger } from "../../shared/utils/logger"; import { handleContractSubscriptions } from "../tasks/chain-indexer"; +import { env } from "../../shared/utils/env"; +import { CronJob } from "cron"; // @TODO: Move all worker logic to Bullmq to better handle multiple hosts. -export const INDEXER_REGISTRY = {} as Record; +export const INDEXER_REGISTRY: Record = {}; export const addChainIndexer = async (chainId: number) => { if (INDEXER_REGISTRY[chainId]) { return; } - // Estimate the block time in the last 100 blocks. Default to 2 second block times. - let blockTimeSeconds: number; - try { - blockTimeSeconds = await getBlockTimeSeconds(chainId, 100); - } catch (error) { - logger({ - service: "worker", - level: "error", - message: `Could not estimate block time for chain ${chainId}`, - error, - }); - blockTimeSeconds = 2; + let cronSchedule = env.CONTRACT_SUBSCRIPTION_CRON_SCHEDULE_OVERRIDE; + if (!cronSchedule) { + // Estimate the block time in the last 100 blocks. Default to 2 second block times. + let blockTimeSeconds: number; + try { + blockTimeSeconds = await getBlockTimeSeconds(chainId, 100); + } catch (error) { + logger({ + service: "worker", + level: "error", + message: `Could not estimate block time for chain ${chainId}`, + error, + }); + blockTimeSeconds = 2; + } + cronSchedule = createScheduleSeconds(blockTimeSeconds); } - const cronSchedule = createScheduleSeconds( - Math.max(Math.round(blockTimeSeconds), 1), - ); logger({ service: "worker", level: "info", @@ -35,7 +37,7 @@ export const addChainIndexer = async (chainId: number) => { let inProgress = false; - const task = cron.schedule(cronSchedule, async () => { + const task = new CronJob(cronSchedule, async () => { if (inProgress) { return; } @@ -56,6 +58,7 @@ export const addChainIndexer = async (chainId: number) => { }); INDEXER_REGISTRY[chainId] = task; + task.start(); }; export const removeChainIndexer = async (chainId: number) => { @@ -74,5 +77,7 @@ export const removeChainIndexer = async (chainId: number) => { delete INDEXER_REGISTRY[chainId]; }; -export const createScheduleSeconds = (seconds: number) => - `*/${seconds} * * * * *`; +function createScheduleSeconds(blockTimeSeconds: number) { + const pollIntervalSeconds = Math.max(Math.round(blockTimeSeconds), 2); + return `*/${pollIntervalSeconds} * * * * *`; +} diff --git a/src/worker/listeners/chain-indexer-listener.ts b/src/worker/listeners/chain-indexer-listener.ts index f2c05e6e0..785b5f64a 100644 --- a/src/worker/listeners/chain-indexer-listener.ts +++ b/src/worker/listeners/chain-indexer-listener.ts @@ -1,21 +1,23 @@ -import cron from "node-cron"; +import { CronJob } from "cron"; import { getConfig } from "../../shared/utils/cache/get-config"; import { logger } from "../../shared/utils/logger"; import { manageChainIndexers } from "../tasks/manage-chain-indexers"; let processChainIndexerStarted = false; -let task: cron.ScheduledTask; +let task: CronJob; export const chainIndexerListener = async (): Promise => { const config = await getConfig(); if (!config.indexerListenerCronSchedule) { return; } + + // Stop the existing task if it exists. if (task) { task.stop(); } - task = cron.schedule(config.indexerListenerCronSchedule, async () => { + task = new CronJob(config.indexerListenerCronSchedule, async () => { if (!processChainIndexerStarted) { processChainIndexerStarted = true; await manageChainIndexers(); @@ -28,4 +30,5 @@ export const chainIndexerListener = async (): Promise => { }); } }); + task.start(); }; diff --git a/src/worker/listeners/config-listener.ts b/src/worker/listeners/config-listener.ts deleted file mode 100644 index bda2e9c9f..000000000 --- a/src/worker/listeners/config-listener.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { knex } from "../../shared/db/client"; -import { getConfig } from "../../shared/utils/cache/get-config"; -import { clearCacheCron } from "../../shared/utils/cron/clear-cache-cron"; -import { logger } from "../../shared/utils/logger"; -import { chainIndexerListener } from "./chain-indexer-listener"; - -export const newConfigurationListener = async (): Promise => { - logger({ - service: "worker", - level: "info", - message: "Listening for new configuration data", - }); - - // TODO: This doesn't even need to be a listener - const connection = await knex.client.acquireConnection(); - connection.query("LISTEN new_configuration_data"); - - // Whenever we receive a new transaction, process it - connection.on( - "notification", - async (_msg: { channel: string; payload: string }) => { - // Update Configs Data - await getConfig(false); - }, - ); - - connection.on("end", async () => { - await knex.destroy(); - knex.client.releaseConnection(connection); - - logger({ - service: "worker", - level: "info", - message: "Released database connection on end", - }); - }); - - connection.on("error", async (err: unknown) => { - logger({ - service: "worker", - level: "error", - message: "Database connection error", - error: err, - }); - - await knex.destroy(); - knex.client.releaseConnection(connection); - - logger({ - service: "worker", - level: "info", - message: "Released database connection on error", - error: err, - }); - }); -}; - -export const updatedConfigurationListener = async (): Promise => { - logger({ - service: "worker", - level: "info", - message: "Listening for updated configuration data", - }); - - // TODO: This doesn't even need to be a listener - const connection = await knex.client.acquireConnection(); - connection.query("LISTEN updated_configuration_data"); - - // Whenever we receive a new transaction, process it - connection.on( - "notification", - async (_msg: { channel: string; payload: string }) => { - // Update Configs Data - logger({ - service: "worker", - level: "info", - message: "Updated configuration data", - }); - await getConfig(false); - await clearCacheCron("worker"); - await chainIndexerListener(); - }, - ); - - connection.on("end", async () => { - await knex.destroy(); - knex.client.releaseConnection(connection); - - logger({ - service: "worker", - level: "info", - message: "Released database connection on end", - }); - }); - - connection.on("error", async (err: unknown) => { - logger({ - service: "worker", - level: "error", - message: "Database connection error", - error: err, - }); - - await knex.destroy(); - knex.client.releaseConnection(connection); - - logger({ - service: "worker", - level: "info", - message: "Released database connection on error", - error: err, - }); - }); -}; diff --git a/src/worker/listeners/webhook-listener.ts b/src/worker/listeners/webhook-listener.ts deleted file mode 100644 index 759182170..000000000 --- a/src/worker/listeners/webhook-listener.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { knex } from "../../shared/db/client"; -import { webhookCache } from "../../shared/utils/cache/get-webhook"; -import { logger } from "../../shared/utils/logger"; - -export const newWebhooksListener = async (): Promise => { - logger({ - service: "worker", - level: "info", - message: "Listening for new webhooks data", - }); - - // TODO: This doesn't even need to be a listener - const connection = await knex.client.acquireConnection(); - connection.query("LISTEN new_webhook_data"); - - // Whenever we receive a new transaction, process it - connection.on( - "notification", - async (_msg: { channel: string; payload: string }) => { - logger({ - service: "worker", - level: "info", - message: "Received new webhooks data", - }); - // Update Webhooks Data - webhookCache.clear(); - }, - ); - - connection.on("end", async () => { - await knex.destroy(); - knex.client.releaseConnection(connection); - - logger({ - service: "worker", - level: "info", - message: "Released database connection on end", - }); - }); - - connection.on("error", async (err: unknown) => { - logger({ - service: "worker", - level: "error", - message: "Database connection error", - error: err, - }); - - await knex.destroy(); - knex.client.releaseConnection(connection); - - logger({ - service: "worker", - level: "info", - message: "Released database connection on error", - error: err, - }); - }); -}; - -export const updatedWebhooksListener = async (): Promise => { - logger({ - service: "worker", - level: "info", - message: "Listening for updated webhooks data", - }); - - // TODO: This doesn't even need to be a listener - const connection = await knex.client.acquireConnection(); - connection.query("LISTEN updated_webhook_data"); - - // Whenever we receive a new transaction, process it - connection.on( - "notification", - async (_msg: { channel: string; payload: string }) => { - // Update Configs Data - logger({ - service: "worker", - level: "info", - message: "Received updated webhooks data", - }); - webhookCache.clear(); - }, - ); - - connection.on("end", async () => { - await knex.destroy(); - knex.client.releaseConnection(connection); - - logger({ - service: "worker", - level: "info", - message: "Released database connection on end", - }); - }); - - connection.on("error", async (err: unknown) => { - logger({ - service: "worker", - level: "error", - message: "Database connection error", - error: err, - }); - - await knex.destroy(); - knex.client.releaseConnection(connection); - - logger({ - service: "worker", - level: "info", - message: "Released database connection on error", - error: err, - }); - }); -}; diff --git a/src/worker/queues/queues.ts b/src/worker/queues/queues.ts index 6331d3a8d..3c783e91e 100644 --- a/src/worker/queues/queues.ts +++ b/src/worker/queues/queues.ts @@ -6,11 +6,11 @@ export const defaultJobOptions: JobsOptions = { // Does not retry by default. Queues must explicitly define their own retry count and backoff behavior. attempts: 0, removeOnComplete: { - age: 7 * 24 * 60 * 60, + age: env.QUEUE_JOB_RETENTION_AGE_SECONDS, count: env.QUEUE_COMPLETE_HISTORY_COUNT, }, removeOnFail: { - age: 7 * 24 * 60 * 60, + age: env.QUEUE_JOB_RETENTION_AGE_SECONDS, count: env.QUEUE_FAIL_HISTORY_COUNT, }, }; diff --git a/src/worker/queues/send-webhook-queue.ts b/src/worker/queues/send-webhook-queue.ts index 1f79bc224..72a97d3ee 100644 --- a/src/worker/queues/send-webhook-queue.ts +++ b/src/worker/queues/send-webhook-queue.ts @@ -8,10 +8,12 @@ import SuperJSON from "superjson"; import { WebhooksEventTypes, type BackendWalletBalanceWebhookParams, + type WalletSubscriptionWebhookParams, } from "../../shared/schemas/webhooks"; import { getWebhooksByEventType } from "../../shared/utils/cache/get-webhook"; import { redis } from "../../shared/utils/redis/redis"; import { defaultJobOptions } from "./queues"; +import { logger } from "../../shared/utils/logger"; export type EnqueueContractSubscriptionWebhookData = { type: WebhooksEventTypes.CONTRACT_SUBSCRIPTION; @@ -34,11 +36,18 @@ export type EnqueueLowBalanceWebhookData = { body: BackendWalletBalanceWebhookParams; }; +export type EnqueueWalletSubscriptionWebhookData = { + type: WebhooksEventTypes.WALLET_SUBSCRIPTION; + webhook: Webhooks; + body: WalletSubscriptionWebhookParams; +}; + // Add other webhook event types here. type EnqueueWebhookData = | EnqueueContractSubscriptionWebhookData | EnqueueTransactionWebhookData - | EnqueueLowBalanceWebhookData; + | EnqueueLowBalanceWebhookData + | EnqueueWalletSubscriptionWebhookData; export interface WebhookJob { data: EnqueueWebhookData; @@ -50,7 +59,7 @@ export class SendWebhookQueue { connection: redis, defaultJobOptions: { ...defaultJobOptions, - attempts: 3, + attempts: 5, backoff: { type: "exponential", delay: 5_000 }, }, }); @@ -66,6 +75,8 @@ export class SendWebhookQueue { return this._enqueueTransactionWebhook(data); case WebhooksEventTypes.BACKEND_WALLET_BALANCE: return this._enqueueBackendWalletBalanceWebhook(data); + case WebhooksEventTypes.WALLET_SUBSCRIPTION: + return this._enqueueWalletSubscriptionWebhook(data); } }; @@ -127,15 +138,41 @@ export class SendWebhookQueue { ...(await getWebhooksByEventType(data.type)), ]; + logger({ + service: "worker", + level: "info", + message: `[Webhook] Enqueuing transaction webhooks to queue for transaction ${data.queueId}`, + queueId: data.queueId, + data: { + eventType: data.type, + webhookCount: webhooks.length, + }, + }); + for (const webhook of webhooks) { const job: WebhookJob = { data, webhook }; const serialized = SuperJSON.stringify(job); + const idempotencyKey = this._getTransactionWebhookIdempotencyKey({ + webhook, + eventType: data.type, + queueId: data.queueId, + }); + await this.q.add(`${data.type}:${webhook.id}`, serialized, { - jobId: this._getTransactionWebhookIdempotencyKey({ - webhook, + jobId: idempotencyKey, + }); + + logger({ + service: "worker", + level: "info", + message: `[Webhook] Transaction webhook added to queue for transaction ${data.queueId} at destination ${webhook.url}`, + queueId: data.queueId, + data: { eventType: data.type, - queueId: data.queueId, - }), + destination: webhook.url, + webhookId: webhook.id, + idempotencyKey, + }, }); } }; @@ -161,4 +198,18 @@ export class SendWebhookQueue { ); } }; + + private static _enqueueWalletSubscriptionWebhook = async ( + data: EnqueueWalletSubscriptionWebhookData, + ) => { + const { type, webhook, body } = data; + if (!webhook.revokedAt && type === webhook.eventType) { + const job: WebhookJob = { data, webhook }; + const serialized = SuperJSON.stringify(job); + await this.q.add( + `${type}:${body.chainId}:${body.walletAddress}:${body.subscriptionId}`, + serialized, + ); + } + }; } diff --git a/src/worker/queues/wallet-subscription-queue.ts b/src/worker/queues/wallet-subscription-queue.ts new file mode 100644 index 000000000..15f4344ae --- /dev/null +++ b/src/worker/queues/wallet-subscription-queue.ts @@ -0,0 +1,14 @@ +import { Queue } from "bullmq"; +import { redis } from "../../shared/utils/redis/redis"; +import { defaultJobOptions } from "./queues"; + +export class WalletSubscriptionQueue { + static q = new Queue("wallet-subscription", { + connection: redis, + defaultJobOptions, + }); + + constructor() { + WalletSubscriptionQueue.q.setGlobalConcurrency(1); + } +} \ No newline at end of file diff --git a/src/worker/tasks/mine-transaction-worker.ts b/src/worker/tasks/mine-transaction-worker.ts index 69d8b0184..7954b2633 100644 --- a/src/worker/tasks/mine-transaction-worker.ts +++ b/src/worker/tasks/mine-transaction-worker.ts @@ -309,6 +309,13 @@ const _notifyIfLowBalance = async (transaction: MinedTransaction) => { } }; +const SCALING_FACTOR = + env.EXPERIMENTAL__MINE_WORKER_POLL_INTERVAL_SCALING_FACTOR; +const MAX_POLL_INTERVAL_MS = + env.EXPERIMENTAL__MINE_WORKER_MAX_POLL_INTERVAL_SECONDS * 1000; +const BASE_POLL_INTERVAL_MS = + env.EXPERIMENTAL__MINE_WORKER_BASE_POLL_INTERVAL_SECONDS * 1000; + // Must be explicitly called for the worker to run on this host. export const initMineTransactionWorker = () => { const _worker = new Worker(MineTransactionQueue.q.name, handler, { @@ -317,7 +324,10 @@ export const initMineTransactionWorker = () => { settings: { backoffStrategy: (attemptsMade: number) => { // Retries at 2s, 4s, 6s, ..., 18s, 20s, 20s, 20s, ... - return Math.min(attemptsMade * 2_000, 20_000); + return Math.min( + attemptsMade * BASE_POLL_INTERVAL_MS * SCALING_FACTOR, // 2_000 default * 1.0 = 2_000 + MAX_POLL_INTERVAL_MS, // 20_000 default + ); }, }, }); diff --git a/src/worker/tasks/nonce-health-check-worker.ts b/src/worker/tasks/nonce-health-check-worker.ts index 839fb794b..b75adf6e7 100644 --- a/src/worker/tasks/nonce-health-check-worker.ts +++ b/src/worker/tasks/nonce-health-check-worker.ts @@ -116,7 +116,10 @@ async function getCurrentNonceState( }; } -function nonceHistoryKey(walletAddress: Address, chainId: number) { +/** + * Stores a list of onchain vs sent nonces to check if the nonce is stuck over time. + */ +export function nonceHistoryKey(chainId: number, walletAddress: Address) { return `nonce-history:${chainId}:${getAddress(walletAddress)}`; } @@ -128,7 +131,7 @@ async function getHistoricalNonceStates( chainId: number, periods: number, ): Promise { - const key = nonceHistoryKey(walletAddress, chainId); + const key = nonceHistoryKey(chainId, walletAddress); const historicalStates = await redis.lrange(key, 0, periods - 1); return historicalStates.map((state) => JSON.parse(state)); } @@ -136,7 +139,7 @@ async function getHistoricalNonceStates( // Update nonce history async function updateNonceHistory(walletAddress: Address, chainId: number) { const currentState = await getCurrentNonceState(walletAddress, chainId); - const key = nonceHistoryKey(walletAddress, chainId); + const key = nonceHistoryKey(chainId, walletAddress); await redis .multi() diff --git a/src/worker/tasks/process-event-logs-worker.ts b/src/worker/tasks/process-event-logs-worker.ts index 95e3c9859..22e85f13b 100644 --- a/src/worker/tasks/process-event-logs-worker.ts +++ b/src/worker/tasks/process-event-logs-worker.ts @@ -1,5 +1,5 @@ import type { Prisma, Webhooks } from "@prisma/client"; -import type { AbiEvent } from "abitype"; +import type { AbiEvent } from "ox"; import { Worker, type Job, type Processor } from "bullmq"; import superjson from "superjson"; import { @@ -144,9 +144,9 @@ const getLogs = async ({ // Get events to filter by, if any. // Resolve the event name, "Transfer", to event signature, "Transfer(address to, uint256 quantity)". - const events: PreparedEvent[] = []; + const events: PreparedEvent[] = []; if (f.events.length > 0) { - const abi = await resolveContractAbi(contract); + const abi = await resolveContractAbi(contract); for (const signature of abi) { if (f.events.includes(signature.name)) { events.push(prepareEvent({ signature })); @@ -224,7 +224,7 @@ const formatDecodedLog = async (args: { }): Promise | undefined> => { const { contract, eventName, logArgs } = args; - const abi = await resolveContractAbi(contract); + const abi = await resolveContractAbi(contract); const eventSignature = abi.find((a) => a.name === eventName); if (!eventSignature) { return; diff --git a/src/worker/tasks/process-transaction-receipts-worker.ts b/src/worker/tasks/process-transaction-receipts-worker.ts index 1d4e64703..02953edac 100644 --- a/src/worker/tasks/process-transaction-receipts-worker.ts +++ b/src/worker/tasks/process-transaction-receipts-worker.ts @@ -1,17 +1,17 @@ import type { Prisma } from "@prisma/client"; -import type { AbiEvent } from "abitype"; -import { Worker, type Job, type Processor } from "bullmq"; +import { type Job, type Processor, Worker } from "bullmq"; +import type { AbiEvent } from "ox"; import superjson from "superjson"; import { + type Address, + type ThirdwebContract, eth_getBlockByNumber, eth_getTransactionReceipt, getContract, getRpcClient, - type Address, - type ThirdwebContract, } from "thirdweb"; import { resolveContractAbi } from "thirdweb/contract"; -import { decodeFunctionData, type Abi, type Hash } from "viem"; +import { type Abi, type Hash, decodeFunctionData } from "viem"; import { bulkInsertContractTransactionReceipts } from "../../shared/db/contract-transaction-receipts/create-contract-transaction-receipts"; import { WebhooksEventTypes } from "../../shared/schemas/webhooks"; import { getChain } from "../../shared/utils/chain"; @@ -20,8 +20,8 @@ import { normalizeAddress } from "../../shared/utils/primitive-types"; import { redis } from "../../shared/utils/redis/redis"; import { thirdwebClient } from "../../shared/utils/sdk"; import { - ProcessTransactionReceiptsQueue, type EnqueueProcessTransactionReceiptsData, + ProcessTransactionReceiptsQueue, } from "../queues/process-transaction-receipts-queue"; import { logWorkerExceptions } from "../queues/queues"; import { SendWebhookQueue } from "../queues/send-webhook-queue"; @@ -211,7 +211,7 @@ const getFunctionName = async (args: { contract: ThirdwebContract; data: Hash; }) => { - const abi = await resolveContractAbi(args.contract); + const abi = await resolveContractAbi(args.contract); const decoded = decodeFunctionData({ abi, data: args.data, diff --git a/src/worker/tasks/send-transaction-worker.ts b/src/worker/tasks/send-transaction-worker.ts index e00825db4..2530eea49 100644 --- a/src/worker/tasks/send-transaction-worker.ts +++ b/src/worker/tasks/send-transaction-worker.ts @@ -1,22 +1,25 @@ -import { Worker, type Job, type Processor } from "bullmq"; import assert from "node:assert"; +import { DelayedError, type Job, type Processor, Worker } from "bullmq"; import superjson from "superjson"; import { + type Address, + type Chain, + type Hex, + type ThirdwebClient, getAddress, getContract, readContract, toSerializableTransaction, toTokens, - type Hex, } from "thirdweb"; import { getChainMetadata } from "thirdweb/chains"; import { isZkSyncChain, stringify } from "thirdweb/utils"; import type { Account } from "thirdweb/wallets"; import { bundleUserOp, - createAndSignUserOp, + prepareUserOp, + signUserOp, smartWallet, - type UserOperation, } from "thirdweb/wallets/smart"; import { getContractAddress } from "viem"; import { TransactionDB } from "../../shared/db/transactions/db"; @@ -56,16 +59,21 @@ import { reportUsage } from "../../shared/utils/usage"; import { MineTransactionQueue } from "../queues/mine-transaction-queue"; import { logWorkerExceptions } from "../queues/queues"; import { - SendTransactionQueue, type SendTransactionData, + SendTransactionQueue, } from "../queues/send-transaction-queue"; +type VersionedUserOp = Awaited>; + /** * Submit a transaction to RPC (EOA transactions) or bundler (userOps). * * This worker also handles retried EOA transactions. */ -const handler: Processor = async (job: Job) => { +const handler: Processor = async ( + job: Job, + token?: string, +) => { const { queueId, resendCount } = superjson.parse( job.data, ); @@ -84,9 +92,9 @@ const handler: Processor = async (job: Job) => { if (transaction.status === "queued") { if (transaction.isUserOp) { - resultTransaction = await _sendUserOp(job, transaction); + resultTransaction = await _sendUserOp(job, transaction, token); } else { - resultTransaction = await _sendTransaction(job, transaction); + resultTransaction = await _sendTransaction(job, transaction, token); } } else if (transaction.status === "sent") { resultTransaction = await _resendTransaction(job, transaction, resendCount); @@ -116,6 +124,7 @@ const handler: Processor = async (job: Job) => { const _sendUserOp = async ( job: Job, queuedTransaction: QueuedTransaction, + token?: string, ): Promise => { assert(queuedTransaction.isUserOp); @@ -180,60 +189,101 @@ const _sendUserOp = async ( }; } - let signedUserOp: UserOperation; - try { - // Resolve the user factory from the provided address, or from the `factory()` method if found. - let accountFactoryAddress = userProvidedAccountFactoryAddress; - if (!accountFactoryAddress) { - // TODO: this is not a good solution since the assumption that the account has a factory function is not guaranteed - // instead, we should use default account factory address or throw here. - try { - const smartAccountContract = getContract({ - client: thirdwebClient, - chain, - address: accountAddress, - }); - const onchainAccountFactoryAddress = await readContract({ - contract: smartAccountContract, - method: "function factory() view returns (address)", - params: [], - }); - accountFactoryAddress = getAddress(onchainAccountFactoryAddress); - } catch { - throw new Error( - `Failed to find factory address for account '${accountAddress}' on chain '${chainId}'`, - ); - } + // Part 1: Prepare the userop + // Step 1: Get factory address + let accountFactoryAddress: Address | undefined; + + if (userProvidedAccountFactoryAddress) { + accountFactoryAddress = userProvidedAccountFactoryAddress; + } else { + const smartAccountContract = getContract({ + client: thirdwebClient, + chain, + address: accountAddress, + }); + + try { + const onchainAccountFactoryAddress = await readContract({ + contract: smartAccountContract, + method: "function factory() view returns (address)", + params: [], + }); + accountFactoryAddress = getAddress(onchainAccountFactoryAddress); + } catch (error) { + const errorMessage = `${ + wrapError(error, "RPC").message + } Failed to find factory address for account`; + const erroredTransaction: ErroredTransaction = { + ...queuedTransaction, + status: "errored", + errorMessage, + }; + job.log(`Failed to get account factory address: ${errorMessage}`); + return erroredTransaction; } + } - const transactions = queuedTransaction.batchOperations - ? queuedTransaction.batchOperations.map((op) => ({ - ...op, - chain, + // Step 2: Get entrypoint address + let entrypointAddress: Address | undefined; + if (userProvidedEntrypointAddress) { + entrypointAddress = queuedTransaction.entrypointAddress; + } else { + try { + entrypointAddress = await getEntrypointFromFactory( + adminAccount.address, + thirdwebClient, + chain, + ); + } catch (error) { + const errorMessage = `${ + wrapError(error, "RPC").message + } Failed to find entrypoint address for account factory`; + const erroredTransaction: ErroredTransaction = { + ...queuedTransaction, + status: "errored", + errorMessage, + }; + job.log( + `Failed to find entrypoint address for account factory: ${errorMessage}`, + ); + return erroredTransaction; + } + } + + // Step 3: Transform transactions for userop + const transactions = queuedTransaction.batchOperations + ? queuedTransaction.batchOperations.map((op) => ({ + ...op, + chain, + client: thirdwebClient, + })) + : [ + { client: thirdwebClient, - })) - : [ - { - client: thirdwebClient, - chain, - ...queuedTransaction, - ...overrides, - to: getChecksumAddress(toAddress), - }, - ]; - - signedUserOp = (await createAndSignUserOp({ - client: thirdwebClient, + chain, + data: queuedTransaction.data, + value: queuedTransaction.value, + ...overrides, // gas-overrides + to: getChecksumAddress(toAddress), + }, + ]; + + // Step 4: Prepare userop + let unsignedUserOp: VersionedUserOp | undefined; + + try { + unsignedUserOp = await prepareUserOp({ transactions, adminAccount, + client: thirdwebClient, smartWalletOptions: { chain, sponsorGas: true, - factoryAddress: accountFactoryAddress, + factoryAddress: accountFactoryAddress, // from step 1 overrides: { accountAddress, accountSalt, - entrypointAddress: userProvidedEntrypointAddress, + entrypointAddress, // from step 2 // TODO: let user pass entrypoint address for 0.7 support }, }, @@ -242,28 +292,126 @@ const _sendUserOp = async ( // until the previous userop for the same account is mined // we don't want this behavior in the engine context waitForDeployment: false, - })) as UserOperation; // TODO support entrypoint v0.7 accounts + }); } catch (error) { const errorMessage = wrapError(error, "Bundler").message; + job.log(`Failed to populate transaction: ${errorMessage}`); + + // If retry is enabled for prepareUserOp errors, throw to trigger job retry + if (env.EXPERIMENTAL__RETRY_PREPARE_USEROP_ERRORS) { + throw error; + } + + // Otherwise, return errored transaction as before const erroredTransaction: ErroredTransaction = { ...queuedTransaction, status: "errored", errorMessage, }; - job.log(`Failed to populate transaction: ${errorMessage}`); return erroredTransaction; } - job.log(`Populated userOp: ${stringify(signedUserOp)}`); + // Handle if `gasFeeCeiling` is overridden. + // Delay the job if the estimated cost is higher than the gas fee ceiling. + const gasFeeCeiling = overrides?.gasFeeCeiling; + if (typeof gasFeeCeiling !== "undefined") { + const estimatedCost = + unsignedUserOp.maxFeePerGas * + (unsignedUserOp.callGasLimit + + unsignedUserOp.preVerificationGas + + unsignedUserOp.verificationGasLimit); + + if (estimatedCost > gasFeeCeiling) { + const retryAt = _minutesFromNow(5); + job.log( + `Override gas fee ceiling (${gasFeeCeiling}) is lower than onchain estimated cost (${estimatedCost}). Delaying job until ${retryAt}. [callGasLimit: ${unsignedUserOp.callGasLimit}, preVerificationGas: ${unsignedUserOp.preVerificationGas}, verificationGasLimit: ${unsignedUserOp.verificationGasLimit}, maxFeePerGas: ${unsignedUserOp.maxFeePerGas}]`, + ); + // token is required to acquire lock for delaying currently processing job: https://docs.bullmq.io/patterns/process-step-jobs#delaying + await job.moveToDelayed(retryAt.getTime(), token); + // throwing delayed error is required to notify bullmq worker not to complete or fail the job + throw new DelayedError("Delaying job due to gas fee override"); + } + } - const userOpHash = await bundleUserOp({ - userOp: signedUserOp, - options: { + // Handle if `maxFeePerGas` is overridden. + // Set it if the transaction will be sent, otherwise delay the job. + const overrideMaxFeePerGas = overrides?.maxFeePerGas; + if (typeof overrideMaxFeePerGas !== "undefined") { + if (unsignedUserOp.maxFeePerGas > overrideMaxFeePerGas) { + const retryAt = _minutesFromNow(5); + job.log( + `Override gas fee (${overrideMaxFeePerGas}) is lower than onchain fee (${unsignedUserOp.maxFeePerGas}). Delaying job until ${retryAt}.`, + ); + // token is required to acquire lock for delaying currently processing job: https://docs.bullmq.io/patterns/process-step-jobs#delaying + await job.moveToDelayed(retryAt.getTime(), token); + // throwing delayed error is required to notify bullmq worker not to complete or fail the job + throw new DelayedError("Delaying job due to gas fee override"); + } + } + + // Part 2: Sign the userop + let signedUserOp: VersionedUserOp | undefined; + try { + signedUserOp = await signUserOp({ client: thirdwebClient, chain, - entrypointAddress: userProvidedEntrypointAddress, - }, - }); + adminAccount, + entrypointAddress, + userOp: unsignedUserOp, + }); + } catch (error) { + const errorMessage = `${ + wrapError(error, "Bundler").message + } Failed to sign prepared userop`; + job.log(`Failed to sign userop: ${errorMessage}`); + + // If retry is enabled for prepareUserOp errors, throw to trigger job retry + if (env.EXPERIMENTAL__RETRY_PREPARE_USEROP_ERRORS) { + throw error; + } + + // Otherwise, return errored transaction as before + const erroredTransaction: ErroredTransaction = { + ...queuedTransaction, + status: "errored", + errorMessage, + }; + return erroredTransaction; + } + + job.log(`Populated and signed userOp: ${stringify(signedUserOp)}`); + + // Finally: bundle the userop + let userOpHash: Hex; + + try { + userOpHash = await bundleUserOp({ + userOp: signedUserOp, + options: { + client: thirdwebClient, + chain, + entrypointAddress: userProvidedEntrypointAddress, + }, + }); + } catch (error) { + const errorMessage = `${ + wrapError(error, "Bundler").message + } Failed to bundle userop`; + job.log(`Failed to bundle userop: ${errorMessage}`); + + // If retry is enabled for prepareUserOp errors, throw to trigger job retry + if (env.EXPERIMENTAL__RETRY_PREPARE_USEROP_ERRORS) { + throw error; + } + + // Otherwise, return errored transaction as before + const erroredTransaction: ErroredTransaction = { + ...queuedTransaction, + status: "errored", + errorMessage, + }; + return erroredTransaction; + } return { ...queuedTransaction, @@ -282,6 +430,7 @@ const _sendUserOp = async ( const _sendTransaction = async ( job: Job, queuedTransaction: QueuedTransaction, + token?: string, ): Promise => { assert(!queuedTransaction.isUserOp); @@ -371,8 +520,34 @@ const _sendTransaction = async ( job.log( `Override gas fee (${overrides.maxFeePerGas}) is lower than onchain fee (${populatedTransaction.maxFeePerGas}). Delaying job until ${retryAt}.`, ); - await job.moveToDelayed(retryAt.getTime()); - return null; + await job.moveToDelayed(retryAt.getTime(), token); + throw new DelayedError("Delaying job due to gas fee override"); + } + } + + // Handle if `gasFeeCeiling` is overridden. + // Delay the job if the estimated cost is higher than the gas fee ceiling. + const gasFeeCeiling = overrides?.gasFeeCeiling; + if (typeof gasFeeCeiling !== "undefined") { + let estimatedCost = 0n; + + if (populatedTransaction.maxFeePerGas) { + estimatedCost = + populatedTransaction.maxFeePerGas * populatedTransaction.gas; + } else if (populatedTransaction.gasPrice) { + estimatedCost = populatedTransaction.gas * populatedTransaction.gasPrice; + } + + // in case neither of the estimations work, the estimatedCost will be 0n, so this check should not pass, and transaction remains unaffected + if (estimatedCost > gasFeeCeiling) { + const retryAt = _minutesFromNow(5); + job.log( + `Override gas fee ceiling (${gasFeeCeiling}) is lower than onchain estimated cost (${estimatedCost}). Delaying job until ${retryAt}. [gas: ${populatedTransaction.gas}, gasPrice: ${populatedTransaction.gasPrice}, maxFeePerGas: ${populatedTransaction.maxFeePerGas}]`, + ); + // token is required to acquire lock for delaying currently processing job: https://docs.bullmq.io/patterns/process-step-jobs#delaying + await job.moveToDelayed(retryAt.getTime(), token); + // throwing delayed error is required to notify bullmq worker not to complete or fail the job + throw new DelayedError("Delaying job due to gas fee override"); } } @@ -384,15 +559,18 @@ const _sendTransaction = async ( }); populatedTransaction.nonce = nonce; job.log( - `Populated transaction (isRecycledNonce=${isRecycledNonce}): ${stringify(populatedTransaction)}`, + `Populated transaction (isRecycledNonce=${isRecycledNonce}): ${stringify( + populatedTransaction, + )}`, ); // Send transaction to RPC. // This call throws if the RPC rejects the transaction. let transactionHash: Hex; try { - const sendTransactionResult = - await account.sendTransaction(populatedTransaction); + const sendTransactionResult = await account.sendTransaction( + populatedTransaction, + ); transactionHash = sendTransactionResult.transactionHash; } catch (error: unknown) { // If the nonce is already seen onchain (nonce too low) or in mempool (replacement underpriced), @@ -643,6 +821,28 @@ export function _updateGasFees( return updated; } +async function getEntrypointFromFactory( + factoryAddress: string, + client: ThirdwebClient, + chain: Chain, +) { + const factoryContract = getContract({ + address: factoryAddress, + client, + chain, + }); + try { + const entrypointAddress = await readContract({ + contract: factoryContract, + method: "function entrypoint() public view returns (address)", + params: [], + }); + return entrypointAddress; + } catch { + return undefined; + } +} + // Must be explicitly called for the worker to run on this host. export const initSendTransactionWorker = () => { const _worker = new Worker(SendTransactionQueue.q.name, handler, { diff --git a/src/worker/tasks/send-webhook-worker.ts b/src/worker/tasks/send-webhook-worker.ts index dda1f1f5b..ef1eaee3b 100644 --- a/src/worker/tasks/send-webhook-worker.ts +++ b/src/worker/tasks/send-webhook-worker.ts @@ -5,6 +5,7 @@ import { TransactionDB } from "../../shared/db/transactions/db"; import { WebhooksEventTypes, type BackendWalletBalanceWebhookParams, + type WalletSubscriptionWebhookParams, } from "../../shared/schemas/webhooks"; import { toEventLogSchema } from "../../server/schemas/event-log"; import { @@ -18,11 +19,36 @@ import { sendWebhookRequest, type WebhookResponse, } from "../../shared/utils/webhook"; -import { SendWebhookQueue, type WebhookJob } from "../queues/send-webhook-queue"; +import { + SendWebhookQueue, + type WebhookJob, +} from "../queues/send-webhook-queue"; +import { env } from "../../shared/utils/env"; const handler: Processor = async (job: Job) => { const { data, webhook } = superjson.parse(job.data); + // Extract transaction ID if available + let transactionId: string | undefined; + if ("queueId" in data) { + transactionId = data.queueId; + } + + // Log webhook attempt with HMAC info + const hmacMode = env.ENABLE_CUSTOM_HMAC_AUTH ? "custom" : "standard"; + logger({ + service: "worker", + level: "info", + message: `[Webhook] Attempting to send webhook for transaction ${transactionId} at destination ${webhook.url}`, + queueId: transactionId, + data: { + eventType: data.type, + destination: webhook.url, + webhookId: webhook.id, + hmacMode, + }, + }); + let resp: WebhookResponse | undefined; switch (data.type) { case WebhooksEventTypes.CONTRACT_SUBSCRIPTION: { @@ -56,6 +82,17 @@ const handler: Processor = async (job: Job) => { const transaction = await TransactionDB.get(data.queueId); if (!transaction) { job.log("Transaction not found."); + logger({ + service: "worker", + level: "warn", + message: `[Webhook] Transaction not found for webhook`, + queueId: data.queueId, + data: { + eventType: data.type, + destination: webhook.url, + webhookId: webhook.id, + }, + }); return; } const webhookBody: Static = @@ -69,6 +106,35 @@ const handler: Processor = async (job: Job) => { resp = await sendWebhookRequest(webhook, webhookBody); break; } + + case WebhooksEventTypes.WALLET_SUBSCRIPTION: { + const webhookBody: WalletSubscriptionWebhookParams = data.body; + resp = await sendWebhookRequest( + webhook, + webhookBody as unknown as Record, + ); + break; + } + } + + // Log the response + if (resp) { + const logLevel = resp.ok ? "info" : resp.status >= 500 ? "error" : "warn"; + logger({ + service: "worker", + level: logLevel, + message: `[Webhook] Webhook response received: ${resp.status} for transaction ${transactionId} at destination ${webhook.url}`, + queueId: transactionId, + data: { + eventType: data.type, + destination: webhook.url, + webhookId: webhook.id, + responseCode: resp.status, + responseOk: resp.ok, + hmacMode, + responseBody: resp.body.substring(0, 200), // Truncate response body to first 200 chars + }, + }); } // Throw on 5xx so it remains in the queue to retry later. @@ -78,9 +144,17 @@ const handler: Processor = async (job: Job) => { ); job.log(error.message); logger({ - level: "debug", - message: error.message, + level: "error", + message: `[Webhook] 5xx error, will retry`, service: "worker", + queueId: transactionId, + data: { + eventType: data.type, + destination: webhook.url, + webhookId: webhook.id, + responseCode: resp.status, + hmacMode, + }, }); throw error; } @@ -89,7 +163,7 @@ const handler: Processor = async (job: Job) => { // Must be explicitly called for the worker to run on this host. export const initSendWebhookWorker = () => { new Worker(SendWebhookQueue.q.name, handler, { - concurrency: 10, + concurrency: env.SEND_WEBHOOK_QUEUE_CONCURRENCY, connection: redis, }); }; diff --git a/src/worker/tasks/wallet-subscription-worker.ts b/src/worker/tasks/wallet-subscription-worker.ts new file mode 100644 index 000000000..9ede42425 --- /dev/null +++ b/src/worker/tasks/wallet-subscription-worker.ts @@ -0,0 +1,155 @@ +import { type Job, type Processor, Worker } from "bullmq"; +import { getAllWalletSubscriptions } from "../../shared/db/wallet-subscriptions/get-all-wallet-subscriptions"; +import { getConfig } from "../../shared/utils/cache/get-config"; +import { logger } from "../../shared/utils/logger"; +import { redis } from "../../shared/utils/redis/redis"; +import { WalletSubscriptionQueue } from "../queues/wallet-subscription-queue"; +import { logWorkerExceptions } from "../queues/queues"; +import { SendWebhookQueue } from "../queues/send-webhook-queue"; +import { WebhooksEventTypes } from "../../shared/schemas/webhooks"; +import { getChain } from "../../shared/utils/chain"; +import { thirdwebClient } from "../../shared/utils/sdk"; +import { getWalletBalance } from "thirdweb/wallets"; +import type { Chain } from "thirdweb/chains"; +import type { WalletCondition } from "../../shared/schemas/wallet-subscription-conditions"; +import type { WalletSubscriptions, Webhooks } from "@prisma/client"; +import { prettifyError } from "../../shared/utils/error"; + +type WalletSubscriptionWithWebhook = WalletSubscriptions & { + conditions: WalletCondition[]; + webhook: Webhooks | null; +}; + +// Split array into chunks of specified size +function chunk(arr: T[], size: number): T[][] { + return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => + arr.slice(i * size, i * size + size), + ); +} + +/** + * Verify if a condition is met for a given wallet + * Returns the current value if condition is met, undefined otherwise + */ +async function verifyCondition({ + condition, + walletAddress, + chain, +}: { + condition: WalletCondition; + walletAddress: string; + chain: Chain; +}): Promise { + switch (condition.type) { + case "token_balance_lt": + case "token_balance_gt": { + const currentBalanceResponse = await getWalletBalance({ + address: walletAddress, + client: thirdwebClient, + tokenAddress: + condition.tokenAddress === "native" + ? undefined + : condition.tokenAddress, + chain, + }); + + const currentBalance = currentBalanceResponse.value; + const threshold = BigInt(condition.value); + + const isConditionMet = + condition.type === "token_balance_lt" + ? currentBalance < threshold + : currentBalance > threshold; + + return isConditionMet ? currentBalance.toString() : null; + } + } +} + +/** + * Process a batch of subscriptions and trigger webhooks for any met conditions + */ +async function processSubscriptions( + subscriptions: WalletSubscriptionWithWebhook[], +) { + await Promise.all( + subscriptions.map(async (subscription) => { + try { + const chain = await getChain(Number.parseInt(subscription.chainId)); + + // Process each condition for the subscription + for (const condition of subscription.conditions) { + const currentValue = await verifyCondition({ + condition, + walletAddress: subscription.walletAddress, + chain, + }); + + if (currentValue && subscription.webhookId && subscription.webhook) { + await SendWebhookQueue.enqueueWebhook({ + type: WebhooksEventTypes.WALLET_SUBSCRIPTION, + webhook: subscription.webhook, + body: { + subscriptionId: subscription.id, + chainId: subscription.chainId, + walletAddress: subscription.walletAddress, + condition, + currentValue, + }, + }); + } + } + } catch (error) { + // Log error but continue processing other subscriptions + const message = prettifyError(error); + logger({ + service: "worker", + level: "error", + message: `Error processing wallet subscription ${subscription.id}: ${message}`, + error: error as Error, + }); + } + }), + ); +} + +// Must be explicitly called for the worker to run on this host. +export const initWalletSubscriptionWorker = async () => { + const config = await getConfig(); + const cronPattern = + config.walletSubscriptionsCronSchedule || "*/30 * * * * *"; // Default to every 30 seconds + + logger({ + service: "worker", + level: "info", + message: `Initializing wallet subscription worker with cron pattern: ${cronPattern}`, + }); + + WalletSubscriptionQueue.q.add("cron", "", { + repeat: { pattern: cronPattern }, + jobId: "wallet-subscription-cron", + }); + + const _worker = new Worker(WalletSubscriptionQueue.q.name, handler, { + connection: redis, + concurrency: 1, + }); + logWorkerExceptions(_worker); +}; + +/** + * Process all wallet subscriptions and notify webhooks when conditions are met. + */ +const handler: Processor = async (_job: Job) => { + // Get all active wallet subscriptions + const subscriptions = await getAllWalletSubscriptions(); + if (subscriptions.length === 0) { + return; + } + + // Process in batches of 50 + const batches = chunk(subscriptions, 50); + for (const batch of batches) { + await processSubscriptions(batch); + } +}; diff --git a/tests/e2e/package.json b/tests/e2e/package.json index d9ecbc3bc..f532508a6 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -11,6 +11,6 @@ "type": "module", "dependencies": { "prool": "^0.0.16", - "thirdweb": "^5.61.5" + "thirdweb": "5.90.0" } } diff --git a/tests/e2e/tests/routes/read.test.ts b/tests/e2e/tests/routes/read.test.ts new file mode 100644 index 000000000..a0e52317e --- /dev/null +++ b/tests/e2e/tests/routes/read.test.ts @@ -0,0 +1,78 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import assert from "node:assert"; +import { ZERO_ADDRESS } from "thirdweb"; +import type { Address } from "thirdweb/utils"; +import { CONFIG } from "../../config"; +import type { setupEngine } from "../../utils/engine"; +import { pollTransactionStatus } from "../../utils/transactions"; +import { setup } from "../setup"; + +describe("readContractRoute", () => { + let engine: ReturnType; + let backendWallet: Address; + let tokenContractAddress: string; + + beforeAll(async () => { + const { engine: _engine, backendWallet: _backendWallet } = await setup(); + engine = _engine; + backendWallet = _backendWallet as Address; + + const res = await engine.deploy.deployToken( + CONFIG.CHAIN.id.toString(), + backendWallet, + { + contractMetadata: { + name: "test token", + platform_fee_basis_points: 0, + platform_fee_recipient: ZERO_ADDRESS, + symbol: "TT", + trusted_forwarders: [], + }, + }, + ); + + expect(res.result.queueId).toBeDefined(); + assert(res.result.queueId, "queueId must be defined"); + expect(res.result.deployedAddress).toBeDefined(); + + const transactionStatus = await pollTransactionStatus( + engine, + res.result.queueId, + true, + ); + + expect(transactionStatus.minedAt).toBeDefined(); + assert(res.result.deployedAddress, "deployedAddress must be defined"); + tokenContractAddress = res.result.deployedAddress; + }); + + test("readContract with function name", async () => { + const res = await engine.contract.read( + "name", + CONFIG.CHAIN.id.toString(), + tokenContractAddress, + ); + + expect(res.result).toEqual("test token"); + }); + + test("readContract with function signature", async () => { + const res = await engine.contract.read( + "function symbol() public view returns (string memory)", + CONFIG.CHAIN.id.toString(), + tokenContractAddress, + ); + + expect(res.result).toEqual("TT"); + }); + + test("readContract with function signature", async () => { + const res = await engine.contract.read( + "function totalSupply() public view returns (uint256)", + CONFIG.CHAIN.id.toString(), + tokenContractAddress, + ); + + expect(res.result).toEqual("0"); + }); +}); diff --git a/tests/e2e/tests/routes/sign-typed-data.test.ts b/tests/e2e/tests/routes/sign-typed-data.test.ts new file mode 100644 index 000000000..f6b40179f --- /dev/null +++ b/tests/e2e/tests/routes/sign-typed-data.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from "bun:test"; +import { signTypedData } from "thirdweb/utils"; +import { ANVIL_PKEY_A } from "../../utils/wallets"; +import { setup } from "../setup"; + +describe("signTypedDataRoute", () => { + const data = { + domain: { + name: "Ether Mail", + version: "1", + chainId: 1, + verifyingContract: "0x0000000000000000000000000000000000000000", + }, + message: { + contents: "Hello, Bob!", + from: { + name: "Alice", + }, + }, + primaryType: "Mail", + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + Mail: [ + { name: "contents", type: "string" }, + { name: "from", type: "Person" }, + ], + Person: [{ name: "name", type: "string" }], + }, + } as const; + + test("Sign typed data", async () => { + const { engine, backendWallet } = await setup(); + + const res = await engine.backendWallet.signTypedData(backendWallet, { + domain: data.domain, + value: data.message, + types: data.types, + primaryType: data.primaryType, + }); + + const expected = signTypedData({ + // @ts-expect-error - bigint serialization + domain: data.domain, + message: data.message, + types: data.types, + primaryType: data.primaryType, + privateKey: ANVIL_PKEY_A, + }); + + expect(res.result).toEqual(expected); + }); + + test("Sign typed data without primary type", async () => { + const { engine, backendWallet } = await setup(); + + const res = await engine.backendWallet.signTypedData(backendWallet, { + domain: data.domain, + value: data.message, + types: data.types, + }); + + const expected = signTypedData({ + // @ts-expect-error - bigint serialization + domain: data.domain, + message: data.message, + types: data.types, + primaryType: data.primaryType, + privateKey: ANVIL_PKEY_A, + }); + + expect(res.result).toEqual(expected); + }); +}); diff --git a/tests/e2e/tests/workers/wallet-subscription-worker.test.ts b/tests/e2e/tests/workers/wallet-subscription-worker.test.ts new file mode 100644 index 000000000..5bdaec1fd --- /dev/null +++ b/tests/e2e/tests/workers/wallet-subscription-worker.test.ts @@ -0,0 +1,222 @@ +import { + beforeAll, + afterAll, + describe, + expect, + test, + beforeEach, + afterEach, +} from "vitest"; +import Fastify, { type FastifyInstance } from "fastify"; +import { setup } from "../setup"; +import type { WalletSubscriptionWebhookParams } from "../../../../src/shared/schemas/webhooks"; +import type { Engine } from "../../../../sdk/dist/thirdweb-dev-engine.cjs"; +import type { WalletCondition } from "../../../../src/shared/schemas/wallet-subscription-conditions"; +import { sleep } from "bun"; + +describe("Wallet Subscription Worker", () => { + let testCallbackServer: FastifyInstance; + let engine: Engine; + let webhookPayloads: WalletSubscriptionWebhookParams[] = []; + let webhookId: number; + + beforeAll(async () => { + engine = (await setup()).engine; + testCallbackServer = await createTempCallbackServer(); + + // Create a webhook that we'll reuse for all tests + const webhook = await engine.webhooks.create({ + url: "http://localhost:3006/callback", + eventType: "wallet_subscription", + }); + webhookId = webhook.result.id; + }); + + afterAll(async () => { + await testCallbackServer.close(); + }); + + beforeEach(() => { + // Clear webhook payloads before each test + webhookPayloads = []; + }); + + afterEach(async () => { + await sleep(5000); // wait for any unsent webhooks to be sent + }); + + const createTempCallbackServer = async () => { + const tempServer = Fastify(); + + tempServer.post("/callback", async (request) => { + const payload = request.body as WalletSubscriptionWebhookParams; + webhookPayloads.push(payload); + return { success: true }; + }); + + await tempServer.listen({ port: 3006 }); + return tempServer; + }; + + const waitForWebhookPayloads = async ( + timeoutMs = 5000, + ): Promise => { + // Wait for initial webhooks to come in + await new Promise((resolve) => setTimeout(resolve, timeoutMs)); + return webhookPayloads; + }; + + const createSubscription = async (conditions: WalletCondition[]) => { + const subscription = await engine.walletSubscriptions.addWalletSubscription( + { + chain: "137", + walletAddress: "0xE52772e599b3fa747Af9595266b527A31611cebd", + conditions, + webhookId, + }, + ); + + return subscription.result; + }; + + test("should create and validate wallet subscription", async () => { + const condition: WalletCondition = { + type: "token_balance_lt", + value: "100000000000000000", // 0.1 ETH + tokenAddress: "native", + }; + + const subscription = await createSubscription([condition]); + + expect(subscription.chainId).toBe("137"); + expect(subscription.walletAddress.toLowerCase()).toBe( + "0xE52772e599b3fa747Af9595266b527A31611cebd".toLowerCase(), + ); + expect(subscription.conditions).toEqual([condition]); + expect(subscription.webhook?.url).toBe("http://localhost:3006/callback"); + + // Cleanup + await engine.walletSubscriptions.deleteWalletSubscription(subscription.id); + }); + + test("should fire webhooks for token balance less than threshold", async () => { + const condition: WalletCondition = { + type: "token_balance_lt", + value: "1000000000000000000000", // 1000 ETH (high threshold to ensure trigger) + tokenAddress: "native", + }; + + const subscription = await createSubscription([condition]); + + try { + const payloads = await waitForWebhookPayloads(); + + // Verify we got webhooks + expect(payloads.length).toBeGreaterThan(0); + + // Verify webhook data is correct + for (const payload of payloads) { + expect(payload.subscriptionId).toBe(subscription.id); + expect(payload.chainId).toBe("137"); + expect(payload.walletAddress.toLowerCase()).toBe( + "0xE52772e599b3fa747Af9595266b527A31611cebd".toLowerCase(), + ); + expect(payload.condition).toEqual(condition); + expect(BigInt(payload.currentValue)).toBeLessThan( + BigInt(condition.value), + ); + } + } finally { + await engine.walletSubscriptions.deleteWalletSubscription( + subscription.id, + ); + } + }); + + test("should fire webhooks for token balance greater than threshold", async () => { + const condition: WalletCondition = { + type: "token_balance_gt", + value: "1000000000000", // Very small threshold to ensure trigger + tokenAddress: "native", + }; + + const subscription = await createSubscription([condition]); + + try { + const payloads = await waitForWebhookPayloads(); + + // Verify we got webhooks + expect(payloads.length).toBeGreaterThan(0); + + // Verify webhook data is correct + for (const payload of payloads) { + expect(payload.subscriptionId).toBe(subscription.id); + expect(payload.chainId).toBe("137"); + expect(payload.walletAddress.toLowerCase()).toBe( + "0xE52772e599b3fa747Af9595266b527A31611cebd".toLowerCase(), + ); + expect(payload.condition).toEqual(condition); + expect(BigInt(payload.currentValue)).toBeGreaterThan( + BigInt(condition.value), + ); + } + } finally { + await engine.walletSubscriptions.deleteWalletSubscription( + subscription.id, + ); + } + }); + + test("should fire webhooks for multiple conditions", async () => { + const conditions: WalletCondition[] = [ + { + type: "token_balance_gt", + value: "1000000000000", // Very small threshold to ensure trigger + tokenAddress: "native", + }, + { + type: "token_balance_lt", + value: "1000000000000000000000", // 1000 ETH (high threshold to ensure trigger) + tokenAddress: "native", + }, + ]; + + const subscription = await createSubscription(conditions); + + try { + const payloads = await waitForWebhookPayloads(); + + // Verify we got webhooks for both conditions + expect(payloads.length).toBeGreaterThan(1); + + // Verify we got webhooks for both conditions + const uniqueConditions = new Set(payloads.map((p) => p.condition.type)); + expect(uniqueConditions.size).toBe(2); + + // Verify each webhook has correct data + for (const payload of payloads) { + expect(payload.subscriptionId).toBe(subscription.id); + expect(payload.chainId).toBe("137"); + expect(payload.walletAddress.toLowerCase()).toBe( + "0xE52772e599b3fa747Af9595266b527A31611cebd".toLowerCase(), + ); + expect(payload.currentValue).toBeDefined(); + + // Verify the value satisfies the condition + if (payload.condition.type === "token_balance_gt") { + expect(BigInt(payload.currentValue)).toBeGreaterThan( + BigInt(payload.condition.value), + ); + } else { + expect(BigInt(payload.currentValue)).toBeLessThan( + BigInt(payload.condition.value), + ); + } + } + } finally { + await engine.walletSubscriptions.deleteWalletSubscription( + subscription.id, + ); + } + }); +}); diff --git a/tests/unit/webhook.test.ts b/tests/unit/webhook.test.ts new file mode 100644 index 000000000..adc0d5dc0 --- /dev/null +++ b/tests/unit/webhook.test.ts @@ -0,0 +1,57 @@ +import type { Webhooks } from "@prisma/client"; +import { describe, expect, it } from "vitest"; +import { WebhooksEventTypes } from "../../src/shared/schemas/webhooks"; +import { generateRequestHeaders } from "../../src/shared/utils/webhook"; +import { generateSecretHmac256 } from "../../src/shared/utils/customAuthHeader"; + +describe("generateSecretHmac256", () => { + it("should generate a valid MAC header with correct structure and values", () => { + const timestamp = new Date("2024-01-01"); + const nonce = "6b98df0d-5f33-4121-96cb-77a0b9df2bbe"; + + const result = generateSecretHmac256({ + webhookUrl: "https://example.com/webhook", + body: { bodyArgName: "bodyArgValue" }, + timestamp, + nonce, + clientId: "testClientId", + clientSecret: "testClientSecret", + }); + + expect(result).toEqual( + `MAC id="testClientId" ts="1704067200000" nonce="6b98df0d-5f33-4121-96cb-77a0b9df2bbe" bodyhash="4Mknknli8NGCwC28djVf/Qa8vN3wtvfeRGKVha0MgjQ=" mac="Qbe9H5yeVvywoL3l1RFLBDC0YvDOCQnytNSlbTWXzEk="`, + ); + }); +}); + +describe("generateRequestHeaders", () => { + const webhook: Webhooks = { + id: 42, + name: "test webhook", + url: "https://www.example.com/webhook", + secret: "test-secret-string", + eventType: WebhooksEventTypes.SENT_TX, + createdAt: new Date(), + updatedAt: new Date(), + revokedAt: null, + }; + const body = { + name: "Alice", + age: 25, + occupation: ["Founder", "Developer"], + }; + const timestamp = new Date("2024-01-01"); + + it("Generate a consistent webhook header", () => { + const result = generateRequestHeaders({ webhook, body, timestamp }); + + expect(result).toEqual({ + Accept: "application/json", + Authorization: "Bearer test-secret-string", + "Content-Type": "application/json", + "x-engine-signature": + "ca272da65f1145b9cfadab6d55086ee458eccc03a2c5f7f5ea84094d95b219cc", + "x-engine-timestamp": "1704067200", + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 79a69d912..904cb697f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,11 +7,6 @@ resolved "https://registry.yarnpkg.com/@account-abstraction/contracts/-/contracts-0.5.0.tgz#a089aee7b4c446251fbbce7df315bbf8f659e37f" integrity sha512-CKyS9Zh5rcYUM+4B6TlaB9+THHzJ+6TY3tWF5QofqvFpqGNvIhF8ddy6wyCmqZw6TB74/yYv7cYD/RarVudfDg== -"@adraffy/ens-normalize@1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" - integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== - "@adraffy/ens-normalize@^1.10.1": version "1.11.0" resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" @@ -658,6 +653,13 @@ dependencies: "@bull-board/api" "5.23.0" +"@circle-fin/developer-controlled-wallets@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@circle-fin/developer-controlled-wallets/-/developer-controlled-wallets-7.0.0.tgz#520bbe54e050dbf9585b54bc61d372887d0dc149" + integrity sha512-GbouORrWpec27DIOVuWfdyP25inrGQUNj2Vwgp7pJm15Z09E9OQBQjB334rGCIM4MT4NVuKKDkbOHTIphoi7zg== + dependencies: + axios "^1.6.2" + "@cloud-cryptographic-wallet/asn1-parser@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@cloud-cryptographic-wallet/asn1-parser/-/asn1-parser-0.0.4.tgz#4494a8f46d2b3974731d6cc2f3f34cb8afeb0c78" @@ -696,10 +698,10 @@ preact "^10.16.0" sha.js "^2.4.11" -"@coinbase/wallet-sdk@4.2.4": - version "4.2.4" - resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-4.2.4.tgz#aff3a95f50f9a26950052b53620828414bd2baab" - integrity sha512-wJ9QOXOhRdGermKAoJSr4JgGqZm/Um0m+ecywzEC9qSOu3TXuVcG3k0XXTXW11UBgjdoPRuf5kAwRX3T9BynFA== +"@coinbase/wallet-sdk@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-4.3.0.tgz#03b8fce92ac2b3b7cf132f64d6008ac081569b4e" + integrity sha512-T3+SNmiCw4HzDm4we9wCHCxlP0pqCiwKe4sOwPH3YAK2KSKjxPRydKu6UQJrdONFVLG7ujXvbd/6ZqmvJb8rkw== dependencies: "@noble/hashes" "^1.4.0" clsx "^1.2.1" @@ -887,10 +889,10 @@ "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" "@emotion/utils" "^1.2.1" -"@emotion/styled@11.14.0": - version "11.14.0" - resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.14.0.tgz#f47ca7219b1a295186d7661583376fcea95f0ff3" - integrity sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA== +"@emotion/styled@11.14.1": + version "11.14.1" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.14.1.tgz#8c34bed2948e83e1980370305614c20955aacd1c" + integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw== dependencies: "@babel/runtime" "^7.18.3" "@emotion/babel-plugin" "^11.13.5" @@ -1994,6 +1996,11 @@ protobufjs "^7.2.5" yargs "^17.7.2" +"@hey-api/client-fetch@0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@hey-api/client-fetch/-/client-fetch-0.10.0.tgz#d11016a3b4cff249f57eb6a323dc001a2c203848" + integrity sha512-C7vzj4t52qPiHCqjn1l8cRTI2p4pZCd7ViLtJDTHr5ZwI4sWOYC1tmv6bd529qqY6HFFbhGCz4TAZSwKAMJncg== + "@ioredis/commands@^1.1.1": version "1.2.0" resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" @@ -2323,6 +2330,11 @@ "@motionone/dom" "^10.16.4" tslib "^2.3.1" +"@msgpack/msgpack@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.1.2.tgz#fdd25cc2202297519798bbaf4689152ad9609e19" + integrity sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ== + "@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz#9edec61b22c3082018a79f6d1c30289ddf3d9d11" @@ -2358,12 +2370,15 @@ resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121" integrity sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw== -"@noble/curves@1.2.0", "@noble/curves@~1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" - integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== - dependencies: - "@noble/hashes" "1.3.2" +"@noble/ciphers@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.2.1.tgz#3812b72c057a28b44ff0ad4aff5ca846e5b9cdc9" + integrity sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA== + +"@noble/ciphers@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.3.0.tgz#f64b8ff886c240e644e5573c097f86e5b43676dc" + integrity sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw== "@noble/curves@1.4.0": version "1.4.0" @@ -2372,57 +2387,83 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/curves@1.4.2": +"@noble/curves@1.4.2", "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== dependencies: "@noble/hashes" "1.4.0" -"@noble/curves@1.7.0", "@noble/curves@^1.4.0", "@noble/curves@^1.6.0", "@noble/curves@~1.7.0": +"@noble/curves@1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.0.tgz#fe035a23959e6aeadf695851b51a87465b5ba8f7" + integrity sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ== + dependencies: + "@noble/hashes" "1.7.0" + +"@noble/curves@1.8.1", "@noble/curves@~1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.1.tgz#19bc3970e205c99e4bdb1c64a4785706bce497ff" + integrity sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ== + dependencies: + "@noble/hashes" "1.7.1" + +"@noble/curves@1.8.2": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.2.tgz#8f24c037795e22b90ae29e222a856294c1d9ffc7" + integrity sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g== + dependencies: + "@noble/hashes" "1.7.2" + +"@noble/curves@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.2.tgz#73388356ce733922396214a933ff7c95afcef911" + integrity sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g== + dependencies: + "@noble/hashes" "1.8.0" + +"@noble/curves@^1.6.0", "@noble/curves@~1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== dependencies: "@noble/hashes" "1.6.0" -"@noble/curves@~1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" - integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== - dependencies: - "@noble/hashes" "1.5.0" - -"@noble/hashes@1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" - integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== - "@noble/hashes@1.4.0", "@noble/hashes@~1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== -"@noble/hashes@1.5.0", "@noble/hashes@~1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" - integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== - "@noble/hashes@1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5" integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== -"@noble/hashes@1.6.1", "@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0", "@noble/hashes@^1.5.0", "@noble/hashes@~1.6.0": +"@noble/hashes@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.0.tgz#5d9e33af2c7d04fee35de1519b80c958b2e35e39" + integrity sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w== + +"@noble/hashes@1.7.1", "@noble/hashes@~1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== + +"@noble/hashes@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.2.tgz#d53c65a21658fb02f3303e7ee3ba89d6754c64b4" + integrity sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ== + +"@noble/hashes@1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + +"@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0", "@noble/hashes@^1.5.0", "@noble/hashes@~1.6.0": version "1.6.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5" integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== -"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" - integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== - "@node-lightning/checksum@^0.27.0": version "0.27.4" resolved "https://registry.yarnpkg.com/@node-lightning/checksum/-/checksum-0.27.4.tgz#493004e76aa76cdbab46f01bf445e4010c30a179" @@ -2695,10 +2736,10 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/primitive@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3" - integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA== +"@radix-ui/primitive@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.2.tgz#83f415c4425f21e3d27914c12b3272a32e3dae65" + integrity sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA== "@radix-ui/react-arrow@1.0.3": version "1.0.3" @@ -2708,12 +2749,12 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" -"@radix-ui/react-arrow@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz#2103721933a8bfc6e53bbfbdc1aaad5fc8ba0dd7" - integrity sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w== +"@radix-ui/react-arrow@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz#e14a2657c81d961598c5e72b73dd6098acc04f09" + integrity sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w== dependencies: - "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-primitive" "2.1.3" "@radix-ui/react-compose-refs@1.0.1": version "1.0.1" @@ -2722,10 +2763,10 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-compose-refs@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec" - integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw== +"@radix-ui/react-compose-refs@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" + integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== "@radix-ui/react-context@1.0.1": version "1.0.1" @@ -2734,10 +2775,10 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-context@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a" - integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q== +"@radix-ui/react-context@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" + integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== "@radix-ui/react-dialog@1.0.5": version "1.0.5" @@ -2760,25 +2801,25 @@ aria-hidden "^1.1.1" react-remove-scroll "2.5.5" -"@radix-ui/react-dialog@1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz#d68e977acfcc0d044b9dab47b6dd2c179d2b3191" - integrity sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA== - dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-dismissable-layer" "1.1.3" - "@radix-ui/react-focus-guards" "1.1.1" - "@radix-ui/react-focus-scope" "1.1.1" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-portal" "1.1.3" - "@radix-ui/react-presence" "1.1.2" - "@radix-ui/react-primitive" "2.0.1" - "@radix-ui/react-slot" "1.1.1" - "@radix-ui/react-use-controllable-state" "1.1.0" - aria-hidden "^1.1.1" - react-remove-scroll "^2.6.1" +"@radix-ui/react-dialog@1.1.14": + version "1.1.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz#4c69c80c258bc6561398cfce055202ea11075107" + integrity sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.10" + "@radix-ui/react-focus-guards" "1.1.2" + "@radix-ui/react-focus-scope" "1.1.7" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-use-controllable-state" "1.2.2" + aria-hidden "^1.2.4" + react-remove-scroll "^2.6.3" "@radix-ui/react-dismissable-layer@1.0.5": version "1.0.5" @@ -2792,27 +2833,16 @@ "@radix-ui/react-use-callback-ref" "1.0.1" "@radix-ui/react-use-escape-keydown" "1.0.3" -"@radix-ui/react-dismissable-layer@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.2.tgz#771594b202f32bc8ffeb278c565f10c513814aee" - integrity sha512-kEHnlhv7wUggvhuJPkyw4qspXLJOdYoAP4dO2c8ngGuXTq1w/HZp1YeVB+NQ2KbH1iEG+pvOCGYSqh9HZOz6hg== +"@radix-ui/react-dismissable-layer@1.1.10": + version "1.1.10" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz#429b9bada3672c6895a5d6a642aca6ecaf4f18c3" + integrity sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ== dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-primitive" "2.0.1" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-escape-keydown" "1.1.0" - -"@radix-ui/react-dismissable-layer@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz#4ee0f0f82d53bf5bd9db21665799bb0d1bad5ed8" - integrity sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg== - dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-primitive" "2.0.1" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-escape-keydown" "1.1.0" + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-escape-keydown" "1.1.1" "@radix-ui/react-focus-guards@1.0.1": version "1.0.1" @@ -2821,10 +2851,10 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-focus-guards@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe" - integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg== +"@radix-ui/react-focus-guards@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz#4ec9a7e50925f7fb661394460045b46212a33bed" + integrity sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA== "@radix-ui/react-focus-scope@1.0.4": version "1.0.4" @@ -2836,14 +2866,14 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-callback-ref" "1.0.1" -"@radix-ui/react-focus-scope@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz#5c602115d1db1c4fcfa0fae4c3b09bb8919853cb" - integrity sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA== +"@radix-ui/react-focus-scope@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz#dfe76fc103537d80bf42723a183773fd07bfb58d" + integrity sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw== dependencies: - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-primitive" "2.0.1" - "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" "@radix-ui/react-icons@1.3.0": version "1.3.0" @@ -2863,12 +2893,12 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" -"@radix-ui/react-id@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed" - integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA== +"@radix-ui/react-id@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7" + integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== dependencies: - "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.1" "@radix-ui/react-popper@1.1.3": version "1.1.3" @@ -2887,21 +2917,21 @@ "@radix-ui/react-use-size" "1.0.1" "@radix-ui/rect" "1.0.1" -"@radix-ui/react-popper@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.1.tgz#2fc66cfc34f95f00d858924e3bee54beae2dff0a" - integrity sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw== +"@radix-ui/react-popper@1.2.7": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.7.tgz#531cf2eebb3d3270d58f7d8136e4517646429978" + integrity sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ== dependencies: "@floating-ui/react-dom" "^2.0.0" - "@radix-ui/react-arrow" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-primitive" "2.0.1" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-layout-effect" "1.1.0" - "@radix-ui/react-use-rect" "1.1.0" - "@radix-ui/react-use-size" "1.1.0" - "@radix-ui/rect" "1.1.0" + "@radix-ui/react-arrow" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-rect" "1.1.1" + "@radix-ui/react-use-size" "1.1.1" + "@radix-ui/rect" "1.1.1" "@radix-ui/react-portal@1.0.4": version "1.0.4" @@ -2911,13 +2941,13 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" -"@radix-ui/react-portal@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440" - integrity sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw== +"@radix-ui/react-portal@1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472" + integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ== dependencies: - "@radix-ui/react-primitive" "2.0.1" - "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-layout-effect" "1.1.1" "@radix-ui/react-presence@1.0.1": version "1.0.1" @@ -2928,13 +2958,13 @@ "@radix-ui/react-compose-refs" "1.0.1" "@radix-ui/react-use-layout-effect" "1.0.1" -"@radix-ui/react-presence@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc" - integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg== +"@radix-ui/react-presence@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.4.tgz#253ac0ad4946c5b4a9c66878335f5cf07c967ced" + integrity sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA== dependencies: - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-use-layout-effect" "1.1.1" "@radix-ui/react-primitive@1.0.3": version "1.0.3" @@ -2944,12 +2974,12 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "1.0.2" -"@radix-ui/react-primitive@2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e" - integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg== +"@radix-ui/react-primitive@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc" + integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ== dependencies: - "@radix-ui/react-slot" "1.1.1" + "@radix-ui/react-slot" "1.2.3" "@radix-ui/react-slot@1.0.2": version "1.0.2" @@ -2959,12 +2989,12 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" -"@radix-ui/react-slot@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3" - integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g== +"@radix-ui/react-slot@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1" + integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== dependencies: - "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-compose-refs" "1.1.2" "@radix-ui/react-tooltip@1.0.7": version "1.0.7" @@ -2985,23 +3015,23 @@ "@radix-ui/react-use-controllable-state" "1.0.1" "@radix-ui/react-visually-hidden" "1.0.3" -"@radix-ui/react-tooltip@1.1.5": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.5.tgz#402f4f7019159bf4a40be3f1fa01978339ea33cc" - integrity sha512-IucoQPcK5nwUuztaxBQvudvYwH58wtRcJlv1qvaMSyIbL9dEBfFN0vRf/D8xDbu6HmAJLlNGty4z8Na+vIqe9Q== - dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-dismissable-layer" "1.1.2" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-popper" "1.2.1" - "@radix-ui/react-portal" "1.1.3" - "@radix-ui/react-presence" "1.1.2" - "@radix-ui/react-primitive" "2.0.1" - "@radix-ui/react-slot" "1.1.1" - "@radix-ui/react-use-controllable-state" "1.1.0" - "@radix-ui/react-visually-hidden" "1.1.1" +"@radix-ui/react-tooltip@1.2.7": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz#23612ac7a5e8e1f6829e46d0e0ad94afe3976c72" + integrity sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.10" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-popper" "1.2.7" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-visually-hidden" "1.2.3" "@radix-ui/react-use-callback-ref@1.0.1": version "1.0.1" @@ -3010,10 +3040,10 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-use-callback-ref@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" - integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== +"@radix-ui/react-use-callback-ref@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40" + integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg== "@radix-ui/react-use-controllable-state@1.0.1": version "1.0.1" @@ -3023,12 +3053,20 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-callback-ref" "1.0.1" -"@radix-ui/react-use-controllable-state@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0" - integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw== +"@radix-ui/react-use-controllable-state@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190" + integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg== dependencies: - "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-effect-event" "0.0.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-use-effect-event@0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907" + integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" "@radix-ui/react-use-escape-keydown@1.0.3": version "1.0.3" @@ -3038,12 +3076,12 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-callback-ref" "1.0.1" -"@radix-ui/react-use-escape-keydown@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754" - integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw== +"@radix-ui/react-use-escape-keydown@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29" + integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g== dependencies: - "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-callback-ref" "1.1.1" "@radix-ui/react-use-layout-effect@1.0.1": version "1.0.1" @@ -3052,10 +3090,10 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/react-use-layout-effect@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" - integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== +"@radix-ui/react-use-layout-effect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" + integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== "@radix-ui/react-use-rect@1.0.1": version "1.0.1" @@ -3065,12 +3103,12 @@ "@babel/runtime" "^7.13.10" "@radix-ui/rect" "1.0.1" -"@radix-ui/react-use-rect@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88" - integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ== +"@radix-ui/react-use-rect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz#01443ca8ed071d33023c1113e5173b5ed8769152" + integrity sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w== dependencies: - "@radix-ui/rect" "1.1.0" + "@radix-ui/rect" "1.1.1" "@radix-ui/react-use-size@1.0.1": version "1.0.1" @@ -3080,12 +3118,12 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" -"@radix-ui/react-use-size@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b" - integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw== +"@radix-ui/react-use-size@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz#6de276ffbc389a537ffe4316f5b0f24129405b37" + integrity sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ== dependencies: - "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.1" "@radix-ui/react-visually-hidden@1.0.3": version "1.0.3" @@ -3095,12 +3133,12 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" -"@radix-ui/react-visually-hidden@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz#f7b48c1af50dfdc366e92726aee6d591996c5752" - integrity sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg== +"@radix-ui/react-visually-hidden@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz#a8c38c8607735dc9f05c32f87ab0f9c2b109efbf" + integrity sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug== dependencies: - "@radix-ui/react-primitive" "2.0.1" + "@radix-ui/react-primitive" "2.1.3" "@radix-ui/rect@1.0.1": version "1.0.1" @@ -3109,10 +3147,10 @@ dependencies: "@babel/runtime" "^7.13.10" -"@radix-ui/rect@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" - integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg== +"@radix-ui/rect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" + integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== "@rollup/rollup-android-arm-eabi@4.28.1": version "4.28.1" @@ -3269,24 +3307,25 @@ "@safe-global/safe-core-sdk-utils" "^1.7.4" ethers "5.7.2" +"@scure/base@1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.6.tgz#ca917184b8231394dd8847509c67a0be522e59f6" + integrity sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== + "@scure/base@^1.1.3", "@scure/base@~1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== -"@scure/base@~1.1.0", "@scure/base@~1.1.2", "@scure/base@~1.1.6", "@scure/base@~1.1.7": +"@scure/base@~1.1.6": version "1.1.9" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== -"@scure/bip32@1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8" - integrity sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA== - dependencies: - "@noble/curves" "~1.2.0" - "@noble/hashes" "~1.3.2" - "@scure/base" "~1.1.2" +"@scure/base@~1.2.2", "@scure/base@~1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.4.tgz#002eb571a35d69bdb4c214d0995dff76a8dcd2a9" + integrity sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ== "@scure/bip32@1.4.0": version "1.4.0" @@ -3297,7 +3336,16 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" -"@scure/bip32@1.6.0", "@scure/bip32@^1.5.0": +"@scure/bip32@1.6.2": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.6.2.tgz#093caa94961619927659ed0e711a6e4bf35bffd0" + integrity sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw== + dependencies: + "@noble/curves" "~1.8.1" + "@noble/hashes" "~1.7.1" + "@scure/base" "~1.2.2" + +"@scure/bip32@^1.5.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.6.0.tgz#6dbc6b4af7c9101b351f41231a879d8da47e0891" integrity sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA== @@ -3306,14 +3354,6 @@ "@noble/hashes" "~1.6.0" "@scure/base" "~1.2.1" -"@scure/bip39@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" - integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== - dependencies: - "@noble/hashes" "~1.3.0" - "@scure/base" "~1.1.0" - "@scure/bip39@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" @@ -3322,7 +3362,15 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" -"@scure/bip39@1.5.0", "@scure/bip39@^1.4.0": +"@scure/bip39@1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.5.4.tgz#07fd920423aa671be4540d59bdd344cc1461db51" + integrity sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA== + dependencies: + "@noble/hashes" "~1.7.1" + "@scure/base" "~1.2.4" + +"@scure/bip39@^1.4.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.5.0.tgz#c8f9533dbd787641b047984356531d84485f19be" integrity sha512-Dop+ASYhnrwm9+HA/HwXg7j2ZqM6yk2fyLWb5znexjctFY3+E+eU8cIWI0Pql0Qx4hPZCijlGq4OL71g+Uz30A== @@ -3872,6 +3920,24 @@ "@stablelib/random" "^1.0.2" "@stablelib/wipe" "^1.0.1" +"@storybook/global@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed" + integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ== + +"@storybook/react-dom-shim@9.0.15": + version "9.0.15" + resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-9.0.15.tgz#5c98ad2a3ead9c4ac1136d75d4e1e08e25fc6443" + integrity sha512-X5VlYKoZSIMU9HEshIwtNzp41nPt4kiJtJ2c5HzFa5F6M8rEHM5n059CGcCZQqff3FnZtK/y6v/kCVZO+8oETA== + +"@storybook/react@9.0.15": + version "9.0.15" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-9.0.15.tgz#e59913879baf6043dd2828cf1818d33f8839588c" + integrity sha512-hewpSH8Ij4Bg7S9Tfw7ecfGPv7YDycRxsfpsDX7Mw3JhLuCdqjpmmTL2RgoNojg7TAW3FPdixcgQi/b4PH50ag== + dependencies: + "@storybook/global" "^5.0.0" + "@storybook/react-dom-shim" "9.0.15" + "@t3-oss/env-core@^0.6.0": version "0.6.1" resolved "https://registry.yarnpkg.com/@t3-oss/env-core/-/env-core-0.6.1.tgz#24d04c22f3e73732e69ac8ff8b13ed00732d20aa" @@ -3882,10 +3948,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.29.0.tgz#d0b3d12c07d5a47f42ab0c1ed4f317106f3d4b20" integrity sha512-WgPTRs58hm9CMzEr5jpISe8HXa3qKQ8CxewdYZeVnA54JrPY9B1CZiwsCoLpLkf0dGRZq+LcX5OiJb0bEsOFww== -"@tanstack/query-core@5.62.16": - version "5.62.16" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.16.tgz#f7efc92b1562a054748bc00c7f8d9d833407503b" - integrity sha512-9Sgft7Qavcd+sN0V25xVyo0nfmcZXBuODy3FVG7BMWTg1HMLm8wwG5tNlLlmSic1u7l1v786oavn+STiFaPH2g== +"@tanstack/query-core@5.81.5": + version "5.81.5" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.81.5.tgz#14e0cc778bad8bc11d1cf130709910d8a353bb73" + integrity sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q== "@tanstack/react-query@5.29.2": version "5.29.2" @@ -3894,12 +3960,12 @@ dependencies: "@tanstack/query-core" "5.29.0" -"@tanstack/react-query@5.62.16": - version "5.62.16" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.62.16.tgz#c267d52650a9e0b61017b04faa43c2e0d2e1de5d" - integrity sha512-XJIZNj65d2IdvU8VBESmrPakfIm6FSdHDzrS1dPrAwmq3ZX+9riMh/ZfbNQHAWnhrgmq7KoXpgZSRyXnqMYT9A== +"@tanstack/react-query@5.81.5": + version "5.81.5" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.81.5.tgz#660dba8bb35f24c4cf3617b299a1e3990a3bb49e" + integrity sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw== dependencies: - "@tanstack/query-core" "5.62.16" + "@tanstack/query-core" "5.81.5" "@thirdweb-dev/auth@^4.1.87": version "4.1.97" @@ -3949,11 +4015,23 @@ resolved "https://registry.yarnpkg.com/@thirdweb-dev/dynamic-contracts/-/dynamic-contracts-1.2.5.tgz#f9735c0d46198e7bf2f98c277f0a9a79c54da1e8" integrity sha512-YVsz+jUWbwj+6aF2eTZGMfyw47a1HRmgNl4LQ3gW9gwYL5y5+OX/yOzv6aV5ibvoqCk/k10aIVK2eFrcpMubQA== +"@thirdweb-dev/engine@3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@thirdweb-dev/engine/-/engine-3.2.1.tgz#22bdb941defa58df8494cffbb2cca7dce4d74183" + integrity sha512-JIIRmBjTSaLjIBk9h3QevCb3V5xXJlCEgjuAqbtviiCJKky+/9zr6IZBbZtO2w9Rq3F7PH+5pDFOKlwRpGU/kw== + dependencies: + "@hey-api/client-fetch" "0.10.0" + "@thirdweb-dev/generated-abis@0.0.2": version "0.0.2" resolved "https://registry.yarnpkg.com/@thirdweb-dev/generated-abis/-/generated-abis-0.0.2.tgz#d0e4f51011ba2ce2bbc266c8a295b04ffd523bab" integrity sha512-FztTzU0KF5u8usNBN5/5s4Ys082p+HwsMI9DfFqOBILm4OwEueLY4B5DbXjF1KlTIuqjGeMGmFDG98MXHUt73A== +"@thirdweb-dev/insight@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@thirdweb-dev/insight/-/insight-1.1.1.tgz#e17673be7a4f8d3b0fe81e8505e2158247505273" + integrity sha512-24oRscLTW9Mod+XpyLlusLxZIZjqQv0XFNSV4lR5u9eoRPaA/BG2HtlVPr0DUtguzTbEyBz98++s5UWleqchVg== + "@thirdweb-dev/merkletree@0.2.6": version "0.2.6" resolved "https://registry.yarnpkg.com/@thirdweb-dev/merkletree/-/merkletree-0.2.6.tgz#874f6d6d98988150785d51c3ce616a06ae2f7563" @@ -4135,6 +4213,11 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/luxon@~3.6.0": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.6.2.tgz#be6536931801f437eafcb9c0f6d6781f72308041" + integrity sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw== + "@types/markdown-it@^14.1.1": version "14.1.2" resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" @@ -4158,11 +4241,6 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -"@types/node-cron@^3.0.8": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.11.tgz#70b7131f65038ae63cfe841354c8aba363632344" - integrity sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg== - "@types/node@*", "@types/node@>=13.7.0": version "22.10.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" @@ -4420,10 +4498,10 @@ lodash.isequal "4.5.0" uint8arrays "3.1.0" -"@walletconnect/core@2.17.3": - version "2.17.3" - resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.17.3.tgz#e59045a666951e9fc2e8420130c4f93221bd2492" - integrity sha512-57uv0FW4L6H/tmkb1kS2nG41MDguyDgZbGR58nkDUd1TO/HydyiTByVOhFzIxgN331cnY/1G1rMaKqncgdnOFA== +"@walletconnect/core@2.20.1": + version "2.20.1" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.20.1.tgz#dc854a1b29e911c1f4dcec3a02ab3c343d235cd8" + integrity sha512-DxybNfznr7aE/U9tJqvpEorUW2f/6kR0S1Zk78NqKam1Ex+BQFDM5j2Az3WayfFDZz3adkxkLAszfdorvPxDlw== dependencies: "@walletconnect/heartbeat" "1.2.2" "@walletconnect/jsonrpc-provider" "1.0.14" @@ -4433,16 +4511,39 @@ "@walletconnect/keyvaluestorage" "1.1.1" "@walletconnect/logger" "2.1.2" "@walletconnect/relay-api" "1.0.11" - "@walletconnect/relay-auth" "1.0.4" + "@walletconnect/relay-auth" "1.1.0" "@walletconnect/safe-json" "1.0.2" "@walletconnect/time" "1.0.2" - "@walletconnect/types" "2.17.3" - "@walletconnect/utils" "2.17.3" + "@walletconnect/types" "2.20.1" + "@walletconnect/utils" "2.20.1" "@walletconnect/window-getters" "1.0.1" + es-toolkit "1.33.0" events "3.3.0" - lodash.isequal "4.5.0" uint8arrays "3.1.0" +"@walletconnect/core@2.21.4": + version "2.21.4" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.21.4.tgz#0c0ede9ae7603743a1de008602c03ca8746bfda6" + integrity sha512-XtwPUCj3bCNX/2yjYGlQyvcsn32wkzixCjyWOD4pdKFVk7opZPAdF4xr85rmo6nJ7AiBYxjV1IH0bemTPEdE6Q== + dependencies: + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.16" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + "@walletconnect/relay-api" "1.0.11" + "@walletconnect/relay-auth" "1.1.0" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.21.4" + "@walletconnect/utils" "2.21.4" + "@walletconnect/window-getters" "1.0.1" + es-toolkit "1.39.3" + events "3.3.0" + uint8arrays "3.1.1" + "@walletconnect/environment@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/environment/-/environment-1.0.1.tgz#1d7f82f0009ab821a2ba5ad5e5a7b8ae3b214cd7" @@ -4466,23 +4567,6 @@ "@walletconnect/utils" "2.12.2" events "^3.3.0" -"@walletconnect/ethereum-provider@2.17.3": - version "2.17.3" - resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.17.3.tgz#53c546c56cb5033258cf4070677d0ba1208a0d6a" - integrity sha512-fgoT+dT9M1P6IIUtBl66ddD+4IJYqdhdAYkW+wa6jbctxKlHYSXf9HsgF/Vvv9lMnxHdAIz0W9VN4D/m20MamA== - dependencies: - "@walletconnect/jsonrpc-http-connection" "1.0.8" - "@walletconnect/jsonrpc-provider" "1.0.14" - "@walletconnect/jsonrpc-types" "1.0.4" - "@walletconnect/jsonrpc-utils" "1.0.8" - "@walletconnect/keyvaluestorage" "1.1.1" - "@walletconnect/modal" "2.7.0" - "@walletconnect/sign-client" "2.17.3" - "@walletconnect/types" "2.17.3" - "@walletconnect/universal-provider" "2.17.3" - "@walletconnect/utils" "2.17.3" - events "3.3.0" - "@walletconnect/events@1.0.1", "@walletconnect/events@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/events/-/events-1.0.1.tgz#2b5f9c7202019e229d7ccae1369a9e86bda7816c" @@ -4616,7 +4700,7 @@ motion "10.16.2" qrcode "1.5.3" -"@walletconnect/modal@2.7.0", "@walletconnect/modal@^2.6.2": +"@walletconnect/modal@^2.6.2": version "2.7.0" resolved "https://registry.yarnpkg.com/@walletconnect/modal/-/modal-2.7.0.tgz#55f969796d104cce1205f5f844d8f8438b79723a" integrity sha512-RQVt58oJ+rwqnPcIvRFeMGKuXb9qkgSmwz4noF8JZGUym3gUAzVs+uW2NQ1Owm9XOJAV+sANrtJ+VoVq1ftElw== @@ -4643,6 +4727,17 @@ tslib "1.14.1" uint8arrays "^3.0.0" +"@walletconnect/relay-auth@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz#c3c5f54abd44a5138ea7d4fe77970597ba66c077" + integrity sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ== + dependencies: + "@noble/curves" "1.8.0" + "@noble/hashes" "1.7.0" + "@walletconnect/safe-json" "^1.0.1" + "@walletconnect/time" "^1.0.2" + uint8arrays "^3.0.0" + "@walletconnect/safe-json@1.0.2", "@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/safe-json/-/safe-json-1.0.2.tgz#7237e5ca48046e4476154e503c6d3c914126fa77" @@ -4680,19 +4775,34 @@ "@walletconnect/utils" "2.17.1" events "3.3.0" -"@walletconnect/sign-client@2.17.3": - version "2.17.3" - resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.17.3.tgz#86c116bc927946bffa8415ca8d92d3ef412082e1" - integrity sha512-OzOWxRTfVGCHU3OOF6ibPkgPfDpivFJjuknfcOUt9PYWpTAv6YKOmT4cyfBPhc7llruyHpV44fYbykMcLIvEcg== +"@walletconnect/sign-client@2.20.1": + version "2.20.1" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.20.1.tgz#56fce57837c197055724b57dcefc6280fa3eade0" + integrity sha512-QXzIAHbyZZ52+97Bp/+/SBkN3hX0pam8l4lnA4P7g+aFPrVZUrMwZPIf+FV7UbEswqqwo3xmFI41TKgj8w8B9w== + dependencies: + "@walletconnect/core" "2.20.1" + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "2.1.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.20.1" + "@walletconnect/utils" "2.20.1" + events "3.3.0" + +"@walletconnect/sign-client@2.21.4": + version "2.21.4" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.21.4.tgz#61ace46547792feb84626846ef755c5c3d76eea0" + integrity sha512-v1OJ9IQCZAqaDEUYGFnGLe2fSp8DN9cv7j8tUYm5ngiFK7h6LjP4Ew3gGCca4AHWzMFkHuIRNQ+6Ypep1K/B7g== dependencies: - "@walletconnect/core" "2.17.3" + "@walletconnect/core" "2.21.4" "@walletconnect/events" "1.0.1" "@walletconnect/heartbeat" "1.2.2" "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/logger" "2.1.2" "@walletconnect/time" "1.0.2" - "@walletconnect/types" "2.17.3" - "@walletconnect/utils" "2.17.3" + "@walletconnect/types" "2.21.4" + "@walletconnect/utils" "2.21.4" events "3.3.0" "@walletconnect/sign-client@^2.13.1": @@ -4753,10 +4863,22 @@ "@walletconnect/logger" "2.1.2" events "3.3.0" -"@walletconnect/types@2.17.3": - version "2.17.3" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.17.3.tgz#906f25cf0c9691704b9161eaa305262b0e7626d0" - integrity sha512-5eFxnbZGJJx0IQyCS99qz+OvozpLJJYfVG96dEHGgbzZMd+C9V1eitYqVClx26uX6V+WQVqVwjpD2Dyzie++Wg== +"@walletconnect/types@2.20.1": + version "2.20.1" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.20.1.tgz#8956e5c610430349ae5f49a2a09547fe16d055a7" + integrity sha512-HM0YZxT+wNqskoZkuju5owbKTlqUXNKfGlJk/zh9pWaVWBR2QamvQ+47Cx09OoGPRQjQH0JmgRiUV4bOwWNeHg== + dependencies: + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + events "3.3.0" + +"@walletconnect/types@2.21.4": + version "2.21.4" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.21.4.tgz#6c9585321ca97c0d5b512bceff27c67d62deba15" + integrity sha512-6O61esDSW8FZNdFezgB4bX2S35HM6tCwBEjGHNB8JeoKCfpXG33hw2raU/SBgBL/jmM57QRW4M1aFH7v1u9z7g== dependencies: "@walletconnect/events" "1.0.1" "@walletconnect/heartbeat" "1.2.2" @@ -4780,10 +4902,10 @@ "@walletconnect/utils" "2.12.2" events "^3.3.0" -"@walletconnect/universal-provider@2.17.3": - version "2.17.3" - resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.17.3.tgz#2a1aaabe796d056911c5bf10dbd8fa5dd1395016" - integrity sha512-Aen8h+vWTN57sv792i96vaTpN06WnpFUWhACY5gHrpL2XgRKmoXUgW7793p252QdgyofNAOol7wJEs1gX8FjgQ== +"@walletconnect/universal-provider@2.21.4": + version "2.21.4" + resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.21.4.tgz#d68c8b0bbdb3c76669f6cc1ba44744811580d14f" + integrity sha512-ZSYU5H7Zi/nEy3L21kw5l3ovMagrbXDRKBG8vjPpGQAkflQocRj6d0SesFOCBEdJS16nt+6dKI2f5blpOGzyTQ== dependencies: "@walletconnect/events" "1.0.1" "@walletconnect/jsonrpc-http-connection" "1.0.8" @@ -4792,11 +4914,11 @@ "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/keyvaluestorage" "1.1.1" "@walletconnect/logger" "2.1.2" - "@walletconnect/sign-client" "2.17.3" - "@walletconnect/types" "2.17.3" - "@walletconnect/utils" "2.17.3" + "@walletconnect/sign-client" "2.21.4" + "@walletconnect/types" "2.21.4" + "@walletconnect/utils" "2.21.4" + es-toolkit "1.39.3" events "3.3.0" - lodash "4.17.21" "@walletconnect/utils@2.12.2": version "2.12.2" @@ -4870,31 +4992,54 @@ query-string "7.1.3" uint8arrays "3.1.0" -"@walletconnect/utils@2.17.3": - version "2.17.3" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.17.3.tgz#a22938567febc3e3771efae8eb351adf3d499a8d" - integrity sha512-tG77UpZNeLYgeOwViwWnifpyBatkPlpKSSayhN0gcjY1lZAUNqtYslpm4AdTxlrA3pL61MnyybXgWYT5eZjarw== +"@walletconnect/utils@2.20.1": + version "2.20.1" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.20.1.tgz#432689b559b573e14237b9f0f555835617e3cb84" + integrity sha512-u/uyJkVyxLLUbHbpMv7MmuOkGfElG08l6P2kMTAfN7nAVyTgpb8g6kWLMNqfmYXVz+h+finf5FSV4DgL2vOvPQ== dependencies: - "@ethersproject/hash" "5.7.0" - "@ethersproject/transactions" "5.7.0" - "@stablelib/chacha20poly1305" "1.0.1" - "@stablelib/hkdf" "1.0.1" - "@stablelib/random" "1.0.2" - "@stablelib/sha256" "1.0.1" - "@stablelib/x25519" "1.0.3" + "@noble/ciphers" "1.2.1" + "@noble/curves" "1.8.1" + "@noble/hashes" "1.7.1" "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/keyvaluestorage" "1.1.1" "@walletconnect/relay-api" "1.0.11" - "@walletconnect/relay-auth" "1.0.4" + "@walletconnect/relay-auth" "1.1.0" "@walletconnect/safe-json" "1.0.2" "@walletconnect/time" "1.0.2" - "@walletconnect/types" "2.17.3" + "@walletconnect/types" "2.20.1" "@walletconnect/window-getters" "1.0.1" "@walletconnect/window-metadata" "1.0.1" + bs58 "6.0.0" detect-browser "5.3.0" - elliptic "6.6.1" query-string "7.1.3" uint8arrays "3.1.0" + viem "2.23.2" + +"@walletconnect/utils@2.21.4": + version "2.21.4" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.21.4.tgz#cab0ffa0c393944923ca001d76026b9386c50a30" + integrity sha512-LuSyBXvRLiDqIu4uMFei5eJ/WPhkIkdW58fmDlRnryatIuBPCho3dlrNSbOjVSdsID+OvxjOlpPLi+5h4oTbaA== + dependencies: + "@msgpack/msgpack" "3.1.2" + "@noble/ciphers" "1.3.0" + "@noble/curves" "1.9.2" + "@noble/hashes" "1.8.0" + "@scure/base" "1.2.6" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/relay-api" "1.0.11" + "@walletconnect/relay-auth" "1.1.0" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.21.4" + "@walletconnect/window-getters" "1.0.1" + "@walletconnect/window-metadata" "1.0.1" + blakejs "1.2.1" + bs58 "6.0.0" + detect-browser "5.3.0" + query-string "7.1.3" + uint8arrays "3.1.1" + viem "2.31.0" "@walletconnect/web3wallet@^1.12.2": version "1.16.1" @@ -4930,7 +5075,12 @@ abitype@1.0.0: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== -abitype@1.0.7, abitype@^1.0.6: +abitype@1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.8.tgz#3554f28b2e9d6e9f35eb59878193eabd1b9f46ba" + integrity sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg== + +abitype@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.7.tgz#876a0005d211e1c9132825d45bcee7b46416b284" integrity sha512-ZfYYSktDQUwc2eduYu8C4wOs+RDPmnRYMh7zNfzeMtGGgb0U+6tLGjixUic6mXf5xKKCcgT5Qp6cv39tOARVFw== @@ -5043,7 +5193,7 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-hidden@^1.1.1: +aria-hidden@^1.1.1, aria-hidden@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== @@ -5170,7 +5320,7 @@ aws4fetch@1.0.20: resolved "https://registry.yarnpkg.com/aws4fetch/-/aws4fetch-1.0.20.tgz#090d6c65e32c6df645dd5e5acf04cc56da575cbe" integrity sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g== -axios@>=1.7.8, axios@^0.21.0, axios@^0.27.2: +axios@>=1.7.8, axios@^0.21.0, axios@^0.27.2, axios@^1.6.2: version "1.7.9" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== @@ -5210,6 +5360,11 @@ base-x@^4.0.0: resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== +base-x@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-5.0.1.tgz#16bf35254be1df8aca15e36b7c1dda74b2aa6b03" + integrity sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg== + base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -5235,7 +5390,7 @@ bintrees@1.0.2: resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw== -blakejs@^1.1.0: +blakejs@1.2.1, blakejs@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ== @@ -5373,6 +5528,13 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" +bs58@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-6.0.0.tgz#a2cda0130558535dd281a2f8697df79caaf425d8" + integrity sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw== + dependencies: + base-x "^5.0.0" + bs58@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" @@ -5458,6 +5620,13 @@ bullmq@^5.11.0: tslib "^2.0.0" uuid "^9.0.0" +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -5553,6 +5722,11 @@ chalk@^4.0.0, chalk@^4.0.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + change-case@5.4.4: version "5.4.4" resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02" @@ -5633,6 +5807,18 @@ cjs-module-lexer@^1.2.2: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== +cli-cursor@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-5.0.0.tgz#24a4831ecf5a6b01ddeb32fb71a4b2088b0dce38" + integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw== + dependencies: + restore-cursor "^5.0.0" + +cli-spinners@^2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + clipboardy@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-4.0.0.tgz#e73ced93a76d19dd379ebf1f297565426dffdca1" @@ -5877,6 +6063,14 @@ cron-parser@^4.6.0, cron-parser@^4.9.0: dependencies: luxon "^3.2.1" +cron@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/cron/-/cron-4.3.0.tgz#c5a62872f74f72294cf1cadef34c72ad8d8f50b5" + integrity sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw== + dependencies: + "@types/luxon" "~3.6.0" + luxon "~3.6.0" + cross-fetch@^3.1.4: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -6044,6 +6238,19 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +default-browser-id@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" + integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== + +default-browser@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf" + integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -6053,6 +6260,11 @@ define-data-property@^1.0.1, define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" @@ -6209,7 +6421,7 @@ ejs@^3.1.10: dependencies: jake "^10.8.5" -elliptic@6.5.4, elliptic@6.5.7, elliptic@6.6.0, elliptic@6.6.1, elliptic@>=6.6.0, elliptic@^6.4.1, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.5, elliptic@^6.5.7: +elliptic@6.5.4, elliptic@6.5.7, elliptic@6.6.0, elliptic@>=6.6.0, elliptic@^6.4.1, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.5, elliptic@^6.5.7: version "6.6.1" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g== @@ -6222,6 +6434,11 @@ elliptic@6.5.4, elliptic@6.5.7, elliptic@6.6.0, elliptic@6.6.1, elliptic@>=6.6.0 minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +emoji-regex@^10.3.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -6290,6 +6507,16 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" +es-toolkit@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.33.0.tgz#bcc9d92ef2e1ed4618c00dd30dfda9faddf4a0b7" + integrity sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg== + +es-toolkit@1.39.3: + version "1.39.3" + resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.39.3.tgz#934b2cab9578c496dcbc0305cae687258cb14aee" + integrity sha512-Qb/TCFCldgOy8lZ5uC7nLGdqJwSabkQiYQShmw4jyiPk1pZzaYWTwaYKYP7EgLccWYgZocMrtItrwh683voaww== + es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.63, es5-ext@^0.10.64, es5-ext@~0.10.14: version "0.10.64" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" @@ -7025,6 +7252,11 @@ fuse.js@7.0.0: resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.0.0.tgz#6573c9fcd4c8268e403b4fc7d7131ffcf99a9eb2" integrity sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q== +fuse.js@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.1.0.tgz#306228b4befeee11e05b027087c2744158527d09" + integrity sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ== + gaxios@^5.0.0, gaxios@^5.0.1: version "5.1.3" resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-5.1.3.tgz#f7fa92da0fe197c846441e5ead2573d4979e9013" @@ -7067,6 +7299,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-east-asian-width@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" + integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== + get-func-name@^2.0.1, get-func-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" @@ -7644,6 +7881,11 @@ is-inside-container@^1.0.0: dependencies: is-docker "^3.0.0" +is-interactive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90" + integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -7686,6 +7928,11 @@ is-typedarray@^1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is-unicode-supported@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" + integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + is-unicode-supported@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a" @@ -7728,11 +7975,6 @@ isomorphic-unfetch@3.1.0, isomorphic-unfetch@^3.1.0: node-fetch "^2.6.1" unfetch "^4.2.0" -isows@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.4.tgz#810cd0d90cc4995c26395d2aa4cfa4037ebdf061" - integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== - isows@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" @@ -8008,6 +8250,11 @@ klaw@^3.0.0: dependencies: graceful-fs "^4.1.9" +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + knex@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/knex/-/knex-3.1.0.tgz#b6ddd5b5ad26a6315234a5b09ec38dc4a370bd8c" @@ -8202,11 +8449,19 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== -lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.21: +lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-6.0.0.tgz#bb95e5f05322651cac30c0feb6404f9f2a8a9439" + integrity sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw== + dependencies: + chalk "^5.3.0" + is-unicode-supported "^1.3.0" + logform@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/logform/-/logform-2.7.0.tgz#cfca97528ef290f2e125a08396805002b2d060d1" @@ -8265,6 +8520,11 @@ luxon@^3.2.1: resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20" integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ== +luxon@~3.6.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.6.1.tgz#d283ffc4c0076cb0db7885ec6da1c49ba97e47b0" + integrity sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ== + magic-sdk@^13.6.2: version "13.6.2" resolved "https://registry.yarnpkg.com/magic-sdk/-/magic-sdk-13.6.2.tgz#68766fd9d1805332d2a00e5da1bd30fce251a6ac" @@ -8403,6 +8663,11 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -8621,13 +8886,6 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== -node-cron@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.3.tgz#c4bc7173dd96d96c50bdb51122c64415458caff2" - integrity sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A== - dependencies: - uuid "8.3.2" - node-fetch-native@^1.6.4: version "1.6.4" resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e" @@ -8805,6 +9063,23 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +onetime@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60" + integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ== + dependencies: + mimic-function "^5.0.0" + +open@10.1.1: + version "10.1.1" + resolved "https://registry.yarnpkg.com/open/-/open-10.1.1.tgz#5fd814699e47ae3e1a09962d39f4f4441cae6c22" + integrity sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA== + dependencies: + default-browser "^5.2.1" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + is-wsl "^3.1.0" + openapi-types@^12.0.0: version "12.1.3" resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" @@ -8838,28 +9113,30 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" +ora@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-8.2.0.tgz#8fbbb7151afe33b540dd153f171ffa8bd38e9861" + integrity sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw== + dependencies: + chalk "^5.3.0" + cli-cursor "^5.0.0" + cli-spinners "^2.9.2" + is-interactive "^2.0.0" + is-unicode-supported "^2.0.0" + log-symbols "^6.0.0" + stdin-discarder "^0.2.2" + string-width "^7.2.0" + strip-ansi "^7.1.0" + os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== -ox@0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ox/-/ox-0.1.2.tgz#0f791be2ccabeaf4928e6d423498fe1c8094e560" - integrity sha512-ak/8K0Rtphg9vnRJlbOdaX9R7cmxD2MiSthjWGaQdMk3D7hrAlDoM+6Lxn7hN52Za3vrXfZ7enfke/5WjolDww== - dependencies: - "@adraffy/ens-normalize" "^1.10.1" - "@noble/curves" "^1.6.0" - "@noble/hashes" "^1.5.0" - "@scure/bip32" "^1.5.0" - "@scure/bip39" "^1.4.0" - abitype "^1.0.6" - eventemitter3 "5.0.1" - -ox@0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/ox/-/ox-0.4.2.tgz#0ef5b322baec0cbd055dfb27f22795f6af522594" - integrity sha512-X3Ho21mTtJiCU2rWmfaheh2b0CG70Adre7Da/XQ0ECy+QppI6pLqdbGAJHiu/cTjumVXfwDGfv48APqePCU+ow== +ox@0.6.7, ox@0.6.9, ox@0.7.0: + version "0.6.9" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.6.9.tgz#da1ee04fa10de30c8d04c15bfb80fe58b1f554bd" + integrity sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug== dependencies: "@adraffy/ens-normalize" "^1.10.1" "@noble/curves" "^1.6.0" @@ -9312,6 +9589,14 @@ prom-client@^15.1.3: "@opentelemetry/api" "^1.4.0" tdigest "^0.1.1" +prompts@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + prool@^0.0.16: version "0.0.16" resolved "https://registry.yarnpkg.com/prool/-/prool-0.0.16.tgz#b18c76fd102485ce4c706bb6031bd85a49859a45" @@ -9563,16 +9848,16 @@ react-remove-scroll@2.5.5: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" -react-remove-scroll@^2.6.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz#2518d2c5112e71ea8928f1082a58459b5c7a2a97" - integrity sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw== +react-remove-scroll@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz#df02cde56d5f2731e058531f8ffd7f9adec91ac2" + integrity sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ== dependencies: react-remove-scroll-bar "^2.3.7" - react-style-singleton "^2.2.1" + react-style-singleton "^2.2.3" tslib "^2.1.0" use-callback-ref "^1.3.3" - use-sidecar "^1.1.2" + use-sidecar "^1.1.3" react-style-singleton@^2.2.1: version "2.2.1" @@ -9583,7 +9868,7 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" -react-style-singleton@^2.2.2: +react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== @@ -9707,6 +9992,14 @@ resolve@^1.19.0, resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +restore-cursor@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-5.1.0.tgz#0766d95699efacb14150993f55baf0953ea1ebe7" + integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== + dependencies: + onetime "^7.0.0" + signal-exit "^4.1.0" + ret@~0.4.0: version "0.4.3" resolved "https://registry.yarnpkg.com/ret/-/ret-0.4.3.tgz#5243fa30e704a2e78a9b9b1e86079e15891aa85c" @@ -9794,6 +10087,11 @@ rollup@^4.20.0: "@rollup/rollup-win32-x64-msvc" "4.28.1" fsevents "~2.3.2" +run-applescript@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" + integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -9974,6 +10272,11 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + solady@0.0.180: version "0.0.180" resolved "https://registry.yarnpkg.com/solady/-/solady-0.0.180.tgz#d806c84a0bf8b6e3d85a8fb0978980de086ff59e" @@ -10055,6 +10358,11 @@ std-env@^3.7.0, std-env@^3.8.0: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== +stdin-discarder@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz#390037f44c4ae1a1ae535c5fe38dc3aba8d997be" + integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ== + stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" @@ -10118,6 +10426,15 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -10146,7 +10463,7 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -10296,33 +10613,41 @@ thirdweb@5.29.6: uqr "0.1.2" viem "2.13.7" -thirdweb@^5.83.0: - version "5.83.0" - resolved "https://registry.yarnpkg.com/thirdweb/-/thirdweb-5.83.0.tgz#0fb445daa472fcb9bdde61614979a33d0101f163" - integrity sha512-E0wRMEQvbh9EZvrwfHCDHE+RRhDjJAewk/umhwA55axRf4/LENHM0l3KmlQKEaWRXmla8fu+d6q39OlU1QrSTw== +thirdweb@^5.105.42: + version "5.105.42" + resolved "https://registry.yarnpkg.com/thirdweb/-/thirdweb-5.105.42.tgz#c166773f72ad576d55923dbd5b199512aae91411" + integrity sha512-tRe/mzMMOhVk4l3jE/dgT8TeffqvdRLOL15DahNkLFIsMt65wqAgOtOtwZFMf3moVcZcd+ou20rZKBpJ0hc1FA== dependencies: - "@coinbase/wallet-sdk" "4.2.4" + "@coinbase/wallet-sdk" "4.3.0" "@emotion/react" "11.14.0" - "@emotion/styled" "11.14.0" - "@google/model-viewer" "2.1.1" - "@noble/curves" "1.7.0" - "@noble/hashes" "1.6.1" + "@emotion/styled" "11.14.1" + "@noble/curves" "1.8.2" + "@noble/hashes" "1.7.2" "@passwordless-id/webauthn" "^2.1.2" - "@radix-ui/react-dialog" "1.1.4" - "@radix-ui/react-focus-scope" "1.1.1" + "@radix-ui/react-dialog" "1.1.14" + "@radix-ui/react-focus-scope" "1.1.7" "@radix-ui/react-icons" "1.3.2" - "@radix-ui/react-tooltip" "1.1.5" - "@tanstack/react-query" "5.62.16" - "@walletconnect/ethereum-provider" "2.17.3" - "@walletconnect/sign-client" "2.17.3" - abitype "1.0.7" + "@radix-ui/react-tooltip" "1.2.7" + "@storybook/react" "9.0.15" + "@tanstack/react-query" "5.81.5" + "@thirdweb-dev/engine" "3.2.1" + "@thirdweb-dev/insight" "1.1.1" + "@walletconnect/sign-client" "2.20.1" + "@walletconnect/universal-provider" "2.21.4" + abitype "1.0.8" cross-spawn "7.0.6" - fuse.js "7.0.0" + fuse.js "7.1.0" input-otp "^1.4.1" mipd "0.0.7" - ox "0.4.2" + open "10.1.1" + ora "8.2.0" + ox "0.7.0" + prompts "2.4.2" + qrcode "1.5.3" + toml "3.0.0" uqr "0.1.2" - viem "2.21.55" + viem "2.33.2" + zod "3.25.75" thread-stream@^0.15.1: version "0.15.2" @@ -10417,6 +10742,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +toml@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -10516,6 +10846,13 @@ uint8arrays@3.1.0: dependencies: multiformats "^9.4.2" +uint8arrays@3.1.1, uint8arrays@^3.0.0, uint8arrays@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" + integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg== + dependencies: + multiformats "^9.4.2" + uint8arrays@^2.1.3: version "2.1.10" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-2.1.10.tgz#34d023c843a327c676e48576295ca373c56e286a" @@ -10523,13 +10860,6 @@ uint8arrays@^2.1.3: dependencies: multiformats "^9.4.2" -uint8arrays@^3.0.0, uint8arrays@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" - integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg== - dependencies: - multiformats "^9.4.2" - uncrypto@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" @@ -10550,6 +10880,11 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== +undici@^6.20.1: + version "6.20.1" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.20.1.tgz#fbb87b1e2b69d963ff2d5410a40ffb4c9e81b621" + integrity sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA== + unenv@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.10.0.tgz#c3394a6c6e4cfe68d699f87af456fe3f0db39571" @@ -10656,6 +10991,14 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sidecar@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" + integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + use-sync-external-store@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" @@ -10703,11 +11046,6 @@ uuid@8.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== -uuid@8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - uuid@9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" @@ -10736,48 +11074,18 @@ varint@^6.0.0: resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== -viem@2.13.7: - version "2.13.7" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.13.7.tgz#c1153c02f7ffaf0263d784fc1d4e4ffa3f66c24a" - integrity sha512-SZWn9LPrz40PHl4PM2iwkPTTtjWPDFsnLr32UwpqC/Z5f0AwxitjLyZdDKcImvbWZ3vLQ0oPggR1aLlqvTcUug== +viem@2.13.7, viem@2.22.17, viem@2.23.2, viem@2.31.0, viem@2.33.2: + version "2.22.17" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.22.17.tgz#71cb5793d898e7850d440653b0043803c2d00c8d" + integrity sha512-eqNhlPGgRLR29XEVUT2uuaoEyMiaQZEKx63xT1py9OYsE+ZwlVgjnfrqbXad7Flg2iJ0Bs5Hh7o0FfRWUJGHvg== dependencies: - "@adraffy/ens-normalize" "1.10.0" - "@noble/curves" "1.2.0" - "@noble/hashes" "1.3.2" - "@scure/bip32" "1.3.2" - "@scure/bip39" "1.2.1" - abitype "1.0.0" - isows "1.0.4" - ws "8.13.0" - -viem@2.21.55: - version "2.21.55" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.55.tgz#a57ad31fcf2a0f6c011b1909f02c94421ec4f781" - integrity sha512-PgXew7C11cAuEtOSgRyQx2kJxEOPUwIwZA9dMglRByqJuFVA7wSGZZOOo/93iylAA8E15bEdqy9xulU3oKZ70Q== - dependencies: - "@noble/curves" "1.7.0" - "@noble/hashes" "1.6.1" - "@scure/bip32" "1.6.0" - "@scure/bip39" "1.5.0" - abitype "1.0.7" + "@noble/curves" "1.8.1" + "@noble/hashes" "1.7.1" + "@scure/bip32" "1.6.2" + "@scure/bip39" "1.5.4" + abitype "1.0.8" isows "1.0.6" - ox "0.1.2" - webauthn-p256 "0.0.10" - ws "8.18.0" - -viem@^2.21.54: - version "2.21.54" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.54.tgz#76d6f86ab8809078f1ac140ac1a2beadbc86b9f6" - integrity sha512-G9mmtbua3UtnVY9BqAtWdNp+3AO+oWhD0B9KaEsZb6gcrOWgmA4rz02yqEMg+qW9m6KgKGie7q3zcHqJIw6AqA== - dependencies: - "@noble/curves" "1.7.0" - "@noble/hashes" "1.6.1" - "@scure/bip32" "1.6.0" - "@scure/bip39" "1.5.0" - abitype "1.0.7" - isows "1.0.6" - ox "0.1.2" - webauthn-p256 "0.0.10" + ox "0.6.7" ws "8.18.0" vite-node@2.1.8: @@ -11052,14 +11360,6 @@ web3-validator@^2.0.6: web3-types "^1.6.0" zod "^3.21.4" -webauthn-p256@0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.10.tgz#877e75abe8348d3e14485932968edf3325fd2fdd" - integrity sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA== - dependencies: - "@noble/curves" "^1.4.0" - "@noble/hashes" "^1.4.0" - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -11193,7 +11493,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@7.4.6, ws@8.13.0, ws@8.18.0, ws@8.9.0, ws@>=8.17.1, ws@^7.4.0, ws@^7.5.1, ws@^8.0.0: +ws@7.4.6, ws@8.18.0, ws@8.9.0, ws@>=8.17.1, ws@^7.4.0, ws@^7.5.1, ws@^8.0.0: version "8.18.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== @@ -11349,6 +11649,11 @@ zod@3.23.8: resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== +zod@3.25.75: + version "3.25.75" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.75.tgz#8ff9be2fbbcb381a9236f9f74a8879ca29dcc504" + integrity sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg== + zod@^3.21.4, zod@^3.22.4, zod@^3.23.8: version "3.24.1" resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"