@oasisprotocol/privana-sdk 0.5.3 → 0.5.5

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, parseUnits, decodeEventLog, formatUnits, createClient, custom } from 'viem';
7
+ import { parseAbiItem, zeroAddress, walletActions, hexToString, formatUnits, parseUnits, 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';
@@ -35,7 +35,7 @@ var config_default = {
35
35
  testnet: {
36
36
  chainId: 23295,
37
37
  name: "Sapphire Testnet",
38
- accountingContract: "0xad3C76e4E621C0cfF7540479Ee9B0A945723A642",
38
+ accountingContract: "0x910CFfe4e8B27bc367F6E34D6D8e3C31DED68B6f",
39
39
  apiUrl: "https://api.testnet.privana.finance"
40
40
  },
41
41
  mainnet: {
@@ -620,9 +620,7 @@ var PrivanaClient = class _PrivanaClient {
620
620
  wallet_address: request.wallet_address ? normalizeAddress(request.wallet_address) : void 0,
621
621
  token_id: normalizeHex(request.token_id),
622
622
  chain_id: request.chain_id,
623
- moonpay_currency_code: request.moonpay_currency_code,
624
- base_currency_code: request.base_currency_code,
625
- base_currency_amount: request.base_currency_amount
623
+ moonpay_currency_code: request.moonpay_currency_code
626
624
  });
627
625
  }
628
626
  async updateOnRamp(transactionId, request) {
@@ -641,8 +639,15 @@ var PrivanaClient = class _PrivanaClient {
641
639
  }
642
640
  );
643
641
  }
