Skip to content
Merged

Qa #577

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b2da0b1
fix: Improve error handling in BundlerApiInterceptor and PaymasterApi…
LiorAgnin Sep 9, 2025
01a09bf
Merge pull request #576 from fuseio/hotfix/logs-and-error-handling
LiorAgnin Sep 9, 2025
28483d3
Merge branch 'master' into qa
LiorAgnin Sep 9, 2025
c93e32e
feat(helm) Change version of external-secrets to v1
JackRooty Sep 9, 2025
35768a4
Test template k8s-deploy-qa.yaml
JackRooty Sep 9, 2025
cb28a63
Rollback helm deploy
JackRooty Sep 9, 2025
341c825
Check that external secret can be created
JackRooty Sep 9, 2025
21a54da
Try force flag
JackRooty Sep 9, 2025
9231b98
Remove whitespace
JackRooty Sep 9, 2025
db29834
Try reset values flag
JackRooty Sep 9, 2025
ea67deb
Add prod to GA
Sep 25, 2025
f885efe
Remove Mongo
Sep 25, 2025
525226d
change template to deploy for helm
Sep 25, 2025
cbf6f42
fix: Enhance error handling in Smart Wallet services
LiorAgnin Sep 28, 2025
ed82309
Merge pull request #579 from fuseio/feat/improve-error-handling
LiorAgnin Sep 28, 2025
271b9ad
fix: Improve error logging and handling in RelayAPIService and client…
LiorAgnin Sep 30, 2025
f51f34f
Merge pull request #581 from fuseio/fix/microservice-error-status-codes
NikolayRn Sep 30, 2025
74ac7ec
fix: Standardize error handling in Smart Wallet services
LiorAgnin Sep 30, 2025
bfdefe0
Merge pull request #582 from fuseio/fix/microservice-error-status-codes
NikolayRn Sep 30, 2025
7c1b249
Migration
Oct 20, 2025
2e5094d
fix temp names and notification service port
Oct 21, 2025
995667c
Merge pull request #583 from fuseio/Add-Prod-to-OVH
JackRooty Oct 21, 2025
5285c2e
Change prod branch name fo master ( for CI )
Oct 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .github/workflows/k8s-deploy-prod.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 2 additions & 0 deletions .github/workflows/k8s-deploy-qa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ jobs:
- name: Release
run: |
helm upgrade \
--force \
--reset-values \
--install \
--history-max 3 \
--debug \
Expand Down
41 changes: 30 additions & 11 deletions apps/charge-api-service/src/bundler-api/bundler-api.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
144 changes: 67 additions & 77 deletions apps/charge-api-service/src/paymaster-api/paymaster-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -132,90 +131,81 @@ export class PaymasterApiService {
}

async estimateUserOpGas (op, requestEnvironment, entrypointAddress): Promise<GasDetails> {
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
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading