diff --git a/.env.example b/.env.example index d14f23e1..e3f7e129 100644 --- a/.env.example +++ b/.env.example @@ -9,8 +9,6 @@ PAYMASTER_SANDBOX_CONTRACT_ADDRESS_V_0_1_0=sandbox_paymaster_contract_address PAYMASTER_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0=production_paymaster_contract_address ENTRYPOINT_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0=production_entrypoint_address ENTRYPOINT_SANDBOX_CONTRACT_ADDRESS_V_0_1_0=sandbox_entrypoint_address -ETHERSPOT_WALLET_FACTORY_SANDBOX_CONTRACT_ADDRESS_V_0_1_0=sandbox_etherspot_wallet_factory_contract_address -ETHERSPOT_WALLET_FACTORY_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0=production_etherspot_wallet_factory_contract_address PAYMASTER_FUNDER_PRIVATE_KEY= PAYMASTER_FUNDER_API_KEY= PAYMASTER_FUNDER_API_SECRET_KEY= @@ -33,8 +31,6 @@ API_MONGO_URI=mongodb://mongo:27017/charge-api LEGACY_FUSE_ADMIN_API_URL=https://studio.fuse.io LEGACY_FUSE_WALLET_API_URL=https://wallet.fuse.io EXPLORER_API_URL=https://explorer.fuse.io/api -BUNDLER_API_PRD_URL=https://fuse-bundler.etherspot.io -BUNDLER_API_SANDBOX_URL=https://fusetestnet-bundler.etherspot.io PIMLICO_API_PRD_URL=https://api.pimlico.io/v2/122/rpc?apikey=api_key PIMLICO_API_SANDBOX_URL=https://api.pimlico.io/v2/123/rpc?apikey=api_key PAYMASTER_PRODUCTION_SIGNER_PRIVATE_KEY_V_0_1_0=production_paymaster_signer diff --git a/ETHERSPOT_TO_PIMLICO_MIGRATION.md b/ETHERSPOT_TO_PIMLICO_MIGRATION.md new file mode 100644 index 00000000..560c1779 --- /dev/null +++ b/ETHERSPOT_TO_PIMLICO_MIGRATION.md @@ -0,0 +1,199 @@ +# Etherspot to Pimlico Migration Plan + +## Context + +Etherspot is the default ERC-4337 bundler (Pimlico is secondary via `?provider=pimlico`). The Etherspot wallet factory's only usage (`predictWallet()`) is dead code - never called. Wallet creation has moved client-side (SAFE SDK). Goal: remove all Etherspot references, make Pimlico the sole bundler. Keep FuseVerifyingPaymaster as-is. + +**Existing AA wallets are unaffected.** The bundler is transport-only - wallets interact with the shared EntryPoint (`0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789` v0.6), not the bundler. No on-chain changes needed. + +--- + +## Pre-Flight Checks + +Before any code changes, confirm externally: + +- [ ] Pimlico supports **Fuse mainnet (chain 122)** and **Fuse Spark testnet (chain 123)** +- [ ] Pimlico supports **EntryPoint v0.6** (`0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789`) +- [ ] Pimlico `eth_estimateUserOperationGas` returns the same gas fields (`callGasLimit`, `verificationGasLimit`, `preVerificationGas`) +- [ ] Clarify ERC-4337 version: the interceptor has v0.7 patterns for Pimlico (separate `paymaster` + `paymasterData` fields). Confirm if the Pimlico endpoint is v0.6 or v0.7 + +--- + +## Why Existing Wallets Are Safe + +``` +Client SDK -> Bundler (transport) -> EntryPoint (on-chain) -> Wallet (on-chain) +``` + +| Component | Etherspot-Dependent? | Why | +| --- | --- | --- | +| **Deployed wallet contract** | No | Standard ERC-4337 account. Factory is only used at deploy time, never called again | +| **EntryPoint** | No | Shared contract `0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789` (v0.6). Same for all bundlers | +| **FuseVerifyingPaymaster** | No | Validates signatures and sponsor IDs. Bundler-agnostic | +| **UserOperation format** | No | Standard ERC-4337 struct. Same fields regardless of bundler | +| **Wallet factory** | Only at creation | `predictWallet()` is dead code. No new wallets are created via Etherspot factory | + +The `migrateOperatorWallet` endpoint is a **database-only** change (renames `smartWalletAddress` to `etherspotSmartWalletAddress`, stores new SAFE address). Nothing happens on-chain. + +--- + +## Critical Risks + +| Risk | Severity | Details | +| --- | --- | --- | +| **Paymaster gas estimation hardcoded to Etherspot** | **Critical** | `paymaster-api.service.ts:214` - breaks immediately if Etherspot URL is removed | +| **EntryPoint version mismatch** | **High** | System uses v0.6. Pimlico branch in interceptor has v0.7 patterns. Must confirm compatibility | +| **Clients passing `?provider=etherspot`** | **High** | Config lookup `bundler.etherspot.*` will crash. Need explicit 400 error | +| **Sponsored quota logic change** | **Medium** | Quota check currently bypasses Etherspot. With Pimlico-only, quota always applies | +| **DB field `etherspotSmartWalletAddress`** | **Low** | Historical data in MongoDB. Leave as-is | + +--- + +## Complete Etherspot Reference Inventory + +### Application Code (8 files) + +| File | Lines | What | +| --- | --- | --- | +| `apps/charge-api-service/src/bundler-api/interfaces/bundler.interface.ts` | 1-4 | `BundlerProvider.ETHERSPOT` enum | +| `apps/charge-api-service/src/bundler-api/config/configuration.ts` | 3-10 | `bundler.etherspot` config block | +| `apps/charge-api-service/src/bundler-api/bundler-api.interceptor.ts` | 110, 138, 146 | Default provider, Pimlico branching | +| `apps/charge-api-service/src/paymaster-api/paymaster-api.service.ts` | 214 | Hardcoded `bundler.etherspot.${env}` | +| `apps/charge-accounts-service/src/operators/operators.service.ts` | 9, 225, 231, 259, 287-299, 1012-1013 | Factory ABI import, wallet migration refs, `predictWallet()`, quota logic | +| `apps/charge-accounts-service/src/operators/operators.controller.ts` | 96, 102 | Swagger docs mention "Etherspot" | +| `apps/charge-accounts-service/src/common/config/configuration.ts` | 11-12, 19-20 | `etherspotWalletFactoryContractAddress` | +| `apps/charge-accounts-service/src/operators/abi/EtherspotWalletFactory.abi.json` | all | Entire ABI file (delete) | + +### Schema / Interfaces (3 files - leave as-is, historical DB data) + +| File | Line | Field | +| --- | --- | --- | +| `operators/schemas/operator-wallet.schema.ts` | 12 | `etherspotSmartWalletAddress` | +| `operators/interfaces/operator-wallet.interface.ts` | 7 | `etherspotSmartWalletAddress` | +| `operators/interfaces/operator-user-interface.ts` | 9 | `etherspotSmartWalletAddress` | + +### Environment / Docker (3 files) + +| File | Lines | What | +| --- | --- | --- | +| `.env.example` | 12-13, 36-37 | `ETHERSPOT_WALLET_FACTORY_*`, `BUNDLER_API_*_URL` (etherspot URLs) | +| `docker-compose.yml` | 111, 114, 175, 178, 236, 239 | `ETHERSPOT_WALLET_FACTORY_*` env vars | +| `docker-compose.yml` | 307-308, 361-362, 412-413 | `BUNDLER_API_PRD_URL` / `BUNDLER_API_SANDBOX_URL` | + +### Helm / K8s (8 files) + +| File | What | +| --- | --- | +| `helm/values/prod.yaml` | Etherspot factory addresses (134-135), bundler sandbox URL (281) | +| `helm/values/qa.yaml` | Same (134-135, 281) | +| `k8s/helm/production.yaml` | Factory addresses (61-62), bundler URLs (141), Skandha config (343-356) | +| `k8s/helm/staging.yaml` | Factory addresses (61-62), bundler URLs (140), Skandha disabled (343-353) | +| `k8s/helm/charts/skandha/` | Entire directory (Chart, templates, values, README) | +| `k8s/helm/charts/accounts/values.yaml` | `etherspot_wallet_factory_*` (81-84) | +| `k8s/helm/charts/accounts/README.md` | Docs for factory addresses (21-22) | +| `k8s/helm/Chart.yaml` | Skandha dependency (46-47) | + +--- + +## Implementation Steps + +### Phase 1: Application Code + +#### Step 1 - Simplify BundlerProvider enum + +**File:** `apps/charge-api-service/src/bundler-api/interfaces/bundler.interface.ts` + +- Remove `ETHERSPOT` value, keep only `PIMLICO` + +#### Step 2 - Flatten bundler config + +**File:** `apps/charge-api-service/src/bundler-api/config/configuration.ts` + +- Remove nested `etherspot` / `pimlico` structure +- Flatten to: `bundler.production.url` / `bundler.sandbox.url` +- Use env vars `PIMLICO_API_PRD_URL` / `PIMLICO_API_SANDBOX_URL` + +#### Step 3 - Clean up bundler interceptor + +**File:** `apps/charge-api-service/src/bundler-api/bundler-api.interceptor.ts` + +- Remove `?provider=` query param routing. If `?provider=etherspot` passed, return 400 error +- `prepareUrl()`: use flat config `bundler.${environment}.url` +- `constructUserOp()`: remove Etherspot/Pimlico branching, keep Pimlico format as the only format + +#### Step 4 - Fix paymaster gas estimation URL (CRITICAL) + +**File:** `apps/charge-api-service/src/paymaster-api/paymaster-api.service.ts` + +- Line 214: `bundler.etherspot.${environment}` -> `bundler.${environment}` + +#### Step 5 - Clean up operators service + +**File:** `apps/charge-accounts-service/src/operators/operators.service.ts` + +- Remove `import etherspotWalletFactoryAbi` (line 9) +- Delete `predictWallet()` method (lines 287-299) - dead code, never called +- Line 1012: Default `BundlerProvider.ETHERSPOT` -> `BundlerProvider.PIMLICO` +- Lines 1013-1015: Remove Etherspot bypass in quota check (quota now always applies) + +#### Step 6 - Update controller docs + +**File:** `apps/charge-accounts-service/src/operators/operators.controller.ts` + +- Lines 96, 102: Update Swagger descriptions from "Etherspot" to "legacy" + +#### Step 7 - Remove Etherspot wallet factory config + ABI + +- `apps/charge-accounts-service/src/common/config/configuration.ts` -> remove `etherspotWalletFactoryContractAddress` +- Delete `apps/charge-accounts-service/src/operators/abi/EtherspotWalletFactory.abi.json` + +#### Step 8 - DB fields (NO CHANGES) + +- `etherspotSmartWalletAddress` stays in schema, interface, and service +- Existing MongoDB documents have this field name - changing it requires a data migration +- The migration endpoint (`migrateOperatorWallet`) still writes to this field for any future migrations + +### Phase 2: Environment and Infrastructure + +#### Step 9 - `.env.example` + +- Remove `ETHERSPOT_WALLET_FACTORY_*` vars +- Remove `BUNDLER_API_PRD_URL` / `BUNDLER_API_SANDBOX_URL` (Etherspot URLs) +- Keep `PIMLICO_API_PRD_URL` / `PIMLICO_API_SANDBOX_URL` + +#### Step 10 - `docker-compose.yml` + +- Remove `ETHERSPOT_WALLET_FACTORY_*` env vars (6 occurrences) +- Update `BUNDLER_API_*` to point to Pimlico or remove if using `PIMLICO_API_*` vars + +#### Step 11 - Helm values + +- `helm/values/prod.yaml` - remove factory addresses, update bundler URLs +- `helm/values/qa.yaml` - same + +#### Step 12 - K8s configs + +- `k8s/helm/production.yaml` - remove factory addresses, update bundler URLs, remove Skandha section +- `k8s/helm/staging.yaml` - same + +#### Step 13 - Remove Skandha + +- Delete `k8s/helm/charts/skandha/` directory +- Remove Skandha dependency from `k8s/helm/Chart.yaml` (line 46-47) + +#### Step 14 - Accounts helm chart + +- `k8s/helm/charts/accounts/values.yaml` - remove `etherspot_wallet_factory_*` +- `k8s/helm/charts/accounts/README.md` - remove corresponding docs + +--- + +## Verification + +1. `npm run build` - clean compilation +2. `npm run lint` - passes +3. `grep -ri "etherspot" --include="*.ts" apps/` - only hits: `etherspotSmartWalletAddress` in schema/interface/service (historical DB field) +4. `grep -ri "skandha" k8s/` - zero results +5. Manual: Bundler API routes to Pimlico for both environments +6. Manual: Paymaster `eth_estimateUserOperationGas` hits Pimlico URL +7. Manual: `?provider=etherspot` returns 400 error diff --git a/apps/charge-accounts-service/src/common/config/configuration.ts b/apps/charge-accounts-service/src/common/config/configuration.ts index 5705e015..4ab787f7 100644 --- a/apps/charge-accounts-service/src/common/config/configuration.ts +++ b/apps/charge-accounts-service/src/common/config/configuration.ts @@ -7,17 +7,13 @@ export default () => ({ paymasterContractAddress: process.env.PAYMASTER_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0, entrypointAddress: process.env.ENTRYPOINT_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0, - url: process.env.RPC_URL || 'https://rpc.fuse.io', - etherspotWalletFactoryContractAddress: - process.env.ETHERSPOT_WALLET_FACTORY_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0 + url: process.env.RPC_URL || 'https://rpc.fuse.io' }, sandbox: { paymasterContractAddress: process.env.PAYMASTER_SANDBOX_CONTRACT_ADDRESS_V_0_1_0, entrypointAddress: process.env.ENTRYPOINT_SANDBOX_CONTRACT_ADDRESS_V_0_1_0, - url: process.env.SPARK_RPC_URL || 'https://rpc.fusespark.io', - etherspotWalletFactoryContractAddress: - process.env.ETHERSPOT_WALLET_FACTORY_SANDBOX_CONTRACT_ADDRESS_V_0_1_0 + url: process.env.SPARK_RPC_URL || 'https://rpc.fusespark.io' } } }, diff --git a/apps/charge-accounts-service/src/operators/abi/EtherspotWalletFactory.abi.json b/apps/charge-accounts-service/src/operators/abi/EtherspotWalletFactory.abi.json deleted file mode 100644 index b2378c3f..00000000 --- a/apps/charge-accounts-service/src/operators/abi/EtherspotWalletFactory.abi.json +++ /dev/null @@ -1,196 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "wallet", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "index", - "type": "uint256" - } - ], - "name": "AccountCreation", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "newImplementation", - "type": "address" - } - ], - "name": "ImplementationSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnerChanged", - "type": "event" - }, - { - "inputs": [], - "name": "accountCreationCode", - "outputs": [ - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [], - "name": "accountImplementation", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_newOwner", - "type": "address" - } - ], - "name": "changeOwner", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_impl", - "type": "address" - } - ], - "name": "checkImplementation", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_index", - "type": "uint256" - } - ], - "name": "createAccount", - "outputs": [ - { - "internalType": "address", - "name": "ret", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_index", - "type": "uint256" - } - ], - "name": "getAddress", - "outputs": [ - { - "internalType": "address", - "name": "proxy", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract EtherspotWallet", - "name": "_newImpl", - "type": "address" - } - ], - "name": "setImplementation", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } -] diff --git a/apps/charge-accounts-service/src/operators/operators.controller.ts b/apps/charge-accounts-service/src/operators/operators.controller.ts index dbd9f99c..5cf4e717 100644 --- a/apps/charge-accounts-service/src/operators/operators.controller.ts +++ b/apps/charge-accounts-service/src/operators/operators.controller.ts @@ -93,13 +93,13 @@ export class OperatorsController { } /** - * Migrate Etherspot AA wallet to SAFE for an operator + * Migrate legacy AA wallet to SAFE for an operator * @param authOperatorDto * @returns the SAFE AA wallet */ @UseGuards(JwtAuthGuard) @Post('/migrate-wallet') - @ApiOperation({ summary: 'Migrate Etherspot AA wallet to SAFE for an operator' }) + @ApiOperation({ summary: 'Migrate legacy AA wallet to SAFE for an operator' }) @ApiBody({ type: CreateOperatorWallet, required: true }) async migrateOperatorWallet (@Body() migrateOperatorWalletDto: CreateOperatorWalletDto, @User('sub') auth0Id: string) { return this.operatorsService.migrateOperatorWallet(migrateOperatorWalletDto, auth0Id) diff --git a/apps/charge-accounts-service/src/operators/operators.service.ts b/apps/charge-accounts-service/src/operators/operators.service.ts index a71f02d0..a61f7cf1 100644 --- a/apps/charge-accounts-service/src/operators/operators.service.ts +++ b/apps/charge-accounts-service/src/operators/operators.service.ts @@ -6,7 +6,6 @@ import { PaymasterService } from '@app/accounts-service/paymaster/paymaster.serv import { UsersService } from '@app/accounts-service/users/users.service' import { AuthOperatorDto } from '@app/accounts-service/operators/dto/auth-operator.dto' import paymasterAbi from '@app/api-service/paymaster-api/abi/FuseVerifyingPaymasterSingleton.abi.json' -import etherspotWalletFactoryAbi from '@app/accounts-service/operators/abi/EtherspotWalletFactory.abi.json' import { ConfigService } from '@nestjs/config' import { CreateOperatorUserDto } from '@app/accounts-service/operators/dto/create-operator-user.dto' import { OperatorWallet } from '@app/accounts-service/operators/interfaces/operator-wallet.interface' @@ -35,7 +34,6 @@ import { ChargeCheckoutWebhookEvent } from '@app/accounts-service/operators/inte import { ChargeCheckoutBillingCycle, ChargeCheckoutPaymentStatus } from '@app/accounts-service/operators/interfaces/charge-checkout.interface' import { differenceInMonths, getDate, getDaysInMonth, startOfMonth } from 'date-fns' import { monthsInYear } from 'date-fns/constants' -import { BundlerProvider } from '@app/api-service/bundler-api/interfaces/bundler.interface' import { CreateChargeBridgeDto } from '@app/accounts-service/operators/dto/create-charge-bridge.dto' import { ChargeBridge } from '@app/accounts-service/operators/interfaces/charge-bridge.interface' import TradeService from '@app/common/token/trade.service' @@ -284,20 +282,6 @@ export class OperatorsService { } } - async predictWallet (owner: string, index: number, ver: string, environment: string): Promise { - const paymasterEnvs = this.configService.getOrThrow(`paymaster.${ver}`) - const contractAddress = paymasterEnvs[environment].etherspotWalletFactoryContractAddress - - const provider = new ethers.providers.JsonRpcProvider(paymasterEnvs[environment].url) - const contract = new ethers.Contract(contractAddress, etherspotWalletFactoryAbi, provider) - - try { - return await contract.getAddress(owner, index) - } catch (error) { - throw new InternalServerErrorException(`Failed to predict wallet: ${error}`) - } - } - async handleWebhookReceiveAndFundPaymasterAndDeleteWalletAddressFromOperatorsWebhook (webhookEvent: WebhookEvent): Promise { this.logger.log(`handleWebhook: ${JSON.stringify(webhookEvent)}`) const DEPOSIT_REQUIRED = 10 // Consider moving to a config or class constant @@ -1009,10 +993,6 @@ export class OperatorsService { async isOperatorSponsoredQuotaExceeded (context: ExecutionContext, requestConfig: AxiosRequestConfig) { const request = context.switchToHttp().getRequest() - const bundlerProvider = request.query?.provider ?? BundlerProvider.ETHERSPOT - if (bundlerProvider !== BundlerProvider.PIMLICO) { - return false - } const paymaster = requestConfig.data?.params?.[0]?.paymaster if (!paymaster) { diff --git a/apps/charge-api-service/src/bundler-api/bundler-api.interceptor.ts b/apps/charge-api-service/src/bundler-api/bundler-api.interceptor.ts index a9cd7858..ace2e026 100644 --- a/apps/charge-api-service/src/bundler-api/bundler-api.interceptor.ts +++ b/apps/charge-api-service/src/bundler-api/bundler-api.interceptor.ts @@ -107,11 +107,10 @@ export class BundlerApiInterceptor implements NestInterceptor { private async prepareRequestConfig (context: ExecutionContext) { const request = context.switchToHttp().getRequest() const requestEnvironment = request.environment - const bundlerProvider = request.query?.provider ?? BundlerProvider.ETHERSPOT const ctxHandlerName = context.getHandler().name const body = request.body const requestConfig: AxiosRequestConfig = { - url: this.prepareUrl(requestEnvironment, bundlerProvider), + url: this.prepareUrl(requestEnvironment), method: ctxHandlerName } @@ -122,11 +121,11 @@ export class BundlerApiInterceptor implements NestInterceptor { return requestConfig } - private prepareUrl (environment, bundlerProvider) { + private prepareUrl (environment) { if (isEmpty(environment)) throw new InternalServerErrorException('Bundler environment is missing') - const config = this.configService.get(`bundler.${bundlerProvider}.${environment}`) + const config = this.configService.get(`bundler.${environment}`) - if (config.url) { + if (config?.url) { return config.url } else { throw new InternalServerErrorException(`${capitalize(environment)} bundler environment is missing`) @@ -135,7 +134,6 @@ export class BundlerApiInterceptor implements NestInterceptor { private constructUserOp (context: ExecutionContext, requestConfig: AxiosRequestConfig, response) { const request = context.switchToHttp().getRequest() - const bundlerProvider = request.query?.provider ?? BundlerProvider.ETHERSPOT const param = requestConfig?.data?.params?.[0] const base = { userOpHash: response?.result, apiKey: request.query.apiKey } @@ -143,16 +141,13 @@ export class BundlerApiInterceptor implements NestInterceptor { throw new InternalServerErrorException('UserOp param is missing') } - if (bundlerProvider === BundlerProvider.PIMLICO) { - return { - ...param, - ...base, - initCode: param.initCode ?? '0x', - sponsorId: param.paymaster ? BundlerProvider.PIMLICO : undefined, - paymasterAndData: param.paymasterData, - paymasterData: undefined - } + return { + ...param, + ...base, + initCode: param.initCode ?? '0x', + sponsorId: param.paymaster ? BundlerProvider.PIMLICO : undefined, + paymasterAndData: param.paymasterData, + paymasterData: undefined } - return { ...param, ...base } } } diff --git a/apps/charge-api-service/src/bundler-api/config/configuration.ts b/apps/charge-api-service/src/bundler-api/config/configuration.ts index d606c459..4ce76a06 100644 --- a/apps/charge-api-service/src/bundler-api/config/configuration.ts +++ b/apps/charge-api-service/src/bundler-api/config/configuration.ts @@ -1,20 +1,10 @@ export default () => ({ bundler: { - etherspot: { - production: { - url: process.env.BUNDLER_API_PRD_URL - }, - sandbox: { - url: process.env.BUNDLER_API_SANDBOX_URL - } + production: { + url: process.env.PIMLICO_API_PRD_URL }, - pimlico: { - production: { - url: process.env.PIMLICO_API_PRD_URL - }, - sandbox: { - url: process.env.PIMLICO_API_SANDBOX_URL - } + sandbox: { + url: process.env.PIMLICO_API_SANDBOX_URL } } }) diff --git a/apps/charge-api-service/src/bundler-api/interfaces/bundler.interface.ts b/apps/charge-api-service/src/bundler-api/interfaces/bundler.interface.ts index 3604e4ee..0d15cc1a 100644 --- a/apps/charge-api-service/src/bundler-api/interfaces/bundler.interface.ts +++ b/apps/charge-api-service/src/bundler-api/interfaces/bundler.interface.ts @@ -1,4 +1,3 @@ export enum BundlerProvider { - ETHERSPOT = 'etherspot', PIMLICO = 'pimlico' } diff --git a/apps/charge-api-service/src/paymaster-api/paymaster-api.service.ts b/apps/charge-api-service/src/paymaster-api/paymaster-api.service.ts index f9d4f95e..33dd2b4a 100644 --- a/apps/charge-api-service/src/paymaster-api/paymaster-api.service.ts +++ b/apps/charge-api-service/src/paymaster-api/paymaster-api.service.ts @@ -211,7 +211,7 @@ export class PaymasterApiService { private prepareUrl (environment) { if (isEmpty(environment)) throw new InternalServerErrorException('Bundler environment is missing') - const config = this.configService.get(`bundler.etherspot.${environment}`) + const config = this.configService.get(`bundler.${environment}`) if (config.url) { return config.url } else { diff --git a/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-legacy.service.spec.ts b/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-legacy.service.spec.ts index 2c63667a..65dfe147 100644 --- a/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-legacy.service.spec.ts +++ b/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-legacy.service.spec.ts @@ -10,702 +10,702 @@ import { HttpStatus } from '@nestjs/common' import * as helperFunctions from '@app/smart-wallets-service/common/utils/helper-functions' describe('SmartWalletsLegacyService - relay', () => { - let service: SmartWalletsLegacyService - let centClient: jest.Mocked - let relayAPIService: jest.Mocked - let configService: jest.Mocked - - const mockRelayDto: any = { - ownerAddress: '0x1234567890123456789012345678901234567890', - walletAddress: '0x0987654321098765432109876543210987654321', - data: '0xabcdef', - nonce: '1', - methodName: 'execute', - signature: '0x1234', - walletModule: '0xmoduleaddress' + let service: SmartWalletsLegacyService + let centClient: jest.Mocked + let relayAPIService: jest.Mocked + let configService: jest.Mocked + + const mockRelayDto: any = { + ownerAddress: '0x1234567890123456789012345678901234567890', + walletAddress: '0x0987654321098765432109876543210987654321', + data: '0xabcdef', + nonce: '1', + methodName: 'execute', + signature: '0x1234', + walletModule: '0xmoduleaddress' + } + + const mockTransactionId = 'test-transaction-id-123' + const mockWsUrl = 'wss://ws.test.io/connection/websocket' + + beforeEach(async () => { + centClient = { + subscribe: jest.fn().mockResolvedValue(undefined), + unsubscribe: jest.fn().mockResolvedValue(undefined), + publish: jest.fn().mockResolvedValue(undefined) + } as any + + relayAPIService = { + relay: jest.fn().mockResolvedValue(undefined), + createWallet: jest.fn().mockResolvedValue(undefined) + } as any + + configService = { + get: jest.fn((key: string) => { + if (key === 'wsUrl') return mockWsUrl + if (key === 'sharedAddresses') return {} + return null + }) + } as any + + const mockModel = { + findOne: jest.fn(), + create: jest.fn(), + findOneAndUpdate: jest.fn() } - const mockTransactionId = 'test-transaction-id-123' - const mockWsUrl = 'wss://ws.test.io/connection/websocket' - - beforeEach(async () => { - centClient = { - subscribe: jest.fn().mockResolvedValue(undefined), - unsubscribe: jest.fn().mockResolvedValue(undefined), - publish: jest.fn().mockResolvedValue(undefined) - } as any - - relayAPIService = { - relay: jest.fn().mockResolvedValue(undefined), - createWallet: jest.fn().mockResolvedValue(undefined) - } as any - - configService = { - get: jest.fn((key: string) => { - if (key === 'wsUrl') return mockWsUrl - if (key === 'sharedAddresses') return {} - return null - }) - } as any - - const mockModel = { - findOne: jest.fn(), - create: jest.fn(), - findOneAndUpdate: jest.fn() + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SmartWalletsLegacyService, + { + provide: JwtService, + useValue: { sign: jest.fn() } + }, + { + provide: ConfigService, + useValue: configService + }, + { + provide: RelayAPIService, + useValue: relayAPIService + }, + { + provide: CentClient, + useValue: centClient + }, + { + provide: smartWalletString, + useValue: mockModel } + ] + }).compile() + + service = module.get(SmartWalletsLegacyService) + jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(mockTransactionId) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('Happy Path', () => { + it('should successfully relay transaction with immediate response', async () => { + const result = await service.relay(mockRelayDto) + + expect(result).toEqual({ + connectionUrl: mockWsUrl, + transactionId: mockTransactionId + }) + expect(helperFunctions.generateTransactionId).toHaveBeenCalledWith(mockRelayDto.data) + }) - const module: TestingModule = await Test.createTestingModule({ - providers: [ - SmartWalletsLegacyService, - { - provide: JwtService, - useValue: { sign: jest.fn() } - }, - { - provide: ConfigService, - useValue: configService - }, - { - provide: RelayAPIService, - useValue: relayAPIService - }, - { - provide: CentClient, - useValue: centClient - }, - { - provide: smartWalletString, - useValue: mockModel - } - ] - }).compile() - - service = module.get(SmartWalletsLegacyService) - jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(mockTransactionId) - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - describe('Happy Path', () => { - it('should successfully relay transaction with immediate response', async () => { - const result = await service.relay(mockRelayDto) - - expect(result).toEqual({ - connectionUrl: mockWsUrl, - transactionId: mockTransactionId - }) - expect(helperFunctions.generateTransactionId).toHaveBeenCalledWith(mockRelayDto.data) - }) - - it('should call centClient.subscribe with correct parameters', async () => { - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) + it('should call centClient.subscribe with correct parameters', async () => { + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) - expect(centClient.subscribe).toHaveBeenCalledWith({ - channel: `transaction:#${mockTransactionId}`, - user: mockRelayDto.ownerAddress - }) - }) + expect(centClient.subscribe).toHaveBeenCalledWith({ + channel: `transaction:#${mockTransactionId}`, + user: mockRelayDto.ownerAddress + }) + }) - it('should call relayAPIService.relay with correct parameters', async () => { - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) + it('should call relayAPIService.relay with correct parameters', async () => { + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) - expect(relayAPIService.relay).toHaveBeenCalledWith({ - v2: true, - transactionId: mockTransactionId, - ...mockRelayDto - }) - }) + expect(relayAPIService.relay).toHaveBeenCalledWith({ + v2: true, + transactionId: mockTransactionId, + ...mockRelayDto + }) + }) - it('should return immediately without waiting for centClient or relayAPI', async () => { - centClient.subscribe.mockImplementation(() => - new Promise(resolve => setTimeout(resolve, 1000)) - ) - relayAPIService.relay.mockImplementation(() => - new Promise(resolve => setTimeout(resolve, 1000)) - ) + it('should return immediately without waiting for centClient or relayAPI', async () => { + centClient.subscribe.mockImplementation(() => + new Promise(resolve => setTimeout(resolve, 1000)) + ) + relayAPIService.relay.mockImplementation(() => + new Promise(resolve => setTimeout(resolve, 1000)) + ) - const startTime = Date.now() - const result = await service.relay(mockRelayDto) - const duration = Date.now() - startTime + const startTime = Date.now() + const result = await service.relay(mockRelayDto) + const duration = Date.now() - startTime - expect(duration).toBeLessThan(100) - expect(result.transactionId).toBe(mockTransactionId) - }) + expect(duration).toBeLessThan(100) + expect(result.transactionId).toBe(mockTransactionId) }) + }) - describe('Resilience - Centrifugo Failures', () => { - it('should still return success when centClient.subscribe fails', async () => { - centClient.subscribe.mockRejectedValue(new Error('Centrifugo is down')) + describe('Resilience - Centrifugo Failures', () => { + it('should still return success when centClient.subscribe fails', async () => { + centClient.subscribe.mockRejectedValue(new Error('Centrifugo is down')) - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result).toEqual({ - connectionUrl: mockWsUrl, - transactionId: mockTransactionId - }) - }) + expect(result).toEqual({ + connectionUrl: mockWsUrl, + transactionId: mockTransactionId + }) + }) - it('should log error when centClient.subscribe fails', async () => { - const loggerErrorSpy = jest.spyOn(service['logger'], 'error') - centClient.subscribe.mockRejectedValue(new Error('Connection timeout')) + it('should log error when centClient.subscribe fails', async () => { + const loggerErrorSpy = jest.spyOn(service.logger, 'error') + centClient.subscribe.mockRejectedValue(new Error('Connection timeout')) - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) - expect(loggerErrorSpy).toHaveBeenCalledWith( - expect.stringContaining('Centrifugo subscription failed') - ) - }) + expect(loggerErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('Centrifugo subscription failed') + ) + }) - it('should throw RpcException when centClient throws synchronous error', async () => { - centClient.subscribe.mockImplementation(() => { - throw new Error('Immediate failure') - }) + it('should throw RpcException when centClient throws synchronous error', async () => { + centClient.subscribe.mockImplementation(() => { + throw new Error('Immediate failure') + }) - await expect(service.relay(mockRelayDto)).rejects.toThrow(RpcException) - }) + await expect(service.relay(mockRelayDto)).rejects.toThrow(RpcException) }) + }) - describe('Resilience - Relay API Failures', () => { - it('should still return success when relayAPIService.relay fails', async () => { - relayAPIService.relay.mockRejectedValue(new Error('Relay API is down')) + describe('Resilience - Relay API Failures', () => { + it('should still return success when relayAPIService.relay fails', async () => { + relayAPIService.relay.mockRejectedValue(new Error('Relay API is down')) - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result).toEqual({ - connectionUrl: mockWsUrl, - transactionId: mockTransactionId - }) - }) + expect(result).toEqual({ + connectionUrl: mockWsUrl, + transactionId: mockTransactionId + }) + }) - it('should log error when relayAPIService.relay fails', async () => { - const loggerErrorSpy = jest.spyOn(service['logger'], 'error') - relayAPIService.relay.mockRejectedValue(new Error('500 Internal Server Error')) + it('should log error when relayAPIService.relay fails', async () => { + const loggerErrorSpy = jest.spyOn(service.logger, 'error') + relayAPIService.relay.mockRejectedValue(new Error('500 Internal Server Error')) - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) - expect(loggerErrorSpy).toHaveBeenCalledWith( - expect.stringContaining('Relay API call failed') - ) - }) + expect(loggerErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('Relay API call failed') + ) + }) - it('should handle 5xx errors from relay API gracefully', async () => { - relayAPIService.relay.mockRejectedValue({ - response: { status: 503, data: 'Service Unavailable' } - }) + it('should handle 5xx errors from relay API gracefully', async () => { + relayAPIService.relay.mockRejectedValue({ + response: { status: 503, data: 'Service Unavailable' } + }) - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result.transactionId).toBe(mockTransactionId) - }) + expect(result.transactionId).toBe(mockTransactionId) }) + }) - describe('Resilience - Multiple Failures', () => { - it('should still return success when both centClient and relayAPI fail', async () => { - centClient.subscribe.mockRejectedValue(new Error('Centrifugo down')) - relayAPIService.relay.mockRejectedValue(new Error('Relay API down')) + describe('Resilience - Multiple Failures', () => { + it('should still return success when both centClient and relayAPI fail', async () => { + centClient.subscribe.mockRejectedValue(new Error('Centrifugo down')) + relayAPIService.relay.mockRejectedValue(new Error('Relay API down')) - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result).toEqual({ - connectionUrl: mockWsUrl, - transactionId: mockTransactionId - }) - }) + expect(result).toEqual({ + connectionUrl: mockWsUrl, + transactionId: mockTransactionId + }) + }) - it('should log both errors when both services fail', async () => { - const loggerErrorSpy = jest.spyOn(service['logger'], 'error') - centClient.subscribe.mockRejectedValue(new Error('Centrifugo error')) - relayAPIService.relay.mockRejectedValue(new Error('Relay error')) - - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) - - expect(loggerErrorSpy).toHaveBeenCalledTimes(2) - expect(loggerErrorSpy).toHaveBeenCalledWith( - expect.stringContaining('Centrifugo subscription failed') - ) - expect(loggerErrorSpy).toHaveBeenCalledWith( - expect.stringContaining('Relay API call failed') - ) - }) + it('should log both errors when both services fail', async () => { + const loggerErrorSpy = jest.spyOn(service.logger, 'error') + centClient.subscribe.mockRejectedValue(new Error('Centrifugo error')) + relayAPIService.relay.mockRejectedValue(new Error('Relay error')) + + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) + + expect(loggerErrorSpy).toHaveBeenCalledTimes(2) + expect(loggerErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('Centrifugo subscription failed') + ) + expect(loggerErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('Relay API call failed') + ) }) + }) - describe('Error Handling - Critical Failures', () => { - it('should throw RpcException when generateTransactionId fails', async () => { - jest.spyOn(helperFunctions, 'generateTransactionId').mockImplementation(() => { - throw new Error('Invalid data format') - }) + describe('Error Handling - Critical Failures', () => { + it('should throw RpcException when generateTransactionId fails', async () => { + jest.spyOn(helperFunctions, 'generateTransactionId').mockImplementation(() => { + throw new Error('Invalid data format') + }) - await expect(service.relay(mockRelayDto)).rejects.toThrow(RpcException) - }) + await expect(service.relay(mockRelayDto)).rejects.toThrow(RpcException) + }) - it('should throw RpcException with correct status and message', async () => { - jest.spyOn(helperFunctions, 'generateTransactionId').mockImplementation(() => { - throw new Error('Transaction ID generation failed') - }) - - try { - await service.relay(mockRelayDto) - fail('Should have thrown RpcException') - } catch (error) { - expect(error).toBeInstanceOf(RpcException) - expect(error.getError()).toEqual({ - error: 'Transaction ID generation failed', - status: HttpStatus.BAD_REQUEST - }) - } - }) + it('should throw RpcException with correct status and message', async () => { + jest.spyOn(helperFunctions, 'generateTransactionId').mockImplementation(() => { + throw new Error('Transaction ID generation failed') + }) + + try { + await service.relay(mockRelayDto) + fail('Should have thrown RpcException') + } catch (error) { + expect(error).toBeInstanceOf(RpcException) + expect(error.getError()).toEqual({ + error: 'Transaction ID generation failed', + status: HttpStatus.BAD_REQUEST + }) + } + }) - it('should log error when critical failure occurs', async () => { - const loggerErrorSpy = jest.spyOn(service['logger'], 'error') - jest.spyOn(helperFunctions, 'generateTransactionId').mockImplementation(() => { - throw new Error('Critical error') - }) - - try { - await service.relay(mockRelayDto) - } catch (error) { - // Expected - } - - expect(loggerErrorSpy).toHaveBeenCalledWith( - expect.stringContaining('An error occurred during relay execution') - ) - }) + it('should log error when critical failure occurs', async () => { + const loggerErrorSpy = jest.spyOn(service.logger, 'error') + jest.spyOn(helperFunctions, 'generateTransactionId').mockImplementation(() => { + throw new Error('Critical error') + }) + + try { + await service.relay(mockRelayDto) + } catch (error) { + // Expected + } + + expect(loggerErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('An error occurred during relay execution') + ) }) + }) - describe('Transaction ID Generation', () => { - it('should generate transaction ID from relay data', async () => { - await service.relay(mockRelayDto) + describe('Transaction ID Generation', () => { + it('should generate transaction ID from relay data', async () => { + await service.relay(mockRelayDto) - expect(helperFunctions.generateTransactionId).toHaveBeenCalledWith(mockRelayDto.data) - }) + expect(helperFunctions.generateTransactionId).toHaveBeenCalledWith(mockRelayDto.data) + }) - it('should use generated transaction ID in centClient subscription', async () => { - const customTransactionId = 'custom-tx-id-456' - jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(customTransactionId) + it('should use generated transaction ID in centClient subscription', async () => { + const customTransactionId = 'custom-tx-id-456' + jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(customTransactionId) - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) - expect(centClient.subscribe).toHaveBeenCalledWith({ - channel: `transaction:#${customTransactionId}`, - user: mockRelayDto.ownerAddress - }) - }) + expect(centClient.subscribe).toHaveBeenCalledWith({ + channel: `transaction:#${customTransactionId}`, + user: mockRelayDto.ownerAddress + }) + }) - it('should use generated transaction ID in relay API call', async () => { - const customTransactionId = 'custom-tx-id-789' - jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(customTransactionId) + it('should use generated transaction ID in relay API call', async () => { + const customTransactionId = 'custom-tx-id-789' + jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(customTransactionId) - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) - expect(relayAPIService.relay).toHaveBeenCalledWith( - expect.objectContaining({ - transactionId: customTransactionId - }) - ) + expect(relayAPIService.relay).toHaveBeenCalledWith( + expect.objectContaining({ + transactionId: customTransactionId }) + ) + }) - it('should return generated transaction ID to client', async () => { - const customTransactionId = 'custom-tx-id-abc' - jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(customTransactionId) + it('should return generated transaction ID to client', async () => { + const customTransactionId = 'custom-tx-id-abc' + jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(customTransactionId) - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result.transactionId).toBe(customTransactionId) - }) + expect(result.transactionId).toBe(customTransactionId) }) + }) - describe('Connection URL', () => { - it('should return correct WebSocket URL from config', async () => { - const result = await service.relay(mockRelayDto) + describe('Connection URL', () => { + it('should return correct WebSocket URL from config', async () => { + const result = await service.relay(mockRelayDto) - expect(result.connectionUrl).toBe(mockWsUrl) - expect(configService.get).toHaveBeenCalledWith('wsUrl') - }) + expect(result.connectionUrl).toBe(mockWsUrl) + expect(configService.get).toHaveBeenCalledWith('wsUrl') + }) - it('should handle different WebSocket URLs', async () => { - const customWsUrl = 'wss://custom.fuse.io/ws' - configService.get.mockReturnValue(customWsUrl) + it('should handle different WebSocket URLs', async () => { + const customWsUrl = 'wss://custom.fuse.io/ws' + configService.get.mockReturnValue(customWsUrl) - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result.connectionUrl).toBe(customWsUrl) - }) + expect(result.connectionUrl).toBe(customWsUrl) }) + }) - describe('Fire-and-Forget Behavior', () => { - it('should not block on slow centClient operations', async () => { - let subscribeResolved = false - centClient.subscribe.mockImplementation(() => - new Promise(resolve => { - setTimeout(() => { - subscribeResolved = true - resolve(undefined) - }, 2000) - }) - ) - - const result = await service.relay(mockRelayDto) - - expect(result).toBeDefined() - expect(subscribeResolved).toBe(false) + describe('Fire-and-Forget Behavior', () => { + it('should not block on slow centClient operations', async () => { + let subscribeResolved = false + centClient.subscribe.mockImplementation(() => + new Promise(resolve => { + setTimeout(() => { + subscribeResolved = true + resolve(undefined) + }, 2000) }) + ) - it('should not block on slow relay API operations', async () => { - let relayResolved = false - relayAPIService.relay.mockImplementation(() => - new Promise(resolve => { - setTimeout(() => { - relayResolved = true - resolve(undefined) - }, 2000) - }) - ) - - const result = await service.relay(mockRelayDto) - - expect(result).toBeDefined() - expect(relayResolved).toBe(false) - }) + const result = await service.relay(mockRelayDto) + + expect(result).toBeDefined() + expect(subscribeResolved).toBe(false) }) - describe('Data Integrity', () => { - it('should pass all relay DTO fields to relay API', async () => { - const fullRelayDto = { - ...mockRelayDto, - gasPrice: 20000000000, - gasLimit: 21000 - } - - await service.relay(fullRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) - - expect(relayAPIService.relay).toHaveBeenCalledWith( - expect.objectContaining({ - ownerAddress: fullRelayDto.ownerAddress, - data: fullRelayDto.data, - walletAddress: fullRelayDto.walletAddress, - nonce: fullRelayDto.nonce, - gasPrice: fullRelayDto.gasPrice, - gasLimit: fullRelayDto.gasLimit, - methodName: fullRelayDto.methodName, - signature: fullRelayDto.signature, - walletModule: fullRelayDto.walletModule - }) - ) + it('should not block on slow relay API operations', async () => { + let relayResolved = false + relayAPIService.relay.mockImplementation(() => + new Promise(resolve => { + setTimeout(() => { + relayResolved = true + resolve(undefined) + }, 2000) }) + ) - it('should include v2 flag in relay API call', async () => { - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) + const result = await service.relay(mockRelayDto) - expect(relayAPIService.relay).toHaveBeenCalledWith( - expect.objectContaining({ - v2: true - }) - ) - }) + expect(result).toBeDefined() + expect(relayResolved).toBe(false) + }) + }) + + describe('Data Integrity', () => { + it('should pass all relay DTO fields to relay API', async () => { + const fullRelayDto = { + ...mockRelayDto, + gasPrice: 20000000000, + gasLimit: 21000 + } + + await service.relay(fullRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) + + expect(relayAPIService.relay).toHaveBeenCalledWith( + expect.objectContaining({ + ownerAddress: fullRelayDto.ownerAddress, + data: fullRelayDto.data, + walletAddress: fullRelayDto.walletAddress, + nonce: fullRelayDto.nonce, + gasPrice: fullRelayDto.gasPrice, + gasLimit: fullRelayDto.gasLimit, + methodName: fullRelayDto.methodName, + signature: fullRelayDto.signature, + walletModule: fullRelayDto.walletModule + }) + ) }) - describe('Production - Config Validation', () => { - it('should throw error when wsUrl is null', async () => { - configService.get.mockReturnValue(null) + it('should include v2 flag in relay API call', async () => { + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) - await expect(service.relay(mockRelayDto)).rejects.toThrow() + expect(relayAPIService.relay).toHaveBeenCalledWith( + expect.objectContaining({ + v2: true }) + ) + }) + }) - it('should throw error when wsUrl is undefined', async () => { - configService.get.mockReturnValue(undefined) + describe('Production - Config Validation', () => { + it('should throw error when wsUrl is null', async () => { + configService.get.mockReturnValue(null) - await expect(service.relay(mockRelayDto)).rejects.toThrow() - }) + await expect(service.relay(mockRelayDto)).rejects.toThrow() + }) - it('should throw error when wsUrl is empty string', async () => { - configService.get.mockReturnValue('') + it('should throw error when wsUrl is undefined', async () => { + configService.get.mockReturnValue(undefined) - await expect(service.relay(mockRelayDto)).rejects.toThrow(RpcException) - }) + await expect(service.relay(mockRelayDto)).rejects.toThrow() }) - describe('Production - Invalid Input', () => { - it('should handle null relayDto.data gracefully', async () => { - const invalidDto = { ...mockRelayDto, data: null } + it('should throw error when wsUrl is empty string', async () => { + configService.get.mockReturnValue('') - const result = await service.relay(invalidDto) + await expect(service.relay(mockRelayDto)).rejects.toThrow(RpcException) + }) + }) - expect(result).toBeDefined() - expect(result.connectionUrl).toBe(mockWsUrl) - }) + describe('Production - Invalid Input', () => { + it('should handle null relayDto.data gracefully', async () => { + const invalidDto = { ...mockRelayDto, data: null } - it('should handle undefined relayDto.ownerAddress', async () => { - const invalidDto = { ...mockRelayDto, ownerAddress: undefined } + const result = await service.relay(invalidDto) - const result = await service.relay(invalidDto) + expect(result).toBeDefined() + expect(result.connectionUrl).toBe(mockWsUrl) + }) - expect(result).toBeDefined() - }) + it('should handle undefined relayDto.ownerAddress', async () => { + const invalidDto = { ...mockRelayDto, ownerAddress: undefined } - it('should handle empty string data', async () => { - const invalidDto = { ...mockRelayDto, data: '' } + const result = await service.relay(invalidDto) - const result = await service.relay(invalidDto) + expect(result).toBeDefined() + }) - expect(result.transactionId).toBe(mockTransactionId) - }) + it('should handle empty string data', async () => { + const invalidDto = { ...mockRelayDto, data: '' } - it('should handle very large data payload', async () => { - const largeData = '0x' + 'a'.repeat(100000) - const largeDto = { ...mockRelayDto, data: largeData } + const result = await service.relay(invalidDto) - const result = await service.relay(largeDto) + expect(result.transactionId).toBe(mockTransactionId) + }) - expect(result.transactionId).toBe(mockTransactionId) - }) + it('should handle very large data payload', async () => { + const largeData = '0x' + 'a'.repeat(100000) + const largeDto = { ...mockRelayDto, data: largeData } + + const result = await service.relay(largeDto) + + expect(result.transactionId).toBe(mockTransactionId) }) + }) - describe('Production - Transaction ID Edge Cases', () => { - it('should handle null transaction ID', async () => { - jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(null as any) + describe('Production - Transaction ID Edge Cases', () => { + it('should handle null transaction ID', async () => { + jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(null as any) - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result.transactionId).toBeNull() - }) + expect(result.transactionId).toBeNull() + }) - it('should handle empty string transaction ID', async () => { - jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue('') + it('should handle empty string transaction ID', async () => { + jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue('') - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result.transactionId).toBe('') - expect(result.connectionUrl).toBe(mockWsUrl) - }) + expect(result.transactionId).toBe('') + expect(result.connectionUrl).toBe(mockWsUrl) + }) - it('should handle special characters in transaction ID', async () => { - const specialTxId = 'tx:@#$%^&*()123' - jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(specialTxId) + it('should handle special characters in transaction ID', async () => { + const specialTxId = 'tx:@#$%^&*()123' + jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(specialTxId) - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) - expect(centClient.subscribe).toHaveBeenCalledWith({ - channel: `transaction:#${specialTxId}`, - user: mockRelayDto.ownerAddress - }) - }) + expect(centClient.subscribe).toHaveBeenCalledWith({ + channel: `transaction:#${specialTxId}`, + user: mockRelayDto.ownerAddress + }) + }) - it('should handle very long transaction ID', async () => { - const longTxId = 'a'.repeat(1000) - jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(longTxId) + it('should handle very long transaction ID', async () => { + const longTxId = 'a'.repeat(1000) + jest.spyOn(helperFunctions, 'generateTransactionId').mockReturnValue(longTxId) - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result.transactionId).toBe(longTxId) - }) + expect(result.transactionId).toBe(longTxId) }) + }) - describe('Production - Promise Behavior', () => { - it('should handle centClient not returning a promise', async () => { - centClient.subscribe = jest.fn() as any + describe('Production - Promise Behavior', () => { + it('should handle centClient not returning a promise', async () => { + centClient.subscribe = jest.fn() as any - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result.transactionId).toBe(mockTransactionId) - }) + expect(result.transactionId).toBe(mockTransactionId) + }) - it('should handle relayAPI not returning a promise', async () => { - relayAPIService.relay = jest.fn() as any + it('should handle relayAPI not returning a promise', async () => { + relayAPIService.relay = jest.fn() as any - const result = await service.relay(mockRelayDto) + const result = await service.relay(mockRelayDto) - expect(result.transactionId).toBe(mockTransactionId) - }) + expect(result.transactionId).toBe(mockTransactionId) + }) - it('should handle promises that never resolve', async () => { - centClient.subscribe.mockImplementation(() => - new Promise(() => { }) // Never resolves - ) + it('should handle promises that never resolve', async () => { + centClient.subscribe.mockImplementation(() => + new Promise(() => { }) // Never resolves + ) - const startTime = Date.now() - const result = await service.relay(mockRelayDto) - const duration = Date.now() - startTime + const startTime = Date.now() + const result = await service.relay(mockRelayDto) + const duration = Date.now() - startTime - expect(duration).toBeLessThan(100) - expect(result.transactionId).toBe(mockTransactionId) - }) + expect(duration).toBeLessThan(100) + expect(result.transactionId).toBe(mockTransactionId) + }) - it('should handle error without message property', async () => { - const loggerErrorSpy = jest.spyOn(service['logger'], 'error') - centClient.subscribe.mockRejectedValue({ code: 500 } as any) + it('should handle error without message property', async () => { + const loggerErrorSpy = jest.spyOn(service.logger, 'error') + centClient.subscribe.mockRejectedValue({ code: 500 } as any) - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) - expect(loggerErrorSpy).toHaveBeenCalled() - }) + expect(loggerErrorSpy).toHaveBeenCalled() + }) + + it('should handle circular reference in error', async () => { + const loggerErrorSpy = jest.spyOn(service.logger, 'error') + const circularError: any = { message: 'Circular' } + circularError.self = circularError - it('should handle circular reference in error', async () => { - const loggerErrorSpy = jest.spyOn(service['logger'], 'error') - const circularError: any = { message: 'Circular' } - circularError.self = circularError + centClient.subscribe.mockRejectedValue(circularError) - centClient.subscribe.mockRejectedValue(circularError) + await service.relay(mockRelayDto) + await new Promise(resolve => setTimeout(resolve, 10)) - await service.relay(mockRelayDto) - await new Promise(resolve => setTimeout(resolve, 10)) + expect(loggerErrorSpy).toHaveBeenCalled() + }) + }) - expect(loggerErrorSpy).toHaveBeenCalled() + describe('Production - Concurrent Calls', () => { + it('should handle multiple concurrent relay calls', async () => { + const promises = Array.from({ length: 10 }, (_, i) => + service.relay({ + ...mockRelayDto, + data: `0xdata${i}` }) + ) + + const results = await Promise.all(promises) + + expect(results).toHaveLength(10) + results.forEach(result => { + expect(result.transactionId).toBe(mockTransactionId) + expect(result.connectionUrl).toBe(mockWsUrl) + }) }) - describe('Production - Concurrent Calls', () => { - it('should handle multiple concurrent relay calls', async () => { - const promises = Array.from({ length: 10 }, (_, i) => - service.relay({ - ...mockRelayDto, - data: `0xdata${i}` - }) - ) + it('should handle concurrent calls with mixed success/failure', async () => { + let callCount = 0 + centClient.subscribe.mockImplementation(() => { + callCount++ + if (callCount % 2 === 0) { + return Promise.reject(new Error('Even call failed')) + } + return Promise.resolve(undefined) + }) - const results = await Promise.all(promises) + const promises = Array.from({ length: 10 }, () => + service.relay(mockRelayDto) + ) - expect(results).toHaveLength(10) - results.forEach(result => { - expect(result.transactionId).toBe(mockTransactionId) - expect(result.connectionUrl).toBe(mockWsUrl) - }) - }) + const results = await Promise.all(promises) - it('should handle concurrent calls with mixed success/failure', async () => { - let callCount = 0 - centClient.subscribe.mockImplementation(() => { - callCount++ - if (callCount % 2 === 0) { - return Promise.reject(new Error('Even call failed')) - } - return Promise.resolve(undefined) - }) - - const promises = Array.from({ length: 10 }, () => - service.relay(mockRelayDto) - ) - - const results = await Promise.all(promises) - - expect(results).toHaveLength(10) - results.forEach(result => { - expect(result.transactionId).toBe(mockTransactionId) - }) - }) + expect(results).toHaveLength(10) + results.forEach(result => { + expect(result.transactionId).toBe(mockTransactionId) + }) }) + }) - describe('Production - Memory Leak Prevention', () => { - it('should not leak memory with many failed promises', async () => { - centClient.subscribe.mockRejectedValue(new Error('Always fails')) - relayAPIService.relay.mockRejectedValue(new Error('Always fails')) + describe('Production - Memory Leak Prevention', () => { + it('should not leak memory with many failed promises', async () => { + centClient.subscribe.mockRejectedValue(new Error('Always fails')) + relayAPIService.relay.mockRejectedValue(new Error('Always fails')) - const promises = Array.from({ length: 100 }, () => - service.relay(mockRelayDto) - ) + const promises = Array.from({ length: 100 }, () => + service.relay(mockRelayDto) + ) - const results = await Promise.all(promises) + const results = await Promise.all(promises) - expect(results).toHaveLength(100) - }) + expect(results).toHaveLength(100) + }) - it('should handle rapid fire-and-forget without blocking', async () => { - const startTime = Date.now() + it('should handle rapid fire-and-forget without blocking', async () => { + const startTime = Date.now() - const promises = Array.from({ length: 50 }, () => - service.relay(mockRelayDto) - ) + const promises = Array.from({ length: 50 }, () => + service.relay(mockRelayDto) + ) - await Promise.all(promises) + await Promise.all(promises) - const duration = Date.now() - startTime + const duration = Date.now() - startTime - expect(duration).toBeLessThan(500) - }) + expect(duration).toBeLessThan(500) }) - - describe('Production - Error Message Edge Cases', () => { - it('should handle error with undefined message', async () => { - jest.spyOn(helperFunctions, 'generateTransactionId').mockImplementation(() => { - const err: any = new Error() - delete err.message - throw err - }) - - try { - await service.relay(mockRelayDto) - fail('Should have thrown') - } catch (error) { - expect(error).toBeInstanceOf(RpcException) - expect(error.getError().error).toBe('Relay execution error') - } - }) + }) + + describe('Production - Error Message Edge Cases', () => { + it('should handle error with undefined message', async () => { + jest.spyOn(helperFunctions, 'generateTransactionId').mockImplementation(() => { + const err: any = new Error() + delete err.message + throw err + }) + + try { + await service.relay(mockRelayDto) + fail('Should have thrown') + } catch (error) { + expect(error).toBeInstanceOf(RpcException) + expect(error.getError().error).toBe('Relay execution error') + } + }) + }) + + describe('Production - Service Integration', () => { + it('should pass exact relay DTO to relay API without modification', async () => { + const complexDto = { + ...mockRelayDto, + customField: 'should be passed', + nestedObject: { a: 1, b: 2 } + } + + await service.relay(complexDto) + await new Promise(resolve => setTimeout(resolve, 10)) + + expect(relayAPIService.relay).toHaveBeenCalledWith( + expect.objectContaining({ + v2: true, + transactionId: mockTransactionId, + customField: 'should be passed', + nestedObject: { a: 1, b: 2 } + }) + ) }) - describe('Production - Service Integration', () => { - it('should pass exact relay DTO to relay API without modification', async () => { - const complexDto = { - ...mockRelayDto, - customField: 'should be passed', - nestedObject: { a: 1, b: 2 } - } - - await service.relay(complexDto) - await new Promise(resolve => setTimeout(resolve, 10)) - - expect(relayAPIService.relay).toHaveBeenCalledWith( - expect.objectContaining({ - v2: true, - transactionId: mockTransactionId, - customField: 'should be passed', - nestedObject: { a: 1, b: 2 } - }) - ) - }) - - it('should override v2 flag even if relayDto has v2: false', async () => { - const dtoWithV2False = { ...mockRelayDto, v2: false } + it('should override v2 flag even if relayDto has v2: false', async () => { + const dtoWithV2False = { ...mockRelayDto, v2: false } - await service.relay(dtoWithV2False) - await new Promise(resolve => setTimeout(resolve, 10)) + await service.relay(dtoWithV2False) + await new Promise(resolve => setTimeout(resolve, 10)) - expect(relayAPIService.relay).toHaveBeenCalledWith( - expect.objectContaining({ - v2: true - }) - ) + expect(relayAPIService.relay).toHaveBeenCalledWith( + expect.objectContaining({ + v2: true }) + ) }) + }) - describe('Production - State Consistency', () => { - it('should maintain consistent state across multiple calls', async () => { - const call1 = await service.relay(mockRelayDto) - const call2 = await service.relay(mockRelayDto) - const call3 = await service.relay(mockRelayDto) + describe('Production - State Consistency', () => { + it('should maintain consistent state across multiple calls', async () => { + const call1 = await service.relay(mockRelayDto) + const call2 = await service.relay(mockRelayDto) + const call3 = await service.relay(mockRelayDto) - expect(call1.connectionUrl).toBe(call2.connectionUrl) - expect(call2.connectionUrl).toBe(call3.connectionUrl) - }) + expect(call1.connectionUrl).toBe(call2.connectionUrl) + expect(call2.connectionUrl).toBe(call3.connectionUrl) + }) - it('should not mutate input relayDto', async () => { - const dtoSnapshot = JSON.parse(JSON.stringify(mockRelayDto)) + it('should not mutate input relayDto', async () => { + const dtoSnapshot = JSON.parse(JSON.stringify(mockRelayDto)) - await service.relay(mockRelayDto) + await service.relay(mockRelayDto) - expect(mockRelayDto).toEqual(dtoSnapshot) - }) + expect(mockRelayDto).toEqual(dtoSnapshot) }) + }) }) diff --git a/docker-compose.yml b/docker-compose.yml index a8194b84..1ea5e27d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,10 +19,8 @@ services: - APPS_TCP_PORT - PAYMASTER_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0 - ENTRYPOINT_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0 - - ETHERSPOT_WALLET_FACTORY_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0 - PAYMASTER_SANDBOX_CONTRACT_ADDRESS_V_0_1_0 - ENTRYPOINT_SANDBOX_CONTRACT_ADDRESS_V_0_1_0 - - ETHERSPOT_WALLET_FACTORY_SANDBOX_CONTRACT_ADDRESS_V_0_1_0 - PAYMASTER_FUNDER_PRIVATE_KEY - PAYMASTER_FUNDER_WEBHOOK_ID - SMART_WALLETS_HOST @@ -80,8 +78,6 @@ services: - SMART_WALLETS_JWT_SECRET - EXPLORER_API_URL - EXPLORER_API_KEY - - BUNDLER_API_PRD_URL - - BUNDLER_API_SANDBOX_URL - PIMLICO_API_PRD_URL - PIMLICO_API_SANDBOX_URL - SPARK_RPC_URL diff --git a/helm/values/prod.yaml b/helm/values/prod.yaml index 47d64828..e2ad321b 100644 --- a/helm/values/prod.yaml +++ b/helm/values/prod.yaml @@ -129,8 +129,6 @@ accounts-service: PAYMASTER_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0: "0xEA1Ba4305A07cEd2bB5e42224D71aBE0BC3C3f28" ENTRYPOINT_SANDBOX_CONTRACT_ADDRESS_V_0_1_0: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" ENTRYPOINT_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" - ETHERSPOT_WALLET_FACTORY_SANDBOX_CONTRACT_ADDRESS_V_0_1_0: "0x7f6d8F107fE8551160BD5351d5F1514A6aD5d40E" - ETHERSPOT_WALLET_FACTORY_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0: "0x7f6d8F107fE8551160BD5351d5F1514A6aD5d40E" PAYMASTER_FUNDER_API_KEY: "pk_yWkBe0hnObJmMkYjq4Ggce1g" PAYMASTER_FUNDER_WEBHOOK_ID: "65c4ba66394c8d9c3e4801ff" USDC_CONTRACT_ADDRESS_MAINNET: "0x28C3d1cD466Ba22f6cae51b1a4692a831696391A" @@ -162,7 +160,6 @@ api-service: - PAYMASTER_PRODUCTION_SIGNER_PRIVATE_KEY_V_0_1_0 - PAYMASTER_SANDBOX_SIGNER_PRIVATE_KEY_V_0_1_0 - EXPLORER_API_KEY - - BUNDLER_API_PRD_URL - PIMLICO_API_PRD_URL - PIMLICO_API_SANDBOX_URL - AMPLITUDE_API_KEY @@ -273,7 +270,6 @@ api-service: SMART_WALLETS_TCP_PORT: "8881" EXPLORER_API_URL: "https://explorer.fuse.io/api" QA_MODE: "false" - BUNDLER_API_SANDBOX_URL: "https://testnet-rpc.etherspot.io/v1/123" SPARK_RPC_URL: "https://rpc.fusespark.io" LEGACY_FUSE_ADMIN_API_URL: "https://studio.fuse.io" LEGACY_FUSE_WALLET_API_URL: "https://wallet.fuse.io" diff --git a/helm/values/qa.yaml b/helm/values/qa.yaml index e6d79431..f03b1c38 100644 --- a/helm/values/qa.yaml +++ b/helm/values/qa.yaml @@ -129,8 +129,6 @@ accounts-service: PAYMASTER_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0: "0xEA1Ba4305A07cEd2bB5e42224D71aBE0BC3C3f28" ENTRYPOINT_SANDBOX_CONTRACT_ADDRESS_V_0_1_0: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" ENTRYPOINT_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" - ETHERSPOT_WALLET_FACTORY_SANDBOX_CONTRACT_ADDRESS_V_0_1_0: "0x7f6d8F107fE8551160BD5351d5F1514A6aD5d40E" - ETHERSPOT_WALLET_FACTORY_PRODUCTION_CONTRACT_ADDRESS_V_0_1_0: "0x7f6d8F107fE8551160BD5351d5F1514A6aD5d40E" PAYMASTER_FUNDER_API_KEY: "pk_u_RbFw7NQBB7ZQIFJtziLE_S" PAYMASTER_FUNDER_WEBHOOK_ID: "65bbbc63fc2f925de8e1a561" USDC_CONTRACT_ADDRESS_MAINNET: "0x28C3d1cD466Ba22f6cae51b1a4692a831696391A" @@ -162,7 +160,6 @@ api-service: - PAYMASTER_PRODUCTION_SIGNER_PRIVATE_KEY_V_0_1_0 - PAYMASTER_SANDBOX_SIGNER_PRIVATE_KEY_V_0_1_0 - EXPLORER_API_KEY - - BUNDLER_API_PRD_URL - PIMLICO_API_PRD_URL - PIMLICO_API_SANDBOX_URL - AMPLITUDE_API_KEY @@ -273,7 +270,6 @@ api-service: SMART_WALLETS_TCP_PORT: "8881" EXPLORER_API_URL: "https://explorer.fuse.io/api" QA_MODE: "true" - BUNDLER_API_SANDBOX_URL: "https://testnet-rpc.etherspot.io/v1/123" SPARK_RPC_URL: "https://rpc.fusespark.io" LEGACY_FUSE_ADMIN_API_URL: "https://studio.fuse.io" LEGACY_FUSE_WALLET_API_URL: "https://wallet.fuse.io" diff --git a/k8s/helm/Chart.yaml b/k8s/helm/Chart.yaml index 7252c28a..f5b68385 100644 --- a/k8s/helm/Chart.yaml +++ b/k8s/helm/Chart.yaml @@ -43,5 +43,3 @@ dependencies: condition: centrifugo.enabled - name: gateway-api condition: gateway-api.enabled - - name: skandha - condition: skandha.enabled diff --git a/k8s/helm/charts/accounts/README.md b/k8s/helm/charts/accounts/README.md index 45286615..3042805b 100644 --- a/k8s/helm/charts/accounts/README.md +++ b/k8s/helm/charts/accounts/README.md @@ -18,8 +18,6 @@ A Helm chart for Kubernetes related accounts component | configMap.console_dapp_url | string | `""` | Console Dapp URL | | configMap.entrypoint_production_contract_address_v_0_1_0 | string | `""` | Bundler - Entrypoint Production contract address | | configMap.entrypoint_sandbox_contract_address_v_0_1_0 | string | `""` | Bundler - Entrypoint Sandbox contract address | -| configMap.etherspot_wallet_factory_production_contract_address_v_0_1_0 | string | `""` | Etherspot Wallet Factory Production contract address | -| configMap.etherspot_wallet_factory_sandbox_contract_address_v_0_1_0 | string | `""` | Etherspot Wallet Factory Sandbox contract address | | configMap.paymaster_funder_api_key | string | `""` | Paymaster Funder API key | | configMap.paymaster_funder_webhook_id | string | `""` | Paymaster Funder Webhook ID | | configMap.paymaster_production_contract_address_v_0_1_0 | string | `""` | Bundler - Paymaster Production contract address | diff --git a/k8s/helm/charts/accounts/values.yaml b/k8s/helm/charts/accounts/values.yaml index 4b56802a..44db51a8 100644 --- a/k8s/helm/charts/accounts/values.yaml +++ b/k8s/helm/charts/accounts/values.yaml @@ -76,10 +76,6 @@ configMap: entrypoint_sandbox_contract_address_v_0_1_0: "" # -- Bundler - Entrypoint Production contract address entrypoint_production_contract_address_v_0_1_0: "" - # -- Etherspot Wallet Factory Sandbox contract address - etherspot_wallet_factory_sandbox_contract_address_v_0_1_0: "" - # -- Etherspot Wallet Factory Production contract address - etherspot_wallet_factory_production_contract_address_v_0_1_0: "" # -- Paymaster Funder API key paymaster_funder_api_key: "" # -- Paymaster Funder Webhook ID diff --git a/k8s/helm/charts/api/README.md b/k8s/helm/charts/api/README.md index 36e1e835..8a421687 100644 --- a/k8s/helm/charts/api/README.md +++ b/k8s/helm/charts/api/README.md @@ -11,7 +11,6 @@ A Helm chart for Kubernetes related api component | affinity | object | `{"zones":["a"]}` | Affinity (available region zones) | | autoscaling.hpa | object | `{"max_replicas":5}` | Horizontal Pod Autoscaler | | autoscaling.hpa.max_replicas | int | `5` | Horizontal Pod Autoscaler - Maximum number of replicas, minimal number is `replicas` value | -| configMap.bundler_api_sandbox_url | string | `""` | Bundler - API Sandbox URL | | configMap.explorer_api_url | string | `""` | BlockScout API URL | | configMap.legacy_fuse_admin_api_url | string | `""` | Legacy - Fuse admin API URL | | configMap.legacy_fuse_wallet_api_url | string | `""` | Legacy - Fuse wallet API URL | @@ -30,6 +29,6 @@ A Helm chart for Kubernetes related api component | replicas | int | `1` | Replicas | | resources.limits | object | `{"cpu":"500m","memory":"1Gi"}` | Resources - Limits | | resources.requests | object | `{"cpu":"500m","memory":"1Gi"}` | Resources - Requests | -| secret | list | `["mongo_uri","rpc_url","fuse_studio_admin_jwt","legacy_jwt_secret","smart_wallets_jwt_secret","paymaster_production_signer_private_key_v_0_1_0","paymaster_sandbox_signer_private_key_v_0_1_0","explorer_api_key","bundler_api_prd_url","pimlico_api_prd_url","pimlico_api_sandbox_url","amplitude_api_key"]` | Secret (external; sensitive information; pulled from Google Cloud, Secret Manager) | +| secret | list | `["mongo_uri","rpc_url","fuse_studio_admin_jwt","legacy_jwt_secret","smart_wallets_jwt_secret","paymaster_production_signer_private_key_v_0_1_0","paymaster_sandbox_signer_private_key_v_0_1_0","explorer_api_key","pimlico_api_prd_url","pimlico_api_sandbox_url","amplitude_api_key"]` | Secret (external; sensitive information; pulled from Google Cloud, Secret Manager) | | securityPolicy | string | `nil` | Security policy name (Cloud Armor) | diff --git a/k8s/helm/charts/api/values.yaml b/k8s/helm/charts/api/values.yaml index 3d9ca179..f257e48e 100644 --- a/k8s/helm/charts/api/values.yaml +++ b/k8s/helm/charts/api/values.yaml @@ -68,8 +68,6 @@ configMap: explorer_api_url: "" # -- QA mode ('true' or 'false') qa_mode: "" - # -- Bundler - API Sandbox URL - bundler_api_sandbox_url: "" # -- RPC URL - Spark spark_rpc_url: "" # -- Legacy - Fuse admin API URL @@ -89,7 +87,6 @@ secret: - paymaster_production_signer_private_key_v_0_1_0 - paymaster_sandbox_signer_private_key_v_0_1_0 - explorer_api_key - - bundler_api_prd_url - pimlico_api_prd_url - pimlico_api_sandbox_url - amplitude_api_key diff --git a/k8s/helm/charts/skandha/Chart.yaml b/k8s/helm/charts/skandha/Chart.yaml deleted file mode 100644 index 79224750..00000000 --- a/k8s/helm/charts/skandha/Chart.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v2 -name: skandha -description: A Helm chart for Kubernetes related Skandha component - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "0.1.0" diff --git a/k8s/helm/charts/skandha/README.md b/k8s/helm/charts/skandha/README.md deleted file mode 100644 index 6ee393b4..00000000 --- a/k8s/helm/charts/skandha/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# skandha - -![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) - -A Helm chart for Kubernetes related Skandha component - -## Values - -| Key | Type | Default | Description | -|-----|------|---------|-------------| -| affinity | object | `{"zones":["a"]}` | Affinity (available region zones) | -| autoscaling.hpa | object | `{"max_replicas":5}` | Horizontal Pod Autoscaler | -| autoscaling.hpa.max_replicas | int | `5` | Horizontal Pod Autoscaler - Maximum number of replicas, minimal number is `replicas` value | -| global.clusterSecretStore | string | `"gcp-store"` | ClusterSecretStore name (should be created before apply) | -| global.domain | string | `"example.com"` | DNS domain (used for `HTTPRoute` resource) | -| global.environment | string | `"development"` | Kubernetes label `environment`` | -| global.image.repository | string | `"etherspot/skandha"` | Repository ID | -| global.image.tag | string | `"v1-1.5.21"` | Tag; overrides the image tag whose default is the chart appVersion. | -| global.project_id | string | `"example-12345"` | Google Cloud - Project ID (used for `Deployment` resource, `container.image` section) | -| global.region | string | `"us-central1"` | Google Cloud - Region (used for `Deployment` resource, `container.image` section) | -| logging | object | `{"enabled":true,"sampleRate":1000000}` | Logging - enabled (`true` or `false`), sampleRate (from 0 to 500000 / 1000000) | -| maxRatePerEndpoint | int | `10` | Service - Annotations - RPS per pod | -| replicas | int | `1` | Replicas | -| resources.limits | object | `{"cpu":"500m","memory":"1Gi"}` | Resources - Limits | -| resources.requests | object | `{"cpu":"500m","memory":"1Gi"}` | Resources - Requests | -| securityPolicy | string | `nil` | Security policy name (Cloud Armor) | - diff --git a/k8s/helm/charts/skandha/templates/backendpolicy.yaml b/k8s/helm/charts/skandha/templates/backendpolicy.yaml deleted file mode 100644 index 4dbe7572..00000000 --- a/k8s/helm/charts/skandha/templates/backendpolicy.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: networking.gke.io/v1 -kind: GCPBackendPolicy -metadata: - name: skandha - namespace: {{ .Release.Namespace }} - labels: - app: skandha - environment: {{ .Values.global.environment }} -spec: - default: - - # Logging (simple HTTP access logs) - logging: - {{- range $key, $val := .Values.logging }} - {{ $key }}: {{ $val }} - {{- end }} - - # Drain connection (in-flight requests) - connectionDraining: - drainingTimeoutSec: 30 - - {{- if .Values.securityPolicy }} - # Security policy (Cloud Armor) - securityPolicy: {{ .Values.securityPolicy }} - {{- end }} - - targetRef: - group: "" - kind: Service - name: skandha diff --git a/k8s/helm/charts/skandha/templates/deployment.yaml b/k8s/helm/charts/skandha/templates/deployment.yaml deleted file mode 100644 index 65c26b29..00000000 --- a/k8s/helm/charts/skandha/templates/deployment.yaml +++ /dev/null @@ -1,85 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: skandha - namespace: {{ .Release.Namespace }} - annotations: - reloader.stakater.com/auto: "true" - labels: - app: skandha - environment: {{ .Values.global.environment }} -spec: - replicas: {{ .Values.replicas }} - strategy: - type: RollingUpdate - rollingUpdate: - maxUnavailable: 0 - selector: - matchLabels: - app: skandha - environment: {{ .Values.global.environment }} - template: - metadata: - labels: - app: skandha - environment: {{ .Values.global.environment }} - spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: topology.kubernetes.io/zone - operator: In - values: - {{- range $zone := .Values.affinity.zones }} - - {{ $.Values.global.region }}-{{ $zone }} - {{- end }} - containers: - - name: skandha - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: Always - command: ["node", "./packages/cli/bin/skandha", "standalone", "--dataDir", "/opt/skandha/app", "--redirectRpc", "--unsafeMode"] - - # Ports - ports: - - containerPort: 14337 - name: http - - # Probes - livenessProbe: - httpGet: - path: /version - port: 14337 - initialDelaySeconds: 5 - periodSeconds: 5 - readinessProbe: - httpGet: - path: /version - port: 14337 - initialDelaySeconds: 5 - periodSeconds: 5 - - # Resources - resources: - requests: - {{- range $key, $val := .Values.resources.requests }} - {{ $key }}: {{ $val | quote }} - {{- end }} - limits: - {{- range $key, $val := .Values.resources.limits }} - {{ $key }}: {{ $val | quote }} - {{- end }} - - # Volume mounts (config.json) - volumeMounts: - - name: skandha - mountPath: /usr/app/config.json - subPath: config.json - readOnly: true - - # Volume (config.json) - volumes: - - name: skandha - secret: - secretName: skandha diff --git a/k8s/helm/charts/skandha/templates/externalsecret.yaml b/k8s/helm/charts/skandha/templates/externalsecret.yaml deleted file mode 100644 index ccf7f9bb..00000000 --- a/k8s/helm/charts/skandha/templates/externalsecret.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: skandha - namespace: {{ .Release.Namespace }} - labels: - app: skandha - environment: {{ .Values.global.environment }} -spec: - refreshInterval: 5m # Pull rate Secret Manager secrets from Google Cloud - secretStoreRef: - kind: ClusterSecretStore - name: {{ .Values.global.clusterSecretStore }} - target: - name: skandha - creationPolicy: Owner - data: - - secretKey: config.json - remoteRef: - key: {{ .Release.Namespace }}-skandha-config diff --git a/k8s/helm/charts/skandha/templates/healthcheckpolicy.yaml b/k8s/helm/charts/skandha/templates/healthcheckpolicy.yaml deleted file mode 100644 index cdcd506e..00000000 --- a/k8s/helm/charts/skandha/templates/healthcheckpolicy.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: networking.gke.io/v1 -kind: HealthCheckPolicy -metadata: - name: skandha - namespace: {{ .Release.Namespace }} - labels: - app: skandha - environment: {{ .Values.global.environment }} -spec: - default: - checkIntervalSec: 30 - timeoutSec: 5 - healthyThreshold: 3 - unhealthyThreshold: 5 - config: - type: HTTP - httpHealthCheck: - port: 14337 - portName: http - host: skandha - requestPath: /version - targetRef: - group: "" - kind: Service - name: skandha diff --git a/k8s/helm/charts/skandha/templates/hpa.yaml b/k8s/helm/charts/skandha/templates/hpa.yaml deleted file mode 100644 index b50147d0..00000000 --- a/k8s/helm/charts/skandha/templates/hpa.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: skandha - namespace: {{ .Release.Namespace }} - labels: - app: skandha - environment: {{ .Values.global.environment }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: skandha - minReplicas: {{ .Values.replicas }} - maxReplicas: {{ .Values.autoscaling.hpa.max_replicas }} - metrics: - - # CPU - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 80 - - # Memory - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: 80 - - # Requests / second - - type: Object - object: - describedObject: - kind: Service - name: skandha - metric: - name: "autoscaling.googleapis.com|gclb-capacity-utilization" - target: - averageValue: 80 - type: AverageValue diff --git a/k8s/helm/charts/skandha/templates/httproute.yaml b/k8s/helm/charts/skandha/templates/httproute.yaml deleted file mode 100644 index d76666bd..00000000 --- a/k8s/helm/charts/skandha/templates/httproute.yaml +++ /dev/null @@ -1,32 +0,0 @@ -kind: HTTPRoute -apiVersion: gateway.networking.k8s.io/v1beta1 -metadata: - name: skandha - namespace: {{ .Release.Namespace }} - labels: - app: skandha - environment: {{ .Values.global.environment }} -spec: - parentRefs: - - kind: Gateway - name: "{{ .Release.Namespace }}-lb" - sectionName: https - hostnames: - - "bundler.{{ .Values.global.domain }}" - rules: - - matches: - - path: - value: /122 - type: Exact - - path: - value: /123 - type: Exact - filters: - - type: ResponseHeaderModifier - responseHeaderModifier: - add: - - name: application - value: skandha - backendRefs: - - name: skandha - port: 14337 diff --git a/k8s/helm/charts/skandha/templates/service.yaml b/k8s/helm/charts/skandha/templates/service.yaml deleted file mode 100644 index af433147..00000000 --- a/k8s/helm/charts/skandha/templates/service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: skandha - namespace: {{ .Release.Namespace }} - annotations: - networking.gke.io/max-rate-per-endpoint: {{ .Values.maxRatePerEndpoint | quote }} - labels: - app: skandha - environment: {{ .Values.global.environment }} -spec: - selector: - app: skandha - environment: {{ .Values.global.environment }} - ports: - - protocol: TCP - port: 14337 - targetPort: 14337 - type: ClusterIP diff --git a/k8s/helm/charts/skandha/values.yaml b/k8s/helm/charts/skandha/values.yaml deleted file mode 100644 index 33d0661e..00000000 --- a/k8s/helm/charts/skandha/values.yaml +++ /dev/null @@ -1,63 +0,0 @@ -# Default values for skandha. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -# Global -global: - # -- Kubernetes label `environment`` - environment: "development" - - # -- Google Cloud - Project ID (used for `Deployment` resource, `container.image` section) - project_id: "example-12345" - # -- Google Cloud - Region (used for `Deployment` resource, `container.image` section) - region: "us-central1" - - # -- ClusterSecretStore name (should be created before apply) - clusterSecretStore: "gcp-store" - - # -- DNS domain (used for `HTTPRoute` resource) - domain: "example.com" - - # Image - image: - # -- Repository ID - repository: "etherspot/skandha" - # -- Tag; overrides the image tag whose default is the chart appVersion. - tag: "v1-1.5.21" - -# -- Replicas -replicas: 1 - -# -- Affinity (available region zones) -affinity: - zones: - - a - -# Resources -resources: - # -- Resources - Requests - requests: - cpu: "500m" - memory: "1Gi" - # -- Resources - Limits - limits: - cpu: "500m" - memory: "1Gi" - -# Autoscaling -autoscaling: - # -- Horizontal Pod Autoscaler - hpa: - # -- Horizontal Pod Autoscaler - Maximum number of replicas, minimal number is `replicas` value - max_replicas: 5 - -# -- Service - Annotations - RPS per pod -maxRatePerEndpoint: 10 - -# -- Security policy name (Cloud Armor) -securityPolicy: null - -# -- Logging - enabled (`true` or `false`), sampleRate (from 0 to 500000 / 1000000) -logging: - enabled: true - sampleRate: 1000000 diff --git a/k8s/helm/production.yaml b/k8s/helm/production.yaml index 5e160ccb..21300b5b 100644 --- a/k8s/helm/production.yaml +++ b/k8s/helm/production.yaml @@ -58,8 +58,6 @@ accounts: paymaster_production_contract_address_v_0_1_0: "0xEA1Ba4305A07cEd2bB5e42224D71aBE0BC3C3f28" entrypoint_sandbox_contract_address_v_0_1_0: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" entrypoint_production_contract_address_v_0_1_0: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" - etherspot_wallet_factory_sandbox_contract_address_v_0_1_0: "0x7f6d8F107fE8551160BD5351d5F1514A6aD5d40E" - etherspot_wallet_factory_production_contract_address_v_0_1_0: "0x7f6d8F107fE8551160BD5351d5F1514A6aD5d40E" paymaster_funder_api_key: "pk_yWkBe0hnObJmMkYjq4Ggce1g" paymaster_funder_webhook_id: "65c4ba66394c8d9c3e4801ff" console_dapp_url: "https://console.fuse.io" @@ -137,7 +135,6 @@ api: configMap: explorer_api_url: "https://explorer.fuse.io/api" qa_mode: "false" - bundler_api_sandbox_url: "https://testnet-rpc.etherspot.io/v1/123" spark_rpc_url: "https://rpc.fusespark.io" legacy_fuse_admin_api_url: "https://studio.fuse.io" legacy_fuse_wallet_api_url: "https://wallet.fuse.io" @@ -153,7 +150,6 @@ api: - paymaster_production_signer_private_key_v_0_1_0 - paymaster_sandbox_signer_private_key_v_0_1_0 - explorer_api_key - - bundler_api_prd_url - pimlico_api_prd_url - pimlico_api_sandbox_url - amplitude_api_key @@ -335,38 +331,6 @@ relay: - mongo_uri - relay_secret -skandha: - enabled: false # Self - hosted instance used as a fall - back. By default used the EtherSpot - hosted instance. - - # Replicas - replicas: 2 - - # Affinity (zones) - affinity: - zones: - - b - - # Image - image: - repository: "etherspot/skandha" - # Overrides the image tag whose default is the chart appVersion. - tag: "v1-1.5.21" - - # Resources (requests & limits) - resources: - requests: - cpu: "500m" - memory: "1Gi" - limits: - cpu: "500m" - memory: "1Gi" - - # Service - RPS per Pod - maxRatePerEndpoint: 50 - - # Security - CloudArmor - securityPolicy: null - smart-wallets: enabled: true diff --git a/k8s/helm/staging.yaml b/k8s/helm/staging.yaml index 13723e04..e1b1a1b3 100644 --- a/k8s/helm/staging.yaml +++ b/k8s/helm/staging.yaml @@ -58,8 +58,6 @@ accounts: paymaster_production_contract_address_v_0_1_0: "0xEA1Ba4305A07cEd2bB5e42224D71aBE0BC3C3f28" entrypoint_sandbox_contract_address_v_0_1_0: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" entrypoint_production_contract_address_v_0_1_0: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789" - etherspot_wallet_factory_sandbox_contract_address_v_0_1_0: "0x7f6d8F107fE8551160BD5351d5F1514A6aD5d40E" - etherspot_wallet_factory_production_contract_address_v_0_1_0: "0x7f6d8F107fE8551160BD5351d5F1514A6aD5d40E" paymaster_funder_api_key: "pk_u_RbFw7NQBB7ZQIFJtziLE_S" paymaster_funder_webhook_id: "65bbbc63fc2f925de8e1a561" usdc_contract_address_mainnet: "0x28C3d1cD466Ba22f6cae51b1a4692a831696391A" @@ -136,7 +134,6 @@ api: configMap: explorer_api_url: "https://explorer.fuse.io/api" qa_mode: "true" - bundler_api_sandbox_url: "https://testnet-rpc.etherspot.io/v1/123" spark_rpc_url: "https://rpc.fusespark.io" legacy_fuse_admin_api_url: "https://studio.fuse.io" legacy_fuse_wallet_api_url: "https://staging-wallet.fuse.io" @@ -152,7 +149,6 @@ api: - paymaster_production_signer_private_key_v_0_1_0 - paymaster_sandbox_signer_private_key_v_0_1_0 - explorer_api_key - - bundler_api_prd_url - pimlico_api_prd_url - pimlico_api_sandbox_url - amplitude_api_key @@ -335,35 +331,6 @@ relay: - mongo_uri - relay_secret -skandha: - enabled: false # Only Production environment has self - hosted Skandha instance for a DR; Ethrespot - hosted endpoint used by both environments. - - # Affinity (zones) - affinity: - zones: - - b - - # Image - image: - repository: "etherspot/skandha" - # Overrides the image tag whose default is the chart appVersion. - tag: "v1-1.5.21" - - # Resources (requests & limits) - resources: - requests: - cpu: "500m" - memory: "1Gi" - limits: - cpu: "500m" - memory: "1Gi" - - # Service - RPS per Pod - maxRatePerEndpoint: 10 - - # Security - CloudArmor - securityPolicy: null - smart-wallets: enabled: true