• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-client-gateway / 20091945613

10 Dec 2025 08:19AM UTC coverage: 89.03% (-1.2%) from 90.212%
20091945613

push

github

web-flow
refactor(cache): change Zerion cache keys to be per address only (#2839)

* refactor(cache): change Zerion cache keys to be per address only

Remove chainId from Zerion cache keys for balances, collectibles, and positions.
This changes the caching strategy from per-chain-per-address to per-address only,
allowing data to be shared across chains for the same address.

Cache key changes:
- Before: {chainId}_zerion_balances_{safeAddress}
- After: zerion_balances_{safeAddress}

Similar changes applied to positions and collectibles cache keys.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Prettier

* refactor: remove unused chainId parameter from clear methods

Remove chainId parameter from clearBalances and clearCollectibles methods
in IBalancesApi interface and implementations. After the refactoring to
per-address cache keys, chainId is no longer needed by the API methods:

- ZerionBalancesApi uses address-only cache keys
- SafeBalancesApi uses its instance chainId variable

The repositories still accept chainId (needed to select the correct API)
but no longer pass it to the clear methods.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Fix tests: Separate unit tests from integration/e2e tests

- Renamed tests requiring real infrastructure (.spec.ts -> .integration.spec.ts or .e2e-spec.ts)
- Updated package.json Jest config to exclude integration and e2e tests from default 'yarn test' run
- Tests requiring Redis, Postgres, or full app bootstrap are now properly categorized
- All 308 unit test suites now pass without requiring external services

Renamed files:
- Redis cache service test -> integration test
- Postgres database module tests (v1 & v2) -> integration tests
- Postgres database service test -> integration test
- Auth decorator test -> e2e test
- Spaces-related controller tests -> e2e tests
- ... (continued)

2835 of 3551 branches covered (79.84%)

Branch coverage included in aggregate %.

4 of 8 new or added lines in 5 files covered. (50.0%)

199 existing lines in 34 files now uncovered.

13616 of 14927 relevant lines covered (91.22%)

564.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.0
/src/modules/users/routes/users.controller.ts
1
import {
92✔
2
  ApiConflictResponse,
3
  ApiNotFoundResponse,
4
  ApiOkResponse,
5
  ApiTags,
6
  ApiUnauthorizedResponse,
7
  ApiOperation,
8
  ApiParam,
9
  ApiBody,
10
  ApiNoContentResponse,
11
  ApiCreatedResponse,
12
} from '@nestjs/swagger';
13
import {
92✔
14
  Body,
15
  Controller,
16
  Delete,
17
  Get,
18
  Param,
19
  Post,
20
  UseGuards,
21
} from '@nestjs/common';
22
import { AuthGuard } from '@/modules/auth/routes/guards/auth.guard';
92✔
23
import { Auth } from '@/modules/auth/routes/decorators/auth.decorator';
92✔
24
import { UsersService } from '@/modules/users/routes/users.service';
92✔
25
import { ValidationPipe } from '@/validation/pipes/validation.pipe';
92✔
26
import { AddressSchema } from '@/validation/entities/schemas/address.schema';
92✔
27
import { UserWithWallets } from '@/modules/users/routes/entities/user-with-wallets.entity';
92✔
28
import { CreatedUserWithWallet } from '@/modules/users/routes/entities/created-user-with-wallet.entity';
92✔
29
import { WalletAddedToUser } from '@/modules/users/routes/entities/wallet-added-to-user.entity';
92✔
30
import { SiweDtoSchema } from '@/modules/auth/routes/entities/siwe.dto.entity';
92✔
31
import { SiweDto } from '@/modules/auth/routes/entities/siwe.dto.entity';
92✔
32
import type { AuthPayload } from '@/modules/auth/domain/entities/auth-payload.entity';
33
import { type Address } from 'viem';
34

35
@ApiTags('users')
36
@Controller({ path: 'users', version: '1' })
37
export class UsersController {
92✔
UNCOV
38
  public constructor(private readonly usersService: UsersService) {}
×
39

40
  @ApiOperation({
41
    summary: 'Get user with wallets',
42
    description:
43
      'Retrieves the authenticated user information along with all associated wallet addresses.',
44
  })
45
  @ApiOkResponse({
46
    type: UserWithWallets,
47
    description:
48
      'User information with associated wallets retrieved successfully',
49
  })
50
  @ApiUnauthorizedResponse({
51
    description: 'Authentication required - valid JWT token must be provided',
52
  })
53
  @ApiNotFoundResponse({
54
    description:
55
      'User not found - the authenticated wallet is not associated with any user',
56
  })
57
  @Get()
58
  @UseGuards(AuthGuard)
59
  public async getWithWallets(
92✔
60
    @Auth() authPayload: AuthPayload,
61
  ): Promise<UserWithWallets> {
UNCOV
62
    return await this.usersService.getWithWallets(authPayload);
×
63
  }
64

65
  @ApiOperation({
66
    summary: 'Delete user',
67
    description:
68
      'Deletes the authenticated user and all associated data including wallets and account information.',
69
  })
70
  @ApiNoContentResponse({
71
    description: 'User deleted successfully',
72
  })
73
  @ApiUnauthorizedResponse({
74
    description: 'Authentication required - valid JWT token must be provided',
75
  })
76
  @ApiNotFoundResponse({
77
    description:
78
      'User not found - the authenticated wallet is not associated with any user',
79
  })
80
  @Delete()
81
  @UseGuards(AuthGuard)
82
  public async delete(@Auth() authPayload: AuthPayload): Promise<void> {
92✔
UNCOV
83
    return await this.usersService.delete(authPayload);
×
84
  }
85

86
  // @todo move wallet methods to Wallet controller
87
  @ApiOperation({
88
    summary: 'Create user with wallet',
89
    description:
90
      'Creates a new user account associated with the authenticated wallet address.',
91
  })
92
  @ApiCreatedResponse({
93
    type: CreatedUserWithWallet,
94
    description: 'User created successfully with wallet association',
95
  })
96
  @ApiUnauthorizedResponse({
97
    description: 'Authentication required - valid JWT token must be provided',
98
  })
99
  @ApiConflictResponse({
100
    description:
101
      'Wallet already exists - this wallet is already associated with a user',
102
  })
103
  @Post('/wallet')
104
  @UseGuards(AuthGuard)
105
  public async createWithWallet(
92✔
106
    @Auth() authPayload: AuthPayload,
107
  ): Promise<CreatedUserWithWallet> {
UNCOV
108
    return await this.usersService.createWithWallet(authPayload);
×
109
  }
110

111
  @ApiOperation({
112
    summary: 'Add wallet to user',
113
    description:
114
      'Associates an additional wallet address with the authenticated user account using Sign-In with Ethereum (SiWE) verification.',
115
  })
116
  @ApiBody({
117
    type: SiweDto,
118
    description:
119
      'Sign-In with Ethereum message and signature for the wallet to add',
120
  })
121
  @ApiOkResponse({
122
    type: WalletAddedToUser,
123
    description: 'Wallet added to user successfully',
124
  })
125
  @ApiUnauthorizedResponse({
126
    description: 'Authentication required or invalid SiWE message/signature',
127
  })
128
  @ApiConflictResponse({
129
    description:
130
      'Wallet already exists - this wallet is already associated with a user',
131
  })
132
  @ApiNotFoundResponse({
133
    description:
134
      'User not found - the authenticated wallet is not associated with any user',
135
  })
136
  @Post('/wallet/add')
137
  @UseGuards(AuthGuard)
138
  public async addWalletToUser(
92✔
139
    @Auth() authPayload: AuthPayload,
140
    @Body(new ValidationPipe(SiweDtoSchema))
141
    siweDto: SiweDto,
142
  ): Promise<WalletAddedToUser> {
UNCOV
143
    return await this.usersService.addWalletToUser({
×
144
      authPayload,
145
      siweDto,
146
    });
147
  }
148

149
  @ApiOperation({
150
    summary: 'Remove wallet from user',
151
    description:
152
      'Removes a wallet address from the authenticated user account. Cannot remove the currently authenticated wallet.',
153
  })
154
  @ApiParam({
155
    name: 'walletAddress',
156
    type: 'string',
157
    description: 'Wallet address to remove (0x prefixed hex string)',
158
  })
159
  @ApiNoContentResponse({
160
    description: 'Wallet removed from user successfully',
161
  })
162
  @ApiUnauthorizedResponse({
163
    description: 'Authentication required - valid JWT token must be provided',
164
  })
165
  @ApiConflictResponse({
166
    description:
167
      'Cannot remove the current wallet - use a different wallet to authenticate',
168
  })
169
  @ApiNotFoundResponse({
170
    description: 'User or specified wallet not found',
171
  })
172
  @Delete('/wallet/:walletAddress')
173
  @UseGuards(AuthGuard)
174
  public async deleteWalletFromUser(
92✔
175
    @Param('walletAddress', new ValidationPipe(AddressSchema))
176
    walletAddress: Address,
177
    @Auth() authPayload: AuthPayload,
178
  ): Promise<void> {
UNCOV
179
    return await this.usersService.deleteWalletFromUser({
×
180
      authPayload,
181
      walletAddress,
182
    });
183
  }
184
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc