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