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