Skip to content

Commit f416ed2

Browse files
committed
Harden x402 payment code for production
- Fix timer leak in client.ts with destroy() method - Add retry with backoff to dispute handling - Use BigInt for amount parsing precision - Prevent nonce collisions with counter+random+timestamp - Add fetch timeouts and response validation to jupiter.ts - Fix wallet listener cleanup in embed.ts - Surface verification errors in payai middleware - Clean up verbose comments
1 parent 7a335f2 commit f416ed2

File tree

5 files changed

+121
-170
lines changed

5 files changed

+121
-170
lines changed

packages/kamiyo-x402-client/src/client.ts

Lines changed: 46 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
11
/**
2-
* X402KamiyoClient - x402 payments with Kamiyo escrow protection
3-
*
4-
* Features:
5-
* - x402 protocol v1/v2 compatibility
6-
* - Escrow-backed payments with dispute resolution
7-
* - SLA monitoring and automatic dispute triggering
8-
* - Retry with exponential backoff
9-
* - Circuit breaker for fault tolerance
10-
* - Quality-based graduated refunds
11-
*
12-
* For cross-chain USDC payments, use the PayAIFacilitator class
13-
* which integrates with PayAI Network (https://facilitator.payai.network)
2+
* x402 payment client with Kamiyo escrow protection.
143
*/
154

165
import {
@@ -40,37 +29,22 @@ import {
4029
LIMITS,
4130
} from './validation';
4231

43-
// Types
44-
4532
export interface X402ClientConfig {
46-
/** Solana RPC connection */
4733
connection: Connection;
48-
/** Agent keypair for signing */
4934
wallet: Keypair;
50-
/** Kamiyo program ID */
5135
programId: PublicKey;
52-
/** Auto-dispute if quality falls below threshold (0-100) */
5336
qualityThreshold?: number;
54-
/** Maximum SOL willing to pay per request */
5537
maxPricePerRequest?: number;
56-
/** Default time lock for escrows in seconds */
5738
defaultTimeLock?: number;
58-
/** Enable automatic SLA monitoring */
5939
enableSlaMonitoring?: boolean;
60-
/** Default request timeout in ms */
6140
defaultTimeoutMs?: number;
62-
/** Retry configuration */
6341
retry?: Partial<RetryConfig>;
64-
/** Enable debug logging */
6542
debug?: boolean;
6643
}
6744

6845
export interface SlaParams {
69-
/** Maximum response latency in ms */
7046
maxLatencyMs?: number;
71-
/** Minimum quality score (0-100) */
7247
minQualityScore?: number;
73-
/** Custom validation function */
7448
customValidator?: (response: unknown, latencyMs: number) => SlaValidationResult;
7549
}
7650

@@ -82,19 +56,12 @@ export interface SlaValidationResult {
8256
}
8357

8458
export interface X402RequestOptions {
85-
/** HTTP method */
8659
method?: string;
87-
/** Request headers */
8860
headers?: Record<string, string>;
89-
/** Request body */
9061
body?: string;
91-
/** Use Kamiyo escrow for payment */
9262
useEscrow?: boolean;
93-
/** Custom transaction ID */
9463
transactionId?: string;
95-
/** SLA parameters to enforce */
9664
sla?: SlaParams;
97-
/** Request timeout in ms */
9865
timeoutMs?: number;
9966
}
10067

@@ -155,7 +122,9 @@ interface X402PaymentRequirement {
155122
};
156123
}
157124

158-
// Client Implementation
125+
const CLEANUP_INTERVAL_MS = 60_000;
126+
const SIGNATURE_TTL_MS = 120_000;
127+
const MAX_DISPUTE_RETRIES = 3;
159128

