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