@oasisprotocol/privana-sdk 0.5.4 → 0.5.6

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