@chipi-stack/backend 14.13.1 → 14.15.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/dist/index.js CHANGED
@@ -732,7 +732,8 @@ var ChipiClient = class {
732
732
  throw new shared.ChipiApiError(
733
733
  errorData.message,
734
734
  errorData.code || `HTTP_${response.status}`,
735
- response.status
735
+ response.status,
736
+ data
736
737
  );
737
738
  }
738
739
  return data;
@@ -743,14 +744,15 @@ var ChipiClient = class {
743
744
  async post({
744
745
  endpoint,
745
746
  bearerToken,
746
- body
747
+ body,
748
+ headers
747
749
  }) {
748
750
  try {
749
751
  const url = new URL(`${this.baseUrl}${endpoint}`);
750
752
  this.addVersionParams(url);
751
753
  const response = await fetch(url.toString(), {
752
754
  method: "POST",
753
- headers: this.getHeaders(bearerToken),
755
+ headers: { ...headers, ...this.getHeaders(bearerToken) },
754
756
  body: body ? JSON.stringify(body) : void 0
755
757
  });
756
758
  const data = await response.json();
@@ -759,7 +761,8 @@ var ChipiClient = class {
759
761
  throw new shared.ChipiApiError(
760
762
  errorData.message,
761
763
  errorData.code || `HTTP_${response.status}`,
762
- response.status
764
+ response.status,
765
+ data
763
766
  );
764
767
  }
765
768
  return data;
@@ -786,7 +789,8 @@ var ChipiClient = class {
786
789
  throw new shared.ChipiApiError(
787
790
  errorData.message,
788
791
  errorData.code || `HTTP_${response.status}`,
789
- response.status
792
+ response.status,
793
+ data
790
794
  );
791
795
  }
792
796
  return data;
@@ -813,7 +817,8 @@ var ChipiClient = class {
813
817
  throw new shared.ChipiApiError(
814
818
  errorData.message,
815
819
  errorData.code || `HTTP_${response.status}`,
816
- response.status
820
+ response.status,
821
+ data
817
822
  );
818
823
  }
819
824
  return data;
@@ -838,7 +843,8 @@ var ChipiClient = class {
838
843
  throw new shared.ChipiApiError(
839
844
  errorData.message,
840
845
  errorData.code || `HTTP_${response.status}`,
841
- response.status
846
+ response.status,
847
+ data
842
848
  );
843
849
  }
844
850
  return data;
@@ -1952,14 +1958,14 @@ var ChipiTransactions = class {
1952
1958
  /**
1953
1959
  * Get on-chain transaction status
1954
1960
  */
1955
- async getTransactionStatus(hash10, bearerToken) {
1961
+ async getTransactionStatus(hash11, bearerToken) {
1956
1962
  return this.client.get({
1957
- endpoint: `${shared.API_ENDPOINTS.TRANSACTIONS}/${hash10}/status`,
1963
+ endpoint: `${shared.API_ENDPOINTS.TRANSACTIONS}/${hash11}/status`,
1958
1964
  bearerToken
1959
1965
  });
1960
1966
  }
1961
1967
  /**
1962
- * Sync on-chain transfers from Voyager into the DB (read-through cache).
1968
+ * Sync on-chain transfers (Starknet Transfer events) into the DB (read-through cache).
1963
1969
  * Discovers transactions not made through the SDK (e.g., external USDC receives).
1964
1970
  * After sync, getTransactionList returns the newly discovered transactions.
1965
1971
  */
@@ -2158,6 +2164,108 @@ function decodeSessionData(result, nowSeconds) {
2158
2164
  const isActive = validUntil !== 0 && nowSeconds <= validUntil && remainingCalls > 0;
2159
2165
  return { isActive, validUntil, remainingCalls, allowedEntrypoints: [] };
2160
2166
  }
2167
+ var U128_MASK = (1n << 128n) - 1n;
2168
+ var U256_MAX = (1n << 256n) - 1n;
2169
+ var U64_MAX = (1n << 64n) - 1n;
2170
+ function splitU256(value) {
2171
+ return [starknet.num.toHex(value & U128_MASK), starknet.num.toHex(value >> 128n)];
2172
+ }
2173
+ function buildAddSessionKeyCall(walletAddress, sessionConfig) {
2174
+ const allowedEntrypoints = normalizeEntrypoints(sessionConfig.allowedEntrypoints);
2175
+ return {
2176
+ contractAddress: walletAddress,
2177
+ entrypoint: shared.SESSION_ENTRYPOINTS.ADD_OR_UPDATE,
2178
+ calldata: [
2179
+ sessionConfig.sessionPublicKey,
2180
+ starknet.num.toHex(sessionConfig.validUntil),
2181
+ starknet.num.toHex(sessionConfig.maxCalls),
2182
+ starknet.num.toHex(allowedEntrypoints.length),
2183
+ ...allowedEntrypoints
2184
+ ]
2185
+ };
2186
+ }
2187
+ function buildRevokeSessionKeyCall(walletAddress, sessionPublicKey) {
2188
+ return {
2189
+ contractAddress: walletAddress,
2190
+ entrypoint: shared.SESSION_ENTRYPOINTS.REVOKE,
2191
+ calldata: [sessionPublicKey]
2192
+ };
2193
+ }
2194
+ function validateSpendingPolicyConfig(config) {
2195
+ const { token, maxPerCall, maxPerWindow, windowSeconds } = config;
2196
+ if (!token || token.trim().length === 0) {
2197
+ throw new shared.ChipiSessionError(
2198
+ `Invalid token address for spending policy: "${token}"`,
2199
+ shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2200
+ );
2201
+ }
2202
+ if (typeof windowSeconds !== "number" || !Number.isFinite(windowSeconds) || !Number.isInteger(windowSeconds) || windowSeconds <= 0 || BigInt(windowSeconds) > U64_MAX) {
2203
+ throw new shared.ChipiSessionError(
2204
+ `Invalid window duration: ${windowSeconds}. Must be a positive integer within u64 range.`,
2205
+ shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2206
+ );
2207
+ }
2208
+ if (maxPerCall < 0n || maxPerCall > U256_MAX) {
2209
+ throw new shared.ChipiSessionError(
2210
+ `Invalid maxPerCall: must be >= 0 and <= 2^256 - 1`,
2211
+ shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2212
+ );
2213
+ }
2214
+ if (maxPerWindow < 0n || maxPerWindow > U256_MAX) {
2215
+ throw new shared.ChipiSessionError(
2216
+ `Invalid maxPerWindow: must be >= 0 and <= 2^256 - 1`,
2217
+ shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2218
+ );
2219
+ }
2220
+ if (maxPerWindow > 0n && maxPerCall > maxPerWindow) {
2221
+ throw new shared.ChipiSessionError(
2222
+ `Invalid spending policy: maxPerCall (${maxPerCall}) cannot exceed maxPerWindow (${maxPerWindow})`,
2223
+ shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2224
+ );
2225
+ }
2226
+ }
2227
+ function buildSetSpendingPolicyCall(walletAddress, config) {
2228
+ validateSpendingPolicyConfig(config);
2229
+ const [maxPerCallLow, maxPerCallHigh] = splitU256(config.maxPerCall);
2230
+ const [maxPerWindowLow, maxPerWindowHigh] = splitU256(config.maxPerWindow);
2231
+ return {
2232
+ contractAddress: walletAddress,
2233
+ entrypoint: shared.SESSION_ENTRYPOINTS.SET_SPENDING_POLICY,
2234
+ calldata: [
2235
+ config.sessionPublicKey,
2236
+ config.token,
2237
+ maxPerCallLow,
2238
+ maxPerCallHigh,
2239
+ maxPerWindowLow,
2240
+ maxPerWindowHigh,
2241
+ starknet.num.toHex(config.windowSeconds)
2242
+ ]
2243
+ };
2244
+ }
2245
+ function buildRemoveSpendingPolicyCall(walletAddress, sessionPublicKey, token) {
2246
+ if (!token || token.trim().length === 0) {
2247
+ throw new shared.ChipiSessionError(
2248
+ `Invalid token address for spending policy removal: "${token}"`,
2249
+ shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2250
+ );
2251
+ }
2252
+ return {
2253
+ contractAddress: walletAddress,
2254
+ entrypoint: shared.SESSION_ENTRYPOINTS.REMOVE_SPENDING_POLICY,
2255
+ calldata: [sessionPublicKey, token]
2256
+ };
2257
+ }
2258
+ function buildSessionSetupCalls(walletAddress, sessionConfig, spendingPolicies = []) {
2259
+ return [
2260
+ buildAddSessionKeyCall(walletAddress, sessionConfig),
2261
+ ...spendingPolicies.map(
2262
+ (policy) => buildSetSpendingPolicyCall(walletAddress, {
2263
+ ...policy,
2264
+ sessionPublicKey: sessionConfig.sessionPublicKey
2265
+ })
2266
+ )
2267
+ ];
2268
+ }
2161
2269
 
2162
2270
  // src/sessions.ts
2163
2271
  var ChipiSessions = class {
@@ -2299,26 +2407,12 @@ var ChipiSessions = class {
2299
2407
  maxCalls: sessionConfig.maxCalls,
2300
2408
  allowedEntrypoints: sessionConfig.allowedEntrypoints.length
2301
2409
  });
2302
- const allowedEntrypoints = normalizeEntrypoints(sessionConfig.allowedEntrypoints);
2303
- const calldata = [
2304
- sessionConfig.sessionPublicKey,
2305
- starknet.num.toHex(sessionConfig.validUntil),
2306
- starknet.num.toHex(sessionConfig.maxCalls),
2307
- // Array: first element is length, then elements
2308
- starknet.num.toHex(allowedEntrypoints.length),
2309
- ...allowedEntrypoints
2310
- ];
2410
+ const addCall = buildAddSessionKeyCall(wallet.publicKey, sessionConfig);
2311
2411
  const txHash = await executePaymasterTransaction({
2312
2412
  params: {
2313
2413
  encryptKey,
2314
2414
  wallet,
2315
- calls: [
2316
- {
2317
- contractAddress: wallet.publicKey,
2318
- entrypoint: shared.SESSION_ENTRYPOINTS.ADD_OR_UPDATE,
2319
- calldata
2320
- }
2321
- ],
2415
+ calls: [addCall],
2322
2416
  saveToDatabase: false
2323
2417
  // Don't record session management txs
2324
2418
  },
@@ -2342,6 +2436,54 @@ var ChipiSessions = class {
2342
2436
  throw error;
2343
2437
  }
2344
2438
  }
2439
+ /**
2440
+ * Register a session and its spending policies in ONE owner-signed transaction.
2441
+ *
2442
+ * Equivalent to `addSessionKeyToContract` followed by one `setSpendingPolicy`
2443
+ * per token, but the owner signs once (one passkey prompt) and the session
2444
+ * never exists on-chain without its caps.
2445
+ *
2446
+ * @example
2447
+ * ```typescript
2448
+ * const txHash = await sdk.sessions.setupSession({
2449
+ * encryptKey,
2450
+ * wallet,
2451
+ * sessionConfig: {
2452
+ * sessionPublicKey: session.publicKey,
2453
+ * validUntil: session.validUntil,
2454
+ * maxCalls: 500,
2455
+ * allowedEntrypoints: ["transfer", "approve"],
2456
+ * },
2457
+ * spendingPolicies: [
2458
+ * { token: USDC, maxPerCall: 5_000_000n, maxPerWindow: 20_000_000n, windowSeconds: 86400 },
2459
+ * ],
2460
+ * }, bearerToken);
2461
+ * ```
2462
+ */
2463
+ async setupSession(params, bearerToken) {
2464
+ const { encryptKey, wallet, sessionConfig, spendingPolicies = [] } = params;
2465
+ this.validateWalletSupportsSessions(wallet.walletType, "Setup", wallet.publicKey);
2466
+ const calls = buildSessionSetupCalls(wallet.publicKey, sessionConfig, spendingPolicies);
2467
+ console.log("[ChipiSDK:Session:Setup] Registering session with spending policies", {
2468
+ walletAddress: wallet.publicKey.slice(0, 15) + "...",
2469
+ sessionPubKey: sessionConfig.sessionPublicKey.slice(0, 15) + "...",
2470
+ validUntil: new Date(sessionConfig.validUntil * 1e3).toISOString(),
2471
+ maxCalls: sessionConfig.maxCalls,
2472
+ allowedEntrypoints: sessionConfig.allowedEntrypoints.length,
2473
+ spendingPolicies: spendingPolicies.length
2474
+ });
2475
+ return executePaymasterTransaction({
2476
+ params: {
2477
+ encryptKey,
2478
+ wallet,
2479
+ calls,
2480
+ saveToDatabase: false
2481
+ // Don't record session management txs
2482
+ },
2483
+ bearerToken,
2484
+ client: this.client
2485
+ });
2486
+ }
2345
2487
  /**
2346
2488
  * Revoke a session key from the smart contract.
2347
2489
  *
@@ -2375,13 +2517,7 @@ var ChipiSessions = class {
2375
2517
  params: {
2376
2518
  encryptKey,
2377
2519
  wallet,
2378
- calls: [
2379
- {
2380
- contractAddress: wallet.publicKey,
2381
- entrypoint: shared.SESSION_ENTRYPOINTS.REVOKE,
2382
- calldata: [sessionPublicKey]
2383
- }
2384
- ],
2520
+ calls: [buildRevokeSessionKeyCall(wallet.publicKey, sessionPublicKey)],
2385
2521
  saveToDatabase: false
2386
2522
  // Don't record session management txs
2387
2523
  },
@@ -2571,13 +2707,6 @@ var ChipiSessions = class {
2571
2707
  // Wraps ISessionSpendingPolicy contract interface.
2572
2708
  // Spending policies enforce per-token limits on ERC-20 operations
2573
2709
  // (transfer, approve, increase_allowance) executed by session keys.
2574
- /**
2575
- * Split a bigint into Cairo u256 representation (low u128, high u128).
2576
- */
2577
- splitU256(value) {
2578
- const mask = (1n << 128n) - 1n;
2579
- return [starknet.num.toHex(value & mask), starknet.num.toHex(value >> 128n)];
2580
- }
2581
2710
  /**
2582
2711
  * Reconstruct a bigint from Cairo u256 (low, high) hex strings.
2583
2712
  */
@@ -2622,38 +2751,7 @@ var ChipiSessions = class {
2622
2751
  shared.SESSION_ERRORS.INVALID_WALLET_TYPE_FOR_SESSION
2623
2752
  );
2624
2753
  }
2625
- const U256_MAX = (1n << 256n) - 1n;
2626
- const U64_MAX = (1n << 64n) - 1n;
2627
- if (!token || token.trim().length === 0) {
2628
- throw new shared.ChipiSessionError(
2629
- `Invalid token address for spending policy: "${token}"`,
2630
- shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2631
- );
2632
- }
2633
- if (typeof windowSeconds !== "number" || !Number.isFinite(windowSeconds) || !Number.isInteger(windowSeconds) || windowSeconds <= 0 || BigInt(windowSeconds) > U64_MAX) {
2634
- throw new shared.ChipiSessionError(
2635
- `Invalid window duration: ${windowSeconds}. Must be a positive integer within u64 range.`,
2636
- shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2637
- );
2638
- }
2639
- if (maxPerCall < 0n || maxPerCall > U256_MAX) {
2640
- throw new shared.ChipiSessionError(
2641
- `Invalid maxPerCall: must be >= 0 and <= 2^256 - 1`,
2642
- shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2643
- );
2644
- }
2645
- if (maxPerWindow < 0n || maxPerWindow > U256_MAX) {
2646
- throw new shared.ChipiSessionError(
2647
- `Invalid maxPerWindow: must be >= 0 and <= 2^256 - 1`,
2648
- shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2649
- );
2650
- }
2651
- if (maxPerWindow > 0n && maxPerCall > maxPerWindow) {
2652
- throw new shared.ChipiSessionError(
2653
- `Invalid spending policy: maxPerCall (${maxPerCall}) cannot exceed maxPerWindow (${maxPerWindow})`,
2654
- shared.SESSION_ERRORS.INVALID_SPENDING_POLICY
2655
- );
2656
- }
2754
+ validateSpendingPolicyConfig(spendingPolicyConfig);
2657
2755
  try {
2658
2756
  console.log("[ChipiSDK:Session:SetSpendingPolicy] Setting spending policy", {
2659
2757
  walletAddress: wallet.publicKey.slice(0, 15) + "...",
@@ -2663,28 +2761,12 @@ var ChipiSessions = class {
2663
2761
  maxPerWindow: maxPerWindow.toString(),
2664
2762
  windowSeconds
2665
2763
  });
2666
- const [maxPerCallLow, maxPerCallHigh] = this.splitU256(maxPerCall);
2667
- const [maxPerWindowLow, maxPerWindowHigh] = this.splitU256(maxPerWindow);
2668
- const calldata = [
2669
- sessionPublicKey,
2670
- token,
2671
- maxPerCallLow,
2672
- maxPerCallHigh,
2673
- maxPerWindowLow,
2674
- maxPerWindowHigh,
2675
- starknet.num.toHex(windowSeconds)
2676
- ];
2764
+ const setPolicyCall = buildSetSpendingPolicyCall(wallet.publicKey, spendingPolicyConfig);
2677
2765
  const txHash = await executePaymasterTransaction({
2678
2766
  params: {
2679
2767
  encryptKey,
2680
2768
  wallet,
2681
- calls: [
2682
- {
2683
- contractAddress: wallet.publicKey,
2684
- entrypoint: shared.SESSION_ENTRYPOINTS.SET_SPENDING_POLICY,
2685
- calldata
2686
- }
2687
- ],
2769
+ calls: [setPolicyCall],
2688
2770
  saveToDatabase: false
2689
2771
  },
2690
2772
  bearerToken,
@@ -2851,18 +2933,12 @@ var ChipiSessions = class {
2851
2933
  sessionPubKey: sessionPublicKey.slice(0, 15) + "...",
2852
2934
  token: token.slice(0, 15) + "..."
2853
2935
  });
2854
- const calldata = [sessionPublicKey, token];
2936
+ const removeCall = buildRemoveSpendingPolicyCall(wallet.publicKey, sessionPublicKey, token);
2855
2937
  const txHash = await executePaymasterTransaction({
2856
2938
  params: {
2857
2939
  encryptKey,
2858
2940
  wallet,
2859
- calls: [
2860
- {
2861
- contractAddress: wallet.publicKey,
2862
- entrypoint: shared.SESSION_ENTRYPOINTS.REMOVE_SPENDING_POLICY,
2863
- calldata
2864
- }
2865
- ],
2941
+ calls: [removeCall],
2866
2942
  saveToDatabase: false
2867
2943
  },
2868
2944
  bearerToken,
@@ -3043,6 +3119,409 @@ var ChipiTreasury = class {
3043
3119
  };
3044
3120
  }
3045
3121
  };
3122
+ var DEFAULT_RETRY_INTERVAL_MS = 3e3;
3123
+ var DEFAULT_TIMEOUT_MS = 3e5;
3124
+ function isPending(finalityStatus) {
3125
+ return finalityStatus === void 0 || finalityStatus === "RECEIVED" || finalityStatus === "PENDING" || finalityStatus === "PRE_CONFIRMED";
3126
+ }
3127
+ async function waitForTransaction(transactionHash, options = {}) {
3128
+ const {
3129
+ nodeUrl = shared.WALLET_RPC_ENDPOINTS.CHIPI,
3130
+ retryIntervalMs = DEFAULT_RETRY_INTERVAL_MS,
3131
+ timeoutMs = DEFAULT_TIMEOUT_MS
3132
+ } = options;
3133
+ const deadline = Date.now() + timeoutMs;
3134
+ let lastError;
3135
+ for (; ; ) {
3136
+ try {
3137
+ const receipt = await shared.withRpcFallback(
3138
+ (provider) => provider.getTransactionReceipt(transactionHash),
3139
+ { fallbackUrl: nodeUrl }
3140
+ );
3141
+ if (!isPending(receipt?.finality_status)) {
3142
+ const reverted = receipt.execution_status === "REVERTED";
3143
+ return {
3144
+ transactionHash,
3145
+ success: !reverted,
3146
+ executionStatus: receipt.execution_status,
3147
+ finalityStatus: receipt.finality_status,
3148
+ ...reverted ? { revertReason: receipt.revert_reason ?? "reverted on chain" } : {},
3149
+ events: receipt.events ?? [],
3150
+ ...receipt.block_number !== void 0 ? { blockNumber: receipt.block_number } : {}
3151
+ };
3152
+ }
3153
+ } catch (err) {
3154
+ lastError = err;
3155
+ }
3156
+ if (Date.now() + retryIntervalMs >= deadline) {
3157
+ const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
3158
+ throw new Error(
3159
+ `Timed out after ${timeoutMs}ms waiting for ${transactionHash}${detail}. The transaction may still land; re-check before treating it as failed.`
3160
+ );
3161
+ }
3162
+ await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
3163
+ }
3164
+ }
3165
+ var InMemoryTxHashStore = class {
3166
+ constructor() {
3167
+ this.used = /* @__PURE__ */ new Set();
3168
+ }
3169
+ consume(transactionHash) {
3170
+ if (this.used.has(transactionHash)) return false;
3171
+ this.used.add(transactionHash);
3172
+ return true;
3173
+ }
3174
+ };
3175
+ var TRANSFER_SELECTOR = BigInt(starknet.hash.getSelectorFromName("Transfer"));
3176
+ function toBigInt2(value) {
3177
+ if (value === void 0) return void 0;
3178
+ try {
3179
+ return BigInt(value);
3180
+ } catch {
3181
+ return void 0;
3182
+ }
3183
+ }
3184
+ function decodeTransfer(event) {
3185
+ if (toBigInt2(event.keys[0]) !== TRANSFER_SELECTOR) return void 0;
3186
+ const [from, to, low, high] = event.keys.length >= 3 ? [event.keys[1], event.keys[2], event.data[0], event.data[1]] : [event.data[0], event.data[1], event.data[2], event.data[3]];
3187
+ const values = [from, to, low, high].map(toBigInt2);
3188
+ if (values.some((v) => v === void 0)) return void 0;
3189
+ const [f, t, l, h] = values;
3190
+ return { from: f, to: t, amount: l + (h << 128n) };
3191
+ }
3192
+ var SessionTxVerifier = class {
3193
+ constructor(config = {}) {
3194
+ const { txHashStore, getReceipt, ...receiptOptions } = config;
3195
+ this.store = txHashStore ?? new InMemoryTxHashStore();
3196
+ this.getReceipt = getReceipt ?? waitForTransaction;
3197
+ this.receiptOptions = receiptOptions;
3198
+ }
3199
+ async verifyTransfer(params) {
3200
+ const { transactionHash, token, from, to, minAmount } = params;
3201
+ const receipt = await this.getReceipt(transactionHash, this.receiptOptions);
3202
+ if (!receipt.success) {
3203
+ return { valid: false, transactionHash, reason: "REVERTED", detail: receipt.revertReason };
3204
+ }
3205
+ const tokenAddress = BigInt(token);
3206
+ const fromAddress = BigInt(from);
3207
+ const toAddress = BigInt(to);
3208
+ let best;
3209
+ for (const event of receipt.events) {
3210
+ if (toBigInt2(event.from_address) !== tokenAddress) continue;
3211
+ const transfer = decodeTransfer(event);
3212
+ if (!transfer || transfer.from !== fromAddress || transfer.to !== toAddress) continue;
3213
+ if (best === void 0 || transfer.amount > best) best = transfer.amount;
3214
+ }
3215
+ if (best === void 0) {
3216
+ return { valid: false, transactionHash, reason: "TRANSFER_NOT_FOUND" };
3217
+ }
3218
+ if (best < minAmount) {
3219
+ return {
3220
+ valid: false,
3221
+ transactionHash,
3222
+ reason: "AMOUNT_TOO_LOW",
3223
+ detail: `paid ${best}, expected at least ${minAmount}`
3224
+ };
3225
+ }
3226
+ const key = "0x" + BigInt(transactionHash).toString(16);
3227
+ if (!await this.store.consume(key)) {
3228
+ return { valid: false, transactionHash, reason: "ALREADY_USED" };
3229
+ }
3230
+ return {
3231
+ valid: true,
3232
+ transactionHash,
3233
+ amount: best,
3234
+ ...receipt.blockNumber !== void 0 ? { blockNumber: receipt.blockNumber } : {}
3235
+ };
3236
+ }
3237
+ };
3238
+
3239
+ // src/ai.ts
3240
+ var AI_BASE = "/ai";
3241
+ var THINK_ACTIONS = /* @__PURE__ */ new Set(["swap", "supply", "withdraw", "hold"]);
3242
+ var PAID_MAX_ATTEMPTS = 3;
3243
+ var PAID_RETRY_DELAY_MS = 5e3;
3244
+ var padFelt = (value) => "0x" + BigInt(value).toString(16).padStart(64, "0");
3245
+ function sameFelt(a, b) {
3246
+ try {
3247
+ return BigInt(a) === BigInt(b);
3248
+ } catch {
3249
+ return false;
3250
+ }
3251
+ }
3252
+ function sessionTransferOffer(err) {
3253
+ if (!(err instanceof shared.ChipiApiError) || err.status !== 402) return void 0;
3254
+ const details = err.details;
3255
+ return details?.x402?.accepts?.find(
3256
+ (r) => r.scheme === "exact" && r.network === "starknet-mainnet" && r.extra?.paymentMethod === "session-transfer" && sameFelt(r.asset, shared.CONTRACT_ADDRESSES.USDC_MAINNET)
3257
+ );
3258
+ }
3259
+ function thinkInputBytes(params) {
3260
+ const bytes = (text) => new TextEncoder().encode(text).length;
3261
+ const chain = params.chain ?? "starknet";
3262
+ return bytes(JSON.stringify(params.portfolio, null, 2)) + (params.history?.recentDecisions ? bytes(String(params.history.recentDecisions)) : 0) + 2 * bytes(chain);
3263
+ }
3264
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3265
+ function isThinkDecision(decision) {
3266
+ if (!decision || typeof decision !== "object") return false;
3267
+ const d = decision;
3268
+ if (typeof d.action !== "string" || !THINK_ACTIONS.has(d.action)) return false;
3269
+ if (typeof d.reason !== "string") return false;
3270
+ if (d.amount !== void 0 && (typeof d.amount !== "number" || !Number.isFinite(d.amount)))
3271
+ return false;
3272
+ if (d.confidence !== void 0 && typeof d.confidence !== "number") return false;
3273
+ if (d.from !== void 0 && typeof d.from !== "string") return false;
3274
+ if (d.to !== void 0 && typeof d.to !== "string") return false;
3275
+ return true;
3276
+ }
3277
+ var ChipiAi = class {
3278
+ constructor(client, apiSecretKey, sessions) {
3279
+ this.client = client;
3280
+ this.apiSecretKey = apiSecretKey;
3281
+ this.sessions = sessions;
3282
+ }
3283
+ secret() {
3284
+ const key = this.apiSecretKey;
3285
+ if (!key || !key.startsWith("sk_")) {
3286
+ throw new shared.ChipiAuthError(
3287
+ "sdk.ai needs a secret key (sk_): configure the SDK with apiSecretKey and call it from a server, never the browser."
3288
+ );
3289
+ }
3290
+ return key;
3291
+ }
3292
+ /**
3293
+ * `GET /v1/ai/signals`: technical signals per token, market sentiment and
3294
+ * DeFi yields. Free up to a monthly quota per organization, then billed per call.
3295
+ */
3296
+ async signals(params = {}) {
3297
+ const query = {};
3298
+ if (params.tokens?.length) query.tokens = params.tokens.join(",");
3299
+ if (params.chain) query.chain = params.chain;
3300
+ return this.client.get({
3301
+ endpoint: `${AI_BASE}/signals`,
3302
+ params: query,
3303
+ bearerToken: this.secret()
3304
+ });
3305
+ }
3306
+ /**
3307
+ * `POST /v1/ai/think`: one DeFi decision for a portfolio, with the latest
3308
+ * signals and your risk parameters injected into the prompt.
3309
+ *
3310
+ * The decision is advice, not an order: check it with `isThinkDecision` and
3311
+ * enforce your own limits before executing anything.
3312
+ *
3313
+ * **Paying per call (x402).** With `payWithSession`, a 402 that carries a
3314
+ * session-transfer offer (credits short, x402 on) is paid with the agent's
3315
+ * session key: a USDC `transfer` to the offer's `payTo`, then the same call
3316
+ * again with `X-PAYMENT`. The price must be at most `maxAmount`, `model`
3317
+ * must be payable and the input within `maxPromptBytes`, all checked before
3318
+ * paying. The session must whitelist `transfer` and cap USDC. A payment
3319
+ * that was made but not served is reported in `error.details.xPayment`;
3320
+ * pass it back as `options.xPayment` to retry without paying again.
3321
+ *
3322
+ * @example
3323
+ * ```typescript
3324
+ * const res = await sdk.ai.think(
3325
+ * { portfolio, model: "haiku" },
3326
+ * { payWithSession: { encryptKey, wallet, session, maxAmount: 20_000n } } // $0.02 at most
3327
+ * );
3328
+ * res.x402Payment; // set when the call was paid over x402
3329
+ * ```
3330
+ */
3331
+ async think(params, options = {}) {
3332
+ try {
3333
+ return await this.postThink(params, options.xPayment);
3334
+ } catch (err) {
3335
+ const offer = options.payWithSession ? sessionTransferOffer(err) : void 0;
3336
+ if (!offer || !options.payWithSession) throw err;
3337
+ return this.payAndThink(params, offer, options.payWithSession);
3338
+ }
3339
+ }
3340
+ postThink(params, xPayment) {
3341
+ return this.client.post({
3342
+ endpoint: `${AI_BASE}/think`,
3343
+ bearerToken: this.secret(),
3344
+ body: params,
3345
+ ...xPayment ? { headers: { "X-PAYMENT": xPayment } } : {}
3346
+ });
3347
+ }
3348
+ /**
3349
+ * Pay one think call with a session-key USDC transfer, then send it again
3350
+ * with `X-PAYMENT`. Every check that can refuse the call runs before the
3351
+ * transfer; the transfer counts only once its receipt shows the USDC moved.
3352
+ */
3353
+ async payAndThink(params, offer, pay) {
3354
+ if (!this.sessions) {
3355
+ throw new shared.ChipiApiError(
3356
+ "payWithSession needs the SDK's session client",
3357
+ "X402_NOT_CONFIGURED",
3358
+ 402
3359
+ );
3360
+ }
3361
+ let maxAmount;
3362
+ try {
3363
+ maxAmount = BigInt(pay.maxAmount);
3364
+ } catch {
3365
+ throw new shared.ChipiApiError(
3366
+ `maxAmount must be an integer of USDC base units (6 decimals), got ${String(pay.maxAmount)}; nothing was paid`,
3367
+ "X402_INVALID_MAX_AMOUNT",
3368
+ 400
3369
+ );
3370
+ }
3371
+ const amount = BigInt(offer.maxAmountRequired);
3372
+ if (amount > maxAmount) {
3373
+ throw new shared.ChipiApiError(
3374
+ `The x402 price (${amount} USDC base units) is above maxAmount (${pay.maxAmount}); nothing was paid`,
3375
+ "X402_PRICE_ABOVE_MAX",
3376
+ 402,
3377
+ { x402: { accepts: [offer] } }
3378
+ );
3379
+ }
3380
+ const payable = offer.extra?.payableModels;
3381
+ if (params.model && Array.isArray(payable) && !payable.includes(params.model)) {
3382
+ throw new shared.ChipiApiError(
3383
+ `Model ${params.model} cannot be paid with x402 (payable: ${payable.join(", ")}); nothing was paid`,
3384
+ "X402_MODEL_NOT_PAYABLE",
3385
+ 402,
3386
+ { x402: { accepts: [offer] } }
3387
+ );
3388
+ }
3389
+ const maxBytes = offer.extra?.maxPromptBytes;
3390
+ if (typeof maxBytes === "number" && thinkInputBytes(params) > maxBytes) {
3391
+ throw new shared.ChipiApiError(
3392
+ `Paid calls accept at most ${maxBytes} bytes of portfolio, history and chain; nothing was paid`,
3393
+ "X402_PAYLOAD_TOO_LARGE",
3394
+ 402,
3395
+ { x402: { accepts: [offer] } }
3396
+ );
3397
+ }
3398
+ const payer = pay.wallet.publicKey;
3399
+ const mask = (1n << 128n) - 1n;
3400
+ const transactionHash = await this.sessions.executeTransactionWithSession(
3401
+ {
3402
+ encryptKey: pay.encryptKey,
3403
+ wallet: pay.wallet,
3404
+ session: pay.session,
3405
+ calls: [
3406
+ {
3407
+ contractAddress: offer.asset,
3408
+ entrypoint: "transfer",
3409
+ calldata: [offer.payTo, (amount & mask).toString(), (amount >> 128n).toString()]
3410
+ }
3411
+ ]
3412
+ },
3413
+ this.secret()
3414
+ );
3415
+ const header = {
3416
+ x402Version: 1,
3417
+ scheme: "exact",
3418
+ network: "starknet-mainnet",
3419
+ payload: {
3420
+ signature: { r: padFelt(transactionHash), s: "session" },
3421
+ fromAddress: padFelt(payer),
3422
+ toAddress: padFelt(offer.payTo),
3423
+ amount: amount.toString(),
3424
+ asset: offer.asset,
3425
+ validUntil: Math.floor(Date.now() / 1e3) + offer.maxTimeoutSeconds,
3426
+ nonce: padFelt(transactionHash)
3427
+ }
3428
+ };
3429
+ const xPayment = JSON.stringify(header);
3430
+ const paidError = (message, code, extra = {}) => new shared.ChipiApiError(message, code, 402, {
3431
+ x402: { accepts: [offer] },
3432
+ x402PaymentHash: transactionHash,
3433
+ ...extra
3434
+ });
3435
+ let receipt;
3436
+ try {
3437
+ receipt = await waitForTransaction(transactionHash, { timeoutMs: 12e4 });
3438
+ } catch (err) {
3439
+ throw paidError(
3440
+ `No receipt yet for the x402 payment ${transactionHash}: ${err instanceof Error ? err.message : String(err)}`,
3441
+ "X402_PAYMENT_PENDING",
3442
+ { xPayment }
3443
+ );
3444
+ }
3445
+ const paid = await new SessionTxVerifier({
3446
+ txHashStore: { consume: () => true },
3447
+ getReceipt: async () => receipt
3448
+ }).verifyTransfer({
3449
+ transactionHash,
3450
+ token: offer.asset,
3451
+ from: payer,
3452
+ to: offer.payTo,
3453
+ minAmount: amount
3454
+ });
3455
+ if (!paid.valid) {
3456
+ throw paidError(
3457
+ `The x402 payment did not move ${amount} USDC base units to payTo (${paid.reason}). Check the session's whitelist (transfer) and its USDC cap.`,
3458
+ "X402_PAYMENT_FAILED",
3459
+ { reason: paid.reason }
3460
+ );
3461
+ }
3462
+ for (let attempt = 1; ; attempt++) {
3463
+ try {
3464
+ return await this.postThink(params, xPayment);
3465
+ } catch (err) {
3466
+ const retryable = err instanceof shared.ChipiApiError && (err.status === 409 || err.status === 503 && err.details?.retryable === true);
3467
+ if (retryable && attempt < PAID_MAX_ATTEMPTS) {
3468
+ await sleep(PAID_RETRY_DELAY_MS);
3469
+ continue;
3470
+ }
3471
+ if (err instanceof shared.ChipiApiError) {
3472
+ throw new shared.ChipiApiError(err.message, err.code, err.status ?? 402, {
3473
+ ...typeof err.details === "object" && err.details !== null ? err.details : {},
3474
+ x402PaymentHash: transactionHash,
3475
+ xPayment
3476
+ });
3477
+ }
3478
+ throw err;
3479
+ }
3480
+ }
3481
+ }
3482
+ /**
3483
+ * `POST /v1/ai/execute`: unsigned AVNU swap calls (approve + swap) for
3484
+ * `walletAddress`. Nothing is submitted; sign the calls with a session key
3485
+ * or the owner's key.
3486
+ */
3487
+ async execute(params) {
3488
+ return this.client.post({
3489
+ endpoint: `${AI_BASE}/execute`,
3490
+ bearerToken: this.secret(),
3491
+ body: params
3492
+ });
3493
+ }
3494
+ /** `POST /v1/ai/chat`: general-purpose chat, billed per token. */
3495
+ async chat(params) {
3496
+ return this.client.post({
3497
+ endpoint: `${AI_BASE}/chat`,
3498
+ bearerToken: this.secret(),
3499
+ body: params
3500
+ });
3501
+ }
3502
+ /**
3503
+ * `GET /v1/ai/prices`: USD prices and 1h / 24h / 7d changes, refreshed about
3504
+ * every 30 seconds. No credits are charged, but like every `sdk.ai` method it
3505
+ * needs the SDK's secret key (`sk_`).
3506
+ */
3507
+ async prices(params = {}) {
3508
+ const query = {};
3509
+ if (params.tokens?.length) query.tokens = params.tokens.join(",");
3510
+ if (params.chain) query.chain = params.chain;
3511
+ return this.client.get({
3512
+ endpoint: `${AI_BASE}/prices`,
3513
+ params: query,
3514
+ bearerToken: this.secret()
3515
+ });
3516
+ }
3517
+ /** `GET /v1/ai/models`: available models, prices and limits. */
3518
+ async models() {
3519
+ return this.client.get({
3520
+ endpoint: `${AI_BASE}/models`,
3521
+ bearerToken: this.secret()
3522
+ });
3523
+ }
3524
+ };
3046
3525
  var InMemoryNonceStore = class {
3047
3526
  constructor() {
3048
3527
  this.used = /* @__PURE__ */ new Set();
@@ -3360,6 +3839,7 @@ var ChipiSDK = class {
3360
3839
  this.sessions = new ChipiSessions(this.client);
3361
3840
  this.exchanges = new ChipiExchanges(this.client);
3362
3841
  this.treasury = new ChipiTreasury(this.client, config.apiSecretKey);
3842
+ this.ai = new ChipiAi(this.client, config.apiSecretKey, this.sessions);
3363
3843
  this.executeTransaction = this.executeTransaction.bind(this);
3364
3844
  this.executeTransactionWithSession = this.executeTransactionWithSession.bind(this);
3365
3845
  this.transfer = this.transfer.bind(this);
@@ -3623,8 +4103,8 @@ var ChipiSDK = class {
3623
4103
  /**
3624
4104
  * Get on-chain transaction status (supports polling for confirmation)
3625
4105
  */
3626
- async getTransactionStatus(hash10, bearerToken) {
3627
- return this.transactions.getTransactionStatus(hash10, this.resolveBearerToken(bearerToken));
4106
+ async getTransactionStatus(hash11, bearerToken) {
4107
+ return this.transactions.getTransactionStatus(hash11, this.resolveBearerToken(bearerToken));
3628
4108
  }
3629
4109
  /**
3630
4110
  * Prepare a wallet upgrade (e.g., READY → CHIPI or CHIPI v29 → v33).
@@ -3672,49 +4152,6 @@ var ChipiBrowserSDK = class extends ChipiSDK {
3672
4152
  // All methods are inherited from ChipiSDK
3673
4153
  // bearerToken parameter is required for each authenticated request
3674
4154
  };
3675
- var DEFAULT_RETRY_INTERVAL_MS = 3e3;
3676
- var DEFAULT_TIMEOUT_MS = 3e5;
3677
- function isPending(finalityStatus) {
3678
- return finalityStatus === void 0 || finalityStatus === "RECEIVED" || finalityStatus === "PENDING" || finalityStatus === "PRE_CONFIRMED";
3679
- }
3680
- async function waitForTransaction(transactionHash, options = {}) {
3681
- const {
3682
- nodeUrl = shared.WALLET_RPC_ENDPOINTS.CHIPI,
3683
- retryIntervalMs = DEFAULT_RETRY_INTERVAL_MS,
3684
- timeoutMs = DEFAULT_TIMEOUT_MS
3685
- } = options;
3686
- const deadline = Date.now() + timeoutMs;
3687
- let lastError;
3688
- for (; ; ) {
3689
- try {
3690
- const receipt = await shared.withRpcFallback(
3691
- (provider) => provider.getTransactionReceipt(transactionHash),
3692
- { fallbackUrl: nodeUrl }
3693
- );
3694
- if (!isPending(receipt?.finality_status)) {
3695
- const reverted = receipt.execution_status === "REVERTED";
3696
- return {
3697
- transactionHash,
3698
- success: !reverted,
3699
- executionStatus: receipt.execution_status,
3700
- finalityStatus: receipt.finality_status,
3701
- ...reverted ? { revertReason: receipt.revert_reason ?? "reverted on chain" } : {},
3702
- events: receipt.events ?? [],
3703
- ...receipt.block_number !== void 0 ? { blockNumber: receipt.block_number } : {}
3704
- };
3705
- }
3706
- } catch (err) {
3707
- lastError = err;
3708
- }
3709
- if (Date.now() + retryIntervalMs >= deadline) {
3710
- const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
3711
- throw new Error(
3712
- `Timed out after ${timeoutMs}ms waiting for ${transactionHash}${detail}. The transaction may still land; re-check before treating it as failed.`
3713
- );
3714
- }
3715
- await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
3716
- }
3717
- }
3718
4155
 
3719
4156
  // src/index.ts
3720
4157
  init_encryption2();
@@ -4444,7 +4881,7 @@ var JWT_APPLE_SUB_KIND_TAG = "JWT_ES256_APPLE_SUB";
4444
4881
  var JWT_APPLE_SUB_KIND_TAG_FELT = BigInt(starknet.shortString.encodeShortString(JWT_APPLE_SUB_KIND_TAG));
4445
4882
  var APPLE_ISSUER = "https://appleid.apple.com";
4446
4883
  var NONCE_B64URL_LEN = 43;
4447
- var U128_MASK = (1n << 128n) - 1n;
4884
+ var U128_MASK2 = (1n << 128n) - 1n;
4448
4885
  function assertBytes3(name, b, len) {
4449
4886
  if (b.length !== len) {
4450
4887
  throw new Error(`${name} must be exactly ${len} bytes; got ${b.length}.`);
@@ -4454,7 +4891,7 @@ function bytesBeToU2563(b) {
4454
4891
  assertBytes3("secp256r1 32B value", b, 32);
4455
4892
  let x = 0n;
4456
4893
  for (let i = 0; i < b.length; i++) x = x << 8n | BigInt(b[i]);
4457
- return { low: x & U128_MASK, high: x >> 128n };
4894
+ return { low: x & U128_MASK2, high: x >> 128n };
4458
4895
  }
4459
4896
  function jwtAppleSubHash(sub) {
4460
4897
  const bytes = new TextEncoder().encode(sub);
@@ -4589,9 +5026,9 @@ function buildEnvelopePayload(fields) {
4589
5026
  BigInt(fields.issOffset),
4590
5027
  BigInt(fields.subOffset),
4591
5028
  BigInt(fields.subLen),
4592
- fields.r & U128_MASK,
5029
+ fields.r & U128_MASK2,
4593
5030
  fields.r >> 128n,
4594
- fields.s & U128_MASK,
5031
+ fields.s & U128_MASK2,
4595
5032
  fields.s >> 128n,
4596
5033
  BigInt(fields.yParity)
4597
5034
  ];
@@ -4951,6 +5388,7 @@ Object.defineProperty(exports, "Currency", {
4951
5388
  });
4952
5389
  exports.APPLE_ISSUER = APPLE_ISSUER;
4953
5390
  exports.BOOTSTRAP_DOMAIN_SEPARATOR = BOOTSTRAP_DOMAIN_SEPARATOR;
5391
+ exports.ChipiAi = ChipiAi;
4954
5392
  exports.ChipiBrowserSDK = ChipiBrowserSDK;
4955
5393
  exports.ChipiClient = ChipiClient;
4956
5394
  exports.ChipiPaymasterAdapter = ChipiPaymasterAdapter;
@@ -4969,12 +5407,14 @@ exports.OP_ROTATE_OWNER = OP_ROTATE_OWNER;
4969
5407
  exports.OP_SET_THRESHOLD = OP_SET_THRESHOLD;
4970
5408
  exports.ROLE_GUARDIAN = ROLE_GUARDIAN;
4971
5409
  exports.ROLE_OWNER = ROLE_OWNER;
5410
+ exports.SessionTxVerifier = SessionTxVerifier;
4972
5411
  exports.ShhhAccount = ShhhAccount;
4973
5412
  exports.TIMELOCK_SECONDS = TIMELOCK_SECONDS;
4974
5413
  exports.VIEW_SELECTORS = VIEW_SELECTORS;
4975
5414
  exports.X402Facilitator = X402Facilitator;
4976
5415
  exports.X402ShhhClient = X402ShhhClient;
4977
5416
  exports.assertValidV2Snip12Envelope = assertValidV2Snip12Envelope;
5417
+ exports.buildAddSessionKeyCall = buildAddSessionKeyCall;
4978
5418
  exports.buildBootstrapSignedCall = buildBootstrapSignedCall;
4979
5419
  exports.buildCancelPendingOpCall = buildCancelPendingOpCall;
4980
5420
  exports.buildCancelRecoveryCall = buildCancelRecoveryCall;
@@ -4996,6 +5436,10 @@ exports.buildProposeAddOwnerCall = buildProposeAddOwnerCall;
4996
5436
  exports.buildProposeRemoveOwnerCall = buildProposeRemoveOwnerCall;
4997
5437
  exports.buildProposeRotateOwnerCall = buildProposeRotateOwnerCall;
4998
5438
  exports.buildProposeSetThresholdCall = buildProposeSetThresholdCall;
5439
+ exports.buildRemoveSpendingPolicyCall = buildRemoveSpendingPolicyCall;
5440
+ exports.buildRevokeSessionKeyCall = buildRevokeSessionKeyCall;
5441
+ exports.buildSessionSetupCalls = buildSessionSetupCalls;
5442
+ exports.buildSetSpendingPolicyCall = buildSetSpendingPolicyCall;
4999
5443
  exports.buildStarkEnvelope = buildStarkEnvelope;
5000
5444
  exports.buildThresholdEnvelope = buildThresholdEnvelope;
5001
5445
  exports.buildWebAuthnEnvelopeFromAssertion = buildWebAuthnEnvelopeFromAssertion;
@@ -5021,6 +5465,7 @@ exports.hashCalls = hashCalls;
5021
5465
  exports.hashOutsideExecutionStruct = hashOutsideExecutionStruct;
5022
5466
  exports.hashStarknetDomain = hashStarknetDomain;
5023
5467
  exports.isReadyToExecute = isReadyToExecute;
5468
+ exports.isThinkDecision = isThinkDecision;
5024
5469
  exports.jwtAppleSubHash = jwtAppleSubHash;
5025
5470
  exports.jwtAppleSubOwnerId = jwtAppleSubOwnerId;
5026
5471
  exports.jwtAppleSubPubkeyFelts = jwtAppleSubPubkeyFelts;
@@ -5028,6 +5473,7 @@ exports.labelToFelt = labelToFelt;
5028
5473
  exports.locateSubClaim = locateSubClaim;
5029
5474
  exports.messageHashToNonceB64url = messageHashToNonceB64url;
5030
5475
  exports.migrateChipiWalletToShhh = migrateChipiWalletToShhh;
5476
+ exports.normalizeEntrypoints = normalizeEntrypoints;
5031
5477
  exports.ownerCommitment = ownerCommitment;
5032
5478
  exports.parseAppleIdToken = parseAppleIdToken;
5033
5479
  exports.parseDerEcdsaSignature = parseDerEcdsaSignature;
@@ -5043,6 +5489,7 @@ exports.snip12HashToHexAsciiBytes = snip12HashToHexAsciiBytes;
5043
5489
  exports.starkOwnerId = starkOwnerId;
5044
5490
  exports.starkPubkeyFelts = starkPubkeyFelts;
5045
5491
  exports.submitViaAccount = submitViaAccount;
5492
+ exports.validateSpendingPolicyConfig = validateSpendingPolicyConfig;
5046
5493
  exports.waitForTransaction = waitForTransaction;
5047
5494
  exports.webauthnOwnerId = webauthnOwnerId;
5048
5495
  exports.webauthnOwnerPubkeyParam = webauthnOwnerPubkeyParam;