diff --git a/.github/workflows/k8s-deploy-prod.yaml b/.github/workflows/k8s-deploy-prod.yaml new file mode 100644 index 00000000..06bcdf7b --- /dev/null +++ b/.github/workflows/k8s-deploy-prod.yaml @@ -0,0 +1,124 @@ +--- +name: Build & Deploy to Kubernetes [PRIVATE_REGISTRY_PASSWORD:] + +on: + push: + branches: + - master + +jobs: + build-and-push: + name: Build & Push + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - dockerfile: ./apps/charge-accounts-service/Dockerfile + image: accounts-service + - dockerfile: ./apps/charge-api-service/Dockerfile + image: api-service + - dockerfile: ./apps/charge-network-service/Dockerfile + image: network-service + - dockerfile: ./apps/charge-notifications-service/Dockerfile + image: notifications-service + - dockerfile: ./apps/charge-smart-wallets-service/Dockerfile + image: smart-wallets-service + env: + PRIVATE_REGISTRY_ENDPOINT: ${{ secrets.PRIVATE_REGISTRY_ENDPOINT }} + PRIVATE_REGISTRY_REPOSITORY: ${{ secrets.PRIVATE_REGISTRY_REPOSITORY_PROD }} + PRIVATE_REGISTRY_USERNAME: ${{ secrets.PRIVATE_REGISTRY_USERNAME_PROD }} + PRIVATE_REGISTRY_PASSWORD: ${{ secrets.PRIVATE_REGISTRY_PASSWORD_PROD }} + outputs: + short_sha: ${{ steps.sha.outputs.short_sha }} + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set GitHub commit short SHA + id: sha + run: | + CALCULATED_SHA=$(git rev-parse --short ${{ github.sha }}) + echo "short_sha=$CALCULATED_SHA" >> $GITHUB_OUTPUT + + - name: Login to private registry + uses: docker/login-action@v3 + with: + registry: ${{ env.PRIVATE_REGISTRY_ENDPOINT }} + username: ${{ env.PRIVATE_REGISTRY_USERNAME }} + password: ${{ env.PRIVATE_REGISTRY_PASSWORD }} + logout: true + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image(s) + uses: docker/build-push-action@v5 + with: + context: . + file: ${{ matrix.dockerfile }} + push: true + tags: | + ${{ env.PRIVATE_REGISTRY_ENDPOINT }}/${{ env.PRIVATE_REGISTRY_REPOSITORY }}/${{ matrix.image }}:latest + ${{ env.PRIVATE_REGISTRY_ENDPOINT }}/${{ env.PRIVATE_REGISTRY_REPOSITORY }}/${{ matrix.image }}:${{ steps.sha.outputs.short_sha }} + + deploy: + name: Deploy + runs-on: ubuntu-latest + env: + PRIVATE_REGISTRY_ENDPOINT: ${{ secrets.PRIVATE_REGISTRY_ENDPOINT }} + PRIVATE_REGISTRY_REPOSITORY: ${{ secrets.PRIVATE_REGISTRY_REPOSITORY_PROD }} + K8S_KUBECONFIG: ${{ secrets.K8S_KUBECONFIG_PROD }} + HELM_CHART_REGISTRY_ENDPOINT: ${{ secrets.HELM_CHART_REGISTRY_ENDPOINT }} + HELM_CHART_REGISTRY_REPOSITORY: ${{ secrets.HELM_CHART_REGISTRY_REPOSITORY }} + HELM_CHART_REGISTRY_USERNAME: ${{ secrets.HELM_CHART_REGISTRY_USERNAME }} + HELM_CHART_REGISTRY_PASSWORD: ${{ secrets.HELM_CHART_REGISTRY_PASSWORD }} + HELM_CHART_VERSION: ${{ vars.HELM_CHART_VERSION_PROD }} + HELM_RELEASE_NAME: ${{ vars.HELM_RELEASE_NAME }} + HELM_RELEASE_NAMESPACE: ${{ vars.HELM_RELEASE_NAMESPACE }} + needs: build-and-push + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Login to private registry + uses: docker/login-action@v3 + with: + registry: ${{ env.HELM_CHART_REGISTRY_ENDPOINT }} + username: ${{ env.HELM_CHART_REGISTRY_USERNAME }} + password: ${{ env.HELM_CHART_REGISTRY_PASSWORD }} + logout: true + + - name: Configure Kubernetes config file + run: | + mkdir -p $HOME/.kube/ + echo "${{ env.K8S_KUBECONFIG }}" | base64 -d > $HOME/.kube/config + + - name: Release + run: | + helm upgrade \ + --force \ + --reset-values \ + --install \ + --history-max 3 \ + --debug \ + --atomic \ + --wait \ + --values helm/values/prod.yaml \ + --set accounts-service.image.repository=${{ env.PRIVATE_REGISTRY_ENDPOINT }}/${{ env.PRIVATE_REGISTRY_REPOSITORY }}/accounts-service \ + --set accounts-service.image.tag=${{ needs.build-and-push.outputs.short_sha }} \ + --set api-service.image.repository=${{ env.PRIVATE_REGISTRY_ENDPOINT }}/${{ env.PRIVATE_REGISTRY_REPOSITORY }}/api-service \ + --set api-service.image.tag=${{ needs.build-and-push.outputs.short_sha }} \ + --set network-service.image.repository=${{ env.PRIVATE_REGISTRY_ENDPOINT }}/${{ env.PRIVATE_REGISTRY_REPOSITORY }}/network-service \ + --set network-service.image.tag=${{ needs.build-and-push.outputs.short_sha }} \ + --set notifications-service.image.repository=${{ env.PRIVATE_REGISTRY_ENDPOINT }}/${{ env.PRIVATE_REGISTRY_REPOSITORY }}/notifications-service \ + --set notifications-service.image.tag=${{ needs.build-and-push.outputs.short_sha }} \ + --set smart-wallets-service.image.repository=${{ env.PRIVATE_REGISTRY_ENDPOINT }}/${{ env.PRIVATE_REGISTRY_REPOSITORY }}/smart-wallets-service \ + --set smart-wallets-service.image.tag=${{ needs.build-and-push.outputs.short_sha }} \ + -n ${{ env.HELM_RELEASE_NAMESPACE }} \ + ${{ env.HELM_RELEASE_NAME }} \ + oci://${{ env.HELM_CHART_REGISTRY_ENDPOINT }}/${{ env.HELM_CHART_REGISTRY_REPOSITORY }} \ + --version ${{ env.HELM_CHART_VERSION }} diff --git a/.github/workflows/k8s-deploy-qa.yaml b/.github/workflows/k8s-deploy-qa.yaml index b7754f87..d45f0239 100644 --- a/.github/workflows/k8s-deploy-qa.yaml +++ b/.github/workflows/k8s-deploy-qa.yaml @@ -100,6 +100,8 @@ jobs: - name: Release run: | helm upgrade \ + --force \ + --reset-values \ --install \ --history-max 3 \ --debug \ 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 dc36bae0..a9cd7858 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 @@ -46,21 +46,40 @@ export class BundlerApiInterceptor implements NestInterceptor { .request(requestConfig) .pipe( map((axiosResponse: AxiosResponse) => { - this.logger.log(`BundlerApiInterceptor succeeded: ${JSON.stringify(axiosResponse.data)}`) - return axiosResponse.data + const data = axiosResponse.data + if (data?.error) { + this.logger.error(`BundlerApiInterceptor JSON-RPC error: ${JSON.stringify(data.error)}`) + // Parse specific error codes + if (data.error?.data?.includes('0xe0cff05f')) { + this.logger.error('FailedOp: UserOperation validation failed at EntryPoint') + } + // For CALL_EXCEPTION errors specifically + if (data.error?.message?.includes('CALL_EXCEPTION')) { + this.logger.error('Transaction simulation failed - possible validation, gas, or contract execution error') + } + } else { + this.logger.log(`BundlerApiInterceptor succeeded: ${JSON.stringify(data)}`) + } + return data }) ) .pipe( catchError((e) => { - const errorReason = - e?.response?.data?.error || - e?.response?.data?.errors?.message || - '' - this.logger.log(`BundlerApiInterceptor error: ${JSON.stringify(e)}`) - throw new HttpException( - `${e?.response?.statusText}: ${errorReason}`, - e?.response?.status - ) + const errorData = e?.response?.data + let errorMessage = e?.response?.statusText || 'Bundler API error' + + if (errorData?.error) { + const error = errorData.error + if (error.data?.includes('0xe0cff05f')) { + errorMessage = 'UserOperation validation failed - possible causes: expired validUntil timestamp, insufficient gas, or paymaster validation failure' + this.logger.error(`FailedOp details: ${JSON.stringify(error)}`) + } else { + errorMessage = error.message || errorMessage + } + } + + this.logger.error(`BundlerApiInterceptor error: ${JSON.stringify(e)}`) + throw new HttpException(errorMessage, e?.response?.status || 500) }) ) ) diff --git a/apps/charge-api-service/src/paymaster-api/paymaster-api.controller.ts b/apps/charge-api-service/src/paymaster-api/paymaster-api.controller.ts index 482f7820..69c4c775 100644 --- a/apps/charge-api-service/src/paymaster-api/paymaster-api.controller.ts +++ b/apps/charge-api-service/src/paymaster-api/paymaster-api.controller.ts @@ -1,4 +1,4 @@ -import { Controller, UseGuards, Req, Post, Res, Body } from '@nestjs/common' +import { Controller, UseGuards, Req, Post, Res } from '@nestjs/common' import { PaymasterApiService } from '@app/api-service/paymaster-api/paymaster-api.service' import { IsPrdOrSbxKeyGuard } from '@app/api-service/api-keys/guards/is-production-or-sandbox-key.guard' import { JSONRPCServer } from 'json-rpc-2.0' 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 e2b4fdba..f9d4f95e 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 @@ -49,7 +49,6 @@ export class PaymasterApiService { const validUntil = parseInt(timestamp.toString()) + 900 const validAfter = 0 - const paymasterInfo = await callMSFunction(this.accountClient, 'get_paymaster_info', { projectId, env }) const minVerificationGasLimit = '140000' @@ -132,90 +131,81 @@ export class PaymasterApiService { } async estimateUserOpGas (op, requestEnvironment, entrypointAddress): Promise { - try { - const data = { - jsonrpc: '2.0', - method: 'eth_estimateUserOperationGas', - params: [ - op, - entrypointAddress - ], - id: 1 - } + const data = { + jsonrpc: '2.0', + method: 'eth_estimateUserOperationGas', + params: [ + op, + entrypointAddress + ], + id: 1 + } - const requestConfig: AxiosRequestConfig = { - url: this.prepareUrl(requestEnvironment), - method: 'post', - data - } - const response = await lastValueFrom( - this.httpService - .request(requestConfig) - .pipe( - map((axiosResponse: AxiosResponse) => { - return axiosResponse.data - }) - ) - .pipe( - catchError((e) => { - const errorReason = - e?.result?.error || - e?.result?.error?.message || - '' - - this.logger.error(`RpcException catchError: ${errorReason} ${JSON.stringify(e)}`) - throw new RpcException(errorReason) - }) - ) - ) + const requestConfig: AxiosRequestConfig = { + url: this.prepareUrl(requestEnvironment), + method: 'post', + data + } - if (has(response, 'error')) { - const error = get(response, 'error') - this.logger.error('Error getting gas estimation', error) - throw new RpcException(error) - } + const response = await lastValueFrom( + this.httpService + .request(requestConfig) + .pipe( + map((axiosResponse: AxiosResponse) => { + return axiosResponse.data + }) + ) + .pipe( + catchError((e) => { + // Log specific error types for better debugging + if (e?.response?.status === 503 || e?.response?.status === 502) { + this.logger.error(`Service unavailable error (${e?.response?.status}): RPC node may be down`) + } + + const errorReason = + e?.response?.data?.error?.message || + e?.result?.error || + e?.message || + 'Unknown error during gas estimation' + + this.logger.error(`RpcException catchError: ${errorReason} ${JSON.stringify(e)}`) + throw new RpcException(errorReason) + }) + ) + ) - if (!has(response, 'result')) { - this.logger.error('Response does not contain result', JSON.stringify(response)) - throw new InternalServerErrorException('Error getting gas estimation from paymaster') - } + if (has(response, 'error')) { + const error = get(response, 'error') + this.logger.error('Error getting gas estimation', error) + throw new RpcException(error) + } - if (has(response, 'result.error')) { - const result = get(response, 'result') - const error = get(response, 'result.error') - this.logger.error('Error in result of gas estimation', result) - throw new RpcException(error) - } + if (!has(response, 'result')) { + this.logger.error('Response does not contain result', JSON.stringify(response)) + throw new InternalServerErrorException('Error getting gas estimation from paymaster') + } - if (!has(response, 'result.callGasLimit')) { - const result = get(response, 'result') - this.logger.error('Result does not contain callGasLimit', result) - throw new InternalServerErrorException('Error getting gas estimation from paymaster') - } + if (has(response, 'result.error')) { + const result = get(response, 'result') + const error = get(response, 'result.error') + this.logger.error('Error in result of gas estimation', result) + throw new RpcException(error) + } - const result = get(response, 'result') as GasDetails - this.logger.log(`Gas estimation received: ${JSON.stringify(result)}`) + if (!has(response, 'result.callGasLimit')) { + const result = get(response, 'result') + this.logger.error('Result does not contain callGasLimit', result) + throw new InternalServerErrorException('Error getting gas estimation from paymaster') + } - const callGasLimit = BigNumber.from(result.callGasLimit).mul(115).div(100).toHexString() // 15% buffer + const result = get(response, 'result') as GasDetails + this.logger.log(`Gas estimation received: ${JSON.stringify(result)}`) - return { - ...result, - callGasLimit - } - } catch (error) { - // Provide hardcoded fallback gas values when estimation fails - this.logger.warn(`Gas estimation failed, using hardcoded values. Error: ${JSON.stringify(error)}`) - - // Hardcoded gas values with generous buffers - return { - preVerificationGas: '0x186a0', // 100,000 - verificationGasLimit: '0x1e8480', // 2,000,000 - verificationGas: '0xf4240', // 1,000,000 - validUntil: '0xffffffff', // Far future - callGasLimit: '0x2dc6c0', // 3,000,000 - maxFeePerGas: '0x3b9aca00', // 1 Gwei - maxPriorityFeePerGas: '0x3b9aca00' // 1 Gwei - } + const callGasLimit = BigNumber.from(result.callGasLimit).mul(115).div(100).toHexString() // 15% buffer + + return { + ...result, + callGasLimit } } diff --git a/apps/charge-smart-wallets-service/src/common/services/relay-api.service.ts b/apps/charge-smart-wallets-service/src/common/services/relay-api.service.ts index e36b57cd..319de136 100644 --- a/apps/charge-smart-wallets-service/src/common/services/relay-api.service.ts +++ b/apps/charge-smart-wallets-service/src/common/services/relay-api.service.ts @@ -1,11 +1,12 @@ import { HttpService } from '@nestjs/axios' -import { HttpException, Injectable } from '@nestjs/common' +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { AxiosRequestConfig } from 'axios' import { catchError, lastValueFrom, map } from 'rxjs' @Injectable() export default class RelayAPIService { + private readonly logger = new Logger(RelayAPIService.name) constructor ( private readonly httpService: HttpService, private readonly configService: ConfigService @@ -62,13 +63,20 @@ export default class RelayAPIService { .pipe(map(res => res.data)) .pipe( catchError(e => { + this.logger.error(`RelayAPIService error: ${JSON.stringify(e)}`) + // More robust error handling - check if response exists before accessing properties const errorReason = e?.response?.data?.error || - e?.response?.data?.errors?.message || '' + e?.response?.data?.errors?.message || + e?.message || + 'Unknown error occurred' - throw new HttpException( - `${e?.response?.statusText}: ${errorReason}`, - e?.response?.status - ) + const statusText = e?.response?.statusText || 'Error' + const status = e?.response?.status || HttpStatus.INTERNAL_SERVER_ERROR + + // Create error message safely + const errorMessage = errorReason ? `${statusText}: ${errorReason}` : statusText + + throw new HttpException(errorMessage, status) }) ) return await lastValueFrom(observable) diff --git a/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-aa.service.ts b/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-aa.service.ts index ec937c8b..b2201e86 100644 --- a/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-aa.service.ts +++ b/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-aa.service.ts @@ -1,5 +1,6 @@ import { SmartWalletsAuthDto } from '@app/smart-wallets-service/dto/smart-wallets-auth.dto' -import { Inject, HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common' +import { Inject, HttpStatus, Injectable, Logger } from '@nestjs/common' +import { RpcException } from '@nestjs/microservices' import { JwtService } from '@nestjs/jwt' import { arrayify, computeAddress, hashMessage, recoverPublicKey } from 'nestjs-ethers' import { SmartWalletService } from '@app/smart-wallets-service/smart-wallets/interfaces/smart-wallets.interface' @@ -57,7 +58,15 @@ export class SmartWalletsAAService implements SmartWalletService { } } catch (err) { this.logger.error(`An error occurred during Smart Wallets Auth. ${err}`) - throw new HttpException(err.message, HttpStatus.BAD_REQUEST) + // If it's already an RpcException, re-throw it + if (err instanceof RpcException) { + throw err + } + // Otherwise wrap it in RpcException for microservice context + throw new RpcException({ + error: err.message || 'Authentication error', + status: HttpStatus.BAD_REQUEST + }) } } diff --git a/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-legacy.service.ts b/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-legacy.service.ts index 24157fda..c2afdc0e 100644 --- a/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-legacy.service.ts +++ b/apps/charge-smart-wallets-service/src/smart-wallets/services/smart-wallets-legacy.service.ts @@ -1,6 +1,7 @@ import { Model } from 'mongoose' import { SmartWalletsAuthDto } from '@app/smart-wallets-service/dto/smart-wallets-auth.dto' -import { HttpException, HttpStatus, Inject, Injectable, Logger } from '@nestjs/common' +import { HttpStatus, Inject, Injectable, Logger } from '@nestjs/common' +import { RpcException } from '@nestjs/microservices' import { JwtService } from '@nestjs/jwt' import { arrayify, computeAddress, hashMessage, recoverPublicKey } from 'nestjs-ethers' import { ConfigService } from '@nestjs/config' @@ -52,7 +53,10 @@ export class SmartWalletsLegacyService implements SmartWalletService { } } catch (err) { this.logger.error(`An error occurred during Smart Wallets Auth. ${err}`) - throw new HttpException(err.message, HttpStatus.BAD_REQUEST) + throw new RpcException({ + error: err.message || 'Authentication error', + status: HttpStatus.BAD_REQUEST + }) } } @@ -63,7 +67,12 @@ export class SmartWalletsLegacyService implements SmartWalletService { const smartWallet = await this.smartWalletModel.findOne({ ownerAddress }) if (!smartWallet) { this.logger.warn(`Smart Wallet not found for owner address: ${ownerAddress}`) - throw new Error('Not found') + const errorObj = { + error: 'Smart wallet not found', + status: HttpStatus.NOT_FOUND + } + this.logger.debug(`Throwing RpcException with: ${JSON.stringify(errorObj)}`) + throw new RpcException(errorObj) } if (!smartWallet.isContractDeployed) { this.logger.log(`Smart Wallet not deployed for owner address: ${ownerAddress}, deploying...`) @@ -102,7 +111,15 @@ export class SmartWalletsLegacyService implements SmartWalletService { } } catch (err) { this.logger.error(`An error occurred during fetching Legacy Smart Wallet. ${err}`) - throw new HttpException(err.message, HttpStatus.BAD_REQUEST) + // If it's already an RpcException, re-throw it + if (err instanceof RpcException) { + throw err + } + // Otherwise wrap it in RpcException for microservice context + throw new RpcException({ + error: err.message || 'Error fetching smart wallet', + status: HttpStatus.BAD_REQUEST + }) } } @@ -110,7 +127,10 @@ export class SmartWalletsLegacyService implements SmartWalletService { try { const { ownerAddress } = smartWalletUser if (await this.smartWalletModel.findOne({ ownerAddress })) { - throw new Error('Owner address already has a deployed smart wallet') + throw new RpcException({ + error: 'Owner address already has a deployed smart wallet', + status: HttpStatus.CONFLICT + }) } const salt = generateSalt() const transactionId = generateTransactionId(salt) @@ -131,7 +151,10 @@ export class SmartWalletsLegacyService implements SmartWalletService { } } catch (err) { this.logger.error(`An error occurred during Smart Wallets Creation. ${err}`) - throw new HttpException(err.message, HttpStatus.BAD_REQUEST) + throw new RpcException({ + error: err.message || 'Error creating smart wallet', + status: HttpStatus.BAD_REQUEST + }) } } @@ -150,7 +173,10 @@ export class SmartWalletsLegacyService implements SmartWalletService { } } catch (err) { this.logger.error(`An error occurred during relay execution. ${err}`) - throw new HttpException(err.message, HttpStatus.BAD_REQUEST) + throw new RpcException({ + error: err.message || 'Relay execution error', + status: HttpStatus.BAD_REQUEST + }) } } @@ -163,7 +189,10 @@ export class SmartWalletsLegacyService implements SmartWalletService { } } catch (error) { this.logger.error(`An error occurred during fetching historical txs. ${error}`) - throw new HttpException(error.message, HttpStatus.BAD_REQUEST) + throw new RpcException({ + error: error.message || 'Error fetching historical transactions', + status: HttpStatus.BAD_REQUEST + }) } } diff --git a/helm/values/prod.yaml b/helm/values/prod.yaml new file mode 100644 index 00000000..47d64828 --- /dev/null +++ b/helm/values/prod.yaml @@ -0,0 +1,659 @@ +# Default values for fusebox-backend. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +accounts-service: + enabled: true + + externalSecret: + secretStoreName: "hashicorp-vault" + remoteRefKey: "fuseio/fusebox-backend/accounts-service" + refreshInterval: "15s" + secretKey: + - MONGO_URI + - SMART_WALLETS_JWT_SECRET + - PAYMASTER_FUNDER_PRIVATE_KEY + - PAYMASTER_FUNDER_API_SECRET_KEY + - AMPLITUDE_API_KEY + - INCOMING_TOKEN_TRANSFERS_WEBHOOK_ID + - GOOGLE_OPERATOR_FORM_URL + - OPERATOR_REFRESH_JWT_SECRET + - CHARGE_PAYMENTS_API_KEY + - COIN_GECKO_API_KEY + - CRON_JOB_SECRET + + replicaCount: 2 + + image: + repository: fusebox-backend/accounts-service + pullPolicy: IfNotPresent + tag: "" + + imagePullSecrets: + - name: regcred + + nameOverride: "" + fullnameOverride: "" + + command: [] + + args: [] + + serviceAccount: + create: false + automount: true + annotations: {} + name: "" + + deploymentAnnotations: + reloader.stakater.com/auto: "true" + + podAnnotations: {} + podLabels: {} + + podSecurityContext: {} + + securityContext: {} + + service: + type: ClusterIP + ports: + - name: http + port: 5001 + protocol: TCP + targetPort: 5001 + - name: tcp + port: 8875 + protocol: TCP + targetPort: 8875 + + ingress: + enabled: true + className: "nginx" + annotations: {} + hosts: + - host: accounts.fuse.io + paths: + - path: / + pathType: Prefix + tls: + - secretName: fusebox-backend-accounts-service-tls-certificate + hosts: + - accounts.fuse.io + + resources: {} + + livenessProbe: + httpGet: + path: /accounts/v1/health + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 15 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /accounts/v1/health + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 15 + successThreshold: 1 + failureThreshold: 3 + + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + + volumes: [] + + volumeMounts: [] + + configMap: + ACCOUNTS_HOST: "0.0.0.0" + ACCOUNTS_PORT: "5001" + ACCOUNTS_TCP_PORT: "8875" + API_HOST: "fusebox-backend-api-service" + API_TCP_PORT: "8876" + SMART_WALLETS_HOST: "fusebox-backend-smart-wallets-service" + SMART_WALLETS_TCP_PORT: "8881" + NOTIFICATIONS_HOST: "fusebox-backend-notifications-service" + NOTIFICATIONS_TCP_PORT: "8879" + AUTH0_ISSUER_URL: "https://auth.fuse.io//" + AUTH0_AUDIENCE: "https://accounts.fuse.io" + PAYMASTER_SANDBOX_CONTRACT_ADDRESS_V_0_1_0: "0x324999f067EA822EEf78e7A4793F672A4F5E80f6" + 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" + USDC_CONTRACT_ADDRESS_TESTNET: "0x28C3d1cD466Ba22f6cae51b1a4692a831696391A" + CHARGE_PAYMENTS_API_URL: "https://payments.chargeweb3.com" + COIN_GECKO_URL: "https://pro-api.coingecko.com/api/v3" + WFUSE_CONTRACT_ADDRESS_MAINNET: "0x0BE9e53fd7EDaC9F859882AfdDa116645287C629" + WFUSE_CONTRACT_ADDRESS_TESTNET: "0x0BE9e53fd7EDaC9F859882AfdDa116645287C629" + + nodeSelector: {} + + tolerations: [] + + affinity: {} + +api-service: + enabled: true + + externalSecret: + secretStoreName: "hashicorp-vault" + remoteRefKey: "fuseio/fusebox-backend/api-service" + refreshInterval: "15s" + secretKey: + - 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 + + replicaCount: 2 + + image: + repository: fusebox-backend/api-service + pullPolicy: IfNotPresent + tag: "" + + imagePullSecrets: + - name: regcred + + nameOverride: "" + fullnameOverride: "" + + command: [] + + args: [] + + serviceAccount: + create: false + automount: true + annotations: {} + name: "" + + deploymentAnnotations: + reloader.stakater.com/auto: "true" + + podAnnotations: {} + podLabels: {} + + podSecurityContext: {} + + securityContext: {} + + service: + type: ClusterIP + ports: + - name: http + port: 5002 + protocol: TCP + targetPort: 5002 + - name: tcp + port: 8876 + protocol: TCP + targetPort: 8876 + + ingress: + enabled: true + className: "nginx" + annotations: {} + hosts: + - host: api.fuse.io + paths: + - path: / + pathType: Prefix + tls: + - secretName: fusebox-backend-api-service-tls-certificate + hosts: + - api.fuse.io + + resources: {} + + livenessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 15 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 15 + successThreshold: 1 + failureThreshold: 3 + + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + + volumes: [] + + volumeMounts: [] + + configMap: + API_HOST: "0.0.0.0" + API_PORT: "5002" + API_TCP_PORT: "8876" + ACCOUNTS_HOST: "fusebox-backend-accounts-service" + ACCOUNTS_TCP_PORT: "8875" + NOTIFICATIONS_HOST: "fusebox-backend-notifications-service" + NOTIFICATIONS_TCP_PORT: "8879" + NETWORK_HOST: "fusebox-backend-network-service" + NETWORK_TCP_PORT: "8878" + SMART_WALLETS_HOST: "fusebox-backend-smart-wallets-service" + SMART_WALLETS_PORT: "5007" + 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" + VOLTAGE_ROUTER_API_URL: "https://router.voltage.finance" + + nodeSelector: {} + + tolerations: [] + + affinity: {} + +# ---------------------- +# !!! - DEPRECATED - !!! +# ---------------------- +apps-service: + enabled: false + +network-service: + enabled: true + + externalSecret: + secretStoreName: "hashicorp-vault" + remoteRefKey: "fuseio/fusebox-backend/network-service" + refreshInterval: "15s" + secretKey: + - RPC_URL + - UNMARSHAL_AUTH_KEY + - EXPLORER_API_KEY + - COIN_GECKO_API_KEY + + replicaCount: 1 + + image: + repository: fusebox-backend/network-service + pullPolicy: IfNotPresent + tag: "" + + imagePullSecrets: + - name: regcred + + nameOverride: "" + fullnameOverride: "" + + command: [] + + args: [] + + serviceAccount: + create: false + automount: true + annotations: {} + name: "" + + deploymentAnnotations: + reloader.stakater.com/auto: "true" + + podAnnotations: {} + podLabels: {} + + podSecurityContext: {} + + securityContext: {} + + service: + type: ClusterIP + ports: + - name: http + port: 5004 + protocol: TCP + targetPort: 5004 + - name: tcp + port: 8878 + protocol: TCP + targetPort: 8878 + + ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + + resources: {} + + livenessProbe: + httpGet: + path: /network/health + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 15 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /network/health + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 15 + successThreshold: 1 + failureThreshold: 3 + + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + + volumes: [] + + volumeMounts: [] + + configMap: + NETWORK_HOST: "0.0.0.0" + NETWORK_PORT: "5004" + NETWORK_TCP_PORT: "8878" + PRIMARY_SERVICE: "explorer" + EXPLORER_API_URL: "https://explorer.fuse.io/api" + UNMARSHAL_BASE_URL: "https://api.unmarshal.com" + COIN_GECKO_URL: "https://pro-api.coingecko.com/api/v3" + + nodeSelector: {} + + tolerations: [] + + affinity: {} + +notifications-service: + enabled: true + + externalSecret: + secretStoreName: "hashicorp-vault" + remoteRefKey: "fuseio/fusebox-backend/notifications-service" + refreshInterval: "15s" + secretKey: + - MONGO_URI + - SMART_WALLETS_JWT_SECRET + - RPC_URL + - FULL_ARCHIVE_RPC_URL + + replicaCount: 1 + + image: + repository: fusebox-backend/notifications-service + pullPolicy: IfNotPresent + tag: "" + + imagePullSecrets: + - name: regcred + + nameOverride: "" + fullnameOverride: "" + + command: [] + + args: [] + + serviceAccount: + create: false + automount: true + annotations: {} + name: "" + + deploymentAnnotations: + reloader.stakater.com/auto: "true" + + podAnnotations: {} + podLabels: {} + + podSecurityContext: {} + + securityContext: {} + + service: + type: ClusterIP + ports: + - name: http + port: 5005 + protocol: TCP + targetPort: 5005 + - name: tcp + port: 8879 + protocol: TCP + targetPort: 8879 + + ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + + resources: {} + + livenessProbe: + httpGet: + path: /notifications/health + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 15 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /notifications/health + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 15 + successThreshold: 1 + failureThreshold: 3 + + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + + volumes: [] + + volumeMounts: [] + + configMap: + NOTIFICATIONS_HOST: "0.0.0.0" + NOTIFICATIONS_PORT: "5005" + NOTIFICATIONS_TCP_PORT: "8879" + SMART_WALLETS_HOST: "fusebox-backend-smart-wallets-service" + SMART_WALLETS_TCP_PORT: "8881" + NETWORK_NAME: "fuse" + CHAIN_ID: "122" + MAX_BLOCKS: "1500" + TIMEOUT_INTERVAL: "20000" + + nodeSelector: {} + + tolerations: [] + + affinity: {} + +# ---------------------- +# !!! - DEPRECATED - !!! +# ---------------------- +relay-service: + enabled: false + +smart-wallets-service: + enabled: true + + externalSecret: + secretStoreName: "hashicorp-vault" + remoteRefKey: "fuseio/fusebox-backend/smart-wallets-service" + refreshInterval: "15s" + secretKey: + - MONGO_URI + - AMPLITUDE_API_KEY + - SMART_WALLETS_JWT_SECRET + - FUSE_WALLET_BACKEND_JWT + - CENTRIFUGO_JWT + - CENTRIFUGO_API_KEY + - CHARGE_PUBLIC_KEY + - CHARGE_SECRET_KEY + - INCOMING_TOKEN_TRANSFERS_WEBHOOK_ID + - COIN_GECKO_API_KEY + + replicaCount: 1 + + image: + repository: fusebox-backend/smart-wallets-service + pullPolicy: IfNotPresent + tag: "" + + imagePullSecrets: + - name: regcred + + nameOverride: "" + fullnameOverride: "" + + command: [] + + args: [] + + serviceAccount: + create: false + automount: true + annotations: {} + name: "" + + deploymentAnnotations: + reloader.stakater.com/auto: "true" + + podAnnotations: {} + podLabels: {} + + podSecurityContext: {} + + securityContext: {} + + service: + type: ClusterIP + ports: + - name: http + port: 5007 + protocol: TCP + targetPort: 5007 + - name: tcp + port: 8881 + protocol: TCP + targetPort: 8881 + + ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + + resources: {} + + livenessProbe: + httpGet: + path: /smart-wallets/v1/health + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 15 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /smart-wallets/v1/health + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 15 + successThreshold: 1 + failureThreshold: 3 + + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + + volumes: [] + + volumeMounts: [] + + configMap: + SMART_WALLETS_HOST: "0.0.0.0" + SMART_WALLETS_PORT: "5007" + SMART_WALLETS_TCP_PORT: "8881" + API_HOST: "fusebox-backend-api-service" + API_TCP_PORT: "8876" + ACCOUNTS_HOST: "fusebox-backend-accounts-service" + ACCOUNTS_TCP_PORT: "8875" + CENTRIFUGO_URI: "wss://ws.fuse.io/connection/websocket" + CENTRIFUGO_API_URL: "https://ws.fuse.io/api" + LEGACY_FUSE_WALLET_API_URL: "https://wallet.fuse.io" + CHARGE_BASE_URL: "https://api.fuse.io" + COIN_GECKO_URL: "https://pro-api.coingecko.com/api/v3" + + nodeSelector: {} + + tolerations: [] + + affinity: {} diff --git a/k8s/helm/charts/accounts/templates/externalsecret.yaml b/k8s/helm/charts/accounts/templates/externalsecret.yaml index bef5d2d3..235ed177 100644 --- a/k8s/helm/charts/accounts/templates/externalsecret.yaml +++ b/k8s/helm/charts/accounts/templates/externalsecret.yaml @@ -1,4 +1,4 @@ -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: accounts diff --git a/k8s/helm/charts/api/templates/externalsecret.yaml b/k8s/helm/charts/api/templates/externalsecret.yaml index 1115d4ed..9e21e890 100644 --- a/k8s/helm/charts/api/templates/externalsecret.yaml +++ b/k8s/helm/charts/api/templates/externalsecret.yaml @@ -1,4 +1,4 @@ -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: api diff --git a/k8s/helm/charts/apps/templates/externalsecret.yaml b/k8s/helm/charts/apps/templates/externalsecret.yaml index 8154426f..2521001b 100644 --- a/k8s/helm/charts/apps/templates/externalsecret.yaml +++ b/k8s/helm/charts/apps/templates/externalsecret.yaml @@ -1,4 +1,4 @@ -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: apps diff --git a/k8s/helm/charts/centrifugo/templates/externalsecret.yaml b/k8s/helm/charts/centrifugo/templates/externalsecret.yaml index 7ec99ddb..f3bc0ff2 100644 --- a/k8s/helm/charts/centrifugo/templates/externalsecret.yaml +++ b/k8s/helm/charts/centrifugo/templates/externalsecret.yaml @@ -1,4 +1,4 @@ -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: centrifugo diff --git a/k8s/helm/charts/network/templates/externalsecret.yaml b/k8s/helm/charts/network/templates/externalsecret.yaml index 377f4946..38904ed5 100644 --- a/k8s/helm/charts/network/templates/externalsecret.yaml +++ b/k8s/helm/charts/network/templates/externalsecret.yaml @@ -1,4 +1,4 @@ -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: network diff --git a/k8s/helm/charts/notifications/templates/externalsecret.yaml b/k8s/helm/charts/notifications/templates/externalsecret.yaml index 008a632b..3a8c57f2 100644 --- a/k8s/helm/charts/notifications/templates/externalsecret.yaml +++ b/k8s/helm/charts/notifications/templates/externalsecret.yaml @@ -1,4 +1,4 @@ -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: notifications diff --git a/k8s/helm/charts/relay/templates/externalsecret.yaml b/k8s/helm/charts/relay/templates/externalsecret.yaml index 6aae9d99..08ac91d4 100644 --- a/k8s/helm/charts/relay/templates/externalsecret.yaml +++ b/k8s/helm/charts/relay/templates/externalsecret.yaml @@ -1,4 +1,4 @@ -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: relay diff --git a/k8s/helm/charts/skandha/templates/externalsecret.yaml b/k8s/helm/charts/skandha/templates/externalsecret.yaml index d778ad9a..ccf7f9bb 100644 --- a/k8s/helm/charts/skandha/templates/externalsecret.yaml +++ b/k8s/helm/charts/skandha/templates/externalsecret.yaml @@ -1,4 +1,4 @@ -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: skandha diff --git a/k8s/helm/charts/smart-wallets/templates/externalsecret.yaml b/k8s/helm/charts/smart-wallets/templates/externalsecret.yaml index 79dfbf07..6c5d1a53 100644 --- a/k8s/helm/charts/smart-wallets/templates/externalsecret.yaml +++ b/k8s/helm/charts/smart-wallets/templates/externalsecret.yaml @@ -1,4 +1,4 @@ -apiVersion: external-secrets.io/v1beta1 +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: smart-wallets diff --git a/libs/common/src/exceptions/all-exceptions.filter.ts b/libs/common/src/exceptions/all-exceptions.filter.ts index 42dd8de0..8e4fc8d4 100644 --- a/libs/common/src/exceptions/all-exceptions.filter.ts +++ b/libs/common/src/exceptions/all-exceptions.filter.ts @@ -8,6 +8,7 @@ import { RpcException } from '@nestjs/microservices' import { ServerResponse } from 'http' import { MongoServerError } from 'mongodb' import { throwError } from 'rxjs' +import { get, isPlainObject } from 'lodash' @Catch() export class AllExceptionsFilter implements ExceptionFilter { @@ -36,8 +37,19 @@ export class AllExceptionsFilter implements ExceptionFilter { errorMessage = `${Object.keys(exception?.keyValue)} must be unique` } } else if (exception instanceof RpcException) { - httpStatus = HttpStatus.INTERNAL_SERVER_ERROR - errorMessage = exception.message + // RpcException can have custom status in the error object + const rpcError = exception.getError() + this.logger.debug(`RpcException caught - Raw error object: ${JSON.stringify(rpcError)}`) + + if (isPlainObject(rpcError)) { + httpStatus = get(rpcError, 'status', get(rpcError, 'statusCode', HttpStatus.INTERNAL_SERVER_ERROR)) + errorMessage = get(rpcError, 'error', get(rpcError, 'message', exception.message)) + this.logger.debug(`RpcException - Extracted status: ${httpStatus}, message: ${errorMessage}`) + } else { + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR + errorMessage = exception.message + this.logger.debug(`RpcException - Using defaults, status: ${httpStatus}, message: ${errorMessage}`) + } } else { httpStatus = HttpStatus.INTERNAL_SERVER_ERROR errorMessage = 'Critical Internal Server Error Occurred' @@ -52,7 +64,11 @@ export class AllExceptionsFilter implements ExceptionFilter { } if (host.getType() === 'rpc') { - return throwError(() => ({ message: errorMessage, status: httpStatus })) + this.logger.debug(`Returning RPC error - status: ${httpStatus}, message: ${errorMessage}`) + return throwError(() => ({ + error: errorMessage, // Use 'error' field for consistency + status: httpStatus + })) } response.statusCode = httpStatus diff --git a/libs/common/src/utils/client-proxy.ts b/libs/common/src/utils/client-proxy.ts index e3b827bb..4e411b4e 100644 --- a/libs/common/src/utils/client-proxy.ts +++ b/libs/common/src/utils/client-proxy.ts @@ -1,6 +1,7 @@ import { HttpException, HttpStatus } from '@nestjs/common' import { ClientProxy } from '@nestjs/microservices' import { lastValueFrom, takeLast, catchError, throwError, timeout, TimeoutError } from 'rxjs' +import { get } from 'lodash' export async function callMSFunction (client: ClientProxy, pattern: string, data: any) { const serviceName = (client as any).host || 'UnknownService' @@ -12,12 +13,28 @@ export async function callMSFunction (client: ClientProxy, pattern: string, data takeLast(1), catchError((error) => { console.error(`Error in microservice call to ${serviceName} (pattern: ${pattern}):`, error) + console.debug('Raw error object structure:', { + status: get(error, 'status'), + statusCode: get(error, 'statusCode'), + error: get(error, 'error'), + message: get(error, 'message'), + fullError: JSON.stringify(error) + }) + if (error instanceof TimeoutError) { return throwError(() => new HttpException(`Timeout in ${serviceName} microservice call`, HttpStatus.REQUEST_TIMEOUT)) } + + // Handle RpcException errors - they use 'error' and 'status' fields + // Also support legacy 'message' and 'statusCode' fields for backward compatibility + const statusCode = get(error, 'status', get(error, 'statusCode', HttpStatus.INTERNAL_SERVER_ERROR)) + const message = get(error, 'error', get(error, 'message', 'Unknown error')) + + console.debug(`Extracted from error - statusCode: ${statusCode}, message: ${message}`) + return throwError(() => new HttpException( - `Error in ${serviceName} microservice: ${error.message || 'Unknown error'}`, - error.status || HttpStatus.INTERNAL_SERVER_ERROR + `Error in ${serviceName} microservice: ${message}`, + statusCode )) }) )