@zendfi/sdk 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -4
- package/dist/cache-T5YPC7OK.mjs +9 -0
- package/dist/{chunk-YFOBPGQE.mjs → chunk-3ACJUM6V.mjs} +0 -8
- package/dist/chunk-5O5NAX65.mjs +366 -0
- package/dist/chunk-XERHBDUK.mjs +587 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/device-bound-crypto-VX7SFVHT.mjs +13 -0
- package/dist/express.d.mts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/express.mjs +2 -1
- package/dist/index.d.mts +1062 -3
- package/dist/index.d.ts +1062 -3
- package/dist/index.js +3185 -609
- package/dist/index.mjs +2181 -586
- package/dist/nextjs.d.mts +1 -1
- package/dist/nextjs.d.ts +1 -1
- package/dist/nextjs.mjs +2 -1
- package/dist/{webhook-handler-D5CigE9G.d.mts → webhook-handler-DGBeCWT-.d.mts} +20 -0
- package/dist/{webhook-handler-D5CigE9G.d.ts → webhook-handler-DGBeCWT-.d.ts} +20 -0
- package/package.json +5 -4
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
-
__require,
|
|
3
2
|
processWebhook
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3ACJUM6V.mjs";
|
|
4
|
+
import {
|
|
5
|
+
DeviceBoundSessionKey,
|
|
6
|
+
DeviceFingerprintGenerator,
|
|
7
|
+
RecoveryQRGenerator,
|
|
8
|
+
SessionKeyCrypto
|
|
9
|
+
} from "./chunk-XERHBDUK.mjs";
|
|
10
|
+
import {
|
|
11
|
+
QuickCaches,
|
|
12
|
+
SessionKeyCache
|
|
13
|
+
} from "./chunk-5O5NAX65.mjs";
|
|
14
|
+
import {
|
|
15
|
+
__require
|
|
16
|
+
} from "./chunk-Y6FXYEAI.mjs";
|
|
5
17
|
|
|
6
18
|
// src/client.ts
|
|
7
19
|
import fetch2 from "cross-fetch";
|
|
@@ -1199,6 +1211,43 @@ var AutonomyAPI = class {
|
|
|
1199
1211
|
throw new Error("delegation_signature must be base64 encoded");
|
|
1200
1212
|
}
|
|
1201
1213
|
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Get spending attestations for a delegate (audit trail)
|
|
1216
|
+
*
|
|
1217
|
+
* Returns all cryptographically signed attestations ZendFi created for
|
|
1218
|
+
* this delegate. Each attestation contains:
|
|
1219
|
+
* - The spending state at the time of payment
|
|
1220
|
+
* - ZendFi's Ed25519 signature
|
|
1221
|
+
* - Timestamp and nonce (for replay protection)
|
|
1222
|
+
*
|
|
1223
|
+
* These attestations can be independently verified using ZendFi's public key
|
|
1224
|
+
* to confirm spending limit enforcement was applied correctly.
|
|
1225
|
+
*
|
|
1226
|
+
* @param delegateId - UUID of the autonomous delegate
|
|
1227
|
+
* @returns Attestation audit response with all signed attestations
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* ```typescript
|
|
1231
|
+
* const audit = await zendfi.autonomy.getAttestations('delegate_123...');
|
|
1232
|
+
*
|
|
1233
|
+
* console.log(`Found ${audit.attestation_count} attestations`);
|
|
1234
|
+
* console.log(`ZendFi public key: ${audit.zendfi_attestation_public_key}`);
|
|
1235
|
+
*
|
|
1236
|
+
* // Verify each attestation independently
|
|
1237
|
+
* for (const signed of audit.attestations) {
|
|
1238
|
+
* console.log(`Payment ${signed.attestation.payment_id}:`);
|
|
1239
|
+
* console.log(` Requested: $${signed.attestation.requested_usd}`);
|
|
1240
|
+
* console.log(` Remaining after: $${signed.attestation.remaining_after_usd}`);
|
|
1241
|
+
* // Verify signature with nacl.sign.detached.verify()
|
|
1242
|
+
* }
|
|
1243
|
+
* ```
|
|
1244
|
+
*/
|
|
1245
|
+
async getAttestations(delegateId) {
|
|
1246
|
+
return this.request(
|
|
1247
|
+
"GET",
|
|
1248
|
+
`/api/v1/ai/delegates/${delegateId}/attestations`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1202
1251
|
};
|
|
1203
1252
|
|
|
1204
1253
|
// src/api/smart-payments.ts
|
|
@@ -2386,589 +2435,8 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
2386
2435
|
});
|
|
2387
2436
|
}
|
|
2388
2437
|
|
|
2389
|
-
// src/device-bound-crypto.ts
|
|
2390
|
-
import { Keypair } from "@solana/web3.js";
|
|
2391
|
-
import * as crypto from "crypto";
|
|
2392
|
-
var DeviceFingerprintGenerator = class {
|
|
2393
|
-
/**
|
|
2394
|
-
* Generate a unique device fingerprint
|
|
2395
|
-
* Combines multiple browser attributes for uniqueness
|
|
2396
|
-
*/
|
|
2397
|
-
static async generate() {
|
|
2398
|
-
const components = {};
|
|
2399
|
-
try {
|
|
2400
|
-
components.canvas = await this.getCanvasFingerprint();
|
|
2401
|
-
components.webgl = await this.getWebGLFingerprint();
|
|
2402
|
-
components.audio = await this.getAudioFingerprint();
|
|
2403
|
-
components.screen = `${screen.width}x${screen.height}x${screen.colorDepth}`;
|
|
2404
|
-
components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2405
|
-
components.languages = navigator.languages?.join(",") || navigator.language;
|
|
2406
|
-
components.platform = navigator.platform;
|
|
2407
|
-
components.hardwareConcurrency = navigator.hardwareConcurrency?.toString() || "unknown";
|
|
2408
|
-
const combined = Object.entries(components).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("|");
|
|
2409
|
-
const fingerprint = await this.sha256(combined);
|
|
2410
|
-
return {
|
|
2411
|
-
fingerprint,
|
|
2412
|
-
generatedAt: Date.now(),
|
|
2413
|
-
components
|
|
2414
|
-
};
|
|
2415
|
-
} catch (error) {
|
|
2416
|
-
console.warn("Device fingerprinting failed, using fallback", error);
|
|
2417
|
-
return this.generateFallbackFingerprint();
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
/**
|
|
2421
|
-
* Graceful fallback fingerprint generation
|
|
2422
|
-
* Works in headless browsers, SSR, and restricted environments
|
|
2423
|
-
*/
|
|
2424
|
-
static async generateFallbackFingerprint() {
|
|
2425
|
-
const components = {};
|
|
2426
|
-
try {
|
|
2427
|
-
if (typeof navigator !== "undefined") {
|
|
2428
|
-
components.platform = navigator.platform || "unknown";
|
|
2429
|
-
components.languages = navigator.languages?.join(",") || navigator.language || "unknown";
|
|
2430
|
-
components.hardwareConcurrency = navigator.hardwareConcurrency?.toString() || "unknown";
|
|
2431
|
-
}
|
|
2432
|
-
if (typeof screen !== "undefined") {
|
|
2433
|
-
components.screen = `${screen.width || 0}x${screen.height || 0}x${screen.colorDepth || 0}`;
|
|
2434
|
-
}
|
|
2435
|
-
if (typeof Intl !== "undefined") {
|
|
2436
|
-
try {
|
|
2437
|
-
components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2438
|
-
} catch {
|
|
2439
|
-
components.timezone = "unknown";
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
} catch {
|
|
2443
|
-
components.platform = "fallback";
|
|
2444
|
-
}
|
|
2445
|
-
let randomEntropy = "";
|
|
2446
|
-
try {
|
|
2447
|
-
if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
|
|
2448
|
-
const arr = new Uint8Array(16);
|
|
2449
|
-
window.crypto.getRandomValues(arr);
|
|
2450
|
-
randomEntropy = Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2451
|
-
} else if (typeof crypto !== "undefined" && crypto.randomBytes) {
|
|
2452
|
-
randomEntropy = crypto.randomBytes(16).toString("hex");
|
|
2453
|
-
}
|
|
2454
|
-
} catch {
|
|
2455
|
-
randomEntropy = Date.now().toString(36) + Math.random().toString(36);
|
|
2456
|
-
}
|
|
2457
|
-
const combined = Object.entries(components).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("|") + "|entropy:" + randomEntropy;
|
|
2458
|
-
const fingerprint = await this.sha256(combined);
|
|
2459
|
-
return {
|
|
2460
|
-
fingerprint,
|
|
2461
|
-
generatedAt: Date.now(),
|
|
2462
|
-
components
|
|
2463
|
-
};
|
|
2464
|
-
}
|
|
2465
|
-
static async getCanvasFingerprint() {
|
|
2466
|
-
const canvas = document.createElement("canvas");
|
|
2467
|
-
const ctx = canvas.getContext("2d");
|
|
2468
|
-
if (!ctx) return "no-canvas";
|
|
2469
|
-
canvas.width = 200;
|
|
2470
|
-
canvas.height = 50;
|
|
2471
|
-
ctx.textBaseline = "top";
|
|
2472
|
-
ctx.font = '14px "Arial"';
|
|
2473
|
-
ctx.fillStyle = "#f60";
|
|
2474
|
-
ctx.fillRect(0, 0, 100, 50);
|
|
2475
|
-
ctx.fillStyle = "#069";
|
|
2476
|
-
ctx.fillText("ZendFi \u{1F510}", 2, 2);
|
|
2477
|
-
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
2478
|
-
ctx.fillText("Device-Bound", 4, 17);
|
|
2479
|
-
return canvas.toDataURL();
|
|
2480
|
-
}
|
|
2481
|
-
static async getWebGLFingerprint() {
|
|
2482
|
-
const canvas = document.createElement("canvas");
|
|
2483
|
-
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
2484
|
-
if (!gl) return "no-webgl";
|
|
2485
|
-
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
2486
|
-
if (!debugInfo) return "no-debug-info";
|
|
2487
|
-
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
2488
|
-
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
2489
|
-
return `${vendor}|${renderer}`;
|
|
2490
|
-
}
|
|
2491
|
-
static async getAudioFingerprint() {
|
|
2492
|
-
try {
|
|
2493
|
-
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
2494
|
-
if (!AudioContext) return "no-audio";
|
|
2495
|
-
const context = new AudioContext();
|
|
2496
|
-
const oscillator = context.createOscillator();
|
|
2497
|
-
const analyser = context.createAnalyser();
|
|
2498
|
-
const gainNode = context.createGain();
|
|
2499
|
-
const scriptProcessor = context.createScriptProcessor(4096, 1, 1);
|
|
2500
|
-
gainNode.gain.value = 0;
|
|
2501
|
-
oscillator.connect(analyser);
|
|
2502
|
-
analyser.connect(scriptProcessor);
|
|
2503
|
-
scriptProcessor.connect(gainNode);
|
|
2504
|
-
gainNode.connect(context.destination);
|
|
2505
|
-
oscillator.start(0);
|
|
2506
|
-
return new Promise((resolve) => {
|
|
2507
|
-
scriptProcessor.onaudioprocess = (event) => {
|
|
2508
|
-
const output = event.inputBuffer.getChannelData(0);
|
|
2509
|
-
const hash = Array.from(output.slice(0, 30)).reduce((acc, val) => acc + Math.abs(val), 0);
|
|
2510
|
-
oscillator.stop();
|
|
2511
|
-
scriptProcessor.disconnect();
|
|
2512
|
-
context.close();
|
|
2513
|
-
resolve(hash.toString());
|
|
2514
|
-
};
|
|
2515
|
-
});
|
|
2516
|
-
} catch (error) {
|
|
2517
|
-
return "audio-error";
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
static async sha256(data) {
|
|
2521
|
-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
2522
|
-
const encoder = new TextEncoder();
|
|
2523
|
-
const dataBuffer = encoder.encode(data);
|
|
2524
|
-
const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);
|
|
2525
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
2526
|
-
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2527
|
-
} else {
|
|
2528
|
-
return crypto.createHash("sha256").update(data).digest("hex");
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
};
|
|
2532
|
-
var SessionKeyCrypto = class {
|
|
2533
|
-
/**
|
|
2534
|
-
* Encrypt a Solana keypair with PIN + device fingerprint
|
|
2535
|
-
* Uses Argon2id for key derivation and AES-256-GCM for encryption
|
|
2536
|
-
*/
|
|
2537
|
-
static async encrypt(keypair, pin, deviceFingerprint) {
|
|
2538
|
-
if (!/^\d{6}$/.test(pin)) {
|
|
2539
|
-
throw new Error("PIN must be exactly 6 numeric digits");
|
|
2540
|
-
}
|
|
2541
|
-
const encryptionKey = await this.deriveKey(pin, deviceFingerprint);
|
|
2542
|
-
const nonce = this.generateNonce();
|
|
2543
|
-
const secretKey = keypair.secretKey;
|
|
2544
|
-
const encryptedData = await this.aesEncrypt(secretKey, encryptionKey, nonce);
|
|
2545
|
-
return {
|
|
2546
|
-
encryptedData: Buffer.from(encryptedData).toString("base64"),
|
|
2547
|
-
nonce: Buffer.from(nonce).toString("base64"),
|
|
2548
|
-
publicKey: keypair.publicKey.toBase58(),
|
|
2549
|
-
deviceFingerprint,
|
|
2550
|
-
version: "argon2id-aes256gcm-v1"
|
|
2551
|
-
};
|
|
2552
|
-
}
|
|
2553
|
-
/**
|
|
2554
|
-
* Decrypt an encrypted session key with PIN + device fingerprint
|
|
2555
|
-
*/
|
|
2556
|
-
static async decrypt(encrypted, pin, deviceFingerprint) {
|
|
2557
|
-
if (!/^\d{6}$/.test(pin)) {
|
|
2558
|
-
throw new Error("PIN must be exactly 6 numeric digits");
|
|
2559
|
-
}
|
|
2560
|
-
if (encrypted.deviceFingerprint !== deviceFingerprint) {
|
|
2561
|
-
throw new Error("Device fingerprint mismatch - wrong device or security threat");
|
|
2562
|
-
}
|
|
2563
|
-
const encryptionKey = await this.deriveKey(pin, deviceFingerprint);
|
|
2564
|
-
const encryptedData = Buffer.from(encrypted.encryptedData, "base64");
|
|
2565
|
-
const nonce = Buffer.from(encrypted.nonce, "base64");
|
|
2566
|
-
try {
|
|
2567
|
-
const secretKey = await this.aesDecrypt(encryptedData, encryptionKey, nonce);
|
|
2568
|
-
return Keypair.fromSecretKey(secretKey);
|
|
2569
|
-
} catch (error) {
|
|
2570
|
-
throw new Error("Decryption failed - wrong PIN or corrupted data");
|
|
2571
|
-
}
|
|
2572
|
-
}
|
|
2573
|
-
/**
|
|
2574
|
-
* Derive encryption key from PIN + device fingerprint using Argon2id
|
|
2575
|
-
*
|
|
2576
|
-
* Argon2id parameters (OWASP recommended):
|
|
2577
|
-
* - Memory: 64MB (65536 KB)
|
|
2578
|
-
* - Iterations: 3
|
|
2579
|
-
* - Parallelism: 4
|
|
2580
|
-
* - Salt: device fingerprint
|
|
2581
|
-
*/
|
|
2582
|
-
static async deriveKey(pin, deviceFingerprint) {
|
|
2583
|
-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
2584
|
-
const encoder = new TextEncoder();
|
|
2585
|
-
const keyMaterial = await window.crypto.subtle.importKey(
|
|
2586
|
-
"raw",
|
|
2587
|
-
encoder.encode(pin),
|
|
2588
|
-
{ name: "PBKDF2" },
|
|
2589
|
-
false,
|
|
2590
|
-
["deriveBits"]
|
|
2591
|
-
);
|
|
2592
|
-
const derivedBits = await window.crypto.subtle.deriveBits(
|
|
2593
|
-
{
|
|
2594
|
-
name: "PBKDF2",
|
|
2595
|
-
salt: encoder.encode(deviceFingerprint),
|
|
2596
|
-
iterations: 1e5,
|
|
2597
|
-
// High iteration count for security
|
|
2598
|
-
hash: "SHA-256"
|
|
2599
|
-
},
|
|
2600
|
-
keyMaterial,
|
|
2601
|
-
256
|
|
2602
|
-
// 256 bits = 32 bytes for AES-256
|
|
2603
|
-
);
|
|
2604
|
-
return new Uint8Array(derivedBits);
|
|
2605
|
-
} else {
|
|
2606
|
-
const salt = crypto.createHash("sha256").update(deviceFingerprint).digest();
|
|
2607
|
-
return crypto.pbkdf2Sync(pin, salt, 1e5, 32, "sha256");
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
|
-
/**
|
|
2611
|
-
* Generate random nonce for AES-GCM (12 bytes)
|
|
2612
|
-
*/
|
|
2613
|
-
static generateNonce() {
|
|
2614
|
-
if (typeof window !== "undefined" && window.crypto) {
|
|
2615
|
-
return window.crypto.getRandomValues(new Uint8Array(12));
|
|
2616
|
-
} else {
|
|
2617
|
-
return crypto.randomBytes(12);
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
/**
|
|
2621
|
-
* Encrypt with AES-256-GCM
|
|
2622
|
-
*/
|
|
2623
|
-
static async aesEncrypt(plaintext, key, nonce) {
|
|
2624
|
-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
2625
|
-
const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
|
|
2626
|
-
const nonceBuffer = nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength);
|
|
2627
|
-
const plaintextBuffer = plaintext.buffer.slice(plaintext.byteOffset, plaintext.byteOffset + plaintext.byteLength);
|
|
2628
|
-
const cryptoKey = await window.crypto.subtle.importKey(
|
|
2629
|
-
"raw",
|
|
2630
|
-
keyBuffer,
|
|
2631
|
-
{ name: "AES-GCM" },
|
|
2632
|
-
false,
|
|
2633
|
-
["encrypt"]
|
|
2634
|
-
);
|
|
2635
|
-
const encrypted = await window.crypto.subtle.encrypt(
|
|
2636
|
-
{
|
|
2637
|
-
name: "AES-GCM",
|
|
2638
|
-
iv: nonceBuffer
|
|
2639
|
-
},
|
|
2640
|
-
cryptoKey,
|
|
2641
|
-
plaintextBuffer
|
|
2642
|
-
);
|
|
2643
|
-
return new Uint8Array(encrypted);
|
|
2644
|
-
} else {
|
|
2645
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
|
|
2646
|
-
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
2647
|
-
const authTag = cipher.getAuthTag();
|
|
2648
|
-
return new Uint8Array(Buffer.concat([encrypted, authTag]));
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
/**
|
|
2652
|
-
* Decrypt with AES-256-GCM
|
|
2653
|
-
*/
|
|
2654
|
-
static async aesDecrypt(ciphertext, key, nonce) {
|
|
2655
|
-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
2656
|
-
const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
|
|
2657
|
-
const nonceBuffer = nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength);
|
|
2658
|
-
const ciphertextBuffer = ciphertext.buffer.slice(ciphertext.byteOffset, ciphertext.byteOffset + ciphertext.byteLength);
|
|
2659
|
-
const cryptoKey = await window.crypto.subtle.importKey(
|
|
2660
|
-
"raw",
|
|
2661
|
-
keyBuffer,
|
|
2662
|
-
{ name: "AES-GCM" },
|
|
2663
|
-
false,
|
|
2664
|
-
["decrypt"]
|
|
2665
|
-
);
|
|
2666
|
-
const decrypted = await window.crypto.subtle.decrypt(
|
|
2667
|
-
{
|
|
2668
|
-
name: "AES-GCM",
|
|
2669
|
-
iv: nonceBuffer
|
|
2670
|
-
},
|
|
2671
|
-
cryptoKey,
|
|
2672
|
-
ciphertextBuffer
|
|
2673
|
-
);
|
|
2674
|
-
return new Uint8Array(decrypted);
|
|
2675
|
-
} else {
|
|
2676
|
-
const authTag = ciphertext.slice(-16);
|
|
2677
|
-
const encrypted = ciphertext.slice(0, -16);
|
|
2678
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
2679
|
-
decipher.setAuthTag(authTag);
|
|
2680
|
-
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
2681
|
-
return new Uint8Array(decrypted);
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
};
|
|
2685
|
-
var RecoveryQRGenerator = class {
|
|
2686
|
-
/**
|
|
2687
|
-
* Generate recovery QR data
|
|
2688
|
-
* This allows users to recover their session key on a new device
|
|
2689
|
-
*/
|
|
2690
|
-
static generate(encrypted) {
|
|
2691
|
-
return {
|
|
2692
|
-
encryptedSessionKey: encrypted.encryptedData,
|
|
2693
|
-
nonce: encrypted.nonce,
|
|
2694
|
-
publicKey: encrypted.publicKey,
|
|
2695
|
-
version: "v1",
|
|
2696
|
-
createdAt: Date.now()
|
|
2697
|
-
};
|
|
2698
|
-
}
|
|
2699
|
-
/**
|
|
2700
|
-
* Encode recovery QR as JSON string
|
|
2701
|
-
*/
|
|
2702
|
-
static encode(recoveryQR) {
|
|
2703
|
-
return JSON.stringify(recoveryQR);
|
|
2704
|
-
}
|
|
2705
|
-
/**
|
|
2706
|
-
* Decode recovery QR from JSON string
|
|
2707
|
-
*/
|
|
2708
|
-
static decode(qrData) {
|
|
2709
|
-
try {
|
|
2710
|
-
const parsed = JSON.parse(qrData);
|
|
2711
|
-
if (!parsed.encryptedSessionKey || !parsed.nonce || !parsed.publicKey) {
|
|
2712
|
-
throw new Error("Invalid recovery QR data");
|
|
2713
|
-
}
|
|
2714
|
-
return parsed;
|
|
2715
|
-
} catch (error) {
|
|
2716
|
-
throw new Error("Failed to decode recovery QR");
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
/**
|
|
2720
|
-
* Re-encrypt session key for new device
|
|
2721
|
-
*/
|
|
2722
|
-
static async reEncryptForNewDevice(recoveryQR, oldPin, oldDeviceFingerprint, newPin, newDeviceFingerprint) {
|
|
2723
|
-
const oldEncrypted = {
|
|
2724
|
-
encryptedData: recoveryQR.encryptedSessionKey,
|
|
2725
|
-
nonce: recoveryQR.nonce,
|
|
2726
|
-
publicKey: recoveryQR.publicKey,
|
|
2727
|
-
deviceFingerprint: oldDeviceFingerprint,
|
|
2728
|
-
version: "argon2id-aes256gcm-v1"
|
|
2729
|
-
};
|
|
2730
|
-
const keypair = await SessionKeyCrypto.decrypt(oldEncrypted, oldPin, oldDeviceFingerprint);
|
|
2731
|
-
return await SessionKeyCrypto.encrypt(keypair, newPin, newDeviceFingerprint);
|
|
2732
|
-
}
|
|
2733
|
-
};
|
|
2734
|
-
var DeviceBoundSessionKey = class _DeviceBoundSessionKey {
|
|
2735
|
-
encrypted = null;
|
|
2736
|
-
deviceFingerprint = null;
|
|
2737
|
-
sessionKeyId = null;
|
|
2738
|
-
recoveryQR = null;
|
|
2739
|
-
// Auto-signing cache: decrypted keypair stored in memory
|
|
2740
|
-
// Enables instant signing without re-entering PIN for subsequent payments
|
|
2741
|
-
cachedKeypair = null;
|
|
2742
|
-
cacheExpiry = null;
|
|
2743
|
-
// Timestamp when cache expires
|
|
2744
|
-
DEFAULT_CACHE_TTL_MS = 30 * 60 * 1e3;
|
|
2745
|
-
// 30 minutes
|
|
2746
|
-
/**
|
|
2747
|
-
* Create a new device-bound session key
|
|
2748
|
-
*/
|
|
2749
|
-
static async create(options) {
|
|
2750
|
-
const deviceFingerprint = await DeviceFingerprintGenerator.generate();
|
|
2751
|
-
const keypair = Keypair.generate();
|
|
2752
|
-
const encrypted = await SessionKeyCrypto.encrypt(
|
|
2753
|
-
keypair,
|
|
2754
|
-
options.pin,
|
|
2755
|
-
deviceFingerprint.fingerprint
|
|
2756
|
-
);
|
|
2757
|
-
const instance = new _DeviceBoundSessionKey();
|
|
2758
|
-
instance.encrypted = encrypted;
|
|
2759
|
-
instance.deviceFingerprint = deviceFingerprint;
|
|
2760
|
-
if (options.generateRecoveryQR) {
|
|
2761
|
-
instance.recoveryQR = RecoveryQRGenerator.generate(encrypted);
|
|
2762
|
-
}
|
|
2763
|
-
return instance;
|
|
2764
|
-
}
|
|
2765
|
-
/**
|
|
2766
|
-
* Get encrypted data for backend storage
|
|
2767
|
-
*/
|
|
2768
|
-
getEncryptedData() {
|
|
2769
|
-
if (!this.encrypted) {
|
|
2770
|
-
throw new Error("Session key not created yet");
|
|
2771
|
-
}
|
|
2772
|
-
return this.encrypted;
|
|
2773
|
-
}
|
|
2774
|
-
/**
|
|
2775
|
-
* Get device fingerprint
|
|
2776
|
-
*/
|
|
2777
|
-
getDeviceFingerprint() {
|
|
2778
|
-
if (!this.deviceFingerprint) {
|
|
2779
|
-
throw new Error("Device fingerprint not generated yet");
|
|
2780
|
-
}
|
|
2781
|
-
return this.deviceFingerprint.fingerprint;
|
|
2782
|
-
}
|
|
2783
|
-
/**
|
|
2784
|
-
* Get public key
|
|
2785
|
-
*/
|
|
2786
|
-
getPublicKey() {
|
|
2787
|
-
if (!this.encrypted) {
|
|
2788
|
-
throw new Error("Session key not created yet");
|
|
2789
|
-
}
|
|
2790
|
-
return this.encrypted.publicKey;
|
|
2791
|
-
}
|
|
2792
|
-
/**
|
|
2793
|
-
* Get recovery QR data (if generated)
|
|
2794
|
-
*/
|
|
2795
|
-
getRecoveryQR() {
|
|
2796
|
-
return this.recoveryQR;
|
|
2797
|
-
}
|
|
2798
|
-
/**
|
|
2799
|
-
* Decrypt and sign a transaction
|
|
2800
|
-
*
|
|
2801
|
-
* @param transaction - The transaction to sign
|
|
2802
|
-
* @param pin - User's PIN (required only if keypair not cached)
|
|
2803
|
-
* @param cacheKeypair - Whether to cache the decrypted keypair for future use (default: true)
|
|
2804
|
-
* @param cacheTTL - Cache time-to-live in milliseconds (default: 30 minutes)
|
|
2805
|
-
*
|
|
2806
|
-
* @example
|
|
2807
|
-
* ```typescript
|
|
2808
|
-
* // First payment: requires PIN, caches keypair
|
|
2809
|
-
* await sessionKey.signTransaction(tx1, '123456', true);
|
|
2810
|
-
*
|
|
2811
|
-
* // Subsequent payments: uses cached keypair, no PIN needed!
|
|
2812
|
-
* await sessionKey.signTransaction(tx2, '', false); // PIN ignored if cached
|
|
2813
|
-
*
|
|
2814
|
-
* // Clear cache when done
|
|
2815
|
-
* sessionKey.clearCache();
|
|
2816
|
-
* ```
|
|
2817
|
-
*/
|
|
2818
|
-
async signTransaction(transaction, pin = "", cacheKeypair = true, cacheTTL) {
|
|
2819
|
-
if (!this.encrypted || !this.deviceFingerprint) {
|
|
2820
|
-
throw new Error("Session key not initialized");
|
|
2821
|
-
}
|
|
2822
|
-
let keypair;
|
|
2823
|
-
if (this.isCached()) {
|
|
2824
|
-
keypair = this.cachedKeypair;
|
|
2825
|
-
if (typeof console !== "undefined") {
|
|
2826
|
-
console.log("\u{1F680} Using cached keypair - instant signing (no PIN required)");
|
|
2827
|
-
}
|
|
2828
|
-
} else {
|
|
2829
|
-
if (!pin) {
|
|
2830
|
-
throw new Error("PIN required: no cached keypair available");
|
|
2831
|
-
}
|
|
2832
|
-
keypair = await SessionKeyCrypto.decrypt(
|
|
2833
|
-
this.encrypted,
|
|
2834
|
-
pin,
|
|
2835
|
-
this.deviceFingerprint.fingerprint
|
|
2836
|
-
);
|
|
2837
|
-
if (cacheKeypair) {
|
|
2838
|
-
const ttl = cacheTTL || this.DEFAULT_CACHE_TTL_MS;
|
|
2839
|
-
this.cacheKeypair(keypair, ttl);
|
|
2840
|
-
if (typeof console !== "undefined") {
|
|
2841
|
-
console.log(`\u2705 Keypair decrypted and cached for ${ttl / 1e3 / 60} minutes`);
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
transaction.sign(keypair);
|
|
2846
|
-
return transaction;
|
|
2847
|
-
}
|
|
2848
|
-
/**
|
|
2849
|
-
* Check if keypair is cached and valid
|
|
2850
|
-
*/
|
|
2851
|
-
isCached() {
|
|
2852
|
-
if (!this.cachedKeypair || !this.cacheExpiry) {
|
|
2853
|
-
return false;
|
|
2854
|
-
}
|
|
2855
|
-
const now = Date.now();
|
|
2856
|
-
if (now > this.cacheExpiry) {
|
|
2857
|
-
this.clearCache();
|
|
2858
|
-
return false;
|
|
2859
|
-
}
|
|
2860
|
-
return true;
|
|
2861
|
-
}
|
|
2862
|
-
/**
|
|
2863
|
-
* Manually cache a keypair
|
|
2864
|
-
* Called internally after PIN decryption
|
|
2865
|
-
*/
|
|
2866
|
-
cacheKeypair(keypair, ttl) {
|
|
2867
|
-
this.cachedKeypair = keypair;
|
|
2868
|
-
this.cacheExpiry = Date.now() + ttl;
|
|
2869
|
-
}
|
|
2870
|
-
/**
|
|
2871
|
-
* Clear cached keypair
|
|
2872
|
-
* Should be called when user logs out or session ends
|
|
2873
|
-
*
|
|
2874
|
-
* @example
|
|
2875
|
-
* ```typescript
|
|
2876
|
-
* // Clear cache on logout
|
|
2877
|
-
* sessionKey.clearCache();
|
|
2878
|
-
*
|
|
2879
|
-
* // Or clear automatically on tab close
|
|
2880
|
-
* window.addEventListener('beforeunload', () => {
|
|
2881
|
-
* sessionKey.clearCache();
|
|
2882
|
-
* });
|
|
2883
|
-
* ```
|
|
2884
|
-
*/
|
|
2885
|
-
clearCache() {
|
|
2886
|
-
this.cachedKeypair = null;
|
|
2887
|
-
this.cacheExpiry = null;
|
|
2888
|
-
if (typeof console !== "undefined") {
|
|
2889
|
-
console.log("\u{1F9F9} Keypair cache cleared");
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
/**
|
|
2893
|
-
* Decrypt and cache keypair without signing a transaction
|
|
2894
|
-
* Useful for pre-warming the cache before user makes payments
|
|
2895
|
-
*
|
|
2896
|
-
* @example
|
|
2897
|
-
* ```typescript
|
|
2898
|
-
* // After session key creation, decrypt and cache
|
|
2899
|
-
* await sessionKey.unlockWithPin('123456');
|
|
2900
|
-
*
|
|
2901
|
-
* // Now all subsequent payments are instant (no PIN)
|
|
2902
|
-
* await sessionKey.signTransaction(tx1, '', false); // Instant!
|
|
2903
|
-
* await sessionKey.signTransaction(tx2, '', false); // Instant!
|
|
2904
|
-
* ```
|
|
2905
|
-
*/
|
|
2906
|
-
async unlockWithPin(pin, cacheTTL) {
|
|
2907
|
-
if (!this.encrypted || !this.deviceFingerprint) {
|
|
2908
|
-
throw new Error("Session key not initialized");
|
|
2909
|
-
}
|
|
2910
|
-
const keypair = await SessionKeyCrypto.decrypt(
|
|
2911
|
-
this.encrypted,
|
|
2912
|
-
pin,
|
|
2913
|
-
this.deviceFingerprint.fingerprint
|
|
2914
|
-
);
|
|
2915
|
-
const ttl = cacheTTL || this.DEFAULT_CACHE_TTL_MS;
|
|
2916
|
-
this.cacheKeypair(keypair, ttl);
|
|
2917
|
-
if (typeof console !== "undefined") {
|
|
2918
|
-
console.log(`\u{1F513} Session key unlocked and cached for ${ttl / 1e3 / 60} minutes`);
|
|
2919
|
-
}
|
|
2920
|
-
}
|
|
2921
|
-
/**
|
|
2922
|
-
* Get time remaining until cache expires (in milliseconds)
|
|
2923
|
-
* Returns 0 if not cached
|
|
2924
|
-
*/
|
|
2925
|
-
getCacheTimeRemaining() {
|
|
2926
|
-
if (!this.isCached() || !this.cacheExpiry) {
|
|
2927
|
-
return 0;
|
|
2928
|
-
}
|
|
2929
|
-
const remaining = this.cacheExpiry - Date.now();
|
|
2930
|
-
return Math.max(0, remaining);
|
|
2931
|
-
}
|
|
2932
|
-
/**
|
|
2933
|
-
* Extend cache expiry time
|
|
2934
|
-
* Useful to keep session active during user activity
|
|
2935
|
-
*
|
|
2936
|
-
* @example
|
|
2937
|
-
* ```typescript
|
|
2938
|
-
* // Extend cache by 15 minutes on each payment
|
|
2939
|
-
* await sessionKey.signTransaction(tx, '');
|
|
2940
|
-
* sessionKey.extendCache(15 * 60 * 1000);
|
|
2941
|
-
* ```
|
|
2942
|
-
*/
|
|
2943
|
-
extendCache(additionalTTL) {
|
|
2944
|
-
if (!this.isCached()) {
|
|
2945
|
-
throw new Error("Cannot extend cache: no cached keypair");
|
|
2946
|
-
}
|
|
2947
|
-
this.cacheExpiry += additionalTTL;
|
|
2948
|
-
if (typeof console !== "undefined") {
|
|
2949
|
-
const remainingMinutes = this.getCacheTimeRemaining() / 1e3 / 60;
|
|
2950
|
-
console.log(`\u23F0 Cache extended - ${remainingMinutes.toFixed(1)} minutes remaining`);
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
/**
|
|
2954
|
-
* Set session key ID after backend creation
|
|
2955
|
-
*/
|
|
2956
|
-
setSessionKeyId(id) {
|
|
2957
|
-
this.sessionKeyId = id;
|
|
2958
|
-
}
|
|
2959
|
-
/**
|
|
2960
|
-
* Get session key ID
|
|
2961
|
-
*/
|
|
2962
|
-
getSessionKeyId() {
|
|
2963
|
-
if (!this.sessionKeyId) {
|
|
2964
|
-
throw new Error("Session key not registered with backend");
|
|
2965
|
-
}
|
|
2966
|
-
return this.sessionKeyId;
|
|
2967
|
-
}
|
|
2968
|
-
};
|
|
2969
|
-
|
|
2970
2438
|
// src/device-bound-session-keys.ts
|
|
2971
|
-
import { Transaction
|
|
2439
|
+
import { Transaction } from "@solana/web3.js";
|
|
2972
2440
|
var ZendFiSessionKeyManager = class {
|
|
2973
2441
|
baseURL;
|
|
2974
2442
|
apiKey;
|
|
@@ -2987,6 +2455,8 @@ var ZendFiSessionKeyManager = class {
|
|
|
2987
2455
|
*
|
|
2988
2456
|
* const sessionKey = await manager.createSessionKey({
|
|
2989
2457
|
* userWallet: '7xKNH....',
|
|
2458
|
+
* agentId: 'shopping-assistant-v1',
|
|
2459
|
+
* agentName: 'AI Shopping Assistant',
|
|
2990
2460
|
* limitUSDC: 100,
|
|
2991
2461
|
* durationDays: 7,
|
|
2992
2462
|
* pin: '123456',
|
|
@@ -2994,6 +2464,7 @@ var ZendFiSessionKeyManager = class {
|
|
|
2994
2464
|
* });
|
|
2995
2465
|
*
|
|
2996
2466
|
* console.log('Session key created:', sessionKey.sessionKeyId);
|
|
2467
|
+
* console.log('Works across all apps with agent:', sessionKey.agentId);
|
|
2997
2468
|
* console.log('Recovery QR:', sessionKey.recoveryQR);
|
|
2998
2469
|
* ```
|
|
2999
2470
|
*/
|
|
@@ -3013,6 +2484,8 @@ var ZendFiSessionKeyManager = class {
|
|
|
3013
2484
|
}
|
|
3014
2485
|
const request = {
|
|
3015
2486
|
userWallet: options.userWallet,
|
|
2487
|
+
agentId: options.agentId,
|
|
2488
|
+
agentName: options.agentName,
|
|
3016
2489
|
limitUsdc: options.limitUSDC,
|
|
3017
2490
|
durationDays: options.durationDays,
|
|
3018
2491
|
encryptedSessionKey: encrypted.encryptedData,
|
|
@@ -3031,10 +2504,13 @@ var ZendFiSessionKeyManager = class {
|
|
|
3031
2504
|
sessionKey.setSessionKeyId(response.sessionKeyId);
|
|
3032
2505
|
return {
|
|
3033
2506
|
sessionKeyId: response.sessionKeyId,
|
|
2507
|
+
agentId: response.agentId,
|
|
2508
|
+
agentName: response.agentName,
|
|
3034
2509
|
sessionWallet: response.sessionWallet,
|
|
3035
2510
|
expiresAt: response.expiresAt,
|
|
3036
2511
|
recoveryQR,
|
|
3037
|
-
limitUsdc: response.limitUsdc
|
|
2512
|
+
limitUsdc: response.limitUsdc,
|
|
2513
|
+
crossAppCompatible: response.crossAppCompatible
|
|
3038
2514
|
};
|
|
3039
2515
|
}
|
|
3040
2516
|
/**
|
|
@@ -3137,7 +2613,7 @@ var ZendFiSessionKeyManager = class {
|
|
|
3137
2613
|
throw new Error("Backend did not return unsigned transaction");
|
|
3138
2614
|
}
|
|
3139
2615
|
const transactionBuffer = Buffer.from(paymentResponse.unsigned_transaction, "base64");
|
|
3140
|
-
const transaction =
|
|
2616
|
+
const transaction = Transaction.from(transactionBuffer);
|
|
3141
2617
|
const signedTransaction = await this.sessionKey.signTransaction(
|
|
3142
2618
|
transaction,
|
|
3143
2619
|
options.pin || "",
|
|
@@ -3448,29 +2924,2146 @@ function decodeSignatureFromLit(result) {
|
|
|
3448
2924
|
}
|
|
3449
2925
|
return bytes;
|
|
3450
2926
|
}
|
|
2927
|
+
|
|
2928
|
+
// src/helpers/ai.ts
|
|
2929
|
+
var PaymentIntentParser = class {
|
|
2930
|
+
/**
|
|
2931
|
+
* Parse natural language into structured intent
|
|
2932
|
+
*/
|
|
2933
|
+
static parse(text) {
|
|
2934
|
+
if (!text || typeof text !== "string") return null;
|
|
2935
|
+
const lowerText = text.toLowerCase().trim();
|
|
2936
|
+
let action = "chat_only";
|
|
2937
|
+
let confidence = 0;
|
|
2938
|
+
if (this.containsPaymentKeywords(lowerText)) {
|
|
2939
|
+
action = "payment";
|
|
2940
|
+
confidence = 0.7;
|
|
2941
|
+
const amount = this.extractAmount(lowerText);
|
|
2942
|
+
const description = this.extractDescription(lowerText);
|
|
2943
|
+
if (amount && description) {
|
|
2944
|
+
confidence = 0.9;
|
|
2945
|
+
return { action, amount, description, confidence, rawText: text };
|
|
2946
|
+
}
|
|
2947
|
+
if (amount) {
|
|
2948
|
+
confidence = 0.8;
|
|
2949
|
+
return { action, amount, confidence, rawText: text };
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
if (this.containsSessionKeywords(lowerText)) {
|
|
2953
|
+
action = "create_session";
|
|
2954
|
+
confidence = 0.8;
|
|
2955
|
+
const amount = this.extractAmount(lowerText);
|
|
2956
|
+
return { action, amount, confidence, rawText: text, description: "Session key budget" };
|
|
2957
|
+
}
|
|
2958
|
+
if (this.containsStatusKeywords(lowerText)) {
|
|
2959
|
+
action = lowerText.includes("session") || lowerText.includes("key") ? "check_status" : "check_balance";
|
|
2960
|
+
confidence = 0.9;
|
|
2961
|
+
return { action, confidence, rawText: text };
|
|
2962
|
+
}
|
|
2963
|
+
if (this.containsTopUpKeywords(lowerText)) {
|
|
2964
|
+
action = "topup";
|
|
2965
|
+
confidence = 0.8;
|
|
2966
|
+
const amount = this.extractAmount(lowerText);
|
|
2967
|
+
return { action, amount, confidence, rawText: text };
|
|
2968
|
+
}
|
|
2969
|
+
if (this.containsRevokeKeywords(lowerText)) {
|
|
2970
|
+
action = "revoke";
|
|
2971
|
+
confidence = 0.9;
|
|
2972
|
+
return { action, confidence, rawText: text };
|
|
2973
|
+
}
|
|
2974
|
+
if (this.containsAutonomyKeywords(lowerText)) {
|
|
2975
|
+
action = "enable_autonomy";
|
|
2976
|
+
confidence = 0.8;
|
|
2977
|
+
const amount = this.extractAmount(lowerText);
|
|
2978
|
+
return { action, amount, confidence, rawText: text, description: "Autonomous delegate limit" };
|
|
2979
|
+
}
|
|
2980
|
+
return null;
|
|
2981
|
+
}
|
|
2982
|
+
/**
|
|
2983
|
+
* Generate system prompt for AI models
|
|
2984
|
+
*/
|
|
2985
|
+
static generateSystemPrompt(capabilities = {}) {
|
|
2986
|
+
const enabledFeatures = Object.entries({
|
|
2987
|
+
createPayment: capabilities.createPayment !== false,
|
|
2988
|
+
createSessionKey: capabilities.createSessionKey !== false,
|
|
2989
|
+
checkBalance: capabilities.checkBalance !== false,
|
|
2990
|
+
checkStatus: capabilities.checkStatus !== false,
|
|
2991
|
+
topUpSession: capabilities.topUpSession !== false,
|
|
2992
|
+
revokeSession: capabilities.revokeSession !== false,
|
|
2993
|
+
enableAutonomy: capabilities.enableAutonomy !== false
|
|
2994
|
+
}).filter(([_, enabled]) => enabled).map(([feature]) => feature);
|
|
2995
|
+
return `You are a ZendFi payment assistant. You can help users with crypto payments on Solana.
|
|
2996
|
+
|
|
2997
|
+
Available Actions:
|
|
2998
|
+
${enabledFeatures.includes("createPayment") ? '- Make payments (e.g., "Buy coffee for $5", "Send $20 to merchant")' : ""}
|
|
2999
|
+
${enabledFeatures.includes("createSessionKey") ? '- Create session keys (e.g., "Create a session key with $100 budget")' : ""}
|
|
3000
|
+
${enabledFeatures.includes("checkBalance") ? `- Check balances (e.g., "What's my balance?", "How much do I have?")` : ""}
|
|
3001
|
+
${enabledFeatures.includes("checkStatus") ? '- Check session status (e.g., "Session key status", "How much is left?")' : ""}
|
|
3002
|
+
${enabledFeatures.includes("topUpSession") ? '- Top up session keys (e.g., "Add $50 to session key", "Top up with $100")' : ""}
|
|
3003
|
+
${enabledFeatures.includes("revokeSession") ? '- Revoke session keys (e.g., "Revoke session key", "Cancel my session")' : ""}
|
|
3004
|
+
${enabledFeatures.includes("enableAutonomy") ? '- Enable autonomous mode (e.g., "Enable auto-pay with $25 limit")' : ""}
|
|
3005
|
+
|
|
3006
|
+
Response Format:
|
|
3007
|
+
Always respond with valid JSON:
|
|
3008
|
+
{
|
|
3009
|
+
"action": "payment" | "create_session" | "check_balance" | "check_status" | "topup" | "revoke" | "enable_autonomy" | "chat_only",
|
|
3010
|
+
"amount_usd": <number if applicable>,
|
|
3011
|
+
"description": "<description>",
|
|
3012
|
+
"message": "<friendly response to user>"
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
Examples:
|
|
3016
|
+
User: "Buy coffee for $5"
|
|
3017
|
+
You: {"action": "payment", "amount_usd": 5, "description": "coffee", "message": "Sure! Processing your $5 coffee payment..."}
|
|
3018
|
+
|
|
3019
|
+
User: "Create a session key with $100"
|
|
3020
|
+
You: {"action": "create_session", "amount_usd": 100, "message": "I'll create a session key with a $100 budget..."}
|
|
3021
|
+
|
|
3022
|
+
User: "What's my balance?"
|
|
3023
|
+
You: {"action": "check_balance", "message": "Let me check your balance..."}
|
|
3024
|
+
|
|
3025
|
+
Be helpful, concise, and always respond in valid JSON format.`;
|
|
3026
|
+
}
|
|
3027
|
+
// ============================================
|
|
3028
|
+
// Keyword Detection
|
|
3029
|
+
// ============================================
|
|
3030
|
+
static containsPaymentKeywords(text) {
|
|
3031
|
+
const keywords = [
|
|
3032
|
+
"buy",
|
|
3033
|
+
"purchase",
|
|
3034
|
+
"pay",
|
|
3035
|
+
"send",
|
|
3036
|
+
"transfer",
|
|
3037
|
+
"payment",
|
|
3038
|
+
"order",
|
|
3039
|
+
"checkout",
|
|
3040
|
+
"subscribe",
|
|
3041
|
+
"donate",
|
|
3042
|
+
"tip"
|
|
3043
|
+
];
|
|
3044
|
+
return keywords.some((kw) => text.includes(kw));
|
|
3045
|
+
}
|
|
3046
|
+
static containsSessionKeywords(text) {
|
|
3047
|
+
const keywords = [
|
|
3048
|
+
"create session",
|
|
3049
|
+
"new session",
|
|
3050
|
+
"session key",
|
|
3051
|
+
"setup session",
|
|
3052
|
+
"generate session",
|
|
3053
|
+
"make session",
|
|
3054
|
+
"start session"
|
|
3055
|
+
];
|
|
3056
|
+
return keywords.some((kw) => text.includes(kw));
|
|
3057
|
+
}
|
|
3058
|
+
static containsStatusKeywords(text) {
|
|
3059
|
+
const keywords = [
|
|
3060
|
+
"status",
|
|
3061
|
+
"balance",
|
|
3062
|
+
"remaining",
|
|
3063
|
+
"left",
|
|
3064
|
+
"how much",
|
|
3065
|
+
"check",
|
|
3066
|
+
"show",
|
|
3067
|
+
"view",
|
|
3068
|
+
"display"
|
|
3069
|
+
];
|
|
3070
|
+
return keywords.some((kw) => text.includes(kw));
|
|
3071
|
+
}
|
|
3072
|
+
static containsTopUpKeywords(text) {
|
|
3073
|
+
const keywords = [
|
|
3074
|
+
"top up",
|
|
3075
|
+
"topup",
|
|
3076
|
+
"add funds",
|
|
3077
|
+
"add money",
|
|
3078
|
+
"fund",
|
|
3079
|
+
"increase",
|
|
3080
|
+
"reload",
|
|
3081
|
+
"refill"
|
|
3082
|
+
];
|
|
3083
|
+
return keywords.some((kw) => text.includes(kw));
|
|
3084
|
+
}
|
|
3085
|
+
static containsRevokeKeywords(text) {
|
|
3086
|
+
const keywords = [
|
|
3087
|
+
"revoke",
|
|
3088
|
+
"cancel",
|
|
3089
|
+
"disable",
|
|
3090
|
+
"remove",
|
|
3091
|
+
"delete",
|
|
3092
|
+
"stop",
|
|
3093
|
+
"end",
|
|
3094
|
+
"terminate"
|
|
3095
|
+
];
|
|
3096
|
+
return keywords.some((kw) => text.includes(kw));
|
|
3097
|
+
}
|
|
3098
|
+
static containsAutonomyKeywords(text) {
|
|
3099
|
+
const keywords = [
|
|
3100
|
+
"autonomous",
|
|
3101
|
+
"auto",
|
|
3102
|
+
"automatic",
|
|
3103
|
+
"delegate",
|
|
3104
|
+
"enable auto",
|
|
3105
|
+
"auto-sign",
|
|
3106
|
+
"auto sign",
|
|
3107
|
+
"auto-pay",
|
|
3108
|
+
"auto pay"
|
|
3109
|
+
];
|
|
3110
|
+
return keywords.some((kw) => text.includes(kw));
|
|
3111
|
+
}
|
|
3112
|
+
// ============================================
|
|
3113
|
+
// Amount Extraction
|
|
3114
|
+
// ============================================
|
|
3115
|
+
static extractAmount(text) {
|
|
3116
|
+
const dollarMatch = text.match(/\$\s*(\d+(?:\.\d{1,2})?)/);
|
|
3117
|
+
if (dollarMatch) {
|
|
3118
|
+
return parseFloat(dollarMatch[1]);
|
|
3119
|
+
}
|
|
3120
|
+
const usdMatch = text.match(/(\d+(?:\.\d{1,2})?)\s*(?:usd|usdc|dollars?)/i);
|
|
3121
|
+
if (usdMatch) {
|
|
3122
|
+
return parseFloat(usdMatch[1]);
|
|
3123
|
+
}
|
|
3124
|
+
const numberMatch = text.match(/(\d+(?:\.\d{1,2})?)/);
|
|
3125
|
+
if (numberMatch) {
|
|
3126
|
+
const num = parseFloat(numberMatch[1]);
|
|
3127
|
+
if (num > 0 && num < 1e5) {
|
|
3128
|
+
return num;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
return void 0;
|
|
3132
|
+
}
|
|
3133
|
+
// ============================================
|
|
3134
|
+
// Description Extraction
|
|
3135
|
+
// ============================================
|
|
3136
|
+
static extractDescription(text) {
|
|
3137
|
+
const lowerText = text.toLowerCase();
|
|
3138
|
+
const items = {
|
|
3139
|
+
"coffee": ["coffee", "espresso", "latte", "cappuccino"],
|
|
3140
|
+
"food": ["food", "meal", "lunch", "dinner", "breakfast"],
|
|
3141
|
+
"drink": ["drink", "beverage", "soda", "juice"],
|
|
3142
|
+
"book": ["book", "ebook", "magazine"],
|
|
3143
|
+
"subscription": ["subscription", "membership", "plan"],
|
|
3144
|
+
"tip": ["tip", "gratuity"],
|
|
3145
|
+
"donation": ["donate", "donation", "contribute"],
|
|
3146
|
+
"game": ["game", "gaming"],
|
|
3147
|
+
"music": ["music", "song", "album"],
|
|
3148
|
+
"video": ["video", "movie", "film"]
|
|
3149
|
+
};
|
|
3150
|
+
for (const [item, keywords] of Object.entries(items)) {
|
|
3151
|
+
if (keywords.some((kw) => lowerText.includes(kw))) {
|
|
3152
|
+
return item;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
const forMatch = text.match(/for\s+(.+?)(?:\s+\$|\s+usd|$)/i);
|
|
3156
|
+
if (forMatch && forMatch[1]) {
|
|
3157
|
+
const desc = forMatch[1].trim();
|
|
3158
|
+
if (desc.length > 0 && desc.length < 50) {
|
|
3159
|
+
return desc;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
return void 0;
|
|
3163
|
+
}
|
|
3164
|
+
};
|
|
3165
|
+
var OpenAIAdapter = class {
|
|
3166
|
+
constructor(apiKey, model = "gpt-4o-mini", capabilities) {
|
|
3167
|
+
this.apiKey = apiKey;
|
|
3168
|
+
this.model = model;
|
|
3169
|
+
this.capabilities = capabilities;
|
|
3170
|
+
}
|
|
3171
|
+
async chat(message, conversationHistory = []) {
|
|
3172
|
+
const systemPrompt = PaymentIntentParser.generateSystemPrompt(this.capabilities);
|
|
3173
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
3174
|
+
method: "POST",
|
|
3175
|
+
headers: {
|
|
3176
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
3177
|
+
"Content-Type": "application/json"
|
|
3178
|
+
},
|
|
3179
|
+
body: JSON.stringify({
|
|
3180
|
+
model: this.model,
|
|
3181
|
+
messages: [
|
|
3182
|
+
{ role: "system", content: systemPrompt },
|
|
3183
|
+
...conversationHistory,
|
|
3184
|
+
{ role: "user", content: message }
|
|
3185
|
+
],
|
|
3186
|
+
temperature: 0.7,
|
|
3187
|
+
max_tokens: 500
|
|
3188
|
+
})
|
|
3189
|
+
});
|
|
3190
|
+
if (!response.ok) {
|
|
3191
|
+
throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`);
|
|
3192
|
+
}
|
|
3193
|
+
const data = await response.json();
|
|
3194
|
+
const text = data.choices[0]?.message?.content || "";
|
|
3195
|
+
let intent = null;
|
|
3196
|
+
try {
|
|
3197
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
3198
|
+
if (jsonMatch) {
|
|
3199
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
3200
|
+
intent = {
|
|
3201
|
+
action: parsed.action || "chat_only",
|
|
3202
|
+
amount: parsed.amount_usd,
|
|
3203
|
+
description: parsed.description,
|
|
3204
|
+
confidence: 0.9,
|
|
3205
|
+
rawText: text,
|
|
3206
|
+
metadata: { message: parsed.message }
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
} catch {
|
|
3210
|
+
intent = PaymentIntentParser.parse(text);
|
|
3211
|
+
}
|
|
3212
|
+
return { text, intent, raw: data };
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
3215
|
+
var AnthropicAdapter = class {
|
|
3216
|
+
constructor(apiKey, model = "claude-3-5-sonnet-20241022", capabilities) {
|
|
3217
|
+
this.apiKey = apiKey;
|
|
3218
|
+
this.model = model;
|
|
3219
|
+
this.capabilities = capabilities;
|
|
3220
|
+
}
|
|
3221
|
+
async chat(message, conversationHistory = []) {
|
|
3222
|
+
const systemPrompt = PaymentIntentParser.generateSystemPrompt(this.capabilities);
|
|
3223
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
3224
|
+
method: "POST",
|
|
3225
|
+
headers: {
|
|
3226
|
+
"x-api-key": this.apiKey,
|
|
3227
|
+
"anthropic-version": "2023-06-01",
|
|
3228
|
+
"Content-Type": "application/json"
|
|
3229
|
+
},
|
|
3230
|
+
body: JSON.stringify({
|
|
3231
|
+
model: this.model,
|
|
3232
|
+
system: systemPrompt,
|
|
3233
|
+
messages: [
|
|
3234
|
+
...conversationHistory.map((msg) => ({
|
|
3235
|
+
role: msg.role === "user" ? "user" : "assistant",
|
|
3236
|
+
content: msg.content
|
|
3237
|
+
})),
|
|
3238
|
+
{ role: "user", content: message }
|
|
3239
|
+
],
|
|
3240
|
+
max_tokens: 500
|
|
3241
|
+
})
|
|
3242
|
+
});
|
|
3243
|
+
if (!response.ok) {
|
|
3244
|
+
throw new Error(`Anthropic API error: ${response.status} ${response.statusText}`);
|
|
3245
|
+
}
|
|
3246
|
+
const data = await response.json();
|
|
3247
|
+
const text = data.content[0]?.text || "";
|
|
3248
|
+
let intent = null;
|
|
3249
|
+
try {
|
|
3250
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
3251
|
+
if (jsonMatch) {
|
|
3252
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
3253
|
+
intent = {
|
|
3254
|
+
action: parsed.action || "chat_only",
|
|
3255
|
+
amount: parsed.amount_usd,
|
|
3256
|
+
description: parsed.description,
|
|
3257
|
+
confidence: 0.9,
|
|
3258
|
+
rawText: text,
|
|
3259
|
+
metadata: { message: parsed.message }
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
} catch {
|
|
3263
|
+
intent = PaymentIntentParser.parse(text);
|
|
3264
|
+
}
|
|
3265
|
+
return { text, intent, raw: data };
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
var GeminiAdapter = class {
|
|
3269
|
+
constructor(apiKey, model = "gemini-2.0-flash-exp", capabilities) {
|
|
3270
|
+
this.apiKey = apiKey;
|
|
3271
|
+
this.model = model;
|
|
3272
|
+
this.capabilities = capabilities;
|
|
3273
|
+
}
|
|
3274
|
+
async chat(message) {
|
|
3275
|
+
const systemPrompt = PaymentIntentParser.generateSystemPrompt(this.capabilities);
|
|
3276
|
+
const fullPrompt = `${systemPrompt}
|
|
3277
|
+
|
|
3278
|
+
User: ${message}`;
|
|
3279
|
+
const response = await fetch(
|
|
3280
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`,
|
|
3281
|
+
{
|
|
3282
|
+
method: "POST",
|
|
3283
|
+
headers: { "Content-Type": "application/json" },
|
|
3284
|
+
body: JSON.stringify({
|
|
3285
|
+
contents: [{ parts: [{ text: fullPrompt }] }]
|
|
3286
|
+
})
|
|
3287
|
+
}
|
|
3288
|
+
);
|
|
3289
|
+
if (!response.ok) {
|
|
3290
|
+
throw new Error(`Gemini API error: ${response.status} ${response.statusText}`);
|
|
3291
|
+
}
|
|
3292
|
+
const data = await response.json();
|
|
3293
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
3294
|
+
let intent = null;
|
|
3295
|
+
try {
|
|
3296
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
3297
|
+
if (jsonMatch) {
|
|
3298
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
3299
|
+
intent = {
|
|
3300
|
+
action: parsed.action || "chat_only",
|
|
3301
|
+
amount: parsed.amount_usd,
|
|
3302
|
+
description: parsed.description,
|
|
3303
|
+
confidence: 0.9,
|
|
3304
|
+
rawText: text,
|
|
3305
|
+
metadata: { message: parsed.message }
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
} catch {
|
|
3309
|
+
intent = PaymentIntentParser.parse(text);
|
|
3310
|
+
}
|
|
3311
|
+
return { text, intent, raw: data };
|
|
3312
|
+
}
|
|
3313
|
+
};
|
|
3314
|
+
|
|
3315
|
+
// src/helpers/wallet.ts
|
|
3316
|
+
var WalletConnector = class _WalletConnector {
|
|
3317
|
+
static connectedWallet = null;
|
|
3318
|
+
/**
|
|
3319
|
+
* Detect and connect to a Solana wallet
|
|
3320
|
+
*/
|
|
3321
|
+
static async detectAndConnect(config = {}) {
|
|
3322
|
+
if (this.connectedWallet && this.connectedWallet.isConnected()) {
|
|
3323
|
+
return this.connectedWallet;
|
|
3324
|
+
}
|
|
3325
|
+
const detected = this.detectWallets();
|
|
3326
|
+
if (detected.length === 0) {
|
|
3327
|
+
if (config.showInstallPrompt !== false) {
|
|
3328
|
+
this.showInstallPrompt();
|
|
3329
|
+
}
|
|
3330
|
+
throw new Error("No Solana wallet detected. Please install Phantom, Solflare, or Backpack.");
|
|
3331
|
+
}
|
|
3332
|
+
let selectedProvider = detected[0];
|
|
3333
|
+
if (config.preferredProvider && detected.includes(config.preferredProvider)) {
|
|
3334
|
+
selectedProvider = config.preferredProvider;
|
|
3335
|
+
}
|
|
3336
|
+
const wallet = await this.connectToProvider(selectedProvider);
|
|
3337
|
+
this.connectedWallet = wallet;
|
|
3338
|
+
return wallet;
|
|
3339
|
+
}
|
|
3340
|
+
/**
|
|
3341
|
+
* Detect available Solana wallets
|
|
3342
|
+
*/
|
|
3343
|
+
static detectWallets() {
|
|
3344
|
+
const detected = [];
|
|
3345
|
+
if (typeof window === "undefined") return detected;
|
|
3346
|
+
if (window.solana?.isPhantom) detected.push("phantom");
|
|
3347
|
+
if (window.solflare?.isSolflare) detected.push("solflare");
|
|
3348
|
+
if (window.backpack?.isBackpack) detected.push("backpack");
|
|
3349
|
+
if (window.coinbaseSolana) detected.push("coinbase");
|
|
3350
|
+
if (window.trustwallet?.solana) detected.push("trust");
|
|
3351
|
+
return detected;
|
|
3352
|
+
}
|
|
3353
|
+
/**
|
|
3354
|
+
* Connect to a specific wallet provider
|
|
3355
|
+
*/
|
|
3356
|
+
static async connectToProvider(provider) {
|
|
3357
|
+
if (typeof window === "undefined") {
|
|
3358
|
+
throw new Error("Wallet connection only works in browser environment");
|
|
3359
|
+
}
|
|
3360
|
+
let adapter;
|
|
3361
|
+
switch (provider) {
|
|
3362
|
+
case "phantom":
|
|
3363
|
+
adapter = window.solana;
|
|
3364
|
+
if (!adapter?.isPhantom) {
|
|
3365
|
+
throw new Error("Phantom wallet not found");
|
|
3366
|
+
}
|
|
3367
|
+
break;
|
|
3368
|
+
case "solflare":
|
|
3369
|
+
adapter = window.solflare;
|
|
3370
|
+
if (!adapter?.isSolflare) {
|
|
3371
|
+
throw new Error("Solflare wallet not found");
|
|
3372
|
+
}
|
|
3373
|
+
break;
|
|
3374
|
+
case "backpack":
|
|
3375
|
+
adapter = window.backpack;
|
|
3376
|
+
if (!adapter?.isBackpack) {
|
|
3377
|
+
throw new Error("Backpack wallet not found");
|
|
3378
|
+
}
|
|
3379
|
+
break;
|
|
3380
|
+
case "coinbase":
|
|
3381
|
+
adapter = window.coinbaseSolana;
|
|
3382
|
+
if (!adapter) {
|
|
3383
|
+
throw new Error("Coinbase Wallet not found");
|
|
3384
|
+
}
|
|
3385
|
+
break;
|
|
3386
|
+
case "trust":
|
|
3387
|
+
adapter = window.trustwallet?.solana;
|
|
3388
|
+
if (!adapter) {
|
|
3389
|
+
throw new Error("Trust Wallet not found");
|
|
3390
|
+
}
|
|
3391
|
+
break;
|
|
3392
|
+
default:
|
|
3393
|
+
throw new Error(`Unknown wallet provider: ${provider}`);
|
|
3394
|
+
}
|
|
3395
|
+
try {
|
|
3396
|
+
const response = await adapter.connect();
|
|
3397
|
+
const publicKey = response.publicKey || adapter.publicKey;
|
|
3398
|
+
if (!publicKey) {
|
|
3399
|
+
throw new Error("Failed to get wallet public key");
|
|
3400
|
+
}
|
|
3401
|
+
const connectedWallet = {
|
|
3402
|
+
address: publicKey.toString(),
|
|
3403
|
+
provider,
|
|
3404
|
+
publicKey,
|
|
3405
|
+
signTransaction: async (tx) => {
|
|
3406
|
+
return await adapter.signTransaction(tx);
|
|
3407
|
+
},
|
|
3408
|
+
signAllTransactions: async (txs) => {
|
|
3409
|
+
if (adapter.signAllTransactions) {
|
|
3410
|
+
return await adapter.signAllTransactions(txs);
|
|
3411
|
+
}
|
|
3412
|
+
const signed = [];
|
|
3413
|
+
for (const tx of txs) {
|
|
3414
|
+
signed.push(await adapter.signTransaction(tx));
|
|
3415
|
+
}
|
|
3416
|
+
return signed;
|
|
3417
|
+
},
|
|
3418
|
+
signMessage: async (message) => {
|
|
3419
|
+
if (adapter.signMessage) {
|
|
3420
|
+
return await adapter.signMessage(message);
|
|
3421
|
+
}
|
|
3422
|
+
throw new Error(`${provider} does not support message signing`);
|
|
3423
|
+
},
|
|
3424
|
+
disconnect: async () => {
|
|
3425
|
+
if (adapter.disconnect) {
|
|
3426
|
+
await adapter.disconnect();
|
|
3427
|
+
}
|
|
3428
|
+
_WalletConnector.connectedWallet = null;
|
|
3429
|
+
},
|
|
3430
|
+
isConnected: () => {
|
|
3431
|
+
return adapter.isConnected ?? false;
|
|
3432
|
+
},
|
|
3433
|
+
raw: adapter
|
|
3434
|
+
};
|
|
3435
|
+
return connectedWallet;
|
|
3436
|
+
} catch (error) {
|
|
3437
|
+
throw new Error(`Failed to connect to ${provider}: ${error.message}`);
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
/**
|
|
3441
|
+
* Sign and submit a transaction
|
|
3442
|
+
*/
|
|
3443
|
+
static async signAndSubmit(transaction, wallet, connection) {
|
|
3444
|
+
const signedTx = await wallet.signTransaction(transaction);
|
|
3445
|
+
const signature = await connection.sendRawTransaction(signedTx.serialize(), {
|
|
3446
|
+
skipPreflight: false,
|
|
3447
|
+
preflightCommitment: "confirmed"
|
|
3448
|
+
});
|
|
3449
|
+
return { signature };
|
|
3450
|
+
}
|
|
3451
|
+
/**
|
|
3452
|
+
* Get current connected wallet
|
|
3453
|
+
*/
|
|
3454
|
+
static getConnectedWallet() {
|
|
3455
|
+
return this.connectedWallet;
|
|
3456
|
+
}
|
|
3457
|
+
/**
|
|
3458
|
+
* Disconnect current wallet
|
|
3459
|
+
*/
|
|
3460
|
+
static async disconnect() {
|
|
3461
|
+
if (this.connectedWallet) {
|
|
3462
|
+
await this.connectedWallet.disconnect();
|
|
3463
|
+
this.connectedWallet = null;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Listen for wallet connection changes
|
|
3468
|
+
*/
|
|
3469
|
+
static onAccountChange(callback) {
|
|
3470
|
+
if (typeof window === "undefined") {
|
|
3471
|
+
return () => {
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
const cleanupFns = [];
|
|
3475
|
+
if (window.solana?.on) {
|
|
3476
|
+
window.solana.on("accountChanged", callback);
|
|
3477
|
+
cleanupFns.push(() => {
|
|
3478
|
+
window.solana?.removeListener("accountChanged", callback);
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
if (window.solflare?.on) {
|
|
3482
|
+
window.solflare.on("accountChanged", callback);
|
|
3483
|
+
cleanupFns.push(() => {
|
|
3484
|
+
window.solflare?.removeListener("accountChanged", callback);
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
return () => {
|
|
3488
|
+
cleanupFns.forEach((fn) => fn());
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
/**
|
|
3492
|
+
* Listen for wallet disconnection
|
|
3493
|
+
*/
|
|
3494
|
+
static onDisconnect(callback) {
|
|
3495
|
+
if (typeof window === "undefined") {
|
|
3496
|
+
return () => {
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
const cleanupFns = [];
|
|
3500
|
+
if (window.solana?.on) {
|
|
3501
|
+
window.solana.on("disconnect", callback);
|
|
3502
|
+
cleanupFns.push(() => {
|
|
3503
|
+
window.solana?.removeListener("disconnect", callback);
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
if (window.solflare?.on) {
|
|
3507
|
+
window.solflare.on("disconnect", callback);
|
|
3508
|
+
cleanupFns.push(() => {
|
|
3509
|
+
window.solflare?.removeListener("disconnect", callback);
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
return () => {
|
|
3513
|
+
cleanupFns.forEach((fn) => fn());
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3516
|
+
/**
|
|
3517
|
+
* Show install prompt UI
|
|
3518
|
+
*/
|
|
3519
|
+
static showInstallPrompt() {
|
|
3520
|
+
const message = `
|
|
3521
|
+
No Solana wallet detected!
|
|
3522
|
+
|
|
3523
|
+
Install one of these wallets:
|
|
3524
|
+
\u2022 Phantom: https://phantom.app
|
|
3525
|
+
\u2022 Solflare: https://solflare.com
|
|
3526
|
+
\u2022 Backpack: https://backpack.app
|
|
3527
|
+
`.trim();
|
|
3528
|
+
console.warn(message);
|
|
3529
|
+
if (typeof window !== "undefined") {
|
|
3530
|
+
const userChoice = window.confirm(
|
|
3531
|
+
"No Solana wallet detected.\n\nWould you like to install Phantom wallet?"
|
|
3532
|
+
);
|
|
3533
|
+
if (userChoice) {
|
|
3534
|
+
window.open("https://phantom.app", "_blank");
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
/**
|
|
3539
|
+
* Check if wallet is installed
|
|
3540
|
+
*/
|
|
3541
|
+
static isWalletInstalled(provider) {
|
|
3542
|
+
return this.detectWallets().includes(provider);
|
|
3543
|
+
}
|
|
3544
|
+
/**
|
|
3545
|
+
* Get wallet download URL
|
|
3546
|
+
*/
|
|
3547
|
+
static getWalletUrl(provider) {
|
|
3548
|
+
const urls = {
|
|
3549
|
+
phantom: "https://phantom.app",
|
|
3550
|
+
solflare: "https://solflare.com",
|
|
3551
|
+
backpack: "https://backpack.app",
|
|
3552
|
+
coinbase: "https://www.coinbase.com/wallet",
|
|
3553
|
+
trust: "https://trustwallet.com"
|
|
3554
|
+
};
|
|
3555
|
+
return urls[provider];
|
|
3556
|
+
}
|
|
3557
|
+
};
|
|
3558
|
+
function createWalletHook() {
|
|
3559
|
+
let useState;
|
|
3560
|
+
let useEffect;
|
|
3561
|
+
try {
|
|
3562
|
+
const React = __require("react");
|
|
3563
|
+
useState = React.useState;
|
|
3564
|
+
useEffect = React.useEffect;
|
|
3565
|
+
} catch {
|
|
3566
|
+
throw new Error("React not found. Install react to use wallet hooks.");
|
|
3567
|
+
}
|
|
3568
|
+
return function useWallet() {
|
|
3569
|
+
const [wallet, setWallet] = useState(null);
|
|
3570
|
+
const [connecting, setConnecting] = useState(false);
|
|
3571
|
+
const [error, setError] = useState(null);
|
|
3572
|
+
const connect = async (config) => {
|
|
3573
|
+
setConnecting(true);
|
|
3574
|
+
setError(null);
|
|
3575
|
+
try {
|
|
3576
|
+
const connected = await WalletConnector.detectAndConnect(config);
|
|
3577
|
+
setWallet(connected);
|
|
3578
|
+
} catch (err) {
|
|
3579
|
+
setError(err);
|
|
3580
|
+
} finally {
|
|
3581
|
+
setConnecting(false);
|
|
3582
|
+
}
|
|
3583
|
+
};
|
|
3584
|
+
const disconnect = async () => {
|
|
3585
|
+
await WalletConnector.disconnect();
|
|
3586
|
+
setWallet(null);
|
|
3587
|
+
};
|
|
3588
|
+
useEffect(() => {
|
|
3589
|
+
const cleanup = WalletConnector.onAccountChange((publicKey) => {
|
|
3590
|
+
if (wallet) {
|
|
3591
|
+
setWallet({ ...wallet, publicKey, address: publicKey.toString() });
|
|
3592
|
+
}
|
|
3593
|
+
});
|
|
3594
|
+
return cleanup;
|
|
3595
|
+
}, [wallet]);
|
|
3596
|
+
return {
|
|
3597
|
+
wallet,
|
|
3598
|
+
connecting,
|
|
3599
|
+
error,
|
|
3600
|
+
connect,
|
|
3601
|
+
disconnect,
|
|
3602
|
+
isConnected: wallet !== null
|
|
3603
|
+
};
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
// src/helpers/security.ts
|
|
3608
|
+
var PINValidator = class {
|
|
3609
|
+
/**
|
|
3610
|
+
* Validate PIN strength and format
|
|
3611
|
+
*/
|
|
3612
|
+
static validate(pin) {
|
|
3613
|
+
const errors = [];
|
|
3614
|
+
const suggestions = [];
|
|
3615
|
+
if (!pin || pin.length < 4) {
|
|
3616
|
+
errors.push("PIN must be at least 4 digits");
|
|
3617
|
+
}
|
|
3618
|
+
if (pin.length > 12) {
|
|
3619
|
+
errors.push("PIN must be at most 12 digits");
|
|
3620
|
+
}
|
|
3621
|
+
if (!/^\d+$/.test(pin)) {
|
|
3622
|
+
errors.push("PIN must contain only digits");
|
|
3623
|
+
}
|
|
3624
|
+
if (this.isWeakPattern(pin)) {
|
|
3625
|
+
errors.push("PIN is too simple");
|
|
3626
|
+
suggestions.push("Avoid sequential numbers (1234) or repeated digits (1111)");
|
|
3627
|
+
}
|
|
3628
|
+
let strength = "strong";
|
|
3629
|
+
if (errors.length > 0) {
|
|
3630
|
+
strength = "weak";
|
|
3631
|
+
} else if (pin.length <= 4 || this.hasRepeatedDigits(pin)) {
|
|
3632
|
+
strength = "weak";
|
|
3633
|
+
suggestions.push("Use at least 6 digits for better security");
|
|
3634
|
+
} else if (pin.length <= 6) {
|
|
3635
|
+
strength = "medium";
|
|
3636
|
+
suggestions.push("Use 8+ digits for maximum security");
|
|
3637
|
+
}
|
|
3638
|
+
return {
|
|
3639
|
+
valid: errors.length === 0,
|
|
3640
|
+
strength,
|
|
3641
|
+
errors,
|
|
3642
|
+
suggestions
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3645
|
+
/**
|
|
3646
|
+
* Check PIN strength (0-100 score)
|
|
3647
|
+
*/
|
|
3648
|
+
static strengthScore(pin) {
|
|
3649
|
+
if (!pin) return 0;
|
|
3650
|
+
let score = 0;
|
|
3651
|
+
score += Math.min(pin.length * 10, 50);
|
|
3652
|
+
const uniqueDigits = new Set(pin).size;
|
|
3653
|
+
score += uniqueDigits * 5;
|
|
3654
|
+
if (!this.isSequential(pin)) {
|
|
3655
|
+
score += 20;
|
|
3656
|
+
}
|
|
3657
|
+
if (!this.hasRepeatedDigits(pin)) {
|
|
3658
|
+
score += 10;
|
|
3659
|
+
}
|
|
3660
|
+
return Math.min(score, 100);
|
|
3661
|
+
}
|
|
3662
|
+
/**
|
|
3663
|
+
* Generate a secure random PIN
|
|
3664
|
+
*/
|
|
3665
|
+
static generateSecureRandom(length = 6) {
|
|
3666
|
+
if (length < 4 || length > 12) {
|
|
3667
|
+
throw new Error("PIN length must be between 4 and 12");
|
|
3668
|
+
}
|
|
3669
|
+
const array = new Uint32Array(length);
|
|
3670
|
+
crypto.getRandomValues(array);
|
|
3671
|
+
const pin = Array.from(array).map((num) => num % 10).join("");
|
|
3672
|
+
if (this.isWeakPattern(pin)) {
|
|
3673
|
+
return this.generateSecureRandom(length);
|
|
3674
|
+
}
|
|
3675
|
+
return pin;
|
|
3676
|
+
}
|
|
3677
|
+
/**
|
|
3678
|
+
* Check for weak patterns
|
|
3679
|
+
*/
|
|
3680
|
+
static isWeakPattern(pin) {
|
|
3681
|
+
if (this.isSequential(pin)) return true;
|
|
3682
|
+
if (/^(\d)\1+$/.test(pin)) return true;
|
|
3683
|
+
const commonPatterns = [
|
|
3684
|
+
"1234",
|
|
3685
|
+
"4321",
|
|
3686
|
+
"1111",
|
|
3687
|
+
"2222",
|
|
3688
|
+
"3333",
|
|
3689
|
+
"4444",
|
|
3690
|
+
"5555",
|
|
3691
|
+
"6666",
|
|
3692
|
+
"7777",
|
|
3693
|
+
"8888",
|
|
3694
|
+
"9999",
|
|
3695
|
+
"0000",
|
|
3696
|
+
"1212",
|
|
3697
|
+
"2323",
|
|
3698
|
+
"0123",
|
|
3699
|
+
"3210",
|
|
3700
|
+
"9876",
|
|
3701
|
+
"6789"
|
|
3702
|
+
];
|
|
3703
|
+
return commonPatterns.some((pattern) => pin.includes(pattern));
|
|
3704
|
+
}
|
|
3705
|
+
/**
|
|
3706
|
+
* Check if PIN is sequential
|
|
3707
|
+
*/
|
|
3708
|
+
static isSequential(pin) {
|
|
3709
|
+
for (let i = 1; i < pin.length; i++) {
|
|
3710
|
+
const diff = parseInt(pin[i]) - parseInt(pin[i - 1]);
|
|
3711
|
+
if (Math.abs(diff) !== 1) return false;
|
|
3712
|
+
}
|
|
3713
|
+
return true;
|
|
3714
|
+
}
|
|
3715
|
+
/**
|
|
3716
|
+
* Check if PIN has repeated digits
|
|
3717
|
+
*/
|
|
3718
|
+
static hasRepeatedDigits(pin) {
|
|
3719
|
+
return /(\d)\1{2,}/.test(pin);
|
|
3720
|
+
}
|
|
3721
|
+
/**
|
|
3722
|
+
* Hash PIN for storage (if needed)
|
|
3723
|
+
* Note: For device-bound keys, the PIN is used for key derivation, not storage
|
|
3724
|
+
*/
|
|
3725
|
+
static async hashPIN(pin, salt) {
|
|
3726
|
+
const actualSalt = salt || crypto.getRandomValues(new Uint8Array(16));
|
|
3727
|
+
const encoder = new TextEncoder();
|
|
3728
|
+
const data = encoder.encode(pin + actualSalt);
|
|
3729
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
3730
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
3731
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3732
|
+
}
|
|
3733
|
+
};
|
|
3734
|
+
var PINRateLimiter = class {
|
|
3735
|
+
attempts = /* @__PURE__ */ new Map();
|
|
3736
|
+
locked = /* @__PURE__ */ new Map();
|
|
3737
|
+
maxAttempts;
|
|
3738
|
+
windowMs;
|
|
3739
|
+
lockoutMs;
|
|
3740
|
+
constructor(config = {}) {
|
|
3741
|
+
this.maxAttempts = config.maxAttempts || 3;
|
|
3742
|
+
this.windowMs = config.windowMs || 6e4;
|
|
3743
|
+
this.lockoutMs = config.lockoutMs || 3e5;
|
|
3744
|
+
}
|
|
3745
|
+
/**
|
|
3746
|
+
* Check if attempt is allowed
|
|
3747
|
+
*/
|
|
3748
|
+
async checkAttempt(sessionKeyId) {
|
|
3749
|
+
const now = Date.now();
|
|
3750
|
+
const lockoutUntil = this.locked.get(sessionKeyId);
|
|
3751
|
+
if (lockoutUntil && now < lockoutUntil) {
|
|
3752
|
+
const lockoutSeconds = Math.ceil((lockoutUntil - now) / 1e3);
|
|
3753
|
+
return {
|
|
3754
|
+
allowed: false,
|
|
3755
|
+
remainingAttempts: 0,
|
|
3756
|
+
lockoutSeconds
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
if (lockoutUntil && now >= lockoutUntil) {
|
|
3760
|
+
this.locked.delete(sessionKeyId);
|
|
3761
|
+
this.attempts.delete(sessionKeyId);
|
|
3762
|
+
}
|
|
3763
|
+
const recentAttempts = this.getRecentAttempts(sessionKeyId, now);
|
|
3764
|
+
const remainingAttempts = this.maxAttempts - recentAttempts.length;
|
|
3765
|
+
if (remainingAttempts <= 0) {
|
|
3766
|
+
this.locked.set(sessionKeyId, now + this.lockoutMs);
|
|
3767
|
+
return {
|
|
3768
|
+
allowed: false,
|
|
3769
|
+
remainingAttempts: 0,
|
|
3770
|
+
lockoutSeconds: Math.ceil(this.lockoutMs / 1e3)
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
return {
|
|
3774
|
+
allowed: true,
|
|
3775
|
+
remainingAttempts
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
/**
|
|
3779
|
+
* Record a failed attempt
|
|
3780
|
+
*/
|
|
3781
|
+
recordFailedAttempt(sessionKeyId) {
|
|
3782
|
+
const now = Date.now();
|
|
3783
|
+
const attempts = this.attempts.get(sessionKeyId) || [];
|
|
3784
|
+
attempts.push(now);
|
|
3785
|
+
this.attempts.set(sessionKeyId, attempts);
|
|
3786
|
+
}
|
|
3787
|
+
/**
|
|
3788
|
+
* Record a successful attempt (clears history)
|
|
3789
|
+
*/
|
|
3790
|
+
recordSuccessfulAttempt(sessionKeyId) {
|
|
3791
|
+
this.attempts.delete(sessionKeyId);
|
|
3792
|
+
this.locked.delete(sessionKeyId);
|
|
3793
|
+
}
|
|
3794
|
+
/**
|
|
3795
|
+
* Get recent attempts within window
|
|
3796
|
+
*/
|
|
3797
|
+
getRecentAttempts(sessionKeyId, now) {
|
|
3798
|
+
const attempts = this.attempts.get(sessionKeyId) || [];
|
|
3799
|
+
const cutoff = now - this.windowMs;
|
|
3800
|
+
const recent = attempts.filter((timestamp) => timestamp > cutoff);
|
|
3801
|
+
if (recent.length !== attempts.length) {
|
|
3802
|
+
this.attempts.set(sessionKeyId, recent);
|
|
3803
|
+
}
|
|
3804
|
+
return recent;
|
|
3805
|
+
}
|
|
3806
|
+
/**
|
|
3807
|
+
* Clear all rate limit data
|
|
3808
|
+
*/
|
|
3809
|
+
clear() {
|
|
3810
|
+
this.attempts.clear();
|
|
3811
|
+
this.locked.clear();
|
|
3812
|
+
}
|
|
3813
|
+
/**
|
|
3814
|
+
* Check lockout status
|
|
3815
|
+
*/
|
|
3816
|
+
isLockedOut(sessionKeyId) {
|
|
3817
|
+
const lockoutUntil = this.locked.get(sessionKeyId);
|
|
3818
|
+
return lockoutUntil ? Date.now() < lockoutUntil : false;
|
|
3819
|
+
}
|
|
3820
|
+
/**
|
|
3821
|
+
* Get remaining lockout time
|
|
3822
|
+
*/
|
|
3823
|
+
getRemainingLockoutTime(sessionKeyId) {
|
|
3824
|
+
const lockoutUntil = this.locked.get(sessionKeyId);
|
|
3825
|
+
if (!lockoutUntil) return 0;
|
|
3826
|
+
return Math.max(0, lockoutUntil - Date.now());
|
|
3827
|
+
}
|
|
3828
|
+
};
|
|
3829
|
+
var SecureStorage = class {
|
|
3830
|
+
/**
|
|
3831
|
+
* Store data with encryption (basic)
|
|
3832
|
+
* For production, consider using Web Crypto API with user-derived keys
|
|
3833
|
+
*/
|
|
3834
|
+
static async setEncrypted(key, value, secret) {
|
|
3835
|
+
const encrypted = await this.encrypt(value, secret);
|
|
3836
|
+
localStorage.setItem(key, JSON.stringify(encrypted));
|
|
3837
|
+
}
|
|
3838
|
+
/**
|
|
3839
|
+
* Retrieve encrypted data
|
|
3840
|
+
*/
|
|
3841
|
+
static async getEncrypted(key, secret) {
|
|
3842
|
+
const stored = localStorage.getItem(key);
|
|
3843
|
+
if (!stored) return null;
|
|
3844
|
+
try {
|
|
3845
|
+
const encrypted = JSON.parse(stored);
|
|
3846
|
+
return await this.decrypt(encrypted, secret);
|
|
3847
|
+
} catch {
|
|
3848
|
+
return null;
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
/**
|
|
3852
|
+
* Encrypt string with AES-GCM
|
|
3853
|
+
*/
|
|
3854
|
+
static async encrypt(plaintext, secret) {
|
|
3855
|
+
const encoder = new TextEncoder();
|
|
3856
|
+
const data = encoder.encode(plaintext);
|
|
3857
|
+
const key = await this.deriveKey(secret);
|
|
3858
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
3859
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
3860
|
+
{ name: "AES-GCM", iv },
|
|
3861
|
+
key,
|
|
3862
|
+
data
|
|
3863
|
+
);
|
|
3864
|
+
return {
|
|
3865
|
+
ciphertext: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
|
|
3866
|
+
iv: btoa(String.fromCharCode(...iv))
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
/**
|
|
3870
|
+
* Decrypt AES-GCM ciphertext
|
|
3871
|
+
*/
|
|
3872
|
+
static async decrypt(encrypted, secret) {
|
|
3873
|
+
const key = await this.deriveKey(secret);
|
|
3874
|
+
const iv = Uint8Array.from(atob(encrypted.iv), (c) => c.charCodeAt(0));
|
|
3875
|
+
const ciphertext = Uint8Array.from(atob(encrypted.ciphertext), (c) => c.charCodeAt(0));
|
|
3876
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
3877
|
+
{ name: "AES-GCM", iv },
|
|
3878
|
+
key,
|
|
3879
|
+
ciphertext
|
|
3880
|
+
);
|
|
3881
|
+
const decoder = new TextDecoder();
|
|
3882
|
+
return decoder.decode(decrypted);
|
|
3883
|
+
}
|
|
3884
|
+
/**
|
|
3885
|
+
* Derive encryption key from secret
|
|
3886
|
+
*/
|
|
3887
|
+
static async deriveKey(secret) {
|
|
3888
|
+
const encoder = new TextEncoder();
|
|
3889
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
3890
|
+
"raw",
|
|
3891
|
+
encoder.encode(secret),
|
|
3892
|
+
{ name: "PBKDF2" },
|
|
3893
|
+
false,
|
|
3894
|
+
["deriveBits", "deriveKey"]
|
|
3895
|
+
);
|
|
3896
|
+
return await crypto.subtle.deriveKey(
|
|
3897
|
+
{
|
|
3898
|
+
name: "PBKDF2",
|
|
3899
|
+
salt: encoder.encode("zendfi-secure-storage"),
|
|
3900
|
+
iterations: 1e5,
|
|
3901
|
+
hash: "SHA-256"
|
|
3902
|
+
},
|
|
3903
|
+
keyMaterial,
|
|
3904
|
+
{ name: "AES-GCM", length: 256 },
|
|
3905
|
+
false,
|
|
3906
|
+
["encrypt", "decrypt"]
|
|
3907
|
+
);
|
|
3908
|
+
}
|
|
3909
|
+
/**
|
|
3910
|
+
* Clear all secure storage
|
|
3911
|
+
*/
|
|
3912
|
+
static clearAll(namespace = "zendfi") {
|
|
3913
|
+
const keysToRemove = [];
|
|
3914
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
3915
|
+
const key = localStorage.key(i);
|
|
3916
|
+
if (key?.startsWith(namespace)) {
|
|
3917
|
+
keysToRemove.push(key);
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
|
3921
|
+
}
|
|
3922
|
+
};
|
|
3923
|
+
|
|
3924
|
+
// src/helpers/polling.ts
|
|
3925
|
+
var TransactionPoller = class {
|
|
3926
|
+
/**
|
|
3927
|
+
* Wait for transaction confirmation
|
|
3928
|
+
*/
|
|
3929
|
+
static async waitForConfirmation(signature, options = {}) {
|
|
3930
|
+
const {
|
|
3931
|
+
timeout = 6e4,
|
|
3932
|
+
interval = 2e3,
|
|
3933
|
+
maxInterval = 1e4,
|
|
3934
|
+
maxAttempts = 30,
|
|
3935
|
+
commitment = "confirmed",
|
|
3936
|
+
rpcUrl
|
|
3937
|
+
} = options;
|
|
3938
|
+
const startTime = Date.now();
|
|
3939
|
+
let currentInterval = interval;
|
|
3940
|
+
let attempts = 0;
|
|
3941
|
+
while (true) {
|
|
3942
|
+
attempts++;
|
|
3943
|
+
if (Date.now() - startTime > timeout) {
|
|
3944
|
+
return {
|
|
3945
|
+
confirmed: false,
|
|
3946
|
+
signature,
|
|
3947
|
+
error: `Transaction confirmation timeout after ${timeout}ms`
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
if (attempts > maxAttempts) {
|
|
3951
|
+
return {
|
|
3952
|
+
confirmed: false,
|
|
3953
|
+
signature,
|
|
3954
|
+
error: `Maximum polling attempts (${maxAttempts}) exceeded`
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3957
|
+
try {
|
|
3958
|
+
const status = await this.checkTransactionStatus(signature, commitment, rpcUrl);
|
|
3959
|
+
if (status.confirmed) {
|
|
3960
|
+
return status;
|
|
3961
|
+
}
|
|
3962
|
+
if (status.error) {
|
|
3963
|
+
return status;
|
|
3964
|
+
}
|
|
3965
|
+
await this.sleep(currentInterval);
|
|
3966
|
+
currentInterval = Math.min(currentInterval * 1.5, maxInterval);
|
|
3967
|
+
} catch (error) {
|
|
3968
|
+
await this.sleep(currentInterval);
|
|
3969
|
+
currentInterval = Math.min(currentInterval * 1.5, maxInterval);
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
/**
|
|
3974
|
+
* Check transaction status via RPC
|
|
3975
|
+
*/
|
|
3976
|
+
static async checkTransactionStatus(signature, commitment = "confirmed", rpcUrl) {
|
|
3977
|
+
const endpoint = rpcUrl || this.getDefaultRpcUrl();
|
|
3978
|
+
const response = await fetch(endpoint, {
|
|
3979
|
+
method: "POST",
|
|
3980
|
+
headers: { "Content-Type": "application/json" },
|
|
3981
|
+
body: JSON.stringify({
|
|
3982
|
+
jsonrpc: "2.0",
|
|
3983
|
+
id: 1,
|
|
3984
|
+
method: "getSignatureStatuses",
|
|
3985
|
+
params: [[signature], { searchTransactionHistory: true }]
|
|
3986
|
+
})
|
|
3987
|
+
});
|
|
3988
|
+
if (!response.ok) {
|
|
3989
|
+
throw new Error(`RPC error: ${response.status} ${response.statusText}`);
|
|
3990
|
+
}
|
|
3991
|
+
const data = await response.json();
|
|
3992
|
+
if (data.error) {
|
|
3993
|
+
return {
|
|
3994
|
+
confirmed: false,
|
|
3995
|
+
signature,
|
|
3996
|
+
error: data.error.message || "RPC error"
|
|
3997
|
+
};
|
|
3998
|
+
}
|
|
3999
|
+
const status = data.result?.value?.[0];
|
|
4000
|
+
if (!status) {
|
|
4001
|
+
return {
|
|
4002
|
+
confirmed: false,
|
|
4003
|
+
signature
|
|
4004
|
+
};
|
|
4005
|
+
}
|
|
4006
|
+
const isConfirmed = this.isCommitmentReached(status, commitment);
|
|
4007
|
+
return {
|
|
4008
|
+
confirmed: isConfirmed,
|
|
4009
|
+
signature,
|
|
4010
|
+
slot: status.slot,
|
|
4011
|
+
confirmations: status.confirmations,
|
|
4012
|
+
error: status.err ? JSON.stringify(status.err) : void 0
|
|
4013
|
+
};
|
|
4014
|
+
}
|
|
4015
|
+
/**
|
|
4016
|
+
* Check if commitment level is reached
|
|
4017
|
+
*/
|
|
4018
|
+
static isCommitmentReached(status, commitment) {
|
|
4019
|
+
if (status.err) return false;
|
|
4020
|
+
switch (commitment) {
|
|
4021
|
+
case "processed":
|
|
4022
|
+
return true;
|
|
4023
|
+
// Any status means processed
|
|
4024
|
+
case "confirmed":
|
|
4025
|
+
return status.confirmationStatus === "confirmed" || status.confirmationStatus === "finalized";
|
|
4026
|
+
case "finalized":
|
|
4027
|
+
return status.confirmationStatus === "finalized";
|
|
4028
|
+
default:
|
|
4029
|
+
return false;
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Get default RPC URL based on environment
|
|
4034
|
+
*/
|
|
4035
|
+
static getDefaultRpcUrl() {
|
|
4036
|
+
if (typeof window !== "undefined" && window.location) {
|
|
4037
|
+
const hostname = window.location.hostname;
|
|
4038
|
+
if (hostname.includes("localhost") || hostname.includes("dev")) {
|
|
4039
|
+
return "https://api.devnet.solana.com";
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
return "https://api.mainnet-beta.solana.com";
|
|
4043
|
+
}
|
|
4044
|
+
/**
|
|
4045
|
+
* Poll multiple transactions in parallel
|
|
4046
|
+
*/
|
|
4047
|
+
static async waitForMultiple(signatures, options = {}) {
|
|
4048
|
+
return await Promise.all(
|
|
4049
|
+
signatures.map((sig) => this.waitForConfirmation(sig, options))
|
|
4050
|
+
);
|
|
4051
|
+
}
|
|
4052
|
+
/**
|
|
4053
|
+
* Get transaction details after confirmation
|
|
4054
|
+
*/
|
|
4055
|
+
static async getTransactionDetails(signature, rpcUrl) {
|
|
4056
|
+
const endpoint = rpcUrl || this.getDefaultRpcUrl();
|
|
4057
|
+
const response = await fetch(endpoint, {
|
|
4058
|
+
method: "POST",
|
|
4059
|
+
headers: { "Content-Type": "application/json" },
|
|
4060
|
+
body: JSON.stringify({
|
|
4061
|
+
jsonrpc: "2.0",
|
|
4062
|
+
id: 1,
|
|
4063
|
+
method: "getTransaction",
|
|
4064
|
+
params: [
|
|
4065
|
+
signature,
|
|
4066
|
+
{
|
|
4067
|
+
encoding: "jsonParsed",
|
|
4068
|
+
commitment: "confirmed",
|
|
4069
|
+
maxSupportedTransactionVersion: 0
|
|
4070
|
+
}
|
|
4071
|
+
]
|
|
4072
|
+
})
|
|
4073
|
+
});
|
|
4074
|
+
if (!response.ok) {
|
|
4075
|
+
throw new Error(`RPC error: ${response.status}`);
|
|
4076
|
+
}
|
|
4077
|
+
const data = await response.json();
|
|
4078
|
+
if (data.error) {
|
|
4079
|
+
throw new Error(data.error.message || "Failed to get transaction");
|
|
4080
|
+
}
|
|
4081
|
+
return data.result;
|
|
4082
|
+
}
|
|
4083
|
+
/**
|
|
4084
|
+
* Check if transaction exists on chain
|
|
4085
|
+
*/
|
|
4086
|
+
static async exists(signature, rpcUrl) {
|
|
4087
|
+
try {
|
|
4088
|
+
const status = await this.checkTransactionStatus(signature, "confirmed", rpcUrl);
|
|
4089
|
+
return status.confirmed || !!status.slot;
|
|
4090
|
+
} catch {
|
|
4091
|
+
return false;
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
/**
|
|
4095
|
+
* Get recent blockhash (useful for transaction building)
|
|
4096
|
+
*/
|
|
4097
|
+
static async getRecentBlockhash(rpcUrl) {
|
|
4098
|
+
const endpoint = rpcUrl || this.getDefaultRpcUrl();
|
|
4099
|
+
const response = await fetch(endpoint, {
|
|
4100
|
+
method: "POST",
|
|
4101
|
+
headers: { "Content-Type": "application/json" },
|
|
4102
|
+
body: JSON.stringify({
|
|
4103
|
+
jsonrpc: "2.0",
|
|
4104
|
+
id: 1,
|
|
4105
|
+
method: "getLatestBlockhash",
|
|
4106
|
+
params: [{ commitment: "finalized" }]
|
|
4107
|
+
})
|
|
4108
|
+
});
|
|
4109
|
+
if (!response.ok) {
|
|
4110
|
+
throw new Error(`RPC error: ${response.status}`);
|
|
4111
|
+
}
|
|
4112
|
+
const data = await response.json();
|
|
4113
|
+
if (data.error) {
|
|
4114
|
+
throw new Error(data.error.message);
|
|
4115
|
+
}
|
|
4116
|
+
return data.result.value;
|
|
4117
|
+
}
|
|
4118
|
+
/**
|
|
4119
|
+
* Sleep utility
|
|
4120
|
+
*/
|
|
4121
|
+
static sleep(ms) {
|
|
4122
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4123
|
+
}
|
|
4124
|
+
};
|
|
4125
|
+
var TransactionMonitor = class {
|
|
4126
|
+
monitors = /* @__PURE__ */ new Map();
|
|
4127
|
+
/**
|
|
4128
|
+
* Start monitoring a transaction
|
|
4129
|
+
*/
|
|
4130
|
+
monitor(signature, callbacks, options = {}) {
|
|
4131
|
+
this.stopMonitoring(signature);
|
|
4132
|
+
const { timeout = 6e4, interval = 2e3 } = options;
|
|
4133
|
+
const startTime = Date.now();
|
|
4134
|
+
const intervalId = setInterval(async () => {
|
|
4135
|
+
if (Date.now() - startTime > timeout) {
|
|
4136
|
+
this.stopMonitoring(signature);
|
|
4137
|
+
callbacks.onTimeout?.();
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
try {
|
|
4141
|
+
const status = await TransactionPoller.waitForConfirmation(signature, {
|
|
4142
|
+
...options,
|
|
4143
|
+
maxAttempts: 1
|
|
4144
|
+
// Single check per interval
|
|
4145
|
+
});
|
|
4146
|
+
if (status.confirmed) {
|
|
4147
|
+
this.stopMonitoring(signature);
|
|
4148
|
+
callbacks.onConfirmed?.(status);
|
|
4149
|
+
} else if (status.error) {
|
|
4150
|
+
this.stopMonitoring(signature);
|
|
4151
|
+
callbacks.onFailed?.(status);
|
|
4152
|
+
}
|
|
4153
|
+
} catch (error) {
|
|
4154
|
+
}
|
|
4155
|
+
}, interval);
|
|
4156
|
+
this.monitors.set(signature, { interval: intervalId, callbacks });
|
|
4157
|
+
}
|
|
4158
|
+
/**
|
|
4159
|
+
* Stop monitoring a transaction
|
|
4160
|
+
*/
|
|
4161
|
+
stopMonitoring(signature) {
|
|
4162
|
+
const monitor = this.monitors.get(signature);
|
|
4163
|
+
if (monitor) {
|
|
4164
|
+
clearInterval(monitor.interval);
|
|
4165
|
+
this.monitors.delete(signature);
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
/**
|
|
4169
|
+
* Stop all monitors
|
|
4170
|
+
*/
|
|
4171
|
+
stopAll() {
|
|
4172
|
+
for (const [signature] of this.monitors) {
|
|
4173
|
+
this.stopMonitoring(signature);
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
/**
|
|
4177
|
+
* Get active monitors
|
|
4178
|
+
*/
|
|
4179
|
+
getActiveMonitors() {
|
|
4180
|
+
return Array.from(this.monitors.keys());
|
|
4181
|
+
}
|
|
4182
|
+
};
|
|
4183
|
+
|
|
4184
|
+
// src/helpers/recovery.ts
|
|
4185
|
+
var RetryStrategy = class {
|
|
4186
|
+
/**
|
|
4187
|
+
* Execute function with retry logic
|
|
4188
|
+
*/
|
|
4189
|
+
static async withRetry(fn, options = {}) {
|
|
4190
|
+
const {
|
|
4191
|
+
maxAttempts = 3,
|
|
4192
|
+
backoffMs = 1e3,
|
|
4193
|
+
backoffMultiplier = 2,
|
|
4194
|
+
maxBackoffMs = 3e4,
|
|
4195
|
+
shouldRetry = () => true,
|
|
4196
|
+
onRetry
|
|
4197
|
+
} = options;
|
|
4198
|
+
let lastError = null;
|
|
4199
|
+
let currentBackoff = backoffMs;
|
|
4200
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4201
|
+
try {
|
|
4202
|
+
return await fn();
|
|
4203
|
+
} catch (error) {
|
|
4204
|
+
lastError = error;
|
|
4205
|
+
if (attempt === maxAttempts || !shouldRetry(error, attempt)) {
|
|
4206
|
+
throw error;
|
|
4207
|
+
}
|
|
4208
|
+
const jitter = Math.random() * 0.3 * currentBackoff;
|
|
4209
|
+
const nextDelay = Math.min(currentBackoff + jitter, maxBackoffMs);
|
|
4210
|
+
onRetry?.(error, attempt, nextDelay);
|
|
4211
|
+
await this.sleep(nextDelay);
|
|
4212
|
+
currentBackoff *= backoffMultiplier;
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
throw lastError || new Error("Retry failed");
|
|
4216
|
+
}
|
|
4217
|
+
/**
|
|
4218
|
+
* Retry with linear backoff
|
|
4219
|
+
*/
|
|
4220
|
+
static async withLinearRetry(fn, maxAttempts = 3, delayMs = 1e3) {
|
|
4221
|
+
return this.withRetry(fn, {
|
|
4222
|
+
maxAttempts,
|
|
4223
|
+
backoffMs: delayMs,
|
|
4224
|
+
backoffMultiplier: 1
|
|
4225
|
+
// Linear
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4228
|
+
/**
|
|
4229
|
+
* Retry with custom backoff function
|
|
4230
|
+
*/
|
|
4231
|
+
static async withCustomBackoff(fn, backoffFn, maxAttempts = 3) {
|
|
4232
|
+
let lastError = null;
|
|
4233
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4234
|
+
try {
|
|
4235
|
+
return await fn();
|
|
4236
|
+
} catch (error) {
|
|
4237
|
+
lastError = error;
|
|
4238
|
+
if (attempt === maxAttempts) {
|
|
4239
|
+
throw error;
|
|
4240
|
+
}
|
|
4241
|
+
const delay = backoffFn(attempt);
|
|
4242
|
+
await this.sleep(delay);
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
throw lastError || new Error("Retry failed");
|
|
4246
|
+
}
|
|
4247
|
+
/**
|
|
4248
|
+
* Check if error is retryable
|
|
4249
|
+
*/
|
|
4250
|
+
static isRetryableError(error) {
|
|
4251
|
+
if (error.name === "NetworkError" || error.message?.includes("network")) {
|
|
4252
|
+
return true;
|
|
4253
|
+
}
|
|
4254
|
+
if (error.name === "TimeoutError" || error.message?.includes("timeout")) {
|
|
4255
|
+
return true;
|
|
4256
|
+
}
|
|
4257
|
+
if (error.status === 429 || error.code === "RATE_LIMIT_EXCEEDED") {
|
|
4258
|
+
return true;
|
|
4259
|
+
}
|
|
4260
|
+
if (error.status >= 500 && error.status < 600) {
|
|
4261
|
+
return true;
|
|
4262
|
+
}
|
|
4263
|
+
if (error.message?.includes("blockhash") || error.message?.includes("recent")) {
|
|
4264
|
+
return true;
|
|
4265
|
+
}
|
|
4266
|
+
return false;
|
|
4267
|
+
}
|
|
4268
|
+
static sleep(ms) {
|
|
4269
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
var ErrorRecovery = class {
|
|
4273
|
+
/**
|
|
4274
|
+
* Recover from network errors with retry
|
|
4275
|
+
*/
|
|
4276
|
+
static async recoverFromNetworkError(fn, maxAttempts = 3) {
|
|
4277
|
+
return RetryStrategy.withRetry(fn, {
|
|
4278
|
+
maxAttempts,
|
|
4279
|
+
backoffMs: 2e3,
|
|
4280
|
+
shouldRetry: (error) => {
|
|
4281
|
+
return error.name === "NetworkError" || error.message?.includes("network") || error.message?.includes("fetch");
|
|
4282
|
+
},
|
|
4283
|
+
onRetry: (error, attempt, nextDelay) => {
|
|
4284
|
+
console.warn(
|
|
4285
|
+
`Network error (attempt ${attempt}), retrying in ${nextDelay}ms:`,
|
|
4286
|
+
error.message
|
|
4287
|
+
);
|
|
4288
|
+
}
|
|
4289
|
+
});
|
|
4290
|
+
}
|
|
4291
|
+
/**
|
|
4292
|
+
* Recover from rate limit errors with exponential backoff
|
|
4293
|
+
*/
|
|
4294
|
+
static async recoverFromRateLimit(fn, maxAttempts = 5) {
|
|
4295
|
+
return RetryStrategy.withRetry(fn, {
|
|
4296
|
+
maxAttempts,
|
|
4297
|
+
backoffMs: 5e3,
|
|
4298
|
+
// Start with 5 seconds
|
|
4299
|
+
backoffMultiplier: 2,
|
|
4300
|
+
maxBackoffMs: 6e4,
|
|
4301
|
+
// Cap at 1 minute
|
|
4302
|
+
shouldRetry: (error) => {
|
|
4303
|
+
return error.status === 429 || error.code === "RATE_LIMIT_EXCEEDED";
|
|
4304
|
+
},
|
|
4305
|
+
onRetry: (attempt, nextDelay) => {
|
|
4306
|
+
console.warn(
|
|
4307
|
+
`Rate limited (attempt ${attempt}), waiting ${nextDelay}ms before retry`
|
|
4308
|
+
);
|
|
4309
|
+
}
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
4312
|
+
/**
|
|
4313
|
+
* Recover from Solana RPC errors (blockhash, etc.)
|
|
4314
|
+
*/
|
|
4315
|
+
static async recoverFromRPCError(fn, maxAttempts = 3) {
|
|
4316
|
+
return RetryStrategy.withRetry(fn, {
|
|
4317
|
+
maxAttempts,
|
|
4318
|
+
backoffMs: 1e3,
|
|
4319
|
+
shouldRetry: (error) => {
|
|
4320
|
+
const message = error.message?.toLowerCase() || "";
|
|
4321
|
+
return message.includes("blockhash") || message.includes("recent") || message.includes("slot") || message.includes("rpc");
|
|
4322
|
+
},
|
|
4323
|
+
onRetry: (error, attempt, nextDelay) => {
|
|
4324
|
+
console.warn(
|
|
4325
|
+
`RPC error (attempt ${attempt}), retrying in ${nextDelay}ms:`,
|
|
4326
|
+
error.message
|
|
4327
|
+
);
|
|
4328
|
+
}
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
4331
|
+
/**
|
|
4332
|
+
* Recover from timeout errors
|
|
4333
|
+
*/
|
|
4334
|
+
static async recoverFromTimeout(fn, timeoutMs = 3e4, maxAttempts = 2) {
|
|
4335
|
+
return RetryStrategy.withRetry(
|
|
4336
|
+
() => this.withTimeout(fn, timeoutMs),
|
|
4337
|
+
{
|
|
4338
|
+
maxAttempts,
|
|
4339
|
+
backoffMs: 5e3,
|
|
4340
|
+
shouldRetry: (error) => {
|
|
4341
|
+
return error.name === "TimeoutError" || error.message?.includes("timeout");
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
);
|
|
4345
|
+
}
|
|
4346
|
+
/**
|
|
4347
|
+
* Add timeout to async function
|
|
4348
|
+
*/
|
|
4349
|
+
static async withTimeout(fn, timeoutMs) {
|
|
4350
|
+
return Promise.race([
|
|
4351
|
+
fn(),
|
|
4352
|
+
new Promise((_, reject) => {
|
|
4353
|
+
setTimeout(() => {
|
|
4354
|
+
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
4355
|
+
}, timeoutMs);
|
|
4356
|
+
})
|
|
4357
|
+
]);
|
|
4358
|
+
}
|
|
4359
|
+
/**
|
|
4360
|
+
* Circuit breaker pattern for repeated failures
|
|
4361
|
+
*/
|
|
4362
|
+
static createCircuitBreaker(fn, options = {}) {
|
|
4363
|
+
const {
|
|
4364
|
+
failureThreshold = 5,
|
|
4365
|
+
resetTimeoutMs = 6e4,
|
|
4366
|
+
onStateChange
|
|
4367
|
+
} = options;
|
|
4368
|
+
let state = "closed";
|
|
4369
|
+
let failureCount = 0;
|
|
4370
|
+
let lastFailureTime = 0;
|
|
4371
|
+
let resetTimer = null;
|
|
4372
|
+
return async () => {
|
|
4373
|
+
if (state === "open" && Date.now() - lastFailureTime > resetTimeoutMs) {
|
|
4374
|
+
state = "half-open";
|
|
4375
|
+
onStateChange?.("half-open");
|
|
4376
|
+
}
|
|
4377
|
+
if (state === "open") {
|
|
4378
|
+
throw new Error("Circuit breaker is OPEN - too many failures");
|
|
4379
|
+
}
|
|
4380
|
+
try {
|
|
4381
|
+
const result = await fn();
|
|
4382
|
+
if (state === "half-open") {
|
|
4383
|
+
state = "closed";
|
|
4384
|
+
onStateChange?.("closed");
|
|
4385
|
+
}
|
|
4386
|
+
failureCount = 0;
|
|
4387
|
+
return result;
|
|
4388
|
+
} catch (error) {
|
|
4389
|
+
failureCount++;
|
|
4390
|
+
lastFailureTime = Date.now();
|
|
4391
|
+
if (failureCount >= failureThreshold) {
|
|
4392
|
+
state = "open";
|
|
4393
|
+
onStateChange?.("open");
|
|
4394
|
+
if (resetTimer) clearTimeout(resetTimer);
|
|
4395
|
+
resetTimer = setTimeout(() => {
|
|
4396
|
+
state = "half-open";
|
|
4397
|
+
onStateChange?.("half-open");
|
|
4398
|
+
}, resetTimeoutMs);
|
|
4399
|
+
}
|
|
4400
|
+
throw error;
|
|
4401
|
+
}
|
|
4402
|
+
};
|
|
4403
|
+
}
|
|
4404
|
+
/**
|
|
4405
|
+
* Fallback to alternative function on error
|
|
4406
|
+
*/
|
|
4407
|
+
static async withFallback(primaryFn, fallbackFn, shouldFallback = () => true) {
|
|
4408
|
+
try {
|
|
4409
|
+
return await primaryFn();
|
|
4410
|
+
} catch (error) {
|
|
4411
|
+
if (shouldFallback(error)) {
|
|
4412
|
+
console.warn("Primary function failed, using fallback:", error.message);
|
|
4413
|
+
return await fallbackFn();
|
|
4414
|
+
}
|
|
4415
|
+
throw error;
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
/**
|
|
4419
|
+
* Graceful degradation - return partial result on error
|
|
4420
|
+
*/
|
|
4421
|
+
static async withGracefulDegradation(fn, defaultValue, onError) {
|
|
4422
|
+
try {
|
|
4423
|
+
return await fn();
|
|
4424
|
+
} catch (error) {
|
|
4425
|
+
onError?.(error);
|
|
4426
|
+
console.warn("Operation failed, returning default value:", error.message);
|
|
4427
|
+
return defaultValue;
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
};
|
|
4431
|
+
|
|
4432
|
+
// src/helpers/lifecycle.ts
|
|
4433
|
+
var SessionKeyLifecycle = class {
|
|
4434
|
+
constructor(client, config = {}) {
|
|
4435
|
+
this.client = client;
|
|
4436
|
+
this.config = config;
|
|
4437
|
+
if (config.autoCleanup && typeof window !== "undefined") {
|
|
4438
|
+
window.addEventListener("beforeunload", () => {
|
|
4439
|
+
this.cleanup().catch(console.error);
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
sessionKeyId = null;
|
|
4444
|
+
sessionWallet = null;
|
|
4445
|
+
encryptedKey = null;
|
|
4446
|
+
deviceFingerprint = null;
|
|
4447
|
+
/**
|
|
4448
|
+
* Create and fund session key in one call
|
|
4449
|
+
* Handles: keypair generation → encryption → backend registration → funding
|
|
4450
|
+
*/
|
|
4451
|
+
async createAndFund(config) {
|
|
4452
|
+
const fingerprint = this.config.deviceFingerprintProvider ? await this.config.deviceFingerprintProvider() : await this.generateDeviceFingerprint();
|
|
4453
|
+
this.deviceFingerprint = fingerprint;
|
|
4454
|
+
const pin = this.config.pinProvider ? await this.config.pinProvider() : await this.promptForPIN("Create PIN for session key");
|
|
4455
|
+
const { Keypair } = await this.getSolanaWeb3();
|
|
4456
|
+
const { SessionKeyCrypto: SessionKeyCrypto2 } = await import("./device-bound-crypto-VX7SFVHT.mjs");
|
|
4457
|
+
const keypair = Keypair.generate();
|
|
4458
|
+
const encrypted = await SessionKeyCrypto2.encrypt(
|
|
4459
|
+
keypair.secretKey,
|
|
4460
|
+
pin,
|
|
4461
|
+
fingerprint
|
|
4462
|
+
);
|
|
4463
|
+
this.encryptedKey = {
|
|
4464
|
+
ciphertext: encrypted.encryptedData,
|
|
4465
|
+
nonce: encrypted.nonce
|
|
4466
|
+
};
|
|
4467
|
+
const response = await this.client.sessionKeys.createDeviceBound({
|
|
4468
|
+
user_wallet: config.userWallet,
|
|
4469
|
+
agent_id: config.agentId,
|
|
4470
|
+
agent_name: config.agentName,
|
|
4471
|
+
limit_usdc: config.limitUsdc,
|
|
4472
|
+
duration_days: config.durationDays || 7,
|
|
4473
|
+
encrypted_session_key: encrypted.encryptedData,
|
|
4474
|
+
nonce: encrypted.nonce,
|
|
4475
|
+
session_public_key: keypair.publicKey.toBase58(),
|
|
4476
|
+
device_fingerprint: fingerprint
|
|
4477
|
+
});
|
|
4478
|
+
this.sessionKeyId = response.session_key_id;
|
|
4479
|
+
this.sessionWallet = response.session_wallet;
|
|
4480
|
+
if (this.config.cache) {
|
|
4481
|
+
await this.config.cache.getCached(
|
|
4482
|
+
this.sessionKeyId,
|
|
4483
|
+
async () => keypair,
|
|
4484
|
+
{ deviceFingerprint: fingerprint }
|
|
4485
|
+
);
|
|
4486
|
+
}
|
|
4487
|
+
if (config.onApprovalNeeded) {
|
|
4488
|
+
const topupResponse = await this.client.sessionKeys.topUp(
|
|
4489
|
+
this.sessionKeyId,
|
|
4490
|
+
{
|
|
4491
|
+
user_wallet: config.userWallet,
|
|
4492
|
+
amount_usdc: config.limitUsdc,
|
|
4493
|
+
device_fingerprint: fingerprint
|
|
4494
|
+
}
|
|
4495
|
+
);
|
|
4496
|
+
await config.onApprovalNeeded(topupResponse.top_up_transaction);
|
|
4497
|
+
}
|
|
4498
|
+
return {
|
|
4499
|
+
sessionKeyId: this.sessionKeyId,
|
|
4500
|
+
sessionWallet: this.sessionWallet
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4503
|
+
/**
|
|
4504
|
+
* Make a payment using the session key
|
|
4505
|
+
* Auto-handles caching, PIN prompts, and signing
|
|
4506
|
+
*/
|
|
4507
|
+
async pay(amount, description) {
|
|
4508
|
+
if (!this.sessionKeyId) {
|
|
4509
|
+
throw new Error("No active session key. Call createAndFund() first.");
|
|
4510
|
+
}
|
|
4511
|
+
let keypair;
|
|
4512
|
+
if (this.config.cache) {
|
|
4513
|
+
keypair = await this.config.cache.getCached(
|
|
4514
|
+
this.sessionKeyId,
|
|
4515
|
+
async () => {
|
|
4516
|
+
const pin = this.config.pinProvider ? await this.config.pinProvider() : await this.promptForPIN("Enter PIN to sign payment");
|
|
4517
|
+
return await this.decryptKeypair(pin);
|
|
4518
|
+
},
|
|
4519
|
+
{ deviceFingerprint: this.deviceFingerprint || void 0 }
|
|
4520
|
+
);
|
|
4521
|
+
} else {
|
|
4522
|
+
const pin = this.config.pinProvider ? await this.config.pinProvider() : await this.promptForPIN("Enter PIN to sign payment");
|
|
4523
|
+
keypair = await this.decryptKeypair(pin);
|
|
4524
|
+
}
|
|
4525
|
+
const paymentResponse = await this.client.smart.execute({
|
|
4526
|
+
user_wallet: this.sessionWallet,
|
|
4527
|
+
// Session wallet, not user wallet
|
|
4528
|
+
amount_usd: amount,
|
|
4529
|
+
description,
|
|
4530
|
+
agent_id: "session-lifecycle",
|
|
4531
|
+
auto_detect_gasless: true
|
|
4532
|
+
});
|
|
4533
|
+
if (paymentResponse.requires_signature && paymentResponse.unsigned_transaction) {
|
|
4534
|
+
const { Transaction: Transaction2 } = await this.getSolanaWeb3();
|
|
4535
|
+
const txBuffer = Uint8Array.from(atob(paymentResponse.unsigned_transaction), (c) => c.charCodeAt(0));
|
|
4536
|
+
const tx = Transaction2.from(txBuffer);
|
|
4537
|
+
tx.partialSign(keypair);
|
|
4538
|
+
const submitUrl = paymentResponse.submit_url || `/api/v1/ai/payments/${paymentResponse.payment_id}/submit-signed`;
|
|
4539
|
+
const submitResponse = await fetch(`${this.client["config"].baseURL}${submitUrl}`, {
|
|
4540
|
+
method: "POST",
|
|
4541
|
+
headers: {
|
|
4542
|
+
"Authorization": `Bearer ${this.client["config"].apiKey}`,
|
|
4543
|
+
"Content-Type": "application/json"
|
|
4544
|
+
},
|
|
4545
|
+
body: JSON.stringify({
|
|
4546
|
+
signed_transaction: btoa(String.fromCharCode(...tx.serialize()))
|
|
4547
|
+
})
|
|
4548
|
+
});
|
|
4549
|
+
if (!submitResponse.ok) {
|
|
4550
|
+
throw new Error(`Failed to submit signed transaction: ${submitResponse.statusText}`);
|
|
4551
|
+
}
|
|
4552
|
+
const submitData = await submitResponse.json();
|
|
4553
|
+
return {
|
|
4554
|
+
paymentId: paymentResponse.payment_id,
|
|
4555
|
+
status: submitData.status,
|
|
4556
|
+
signature: submitData.transaction_signature,
|
|
4557
|
+
confirmedInMs: submitData.confirmed_in_ms
|
|
4558
|
+
};
|
|
4559
|
+
}
|
|
4560
|
+
return {
|
|
4561
|
+
paymentId: paymentResponse.payment_id,
|
|
4562
|
+
status: paymentResponse.status,
|
|
4563
|
+
signature: paymentResponse.transaction_signature,
|
|
4564
|
+
confirmedInMs: paymentResponse.confirmed_in_ms
|
|
4565
|
+
};
|
|
4566
|
+
}
|
|
4567
|
+
/**
|
|
4568
|
+
* Check session key status
|
|
4569
|
+
*/
|
|
4570
|
+
async getStatus() {
|
|
4571
|
+
if (!this.sessionKeyId) {
|
|
4572
|
+
throw new Error("No active session key");
|
|
4573
|
+
}
|
|
4574
|
+
return await this.client.sessionKeys.getStatus(this.sessionKeyId);
|
|
4575
|
+
}
|
|
4576
|
+
/**
|
|
4577
|
+
* Top up session key
|
|
4578
|
+
*/
|
|
4579
|
+
async topUp(amount, userWallet, onApprovalNeeded) {
|
|
4580
|
+
if (!this.sessionKeyId || !this.deviceFingerprint) {
|
|
4581
|
+
throw new Error("No active session key");
|
|
4582
|
+
}
|
|
4583
|
+
const response = await this.client.sessionKeys.topUp(
|
|
4584
|
+
this.sessionKeyId,
|
|
4585
|
+
{
|
|
4586
|
+
user_wallet: userWallet,
|
|
4587
|
+
amount_usdc: amount,
|
|
4588
|
+
device_fingerprint: this.deviceFingerprint
|
|
4589
|
+
}
|
|
4590
|
+
);
|
|
4591
|
+
if (onApprovalNeeded) {
|
|
4592
|
+
await onApprovalNeeded(response.top_up_transaction);
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
/**
|
|
4596
|
+
* Revoke session key
|
|
4597
|
+
*/
|
|
4598
|
+
async revoke() {
|
|
4599
|
+
if (!this.sessionKeyId) {
|
|
4600
|
+
throw new Error("No active session key");
|
|
4601
|
+
}
|
|
4602
|
+
await this.client.sessionKeys.revoke(this.sessionKeyId);
|
|
4603
|
+
await this.cleanup();
|
|
4604
|
+
}
|
|
4605
|
+
/**
|
|
4606
|
+
* Cleanup (clear cache, reset state)
|
|
4607
|
+
*/
|
|
4608
|
+
async cleanup() {
|
|
4609
|
+
if (this.config.cache && this.sessionKeyId) {
|
|
4610
|
+
await this.config.cache.invalidate(this.sessionKeyId);
|
|
4611
|
+
}
|
|
4612
|
+
this.sessionKeyId = null;
|
|
4613
|
+
this.sessionWallet = null;
|
|
4614
|
+
this.encryptedKey = null;
|
|
4615
|
+
this.deviceFingerprint = null;
|
|
4616
|
+
}
|
|
4617
|
+
/**
|
|
4618
|
+
* Get current session key ID
|
|
4619
|
+
*/
|
|
4620
|
+
getSessionKeyId() {
|
|
4621
|
+
return this.sessionKeyId;
|
|
4622
|
+
}
|
|
4623
|
+
/**
|
|
4624
|
+
* Check if session is active
|
|
4625
|
+
*/
|
|
4626
|
+
isActive() {
|
|
4627
|
+
return this.sessionKeyId !== null;
|
|
4628
|
+
}
|
|
4629
|
+
// ============================================
|
|
4630
|
+
// Private Helpers
|
|
4631
|
+
// ============================================
|
|
4632
|
+
async decryptKeypair(pin) {
|
|
4633
|
+
if (!this.encryptedKey || !this.deviceFingerprint) {
|
|
4634
|
+
throw new Error("No encrypted key available");
|
|
4635
|
+
}
|
|
4636
|
+
const { SessionKeyCrypto: SessionKeyCrypto2 } = await import("./device-bound-crypto-VX7SFVHT.mjs");
|
|
4637
|
+
const encrypted = {
|
|
4638
|
+
encryptedData: this.encryptedKey.ciphertext,
|
|
4639
|
+
nonce: this.encryptedKey.nonce,
|
|
4640
|
+
publicKey: "",
|
|
4641
|
+
// Not needed for decryption
|
|
4642
|
+
deviceFingerprint: this.deviceFingerprint,
|
|
4643
|
+
version: "argon2id-aes256gcm-v1"
|
|
4644
|
+
};
|
|
4645
|
+
return await SessionKeyCrypto2.decrypt(encrypted, pin, this.deviceFingerprint);
|
|
4646
|
+
}
|
|
4647
|
+
async generateDeviceFingerprint() {
|
|
4648
|
+
const { DeviceFingerprintGenerator: DeviceFingerprintGenerator2 } = await import("./device-bound-crypto-VX7SFVHT.mjs");
|
|
4649
|
+
const fingerprint = await DeviceFingerprintGenerator2.generate();
|
|
4650
|
+
return fingerprint.fingerprint;
|
|
4651
|
+
}
|
|
4652
|
+
async promptForPIN(message) {
|
|
4653
|
+
if (typeof window !== "undefined" && window.prompt) {
|
|
4654
|
+
const pin = window.prompt(message);
|
|
4655
|
+
if (!pin) {
|
|
4656
|
+
throw new Error("PIN required");
|
|
4657
|
+
}
|
|
4658
|
+
return pin;
|
|
4659
|
+
}
|
|
4660
|
+
throw new Error("PIN provider not configured and no browser prompt available");
|
|
4661
|
+
}
|
|
4662
|
+
async getSolanaWeb3() {
|
|
4663
|
+
try {
|
|
4664
|
+
return await import("@solana/web3.js");
|
|
4665
|
+
} catch {
|
|
4666
|
+
throw new Error("@solana/web3.js not installed. Install it to use device-bound payments.");
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
};
|
|
4670
|
+
async function setupQuickSessionKey(client, config) {
|
|
4671
|
+
const { SessionKeyCache: SessionKeyCache2 } = await import("./cache-T5YPC7OK.mjs");
|
|
4672
|
+
const lifecycle = new SessionKeyLifecycle(client, {
|
|
4673
|
+
cache: new SessionKeyCache2({ storage: "localStorage", ttl: 36e5 }),
|
|
4674
|
+
autoCleanup: true
|
|
4675
|
+
});
|
|
4676
|
+
await lifecycle.createAndFund({
|
|
4677
|
+
userWallet: config.userWallet,
|
|
4678
|
+
agentId: config.agentId,
|
|
4679
|
+
limitUsdc: config.budgetUsdc,
|
|
4680
|
+
onApprovalNeeded: config.onApproval
|
|
4681
|
+
});
|
|
4682
|
+
return lifecycle;
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
// src/helpers/dev.ts
|
|
4686
|
+
var DevTools = class {
|
|
4687
|
+
static debugEnabled = false;
|
|
4688
|
+
static requestLog = [];
|
|
4689
|
+
/**
|
|
4690
|
+
* Enable debug mode (logs all API requests/responses)
|
|
4691
|
+
*/
|
|
4692
|
+
static enableDebugMode() {
|
|
4693
|
+
if (this.isDevelopment()) {
|
|
4694
|
+
this.debugEnabled = true;
|
|
4695
|
+
console.log("\u{1F527} ZendFi Debug Mode: ENABLED");
|
|
4696
|
+
console.log("All API requests will be logged to console");
|
|
4697
|
+
} else {
|
|
4698
|
+
console.warn("Debug mode can only be enabled in development environment");
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
/**
|
|
4702
|
+
* Disable debug mode
|
|
4703
|
+
*/
|
|
4704
|
+
static disableDebugMode() {
|
|
4705
|
+
this.debugEnabled = false;
|
|
4706
|
+
console.log("\u{1F527} ZendFi Debug Mode: DISABLED");
|
|
4707
|
+
}
|
|
4708
|
+
/**
|
|
4709
|
+
* Check if debug mode is enabled
|
|
4710
|
+
*/
|
|
4711
|
+
static isDebugEnabled() {
|
|
4712
|
+
return this.debugEnabled;
|
|
4713
|
+
}
|
|
4714
|
+
/**
|
|
4715
|
+
* Log API request
|
|
4716
|
+
*/
|
|
4717
|
+
static logRequest(method, url, body) {
|
|
4718
|
+
if (!this.debugEnabled) return;
|
|
4719
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
4720
|
+
console.group(`\u{1F4E4} API Request: ${method} ${url}`);
|
|
4721
|
+
console.log("Time:", timestamp.toISOString());
|
|
4722
|
+
if (body) {
|
|
4723
|
+
console.log("Body:", body);
|
|
4724
|
+
}
|
|
4725
|
+
console.groupEnd();
|
|
4726
|
+
this.requestLog.push({ timestamp, method, url });
|
|
4727
|
+
}
|
|
4728
|
+
/**
|
|
4729
|
+
* Log API response
|
|
4730
|
+
*/
|
|
4731
|
+
static logResponse(method, url, status, data, duration) {
|
|
4732
|
+
if (!this.debugEnabled) return;
|
|
4733
|
+
const emoji = status >= 200 && status < 300 ? "\u2705" : "\u274C";
|
|
4734
|
+
console.group(`${emoji} API Response: ${method} ${url} [${status}]`);
|
|
4735
|
+
if (duration) {
|
|
4736
|
+
console.log("Duration:", `${duration}ms`);
|
|
4737
|
+
}
|
|
4738
|
+
console.log("Data:", data);
|
|
4739
|
+
console.groupEnd();
|
|
4740
|
+
const lastRequest = this.requestLog[this.requestLog.length - 1];
|
|
4741
|
+
if (lastRequest && lastRequest.method === method && lastRequest.url === url) {
|
|
4742
|
+
lastRequest.status = status;
|
|
4743
|
+
lastRequest.duration = duration;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
/**
|
|
4747
|
+
* Get request log
|
|
4748
|
+
*/
|
|
4749
|
+
static getRequestLog() {
|
|
4750
|
+
return [...this.requestLog];
|
|
4751
|
+
}
|
|
4752
|
+
/**
|
|
4753
|
+
* Clear request log
|
|
4754
|
+
*/
|
|
4755
|
+
static clearRequestLog() {
|
|
4756
|
+
this.requestLog = [];
|
|
4757
|
+
console.log("\u{1F5D1}\uFE0F Request log cleared");
|
|
4758
|
+
}
|
|
4759
|
+
/**
|
|
4760
|
+
* Create a test session key (devnet only)
|
|
4761
|
+
*/
|
|
4762
|
+
static async createTestSessionKey() {
|
|
4763
|
+
if (!this.isDevelopment()) {
|
|
4764
|
+
throw new Error("Test session keys can only be created in development");
|
|
4765
|
+
}
|
|
4766
|
+
const { Keypair } = await this.getSolanaWeb3();
|
|
4767
|
+
const keypair = Keypair.generate();
|
|
4768
|
+
return {
|
|
4769
|
+
sessionKeyId: this.generateTestId("sk_test"),
|
|
4770
|
+
sessionWallet: keypair.publicKey.toString(),
|
|
4771
|
+
privateKey: keypair.secretKey,
|
|
4772
|
+
budget: 10
|
|
4773
|
+
// $10 test budget
|
|
4774
|
+
};
|
|
4775
|
+
}
|
|
4776
|
+
/**
|
|
4777
|
+
* Create a mock wallet for testing
|
|
4778
|
+
*/
|
|
4779
|
+
static mockWallet(address) {
|
|
4780
|
+
const mockAddress = address || this.generateTestAddress();
|
|
4781
|
+
return {
|
|
4782
|
+
address: mockAddress,
|
|
4783
|
+
publicKey: { toString: () => mockAddress },
|
|
4784
|
+
signTransaction: async (tx) => {
|
|
4785
|
+
console.log("\u{1F527} Mock wallet: Signing transaction");
|
|
4786
|
+
return tx;
|
|
4787
|
+
},
|
|
4788
|
+
signMessage: async (_msg) => {
|
|
4789
|
+
console.log("\u{1F527} Mock wallet: Signing message");
|
|
4790
|
+
return {
|
|
4791
|
+
signature: new Uint8Array(64)
|
|
4792
|
+
// Mock signature
|
|
4793
|
+
};
|
|
4794
|
+
},
|
|
4795
|
+
isConnected: () => true,
|
|
4796
|
+
disconnect: async () => {
|
|
4797
|
+
console.log("\u{1F527} Mock wallet: Disconnected");
|
|
4798
|
+
}
|
|
4799
|
+
};
|
|
4800
|
+
}
|
|
4801
|
+
/**
|
|
4802
|
+
* Log transaction flow (visual diagram in console)
|
|
4803
|
+
*/
|
|
4804
|
+
static logTransactionFlow(paymentId) {
|
|
4805
|
+
console.log(`
|
|
4806
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
4807
|
+
\u2551 TRANSACTION FLOW \u2551
|
|
4808
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
4809
|
+
\u2551 \u2551
|
|
4810
|
+
\u2551 Payment ID: ${paymentId} \u2551
|
|
4811
|
+
\u2551 \u2551
|
|
4812
|
+
\u2551 1. \u{1F3D7}\uFE0F Create Payment Intent \u2551
|
|
4813
|
+
\u2551 \u2514\u2500> POST /api/v1/ai/smart-payment \u2551
|
|
4814
|
+
\u2551 \u2551
|
|
4815
|
+
\u2551 2. \u{1F510} Sign Transaction (Device-Bound) \u2551
|
|
4816
|
+
\u2551 \u2514\u2500> Client-side signing with cached keypair \u2551
|
|
4817
|
+
\u2551 \u2551
|
|
4818
|
+
\u2551 3. \u{1F4E4} Submit Signed Transaction \u2551
|
|
4819
|
+
\u2551 \u2514\u2500> POST /api/v1/ai/payments/{id}/submit-signed \u2551
|
|
4820
|
+
\u2551 \u2551
|
|
4821
|
+
\u2551 4. \u23F3 Wait for Blockchain Confirmation \u2551
|
|
4822
|
+
\u2551 \u2514\u2500> Poll Solana RPC (~30-60 seconds) \u2551
|
|
4823
|
+
\u2551 \u2551
|
|
4824
|
+
\u2551 5. \u2705 Payment Confirmed \u2551
|
|
4825
|
+
\u2551 \u2514\u2500> Webhook fired: payment.confirmed \u2551
|
|
4826
|
+
\u2551 \u2551
|
|
4827
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
4828
|
+
`);
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* Log session key lifecycle
|
|
4832
|
+
*/
|
|
4833
|
+
static logSessionKeyLifecycle(sessionKeyId) {
|
|
4834
|
+
console.log(`
|
|
4835
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
4836
|
+
\u2551 SESSION KEY LIFECYCLE \u2551
|
|
4837
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
4838
|
+
\u2551 \u2551
|
|
4839
|
+
\u2551 Session Key ID: ${sessionKeyId} \u2551
|
|
4840
|
+
\u2551 \u2551
|
|
4841
|
+
\u2551 Phase 1: CREATION \u2551
|
|
4842
|
+
\u2551 \u251C\u2500 Generate keypair (client-side) \u2551
|
|
4843
|
+
\u2551 \u251C\u2500 Encrypt with PIN + device fingerprint \u2551
|
|
4844
|
+
\u2551 \u251C\u2500 Send encrypted blob to backend \u2551
|
|
4845
|
+
\u2551 \u2514\u2500 Session key record created \u2551
|
|
4846
|
+
\u2551 \u2551
|
|
4847
|
+
\u2551 Phase 2: FUNDING \u2551
|
|
4848
|
+
\u2551 \u251C\u2500 Create top-up transaction \u2551
|
|
4849
|
+
\u2551 \u251C\u2500 User signs with main wallet \u2551
|
|
4850
|
+
\u2551 \u251C\u2500 Submit to Solana \u2551
|
|
4851
|
+
\u2551 \u2514\u2500 Session wallet funded \u2551
|
|
4852
|
+
\u2551 \u2551
|
|
4853
|
+
\u2551 Phase 3: USAGE \u2551
|
|
4854
|
+
\u2551 \u251C\u2500 Decrypt keypair with PIN \u2551
|
|
4855
|
+
\u2551 \u251C\u2500 Cache for 30min/1hr/24hr \u2551
|
|
4856
|
+
\u2551 \u251C\u2500 Sign payments automatically \u2551
|
|
4857
|
+
\u2551 \u2514\u2500 Track spending against limit \u2551
|
|
4858
|
+
\u2551 \u2551
|
|
4859
|
+
\u2551 Phase 4: REVOCATION \u2551
|
|
4860
|
+
\u2551 \u251C\u2500 Mark as inactive in DB \u2551
|
|
4861
|
+
\u2551 \u251C\u2500 Clear local cache \u2551
|
|
4862
|
+
\u2551 \u2514\u2500 Remaining funds locked \u2551
|
|
4863
|
+
\u2551 \u2551
|
|
4864
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
4865
|
+
`);
|
|
4866
|
+
}
|
|
4867
|
+
/**
|
|
4868
|
+
* Benchmark API request
|
|
4869
|
+
*/
|
|
4870
|
+
static async benchmarkRequest(name, fn) {
|
|
4871
|
+
const start = performance.now();
|
|
4872
|
+
const result = await fn();
|
|
4873
|
+
const duration = performance.now() - start;
|
|
4874
|
+
console.log(`\u23F1\uFE0F Benchmark [${name}]: ${duration.toFixed(2)}ms`);
|
|
4875
|
+
return {
|
|
4876
|
+
result,
|
|
4877
|
+
durationMs: duration
|
|
4878
|
+
};
|
|
4879
|
+
}
|
|
4880
|
+
/**
|
|
4881
|
+
* Stress test (send multiple concurrent requests)
|
|
4882
|
+
*/
|
|
4883
|
+
static async stressTest(name, fn, concurrency = 10, iterations = 100) {
|
|
4884
|
+
console.log(`\u{1F525} Stress Test: ${name}`);
|
|
4885
|
+
console.log(`Concurrency: ${concurrency}, Iterations: ${iterations}`);
|
|
4886
|
+
const durations = [];
|
|
4887
|
+
let successful = 0;
|
|
4888
|
+
let failed = 0;
|
|
4889
|
+
for (let i = 0; i < iterations; i += concurrency) {
|
|
4890
|
+
const batch = Array(Math.min(concurrency, iterations - i)).fill(null).map(() => this.benchmarkRequest(`${name}-${i}`, fn));
|
|
4891
|
+
const results = await Promise.allSettled(batch);
|
|
4892
|
+
results.forEach((result) => {
|
|
4893
|
+
if (result.status === "fulfilled") {
|
|
4894
|
+
successful++;
|
|
4895
|
+
durations.push(result.value.durationMs);
|
|
4896
|
+
} else {
|
|
4897
|
+
failed++;
|
|
4898
|
+
}
|
|
4899
|
+
});
|
|
4900
|
+
}
|
|
4901
|
+
const stats = {
|
|
4902
|
+
totalRequests: iterations,
|
|
4903
|
+
successful,
|
|
4904
|
+
failed,
|
|
4905
|
+
avgDurationMs: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
4906
|
+
minDurationMs: Math.min(...durations),
|
|
4907
|
+
maxDurationMs: Math.max(...durations)
|
|
4908
|
+
};
|
|
4909
|
+
console.table(stats);
|
|
4910
|
+
return stats;
|
|
4911
|
+
}
|
|
4912
|
+
/**
|
|
4913
|
+
* Inspect ZendFi SDK configuration
|
|
4914
|
+
*/
|
|
4915
|
+
static inspectConfig(client) {
|
|
4916
|
+
console.group("\u{1F50D} ZendFi SDK Configuration");
|
|
4917
|
+
console.log("Base URL:", client.config?.baseURL || "Unknown");
|
|
4918
|
+
console.log("API Key:", client.config?.apiKey ? `${client.config.apiKey.slice(0, 10)}...` : "Not set");
|
|
4919
|
+
console.log("Mode:", client.config?.mode || "Unknown");
|
|
4920
|
+
console.log("Environment:", client.config?.environment || "Unknown");
|
|
4921
|
+
console.log("Timeout:", client.config?.timeout || "Default");
|
|
4922
|
+
console.groupEnd();
|
|
4923
|
+
}
|
|
4924
|
+
/**
|
|
4925
|
+
* Generate test data
|
|
4926
|
+
*/
|
|
4927
|
+
static generateTestData() {
|
|
4928
|
+
return {
|
|
4929
|
+
userWallet: this.generateTestAddress(),
|
|
4930
|
+
agentId: `test-agent-${Date.now()}`,
|
|
4931
|
+
sessionKeyId: this.generateTestId("sk_test"),
|
|
4932
|
+
paymentId: this.generateTestId("pay_test")
|
|
4933
|
+
};
|
|
4934
|
+
}
|
|
4935
|
+
/**
|
|
4936
|
+
* Check if running in development environment
|
|
4937
|
+
*/
|
|
4938
|
+
static isDevelopment() {
|
|
4939
|
+
if (typeof process !== "undefined" && process.env) {
|
|
4940
|
+
return process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
4941
|
+
}
|
|
4942
|
+
if (typeof window !== "undefined" && window.location) {
|
|
4943
|
+
return window.location.hostname === "localhost" || window.location.hostname.includes("dev") || window.location.hostname.includes("staging");
|
|
4944
|
+
}
|
|
4945
|
+
return false;
|
|
4946
|
+
}
|
|
4947
|
+
/**
|
|
4948
|
+
* Generate test Solana address
|
|
4949
|
+
*/
|
|
4950
|
+
static generateTestAddress() {
|
|
4951
|
+
const chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
4952
|
+
let address = "";
|
|
4953
|
+
for (let i = 0; i < 44; i++) {
|
|
4954
|
+
address += chars[Math.floor(Math.random() * chars.length)];
|
|
4955
|
+
}
|
|
4956
|
+
return address;
|
|
4957
|
+
}
|
|
4958
|
+
/**
|
|
4959
|
+
* Generate test ID with prefix
|
|
4960
|
+
*/
|
|
4961
|
+
static generateTestId(prefix) {
|
|
4962
|
+
const id = Array(32).fill(null).map(() => Math.floor(Math.random() * 16).toString(16)).join("");
|
|
4963
|
+
return `${prefix}_${id}`;
|
|
4964
|
+
}
|
|
4965
|
+
/**
|
|
4966
|
+
* Get Solana Web3.js
|
|
4967
|
+
*/
|
|
4968
|
+
static async getSolanaWeb3() {
|
|
4969
|
+
try {
|
|
4970
|
+
return await import("@solana/web3.js");
|
|
4971
|
+
} catch {
|
|
4972
|
+
throw new Error("@solana/web3.js not installed");
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
};
|
|
4976
|
+
var PerformanceMonitor = class {
|
|
4977
|
+
metrics = /* @__PURE__ */ new Map();
|
|
4978
|
+
/**
|
|
4979
|
+
* Record a metric
|
|
4980
|
+
*/
|
|
4981
|
+
record(name, value) {
|
|
4982
|
+
const values = this.metrics.get(name) || [];
|
|
4983
|
+
values.push(value);
|
|
4984
|
+
this.metrics.set(name, values);
|
|
4985
|
+
}
|
|
4986
|
+
/**
|
|
4987
|
+
* Get statistics for a metric
|
|
4988
|
+
*/
|
|
4989
|
+
getStats(name) {
|
|
4990
|
+
const values = this.metrics.get(name);
|
|
4991
|
+
if (!values || values.length === 0) return null;
|
|
4992
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4993
|
+
const count = values.length;
|
|
4994
|
+
return {
|
|
4995
|
+
count,
|
|
4996
|
+
avg: values.reduce((a, b) => a + b, 0) / count,
|
|
4997
|
+
min: sorted[0],
|
|
4998
|
+
max: sorted[count - 1],
|
|
4999
|
+
p50: sorted[Math.floor(count * 0.5)],
|
|
5000
|
+
p95: sorted[Math.floor(count * 0.95)],
|
|
5001
|
+
p99: sorted[Math.floor(count * 0.99)]
|
|
5002
|
+
};
|
|
5003
|
+
}
|
|
5004
|
+
/**
|
|
5005
|
+
* Get all metrics
|
|
5006
|
+
*/
|
|
5007
|
+
getAllStats() {
|
|
5008
|
+
const stats = {};
|
|
5009
|
+
for (const [name] of this.metrics) {
|
|
5010
|
+
stats[name] = this.getStats(name);
|
|
5011
|
+
}
|
|
5012
|
+
return stats;
|
|
5013
|
+
}
|
|
5014
|
+
/**
|
|
5015
|
+
* Print report
|
|
5016
|
+
*/
|
|
5017
|
+
printReport() {
|
|
5018
|
+
console.table(this.getAllStats());
|
|
5019
|
+
}
|
|
5020
|
+
/**
|
|
5021
|
+
* Clear all metrics
|
|
5022
|
+
*/
|
|
5023
|
+
clear() {
|
|
5024
|
+
this.metrics.clear();
|
|
5025
|
+
}
|
|
5026
|
+
};
|
|
3451
5027
|
export {
|
|
3452
5028
|
AgentAPI,
|
|
5029
|
+
AnthropicAdapter,
|
|
3453
5030
|
ApiError,
|
|
3454
5031
|
AuthenticationError2 as AuthenticationError,
|
|
3455
5032
|
AutonomyAPI,
|
|
3456
5033
|
ConfigLoader,
|
|
5034
|
+
DevTools,
|
|
3457
5035
|
DeviceBoundSessionKey,
|
|
3458
5036
|
DeviceFingerprintGenerator,
|
|
3459
5037
|
ERROR_CODES,
|
|
5038
|
+
ErrorRecovery,
|
|
5039
|
+
GeminiAdapter,
|
|
3460
5040
|
InterceptorManager,
|
|
3461
5041
|
LitCryptoSigner,
|
|
3462
5042
|
NetworkError2 as NetworkError,
|
|
5043
|
+
OpenAIAdapter,
|
|
5044
|
+
PINRateLimiter,
|
|
5045
|
+
PINValidator,
|
|
3463
5046
|
PaymentError,
|
|
5047
|
+
PaymentIntentParser,
|
|
3464
5048
|
PaymentIntentsAPI,
|
|
5049
|
+
PerformanceMonitor,
|
|
3465
5050
|
PricingAPI,
|
|
5051
|
+
QuickCaches,
|
|
3466
5052
|
RateLimitError2 as RateLimitError,
|
|
3467
5053
|
RateLimiter,
|
|
3468
5054
|
RecoveryQRGenerator,
|
|
5055
|
+
RetryStrategy,
|
|
3469
5056
|
SPENDING_LIMIT_ACTION_CID,
|
|
5057
|
+
SecureStorage,
|
|
5058
|
+
SessionKeyCache,
|
|
3470
5059
|
SessionKeyCrypto,
|
|
5060
|
+
SessionKeyLifecycle,
|
|
3471
5061
|
SessionKeysAPI,
|
|
3472
5062
|
SmartPaymentsAPI,
|
|
5063
|
+
TransactionMonitor,
|
|
5064
|
+
TransactionPoller,
|
|
3473
5065
|
ValidationError2 as ValidationError,
|
|
5066
|
+
WalletConnector,
|
|
3474
5067
|
WebhookError,
|
|
3475
5068
|
ZendFiClient,
|
|
3476
5069
|
ZendFiError2 as ZendFiError,
|
|
@@ -3485,6 +5078,7 @@ export {
|
|
|
3485
5078
|
asPaymentLinkCode,
|
|
3486
5079
|
asSessionId,
|
|
3487
5080
|
asSubscriptionId,
|
|
5081
|
+
createWalletHook,
|
|
3488
5082
|
createZendFiError,
|
|
3489
5083
|
decodeSignatureFromLit,
|
|
3490
5084
|
encodeTransactionForLit,
|
|
@@ -3492,6 +5086,7 @@ export {
|
|
|
3492
5086
|
isZendFiError,
|
|
3493
5087
|
processWebhook,
|
|
3494
5088
|
requiresLitSigning,
|
|
5089
|
+
setupQuickSessionKey,
|
|
3495
5090
|
sleep,
|
|
3496
5091
|
verifyExpressWebhook,
|
|
3497
5092
|
verifyNextWebhook,
|