160129
export class X402KamiyoClient {
161130
private readonly connection: Connection;
@@ -170,10 +139,10 @@ export class X402KamiyoClient {
170139
private readonly executor: ResilientExecutor;
171140
private readonly escrowHandler: EscrowHandler;
172141

173-
// Track active escrows
174142
private readonly activeEscrows = new Map<string, EscrowInfo>();
175-
// Track used payment signatures to prevent replay
176143
private readonly usedSignatures = new Map<string, number>();
144+
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
145+
private destroyed = false;
177146

178147
constructor(config: X402ClientConfig) {
179148
// Validate required config
@@ -215,15 +184,9 @@ export class X402KamiyoClient {
215184
programId: this.programId,
216185
});
217186

218-
// Cleanup old signatures periodically
219187
this.startSignatureCleanup();
220188
}
221189

222-
// Public API
223-
224-
/**
225-
* Make HTTP request with automatic x402 payment handling
226-
*/
227190
async request<T = unknown>(
228191
url: string,
229192
options: X402RequestOptions = {}
@@ -309,8 +272,7 @@ export class X402KamiyoClient {
309272
const escrow = this.activeEscrows.get(transactionId);
310273
if (escrow) {
311274
this.log(`SLA violation for ${transactionId}, quality: ${slaResult.qualityScore}`);
312-
// Queue dispute asynchronously
313-
this.queueDispute(escrow, slaResult).catch(e => this.log(`Dispute failed: ${e}`));
275+
this.queueDispute(escrow, slaResult);
314276
}
315277
}
316278
}
@@ -331,9 +293,6 @@ export class X402KamiyoClient {
331293
}
332294
}
333295

334-
/**
335-
* Create escrow for protected payment
336-
*/
337296
async createEscrow(
338297
provider: PublicKey,
339298
amountLamports: number,
@@ -399,9 +358,6 @@ export class X402KamiyoClient {
399358
}
400359
}
401360

402-
/**
403-
* Release escrow funds to provider
404-
*/
405361
async releaseEscrow(transactionId: string): Promise<PaymentResult> {
406362
assertValid(validateTransactionId(transactionId, 'transactionId'), 'transactionId');
407363

@@ -433,9 +389,6 @@ export class X402KamiyoClient {
433389
}
434390
}
435391

436-
/**
437-
* File dispute for an escrow
438-
*/
439392
async disputeEscrow(transactionId: string): Promise<PaymentResult> {
440393
assertValid(validateTransactionId(transactionId, 'transactionId'), 'transactionId');
441394

@@ -467,43 +420,36 @@ export class X402KamiyoClient {
467420
}
468421
}
469422

470-
/**
471-
* Get wallet balance in SOL
472-
*/
473423
async getBalance(): Promise<number> {
474424
const lamports = await this.connection.getBalance(this.wallet.publicKey);
475425
return lamports / LAMPORTS_PER_SOL;
476426
}
477427

478-
/**
479-
* Get public key
480-
*/
481428
getPublicKey(): PublicKey {
482429
return this.wallet.publicKey;
483430
}
484431

485-
/**
486-
* Get active escrows
487-
*/
488432
getActiveEscrows(): Map<string, EscrowInfo> {
489433
return new Map(this.activeEscrows);
490434
}
491435

492-
/**
493-
* Get circuit breaker state
494-
*/
495436
getCircuitState(): string {
496437
return this.executor.getCircuitState();
497438
}
498439

499-
/**
500-
* Reset circuit breaker
501-
*/
502440
resetCircuit(): void {
503441
this.executor.resetCircuit();
504442
}
505443

506-
// Private Methods
444+
destroy(): void {
445+
if (this.cleanupTimer) {
446+
clearInterval(this.cleanupTimer);
447+
this.cleanupTimer = null;
448+
}
449+
this.destroyed = true;
450+
this.activeEscrows.clear();
451+
this.usedSignatures.clear();
452+
}
507453

