@oasisprotocol/privana-sdk 0.5.7 → 0.5.8

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.
@@ -643,6 +643,12 @@ var PrivanaClient = class _PrivanaClient {
643
643
  moonpay_currency_code: request.moonpay_currency_code
644
644
  });
645
645
  }
646
+ async createOnRampSession(request) {
647
+ return this.http.post("/v1/accounting/onramp/session", {
648
+ transaction_id: request.transaction_id,
649
+ ip_attestation: request.ip_attestation
650
+ });
651
+ }
646
652
  async updateOnRamp(transactionId, request) {
647
653
  return this.http.post(
648
654
  `/v1/accounting/onramp/${encodeURIComponent(transactionId)}`,
@@ -1707,6 +1713,7 @@ var DEFAULT_NETWORK_CONFIG = NETWORK_CONFIG.testnet;
1707
1713
  function PrivanaProvider({
1708
1714
  children,
1709
1715
  networkConfig: networkConfigOverride,
1716
+ onRamp,
1710
1717
  tokens,
1711
1718
  chains,
1712
1719
  pollingInterval = 1e4,
@@ -1740,7 +1747,9 @@ function PrivanaProvider({
1740
1747
  networkConfigOverride?.chainId,
1741
1748
  networkConfigOverride?.name,
1742
1749
  networkConfigOverride?.accountingContract,
1743
- networkConfigOverride?.apiUrl
1750
+ networkConfigOverride?.apiUrl,
1751
+ networkConfigOverride?.moonpayApiUrl,
1752
+ networkConfigOverride?.moonpayApiKey
1744
1753
  ]);
1745
1754
  const resolvedChains = react.useMemo(() => {
1746
1755
  if (chains && chains.length > 0) return chains;
@@ -1923,6 +1932,7 @@ function PrivanaProvider({
1923
1932
  () => ({
1924
1933
  client,
1925
1934
  networkConfig,
1935
+ onRamp,
1926
1936
  enabledTokens,
1927
1937
  defaultToken: enabledTokens[0],
1928
1938
  getTokenById,
@@ -1943,6 +1953,7 @@ function PrivanaProvider({
1943
1953
  [
1944
1954
  client,
1945
1955
  networkConfig,
1956
+ onRamp,
1946
1957
  enabledTokens,
1947
1958
  getTokenById,
1948
1959
  getChainById2,
@@ -2694,6 +2705,176 @@ function creditedAmountFromResponse(response, requestedAmount) {
2694
2705
  function isDefinitiveCandidateFailure(error) {
2695
2706
  return error instanceof AccountingApiError && error.statusCode === 400;
2696
2707
  }
2708
+ var BYTES32_PATTERN = /^0x[0-9a-fA-F]{64}$/;
2709
+ var PROVIDER_ASSET_CODE_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
2710
+ var TRANSAK_MINIMUM_TARGET_PERCENT = 105n;
2711
+ function resolveProductOnRamp({
2712
+ config,
2713
+ enabledTokens,
2714
+ legacyToken,
2715
+ moonpayApiKey
2716
+ }) {
2717
+ if (config === void 0) {
2718
+ if (!moonpayApiKey) {
2719
+ return unavailable("moonpay", legacyToken, false, "MoonPay is not configured.");
2720
+ }
2721
+ if (!legacyToken) {
2722
+ return available("moonpay", void 0, void 0, false);
2723
+ }
2724
+ if (!legacyToken.moonpayCurrencyCode) {
2725
+ return unavailable(
2726
+ "moonpay",
2727
+ legacyToken,
2728
+ false,
2729
+ `${legacyToken.symbol} isn\u2019t available for card purchases yet.`
2730
+ );
2731
+ }
2732
+ return available("moonpay", legacyToken, legacyToken.moonpayCurrencyCode, false);
2733
+ }
2734
+ if (!isValidConfig(config)) {
2735
+ return unavailable(null, void 0, true, "Card on-ramp configuration is invalid.");
2736
+ }
2737
+ const providerAssetCode = config.providerAssetCode.trim().toLowerCase();
2738
+ const token = enabledTokens.find(
2739
+ (candidate) => candidate.id.toLowerCase() === config.tokenId.toLowerCase()
2740
+ );
2741
+ if (!token) {
2742
+ return unavailable(
2743
+ config.provider,
2744
+ void 0,
2745
+ true,
2746
+ "The configured card-purchase token is not enabled."
2747
+ );
2748
+ }
2749
+ if (token.contract.toLowerCase() === viem.zeroAddress) {
2750
+ return unavailable(config.provider, token, true, "Card purchases require an ERC20 token.");
2751
+ }
2752
+ if (config.provider === "moonpay") {
2753
+ if (!moonpayApiKey) {
2754
+ return unavailable("moonpay", token, true, "MoonPay is not configured.");
2755
+ }
2756
+ if (!token.moonpayCurrencyCode || token.moonpayCurrencyCode.toLowerCase() !== providerAssetCode) {
2757
+ return unavailable(
2758
+ "moonpay",
2759
+ token,
2760
+ true,
2761
+ "The configured MoonPay asset does not match the selected token."
2762
+ );
2763
+ }
2764
+ }
2765
+ return available(config.provider, token, providerAssetCode, true);
2766
+ }
2767
+ function createProductOnRampFlowSnapshot({
2768
+ id,
2769
+ selection,
2770
+ amount,
2771
+ allowance,
2772
+ lockServiceAddress,
2773
+ moonpayApiKey,
2774
+ scope
2775
+ }) {
2776
+ if (selection.unavailableReason || !selection.provider || !selection.token || !selection.providerAssetCode) {
2777
+ throw new Error(selection.unavailableReason ?? "Card on-ramp configuration is unavailable.");
2778
+ }
2779
+ const amountBaseUnits = viem.parseUnits(amount, selection.token.decimals);
2780
+ if (amountBaseUnits <= 0n) throw new Error("Card purchase amount must be positive.");
2781
+ if (!scope.beneficiaryAddress) {
2782
+ throw new Error("Sign in or connect a wallet before starting a card purchase.");
2783
+ }
2784
+ if (allowance && !lockServiceAddress) {
2785
+ throw new Error("The lock service address is not configured.");
2786
+ }
2787
+ return {
2788
+ id,
2789
+ selection: {
2790
+ ...selection,
2791
+ provider: selection.provider,
2792
+ token: { ...selection.token },
2793
+ providerAssetCode: selection.providerAssetCode,
2794
+ unavailableReason: null
2795
+ },
2796
+ amount,
2797
+ amountBaseUnits,
2798
+ allowance: cloneAllowance(allowance),
2799
+ requiresLock: allowance !== void 0,
2800
+ lockServiceAddress: allowance ? lockServiceAddress : void 0,
2801
+ moonpayApiKey: selection.provider === "moonpay" ? moonpayApiKey : void 0,
2802
+ scope: { ...scope }
2803
+ };
2804
+ }
2805
+ function matchesProductOnRampScope(expected, current) {
2806
+ return expected.apiUrl === current.apiUrl && expected.accountingChainId === current.accountingChainId && expected.accountingContract.toLowerCase() === current.accountingContract.toLowerCase() && expected.beneficiaryAddress?.toLowerCase() === current.beneficiaryAddress?.toLowerCase();
2807
+ }
2808
+ function isMoonPayProductOnRamp(selection) {
2809
+ return selection.provider === "moonpay" && !selection.unavailableReason;
2810
+ }
2811
+ function getTransakMinimumTargetBaseUnits(minimum) {
2812
+ return (minimum * TRANSAK_MINIMUM_TARGET_PERCENT + 99n) / 100n;
2813
+ }
2814
+ function getOnRampTokenFingerprint(token) {
2815
+ if (!token) return "";
2816
+ return [token.id.toLowerCase(), token.contract.toLowerCase(), token.chainId, token.decimals].join(
2817
+ "\0"
2818
+ );
2819
+ }
2820
+ function matchesFrozenOnRampToken(frozen, live) {
2821
+ return live !== void 0 && getOnRampTokenFingerprint(live) === getOnRampTokenFingerprint(frozen);
2822
+ }
2823
+ function createProductOnRampOutcomeCallbacks({
2824
+ requiresLock,
2825
+ onComplete,
2826
+ onLockFailed
2827
+ }) {
2828
+ return {
2829
+ onCredited: requiresLock ? void 0 : onComplete,
2830
+ onLockSubmitted: onComplete,
2831
+ onLockFailed
2832
+ };
2833
+ }
2834
+ function isValidConfig(config) {
2835
+ const candidate = config;
2836
+ if (!isPlainRecord(candidate)) return false;
2837
+ if (candidate.provider !== "moonpay" && candidate.provider !== "transak") return false;
2838
+ if (typeof candidate.tokenId !== "string" || !BYTES32_PATTERN.test(candidate.tokenId)) {
2839
+ return false;
2840
+ }
2841
+ return typeof candidate.providerAssetCode === "string" && PROVIDER_ASSET_CODE_PATTERN.test(candidate.providerAssetCode.trim());
2842
+ }
2843
+ function available(provider, token, providerAssetCode, tokenSelectionLocked) {
2844
+ return {
2845
+ provider,
2846
+ token,
2847
+ providerAssetCode,
2848
+ tokenSelectionLocked,
2849
+ unavailableReason: null
2850
+ };
2851
+ }
2852
+ function unavailable(provider, token, tokenSelectionLocked, unavailableReason) {
2853
+ return {
2854
+ provider,
2855
+ token,
2856
+ providerAssetCode: void 0,
2857
+ tokenSelectionLocked,
2858
+ unavailableReason
2859
+ };
2860
+ }
2861
+ function isPlainRecord(value) {
2862
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
2863
+ const prototype = Object.getPrototypeOf(value);
2864
+ return prototype === Object.prototype || prototype === null;
2865
+ }
2866
+ function cloneAllowance(allowance) {
2867
+ if (!allowance) return void 0;
2868
+ return {
2869
+ value: allowance.value,
2870
+ minAmount: allowance.minAmount,
2871
+ lockDuration: allowance.lockDuration,
2872
+ terms: allowance.terms ? {
2873
+ permissions: allowance.terms.permissions?.map((term) => ({ ...term })),
2874
+ restrictions: allowance.terms.restrictions?.map((term) => ({ ...term }))
2875
+ } : void 0
2876
+ };
2877
+ }
2697
2878
  function cn(...inputs) {
2698
2879
  return tailwindMerge.twMerge(clsx.clsx(inputs));
2699
2880
  }
@@ -2811,6 +2992,9 @@ var moonPayOnRampAdapter = {
2811
2992
  token_id: tokenId,
2812
2993
  chain_id: chainId,
2813
2994
  moonpay_transaction_id: intentId === providerTransactionId ? void 0 : providerTransactionId
2995
+ }),
2996
+ recordDeposit: async ({ client, record, depositTxHash }) => client.updateOnRamp(record.transaction_id, {
2997
+ deposit_tx_hash: depositTxHash
2814
2998
  })
2815
2999
  };
2816
3000
  function normalizeMoonPayProviderEvent(kind, event) {
@@ -3098,15 +3282,39 @@ function assertOnRampRecordProvider(record, configuredProvider) {
3098
3282
  );
3099
3283
  }
3100
3284
  }
3285
+ function assertCreatedOnRampIntent(record, configuredProvider, expected) {
3286
+ assertOnRampRecordProvider(record, configuredProvider);
3287
+ if (typeof record.provider_asset_code !== "string" || record.provider_asset_code.toLowerCase() !== expected.providerAssetCode.toLowerCase()) {
3288
+ throw new Error(
3289
+ `On-ramp intent asset ${record.provider_asset_code} does not match requested asset ${expected.providerAssetCode}`
3290
+ );
3291
+ }
3292
+ if (typeof record.token_id !== "string" || record.token_id.toLowerCase() !== expected.tokenId.toLowerCase()) {
3293
+ throw new Error("On-ramp intent token does not match the requested token");
3294
+ }
3295
+ if (record.chain_id !== expected.chainId) {
3296
+ throw new Error("On-ramp intent chain does not match the requested chain");
3297
+ }
3298
+ if (typeof record.wallet_address !== "string" || record.wallet_address.toLowerCase() !== expected.walletAddress.toLowerCase()) {
3299
+ throw new Error("On-ramp intent wallet does not match the Privana deposit address");
3300
+ }
3301
+ }
3101
3302
  function matchesOnRampTransaction(record, transactionId) {
3102
3303
  return record.transaction_id === transactionId || record.external_transaction_id === transactionId || record.provider_transaction_id === transactionId || record.moonpay_transaction_id === transactionId;
3103
3304
  }
3104
- function getOnRampVerificationKey(record) {
3105
- return record.on_chain_tx_hash ?? record.transaction_id;
3106
- }
3107
3305
  function getOnRampIntentId(record) {
3108
3306
  return record.external_transaction_id ?? record.transaction_id;
3109
3307
  }
3308
+ function getOnRampVerificationKey(record) {
3309
+ return getOnRampIntentId(record);
3310
+ }
3311
+ function canRetryOnRampVerification(record, activeVerificationId) {
3312
+ return Boolean(record.on_chain_tx_hash) && activeVerificationId === null;
3313
+ }
3314
+ async function recordOnRampProviderDeposit(adapter, context) {
3315
+ if (context.record.provider !== adapter.provider) return void 0;
3316
+ return adapter.recordDeposit?.(context);
3317
+ }
3110
3318
  async function verifyPendingOnRampsSequentially({
3111
3319
  records,
3112
3320
  shouldStop,
@@ -3130,13 +3338,19 @@ async function verifyPendingOnRampsSequentially({
3130
3338
 
3131
3339
  // src/sdk/on-ramp/recovery.ts
3132
3340
  var MAX_UNRESOLVED_ONRAMP_INTENTS = 10;
3341
+ var MAX_CREDITED_ONRAMP_VERIFICATIONS = 1e3;
3133
3342
  var MIN_ONRAMP_PENDING_REQUEST_INTERVAL_MS = 1e4;
3134
3343
  var ONRAMP_INTENT_RETENTION_MS = 365 * 24 * 60 * 60 * 1e3;
3135
3344
  var ONRAMP_RECOVERY_VERSION = 1;
3345
+ var ONRAMP_CREDIT_RECOVERY_VERSION = 2;
3136
3346
  function recoveryKey(scope) {
3137
3347
  const api = encodeURIComponent(scope.apiUrl.replace(/\/$/, ""));
3138
3348
  return `privana:onramp-intents:${api}:${scope.chainId}:${scope.userAddress.toLowerCase()}`;
3139
3349
  }
3350
+ function creditedRecoveryKey(scope) {
3351
+ const api = encodeURIComponent(scope.apiUrl.replace(/\/$/, ""));
3352
+ return `privana:onramp-credited:${api}:${scope.chainId}:${scope.userAddress.toLowerCase()}`;
3353
+ }
3140
3354
  function isIntent(value) {
3141
3355
  if (!value || typeof value !== "object") return false;
3142
3356
  const intent = value;
@@ -3155,6 +3369,25 @@ function writeIntents(scope, intents) {
3155
3369
  })
3156
3370
  );
3157
3371
  }
3372
+ function isCreditedVerification(value) {
3373
+ if (!value || typeof value !== "object") return false;
3374
+ const verification = value;
3375
+ return typeof verification.verificationKey === "string" && verification.verificationKey.length > 0 && verification.verificationKey.length <= 512 && Number.isFinite(verification.savedAt) && (verification.savedAt ?? 0) > 0;
3376
+ }
3377
+ function writeCreditedVerifications(scope, verifications) {
3378
+ const key = creditedRecoveryKey(scope);
3379
+ if (verifications.length === 0) {
3380
+ removeBrowserStorageItem(key);
3381
+ return true;
3382
+ }
3383
+ return setBrowserStorageItem(
3384
+ key,
3385
+ JSON.stringify({
3386
+ version: ONRAMP_CREDIT_RECOVERY_VERSION,
3387
+ verifications
3388
+ })
3389
+ );
3390
+ }
3158
3391
  function loadUnresolvedOnRampIntents(scope, now = Date.now()) {
3159
3392
  const key = recoveryKey(scope);
3160
3393
  try {
@@ -3186,6 +3419,48 @@ function forgetUnresolvedOnRampIntent(scope, transactionId, now = Date.now()) {
3186
3419
  );
3187
3420
  writeIntents(scope, intents);
3188
3421
  }
3422
+ function loadCreditedOnRampVerifications(scope, now = Date.now()) {
3423
+ const key = creditedRecoveryKey(scope);
3424
+ try {
3425
+ const raw = getBrowserStorageItem(key);
3426
+ if (!raw) return [];
3427
+ const parsed = JSON.parse(raw);
3428
+ if (parsed.version !== ONRAMP_CREDIT_RECOVERY_VERSION || !Array.isArray(parsed.verifications)) {
3429
+ removeBrowserStorageItem(key);
3430
+ return [];
3431
+ }
3432
+ const retained = parsed.verifications.filter(isCreditedVerification).filter((verification) => now - verification.savedAt <= ONRAMP_INTENT_RETENTION_MS).sort((left, right) => left.savedAt - right.savedAt).slice(-MAX_CREDITED_ONRAMP_VERIFICATIONS);
3433
+ if (retained.length !== parsed.verifications.length) {
3434
+ writeCreditedVerifications(scope, retained);
3435
+ }
3436
+ return retained;
3437
+ } catch {
3438
+ removeBrowserStorageItem(key);
3439
+ return [];
3440
+ }
3441
+ }
3442
+ function rememberCreditedOnRampVerification(scope, verificationKey, now = Date.now()) {
3443
+ if (!verificationKey || verificationKey.length > 512) return false;
3444
+ const verifications = loadCreditedOnRampVerifications(scope, now).filter(
3445
+ (verification) => verification.verificationKey !== verificationKey
3446
+ );
3447
+ verifications.push({ verificationKey, savedAt: now });
3448
+ return writeCreditedVerifications(scope, verifications.slice(-MAX_CREDITED_ONRAMP_VERIFICATIONS));
3449
+ }
3450
+ function finalizeCreditedOnRampIntent(scope, record, now = Date.now()) {
3451
+ const remembered = rememberCreditedOnRampVerification(
3452
+ scope,
3453
+ getOnRampVerificationKey(record),
3454
+ now
3455
+ );
3456
+ if (remembered) forgetUnresolvedOnRampIntent(scope, getOnRampIntentId(record), now);
3457
+ return remembered;
3458
+ }
3459
+ function filterCreditedOnRampRecords(records, creditedVerificationKeys) {
3460
+ const credited = new Set(creditedVerificationKeys);
3461
+ if (credited.size === 0) return [...records];
3462
+ return records.filter((record) => !credited.has(getOnRampVerificationKey(record)));
3463
+ }
3189
3464
  function discardInvalidOnRampIntent(scope, invalidIntentId, activeIntentId) {
3190
3465
  forgetUnresolvedOnRampIntent(scope, invalidIntentId);
3191
3466
  const invalidatedActiveIntent = activeIntentId === invalidIntentId;
@@ -3236,17 +3511,31 @@ async function getPendingOnRampsWithRecovery({
3236
3511
  } catch (error) {
3237
3512
  if (!isBadRequest(error) || bounded.length === 0) throw error;
3238
3513
  }
3239
- const valid = [];
3514
+ const validResponses = [];
3240
3515
  for (const intentId of bounded) {
3241
3516
  try {
3242
- await client.getPendingOnRamps([intentId]);
3243
- valid.push(intentId);
3517
+ validResponses.push(await client.getPendingOnRamps([intentId]));
3244
3518
  } catch (error) {
3245
3519
  if (!isBadRequest(error)) throw error;
3246
3520
  onInvalidIntent(intentId);
3247
3521
  }
3248
3522
  }
3249
- return client.getPendingOnRamps(valid);
3523
+ if (validResponses.length === 0) return client.getPendingOnRamps([]);
3524
+ return { pending: mergePendingOnRampRows(validResponses.flatMap((response) => response.pending)) };
3525
+ }
3526
+ function mergePendingOnRampRows(rows) {
3527
+ const seen = /* @__PURE__ */ new Set();
3528
+ return rows.filter((record) => {
3529
+ const identifier = record.provider_transaction_id || record.transaction_id;
3530
+ const key = `${record.provider}\0${identifier}`;
3531
+ if (!identifier || seen.has(key)) return false;
3532
+ seen.add(key);
3533
+ return true;
3534
+ }).sort(
3535
+ (left, right) => right.updated_at - left.updated_at || right.created_at - left.created_at || String(right.provider_transaction_id ?? "").localeCompare(
3536
+ String(left.provider_transaction_id ?? "")
3537
+ )
3538
+ );
3250
3539
  }
3251
3540
  function isBadRequest(error) {
3252
3541
  return error instanceof AccountingApiError && error.statusCode === 400;
@@ -3254,6 +3543,17 @@ function isBadRequest(error) {
3254
3543
  var ERC20_TRANSFER_EVENT = viem.parseAbiItem(
3255
3544
  "event Transfer(address indexed from, address indexed to, uint256 value)"
3256
3545
  );
3546
+ function decodeErc20TransferLog(log) {
3547
+ try {
3548
+ return viem.decodeEventLog({
3549
+ abi: [ERC20_TRANSFER_EVENT],
3550
+ data: log.data,
3551
+ topics: log.topics
3552
+ });
3553
+ } catch {
3554
+ return void 0;
3555
+ }
3556
+ }
3257
3557
  function assertErc20OnRampToken(tokenAddress) {
3258
3558
  if (tokenAddress.toLowerCase() === viem.zeroAddress) {
3259
3559
  throw new Error("On-ramp verification supports ERC-20 tokens only");
@@ -3263,23 +3563,31 @@ function erc20MinDepositBaseUnits(minDepositByChain, chainId) {
3263
3563
  const minimum = minDepositByChain?.[String(chainId)]?.erc20;
3264
3564
  return minimum !== void 0 && /^\d+$/.test(minimum) ? BigInt(minimum) : void 0;
3265
3565
  }
3266
- function deliveredErc20Amount(logs, tokenAddress, depositAddress) {
3267
- let delivered = 0n;
3566
+ function resolveErc20OnRampTransfer(logs, tokenAddress, depositAddress) {
3567
+ let match;
3268
3568
  for (const log of logs) {
3269
3569
  if (log.address.toLowerCase() !== tokenAddress.toLowerCase()) continue;
3270
- try {
3271
- const decoded = viem.decodeEventLog({
3272
- abi: [ERC20_TRANSFER_EVENT],
3273
- data: log.data,
3274
- topics: log.topics
3275
- });
3276
- if (decoded.eventName === "Transfer" && decoded.args.to.toLowerCase() === depositAddress.toLowerCase()) {
3277
- delivered += decoded.args.value;
3278
- }
3279
- } catch {
3570
+ const decoded = decodeErc20TransferLog(log);
3571
+ if (!decoded) continue;
3572
+ if (decoded.eventName !== "Transfer" || decoded.args.to.toLowerCase() !== depositAddress.toLowerCase()) {
3573
+ continue;
3280
3574
  }
3575
+ if (match) {
3576
+ throw new Error("On-ramp receipt contains multiple matching ERC-20 Transfer logs");
3577
+ }
3578
+ const logIndex = log.logIndex;
3579
+ if (typeof logIndex !== "number" || !Number.isSafeInteger(logIndex) || logIndex < 0) {
3580
+ throw new Error("Matching on-ramp ERC-20 Transfer does not have a valid log index");
3581
+ }
3582
+ match = { amount: decoded.args.value, logIndex };
3583
+ }
3584
+ if (!match) {
3585
+ throw new Error("On-ramp receipt does not contain a matching ERC-20 Transfer");
3281
3586
  }
3282
- return delivered;
3587
+ if (match.amount <= 0n) {
3588
+ throw new Error("Matching on-ramp ERC-20 Transfer must have a positive amount");
3589
+ }
3590
+ return match;
3283
3591
  }
3284
3592
 
3285
3593
  // src/sdk/on-ramp/settlement.ts
@@ -3361,6 +3669,12 @@ function useEnsureCorrectChain() {
3361
3669
  var DEFAULT_DELIVERY_TIMEOUT_MS = 12e4;
3362
3670
  var DEFAULT_VERIFICATION_TIMEOUT_MS = 10 * 6e4;
3363
3671
  var DEFAULT_FINALITY_RETRY_INTERVAL_MS = 15e3;
3672
+ function bindOnRampFlowSession(ref, flowSession) {
3673
+ ref.current = flowSession;
3674
+ return () => {
3675
+ if (ref.current === flowSession) ref.current = null;
3676
+ };
3677
+ }
3364
3678
  function useOnRamp(options) {
3365
3679
  const {
3366
3680
  adapter,
@@ -3402,6 +3716,7 @@ function useOnRamp(options) {
3402
3716
  const flowSession = react.useMemo(() => Symbol(flowIdentity), [flowIdentity]);
3403
3717
  const flowSessionRef = react.useRef(flowSession);
3404
3718
  flowSessionRef.current = flowSession;
3719
+ react.useEffect(() => bindOnRampFlowSession(flowSessionRef, flowSession), [flowSession]);
3405
3720
  const [status, setStatus] = react.useState("idle");
3406
3721
  const [pending, setPending] = react.useState([]);
3407
3722
  const [error, setError] = react.useState(null);
@@ -3424,6 +3739,7 @@ function useOnRamp(options) {
3424
3739
  const activeVerificationSurfacesFailureRef = react.useRef(false);
3425
3740
  const lockOwnerRef = react.useRef(null);
3426
3741
  const triggeredVerificationKeysRef = react.useRef(/* @__PURE__ */ new Set());
3742
+ const creditedVerificationKeysRef = react.useRef(/* @__PURE__ */ new Set());
3427
3743
  const activeVerificationDoneRef = react.useRef(null);
3428
3744
  const closeReconcilePromiseRef = react.useRef(null);
3429
3745
  const deliveryWaitPromiseRef = react.useRef(null);
@@ -3531,7 +3847,13 @@ function useOnRamp(options) {
3531
3847
  }
3532
3848
  })
3533
3849
  );
3534
- return rows;
3850
+ const creditedKeys = recoveryScope ? loadCreditedOnRampVerifications(recoveryScope).map(
3851
+ (verification) => verification.verificationKey
3852
+ ) : [];
3853
+ return filterCreditedOnRampRecords(rows, [
3854
+ ...creditedKeys,
3855
+ ...creditedVerificationKeysRef.current
3856
+ ]);
3535
3857
  }, [emitDebug, executeOnRampPrivateRead, flowSession, recoveryScope]);
3536
3858
  const readPendingRows = react.useMemo(
3537
3859
  () => createPendingOnRampReadCoordinator({
@@ -3574,7 +3896,8 @@ function useOnRamp(options) {
3574
3896
  activeVerificationDoneRef.current = null;
3575
3897
  }, []);
3576
3898
  const submitPendingLockAfterCredit = react.useCallback(
3577
- async (transactionId, userAddress, creditedAmount) => {
3899
+ async (record, userAddress, creditedAmount) => {
3900
+ const transactionId = record.transaction_id;
3578
3901
  try {
3579
3902
  const settlement = await settlePendingOnRampLock({
3580
3903
  client,
@@ -3589,7 +3912,8 @@ function useOnRamp(options) {
3589
3912
  "not-found"
3590
3913
  );
3591
3914
  emitDebug("lock:not-found");
3592
- (onLockFailedRef.current ?? onErrorRef.current)?.(lockError);
3915
+ if (onLockFailedRef.current) onLockFailedRef.current(lockError, record);
3916
+ else onErrorRef.current?.(lockError);
3593
3917
  return;
3594
3918
  }
3595
3919
  queryClient.invalidateQueries({ queryKey: ["accounting-balance"] });
@@ -3599,7 +3923,7 @@ function useOnRamp(options) {
3599
3923
  emitDebug("lock:submitted", {
3600
3924
  submissionIdPresent: Boolean(settlement.response.submission_id)
3601
3925
  });
3602
- onLockSubmittedRef.current?.(settlement.response);
3926
+ onLockSubmittedRef.current?.(settlement.response, record);
3603
3927
  } catch (err) {
3604
3928
  const error2 = err instanceof PostDepositLockError ? err : new PostDepositLockError(
3605
3929
  err instanceof Error ? err.message : "Lock submission failed",
@@ -3612,7 +3936,8 @@ function useOnRamp(options) {
3612
3936
  reason: error2.reason,
3613
3937
  message: error2.message
3614
3938
  });
3615
- (onLockFailedRef.current ?? onErrorRef.current)?.(error2);
3939
+ if (onLockFailedRef.current) onLockFailedRef.current(error2, record);
3940
+ else onErrorRef.current?.(error2);
3616
3941
  }
3617
3942
  },
3618
3943
  [client, emitDebug, postDepositLock, queryClient]
@@ -3642,6 +3967,13 @@ function useOnRamp(options) {
3642
3967
  setStatus("credited");
3643
3968
  }
3644
3969
  if (record) {
3970
+ creditedVerificationKeysRef.current.add(getOnRampVerificationKey(record));
3971
+ setPending((rows) => filterCreditedOnRampRecords(rows, creditedVerificationKeysRef.current));
3972
+ const finalizeRecovery = () => {
3973
+ if (recoveryScopeAtCredit && !finalizeCreditedOnRampIntent(recoveryScopeAtCredit, record)) {
3974
+ emitDebug("verification:credit-recovery-storage-unavailable");
3975
+ }
3976
+ };
3645
3977
  setFinalityProgress((prev) => {
3646
3978
  if (!(record.transaction_id in prev)) return prev;
3647
3979
  const next = { ...prev };
@@ -3650,39 +3982,48 @@ function useOnRamp(options) {
3650
3982
  });
3651
3983
  const lockOwner = lockOwnerRef.current ?? privateReadAddress;
3652
3984
  if (lockOwner) {
3653
- void submitPendingLockAfterCredit(record.transaction_id, lockOwner, creditedAmount);
3985
+ void submitPendingLockAfterCredit(record, lockOwner, creditedAmount).finally(
3986
+ finalizeRecovery
3987
+ );
3654
3988
  } else if (postDepositLock) {
3655
3989
  emitDebug("lock:owner-unavailable");
3656
- (onLockFailedRef.current ?? onErrorRef.current)?.(
3657
- new PostDepositLockError(
3658
- "No wallet address available to look up the signed lock for this on-ramp",
3659
- "not-found"
3660
- )
3990
+ const lockError = new PostDepositLockError(
3991
+ "No wallet address available to look up the signed lock for this on-ramp",
3992
+ "not-found"
3661
3993
  );
3994
+ if (onLockFailedRef.current) onLockFailedRef.current(lockError, record);
3995
+ else onErrorRef.current?.(lockError);
3996
+ finalizeRecovery();
3997
+ } else {
3998
+ finalizeRecovery();
3662
3999
  }
3663
4000
  }
3664
4001
  void (async () => {
3665
4002
  try {
3666
- if (record && depositTxHash.startsWith("0x")) {
3667
- emitDebug("onramp:mark-deposit-triggered-request", {
4003
+ if (record && adapter.recordDeposit && record.provider === adapter.provider && depositTxHash.startsWith("0x")) {
4004
+ emitDebug("provider-deposit:record-request", {
4005
+ provider: adapter.provider,
3668
4006
  depositTxHash
3669
4007
  });
3670
4008
  const updated = await executeOnRampPrivateRead(
3671
- (readClient) => readClient.updateOnRamp(record.transaction_id, {
3672
- deposit_tx_hash: depositTxHash
4009
+ (readClient) => recordOnRampProviderDeposit(adapter, {
4010
+ client: readClient,
4011
+ record,
4012
+ depositTxHash
3673
4013
  })
3674
4014
  );
3675
- emitDebug("onramp:mark-deposit-triggered-success", {
3676
- record: summariseOnRampRecord(updated)
4015
+ emitDebug("provider-deposit:record-success", {
4016
+ provider: adapter.provider,
4017
+ record: updated ? summariseOnRampRecord(updated) : null
3677
4018
  });
3678
4019
  }
3679
4020
  } catch (err) {
3680
- emitDebug("onramp:mark-deposit-triggered-error", errorPayload(err));
3681
- console.warn("Failed to mark on-ramp row complete:", err);
4021
+ emitDebug("provider-deposit:record-error", {
4022
+ provider: adapter.provider,
4023
+ ...errorPayload(err)
4024
+ });
4025
+ console.warn("Failed to record provider deposit:", err);
3682
4026
  } finally {
3683
- if (record && recoveryScopeAtCredit) {
3684
- forgetUnresolvedOnRampIntent(recoveryScopeAtCredit, getOnRampIntentId(record));
3685
- }
3686
4027
  await refreshPending();
3687
4028
  clearActiveVerification(verificationKey);
3688
4029
  if (record && activeIntentIdRef.current && matchesOnRampTransaction(record, activeIntentIdRef.current)) {
@@ -3694,7 +4035,7 @@ function useOnRamp(options) {
3694
4035
  }
3695
4036
  }
3696
4037
  })();
3697
- onCreditedRef.current?.(depositTxHash);
4038
+ if (record) onCreditedRef.current?.(depositTxHash, record);
3698
4039
  },
3699
4040
  onCheckTimeout: (depositTxHash) => {
3700
4041
  const record = activeVerificationRecordRef.current;
@@ -3732,6 +4073,7 @@ function useOnRamp(options) {
3732
4073
  resetDepositVerification();
3733
4074
  clearActiveVerification();
3734
4075
  triggeredVerificationKeysRef.current.clear();
4076
+ creditedVerificationKeysRef.current.clear();
3735
4077
  setPending([]);
3736
4078
  setFinalityProgress({});
3737
4079
  statusRef.current = "idle";
@@ -3787,28 +4129,19 @@ function useOnRamp(options) {
3787
4129
  quoteCurrencyAmountPresent: quoteCurrencyAmount !== void 0,
3788
4130
  depositAddressReady: true
3789
4131
  });
4132
+ const intentInput = {
4133
+ walletAddress: scopedDepositAddress,
4134
+ tokenId,
4135
+ chainId: token.chainId,
4136
+ providerAssetCode
4137
+ };
3790
4138
  const record = await executeOnRampPrivateRead(
3791
- (readClient) => readClient.createOnRampIntent(
3792
- adapter.buildIntentRequest({
3793
- walletAddress: scopedDepositAddress,
3794
- tokenId,
3795
- chainId: token.chainId,
3796
- providerAssetCode
3797
- })
3798
- )
4139
+ (readClient) => readClient.createOnRampIntent(adapter.buildIntentRequest(intentInput))
3799
4140
  );
3800
4141
  if (flowSessionRef.current !== flowSession) {
3801
4142
  throw new Error("On-ramp account or network changed while creating the intent");
3802
4143
  }
3803
- assertOnRampRecordProvider(record, adapter.provider);
3804
- if (!record.provider_asset_code) {
3805
- throw new Error("On-ramp intent response is missing provider_asset_code");
3806
- }
3807
- if (record.provider_asset_code.toLowerCase() !== providerAssetCode.toLowerCase()) {
3808
- throw new Error(
3809
- `On-ramp intent asset ${record.provider_asset_code} does not match requested asset ${providerAssetCode}`
3810
- );
3811
- }
4144
+ assertCreatedOnRampIntent(record, adapter.provider, intentInput);
3812
4145
  if (postDepositLock && lockOwner && lockAmount !== void 0) {
3813
4146
  const signingWalletClient = await getWalletClient3(wagmiConfig, {
3814
4147
  chainId: networkConfig.chainId
@@ -4049,7 +4382,7 @@ function useOnRamp(options) {
4049
4382
  `Token ${recordTokenId} is on chain ${token.chainId} but record is on chain ${record.chain_id}`
4050
4383
  );
4051
4384
  }
4052
- const amount = await resolveDeliveredAmount({
4385
+ const delivered = await resolveDeliveredTransfer({
4053
4386
  onChainTxHash: record.on_chain_tx_hash,
4054
4387
  chainId: record.chain_id,
4055
4388
  walletAddress: record.wallet_address,
@@ -4062,12 +4395,14 @@ function useOnRamp(options) {
4062
4395
  scopedMinDepositByChain,
4063
4396
  record.chain_id
4064
4397
  );
4065
- if (recordMinDepositBaseUnits !== void 0 && amount < recordMinDepositBaseUnits) {
4398
+ if (recordMinDepositBaseUnits !== void 0 && delivered.amount < recordMinDepositBaseUnits) {
4066
4399
  emitDebug("verification:below-minimum", {
4067
- deliveredAmount: amount.toString(),
4400
+ deliveredAmount: delivered.amount.toString(),
4068
4401
  minDepositBaseUnits: String(recordMinDepositBaseUnits)
4069
4402
  });
4070
- throw new Error(`Delivered amount (${amount} base units) is below the minimum deposit.`);
4403
+ throw new Error(
4404
+ `Delivered amount (${delivered.amount} base units) is below the minimum deposit.`
4405
+ );
4071
4406
  }
4072
4407
  if (activeIntentIdRef.current && matchesOnRampTransaction(record, activeIntentIdRef.current)) {
4073
4408
  setStatus("verifying");
@@ -4075,12 +4410,14 @@ function useOnRamp(options) {
4075
4410
  emitDebug("verification:check-deposit-request", {
4076
4411
  hash: record.on_chain_tx_hash,
4077
4412
  chainId: record.chain_id,
4078
- amount: amount.toString()
4413
+ amount: delivered.amount.toString(),
4414
+ logIndex: delivered.logIndex
4079
4415
  });
4080
4416
  await verify({
4081
4417
  hash: record.on_chain_tx_hash,
4082
4418
  chainId: record.chain_id,
4083
- amount
4419
+ amount: delivered.amount,
4420
+ logIndex: delivered.logIndex
4084
4421
  });
4085
4422
  } catch (err) {
4086
4423
  if (flowSessionRef.current !== flowSession) return;
@@ -4338,7 +4675,7 @@ function summariseOnRampRecord(record) {
4338
4675
  credited_at: record.credited_at ?? null
4339
4676
  };
4340
4677
  }
4341
- async function resolveDeliveredAmount({
4678
+ async function resolveDeliveredTransfer({
4342
4679
  onChainTxHash,
4343
4680
  chainId,
4344
4681
  walletAddress,
@@ -4347,7 +4684,6 @@ async function resolveDeliveredAmount({
4347
4684
  emitDebug
4348
4685
  }) {
4349
4686
  assertErc20OnRampToken(token.contract);
4350
- let receiptError;
4351
4687
  try {
4352
4688
  const receipt = await waitForTransactionReceipt(wagmiConfig, {
4353
4689
  hash: onChainTxHash,
@@ -4355,27 +4691,21 @@ async function resolveDeliveredAmount({
4355
4691
  timeout: 6e4,
4356
4692
  pollingInterval: 4e3
4357
4693
  });
4358
- const delivered = deliveredErc20Amount(receipt.logs, token.contract, walletAddress);
4359
- if (delivered > 0n) {
4360
- emitDebug("verification:amount-from-receipt", {
4361
- amount: delivered.toString(),
4362
- tokenAddress: token.contract,
4363
- depositAddressMatched: true
4364
- });
4365
- return delivered;
4366
- }
4367
- emitDebug("verification:amount-from-receipt-missing", {
4694
+ const delivered = resolveErc20OnRampTransfer(receipt.logs, token.contract, walletAddress);
4695
+ emitDebug("verification:amount-from-receipt", {
4696
+ amount: delivered.amount.toString(),
4697
+ logIndex: delivered.logIndex,
4368
4698
  tokenAddress: token.contract,
4369
- depositAddressMatched: false
4699
+ depositAddressMatched: true
4370
4700
  });
4701
+ return delivered;
4371
4702
  } catch (err) {
4372
4703
  emitDebug("verification:amount-from-receipt-error", errorPayload(err));
4373
- receiptError = err;
4704
+ const errorDetail = err instanceof Error ? err.message : String(err);
4705
+ throw new Error(
4706
+ `Unable to derive delivered ${token.symbol} transfer from its receipt: ${errorDetail}`
4707
+ );
4374
4708
  }
4375
- const errorDetail = receiptError instanceof Error ? receiptError.message : receiptError === void 0 ? `no ${token.symbol} Transfer to the derived deposit address found` : String(receiptError);
4376
- throw new Error(
4377
- `Unable to derive delivered ${token.symbol} amount from its receipt: ${errorDetail}`
4378
- );
4379
4709
  }
4380
4710
  function errorPayload(err) {
4381
4711
  if (err instanceof Error) {
@@ -4655,7 +4985,12 @@ function FiatOnRampForm({
4655
4985
  onLockSubmitted,
4656
4986
  onLockFailed,
4657
4987
  onError,
4658
- onDebugEvent
4988
+ onLeaveFlow,
4989
+ onDebugEvent,
4990
+ onlyRouteActiveIntentCallbacks = false,
4991
+ frozenToken,
4992
+ onUnsafeToCloseChange,
4993
+ onActiveFlowChange
4659
4994
  }) {
4660
4995
  const { address } = wagmi.useAccount();
4661
4996
  const [visible, setVisible] = react.useState(false);
@@ -4663,6 +4998,14 @@ function FiatOnRampForm({
4663
4998
  const [rowError, setRowError] = react.useState(null);
4664
4999
  const [lockError, setLockError] = react.useState(null);
4665
5000
  const [lockSettled, setLockSettled] = react.useState(false);
5001
+ const [ownedTerminal, setOwnedTerminal] = react.useState(false);
5002
+ const ownedIntentIdRef = react.useRef(null);
5003
+ const ownsRecord = react.useCallback(
5004
+ (record) => Boolean(
5005
+ ownedIntentIdRef.current && matchesOnRampTransaction(record, ownedIntentIdRef.current)
5006
+ ),
5007
+ []
5008
+ );
4666
5009
  const {
4667
5010
  status,
4668
5011
  activeIntentId,
@@ -4683,18 +5026,37 @@ function FiatOnRampForm({
4683
5026
  } = useFiatOnRamp({
4684
5027
  tokenId,
4685
5028
  postDepositLock,
4686
- onCredited,
4687
- // Lock callbacks aren't intent-keyed, so a resumed background row's lock
4688
- // can settle these flags while a newer purchase is still locking — a
4689
- // transient overpromise that the newer lock's own outcome then corrects.
4690
- onLockSubmitted: (response) => {
5029
+ onCredited: (depositTxHash, record) => {
5030
+ if (!onlyRouteActiveIntentCallbacks || ownsRecord(record)) {
5031
+ onCredited?.(depositTxHash);
5032
+ }
5033
+ if (ownsRecord(record) && !postDepositLock) {
5034
+ setOwnedTerminal(true);
5035
+ onActiveFlowChange?.(false);
5036
+ }
5037
+ },
5038
+ // Product modals route only the snapshotted intent. Standalone consumers
5039
+ // retain the legacy behavior of receiving recovered-row callbacks.
5040
+ onLockSubmitted: (response, record) => {
5041
+ const routesToCurrentFlow = !onlyRouteActiveIntentCallbacks || ownsRecord(record);
5042
+ if (!routesToCurrentFlow) return;
4691
5043
  setLockError(null);
4692
5044
  setLockSettled(true);
4693
5045
  onLockSubmitted?.(response);
5046
+ if (ownsRecord(record)) {
5047
+ setOwnedTerminal(true);
5048
+ onActiveFlowChange?.(false);
5049
+ }
4694
5050
  },
4695
- onLockFailed: (err) => {
5051
+ onLockFailed: (err, record) => {
5052
+ const routesToCurrentFlow = !onlyRouteActiveIntentCallbacks || ownsRecord(record);
5053
+ if (!routesToCurrentFlow) return;
4696
5054
  setLockError(err.message);
4697
5055
  onLockFailed?.(err);
5056
+ if (ownsRecord(record)) {
5057
+ setOwnedTerminal(true);
5058
+ onActiveFlowChange?.(false);
5059
+ }
4698
5060
  },
4699
5061
  onError,
4700
5062
  onDebugEvent
@@ -4723,6 +5085,8 @@ function FiatOnRampForm({
4723
5085
  }
4724
5086
  })();
4725
5087
  const isBelowMin = quoteBaseUnits !== void 0 && minDepositBaseUnits !== void 0 ? quoteBaseUnits < minDepositBaseUnits : minFiatGate !== void 0 && Number(defaultBaseCurrencyAmount) < minFiatGate;
5088
+ const minimumUnknown = minDepositBaseUnits === void 0;
5089
+ const tokenMismatch = frozenToken !== void 0 && !matchesFrozenOnRampToken(frozenToken, selectedToken);
4726
5090
  const isBusy = isPreparing || status === "awaiting-purchase";
4727
5091
  const lockPending = !!postDepositLock && status === "credited" && !lockSettled && !lockError;
4728
5092
  const isInitializing = !!address && !depositAddress;
@@ -4734,10 +5098,23 @@ function FiatOnRampForm({
4734
5098
  !depositAddress ? "deposit-address-not-loaded" : null,
4735
5099
  isBusy ? `busy:${isPreparing ? "preparing" : status}` : null,
4736
5100
  visible ? "widget-open" : null,
5101
+ minimumUnknown ? "minimum-unknown" : null,
4737
5102
  isBelowMin ? "below-minimum" : null,
5103
+ tokenMismatch ? "token-drift" : null,
4738
5104
  quoteParseFailed ? "invalid-quote-amount" : null
4739
5105
  ].filter((reason) => Boolean(reason)),
4740
- [address, depositAddress, isBelowMin, isBusy, isPreparing, quoteParseFailed, status, visible]
5106
+ [
5107
+ address,
5108
+ depositAddress,
5109
+ isBelowMin,
5110
+ isBusy,
5111
+ isPreparing,
5112
+ minimumUnknown,
5113
+ quoteParseFailed,
5114
+ status,
5115
+ tokenMismatch,
5116
+ visible
5117
+ ]
4741
5118
  );
4742
5119
  const canBuy = blockReasons.length === 0;
4743
5120
  const handleOpen = react.useCallback(async () => {
@@ -4756,6 +5133,8 @@ function FiatOnRampForm({
4756
5133
  return;
4757
5134
  }
4758
5135
  setIsPreparing(true);
5136
+ setOwnedTerminal(false);
5137
+ ownedIntentIdRef.current = null;
4759
5138
  setLockError(null);
4760
5139
  setLockSettled(false);
4761
5140
  emitFormDebug("form:open-click", {
@@ -4774,6 +5153,8 @@ function FiatOnRampForm({
4774
5153
  baseCurrencyAmount: defaultBaseCurrencyAmount,
4775
5154
  quoteCurrencyAmount
4776
5155
  });
5156
+ ownedIntentIdRef.current = intent.transaction_id;
5157
+ onActiveFlowChange?.(true);
4777
5158
  emitFormDebug("form:intent-ready", {
4778
5159
  transactionIdPresent: Boolean(intent.transaction_id),
4779
5160
  externalTransactionIdPresent: Boolean(intent.external_transaction_id)
@@ -4801,7 +5182,8 @@ function FiatOnRampForm({
4801
5182
  depositAddress,
4802
5183
  emitFormDebug,
4803
5184
  prepareOnRampIntent,
4804
- status
5185
+ status,
5186
+ onActiveFlowChange
4805
5187
  ]);
4806
5188
  const handleClose = react.useCallback(async () => {
4807
5189
  emitFormDebug("moonpay:onClose");
@@ -4816,6 +5198,11 @@ function FiatOnRampForm({
4816
5198
  const handleReady = react.useCallback(async () => {
4817
5199
  emitFormDebug("moonpay:onReady");
4818
5200
  }, [emitFormDebug]);
5201
+ const handleLeaveFlow = react.useCallback(() => {
5202
+ setVisible(false);
5203
+ onActiveFlowChange?.(false);
5204
+ onLeaveFlow?.();
5205
+ }, [onActiveFlowChange, onLeaveFlow]);
4819
5206
  const widgetElement = useMoonPayOnRampAdapter({
4820
5207
  variant,
4821
5208
  visible,
@@ -4843,6 +5230,21 @@ function FiatOnRampForm({
4843
5230
  onTransactionCreated: handleTransactionCreated,
4844
5231
  onTransactionCompleted: handleTransactionCompleted
4845
5232
  });
5233
+ const ownsActiveIntent = Boolean(
5234
+ activeIntentId && ownedIntentIdRef.current && activeIntentId === ownedIntentIdRef.current
5235
+ );
5236
+ const ownsPending = Boolean(
5237
+ ownedIntentIdRef.current && pending.some((record) => matchesOnRampTransaction(record, ownedIntentIdRef.current))
5238
+ );
5239
+ const ownedFlowActive = !ownedTerminal && (ownsActiveIntent || ownsPending || lockPending);
5240
+ react.useEffect(() => {
5241
+ onActiveFlowChange?.(ownedFlowActive);
5242
+ }, [onActiveFlowChange, ownedFlowActive]);
5243
+ const unsafeToClose = isPreparing || lockPending;
5244
+ react.useEffect(() => {
5245
+ onUnsafeToCloseChange?.(unsafeToClose);
5246
+ return () => onUnsafeToCloseChange?.(false);
5247
+ }, [onUnsafeToCloseChange, unsafeToClose]);
4846
5248
  react.useEffect(() => {
4847
5249
  if (visible && !activeIntentId && status === "idle") setVisible(false);
4848
5250
  }, [activeIntentId, status, visible]);
@@ -4858,10 +5260,8 @@ function FiatOnRampForm({
4858
5260
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-foreground text-sm font-medium", children: "Validating purchases" }),
4859
5261
  pending.map((record) => {
4860
5262
  const progress = parseFinalityProgress(finalityProgress[record.transaction_id]);
4861
- const hasProgress = !!finalityProgress[record.transaction_id];
4862
- const isStalled = !hasProgress && Date.now() / 1e3 - (record.updated_at ?? 0) > 60;
4863
5263
  const isActivelyVerifying = record.transaction_id === activeVerificationId;
4864
- const showRetry = rowError?.id === record.transaction_id || isStalled && !isActivelyVerifying;
5264
+ const showRetry = canRetryOnRampVerification(record, activeVerificationId);
4865
5265
  return /* @__PURE__ */ jsxRuntime.jsxs(
4866
5266
  "div",
4867
5267
  {
@@ -4870,7 +5270,7 @@ function FiatOnRampForm({
4870
5270
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
4871
5271
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-muted-foreground flex items-center gap-1 text-xs", children: [
4872
5272
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "size-3 animate-spin", "aria-hidden": true }),
4873
- progress ?? "Verifying\u2026"
5273
+ record.on_chain_tx_hash ? progress ?? (isActivelyVerifying ? "Verifying\u2026" : "Ready to verify") : "Waiting for provider delivery\u2026"
4874
5274
  ] }),
4875
5275
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-muted-foreground text-xs", children: [
4876
5276
  record.quote_currency_amount ?? "?",
@@ -4896,7 +5296,17 @@ function FiatOnRampForm({
4896
5296
  });
4897
5297
  }
4898
5298
  },
4899
- children: "Retry"
5299
+ children: "Retry verification"
5300
+ }
5301
+ ),
5302
+ !record.on_chain_tx_hash && /* @__PURE__ */ jsxRuntime.jsx(
5303
+ Button,
5304
+ {
5305
+ type: "button",
5306
+ variant: "outline",
5307
+ size: "sm",
5308
+ onClick: () => void refreshPending(),
5309
+ children: "Refresh delivery"
4900
5310
  }
4901
5311
  )
4902
5312
  ]
@@ -4923,6 +5333,10 @@ function FiatOnRampForm({
4923
5333
  "Buy"
4924
5334
  ] }),
4925
5335
  widgetElement,
5336
+ onLeaveFlow && ownedFlowActive && !unsafeToClose && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
5337
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", variant: "outline", onClick: handleLeaveFlow, children: "Leave checkout" }),
5338
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-xs", children: "Privana will keep this signed purchase available for exact recovery." })
5339
+ ] }),
4926
5340
  error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive text-sm", role: "alert", children: error.message }),
4927
5341
  lockError && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-destructive text-sm", role: "alert", children: [
4928
5342
  "Purchase credited to your account, but locking the funds to a service failed: ",
@@ -4933,6 +5347,8 @@ function FiatOnRampForm({
4933
5347
  minFiatGate.toFixed(2),
4934
5348
  "."
4935
5349
  ] }),
5350
+ tokenMismatch && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive text-sm", role: "alert", children: "The token configuration changed. Close this purchase and start again." }),
5351
+ !tokenMismatch && depositAddress && minimumUnknown && !error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive text-sm", role: "alert", children: "The minimum purchase amount is unavailable. Close this purchase and try again." }),
4936
5352
  isVerifying && pending.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-muted-foreground flex items-center gap-2 text-sm", children: [
4937
5353
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "size-4 animate-spin", "aria-hidden": true }),
4938
5354
  "Verifying your purchase\u2026"
@@ -4945,6 +5361,702 @@ function parseFinalityProgress(message) {
4945
5361
  return match ? `${match[1]} confirmations` : null;
4946
5362
  }
4947
5363
 
5364
+ // src/sdk/on-ramp/transak-adapter.ts
5365
+ var MAX_INTENT_ID_LENGTH = 512;
5366
+ var MAX_WIDGET_URL_LENGTH = 8192;
5367
+ var TRANSAK_IP_ATTESTATION_PATH = "/__onramp-ip-attest";
5368
+ var TRANSAK_IP_ATTESTATION_TIMEOUT_MS = 1e4;
5369
+ var TRANSAK_WIDGET_ORIGINS = /* @__PURE__ */ new Set([
5370
+ "https://global-stg.transak.com",
5371
+ "https://global.transak.com"
5372
+ ]);
5373
+ var transakOnRampAdapter = {
5374
+ provider: "transak",
5375
+ pollPendingWhileOpen: true,
5376
+ buildIntentRequest: ({ walletAddress, tokenId, chainId }) => ({
5377
+ wallet_address: walletAddress,
5378
+ token_id: tokenId,
5379
+ chain_id: chainId
5380
+ })
5381
+ };
5382
+ async function requestTransakWidgetSession({
5383
+ client,
5384
+ intentId,
5385
+ generation,
5386
+ now = Date.now,
5387
+ fetcher = globalThis.fetch
5388
+ }) {
5389
+ if (!intentId || intentId.length > MAX_INTENT_ID_LENGTH) {
5390
+ throw new Error("Transak session requires a valid on-ramp intent");
5391
+ }
5392
+ const ipAttestation = await fetchTransakIpAttestation({
5393
+ intentId,
5394
+ fetcher
5395
+ });
5396
+ const response = await client.createOnRampSession({
5397
+ transaction_id: intentId,
5398
+ ip_attestation: ipAttestation
5399
+ });
5400
+ return validateTransakWidgetSession(response, intentId, generation, now());
5401
+ }
5402
+ async function fetchTransakIpAttestation({
5403
+ intentId,
5404
+ fetcher
5405
+ }) {
5406
+ const subtleCrypto = globalThis.crypto?.subtle;
5407
+ const origin = globalThis.location?.origin;
5408
+ if (typeof fetcher !== "function" || !subtleCrypto || !origin || origin === "null") {
5409
+ throw new Error("Secure client-IP attestation is unavailable");
5410
+ }
5411
+ const digest = await subtleCrypto.digest("SHA-256", new TextEncoder().encode(intentId));
5412
+ const intentHash = Array.from(
5413
+ new Uint8Array(digest),
5414
+ (byte) => byte.toString(16).padStart(2, "0")
5415
+ ).join("");
5416
+ const controller = new AbortController();
5417
+ const timeoutId = setTimeout(() => controller.abort(), TRANSAK_IP_ATTESTATION_TIMEOUT_MS);
5418
+ try {
5419
+ let response;
5420
+ try {
5421
+ response = await fetcher(`${origin}${TRANSAK_IP_ATTESTATION_PATH}`, {
5422
+ method: "POST",
5423
+ headers: { "Content-Type": "application/json" },
5424
+ body: JSON.stringify({ intentHash }),
5425
+ cache: "no-store",
5426
+ credentials: "omit",
5427
+ redirect: "error",
5428
+ signal: controller.signal
5429
+ });
5430
+ } catch (error) {
5431
+ throw new Error("Client-IP attestation request failed", { cause: error });
5432
+ }
5433
+ if (!response.ok) {
5434
+ throw new Error(`Client-IP attestation request failed with HTTP ${response.status}`);
5435
+ }
5436
+ try {
5437
+ return await response.json();
5438
+ } catch (error) {
5439
+ throw new Error("Client-IP attestation response is malformed", { cause: error });
5440
+ }
5441
+ } finally {
5442
+ clearTimeout(timeoutId);
5443
+ }
5444
+ }
5445
+ function validateTransakWidgetSession(response, intentId, generation, now = Date.now()) {
5446
+ if (!isPlainRecord2(response) || response.provider !== "transak") {
5447
+ throw new Error("On-ramp session provider does not match Transak");
5448
+ }
5449
+ if (!Number.isSafeInteger(response.expires_at) || response.expires_at * 1e3 <= now) {
5450
+ throw new Error("Transak session is expired or malformed");
5451
+ }
5452
+ const origin = validateTransakWidgetUrl(response.url);
5453
+ return {
5454
+ provider: "transak",
5455
+ url: response.url,
5456
+ origin,
5457
+ expiresAt: response.expires_at,
5458
+ intentId,
5459
+ generation
5460
+ };
5461
+ }
5462
+ function isTransakWidgetSessionLoadable(session, now = Date.now()) {
5463
+ return session.expiresAt * 1e3 > now;
5464
+ }
5465
+ function resolveTransakWidgetMessage({
5466
+ message,
5467
+ iframeWindow,
5468
+ session,
5469
+ currentGeneration
5470
+ }) {
5471
+ if (session.generation !== currentGeneration) return null;
5472
+ if (!iframeWindow || message.source !== iframeWindow) return null;
5473
+ if (message.origin !== session.origin) return null;
5474
+ return normalizeTransakWidgetMessage(message.data, session.intentId);
5475
+ }
5476
+ function normalizeTransakWidgetMessage(data, intentId) {
5477
+ if (!isPlainRecord2(data) || typeof data.event_id !== "string") return null;
5478
+ switch (data.event_id) {
5479
+ case "TRANSAK_WIDGET_INITIALISED":
5480
+ case "TRANSAK_WIDGET_OPEN":
5481
+ return { type: "ready" };
5482
+ case "TRANSAK_ORDER_CREATED":
5483
+ return {
5484
+ type: "provider-event",
5485
+ event: {
5486
+ provider: "transak",
5487
+ kind: "transaction-created",
5488
+ providerTransactionId: intentId,
5489
+ intentId
5490
+ }
5491
+ };
5492
+ case "TRANSAK_ORDER_SUCCESSFUL":
5493
+ return {
5494
+ type: "provider-event",
5495
+ event: {
5496
+ provider: "transak",
5497
+ kind: "transaction-completed",
5498
+ providerTransactionId: intentId,
5499
+ intentId
5500
+ }
5501
+ };
5502
+ case "TRANSAK_ORDER_CANCELLED":
5503
+ case "TRANSAK_ORDER_FAILED":
5504
+ return { type: "refresh" };
5505
+ case "TRANSAK_WIDGET_CLOSE":
5506
+ return { type: "close" };
5507
+ default:
5508
+ return null;
5509
+ }
5510
+ }
5511
+ function validateTransakWidgetUrl(url) {
5512
+ if (typeof url !== "string" || url.length === 0 || url.length > MAX_WIDGET_URL_LENGTH || !/^[\x21-\x7e]+$/.test(url) || url.includes("\\")) {
5513
+ throw new Error("Transak widget URL is malformed");
5514
+ }
5515
+ let parsed;
5516
+ try {
5517
+ parsed = new URL(url);
5518
+ } catch {
5519
+ throw new Error("Transak widget URL is malformed");
5520
+ }
5521
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password) {
5522
+ throw new Error("Transak widget URL is malformed");
5523
+ }
5524
+ if (!TRANSAK_WIDGET_ORIGINS.has(parsed.origin)) {
5525
+ throw new Error("Transak widget URL origin is not allowed");
5526
+ }
5527
+ return parsed.origin;
5528
+ }
5529
+ function isPlainRecord2(value) {
5530
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
5531
+ const prototype = Object.getPrototypeOf(value);
5532
+ return prototype === Object.prototype || prototype === null;
5533
+ }
5534
+ function isTransakWidgetFrameRenderable(session, loadedGeneration, now = Date.now()) {
5535
+ return loadedGeneration === session.generation || isTransakWidgetSessionLoadable(session, now);
5536
+ }
5537
+ function TransakWidgetFrame({
5538
+ session,
5539
+ getCurrentGeneration,
5540
+ shouldPollPending,
5541
+ refreshPending,
5542
+ onReady,
5543
+ onAction,
5544
+ onExpired,
5545
+ title = "Transak secure checkout",
5546
+ className
5547
+ }) {
5548
+ const iframeRef = react.useRef(null);
5549
+ const loadedGenerationRef = react.useRef(null);
5550
+ const renderable = isTransakWidgetFrameRenderable(session, loadedGenerationRef.current);
5551
+ const readyRef = react.useRef(false);
5552
+ const expiryTimerRef = react.useRef(null);
5553
+ const callbacksRef = react.useRef({
5554
+ getCurrentGeneration,
5555
+ refreshPending,
5556
+ onReady,
5557
+ onAction,
5558
+ onExpired
5559
+ });
5560
+ callbacksRef.current = {
5561
+ getCurrentGeneration,
5562
+ refreshPending,
5563
+ onReady,
5564
+ onAction,
5565
+ onExpired
5566
+ };
5567
+ const expireUnloadedSession = react.useCallback(() => {
5568
+ if (loadedGenerationRef.current === session.generation) return;
5569
+ callbacksRef.current.onExpired(session.generation);
5570
+ }, [session.generation]);
5571
+ react.useEffect(() => {
5572
+ loadedGenerationRef.current = null;
5573
+ readyRef.current = false;
5574
+ const remaining = session.expiresAt * 1e3 - Date.now();
5575
+ if (remaining <= 0) {
5576
+ expireUnloadedSession();
5577
+ return;
5578
+ }
5579
+ expiryTimerRef.current = setTimeout(expireUnloadedSession, remaining);
5580
+ return () => {
5581
+ if (expiryTimerRef.current) clearTimeout(expiryTimerRef.current);
5582
+ expiryTimerRef.current = null;
5583
+ };
5584
+ }, [expireUnloadedSession, session.expiresAt]);
5585
+ react.useEffect(() => {
5586
+ const handleMessage = (message) => {
5587
+ const action = resolveTransakWidgetMessage({
5588
+ message,
5589
+ iframeWindow: iframeRef.current?.contentWindow ?? null,
5590
+ session,
5591
+ currentGeneration: callbacksRef.current.getCurrentGeneration()
5592
+ });
5593
+ if (!action) return;
5594
+ loadedGenerationRef.current = session.generation;
5595
+ if (expiryTimerRef.current) clearTimeout(expiryTimerRef.current);
5596
+ expiryTimerRef.current = null;
5597
+ void callbacksRef.current.onAction(session.generation, action);
5598
+ };
5599
+ window.addEventListener("message", handleMessage);
5600
+ return () => window.removeEventListener("message", handleMessage);
5601
+ }, [session]);
5602
+ react.useEffect(() => {
5603
+ if (!renderable || !transakOnRampAdapter.pollPendingWhileOpen || !shouldPollPending) return;
5604
+ const id = setInterval(
5605
+ () => void callbacksRef.current.refreshPending(),
5606
+ MIN_ONRAMP_PENDING_REQUEST_INTERVAL_MS
5607
+ );
5608
+ return () => clearInterval(id);
5609
+ }, [renderable, shouldPollPending]);
5610
+ const handleLoad = react.useCallback(() => {
5611
+ if (!isTransakWidgetSessionLoadable(session)) {
5612
+ expireUnloadedSession();
5613
+ return;
5614
+ }
5615
+ loadedGenerationRef.current = session.generation;
5616
+ if (expiryTimerRef.current) clearTimeout(expiryTimerRef.current);
5617
+ expiryTimerRef.current = null;
5618
+ if (readyRef.current) return;
5619
+ readyRef.current = true;
5620
+ callbacksRef.current.onReady(session.generation);
5621
+ }, [expireUnloadedSession, session]);
5622
+ if (!renderable) return null;
5623
+ return /* @__PURE__ */ jsxRuntime.jsx(
5624
+ "iframe",
5625
+ {
5626
+ ref: iframeRef,
5627
+ src: session.url,
5628
+ title,
5629
+ className,
5630
+ allow: "camera; microphone; payment",
5631
+ referrerPolicy: "strict-origin-when-cross-origin",
5632
+ onLoad: handleLoad,
5633
+ "data-privana-transak-widget": true
5634
+ }
5635
+ );
5636
+ }
5637
+ function transitionTransakSessionRecreation(state, event) {
5638
+ if (event === "reset") return "none";
5639
+ if (event === "intent-created") return "preload-only";
5640
+ if (state === "none") return "none";
5641
+ return "blocked";
5642
+ }
5643
+ function getMountedTransakSessionError(action, hasMountedSession) {
5644
+ if (!hasMountedSession) return null;
5645
+ return action === "launch" ? new Error("Close the current Transak checkout before launching another") : new Error("Close or expire the current Transak checkout before reopening");
5646
+ }
5647
+ function getTransakRecreateSessionError({
5648
+ hasMountedSession,
5649
+ activeIntentId,
5650
+ canRecreateSession
5651
+ }) {
5652
+ const mountedError = getMountedTransakSessionError("reopen", hasMountedSession);
5653
+ if (mountedError) return mountedError;
5654
+ if (!activeIntentId) {
5655
+ return new Error("No unresolved Transak intent is available to reopen");
5656
+ }
5657
+ if (!canRecreateSession) {
5658
+ return new Error(
5659
+ "This Transak purchase may already be in progress; continue recovery instead of reopening checkout"
5660
+ );
5661
+ }
5662
+ return null;
5663
+ }
5664
+ function getTransakSessionContextError({
5665
+ currentGeneration,
5666
+ expectedGeneration,
5667
+ scopeChanged
5668
+ }) {
5669
+ if (currentGeneration !== expectedGeneration) {
5670
+ return new Error("Transak checkout request was cancelled");
5671
+ }
5672
+ if (scopeChanged) {
5673
+ return new Error("On-ramp account or network changed while creating the session");
5674
+ }
5675
+ return null;
5676
+ }
5677
+ function getTransakSessionRequestError({
5678
+ currentGeneration,
5679
+ expectedGeneration,
5680
+ scopeChanged,
5681
+ activeIntentId,
5682
+ expectedIntentId
5683
+ }) {
5684
+ const contextError = getTransakSessionContextError({
5685
+ currentGeneration,
5686
+ expectedGeneration,
5687
+ scopeChanged
5688
+ });
5689
+ if (contextError) return contextError;
5690
+ if (activeIntentId !== expectedIntentId) {
5691
+ return new Error("The Transak purchase intent is no longer active");
5692
+ }
5693
+ return null;
5694
+ }
5695
+ function shouldSurfaceTransakSessionFailure({
5696
+ status,
5697
+ activeIntentId,
5698
+ activeVerificationId,
5699
+ activeVerificationRecord,
5700
+ expectedIntentId
5701
+ }) {
5702
+ if (!expectedIntentId || activeIntentId !== expectedIntentId) return false;
5703
+ if (status === "awaiting-delivery") return false;
5704
+ const ownsActiveVerification = activeVerificationRecord ? matchesOnRampTransaction(activeVerificationRecord, expectedIntentId) : activeVerificationId === expectedIntentId;
5705
+ return !(ownsActiveVerification && (status === "verifying" || status === "credited"));
5706
+ }
5707
+ function hasTransakProviderEvidence({
5708
+ status,
5709
+ activeIntentId,
5710
+ pending,
5711
+ activeVerificationRecord
5712
+ }) {
5713
+ if (!activeIntentId) return false;
5714
+ const ownsRecord = (record) => matchesOnRampTransaction(record, activeIntentId);
5715
+ return pending.some(ownsRecord) || activeVerificationRecord !== null && ownsRecord(activeVerificationRecord) || status === "awaiting-delivery" || status === "credited";
5716
+ }
5717
+ function useTransakOnRamp(options) {
5718
+ const { iframeTitle, iframeClassName, ...coreOptions } = options;
5719
+ const { executePrivateRead, privateReadQueryScope } = usePrivateReadRequest();
5720
+ const core = useOnRamp({ ...coreOptions, adapter: transakOnRampAdapter });
5721
+ const {
5722
+ prepareOnRampIntent,
5723
+ handleProviderLaunchReady,
5724
+ handleProviderLaunchFailed,
5725
+ handleProviderEvent,
5726
+ handleProviderClosed,
5727
+ refreshPending
5728
+ } = core;
5729
+ const [session, setSessionState] = react.useState(null);
5730
+ const [isLaunching, setIsLaunching] = react.useState(false);
5731
+ const [sessionRecreationState, setSessionRecreationState] = react.useState("none");
5732
+ const sessionRecreationStateRef = react.useRef("none");
5733
+ const sessionRef = react.useRef(null);
5734
+ const mountedSessionScopeRef = react.useRef(null);
5735
+ const generationRef = react.useRef(0);
5736
+ const sessionRequestRef = react.useRef(null);
5737
+ const activeIntentIdRef = react.useRef(core.activeIntentId);
5738
+ activeIntentIdRef.current = core.activeIntentId;
5739
+ const activeVerificationIdRef = react.useRef(core.activeVerificationId);
5740
+ activeVerificationIdRef.current = core.activeVerificationId;
5741
+ const activeVerificationRecord = react.useMemo(
5742
+ () => core.activeVerificationId ? core.pending.find((record) => record.transaction_id === core.activeVerificationId) ?? null : null,
5743
+ [core.activeVerificationId, core.pending]
5744
+ );
5745
+ const activeVerificationRecordRef = react.useRef(activeVerificationRecord);
5746
+ activeVerificationRecordRef.current = activeVerificationRecord;
5747
+ const pendingRef = react.useRef(core.pending);
5748
+ pendingRef.current = core.pending;
5749
+ const coreStatusRef = react.useRef(core.status);
5750
+ coreStatusRef.current = core.status;
5751
+ const [apiUrl, chainId, privateReadAddress] = privateReadQueryScope;
5752
+ const scopeIdentity = [
5753
+ apiUrl,
5754
+ chainId,
5755
+ privateReadAddress?.toLowerCase() ?? "",
5756
+ coreOptions.tokenId.toLowerCase(),
5757
+ getOnRampTokenFingerprint(core.selectedToken)
5758
+ ].join("\0");
5759
+ const scopeSession = react.useMemo(() => Symbol(scopeIdentity), [scopeIdentity]);
5760
+ const scopeSessionRef = react.useRef(scopeSession);
5761
+ scopeSessionRef.current = scopeSession;
5762
+ const setSession = react.useCallback((next, nextScope) => {
5763
+ sessionRef.current = next;
5764
+ mountedSessionScopeRef.current = nextScope;
5765
+ setSessionState(next);
5766
+ }, []);
5767
+ const updateSessionRecreation = react.useCallback((event) => {
5768
+ const next = transitionTransakSessionRecreation(sessionRecreationStateRef.current, event);
5769
+ sessionRecreationStateRef.current = next;
5770
+ setSessionRecreationState(next);
5771
+ }, []);
5772
+ const hasProviderEvidence = hasTransakProviderEvidence({
5773
+ status: core.status,
5774
+ activeIntentId: core.activeIntentId,
5775
+ pending: core.pending,
5776
+ activeVerificationRecord
5777
+ });
5778
+ const canRecreateSession = sessionRecreationState === "preload-only" && !hasProviderEvidence;
5779
+ const requestSession = react.useCallback(
5780
+ (intentId, generation) => executePrivateRead(
5781
+ (readClient) => requestTransakWidgetSession({
5782
+ client: readClient,
5783
+ intentId,
5784
+ generation
5785
+ })
5786
+ ),
5787
+ [executePrivateRead]
5788
+ );
5789
+ const launch = react.useCallback(
5790
+ (request) => {
5791
+ const currentRequest = sessionRequestRef.current;
5792
+ if (currentRequest?.generation === generationRef.current) return currentRequest.promise;
5793
+ const mountedSessionError = getMountedTransakSessionError(
5794
+ "launch",
5795
+ sessionRef.current !== null
5796
+ );
5797
+ if (mountedSessionError) return Promise.reject(mountedSessionError);
5798
+ const generation = ++generationRef.current;
5799
+ setIsLaunching(true);
5800
+ updateSessionRecreation("reset");
5801
+ let launchedIntentId = null;
5802
+ const pendingRequest = (async () => {
5803
+ try {
5804
+ const intent = await prepareOnRampIntent({
5805
+ providerAssetCode: request.providerAssetCode,
5806
+ quoteCurrencyAmount: request.quoteCurrencyAmount
5807
+ });
5808
+ launchedIntentId = intent.transaction_id;
5809
+ updateSessionRecreation("intent-created");
5810
+ const beforeSessionError = getTransakSessionContextError({
5811
+ currentGeneration: generationRef.current,
5812
+ expectedGeneration: generation,
5813
+ scopeChanged: scopeSessionRef.current !== scopeSession
5814
+ });
5815
+ if (beforeSessionError) throw beforeSessionError;
5816
+ const next = await requestSession(intent.transaction_id, generation);
5817
+ const afterSessionError = getTransakSessionRequestError({
5818
+ currentGeneration: generationRef.current,
5819
+ expectedGeneration: generation,
5820
+ scopeChanged: scopeSessionRef.current !== scopeSession,
5821
+ activeIntentId: activeIntentIdRef.current,
5822
+ expectedIntentId: intent.transaction_id
5823
+ });
5824
+ if (afterSessionError) throw afterSessionError;
5825
+ setSession(next, scopeSession);
5826
+ } catch (error) {
5827
+ const launchError = error instanceof Error ? error : new Error("Failed to launch Transak checkout");
5828
+ if (generationRef.current === generation && shouldSurfaceTransakSessionFailure({
5829
+ status: coreStatusRef.current,
5830
+ activeIntentId: activeIntentIdRef.current,
5831
+ activeVerificationId: activeVerificationIdRef.current,
5832
+ activeVerificationRecord: activeVerificationRecordRef.current,
5833
+ expectedIntentId: launchedIntentId
5834
+ })) {
5835
+ handleProviderLaunchFailed(launchError);
5836
+ }
5837
+ throw launchError;
5838
+ } finally {
5839
+ if (generationRef.current === generation) setIsLaunching(false);
5840
+ if (sessionRequestRef.current?.generation === generation) {
5841
+ sessionRequestRef.current = null;
5842
+ }
5843
+ }
5844
+ })();
5845
+ sessionRequestRef.current = { generation, promise: pendingRequest };
5846
+ return pendingRequest;
5847
+ },
5848
+ [
5849
+ handleProviderLaunchFailed,
5850
+ prepareOnRampIntent,
5851
+ requestSession,
5852
+ scopeSession,
5853
+ setSession,
5854
+ updateSessionRecreation
5855
+ ]
5856
+ );
5857
+ const recreateSession = react.useCallback(() => {
5858
+ const currentRequest = sessionRequestRef.current;
5859
+ if (currentRequest?.generation === generationRef.current) return currentRequest.promise;
5860
+ const intentId = activeIntentIdRef.current;
5861
+ const hasProviderEvidence2 = hasTransakProviderEvidence({
5862
+ status: coreStatusRef.current,
5863
+ activeIntentId: intentId,
5864
+ pending: pendingRef.current,
5865
+ activeVerificationRecord: activeVerificationRecordRef.current
5866
+ });
5867
+ const recreateError = getTransakRecreateSessionError({
5868
+ hasMountedSession: sessionRef.current !== null,
5869
+ activeIntentId: intentId,
5870
+ canRecreateSession: sessionRecreationStateRef.current === "preload-only" && !hasProviderEvidence2
5871
+ });
5872
+ if (recreateError) return Promise.reject(recreateError);
5873
+ if (!intentId) return Promise.reject(new Error("No unresolved Transak intent is available"));
5874
+ const generation = ++generationRef.current;
5875
+ setIsLaunching(true);
5876
+ const pendingRequest = (async () => {
5877
+ try {
5878
+ const next = await requestSession(intentId, generation);
5879
+ const requestError = getTransakSessionRequestError({
5880
+ currentGeneration: generationRef.current,
5881
+ expectedGeneration: generation,
5882
+ scopeChanged: scopeSessionRef.current !== scopeSession,
5883
+ activeIntentId: activeIntentIdRef.current,
5884
+ expectedIntentId: intentId
5885
+ });
5886
+ if (requestError) throw requestError;
5887
+ setSession(next, scopeSession);
5888
+ } catch (error) {
5889
+ const launchError = error instanceof Error ? error : new Error("Failed to recreate Transak checkout");
5890
+ if (generationRef.current === generation && shouldSurfaceTransakSessionFailure({
5891
+ status: coreStatusRef.current,
5892
+ activeIntentId: activeIntentIdRef.current,
5893
+ activeVerificationId: activeVerificationIdRef.current,
5894
+ activeVerificationRecord: activeVerificationRecordRef.current,
5895
+ expectedIntentId: intentId
5896
+ })) {
5897
+ handleProviderLaunchFailed(launchError);
5898
+ }
5899
+ throw launchError;
5900
+ } finally {
5901
+ if (generationRef.current === generation) setIsLaunching(false);
5902
+ if (sessionRequestRef.current?.generation === generation) {
5903
+ sessionRequestRef.current = null;
5904
+ }
5905
+ }
5906
+ })();
5907
+ sessionRequestRef.current = { generation, promise: pendingRequest };
5908
+ return pendingRequest;
5909
+ }, [handleProviderLaunchFailed, requestSession, scopeSession, setSession]);
5910
+ const closeWidget = react.useCallback(async () => {
5911
+ generationRef.current++;
5912
+ const shouldReconcile = Boolean(sessionRef.current || activeIntentIdRef.current);
5913
+ setSession(null, null);
5914
+ setIsLaunching(false);
5915
+ if (shouldReconcile) await handleProviderClosed();
5916
+ }, [handleProviderClosed, setSession]);
5917
+ const getCurrentGeneration = react.useCallback(() => generationRef.current, []);
5918
+ const handleFrameReady = react.useCallback(
5919
+ (generation) => {
5920
+ const current = sessionRef.current;
5921
+ if (!current || current.generation !== generation || generationRef.current !== generation)
5922
+ return;
5923
+ updateSessionRecreation("provider-ui-activated");
5924
+ handleProviderLaunchReady();
5925
+ },
5926
+ [handleProviderLaunchReady, updateSessionRecreation]
5927
+ );
5928
+ const handleFrameExpired = react.useCallback(
5929
+ (generation) => {
5930
+ const current = sessionRef.current;
5931
+ if (!current || current.generation !== generation || generationRef.current !== generation)
5932
+ return;
5933
+ const shouldSurfaceFailure = shouldSurfaceTransakSessionFailure({
5934
+ status: coreStatusRef.current,
5935
+ activeIntentId: activeIntentIdRef.current,
5936
+ activeVerificationId: activeVerificationIdRef.current,
5937
+ activeVerificationRecord: activeVerificationRecordRef.current,
5938
+ expectedIntentId: current.intentId
5939
+ });
5940
+ generationRef.current++;
5941
+ setSession(null, null);
5942
+ if (shouldSurfaceFailure) {
5943
+ handleProviderLaunchFailed(
5944
+ new Error(
5945
+ "Transak checkout session expired before it could be loaded; reopen to continue"
5946
+ )
5947
+ );
5948
+ }
5949
+ },
5950
+ [handleProviderLaunchFailed, setSession]
5951
+ );
5952
+ const handleFrameAction = react.useCallback(
5953
+ async (generation, action) => {
5954
+ const current = sessionRef.current;
5955
+ if (!current || current.generation !== generation || generationRef.current !== generation)
5956
+ return;
5957
+ updateSessionRecreation("provider-ui-activated");
5958
+ switch (action.type) {
5959
+ case "ready":
5960
+ handleProviderLaunchReady();
5961
+ return;
5962
+ case "refresh":
5963
+ await refreshPending();
5964
+ return;
5965
+ case "provider-event":
5966
+ await handleProviderEvent(action.event);
5967
+ return;
5968
+ case "close":
5969
+ await closeWidget();
5970
+ }
5971
+ },
5972
+ [
5973
+ closeWidget,
5974
+ handleProviderEvent,
5975
+ handleProviderLaunchReady,
5976
+ refreshPending,
5977
+ updateSessionRecreation
5978
+ ]
5979
+ );
5980
+ react.useEffect(() => {
5981
+ const current = sessionRef.current;
5982
+ if (!current || core.activeIntentId === current.intentId) return;
5983
+ generationRef.current++;
5984
+ setSession(null, null);
5985
+ }, [core.activeIntentId, setSession]);
5986
+ react.useEffect(() => {
5987
+ if (!core.activeIntentId) {
5988
+ updateSessionRecreation("reset");
5989
+ return;
5990
+ }
5991
+ if (hasProviderEvidence) {
5992
+ updateSessionRecreation("provider-evidence");
5993
+ }
5994
+ }, [core.activeIntentId, hasProviderEvidence, updateSessionRecreation]);
5995
+ react.useEffect(() => {
5996
+ generationRef.current++;
5997
+ setSession(null, null);
5998
+ setIsLaunching(false);
5999
+ updateSessionRecreation("reset");
6000
+ }, [scopeSession, setSession, updateSessionRecreation]);
6001
+ react.useEffect(
6002
+ () => () => {
6003
+ generationRef.current++;
6004
+ sessionRef.current = null;
6005
+ mountedSessionScopeRef.current = null;
6006
+ },
6007
+ []
6008
+ );
6009
+ const scopedSession = mountedSessionScopeRef.current === scopeSession ? session : null;
6010
+ const widget = react.useMemo(
6011
+ () => scopedSession ? /* @__PURE__ */ jsxRuntime.jsx(
6012
+ TransakWidgetFrame,
6013
+ {
6014
+ session: scopedSession,
6015
+ getCurrentGeneration,
6016
+ shouldPollPending: core.status === "awaiting-purchase",
6017
+ refreshPending,
6018
+ onReady: handleFrameReady,
6019
+ onAction: handleFrameAction,
6020
+ onExpired: handleFrameExpired,
6021
+ title: iframeTitle,
6022
+ className: iframeClassName
6023
+ },
6024
+ scopedSession.generation
6025
+ ) : null,
6026
+ [
6027
+ core.status,
6028
+ getCurrentGeneration,
6029
+ handleFrameAction,
6030
+ handleFrameExpired,
6031
+ handleFrameReady,
6032
+ iframeClassName,
6033
+ iframeTitle,
6034
+ refreshPending,
6035
+ scopedSession
6036
+ ]
6037
+ );
6038
+ return {
6039
+ status: core.status,
6040
+ activeIntentId: core.activeIntentId,
6041
+ pending: core.pending,
6042
+ activeVerificationId: core.activeVerificationId,
6043
+ error: core.error,
6044
+ finalityProgress: core.finalityProgress,
6045
+ depositAddress: core.depositAddress,
6046
+ minDepositBaseUnits: core.minDepositBaseUnits,
6047
+ selectedToken: core.selectedToken,
6048
+ finishPendingVerification: core.finishPendingVerification,
6049
+ refreshPending,
6050
+ isLaunching,
6051
+ isWidgetOpen: scopedSession !== null,
6052
+ canRecreateSession,
6053
+ widget,
6054
+ launch,
6055
+ recreateSession,
6056
+ closeWidget
6057
+ };
6058
+ }
6059
+
4948
6060
  exports.AccountingApiError = AccountingApiError;
4949
6061
  exports.Button = Button;
4950
6062
  exports.DEFAULT_LOCK_DURATION_SECONDS = DEFAULT_LOCK_DURATION_SECONDS;
@@ -4975,6 +6087,7 @@ exports.applyRefreshResponse = applyRefreshResponse;
4975
6087
  exports.buildHostedAuthSession = buildHostedAuthSession;
4976
6088
  exports.buildSiweStatement = buildSiweStatement;
4977
6089
  exports.buttonVariants = buttonVariants;
6090
+ exports.canRetryOnRampVerification = canRetryOnRampVerification;
4978
6091
  exports.canUseBrowserStorage = canUseBrowserStorage;
4979
6092
  exports.canUseSharedBrowserStorage = canUseSharedBrowserStorage;
4980
6093
  exports.clampLockAmount = clampLockAmount;
@@ -4988,6 +6101,8 @@ exports.createHostedAuthStorageKey = createHostedAuthStorageKey;
4988
6101
  exports.createLockExpiry = createLockExpiry;
4989
6102
  exports.createPkceChallenge = createPkceChallenge;
4990
6103
  exports.createPkceVerifier = createPkceVerifier;
6104
+ exports.createProductOnRampFlowSnapshot = createProductOnRampFlowSnapshot;
6105
+ exports.createProductOnRampOutcomeCallbacks = createProductOnRampOutcomeCallbacks;
4991
6106
  exports.createSignedLockRequest = createSignedLockRequest;
4992
6107
  exports.formatCountdown = formatCountdown;
4993
6108
  exports.formatTimeRemaining = formatTimeRemaining;
@@ -5002,11 +6117,16 @@ exports.getExplorerAddressUrl = getExplorerAddressUrl;
5002
6117
  exports.getExplorerLabel = getExplorerLabel;
5003
6118
  exports.getSharedBrowserStorageItem = getSharedBrowserStorageItem;
5004
6119
  exports.getTransactionReceipt = getTransactionReceipt;
6120
+ exports.getTransakMinimumTargetBaseUnits = getTransakMinimumTargetBaseUnits;
5005
6121
  exports.getWalletClient = getWalletClient3;
5006
6122
  exports.isHostedAuthRefreshActive = isHostedAuthRefreshActive;
5007
6123
  exports.isHostedAuthSessionActive = isHostedAuthSessionActive;
6124
+ exports.isMoonPayProductOnRamp = isMoonPayProductOnRamp;
5008
6125
  exports.isSignedLockUsable = isSignedLockUsable;
5009
6126
  exports.loadPendingLock = loadPendingLock;
6127
+ exports.matchesFrozenOnRampToken = matchesFrozenOnRampToken;
6128
+ exports.matchesOnRampTransaction = matchesOnRampTransaction;
6129
+ exports.matchesProductOnRampScope = matchesProductOnRampScope;
5010
6130
  exports.normalizeAddress = normalizeAddress;
5011
6131
  exports.normalizeHex = normalizeHex;
5012
6132
  exports.parseHostedAuthCallback = parseHostedAuthCallback;
@@ -5018,6 +6138,7 @@ exports.removeBrowserStorageItem = removeBrowserStorageItem;
5018
6138
  exports.removeSharedBrowserStorageItem = removeSharedBrowserStorageItem;
5019
6139
  exports.requireDepositLockOwner = requireDepositLockOwner;
5020
6140
  exports.requireServiceAddress = requireServiceAddress;
6141
+ exports.resolveProductOnRamp = resolveProductOnRamp;
5021
6142
  exports.savePendingLock = savePendingLock;
5022
6143
  exports.setBrowserStorageItem = setBrowserStorageItem;
5023
6144
  exports.setSharedBrowserStorageItem = setSharedBrowserStorageItem;
@@ -5039,6 +6160,7 @@ exports.usePrivateReadRequest = usePrivateReadRequest;
5039
6160
  exports.useSafeAccount = useSafeAccount;
5040
6161
  exports.useSafePrivanaContext = useSafePrivanaContext;
5041
6162
  exports.useSiweAuth = useSiweAuth;
6163
+ exports.useTransakOnRamp = useTransakOnRamp;
5042
6164
  exports.waitForTransactionReceipt = waitForTransactionReceipt;
5043
- //# sourceMappingURL=chunk-FFYLKZ7J.cjs.map
5044
- //# sourceMappingURL=chunk-FFYLKZ7J.cjs.map
6165
+ //# sourceMappingURL=chunk-EXFISDHQ.cjs.map
6166
+ //# sourceMappingURL=chunk-EXFISDHQ.cjs.map