feature:timed expiry of wallets is implemented in the backend and can be integrated with frontend when needed.#55
Conversation
… be integrated on the frontend anytime
|
@0xvikram is attempting to deploy a commit to the Tempwallets Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Pull request overview
This PR implements timed wallet expiry functionality for the backend, allowing wallets to be automatically deleted after a specified time period. The implementation adds an optional ttlHours parameter to wallet creation (1-8760 hours), stores expiry timestamps in the database, and includes a scheduled cleanup service that runs hourly to remove expired wallets.
Changes:
- Added
@nestjs/scheduledependency and scheduled cleanup service to automatically delete expired wallets hourly - Extended database schema with
expiresAtfield inWalletSeedtable and added expiry validation inSeedRepository - Added
ttlHoursparameter (1-8760 hours) to wallet creation API with DTO validation and expiry calculation logic
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Added @nestjs/schedule v6.1.0 and its dependencies (cron, luxon) for scheduled task support |
| apps/backend/package.json | Added @nestjs/schedule dependency to enable cron-based cleanup jobs |
| apps/backend/prisma/schema.prisma | Added expiresAt field to WalletSeed model with index, and walletExpiresAt to User model |
| apps/backend/prisma/migrations/20260126061639_add_wallet_expiry/migration.sql | Database migration adding expiry timestamp columns and index |
| apps/backend/src/wallet/wallet.module.ts | Registered ScheduleModule and WalletCleanupService in the module |
| apps/backend/src/wallet/services/wallet.cleanup.service.ts | New service implementing hourly cron job to delete expired wallets for both registered and unregistered users |
| apps/backend/src/wallet/seed.repository.ts | Updated to store and validate expiry timestamps, throws ForbiddenException for expired wallets |
| apps/backend/src/wallet/managers/seed.manager.ts | Added TTL calculation logic converting hours to milliseconds and creating expiry Date |
| apps/backend/src/wallet/wallet.service.ts | Integrated ttlHours parameter into seed creation flow |
| apps/backend/src/wallet/wallet.controller.ts | Added ttlHours to createOrImportSeed endpoint with enhanced logging |
| apps/backend/src/wallet/dto/wallet.dto.ts | Added ttlHours validation with @min(1) and @max(8760) decorators |
| apps/web/lib/api.ts | Added WALLET_EXPIRY_OPTIONS constant and ttlHours to CreateOrImportSeedRequest interface |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| // Delete entire user record for burner wallets | ||
| if (unregisteredUserIds.length > 0) { | ||
| const burnerResult = await this.prisma.user.deleteMany({ | ||
| where: { id: { in: unregisteredUserIds } }, | ||
| }); | ||
| deletedBurnerWallets = burnerResult.count; | ||
| } | ||
|
|
||
| // Delete only WalletSeed for registered users | ||
| if (registeredUserIds.length > 0) { | ||
| // Clean up cached addresses | ||
| await this.prisma.walletAddressCache.deleteMany({ | ||
| where: { fingerprint: { in: registeredUserIds } }, | ||
| }); | ||
|
|
||
| const seedResult = await this.prisma.walletSeed.deleteMany({ | ||
| where: { userId: { in: registeredUserIds } }, | ||
| }); | ||
| deletedRegisteredSeeds = seedResult.count; | ||
| } |
There was a problem hiding this comment.
The WalletCache table cleanup is missing in the manual cleanup method. Similar to the scheduled cleanup, both unregistered and registered user cleanups should include deletion of WalletCache records to ensure complete cleanup of expired wallets.
| const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5005'; | ||
| // Wallet expiry time options (in hours) | ||
| export const WALLET_EXPIRY_OPTIONS = [ | ||
| { label: '10 minutes', hours: 10 / 60 }, |
There was a problem hiding this comment.
The first option has a formatting inconsistency - it uses division (10 / 60) instead of a decimal literal. For consistency with the other options and to avoid potential floating-point precision issues, change this to hours: 0.167 or hours: 1/6.
| { label: '10 minutes', hours: 10 / 60 }, | |
| { label: '10 minutes', hours: 0.167 }, |
| @IsOptional() | ||
| @Min(1) | ||
| @Max(8760) |
There was a problem hiding this comment.
The validation decorators order should be corrected. The @IsNumber() decorator should come before @IsOptional() for better validation flow. Additionally, @min(1) allows fractional hours like 0.5 (30 minutes) to fail validation, but the frontend WALLET_EXPIRY_OPTIONS includes fractional values like 0.5 and 0.167. Consider changing @min(1) to @min(0.001) or documenting that only whole hours are supported on the backend.
| @IsOptional() | |
| @Min(1) | |
| @Max(8760) | |
| @Min(0.001) | |
| @Max(8760) | |
| @IsOptional() |
| ALTER TABLE "User" ADD COLUMN "walletExpiresAt" TIMESTAMP(3); | ||
|
|
||
| -- AlterTable |
There was a problem hiding this comment.
The migration adds the walletExpiresAt column to the User table, but this field is never used in the codebase. The wallet expiry functionality uses the expiresAt field in the WalletSeed table instead. Consider removing this column from both the schema and the migration to avoid confusion and database bloat.
| ALTER TABLE "User" ADD COLUMN "walletExpiresAt" TIMESTAMP(3); | |
| -- AlterTable |
| import { AptosAddressManager } from './aptos/managers/aptos-address.manager.js'; | ||
| import { AptosAccountFactory } from './aptos/factories/aptos-account.factory.js'; | ||
| import { AptosRpcService } from './aptos/services/aptos-rpc.service.js'; | ||
| //imported scheduled call and wallet cleanup service |
There was a problem hiding this comment.
The comment formatting is inconsistent with the rest of the file. Other import comments in this file use capital letters and proper punctuation (e.g., "// Import Substrate module"). Change to "// Import scheduled task and wallet cleanup service" for consistency.
| //imported scheduled call and wallet cleanup service | |
| // Import scheduled task and wallet cleanup service |
| xp Int @default(0) // Experience points | ||
| createdAt DateTime @default(now()) | ||
| lastLoginAt DateTime @updatedAt | ||
| walletExpiresAt DateTime? |
There was a problem hiding this comment.
The walletExpiresAt field is added to the User model but is never used in the codebase. The wallet expiry functionality uses the expiresAt field in the WalletSeed model instead. This field should either be removed or implemented if there's a different intended purpose.
| walletExpiresAt DateTime? |
| // Step 3a: Delete ENTIRE user record for unregistered burner wallets | ||
| // (CASCADE deletes WalletSeed, addresses, etc.) | ||
| if (unregisteredUserIds.length > 0) { | ||
| const burnerResult = await this.prisma.user.deleteMany({ | ||
| where: { id: { in: unregisteredUserIds } }, | ||
| }); | ||
| deletedBurnerWallets = burnerResult.count; | ||
| this.logger.log( | ||
| `Deleted ${deletedBurnerWallets} expired burner wallet(s) (entire user records)`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
The WalletCache table (which stores cached balances keyed by fingerprint/userId) is not being cleaned up when users are deleted. When deleting unregistered users, the cleanup service should also delete corresponding WalletCache records to avoid orphaned data. Add a deletion statement similar to the WalletAddressCache cleanup at line 81.
| // Step 3b: For registered users, delete ONLY the WalletSeed | ||
| // Keep User record so they can create a new wallet | ||
| if (registeredUserIds.length > 0) { | ||
| // Also delete cached addresses for these users | ||
| await this.prisma.walletAddressCache.deleteMany({ | ||
| where: { fingerprint: { in: registeredUserIds } }, | ||
| }); | ||
|
|
||
| const seedResult = await this.prisma.walletSeed.deleteMany({ | ||
| where: { userId: { in: registeredUserIds } }, | ||
| }); | ||
| deletedRegisteredSeeds = seedResult.count; | ||
| this.logger.log( | ||
| `Deleted ${deletedRegisteredSeeds} expired seed(s) for registered users (kept user accounts)`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
The WalletCache table cleanup is missing for registered users as well. When deleting expired seeds for registered users, the corresponding WalletCache records should also be deleted to ensure complete cleanup. Add a WalletCache deletion statement before or after the WalletAddressCache deletion.
The flow for the backend
User creates wallet → Sets expiry (e.g., "1 hour")
↓
Frontend sends ttlHours: 1
↓
Controller receives & validates (1-8760 hours allowed)
↓
Service passes to SeedManager
↓
SeedManager calculates: expiresAt = now + (ttlHours * 60 * 60 * 1000)
↓
Repository stores encrypted seed with expiresAt timestamp
I have tested it on the backend, and it is working fine
