fix: one wallet per EncryptID user, deduplicate fund claims
- OpenfortProvider.findOrCreateWallet() searches by player name before creating, ensuring the same email always maps to the same wallet - Fund claims endpoint expires old pending claims before creating new ones - Added expireFundClaim() to db layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5f3ffcc8d2
commit
6c807afeb0
|
|
@ -32,11 +32,44 @@ export class OpenfortProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an embedded wallet for an on-ramp user.
|
* Find an existing wallet by player name, or create a new one.
|
||||||
* Each user gets an Openfort "player" with a smart account on Base.
|
* Ensures one wallet per label (e.g. "user:alice@example.com").
|
||||||
*/
|
*/
|
||||||
async createWallet(label: string, metadata?: Record<string, string>): Promise<WalletInfo> {
|
async findOrCreateWallet(label: string, metadata?: Record<string, string>): Promise<WalletInfo> {
|
||||||
try {
|
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({
|
const player = await this.client.players.create({
|
||||||
name: label,
|
name: label,
|
||||||
metadata: metadata ?? {},
|
metadata: metadata ?? {},
|
||||||
|
|
@ -47,7 +80,7 @@ export class OpenfortProvider {
|
||||||
chainId: this.config.chainId,
|
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 {
|
return {
|
||||||
playerId: player.id,
|
playerId: player.id,
|
||||||
|
|
@ -56,8 +89,15 @@ export class OpenfortProvider {
|
||||||
chainId: this.config.chainId,
|
chainId: this.config.chainId,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use findOrCreateWallet instead
|
||||||
|
*/
|
||||||
|
async createWallet(label: string, metadata?: Record<string, string>): Promise<WalletInfo> {
|
||||||
|
return this.findOrCreateWallet(label, metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,8 +177,8 @@ routes.post("/api/flows/user-onramp", async (c) => {
|
||||||
|| process.env.ONRAMP_PROVIDER
|
|| process.env.ONRAMP_PROVIDER
|
||||||
|| (_coinbaseOnramp ? 'coinbase' : 'transak');
|
|| (_coinbaseOnramp ? 'coinbase' : 'transak');
|
||||||
|
|
||||||
// 1. Create Openfort smart wallet for this user
|
// 1. Find or create Openfort smart wallet for this user (one wallet per email)
|
||||||
const wallet = await _openfort.createWallet(`user:${email}`, {
|
const wallet = await _openfort.findOrCreateWallet(`user:${email}`, {
|
||||||
type: 'user-onramp',
|
type: 'user-onramp',
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1271,6 +1271,10 @@ export async function acceptFundClaim(token: string, userId: string): Promise<St
|
||||||
return rowToFundClaim(rows[0]);
|
return rowToFundClaim(rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function expireFundClaim(claimId: string): Promise<void> {
|
||||||
|
await sql`UPDATE fund_claims SET status = 'expired', email = NULL WHERE id = ${claimId} AND status IN ('pending', 'resent')`;
|
||||||
|
}
|
||||||
|
|
||||||
export async function cleanExpiredFundClaims(): Promise<number> {
|
export async function cleanExpiredFundClaims(): Promise<number> {
|
||||||
// Null out email on expired claims, then mark them expired
|
// 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()`;
|
await sql`UPDATE fund_claims SET email = NULL, status = 'expired' WHERE status IN ('pending', 'resent') AND expires_at < NOW()`;
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ import {
|
||||||
getFundClaimByToken,
|
getFundClaimByToken,
|
||||||
getFundClaimsByEmailHash,
|
getFundClaimsByEmailHash,
|
||||||
acceptFundClaim,
|
acceptFundClaim,
|
||||||
|
expireFundClaim,
|
||||||
cleanExpiredFundClaims,
|
cleanExpiredFundClaims,
|
||||||
sql,
|
sql,
|
||||||
} from './db.js';
|
} from './db.js';
|
||||||
|
|
@ -2930,6 +2931,12 @@ app.post('/api/internal/fund-claims', async (c) => {
|
||||||
const emailHashed = await hashEmail(email);
|
const emailHashed = await hashEmail(email);
|
||||||
const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days
|
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({
|
const claim = await createFundClaim({
|
||||||
id,
|
id,
|
||||||
token,
|
token,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue