diff --git a/modules/rflows/lib/openfort.ts b/modules/rflows/lib/openfort.ts index 1f4118c..c69f97d 100644 --- a/modules/rflows/lib/openfort.ts +++ b/modules/rflows/lib/openfort.ts @@ -32,11 +32,44 @@ export class OpenfortProvider { } /** - * Create an embedded wallet for an on-ramp user. - * Each user gets an Openfort "player" with a smart account on Base. + * Find an existing wallet by player name, or create a new one. + * Ensures one wallet per label (e.g. "user:alice@example.com"). */ - async createWallet(label: string, metadata?: Record): Promise { + async findOrCreateWallet(label: string, metadata?: Record): Promise { try { + // Search for existing player by name + const existing = await this.client.players.list({ name: label, limit: 1 }); + if (existing.data.length > 0) { + const player = existing.data[0]; + const accounts = await this.client.accounts.v1.list({ + player: player.id, + chainId: this.config.chainId, + }); + if (accounts.data.length > 0) { + const account = accounts.data[0]; + console.log(`[openfort] Found existing wallet: player=${player.id} address=${account.address} label=${label}`); + return { + playerId: player.id, + accountId: account.id, + address: account.address, + chainId: this.config.chainId, + }; + } + // Player exists but no account on this chain — create one + const account = await this.client.accounts.v1.create({ + player: player.id, + chainId: this.config.chainId, + }); + console.log(`[openfort] Created account for existing player: player=${player.id} address=${account.address}`); + return { + playerId: player.id, + accountId: account.id, + address: account.address, + chainId: this.config.chainId, + }; + } + + // No existing player — create new const player = await this.client.players.create({ name: label, metadata: metadata ?? {}, @@ -47,7 +80,7 @@ export class OpenfortProvider { chainId: this.config.chainId, }); - console.log(`[openfort] Created wallet: player=${player.id} address=${account.address} label=${label}`); + console.log(`[openfort] Created new wallet: player=${player.id} address=${account.address} label=${label}`); return { playerId: player.id, @@ -56,8 +89,15 @@ export class OpenfortProvider { chainId: this.config.chainId, }; } catch (error) { - console.error(`[openfort] Failed to create wallet for "${label}":`, error); + console.error(`[openfort] Failed to find/create wallet for "${label}":`, error); throw error; } } + + /** + * @deprecated Use findOrCreateWallet instead + */ + async createWallet(label: string, metadata?: Record): Promise { + return this.findOrCreateWallet(label, metadata); + } } diff --git a/modules/rflows/mod.ts b/modules/rflows/mod.ts index ac998c9..8b89c4f 100644 --- a/modules/rflows/mod.ts +++ b/modules/rflows/mod.ts @@ -177,8 +177,8 @@ routes.post("/api/flows/user-onramp", async (c) => { || process.env.ONRAMP_PROVIDER || (_coinbaseOnramp ? 'coinbase' : 'transak'); - // 1. Create Openfort smart wallet for this user - const wallet = await _openfort.createWallet(`user:${email}`, { + // 1. Find or create Openfort smart wallet for this user (one wallet per email) + const wallet = await _openfort.findOrCreateWallet(`user:${email}`, { type: 'user-onramp', email, }); diff --git a/src/encryptid/db.ts b/src/encryptid/db.ts index d403a78..a2eb4cb 100644 --- a/src/encryptid/db.ts +++ b/src/encryptid/db.ts @@ -1271,6 +1271,10 @@ export async function acceptFundClaim(token: string, userId: string): Promise { + await sql`UPDATE fund_claims SET status = 'expired', email = NULL WHERE id = ${claimId} AND status IN ('pending', 'resent')`; +} + export async function cleanExpiredFundClaims(): Promise { // Null out email on expired claims, then mark them expired await sql`UPDATE fund_claims SET email = NULL, status = 'expired' WHERE status IN ('pending', 'resent') AND expires_at < NOW()`; diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index b5f4605..b67bbdc 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -77,6 +77,7 @@ import { getFundClaimByToken, getFundClaimsByEmailHash, acceptFundClaim, + expireFundClaim, cleanExpiredFundClaims, sql, } from './db.js'; @@ -2930,6 +2931,12 @@ app.post('/api/internal/fund-claims', async (c) => { const emailHashed = await hashEmail(email); const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days + // Expire any existing pending claims for this email (one active claim per user) + const existingClaims = await getFundClaimsByEmailHash(emailHashed); + for (const ec of existingClaims) { + await expireFundClaim(ec.id); + } + const claim = await createFundClaim({ id, token,