644
- async getPendingOnRamps() {
645
- return this.http.get("/v1/accounting/onramp/pending");
642
+ async getPendingOnRamps(externalTransactionIds = []) {
643
+ const params = new URLSearchParams();
644
+ for (const transactionId of externalTransactionIds.slice(0, 10)) {
645
+ params.append("externalTransactionId", transactionId);
646
+ }
647
+ const query = params.toString();
648
+ return this.http.get(
649
+ `/v1/accounting/onramp/pending${query ? `?${query}` : ""}`
650
+ );
646
651
  }
647
652
  /**
648
653
  * @deprecated This mutates the shared client's headers and displaces its Authorization
@@ -2222,6 +2227,49 @@ function loadPendingLock(userAddress, correlationId) {
2222
2227
  function clearPendingLock(userAddress, correlationId) {
2223
2228
  removeBrowserStorageItem(pendingLockKey(userAddress, correlationId));
2224
2229
  }
2230
+
2231
+ // src/sdk/hooks/deposit-finality.ts
2232
+ async function checkDepositWithFinalityRetry({
2233
+ checkDeposit,
2234
+ isStale,
2235
+ onRetry,
2236
+ timeoutMs,
2237
+ retryIntervalMs,
2238
+ startedAt = Date.now(),
2239
+ now = Date.now,
2240
+ sleep = defaultSleep
2241
+ }) {
2242
+ while (true) {
2243
+ if (isStale()) return { kind: "stale" };
2244
+ try {
2245
+ const response = await checkDeposit();
2246
+ if (!isInsufficientFinalityMessage(response.detail) || response.status !== "error") {
2247
+ return { kind: "response", response };
2248
+ }
2249
+ if (response.detail) onRetry(response.detail);
2250
+ } catch (error) {
2251
+ if (isStale()) return { kind: "stale" };
2252
+ if (!isInsufficientFinalityError(error)) throw error;
2253
+ onRetry(
2254
+ error instanceof AccountingApiError && error.detail ? error.detail : error instanceof Error ? error.message : String(error)
2255
+ );
2256
+ }
2257
+ if (now() - startedAt > timeoutMs) return { kind: "timeout" };
2258
+ await sleep(retryIntervalMs);
2259
+ }
2260
+ }
2261
+ function isInsufficientFinalityError(error) {
2262
+ if (error instanceof AccountingApiError) {
2263
+ return isInsufficientFinalityMessage(error.detail) || isInsufficientFinalityMessage(error.message);
2264
+ }
2265
+ return error instanceof Error && isInsufficientFinalityMessage(error.message);
2266
+ }
2267
+ function isInsufficientFinalityMessage(message) {
2268
+ return message?.includes("Insufficient finality") ?? false;
2269
+ }
2270
+ function defaultSleep(milliseconds) {
2271
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
2272
+ }
2225
2273
  var INITIAL_AUTH_BACKOFF_MS = 5e3;
2226
2274
  var MAX_AUTH_BACKOFF_MS = 6e4;
2227
2275
  var privateReadFailureCache = /* @__PURE__ */ new Map();
@@ -2461,44 +2509,27 @@ function useDepositVerification(options = {}) {
2461
2509
  queryClient.invalidateQueries({ queryKey: ["accounting-balance"] });
2462
2510
  };
2463
2511
  try {
2464
- let triggerResult;
2465
- while (!triggerResult) {
2466
- if (isStale()) return;
2467
- try {
2468
- const result = await executePrivateRead(
2469
- (readClient) => readClient.checkDeposit({
2470
- chain_id: chainId,
2471
- tx_hash: hash,
2472
- amount: amount.toString(),
2473
- log_index: logIndex
2474
- })
2475
- );
2476
- if (result.status === "error" && isInsufficientFinalityMessage(result.detail)) {
2477
- if (result.detail) onCheckRetryRef.current?.(result.detail);
2478
- if (Date.now() - pollStartTime > pollTimeout) {
2479
- markVerificationTimedOut();
2480
- return;
2481
- }
2482
- await sleep(finalityRetryInterval);
2483
- if (isStale()) return;
2484
- continue;
2485
- }
2486
- triggerResult = result;
2487
- } catch (err) {
2488
- if (isStale()) return;
2489
- if (!isInsufficientFinalityError(err)) {
2490
- throw err;
2491
- }
2492
- const message = err instanceof AccountingApiError && err.detail ? err.detail : err instanceof Error ? err.message : String(err);
2493
- onCheckRetryRef.current?.(message);
2494
- if (Date.now() - pollStartTime > pollTimeout) {
2495
- markVerificationTimedOut();
2496
- return;
2497
- }
2498
- await sleep(finalityRetryInterval);
2499
- if (isStale()) return;
2500
- }
2512
+ const finality = await checkDepositWithFinalityRetry({
2513
+ checkDeposit: () => executePrivateRead(
2514
+ (readClient) => readClient.checkDeposit({
2515
+ chain_id: chainId,
2516
+ tx_hash: hash,
2517
+ amount: amount.toString(),
2518
+ log_index: logIndex
2519
+ })
2520
+ ),
2521
+ isStale,
2522
+ onRetry: (message) => onCheckRetryRef.current?.(message),
2523
+ timeoutMs: pollTimeout,
2524
+ retryIntervalMs: finalityRetryInterval,
2525
+ startedAt: pollStartTime
2526
+ });
2527
+ if (finality.kind === "stale") return;
2528
+ if (finality.kind === "timeout") {
2529
+ markVerificationTimedOut();
2530
+ return;
2501
2531
  }
2532
+ const triggerResult = finality.response;
2502
2533
  if (isStale()) return;
2503
2534
  if (triggerResult.status === "credited") {
2504
2535
  const creditedAmount = creditedAmountFromResponse(triggerResult, amount);
@@ -2643,18 +2674,6 @@ function creditedAmountFromResponse(response, requestedAmount) {
2643
2674
  function isDefinitiveCandidateFailure(error) {
2644
2675
  return error instanceof AccountingApiError && error.statusCode === 400;
2645
2676
  }
2646
- function sleep(ms) {
2647
- return new Promise((resolve) => setTimeout(resolve, ms));
2648
- }
2649
- function isInsufficientFinalityError(error) {
2650
- if (error instanceof AccountingApiError) {
2651
- return isInsufficientFinalityMessage(error.detail) || isInsufficientFinalityMessage(error.message);
2652
- }
2653
- return error instanceof Error && isInsufficientFinalityMessage(error.message);
2654
- }
2655
- function isInsufficientFinalityMessage(message) {
2656
- return message?.includes("Insufficient finality") ?? false;
2657
- }
2658
2677
  function cn(...inputs) {
2659
2678
  return twMerge(clsx(inputs));
2660
2679
  }
@@ -2758,6 +2777,31 @@ function Skeleton({ className, ...props }) {
2758
2777
  );
2759
2778
  }
2760
2779
 
2780
+ // src/sdk/on-ramp/moonpay-adapter.ts
2781
+ var moonPayOnRampAdapter = {
2782
+ provider: "moonpay",
2783
+ pollPendingWhileOpen: true,
2784
+ buildIntentRequest: ({ walletAddress, tokenId, chainId, providerAssetCode }) => ({
2785
+ wallet_address: walletAddress,
2786
+ token_id: tokenId,
2787
+ chain_id: chainId,
2788
+ moonpay_currency_code: providerAssetCode
2789
+ }),
2790
+ registerTransaction: async ({ client, intentId, providerTransactionId, tokenId, chainId }) => client.updateOnRamp(intentId, {
2791
+ token_id: tokenId,
2792
+ chain_id: chainId,
2793
+ moonpay_transaction_id: intentId === providerTransactionId ? void 0 : providerTransactionId
2794
+ })
2795
+ };
2796
+ function normalizeMoonPayProviderEvent(kind, event) {
2797
+ return {
2798
+ provider: "moonpay",
2799
+ kind,
2800
+ providerTransactionId: event.id,
2801
+ intentId: event.externalTransactionId || void 0
2802
+ };
2803
+ }
2804
+
2761
2805
  // ../../node_modules/@wagmi/core/dist/esm/utils/getAction.js
2762
2806
  function getAction(client, actionFn, name) {
2763
2807
  const action_implicit = client[actionFn.name];
@@ -3008,6 +3052,239 @@ async function waitForTransactionReceipt(config, parameters) {
3008
3052
  chainId: client.chain.id
3009
3053
  };
3010
3054
  }
3055
+
3056
+ // src/sdk/on-ramp/provider.ts
3057
+ function resolveOnRampProviderEventTarget(configuredProvider, activeIntentId, event) {
3058
+ if (event.provider !== configuredProvider) {
3059
+ throw new Error(
3060
+ `On-ramp event provider ${event.provider} does not match configured adapter ${configuredProvider}`
3061
+ );
3062
+ }
3063
+ if (!event.providerTransactionId) {
3064
+ throw new Error("On-ramp provider event is missing a transaction id");
3065
+ }
3066
+ const intentId = event.intentId || activeIntentId || event.providerTransactionId;
3067
+ const isActive = activeIntentId !== null && intentId === activeIntentId;
3068
+ return {
3069
+ intentId,
3070
+ isActive,
3071
+ isStale: activeIntentId !== null && event.intentId !== void 0 && !isActive
3072
+ };
3073
+ }
3074
+ function assertOnRampRecordProvider(record, configuredProvider) {
3075
+ if (record.provider !== configuredProvider) {
3076
+ throw new Error(
3077
+ `On-ramp record provider ${String(record.provider)} does not match configured adapter ${configuredProvider}`
3078
+ );
3079
+ }
3080
+ }
3081
+ function matchesOnRampTransaction(record, transactionId) {
3082
+ return record.transaction_id === transactionId || record.external_transaction_id === transactionId || record.provider_transaction_id === transactionId || record.moonpay_transaction_id === transactionId;
3083
+ }
3084
+ function getOnRampVerificationKey(record) {
3085
+ return record.on_chain_tx_hash ?? record.transaction_id;
3086
+ }
3087
+ function getOnRampIntentId(record) {
3088
+ return record.external_transaction_id ?? record.transaction_id;
3089
+ }
3090
+ async function verifyPendingOnRampsSequentially({
3091
+ records,
3092
+ shouldStop,
3093
+ wasTriggered,
3094
+ trigger,
3095
+ waitForTerminal
3096
+ }) {
3097
+ for (const record of records) {
3098
+ if (shouldStop()) return;
3099
+ if (!record.on_chain_tx_hash) continue;
3100
+ const key = getOnRampVerificationKey(record);
3101
+ if (wasTriggered(key)) continue;
3102
+ try {
3103
+ await trigger(record);
3104
+ } catch {
3105
+ continue;
3106
+ }
3107
+ await waitForTerminal(key);
3108
+ }
3109
+ }
3110
+
3111
+ // src/sdk/on-ramp/recovery.ts
3112
+ var MAX_UNRESOLVED_ONRAMP_INTENTS = 10;
3113
+ var MIN_ONRAMP_PENDING_REQUEST_INTERVAL_MS = 1e4;
3114
+ var ONRAMP_INTENT_RETENTION_MS = 365 * 24 * 60 * 60 * 1e3;
3115
+ var ONRAMP_RECOVERY_VERSION = 1;
3116
+ function recoveryKey(scope) {
3117
+ const api = encodeURIComponent(scope.apiUrl.replace(/\/$/, ""));
3118
+ return `privana:onramp-intents:${api}:${scope.chainId}:${scope.userAddress.toLowerCase()}`;
3119
+ }
3120
+ function isIntent(value) {
3121
+ if (!value || typeof value !== "object") return false;
3122
+ const intent = value;
3123
+ return typeof intent.transactionId === "string" && intent.transactionId.length > 0 && intent.transactionId.length <= 512 && Number.isFinite(intent.savedAt) && (intent.savedAt ?? 0) > 0;
3124
+ }
3125
+ function writeIntents(scope, intents) {
3126
+ if (intents.length === 0) {
3127
+ removeBrowserStorageItem(recoveryKey(scope));
3128
+ return true;
3129
+ }
3130
+ return setBrowserStorageItem(
3131
+ recoveryKey(scope),
3132
+ JSON.stringify({
3133
+ version: ONRAMP_RECOVERY_VERSION,
3134
+ intents
3135
+ })
3136
+ );
3137
+ }
3138
+ function loadUnresolvedOnRampIntents(scope, now = Date.now()) {
3139
+ const key = recoveryKey(scope);
3140
+ try {
3141
+ const raw = getBrowserStorageItem(key);
3142
+ if (!raw) return [];
3143
+ const parsed = JSON.parse(raw);
3144
+ if (parsed.version !== ONRAMP_RECOVERY_VERSION || !Array.isArray(parsed.intents)) {
3145
+ removeBrowserStorageItem(key);
3146
+ return [];
3147
+ }
3148
+ const retained = parsed.intents.filter(isIntent).filter((intent) => now - intent.savedAt <= ONRAMP_INTENT_RETENTION_MS).sort((left, right) => left.savedAt - right.savedAt).slice(-MAX_UNRESOLVED_ONRAMP_INTENTS);
3149
+ if (retained.length !== parsed.intents.length) writeIntents(scope, retained);
3150
+ return retained;
3151
+ } catch {
3152
+ removeBrowserStorageItem(key);
3153
+ return [];
3154
+ }
3155
+ }
3156
+ function rememberUnresolvedOnRampIntent(scope, transactionId, now = Date.now()) {
3157
+ const intents = loadUnresolvedOnRampIntents(scope, now).filter(
3158
+ (intent) => intent.transactionId !== transactionId
3159
+ );
3160
+ intents.push({ transactionId, savedAt: now });
3161
+ return writeIntents(scope, intents.slice(-MAX_UNRESOLVED_ONRAMP_INTENTS));
3162
+ }
3163
+ function forgetUnresolvedOnRampIntent(scope, transactionId, now = Date.now()) {
3164
+ const intents = loadUnresolvedOnRampIntents(scope, now).filter(
3165
+ (intent) => intent.transactionId !== transactionId
3166
+ );
3167
+ writeIntents(scope, intents);
3168
+ }
3169
+ function discardInvalidOnRampIntent(scope, invalidIntentId, activeIntentId) {
3170
+ forgetUnresolvedOnRampIntent(scope, invalidIntentId);
3171
+ const invalidatedActiveIntent = activeIntentId === invalidIntentId;
3172
+ return {
3173
+ activeIntentId: invalidatedActiveIntent ? null : activeIntentId,
3174
+ invalidatedActiveIntent
3175
+ };
3176
+ }
3177
+ function getOnRampCloseRecoveryAction(activeIntentId, purchaseEventObserved) {
3178
+ if (!activeIntentId) return "refresh";
3179
+ return purchaseEventObserved ? "poll-for-delivery" : "refresh-and-retain";
3180
+ }
3181
+ function createPendingOnRampReadCoordinator({
3182
+ read,
3183
+ intervalMs = MIN_ONRAMP_PENDING_REQUEST_INTERVAL_MS,
3184
+ now = Date.now,
3185
+ sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
3186
+ }) {
3187
+ const safeInterval = Math.max(
3188
+ MIN_ONRAMP_PENDING_REQUEST_INTERVAL_MS,
3189
+ Number.isFinite(intervalMs) ? intervalMs : MIN_ONRAMP_PENDING_REQUEST_INTERVAL_MS
3190
+ );
3191
+ let inFlight = null;
3192
+ let nextReadAt = 0;
3193
+ return () => {
3194
+ if (inFlight) return inFlight;
3195
+ const request = (async () => {
3196
+ const delay = Math.max(0, nextReadAt - now());
3197
+ if (delay > 0) await sleep(delay);
3198
+ nextReadAt = now() + safeInterval;
3199
+ return read();
3200
+ })();
3201
+ const tracked = request.finally(() => {
3202
+ if (inFlight === tracked) inFlight = null;
3203
+ });
3204
+ inFlight = tracked;
3205
+ return tracked;
3206
+ };
3207
+ }
3208
+ async function getPendingOnRampsWithRecovery({
3209
+ client,
3210
+ intentIds,
3211
+ onInvalidIntent
3212
+ }) {
3213
+ const bounded = [...new Set(intentIds)].slice(-MAX_UNRESOLVED_ONRAMP_INTENTS);
3214
+ try {
3215
+ return await client.getPendingOnRamps(bounded);
3216
+ } catch (error) {
3217
+ if (!isBadRequest(error) || bounded.length === 0) throw error;
3218
+ }
3219
+ const valid = [];
3220
+ for (const intentId of bounded) {
3221
+ try {
3222
+ await client.getPendingOnRamps([intentId]);
3223
+ valid.push(intentId);
3224
+ } catch (error) {
3225
+ if (!isBadRequest(error)) throw error;
3226
+ onInvalidIntent(intentId);
3227
+ }
3228
+ }
3229
+ return client.getPendingOnRamps(valid);
3230
+ }
3231
+ function isBadRequest(error) {
3232
+ return error instanceof AccountingApiError && error.statusCode === 400;
3233
+ }
3234
+ var ERC20_TRANSFER_EVENT = parseAbiItem(
3235
+ "event Transfer(address indexed from, address indexed to, uint256 value)"
3236
+ );
3237
+ function assertErc20OnRampToken(tokenAddress) {
3238
+ if (tokenAddress.toLowerCase() === zeroAddress) {
3239
+ throw new Error("On-ramp verification supports ERC-20 tokens only");
3240
+ }
3241
+ }
3242
+ function erc20MinDepositBaseUnits(minDepositByChain, chainId) {
3243
+ const minimum = minDepositByChain?.[String(chainId)]?.erc20;
3244
+ return minimum !== void 0 && /^\d+$/.test(minimum) ? BigInt(minimum) : void 0;
3245
+ }
3246
+ function deliveredErc20Amount(logs, tokenAddress, depositAddress) {
3247
+ let delivered = 0n;
3248
+ for (const log of logs) {
3249
+ if (log.address.toLowerCase() !== tokenAddress.toLowerCase()) continue;
3250
+ try {
3251
+ const decoded = decodeEventLog({
3252
+ abi: [ERC20_TRANSFER_EVENT],
3253
+ data: log.data,
3254
+ topics: log.topics
3255
+ });
3256
+ if (decoded.eventName === "Transfer" && decoded.args.to.toLowerCase() === depositAddress.toLowerCase()) {
3257
+ delivered += decoded.args.value;
3258
+ }
3259
+ } catch {
3260
+ }
3261
+ }
3262
+ return delivered;
3263
+ }
3264
+
3265
+ // src/sdk/on-ramp/settlement.ts
3266
+ async function settlePendingOnRampLock({
3267
+ client,
3268
+ userAddress,
3269
+ transactionId,
3270
+ creditedAmount
3271
+ }) {
3272
+ const payload = loadPendingLock(userAddress, transactionId);
3273
+ if (!payload) {
3274
+ clearPendingLock(userAddress, transactionId);
3275
+ return { kind: "not-found" };
3276
+ }
3277
+ try {
3278
+ const response = await submitPendingLock({
3279
+ client,
3280
+ payload,
3281
+ creditedAmount
3282
+ });
3283
+ return { kind: "submitted", payload, response };
3284
+ } finally {
3285
+ clearPendingLock(userAddress, transactionId);
3286
+ }
3287
+ }
3011
3288
  function useEnsureCorrectChain() {
3012
3289
  const config = useConfig();
3013
3290
  const chainId = useChainId();
@@ -3060,15 +3337,13 @@ function useEnsureCorrectChain() {
3060
3337
  };
3061
3338
  }
3062
3339
 
3063
- // src/sdk/hooks/use-fiat-on-ramp.ts
3340
+ // src/sdk/hooks/use-on-ramp.ts
3064
3341
  var DEFAULT_DELIVERY_TIMEOUT_MS = 12e4;
3065
3342
  var DEFAULT_VERIFICATION_TIMEOUT_MS = 10 * 6e4;
3066
3343
  var DEFAULT_FINALITY_RETRY_INTERVAL_MS = 15e3;
3067
- var ERC20_TRANSFER_EVENT = parseAbiItem(
3068
- "event Transfer(address indexed from, address indexed to, uint256 value)"
3069
- );
3070
- function useFiatOnRamp(options) {
3344
+ function useOnRamp(options) {
3071
3345
  const {
3346
+ adapter,
3072
3347
  tokenId,
3073
3348
  postDepositLock,
3074
3349
  onCredited,
@@ -3078,24 +3353,40 @@ function useFiatOnRamp(options) {
3078
3353
  onDebugEvent
3079
3354
  } = options;
3080
3355
  const deliveryTimeout = options.deliveryTimeout ?? DEFAULT_DELIVERY_TIMEOUT_MS;
3081
- const deliveryPollInterval = options.deliveryPollInterval ?? 3e3;
3356
+ const requestedDeliveryPollInterval = options.deliveryPollInterval ?? MIN_ONRAMP_PENDING_REQUEST_INTERVAL_MS;
3357
+ const deliveryPollInterval = Number.isFinite(requestedDeliveryPollInterval) ? Math.max(requestedDeliveryPollInterval, MIN_ONRAMP_PENDING_REQUEST_INTERVAL_MS) : MIN_ONRAMP_PENDING_REQUEST_INTERVAL_MS;
3082
3358
  const verificationTimeout = options.verificationTimeout ?? DEFAULT_VERIFICATION_TIMEOUT_MS;
3083
3359
  const finalityRetryInterval = options.finalityRetryInterval ?? DEFAULT_FINALITY_RETRY_INTERVAL_MS;
3084
3360
  const { address } = useAccount();
3085
3361
  const { data: walletClient } = useWalletClient();
3086
3362
  const { client, enabledTokens, networkConfig, serviceAddress } = usePrivanaContext();
3087
3363
  const { executePrivateRead, privateReadAddress, privateReadReady } = usePrivateReadRequest();
3364
+ const executeOnRampPrivateRead = executePrivateRead;
3088
3365
  const privateReadAddressRef = useRef(privateReadAddress);
3089
3366
  privateReadAddressRef.current = privateReadAddress;
3090
3367
  const { ensureCorrectChain } = useEnsureCorrectChain();
3091
3368
  const wagmiConfig = useConfig();
3092
3369
  const queryClient = useQueryClient();
3093
3370
  const selectedToken = enabledTokens.find((t) => t.id.toLowerCase() === tokenId.toLowerCase());
3371
+ const recoveryScope = useMemo(
3372
+ () => privateReadAddress ? {
3373
+ apiUrl: networkConfig.apiUrl,
3374
+ chainId: networkConfig.chainId,
3375
+ userAddress: privateReadAddress
3376
+ } : null,
3377
+ [networkConfig.apiUrl, networkConfig.chainId, privateReadAddress]
3378
+ );
3379
+ const recoveryScopeRef = useRef(recoveryScope);
3380
+ recoveryScopeRef.current = recoveryScope;
3381
+ const flowIdentity = `${networkConfig.apiUrl}\0${networkConfig.chainId}\0${privateReadAddress ?? ""}\0${privateReadReady}\0${tokenId}\0${adapter.provider}`;
3382
+ const flowSession = useMemo(() => Symbol(flowIdentity), [flowIdentity]);
3383
+ const flowSessionRef = useRef(flowSession);
3384
+ flowSessionRef.current = flowSession;
3094
3385
  const [status, setStatus] = useState("idle");
3095
3386
  const [pending, setPending] = useState([]);
3096
3387
  const [error, setError] = useState(null);
3097
3388
  const [depositAddress, setDepositAddress] = useState();
3098
- const [minDepositBaseUnits, setMinDepositBaseUnits] = useState();
3389
+ const [minDepositByChain, setMinDepositByChain] = useState();
3099
3390
  const [activeIntentId, setActiveIntentId] = useState(null);
3100
3391
  const [activeVerificationId, setActiveVerificationId] = useState(null);
3101
3392
  const [finalityProgress, setFinalityProgress] = useState({});
@@ -3105,14 +3396,21 @@ function useFiatOnRamp(options) {
3105
3396
  const onErrorRef = useRef(onError);
3106
3397
  const onDebugEventRef = useRef(onDebugEvent);
3107
3398
  const statusRef = useRef(status);
3399
+ const depositAddressSessionRef = useRef(null);
3108
3400
  const activeIntentIdRef = useRef(null);
3401
+ const activeIntentSessionRef = useRef(null);
3109
3402
  const activeVerificationRecordRef = useRef(null);
3110
3403
  const activeVerificationKeyRef = useRef(null);
3404
+ const activeVerificationSurfacesFailureRef = useRef(false);
3111
3405
  const lockOwnerRef = useRef(null);
3112
3406
  const triggeredVerificationKeysRef = useRef(/* @__PURE__ */ new Set());
3113
3407
  const activeVerificationDoneRef = useRef(null);
3114
3408
  const closeReconcilePromiseRef = useRef(null);
3409
+ const deliveryWaitPromiseRef = useRef(null);
3115
3410
  const purchaseInitiatedRef = useRef(false);
3411
+ const scopedDepositAddress = depositAddressSessionRef.current === flowSession ? depositAddress : void 0;
3412
+ const scopedMinDepositByChain = depositAddressSessionRef.current === flowSession ? minDepositByChain : void 0;
3413
+ const scopedMinDepositBaseUnits = selectedToken ? erc20MinDepositBaseUnits(scopedMinDepositByChain, selectedToken.chainId) : void 0;
3116
3414
  useEffect(() => {
3117
3415
  onCreditedRef.current = onCredited;
3118
3416
  onLockSubmittedRef.current = onLockSubmitted;
@@ -3126,10 +3424,6 @@ function useFiatOnRamp(options) {
3126
3424
  useEffect(() => {
3127
3425
  activeIntentIdRef.current = activeIntentId;
3128
3426
  }, [activeIntentId]);
3129
- useEffect(() => {
3130
- activeIntentIdRef.current = null;
3131
- setActiveIntentId(null);
3132
- }, [tokenId]);
3133
3427
  const emitDebug = useCallback(
3134
3428
  (event, payload) => {
3135
3429
  onDebugEventRef.current?.({
@@ -3146,24 +3440,28 @@ function useFiatOnRamp(options) {
3146
3440
  emitDebug("private-read-state", { privateReadReady });
3147
3441
  }, [emitDebug, privateReadReady]);
3148
3442
  useEffect(() => {
3443
+ depositAddressSessionRef.current = null;
3149
3444
  if (!privateReadReady) {
3150
3445
  emitDebug("deposit-address:skip", { reason: "private-read-not-ready" });
3151
3446
  setDepositAddress(void 0);
3152
- setMinDepositBaseUnits(void 0);
3447
+ setMinDepositByChain(void 0);
3153
3448
  return;
3154
3449
  }
3450
+ setDepositAddress(void 0);
3451
+ setMinDepositByChain(void 0);
3155
3452
  let cancelled = false;
3156
3453
  void (async () => {
3157
3454
  try {
3158
3455
  emitDebug("deposit-address:request");
3159
- const resp = await executePrivateRead((readClient) => readClient.getDepositAddress());
3160
- if (cancelled) return;
3456
+ const resp = await executeOnRampPrivateRead((readClient) => readClient.getDepositAddress());
3457
+ if (cancelled || flowSessionRef.current !== flowSession) return;
3458
+ depositAddressSessionRef.current = flowSession;
3161
3459
  setDepositAddress(resp.deposit_address);
3460
+ setMinDepositByChain(resp.min_deposit);
3162
3461
  const token = enabledTokens.find((t) => t.id.toLowerCase() === tokenId.toLowerCase());
3163
3462
  const mins = token ? resp.min_deposit?.[String(token.chainId)] : void 0;
3164
- if (mins?.erc20) setMinDepositBaseUnits(BigInt(mins.erc20));
3165
3463
  emitDebug("deposit-address:success", {
3166
- depositAddress: resp.deposit_address,
3464
+ depositAddressReady: true,
3167
3465
  selectedToken: token ? summariseToken(token) : null,
3168
3466
  minDepositBaseUnits: mins?.erc20 ?? null
3169
3467
  });
@@ -3177,8 +3475,53 @@ function useFiatOnRamp(options) {
3177
3475
  return () => {
3178
3476
  cancelled = true;
3179
3477
  };
3180
- }, [client, emitDebug, enabledTokens, executePrivateRead, privateReadReady, tokenId]);
3478
+ }, [emitDebug, enabledTokens, executeOnRampPrivateRead, flowSession, privateReadReady, tokenId]);
3479
+ const fetchPendingRows = useCallback(async () => {
3480
+ const intents = recoveryScope ? loadUnresolvedOnRampIntents(recoveryScope) : [];
3481
+ const intentIds = intents.map((intent) => intent.transactionId);
3482
+ if (activeIntentIdRef.current && activeIntentSessionRef.current === flowSession) {
3483
+ intentIds.push(activeIntentIdRef.current);
3484
+ }
3485
+ const { pending: rows } = await executeOnRampPrivateRead(
3486
+ (readClient) => getPendingOnRampsWithRecovery({
3487
+ client: readClient,
3488
+ intentIds,
3489
+ onInvalidIntent: (intentId) => {
3490
+ if (!recoveryScope || flowSessionRef.current !== flowSession) return;
3491
+ const disposition = discardInvalidOnRampIntent(
3492
+ recoveryScope,
3493
+ intentId,
3494
+ activeIntentIdRef.current
3495
+ );
3496
+ if (disposition.invalidatedActiveIntent) {
3497
+ activeIntentIdRef.current = null;
3498
+ activeIntentSessionRef.current = null;
3499
+ setActiveIntentId(null);
3500
+ purchaseInitiatedRef.current = false;
3501
+ lockOwnerRef.current = null;
3502
+ if (statusRef.current !== "verifying" && statusRef.current !== "credited") {
3503
+ statusRef.current = "idle";
3504
+ setStatus("idle");
3505
+ setError(null);
3506
+ }
3507
+ }
3508
+ emitDebug("pending:discard-invalid-intent", {
3509
+ invalidatedActiveIntent: disposition.invalidatedActiveIntent
3510
+ });
3511
+ }
3512
+ })
3513
+ );
3514
+ return rows;
3515
+ }, [emitDebug, executeOnRampPrivateRead, flowSession, recoveryScope]);
3516
+ const readPendingRows = useMemo(
3517
+ () => createPendingOnRampReadCoordinator({
3518
+ read: fetchPendingRows,
3519
+ intervalMs: deliveryPollInterval
3520
+ }),
3521
+ [deliveryPollInterval, fetchPendingRows]
3522
+ );
3181
3523
  const refreshPending = useCallback(async () => {
3524
+ if (flowSessionRef.current !== flowSession) return;
3182
3525
  if (!privateReadReady) {
3183
3526
  emitDebug("pending:skip", { reason: "private-read-not-ready" });
3184
3527
  setPending([]);
@@ -3186,79 +3529,75 @@ function useFiatOnRamp(options) {
3186
3529
  }
3187
3530
  try {
3188
3531
  emitDebug("pending:request");
3189
- const { pending: rows } = await executePrivateRead(
3190
- (readClient) => readClient.getPendingOnRamps()
3191
- );
3532
+ const rows = await readPendingRows();
3533
+ if (flowSessionRef.current !== flowSession) return;
3192
3534
  setPending(rows);
3193
3535
  emitDebug("pending:success", {
3194
3536
  count: rows.length,
3195
3537
  rows: rows.map(summariseOnRampRecord)
3196
3538
  });
3197
3539
  } catch (err) {
3540
+ if (flowSessionRef.current !== flowSession) return;
3198
3541
  emitDebug("pending:error", errorPayload(err));
3199
3542
  console.warn("Failed to load pending on-ramps:", err);
3200
3543
  }
3201
- }, [emitDebug, executePrivateRead, privateReadReady]);
3202
- useEffect(() => {
3203
- refreshPending();
3204
- }, [refreshPending]);
3544
+ }, [emitDebug, flowSession, privateReadReady, readPendingRows]);
3205
3545
  const clearActiveVerification = useCallback((expectedKey) => {
3206
3546
  const key = activeVerificationKeyRef.current;
3207
3547
  if (expectedKey != null && key !== null && key !== expectedKey) return;
3208
3548
  if (key) triggeredVerificationKeysRef.current.delete(key);
3209
3549
  activeVerificationKeyRef.current = null;
3210
3550
  activeVerificationRecordRef.current = null;
3551
+ activeVerificationSurfacesFailureRef.current = false;
3211
3552
  setActiveVerificationId(null);
3212
3553
  activeVerificationDoneRef.current?.();
3213
3554
  activeVerificationDoneRef.current = null;
3214
3555
  }, []);
3215
3556
  const submitPendingLockAfterCredit = useCallback(
3216
3557
  async (transactionId, userAddress, creditedAmount) => {
3217
- const signedLock = loadPendingLock(userAddress, transactionId);
3218
- if (!signedLock) {
3219
- clearPendingLock(userAddress, transactionId);
3220
- if (!postDepositLock || transactionId !== activeIntentIdRef.current) return;
3221
- const error2 = new PostDepositLockError(
3222
- "No persisted signed lock found for this on-ramp",
3223
- "not-found"
3224
- );
3225
- emitDebug("lock:not-found", { transactionId });
3226
- (onLockFailedRef.current ?? onErrorRef.current)?.(error2);
3227
- return;
3228
- }
3229
3558
  try {
3230
- const result = await submitPendingLock({ client, payload: signedLock, creditedAmount });
3559
+ const settlement = await settlePendingOnRampLock({
3560
+ client,
3561
+ userAddress,
3562
+ transactionId,
3563
+ creditedAmount
3564
+ });
3565
+ if (settlement.kind === "not-found") {
3566
+ if (!postDepositLock || transactionId !== activeIntentIdRef.current) return;
3567
+ const lockError = new PostDepositLockError(
3568
+ "No persisted signed lock found for this on-ramp",
3569
+ "not-found"
3570
+ );
3571
+ emitDebug("lock:not-found");
3572
+ (onLockFailedRef.current ?? onErrorRef.current)?.(lockError);
3573
+ return;
3574
+ }
3231
3575
  queryClient.invalidateQueries({ queryKey: ["accounting-balance"] });
3232
3576
  queryClient.invalidateQueries({ queryKey: ["accounting-locked-funds"] });
3233
3577
  queryClient.invalidateQueries({ queryKey: ["accounting-total-locked-balance"] });
3234
3578
  queryClient.invalidateQueries({ queryKey: ["accounting-history"] });
3235
3579
  emitDebug("lock:submitted", {
3236
- transactionId,
3237
- amount: signedLock.amount,
3238
- submissionId: result.submission_id
3580
+ submissionIdPresent: Boolean(settlement.response.submission_id)
3239
3581
  });
3240
- onLockSubmittedRef.current?.(result);
3582
+ onLockSubmittedRef.current?.(settlement.response);
3241
3583
  } catch (err) {
3242
3584
  const error2 = err instanceof PostDepositLockError ? err : new PostDepositLockError(
3243
3585
  err instanceof Error ? err.message : "Lock submission failed",
3244
3586
  "submission-failed",
3245
- BigInt(signedLock.amount),
3587
+ void 0,
3246
3588
  creditedAmount,
3247
3589
  { cause: err }
3248
3590
  );
3249
3591
  emitDebug("lock:failed", {
3250
- transactionId,
3251
3592
  reason: error2.reason,
3252
3593
  message: error2.message
3253
3594
  });
3254
3595
  (onLockFailedRef.current ?? onErrorRef.current)?.(error2);
3255
- } finally {
3256
- clearPendingLock(userAddress, transactionId);
3257
3596
  }
3258
3597
  },
3259
3598
  [client, emitDebug, postDepositLock, queryClient]
3260
3599
  );
3261
- const { verify } = useDepositVerification({
3600
+ const { verify, reset: resetDepositVerification } = useDepositVerification({
3262
3601
  pollTimeout: verificationTimeout,
3263
3602
  pollInterval: options.verificationPollInterval,
3264
3603
  finalityRetryInterval,
@@ -3273,12 +3612,13 @@ function useFiatOnRamp(options) {
3273
3612
  },
3274
3613
  onCredited: (depositTxHash, _response, creditedAmount) => {
3275
3614
  const record = activeVerificationRecordRef.current;
3615
+ const recoveryScopeAtCredit = recoveryScopeRef.current;
3276
3616
  const verificationKey = record ? getOnRampVerificationKey(record) : null;
3277
3617
  emitDebug("verification:credited", {
3278
3618
  depositTxHash,
3279
3619
  record: record ? summariseOnRampRecord(record) : null
3280
3620
  });
3281
- if (record && activeIntentIdRef.current === record.transaction_id) {
3621
+ if (record && activeIntentIdRef.current && matchesOnRampTransaction(record, activeIntentIdRef.current)) {
3282
3622
  setStatus("credited");
3283
3623
  }
3284
3624
  if (record) {
@@ -3292,7 +3632,7 @@ function useFiatOnRamp(options) {
3292
3632
  if (lockOwner) {
3293
3633
  void submitPendingLockAfterCredit(record.transaction_id, lockOwner, creditedAmount);
3294
3634
  } else if (postDepositLock) {
3295
- emitDebug("lock:owner-unavailable", { transactionId: record.transaction_id });
3635
+ emitDebug("lock:owner-unavailable");
3296
3636
  (onLockFailedRef.current ?? onErrorRef.current)?.(
3297
3637
  new PostDepositLockError(
3298
3638
  "No wallet address available to look up the signed lock for this on-ramp",
@@ -3305,10 +3645,9 @@ function useFiatOnRamp(options) {
3305
3645
  try {
3306
3646
  if (record && depositTxHash.startsWith("0x")) {
3307
3647
  emitDebug("onramp:mark-deposit-triggered-request", {
3308
- transactionId: record.transaction_id,
3309
3648
  depositTxHash
3310
3649
  });
3311
- const updated = await executePrivateRead(
3650
+ const updated = await executeOnRampPrivateRead(
3312
3651
  (readClient) => readClient.updateOnRamp(record.transaction_id, {
3313
3652
  deposit_tx_hash: depositTxHash
3314
3653
  })
@@ -3321,11 +3660,17 @@ function useFiatOnRamp(options) {
3321
3660
  emitDebug("onramp:mark-deposit-triggered-error", errorPayload(err));
3322
3661
  console.warn("Failed to mark on-ramp row complete:", err);
3323
3662
  } finally {
3663
+ if (record && recoveryScopeAtCredit) {
3664
+ forgetUnresolvedOnRampIntent(recoveryScopeAtCredit, getOnRampIntentId(record));
3665
+ }
3324
3666
  await refreshPending();
3325
3667
  clearActiveVerification(verificationKey);
3326
- if (record && activeIntentIdRef.current === record.transaction_id) {
3668
+ if (record && activeIntentIdRef.current && matchesOnRampTransaction(record, activeIntentIdRef.current)) {
3327
3669
  activeIntentIdRef.current = null;
3670
+ activeIntentSessionRef.current = null;
3328
3671
  setActiveIntentId(null);
3672
+ purchaseInitiatedRef.current = false;
3673
+ lockOwnerRef.current = null;
3329
3674
  }
3330
3675
  }
3331
3676
  })();
@@ -3333,12 +3678,13 @@ function useFiatOnRamp(options) {
3333
3678
  },
3334
3679
  onCheckTimeout: (depositTxHash) => {
3335
3680
  const record = activeVerificationRecordRef.current;
3681
+ const shouldSurfaceFailure = activeVerificationSurfacesFailureRef.current || !record;
3336
3682
  const err = new Error(
3337
3683
  "Privana verification is still pending. Retry from the pending on-ramp list if it does not complete."
3338
3684
  );
3339
3685
  emitDebug("verification:timeout", { depositTxHash, message: err.message });
3340
3686
  clearActiveVerification();
3341
- if (!record || activeIntentIdRef.current === record.transaction_id) {
3687
+ if (shouldSurfaceFailure) {
3342
3688
  setStatus("failed");
3343
3689
  setError(err);
3344
3690
  }
@@ -3347,18 +3693,37 @@ function useFiatOnRamp(options) {
3347
3693
  },
3348
3694
  onError: (err) => {
3349
3695
  const record = activeVerificationRecordRef.current;
3696
+ const shouldSurfaceFailure = activeVerificationSurfacesFailureRef.current || !record;
3350
3697
  emitDebug("verification:error", errorPayload(err));
3351
3698
  clearActiveVerification();
3352
- if (!record || activeIntentIdRef.current === record.transaction_id) {
3699
+ if (shouldSurfaceFailure) {
3353
3700
  setStatus("failed");
3354
3701
  setError(err);
3355
3702
  }
3356
3703
  onErrorRef.current?.(err);
3357
3704
  }
3358
3705
  });
3706
+ useEffect(() => {
3707
+ activeIntentIdRef.current = null;
3708
+ activeIntentSessionRef.current = null;
3709
+ setActiveIntentId(null);
3710
+ purchaseInitiatedRef.current = false;
3711
+ lockOwnerRef.current = null;
3712
+ resetDepositVerification();
3713
+ clearActiveVerification();
3714
+ triggeredVerificationKeysRef.current.clear();
3715
+ setPending([]);
3716
+ setFinalityProgress({});
3717
+ statusRef.current = "idle";
3718
+ setStatus("idle");
3719
+ setError(null);
3720
+ }, [clearActiveVerification, flowSession, resetDepositVerification]);
3721
+ useEffect(() => {
3722
+ void refreshPending();
3723
+ }, [refreshPending]);
3359
3724
  const prepareOnRampIntent = useCallback(
3360
3725
  async ({
3361
- currencyCode,
3726
+ providerAssetCode,
3362
3727
  baseCurrencyCode,
3363
3728
  baseCurrencyAmount,
3364
3729
  quoteCurrencyAmount
@@ -3368,7 +3733,7 @@ function useFiatOnRamp(options) {
3368
3733
  purchaseInitiatedRef.current = false;
3369
3734
  const token = enabledTokens.find((t) => t.id.toLowerCase() === tokenId.toLowerCase());
3370
3735
  if (!token) throw new Error(`Unknown token: ${tokenId}`);
3371
- if (!depositAddress) throw new Error("Privana deposit address is not ready");
3736
+ if (!scopedDepositAddress) throw new Error("Privana deposit address is not ready");
3372
3737
  let lockAmount;
3373
3738
  let lockOwner;
3374
3739
  if (postDepositLock) {
@@ -3395,22 +3760,35 @@ function useFiatOnRamp(options) {
3395
3760
  emitDebug("intent:create-request", {
3396
3761
  tokenId,
3397
3762
  chainId: token.chainId,
3398
- currencyCode,
3763
+ provider: adapter.provider,
3764
+ providerAssetCode,
3399
3765
  baseCurrencyCode: baseCurrencyCode ?? null,
3400
- baseCurrencyAmount: baseCurrencyAmount ?? null,
3401
- quoteCurrencyAmount: quoteCurrencyAmount ?? null,
3402
- depositAddress
3766
+ baseCurrencyAmountPresent: baseCurrencyAmount !== void 0,
3767
+ quoteCurrencyAmountPresent: quoteCurrencyAmount !== void 0,
3768
+ depositAddressReady: true
3403
3769
  });
3404
- const record = await executePrivateRead(
3405
- (readClient) => readClient.createOnRampIntent({
3406
- wallet_address: depositAddress,
3407
- token_id: tokenId,
3408
- chain_id: token.chainId,
3409
- moonpay_currency_code: currencyCode,
3410
- base_currency_code: baseCurrencyCode,
3411
- base_currency_amount: baseCurrencyAmount
3412
- })
3770
+ const record = await executeOnRampPrivateRead(
3771
+ (readClient) => readClient.createOnRampIntent(
3772
+ adapter.buildIntentRequest({
3773
+ walletAddress: scopedDepositAddress,
3774
+ tokenId,
3775
+ chainId: token.chainId,
3776
+ providerAssetCode
3777
+ })
3778
+ )
3413
3779
  );
3780
+ if (flowSessionRef.current !== flowSession) {
3781
+ throw new Error("On-ramp account or network changed while creating the intent");
3782
+ }
3783
+ assertOnRampRecordProvider(record, adapter.provider);
3784
+ if (!record.provider_asset_code) {
3785
+ throw new Error("On-ramp intent response is missing provider_asset_code");
3786
+ }
3787
+ if (record.provider_asset_code.toLowerCase() !== providerAssetCode.toLowerCase()) {
3788
+ throw new Error(
3789
+ `On-ramp intent asset ${record.provider_asset_code} does not match requested asset ${providerAssetCode}`
3790
+ );
3791
+ }
3414
3792
  if (postDepositLock && lockOwner && lockAmount !== void 0) {
3415
3793
  const signingWalletClient = await getWalletClient3(wagmiConfig, {
3416
3794
  chainId: networkConfig.chainId
@@ -3428,15 +3806,23 @@ function useFiatOnRamp(options) {
3428
3806
  if (privateReadAddressRef.current?.toLowerCase() !== lockOwner.toLowerCase()) {
3429
3807
  throw new Error("Authenticated deposit account changed while signing");
3430
3808
  }
3809
+ if (flowSessionRef.current !== flowSession) {
3810
+ throw new Error("On-ramp account or network changed while signing");
3811
+ }
3431
3812
  savePendingLock(lockOwner, record.transaction_id, signedLock);
3432
3813
  lockOwnerRef.current = lockOwner;
3433
3814
  emitDebug("intent:lock-signed", {
3434
- transactionId: record.transaction_id,
3435
- amount: signedLock.amount,
3436
- expiry: signedLock.expiry
3815
+ lockConfigured: true
3437
3816
  });
3438
3817
  }
3818
+ if (recoveryScope && !rememberUnresolvedOnRampIntent(recoveryScope, record.transaction_id)) {
3819
+ emitDebug("intent:recovery-storage-unavailable");
3820
+ }
3821
+ if (flowSessionRef.current !== flowSession) {
3822
+ throw new Error("On-ramp account or network changed while preparing the purchase");
3823
+ }
3439
3824
  activeIntentIdRef.current = record.transaction_id;
3825
+ activeIntentSessionRef.current = flowSession;
3440
3826
  setActiveIntentId(record.transaction_id);
3441
3827
  emitDebug("intent:create-success", {
3442
3828
  record: summariseOnRampRecord(record)
@@ -3444,150 +3830,163 @@ function useFiatOnRamp(options) {
3444
3830
  return record;
3445
3831
  } catch (err) {
3446
3832
  const e = err instanceof Error ? err : new Error("Failed to create on-ramp intent");
3447
- setStatus("failed");
3448
- setError(e);
3449
- emitDebug("intent:create-error", errorPayload(e));
3450
- onErrorRef.current?.(e);
3833
+ if (flowSessionRef.current === flowSession) {
3834
+ setStatus("failed");
3835
+ setError(e);
3836
+ emitDebug("intent:create-error", errorPayload(e));
3837
+ onErrorRef.current?.(e);
3838
+ }
3451
3839
  throw e;
3452
3840
  }
3453
3841
  },
3454
3842
  [
3843
+ adapter,
3455
3844
  address,
3456
3845
  client,
3457
- depositAddress,
3458
3846
  emitDebug,
3459
3847
  enabledTokens,
3460
3848
  ensureCorrectChain,
3461
- executePrivateRead,
3849
+ executeOnRampPrivateRead,
3850
+ flowSession,
3462
3851
  networkConfig,
3463
3852
  postDepositLock,
3464
3853
  privateReadAddress,
3854
+ recoveryScope,
3855
+ scopedDepositAddress,
3465
3856
  serviceAddress,
3466
3857
  tokenId,
3467
3858
  wagmiConfig,
3468
3859
  walletClient
3469
3860
  ]
3470
3861
  );
3471
- const registerOnRampTokenMapping = useCallback(
3472
- async (moonpayTransactionId) => {
3862
+ const registerProviderTransaction = useCallback(
3863
+ async (event, intentId) => {
3864
+ if (flowSessionRef.current !== flowSession) return;
3865
+ const register = adapter.registerTransaction;
3866
+ if (!register) return;
3473
3867
  const token = enabledTokens.find((t) => t.id.toLowerCase() === tokenId.toLowerCase());
3474
3868
  if (!token) {
3475
- emitDebug("register-token-mapping:skip", {
3476
- moonpayTransactionId,
3869
+ emitDebug("provider-transaction:register-skip", {
3870
+ provider: adapter.provider,
3477
3871
  reason: "selected-token-not-found"
3478
3872
  });
3479
3873
  return;
3480
3874
  }
3481
- const transactionId = activeIntentIdRef.current ?? moonpayTransactionId;
3482
3875
  try {
3483
- emitDebug("register-token-mapping:request", {
3484
- transactionId,
3485
- moonpayTransactionId,
3876
+ emitDebug("provider-transaction:register-request", {
3877
+ provider: adapter.provider,
3878
+ intentIdPresent: Boolean(intentId),
3879
+ providerTransactionIdPresent: Boolean(event.providerTransactionId),
3486
3880
  tokenId,
3487
3881
  chainId: token.chainId
3488
3882
  });
3489
- const record = await executePrivateRead(
3490
- (readClient) => readClient.updateOnRamp(transactionId, {
3491
- token_id: tokenId,
3492
- chain_id: token.chainId,
3493
- moonpay_transaction_id: transactionId === moonpayTransactionId ? void 0 : moonpayTransactionId
3883
+ const record = await executeOnRampPrivateRead(
3884
+ (readClient) => register({
3885
+ client: readClient,
3886
+ intentId,
3887
+ providerTransactionId: event.providerTransactionId,
3888
+ tokenId,
3889
+ chainId: token.chainId
3494
3890
  })
3495
3891
  );
3496
- emitDebug("register-token-mapping:success", {
3497
- transactionId,
3498
- moonpayTransactionId,
3499
- record: summariseOnRampRecord(record)
3892
+ if (flowSessionRef.current !== flowSession) return;
3893
+ if (record) assertOnRampRecordProvider(record, adapter.provider);
3894
+ emitDebug("provider-transaction:register-success", {
3895
+ provider: adapter.provider,
3896
+ record: record ? summariseOnRampRecord(record) : null
3500
3897
  });
3501
3898
  } catch (err) {
3502
- emitDebug("register-token-mapping:error", {
3503
- transactionId,
3504
- moonpayTransactionId,
3899
+ if (flowSessionRef.current !== flowSession) return;
3900
+ emitDebug("provider-transaction:register-error", {
3901
+ provider: adapter.provider,
3505
3902
  ...errorPayload(err)
3506
3903
  });
3507
3904
  console.warn("Failed to register on-ramp token mapping:", err);
3508
3905
  }
3509
3906
  },
3510
- [emitDebug, enabledTokens, executePrivateRead, tokenId]
3511
- );
3512
- const handleTransactionCreated = useCallback(
3513
- async (props) => {
3514
- emitDebug("moonpay:onTransactionCreated", summariseMoonPayEventProps(props));
3515
- purchaseInitiatedRef.current = true;
3516
- await registerOnRampTokenMapping(props.id);
3517
- },
3518
- [emitDebug, registerOnRampTokenMapping]
3907
+ [adapter, emitDebug, enabledTokens, executeOnRampPrivateRead, flowSession, tokenId]
3519
3908
  );
3520
- const signUrl = useCallback(
3521
- async (url) => {
3522
- setError(null);
3523
- try {
3524
- emitDebug("moonpay:onUrlSignatureRequested", summariseMoonPayUrl(url));
3525
- const { signature } = await executePrivateRead(
3526
- (readClient) => readClient.signOnRampUrl({ url })
3527
- );
3528
- setStatus("awaiting-purchase");
3529
- emitDebug("sign-url:success", {
3530
- signatureLength: signature.length
3531
- });
3532
- return signature;
3533
- } catch (err) {
3534
- const e = err instanceof Error ? err : new Error("Failed to sign on-ramp URL");
3535
- setStatus("failed");
3536
- setError(e);
3537
- emitDebug("sign-url:error", errorPayload(e));
3538
- onErrorRef.current?.(e);
3539
- throw err;
3540
- }
3909
+ const handleProviderLaunchReady = useCallback(() => {
3910
+ if (flowSessionRef.current !== flowSession) return;
3911
+ setError(null);
3912
+ statusRef.current = "awaiting-purchase";
3913
+ setStatus("awaiting-purchase");
3914
+ emitDebug("provider:launch-ready", { provider: adapter.provider });
3915
+ }, [adapter.provider, emitDebug, flowSession]);
3916
+ const handleProviderLaunchFailed = useCallback(
3917
+ (launchError) => {
3918
+ if (flowSessionRef.current !== flowSession) return;
3919
+ statusRef.current = "failed";
3920
+ setStatus("failed");
3921
+ setError(launchError);
3922
+ emitDebug("provider:launch-error", {
3923
+ provider: adapter.provider,
3924
+ ...errorPayload(launchError)
3925
+ });
3926
+ onErrorRef.current?.(launchError);
3541
3927
  },
3542
- [emitDebug, executePrivateRead]
3928
+ [adapter.provider, emitDebug, flowSession]
3543
3929
  );
3544
3930
  const waitForOnChainHash = useCallback(
3545
3931
  async (transactionId) => {
3546
- const startTime = Date.now();
3547
- emitDebug("delivery-poll:start", {
3548
- transactionId,
3549
- deliveryTimeout,
3550
- deliveryPollInterval
3551
- });
3552
- while (Date.now() - startTime < deliveryTimeout) {
3553
- try {
3554
- const { pending: rows } = await executePrivateRead(
3555
- (readClient) => readClient.getPendingOnRamps()
3556
- );
3557
- setPending(rows);
3558
- const record = rows.find((r) => matchesOnRampTransaction(r, transactionId));
3559
- emitDebug("delivery-poll:tick", {
3560
- transactionId,
3561
- count: rows.length,
3562
- matchingRecord: record ? summariseOnRampRecord(record) : null
3563
- });
3564
- if (record?.on_chain_tx_hash && record.quote_currency_amount) {
3565
- emitDebug("delivery-poll:success", {
3566
- transactionId,
3567
- record: summariseOnRampRecord(record)
3932
+ if (flowSessionRef.current !== flowSession) return null;
3933
+ const existing = deliveryWaitPromiseRef.current;
3934
+ if (existing?.flowSession === flowSession && existing.transactionId === transactionId) {
3935
+ return existing.promise;
3936
+ }
3937
+ const promise = (async () => {
3938
+ const startTime = Date.now();
3939
+ emitDebug("delivery-poll:start", {
3940
+ deliveryTimeout,
3941
+ deliveryPollInterval
3942
+ });
3943
+ while (Date.now() - startTime < deliveryTimeout) {
3944
+ if (flowSessionRef.current !== flowSession) return null;
3945
+ try {
3946
+ const rows = await readPendingRows();
3947
+ if (flowSessionRef.current !== flowSession) return null;
3948
+ setPending(rows);
3949
+ const record = rows.find((r) => matchesOnRampTransaction(r, transactionId));
3950
+ emitDebug("delivery-poll:tick", {
3951
+ count: rows.length,
3952
+ matchingRecord: record ? summariseOnRampRecord(record) : null
3953
+ });
3954
+ if (record?.on_chain_tx_hash) {
3955
+ emitDebug("delivery-poll:success", {
3956
+ record: summariseOnRampRecord(record)
3957
+ });
3958
+ return record;
3959
+ }
3960
+ } catch (err) {
3961
+ emitDebug("delivery-poll:error", {
3962
+ ...errorPayload(err)
3568
3963
  });
3569
- return record;
3964
+ console.warn("Polling pending on-ramps failed:", err);
3570
3965
  }
3571
- } catch (err) {
3572
- emitDebug("delivery-poll:error", {
3573
- transactionId,
3574
- ...errorPayload(err)
3575
- });
3576
- console.warn("Polling pending on-ramps failed:", err);
3966
+ await new Promise((resolve) => setTimeout(resolve, deliveryPollInterval));
3967
+ }
3968
+ if (flowSessionRef.current !== flowSession) return null;
3969
+ emitDebug("delivery-poll:timeout");
3970
+ return null;
3971
+ })();
3972
+ const entry = { flowSession, transactionId, promise };
3973
+ deliveryWaitPromiseRef.current = entry;
3974
+ try {
3975
+ return await promise;
3976
+ } finally {
3977
+ if (deliveryWaitPromiseRef.current === entry) {
3978
+ deliveryWaitPromiseRef.current = null;
3577
3979
  }
3578
- await new Promise((r) => setTimeout(r, deliveryPollInterval));
3579
3980
  }
3580
- emitDebug("delivery-poll:timeout", { transactionId });
3581
- return null;
3582
3981
  },
3583
- [deliveryPollInterval, deliveryTimeout, emitDebug, executePrivateRead]
3982
+ [deliveryPollInterval, deliveryTimeout, emitDebug, flowSession, readPendingRows]
3584
3983
  );
3585
3984
  const triggerVerification = useCallback(
3586
- async (record) => {
3985
+ async (record, surfaceFailure = false) => {
3986
+ if (flowSessionRef.current !== flowSession) return;
3587
3987
  const verificationKey = getOnRampVerificationKey(record);
3588
3988
  if (triggeredVerificationKeysRef.current.has(verificationKey)) {
3589
3989
  emitDebug("verification:skip-duplicate", {
3590
- verificationKey,
3591
3990
  record: summariseOnRampRecord(record)
3592
3991
  });
3593
3992
  return;
@@ -3601,6 +4000,9 @@ function useFiatOnRamp(options) {
3601
4000
  triggeredVerificationKeysRef.current.add(verificationKey);
3602
4001
  activeVerificationKeyRef.current = verificationKey;
3603
4002
  activeVerificationRecordRef.current = record;
4003
+ activeVerificationSurfacesFailureRef.current = surfaceFailure || Boolean(
4004
+ activeIntentIdRef.current && matchesOnRampTransaction(record, activeIntentIdRef.current)
4005
+ );
3604
4006
  setActiveVerificationId(record.transaction_id);
3605
4007
  setFinalityProgress((prev) => {
3606
4008
  if (!(record.transaction_id in prev)) return prev;
@@ -3609,14 +4011,11 @@ function useFiatOnRamp(options) {
3609
4011
  return next;
3610
4012
  });
3611
4013
  emitDebug("verification:start", {
3612
- verificationKey,
3613
4014
  record: summariseOnRampRecord(record)
3614
4015
  });
3615
4016
  try {
3616
- if (!record.on_chain_tx_hash || !record.quote_currency_amount) {
3617
- throw new Error("On-ramp record missing on-chain tx hash or delivered amount");
3618
- }
3619
- if (record.chain_id === void 0 || !record.wallet_address) {
4017
+ if (!record.on_chain_tx_hash) throw new Error("On-ramp record missing on-chain tx hash");
4018
+ if (record.chain_id == null || !record.wallet_address) {
3620
4019
  throw new Error("On-ramp record missing chain id or wallet address");
3621
4020
  }
3622
4021
  const recordTokenId = record.token_id;
@@ -3635,20 +4034,22 @@ function useFiatOnRamp(options) {
3635
4034
  chainId: record.chain_id,
3636
4035
  walletAddress: record.wallet_address,
3637
4036
  token,
3638
- fallbackAmount: record.quote_currency_amount,
3639
4037
  wagmiConfig,
3640
4038
  emitDebug
3641
4039
  });
3642
- if (minDepositBaseUnits !== void 0 && amount < minDepositBaseUnits) {
4040
+ if (flowSessionRef.current !== flowSession) return;
4041
+ const recordMinDepositBaseUnits = erc20MinDepositBaseUnits(
4042
+ scopedMinDepositByChain,
4043
+ record.chain_id
4044
+ );
4045
+ if (recordMinDepositBaseUnits !== void 0 && amount < recordMinDepositBaseUnits) {
3643
4046
  emitDebug("verification:below-minimum", {
3644
- quoteCurrencyAmount: record.quote_currency_amount,
3645
- minDepositBaseUnits: String(minDepositBaseUnits)
4047
+ deliveredAmount: amount.toString(),
4048
+ minDepositBaseUnits: String(recordMinDepositBaseUnits)
3646
4049
  });
3647
- throw new Error(
3648
- `Delivered amount (${record.quote_currency_amount}) is below the minimum deposit.`
3649
- );
4050
+ throw new Error(`Delivered amount (${amount} base units) is below the minimum deposit.`);
3650
4051
  }
3651
- if (activeIntentIdRef.current === record.transaction_id) {
4052
+ if (activeIntentIdRef.current && matchesOnRampTransaction(record, activeIntentIdRef.current)) {
3652
4053
  setStatus("verifying");
3653
4054
  }
3654
4055
  emitDebug("verification:check-deposit-request", {
@@ -3662,107 +4063,180 @@ function useFiatOnRamp(options) {
3662
4063
  amount
3663
4064
  });
3664
4065
  } catch (err) {
4066
+ if (flowSessionRef.current !== flowSession) return;
3665
4067
  triggeredVerificationKeysRef.current.delete(verificationKey);
3666
4068
  if (activeVerificationKeyRef.current === verificationKey) {
3667
4069
  activeVerificationKeyRef.current = null;
3668
4070
  activeVerificationRecordRef.current = null;
4071
+ activeVerificationSurfacesFailureRef.current = false;
3669
4072
  setActiveVerificationId(null);
3670
4073
  }
3671
4074
  throw err;
3672
4075
  }
3673
4076
  },
3674
- [emitDebug, enabledTokens, minDepositBaseUnits, verify, wagmiConfig]
4077
+ [emitDebug, enabledTokens, flowSession, scopedMinDepositByChain, verify, wagmiConfig]
3675
4078
  );
3676
- const handleTransactionCompleted = useCallback(
3677
- async (props) => {
3678
- emitDebug("moonpay:onTransactionCompleted", summariseMoonPayEventProps(props));
4079
+ const handleProviderEvent = useCallback(
4080
+ async (event) => {
4081
+ if (flowSessionRef.current !== flowSession) return;
4082
+ let target;
3679
4083
  try {
3680
- setStatus("awaiting-delivery");
3681
- await registerOnRampTokenMapping(props.id);
3682
- const transactionId = activeIntentIdRef.current ?? props.id;
3683
- const record = await waitForOnChainHash(transactionId);
4084
+ target = resolveOnRampProviderEventTarget(
4085
+ adapter.provider,
4086
+ activeIntentIdRef.current,
4087
+ event
4088
+ );
4089
+ emitDebug(`provider:${event.kind}`, {
4090
+ provider: event.provider,
4091
+ intentIdPresent: Boolean(target.intentId),
4092
+ providerTransactionIdPresent: Boolean(event.providerTransactionId),
4093
+ stale: target.isStale
4094
+ });
4095
+ if (target.isActive) purchaseInitiatedRef.current = true;
4096
+ if (event.kind === "transaction-created") {
4097
+ await registerProviderTransaction(event, target.intentId);
4098
+ if (flowSessionRef.current !== flowSession) return;
4099
+ if (target.isStale) await refreshPending();
4100
+ return;
4101
+ }
4102
+ if (target.isActive || activeIntentIdRef.current === null) {
4103
+ statusRef.current = "awaiting-delivery";
4104
+ setStatus("awaiting-delivery");
4105
+ }
4106
+ await registerProviderTransaction(event, target.intentId);
4107
+ if (flowSessionRef.current !== flowSession) return;
4108
+ if (target.isStale) {
4109
+ await refreshPending();
4110
+ return;
4111
+ }
4112
+ const record = await waitForOnChainHash(target.intentId);
4113
+ if (flowSessionRef.current !== flowSession) return;
3684
4114
  if (!record) {
3685
- const err = new Error(
4115
+ const deliveryError = new Error(
3686
4116
  "Backend has not yet confirmed delivery. You can finish from the pending list."
3687
4117
  );
3688
- emitDebug("moonpay:completed-without-backend-row", {
3689
- transactionId,
3690
- moonpayTransactionId: props.id,
3691
- message: err.message
4118
+ emitDebug("provider:completed-without-backend-row", {
4119
+ provider: event.provider,
4120
+ message: deliveryError.message
3692
4121
  });
3693
- setStatus("failed");
3694
- setError(err);
3695
- onErrorRef.current?.(err);
4122
+ if (target.isActive || activeIntentIdRef.current === null) {
4123
+ statusRef.current = "failed";
4124
+ setStatus("failed");
4125
+ setError(deliveryError);
4126
+ onErrorRef.current?.(deliveryError);
4127
+ }
3696
4128
  return;
3697
4129
  }
3698
- await triggerVerification(record);
4130
+ await triggerVerification(record, true);
3699
4131
  } catch (err) {
3700
- const e = err instanceof Error ? err : new Error("Verification failed");
3701
- emitDebug("moonpay:onTransactionCompleted-error", errorPayload(e));
3702
- setStatus("failed");
3703
- setError(e);
3704
- onErrorRef.current?.(e);
4132
+ if (flowSessionRef.current !== flowSession) return;
4133
+ const providerError = err instanceof Error ? err : new Error("Verification failed");
4134
+ emitDebug("provider:event-error", {
4135
+ provider: event.provider,
4136
+ kind: event.kind,
4137
+ ...errorPayload(providerError)
4138
+ });
4139
+ if (!target?.isStale) {
4140
+ statusRef.current = "failed";
4141
+ setStatus("failed");
4142
+ setError(providerError);
4143
+ onErrorRef.current?.(providerError);
4144
+ }
3705
4145
  }
3706
4146
  },
3707
- [emitDebug, registerOnRampTokenMapping, triggerVerification, waitForOnChainHash]
4147
+ [
4148
+ adapter.provider,
4149
+ emitDebug,
4150
+ flowSession,
4151
+ refreshPending,
4152
+ registerProviderTransaction,
4153
+ triggerVerification,
4154
+ waitForOnChainHash
4155
+ ]
3708
4156
  );
3709
- const handleWidgetClosed = useCallback(async () => {
3710
- if (closeReconcilePromiseRef.current) return closeReconcilePromiseRef.current;
3711
- closeReconcilePromiseRef.current = (async () => {
4157
+ const handleProviderClosed = useCallback(async () => {
4158
+ if (flowSessionRef.current !== flowSession) return;
4159
+ const existing = closeReconcilePromiseRef.current;
4160
+ if (existing?.flowSession === flowSession) return existing.promise;
4161
+ const promise = (async () => {
3712
4162
  const previousStatus = statusRef.current;
3713
4163
  const transactionId = activeIntentIdRef.current;
3714
- emitDebug("moonpay:widget-closed-reconcile", {
4164
+ const action = getOnRampCloseRecoveryAction(transactionId, purchaseInitiatedRef.current);
4165
+ emitDebug("provider:closed-reconcile", {
4166
+ provider: adapter.provider,
3715
4167
  previousStatus,
3716
- transactionId
4168
+ intentPresent: Boolean(transactionId),
4169
+ action
3717
4170
  });
3718
- if (!transactionId) {
4171
+ if (action === "refresh") {
3719
4172
  await refreshPending();
3720
4173
  return;
3721
4174
  }
3722
- if (!purchaseInitiatedRef.current) {
3723
- emitDebug("moonpay:widget-closed-without-purchase", {
4175
+ if (action === "refresh-and-retain") {
4176
+ emitDebug("provider:closed-without-event", {
4177
+ provider: adapter.provider,
3724
4178
  previousStatus,
3725
- transactionId
4179
+ recoveryRetained: true
3726
4180
  });
3727
- if (address) clearPendingLock(address, transactionId);
3728
4181
  await refreshPending();
4182
+ if (flowSessionRef.current !== flowSession) return;
3729
4183
  if (previousStatus === "awaiting-purchase" || previousStatus === "awaiting-delivery") {
4184
+ statusRef.current = "idle";
3730
4185
  setStatus("idle");
3731
4186
  }
3732
4187
  return;
3733
4188
  }
3734
4189
  try {
4190
+ statusRef.current = "awaiting-delivery";
3735
4191
  setStatus("awaiting-delivery");
3736
4192
  const record = await waitForOnChainHash(transactionId);
4193
+ if (flowSessionRef.current !== flowSession) return;
3737
4194
  if (record) {
3738
- await triggerVerification(record);
4195
+ await triggerVerification(record, true);
3739
4196
  return;
3740
4197
  }
3741
4198
  await refreshPending();
4199
+ if (flowSessionRef.current !== flowSession) return;
3742
4200
  if (previousStatus === "awaiting-purchase" || previousStatus === "awaiting-delivery") {
4201
+ statusRef.current = "idle";
3743
4202
  setStatus("idle");
3744
4203
  }
3745
4204
  } catch (err) {
4205
+ if (flowSessionRef.current !== flowSession) return;
3746
4206
  const e = err instanceof Error ? err : new Error("On-ramp reconciliation failed");
3747
- emitDebug("moonpay:widget-closed-reconcile-error", errorPayload(e));
4207
+ emitDebug("provider:closed-reconcile-error", {
4208
+ provider: adapter.provider,
4209
+ ...errorPayload(e)
4210
+ });
3748
4211
  setStatus("failed");
3749
4212
  setError(e);
3750
4213
  onErrorRef.current?.(e);
3751
4214
  }
3752
4215
  })();
4216
+ const entry = { flowSession, promise };
4217
+ closeReconcilePromiseRef.current = entry;
3753
4218
  try {
3754
- await closeReconcilePromiseRef.current;
4219
+ await promise;
3755
4220
  } finally {
3756
- closeReconcilePromiseRef.current = null;
4221
+ if (closeReconcilePromiseRef.current === entry) {
4222
+ closeReconcilePromiseRef.current = null;
4223
+ }
3757
4224
  }
3758
- }, [address, emitDebug, refreshPending, triggerVerification, waitForOnChainHash]);
4225
+ }, [
4226
+ adapter.provider,
4227
+ emitDebug,
4228
+ flowSession,
4229
+ refreshPending,
4230
+ triggerVerification,
4231
+ waitForOnChainHash
4232
+ ]);
3759
4233
  const finishPendingVerification = useCallback(
3760
4234
  async (record) => {
3761
4235
  try {
3762
4236
  emitDebug("pending:finish-verification", {
3763
4237
  record: summariseOnRampRecord(record)
3764
4238
  });
3765
- await triggerVerification(record);
4239
+ await triggerVerification(record, true);
3766
4240
  } catch (err) {
3767
4241
  const e = err instanceof Error ? err : new Error("Verification failed");
3768
4242
  emitDebug("pending:finish-verification-error", errorPayload(e));
@@ -3781,22 +4255,17 @@ function useFiatOnRamp(options) {
3781
4255
  useEffect(() => {
3782
4256
  let cancelled = false;
3783
4257
  void (async () => {
3784
- for (const record of pending) {
3785
- if (cancelled) break;
3786
- if (!record.on_chain_tx_hash || !record.quote_currency_amount) continue;
3787
- const key = getOnRampVerificationKey(record);
3788
- if (triggeredVerificationKeysRef.current.has(key)) continue;
3789
- try {
3790
- await triggerVerificationRef.current(record);
3791
- } catch {
3792
- continue;
3793
- }
3794
- if (activeVerificationKeyRef.current === key) {
3795
- await new Promise((resolve) => {
3796
- activeVerificationDoneRef.current = resolve;
3797
- });
3798
- }
3799
- }
4258
+ await verifyPendingOnRampsSequentially({
4259
+ records: pending,
4260
+ shouldStop: () => cancelled,
4261
+ wasTriggered: (key) => triggeredVerificationKeysRef.current.has(key),
4262
+ trigger: (record) => triggerVerificationRef.current(record),
4263
+ // verify() resolves after scheduling status polling. Waiting for its
4264
+ // terminal callback prevents the next record from cancelling it.
4265
+ waitForTerminal: (key) => activeVerificationKeyRef.current === key ? new Promise((resolve) => {
4266
+ activeVerificationDoneRef.current = resolve;
4267
+ }) : Promise.resolve()
4268
+ });
3800
4269
  })();
3801
4270
  return () => {
3802
4271
  cancelled = true;
@@ -3809,15 +4278,15 @@ function useFiatOnRamp(options) {
3809
4278
  activeVerificationId,
3810
4279
  error,
3811
4280
  finalityProgress,
3812
- depositAddress,
3813
- minDepositBaseUnits,
4281
+ depositAddress: scopedDepositAddress,
4282
+ minDepositBaseUnits: scopedMinDepositBaseUnits,
3814
4283
  selectedToken,
3815
4284
  prepareOnRampIntent,
3816
- signUrl,
3817
- handleTransactionCreated,
3818
- handleTransactionCompleted,
4285
+ handleProviderLaunchReady,
4286
+ handleProviderLaunchFailed,
4287
+ handleProviderEvent,
3819
4288
  finishPendingVerification,
3820
- handleWidgetClosed,
4289
+ handleProviderClosed,
3821
4290
  refreshPending
3822
4291
  };
3823
4292
  }
@@ -3831,17 +4300,20 @@ function summariseToken(token) {
3831
4300
  }
3832
4301
  function summariseOnRampRecord(record) {
3833
4302
  return {
3834
- transaction_id: record.transaction_id,
3835
- external_transaction_id: record.external_transaction_id ?? null,
3836
- moonpay_transaction_id: record.moonpay_transaction_id ?? null,
4303
+ provider: record.provider,
4304
+ provider_asset_code: record.provider_asset_code,
3837
4305
  status: record.status,
3838
- wallet_address: record.wallet_address,
3839
4306
  token_id: record.token_id,
3840
4307
  chain_id: record.chain_id,
3841
4308
  moonpay_currency_code: record.moonpay_currency_code ?? null,
3842
- quote_currency_amount: record.quote_currency_amount ?? null,
3843
- on_chain_tx_hash: record.on_chain_tx_hash ?? null,
3844
- deposit_tx_hash: record.deposit_tx_hash ?? null,
4309
+ external_transaction_id_present: Boolean(record.external_transaction_id),
4310
+ provider_transaction_id_present: Boolean(record.provider_transaction_id),
4311
+ moonpay_transaction_id_present: Boolean(record.moonpay_transaction_id),
4312
+ wallet_address_present: Boolean(record.wallet_address),
4313
+ quote_currency_amount_present: record.quote_currency_amount != null,
4314
+ on_chain_tx_hash_present: Boolean(record.on_chain_tx_hash),
4315
+ deposit_id_present: Boolean(record.deposit_id),
4316
+ deposit_tx_hash_present: Boolean(record.deposit_tx_hash),
3845
4317
  deposit_triggered_at: record.deposit_triggered_at ?? null,
3846
4318
  credited_at: record.credited_at ?? null
3847
4319
  };
@@ -3851,13 +4323,10 @@ async function resolveDeliveredAmount({
3851
4323
  chainId,
3852
4324
  walletAddress,
3853
4325
  token,
3854
- fallbackAmount,
3855
4326
  wagmiConfig,
3856
4327
  emitDebug
3857
4328
  }) {
3858
- if (token.contract === zeroAddress) {
3859
- return parseUnits(fallbackAmount, token.decimals);
3860
- }
4329
+ assertErc20OnRampToken(token.contract);
3861
4330
  let receiptError;
3862
4331
  try {
3863
4332
  const receipt = await waitForTransactionReceipt(wagmiConfig, {
@@ -3866,63 +4335,142 @@ async function resolveDeliveredAmount({
3866
4335
  timeout: 6e4,
3867
4336
  pollingInterval: 4e3
3868
4337
  });
3869
- let delivered = 0n;
3870
- for (const log of receipt.logs) {
3871
- if (log.address.toLowerCase() !== token.contract.toLowerCase()) continue;
3872
- try {
3873
- const decoded = decodeEventLog({
3874
- abi: [ERC20_TRANSFER_EVENT],
3875
- data: log.data,
3876
- topics: log.topics
3877
- });
3878
- if (decoded.eventName !== "Transfer") continue;
3879
- const to = decoded.args.to.toLowerCase();
3880
- if (to !== walletAddress.toLowerCase()) continue;
3881
- delivered += decoded.args.value;
3882
- } catch {
3883
- }
3884
- }
4338
+ const delivered = deliveredErc20Amount(receipt.logs, token.contract, walletAddress);
3885
4339
  if (delivered > 0n) {
3886
4340
  emitDebug("verification:amount-from-receipt", {
3887
4341
  amount: delivered.toString(),
3888
4342
  tokenAddress: token.contract,
3889
- walletAddress,
3890
- moonpayQuoteCurrencyAmount: fallbackAmount
4343
+ depositAddressMatched: true
3891
4344
  });
3892
4345
  return delivered;
3893
4346
  }
3894
4347
  emitDebug("verification:amount-from-receipt-missing", {
3895
4348
  tokenAddress: token.contract,
3896
- walletAddress,
3897
- moonpayQuoteCurrencyAmount: fallbackAmount
4349
+ depositAddressMatched: false
3898
4350
  });
3899
4351
  } catch (err) {
3900
4352
  emitDebug("verification:amount-from-receipt-error", errorPayload(err));
3901
4353
  receiptError = err;
3902
4354
  }
3903
- const errorDetail = receiptError instanceof Error ? receiptError.message : receiptError === void 0 ? `no ${token.symbol} Transfer to ${walletAddress} found` : String(receiptError);
4355
+ const errorDetail = receiptError instanceof Error ? receiptError.message : receiptError === void 0 ? `no ${token.symbol} Transfer to the derived deposit address found` : String(receiptError);
3904
4356
  throw new Error(
3905
- `Unable to derive delivered ${token.symbol} amount from ${onChainTxHash}: ${errorDetail}`
4357
+ `Unable to derive delivered ${token.symbol} amount from its receipt: ${errorDetail}`
3906
4358
  );
3907
4359
  }
3908
- function matchesOnRampTransaction(record, transactionId) {
3909
- return record.transaction_id === transactionId || record.external_transaction_id === transactionId || record.moonpay_transaction_id === transactionId;
4360
+ function errorPayload(err) {
4361
+ if (err instanceof Error) {
4362
+ return {
4363
+ name: err.name,
4364
+ message: err.message,
4365
+ stack: err.stack?.split("\n").slice(0, 4).join("\n")
4366
+ };
4367
+ }
4368
+ return { message: String(err) };
3910
4369
  }
3911
- function getOnRampVerificationKey(record) {
3912
- return record.on_chain_tx_hash ?? record.transaction_id;
4370
+
4371
+ // src/sdk/hooks/use-fiat-on-ramp.ts
4372
+ function useFiatOnRamp(options) {
4373
+ const { executePrivateRead } = usePrivateReadRequest();
4374
+ const core = useOnRamp({ ...options, adapter: moonPayOnRampAdapter });
4375
+ const {
4376
+ prepareOnRampIntent: prepareProviderIntent,
4377
+ handleProviderLaunchReady,
4378
+ handleProviderLaunchFailed,
4379
+ handleProviderEvent,
4380
+ handleProviderClosed
4381
+ } = core;
4382
+ const debugRef = useRef(options.onDebugEvent);
4383
+ const statusRef = useRef(core.status);
4384
+ useEffect(() => {
4385
+ debugRef.current = options.onDebugEvent;
4386
+ statusRef.current = core.status;
4387
+ }, [core.status, options.onDebugEvent]);
4388
+ const emitDebug = useCallback(
4389
+ (event, payload) => {
4390
+ debugRef.current?.({
4391
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4392
+ event,
4393
+ status: statusRef.current,
4394
+ tokenId: options.tokenId,
4395
+ payload
4396
+ });
4397
+ },
4398
+ [options.tokenId]
4399
+ );
4400
+ const prepareOnRampIntent = useCallback(
4401
+ ({
4402
+ currencyCode,
4403
+ baseCurrencyCode,
4404
+ baseCurrencyAmount,
4405
+ quoteCurrencyAmount
4406
+ }) => prepareProviderIntent({
4407
+ providerAssetCode: currencyCode,
4408
+ baseCurrencyCode,
4409
+ baseCurrencyAmount,
4410
+ quoteCurrencyAmount
4411
+ }),
4412
+ [prepareProviderIntent]
4413
+ );
4414
+ const signUrl = useCallback(
4415
+ async (url) => {
4416
+ emitDebug("moonpay:onUrlSignatureRequested", summariseMoonPayUrl(url));
4417
+ try {
4418
+ const { signature } = await executePrivateRead(
4419
+ (readClient) => readClient.signOnRampUrl({ url })
4420
+ );
4421
+ handleProviderLaunchReady();
4422
+ emitDebug("sign-url:success", { signatureLength: signature.length });
4423
+ return signature;
4424
+ } catch (error) {
4425
+ const launchError = error instanceof Error ? error : new Error("Failed to sign on-ramp URL");
4426
+ handleProviderLaunchFailed(launchError);
4427
+ emitDebug("sign-url:error", errorPayload2(launchError));
4428
+ throw launchError;
4429
+ }
4430
+ },
4431
+ [emitDebug, executePrivateRead, handleProviderLaunchFailed, handleProviderLaunchReady]
4432
+ );
4433
+ const handleTransactionCreated = useCallback(
4434
+ async (props) => {
4435
+ emitDebug("moonpay:onTransactionCreated", summariseMoonPayEventProps(props));
4436
+ await handleProviderEvent(normalizeMoonPayProviderEvent("transaction-created", props));
4437
+ },
4438
+ [emitDebug, handleProviderEvent]
4439
+ );
4440
+ const handleTransactionCompleted = useCallback(
4441
+ async (props) => {
4442
+ emitDebug("moonpay:onTransactionCompleted", summariseMoonPayEventProps(props));
4443
+ await handleProviderEvent(normalizeMoonPayProviderEvent("transaction-completed", props));
4444
+ },
4445
+ [emitDebug, handleProviderEvent]
4446
+ );
4447
+ return {
4448
+ status: core.status,
4449
+ activeIntentId: core.activeIntentId,
4450
+ pending: core.pending,
4451
+ activeVerificationId: core.activeVerificationId,
4452
+ error: core.error,
4453
+ finalityProgress: core.finalityProgress,
4454
+ depositAddress: core.depositAddress,
4455
+ minDepositBaseUnits: core.minDepositBaseUnits,
4456
+ selectedToken: core.selectedToken,
4457
+ finishPendingVerification: core.finishPendingVerification,
4458
+ refreshPending: core.refreshPending,
4459
+ prepareOnRampIntent,
4460
+ signUrl,
4461
+ handleTransactionCreated,
4462
+ handleTransactionCompleted,
4463
+ handleWidgetClosed: handleProviderClosed
4464
+ };
3913
4465
  }
3914
4466
  function summariseMoonPayEventProps(props) {
3915
4467
  return {
3916
- id: props.id,
3917
- externalTransactionId: props.externalTransactionId,
4468
+ transactionIdPresent: Boolean(props.id),
4469
+ externalTransactionIdPresent: Boolean(props.externalTransactionId),
3918
4470
  status: props.status,
3919
- walletAddress: props.walletAddress,
3920
- walletAddressTag: props.walletAddressTag,
3921
- baseCurrencyAmount: props.baseCurrencyAmount,
3922
- quoteCurrencyAmount: props.quoteCurrencyAmount,
3923
4471
  baseCurrency: props.baseCurrency,
3924
4472
  quoteCurrency: props.quoteCurrency,
3925
- createdAt: props.createdAt
4473
+ walletAddressPresent: Boolean(props.walletAddress)
3926
4474
  };
3927
4475
  }
3928
4476
  function summariseMoonPayUrl(url) {
@@ -3932,36 +4480,34 @@ function summariseMoonPayUrl(url) {
3932
4480
  return {
3933
4481
  origin: parsed.origin,
3934
4482
  pathname: parsed.pathname,
3935
- apiKeyPrefix: params.get("apiKey")?.slice(0, 8) ?? null,
4483
+ apiKeyPresent: params.has("apiKey"),
3936
4484
  currencyCode: params.get("currencyCode"),
3937
4485
  baseCurrencyCode: params.get("baseCurrencyCode"),
3938
- baseCurrencyAmount: params.get("baseCurrencyAmount"),
3939
- walletAddress: params.get("walletAddress"),
3940
- externalCustomerId: params.get("externalCustomerId"),
3941
- externalTransactionId: params.get("externalTransactionId"),
3942
- redirectURL: params.get("redirectURL"),
4486
+ baseCurrencyAmountPresent: params.has("baseCurrencyAmount"),
4487
+ walletAddressPresent: params.has("walletAddress"),
4488
+ externalCustomerIdPresent: params.has("externalCustomerId"),
4489
+ externalTransactionIdPresent: params.has("externalTransactionId"),
4490
+ redirectURLPresent: params.has("redirectURL"),
3943
4491
  signaturePresent: params.has("signature")
3944
4492
  };
3945
4493
  } catch {
3946
4494
  return { parseError: true, length: url.length };
3947
4495
  }
3948
4496
  }
3949
- function errorPayload(err) {
3950
- if (err instanceof Error) {
3951
- return {
3952
- name: err.name,
3953
- message: err.message,
3954
- stack: err.stack?.split("\n").slice(0, 4).join("\n")
3955
- };
3956
- }
3957
- return { message: String(err) };
4497
+ function errorPayload2(error) {
4498
+ return {
4499
+ name: error.name,
4500
+ message: error.message,
4501
+ stack: error.stack?.split("\n").slice(0, 4).join("\n")
4502
+ };
3958
4503
  }
3959
- function useMoonPayBuyWidget({
4504
+ function useMoonPayOnRampAdapter({
3960
4505
  variant,
3961
4506
  visible,
3962
4507
  autoStart,
3963
4508
  canBuy,
3964
4509
  openWidget,
4510
+ shouldPollPending,
3965
4511
  refreshPending,
3966
4512
  theme,
3967
4513
  themeId,
@@ -3989,10 +4535,12 @@ function useMoonPayBuyWidget({
3989
4535
  void openWidget();
3990
4536
  }, [autoStart, canBuy, openWidget]);
3991
4537
  useEffect(() => {
3992
- if (variant !== "embedded" || !visible) return;
3993
- const id = setInterval(() => void refreshPending(), 5e3);
4538
+ if (!moonPayOnRampAdapter.pollPendingWhileOpen || variant !== "embedded" || !visible || !shouldPollPending) {
4539
+ return;
4540
+ }
4541
+ const id = setInterval(() => void refreshPending(), MIN_ONRAMP_PENDING_REQUEST_INTERVAL_MS);
3994
4542
  return () => clearInterval(id);
3995
- }, [variant, visible, refreshPending]);
4543
+ }, [variant, visible, shouldPollPending, refreshPending]);
3996
4544
  const callbacksRef = useRef({
3997
4545
  onClose,
3998
4546
  onCloseOverlay,
@@ -4180,9 +4728,9 @@ function FiatOnRampForm({
4180
4728
  tokenSymbol: displaySymbol,
4181
4729
  tokenDecimals: decimals ?? null,
4182
4730
  baseCurrencyCode,
4183
- defaultBaseCurrencyAmount,
4184
- depositAddress: depositAddress ?? null,
4185
- walletAddress: address ?? null,
4731
+ baseCurrencyAmountPresent: Boolean(defaultBaseCurrencyAmount),
4732
+ depositAddressReady: Boolean(depositAddress),
4733
+ walletConnected: Boolean(address),
4186
4734
  status
4187
4735
  });
4188
4736
  return;
@@ -4195,8 +4743,8 @@ function FiatOnRampForm({
4195
4743
  tokenSymbol: displaySymbol,
4196
4744
  tokenDecimals: decimals ?? null,
4197
4745
  baseCurrencyCode,
4198
- defaultBaseCurrencyAmount,
4199
- depositAddress: depositAddress ?? null,
4746
+ baseCurrencyAmountPresent: Boolean(defaultBaseCurrencyAmount),
4747
+ depositAddressReady: Boolean(depositAddress),
4200
4748
  walletConnected: Boolean(address)
4201
4749
  });
4202
4750
  try {
@@ -4207,8 +4755,8 @@ function FiatOnRampForm({
4207
4755
  quoteCurrencyAmount
4208
4756
  });
4209
4757
  emitFormDebug("form:intent-ready", {
4210
- transactionId: intent.transaction_id,
4211
- externalTransactionId: intent.external_transaction_id ?? null
4758
+ transactionIdPresent: Boolean(intent.transaction_id),
4759
+ externalTransactionIdPresent: Boolean(intent.external_transaction_id)
4212
4760
  });
4213
4761
  setVisible(true);
4214
4762
  } catch (err) {
@@ -4248,12 +4796,13 @@ function FiatOnRampForm({
4248
4796
  const handleReady = useCallback(async () => {
4249
4797
  emitFormDebug("moonpay:onReady");
4250
4798
  }, [emitFormDebug]);
4251
- const widgetElement = useMoonPayBuyWidget({
4799
+ const widgetElement = useMoonPayOnRampAdapter({
4252
4800
  variant,
4253
4801
  visible,
4254
4802
  autoStart,
4255
4803
  canBuy,
4256
4804
  openWidget: handleOpen,
4805
+ shouldPollPending: status === "awaiting-purchase",
4257
4806
  refreshPending,
4258
4807
  theme,
4259
4808
  themeId,
@@ -4274,6 +4823,9 @@ function FiatOnRampForm({
4274
4823
  onTransactionCreated: handleTransactionCreated,
4275
4824
  onTransactionCompleted: handleTransactionCompleted
4276
4825
  });
4826
+ useEffect(() => {
4827
+ if (visible && !activeIntentId && status === "idle") setVisible(false);
4828
+ }, [activeIntentId, status, visible]);
4277
4829
  useEffect(() => {
4278
4830
  if (variant !== "embedded" || !visible) return;
4279
4831
  if (isVerifying || status === "credited") {
@@ -4374,5 +4926,5 @@ function parseFinalityProgress(message) {
4374
4926
  }
4375
4927
 
4376
4928
  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 };
4377
- //# sourceMappingURL=chunk-DZURIIRE.js.map
4378
- //# sourceMappingURL=chunk-DZURIIRE.js.map
4929
+ //# sourceMappingURL=chunk-PMBU2F2R.js.map
4930
+ //# sourceMappingURL=chunk-PMBU2F2R.js.map