508454
private async pay(
509455
requirement: X402PaymentRequirement,
@@ -644,10 +590,21 @@ export class X402KamiyoClient {
644590
return { passed: violations.length === 0, qualityScore, violations, metrics };
645591
}
646592

647-
private async queueDispute(escrow: EscrowInfo, slaResult: SlaValidationResult): Promise<void> {
593+
private queueDispute(escrow: EscrowInfo, slaResult: SlaValidationResult): void {
648594
this.log(`Filing dispute for ${escrow.transactionId}: score ${slaResult.qualityScore}`);
649-
// In production, this would call the Kamiyo dispute instruction
650-
// The oracle network evaluates and returns a quality score
595+
596+
const tryDispute = async (attempt: number): Promise<void> => {
597+
if (this.destroyed || attempt >= MAX_DISPUTE_RETRIES) return;
598+
try {
599+
await this.escrowHandler.dispute(escrow.transactionId);
600+
escrow.status = EscrowStatus.Disputed;
601+
} catch (err) {
602+
this.log(`Dispute attempt ${attempt + 1} failed: ${err}`);
603+
setTimeout(() => tryDispute(attempt + 1), 1000 * Math.pow(2, attempt));
604+
}
605+
};
606+
607+
tryDispute(0);
651608
}
652609

653610
private async fetchWithTimeout(
@@ -672,10 +629,17 @@ export class X402KamiyoClient {
672629

673630
private parseAmount(amount: string): number {
674631
const cleaned = amount.replace(/[^0-9.]/g, '');
675-
const value = parseFloat(cleaned);
676-
// If contains decimal point, treat as SOL; otherwise treat as lamports
677-
const hasDecimal = cleaned.includes('.');
678-
return hasDecimal ? Math.floor(value * LAMPORTS_PER_SOL) : Math.floor(value);
632+
if (!cleaned) return 0;
633+
634+
// Use BigInt for precision when possible
635+
if (cleaned.includes('.')) {
636+
const [whole, frac = ''] = cleaned.split('.');
637+
const padded = frac.padEnd(9, '0').slice(0, 9);
638+
const lamports = BigInt(whole || '0') * BigInt(LAMPORTS_PER_SOL) + BigInt(padded);
639+
return Number(lamports);
640+
}
641+
642+
return Number(BigInt(cleaned));
679643
}
680644

681645
private errorResponse<T>(error: X402Error): X402Response<T> {
@@ -689,21 +653,16 @@ export class X402KamiyoClient {
689653
}
690654

691655
private startSignatureCleanup(): void {
692-
// Clean up old signatures every 10 minutes
693-
setInterval(() => {
694-
const cutoff = Date.now() - 600_000; // 10 minutes ago
656+
this.cleanupTimer = setInterval(() => {
657+
if (this.destroyed) return;
658+
const cutoff = Date.now() - SIGNATURE_TTL_MS;
695659
for (const [sig, time] of this.usedSignatures) {
696-
if (time < cutoff) {
697-
this.usedSignatures.delete(sig);
698-
}
660+
if (time < cutoff) this.usedSignatures.delete(sig);
699661
}
700-
}, 600_000);
662+
}, CLEANUP_INTERVAL_MS);
701663
}
702664
}
703665

704-
/**
705-
* Create x402 client with Kamiyo protection
706-
*/
707666
export function createX402KamiyoClient(
708667
connection: Connection,
709668
wallet: Keypair,

packages/kamiyo-x402-client/src/embed.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,5 @@
11
/**
2-
* KAMIYO Pay - Embeddable Payment Widget
3-
*
4-
* Usage:
5-
*
6-
* 1. Custom Element:
7-
* <kamiyo-pay provider="..." amount="1" mode="escrow"></kamiyo-pay>
8-
*
9-
* 2. JavaScript:
10-
* KamiyoPay.create('#container', { provider: '...', amount: 1 })
11-
*
12-
* 3. Programmatic:
13-
* const widget = new KamiyoPayEmbed(element, config);
14-
* await widget.pay();
2+
* Embeddable payment widget for browser environments.
153
*/
164

175
export interface EmbedConfig {
@@ -291,6 +279,7 @@ export class KamiyoPayEmbed {
291279
private escrowId: string | null = null;
292280
private walletAddress: string | null = null;
293281
private abortController: AbortController | null = null;
282+
private walletListeners: { wallet: WalletAdapter; connect: () => void; disconnect: () => void } | null = null;
294283

295284
constructor(container: HTMLElement, config: EmbedConfig) {
296285
validateConfig(config);
@@ -334,11 +323,22 @@ export class KamiyoPayEmbed {
334323
wallet.on?.('connect', handleConnect);
335324
wallet.on?.('disconnect', handleDisconnect);
336325

326+
this.walletListeners = { wallet, connect: handleConnect, disconnect: handleDisconnect };
327+
337328
if (wallet.connected && wallet.publicKey) {
338329
this.walletAddress = wallet.publicKey.toBase58();
339330
}
340331
}
341332

333+
private cleanupWalletListeners(): void {
334+
if (this.walletListeners) {
335+
const { wallet, connect, disconnect } = this.walletListeners;
336+
wallet.off?.('connect', connect);
337+
wallet.off?.('disconnect', disconnect);
338+
this.walletListeners = null;
339+
}
340+
}
341+
342342
getState(): WidgetState {
343343
return {
344344
status: this.status,
@@ -599,6 +599,7 @@ export class KamiyoPayEmbed {
599599

600600
destroy(): void {
601601
this.abortController?.abort();
602+
this.cleanupWalletListeners();
602603
this.container.innerHTML = '';
603604
}
604605
}

0 commit comments

Comments
 (0)