UNPKG

@funkit/connect

Version:

Funkit Connect SDK elevates DeFi apps via web2 sign-ins and one-click checkouts.

667 lines (656 loc) 21.8 kB
"use client"; import { AsyncImage } from "./chunk-2NBYHUIW.js"; import { useFunkitConfig } from "./chunk-E3USJY7A.js"; import { Box } from "./chunk-LUHEVXPD.js"; import "./chunk-UPZNKPYU.js"; import { useFunkitTranslation } from "./chunk-STLOQEN7.js"; import { logger } from "./chunk-AHYOV7AV.js"; // src/clients/lighter.tsx import { DirectExecutionType, LIGHTERXYZ_API_KEY as LIGHTERXYZ_API_KEY2, createDirectExecution } from "@funkit/api-base"; import { LIGHTER_CHAIN_ID, SOLANA_MAINNET_CHAIN_ID } from "@funkit/chains"; import { RELAY_LIGHTER_CHAIN_ID as RELAY_LIGHTER_CHAIN_ID2 } from "@funkit/fun-relay"; import { retry } from "@lifeomic/attempt"; import { useQuery as useQuery2 } from "@tanstack/react-query"; import i18next from "i18next"; import React3, { useMemo as useMemo2 } from "react"; import { v4 as uuid } from "uuid"; import { pad, parseUnits, toHex } from "viem"; import { arbitrum, base, bsc, mainnet as mainnet3, optimism } from "viem/chains"; // src/components/Icons/EvmWallet.tsx import React from "react"; var EvmWallet = ({ size = 20 }) => /* @__PURE__ */ React.createElement( "svg", { width: size, height: size, viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ React.createElement( "path", { d: "M13.3333 6.66628V3.75016C13.3333 3.05704 13.3333 2.71048 13.1873 2.49751C13.0598 2.31143 12.8622 2.18503 12.6398 2.14718C12.3852 2.10386 12.0706 2.24909 11.4413 2.53955L4.04918 5.95127C3.48792 6.21031 3.20729 6.33984 3.00175 6.54071C2.82005 6.71829 2.68135 6.93507 2.59625 7.17447C2.5 7.44527 2.5 7.75435 2.5 8.3725V12.4996M13.75 12.0829H13.7583M2.5 9.33295L2.5 14.8329C2.5 15.7664 2.5 16.2331 2.68166 16.5896C2.84144 16.9032 3.09641 17.1582 3.41002 17.318C3.76654 17.4996 4.23325 17.4996 5.16667 17.4996H14.8333C15.7668 17.4996 16.2335 17.4996 16.59 17.318C16.9036 17.1582 17.1586 16.9032 17.3183 16.5896C17.5 16.2331 17.5 15.7664 17.5 14.8329V9.33295C17.5 8.39953 17.5 7.93282 17.3183 7.5763C17.1586 7.26269 16.9036 7.00773 16.59 6.84794C16.2335 6.66628 15.7668 6.66628 14.8333 6.66628L5.16667 6.66628C4.23325 6.66628 3.76654 6.66628 3.41002 6.84793C3.09641 7.00772 2.84144 7.26269 2.68166 7.57629C2.5 7.93281 2.5 8.39952 2.5 9.33295ZM14.1667 12.0829C14.1667 12.3131 13.9801 12.4996 13.75 12.4996C13.5199 12.4996 13.3333 12.3131 13.3333 12.0829C13.3333 11.8528 13.5199 11.6663 13.75 11.6663C13.9801 11.6663 14.1667 11.8528 14.1667 12.0829Z", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" } ) ); // src/modals/WithdrawalModal/LighterWithdrawal.tsx import { RELAY_LIGHTER_CHAIN_ID } from "@funkit/fun-relay"; import { formatAddress, formatCryptoAndStringify } from "@funkit/utils"; import * as dnum from "dnum"; import React2, { useEffect, useMemo } from "react"; import { createPortal } from "react-dom"; import { Trans } from "react-i18next"; import { mainnet as mainnet2 } from "viem/chains"; // src/utils/customer.ts import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback } from "react"; // src/consts/customers.ts import { AAVE_API_KEY, AVANTIS_API_KEY, BASED_API_KEY, BSX_API_KEY, ETHEREAL_API_KEY, FANATICS_API_KEY, HYENA_API_KEY, HYPERDASH_API_KEY, LIGHTERXYZ_API_KEY, MALLARD_API_KEY, MONAD_API_KEY, MONKEY_TILT_API_KEY, NADO_API_KEY, OSTIUM_API_KEY, PERPL_API_KEY, POLYMARKET_API_KEY } from "@funkit/api-base"; import { mainnet } from "viem/chains"; function isLighterxyzCustomer(apiKey) { return apiKey === LIGHTERXYZ_API_KEY; } // src/utils/customer.ts var LIGHTER_MAIN_ACCOUNT_TYPE = 0; var LIGHTER_TRADING_MODE = { SIMPLE: 0, UNIFIED: 1 }; async function getLighterAccountsByL1Address(address) { const response = await fetch( `https://mainnet.zklighter.elliot.ai/api/v1/accountsByL1Address?l1_address=${address}` ); if (!response.ok) { throw new Error(`Failed to fetch lighter accounts: ${response.statusText}`); } const data = await response.json(); return data; } function useLighterAccounts({ address }) { const { apiKey } = useFunkitConfig(); const isLighter = isLighterxyzCustomer(apiKey); const enabled = !!address && address !== "0x" && isLighter; const query = useQuery({ queryKey: ["lighterAccounts", address], queryFn: () => getLighterAccountsByL1Address(address), enabled, staleTime: Number.POSITIVE_INFINITY, gcTime: Number.POSITIVE_INFINITY, retry: false, // allows us to always refetch the data but return the previous data until the new data is fetched refetchOnMount: "always" }); const subAccounts = query.data?.sub_accounts; const mainAccount = subAccounts?.find( (account) => account.account_type === LIGHTER_MAIN_ACCOUNT_TYPE ); return { ...query, mainAccountIndex: mainAccount?.index?.toString(), // False until the accounts load — consumers fall back to the simple-mode // UI (both USDC variants shown) while the trading mode is unknown. isUnifiedTradingMode: mainAccount?.account_trading_mode === LIGHTER_TRADING_MODE.UNIFIED, subAccounts }; } // src/providers/provideFunkitConnectChains.ts import { chainMetadataById } from "@funkit/chains"; // src/utils/timeFormat.ts import { formatSecondsToReadableForm } from "@funkit/utils"; function formatSecondsTranslated(seconds, t, specifyUnderMinute = false, omitSeconds = false) { return formatSecondsToReadableForm(seconds, specifyUnderMinute, omitSeconds, { lessThanOneMin: t("time.lessThanOneMin"), seconds: (count) => t("time.seconds", { count }), minutes: (count) => t("time.minutes", { count }), hours: (count) => t("time.hours", { count }), secondsShort: (count) => t("time.secondsShort", { count }) }); } // src/modals/WithdrawalModal/LighterWithdrawal.tsx var LIGHTER_FAST_METHOD_ID = "lighter-fast"; var LIGHTER_SECURE_METHOD_ID = "lighter-secure"; var LIGHTER_ROUTE_TYPE = { PERPS: 0, SPOT: 1 }; function getLighterBaseSymbol(symbol) { return symbol.replace(/\s*\((?:spot|perps)\)\s*$/i, ""); } var LIGHTER_FAST_MAX_WITHDRAWAL_USD = 3e6; // src/clients/lighter.tsx var LIGHTER_USDC_PERPS_ADDRESS = "0x0000000000000000000000000000000000000000"; var MAINNET_USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; var LIGHTER_SECURE_ASSETS_BY_INDEX = { 1: { symbol: "ETH", decimals: 18 }, 2: { symbol: "LIT", decimals: 18 }, 3: { symbol: "USDC", decimals: 6 }, 5: { symbol: "LINK", decimals: 18 }, 6: { symbol: "UNI", decimals: 18 }, 7: { symbol: "AAVE", decimals: 18 }, 8: { symbol: "SKY", decimals: 18 }, 9: { symbol: "LDO", decimals: 18 }, 10: { symbol: "AZTEC", decimals: 18 } }; var LIGHTER_API_BASE = "https://mainnet.zklighter.elliot.ai"; async function getLighterWithdrawalDelay() { const response = await fetch(`${LIGHTER_API_BASE}/api/v1/withdrawalDelay`); if (!response.ok) { throw new Error( `Failed to fetch Lighter withdrawal delay: ${response.statusText}` ); } return response.json(); } function useLighterSecureDelayText() { const { t } = useFunkitTranslation(); const query = useQuery2({ queryKey: ["lighterWithdrawalDelay"], queryFn: getLighterWithdrawalDelay, staleTime: 6e4, retry: 2 }); const seconds = query.data?.seconds; if (seconds === void 0 || !Number.isFinite(seconds) || seconds <= 0) { return void 0; } return formatSecondsTranslated( seconds, t, /*specifyUnderMinutes*/ false, /*omitSeconds*/ true ); } var LIGHTER_TX_STATUS = { PENDING: 0, QUEUED: 1, COMMITTED: 2, EXECUTED: 3, FAILED: 4, REJECTED: 5 }; function coerceLighterStatus(status) { if (typeof status === "number") { return status; } switch (status) { case "confirmed": return LIGHTER_TX_STATUS.EXECUTED; case "failed": return LIGHTER_TX_STATUS.FAILED; default: return LIGHTER_TX_STATUS.PENDING; } } var LighterTerminalError = class extends Error { }; async function pollLighterTransaction(txHash, options) { const { pollIntervalMs, maxAttempts, waitFor } = options; return retry( async () => { const resp = await fetch( `${LIGHTER_API_BASE}/api/v1/tx?by=hash&value=${txHash}` ); if (!resp.ok) { throw new Error(`Lighter API error ${resp.status} for tx ${txHash}`); } const tx = await resp.json(); if (!tx.hash) { throw new Error(`Lighter tx ${txHash} not found yet`); } const status = coerceLighterStatus(tx.status ?? LIGHTER_TX_STATUS.PENDING); if (status === LIGHTER_TX_STATUS.FAILED || status === LIGHTER_TX_STATUS.REJECTED) { const label = status === LIGHTER_TX_STATUS.FAILED ? "failed" : "rejected"; throw new LighterTerminalError( `Lighter transaction ${label}: ${tx.message ?? "unknown error"}` ); } if (waitFor === "findable") { return tx; } if (status === LIGHTER_TX_STATUS.COMMITTED || status === LIGHTER_TX_STATUS.EXECUTED) { return tx; } throw new Error( `Lighter transaction ${txHash} did not reach '${waitFor}' after ${maxAttempts} attempts` ); }, { maxAttempts, delay: pollIntervalMs, factor: 1, jitter: false, handleError: (err, context) => { if (err instanceof LighterTerminalError) { context.abort(); } } } ); } function createLighterAdaptedWallet({ signerClient, accountIndex }) { async function handleSendTransactionStep(_chainId, stepItem) { const action = stepItem.data?.action; if (!action || action.type !== "transfer") { throw new Error( `Unsupported Lighter action: ${action?.type ?? "undefined"}` ); } const p = action.parameters; logger.info("lighter:wallet:transfer", p); const [, txHash, error] = await signerClient.transfer({ toAccountIndex: p.toAccountIndex, assetIndex: p.assetIndex, fromRouteType: p.fromRouteType, toRouteType: p.toRouteType, amount: p.amount, usdcFee: p.usdcFee, memo: p.memo }); if (error) { throw new Error(`Lighter transfer failed: ${error}`); } if (!txHash) { throw new Error("Lighter transfer returned no transaction hash"); } logger.info("lighter:wallet:transfer:done", { txHash }); return txHash; } return { vmType: "lvm", getChainId: async () => Number(RELAY_LIGHTER_CHAIN_ID2), address: async () => accountIndex.toString(), handleSignMessageStep: async () => { throw new Error("Message signing not implemented for Lighter"); }, handleSendTransactionStep, handleConfirmTransactionStep: async (txHash) => { const tx = await pollLighterTransaction(txHash, { pollIntervalMs: 2e3, maxAttempts: 60, waitFor: "findable" }); return { txHash: tx.hash, blockHeight: tx.block_height ?? 0, status: tx.status }; }, // biome-ignore lint/suspicious/noEmptyBlockStatements: Lighter is single-chain — no switching needed switchChain: async () => { } }; } var LIGHTER_DEFAULT_SECURE_MIN_WITHDRAWAL_AMOUNTS = { USDC: 1, ETH: 1e-3, LIT: 1, LINK: 0.1, UNI: 0.2, AAVE: 0.01, SKY: 20, LDO: 2, AZTEC: 100 }; function truncateAmountToDecimals(amount, decimals) { const [whole = "0", frac = ""] = amount.split("."); if (frac.length <= decimals) { return amount; } const truncated = frac.slice(0, decimals); return truncated ? `${whole}.${truncated}` : whole; } async function trackLighterSecureWithdrawal({ apiKey, userId, txHash, recipientAddr, assetIndex, mainnetTokenAddress, amountTokenUnits, withdrawalUSD }) { const assetMeta = LIGHTER_SECURE_ASSETS_BY_INDEX[assetIndex]; if (!assetMeta) { logger.error( "lighter:withdrawal:secure:tracking:unknownAssetIndex", new Error(`Unknown Lighter Secure asset index: ${assetIndex}`), { assetIndex, txHash } ); return; } const { symbol, decimals } = assetMeta; const estTotalUsd = Number(withdrawalUSD); const safeAmount = truncateAmountToDecimals(amountTokenUnits, decimals); const baseUnitAmount = parseUnits(safeAmount, decimals).toString(); const fromTokenAddress = pad(toHex(assetIndex), { size: 20 }); await createDirectExecution({ apiKey, logger, // Lighter L2 tx hashes are not 0x-prefixed EVM hashes — they're the L2's // own format. We cast here for convenience to satisfy the DE row type; // the backend stores the literal string and we never use it as an EVM hash. txHash, type: DirectExecutionType.EXTERNALLY_FULFILLED, userId, recipientAddr, fromChainId: LIGHTER_CHAIN_ID.toString(), fromTokenAddress, toChainId: mainnet3.id.toString(), toTokenAddress: mainnetTokenAddress, fromAmountBaseUnit: baseUnitAmount, toAmountBaseUnit: baseUnitAmount, estTotalUsd, // Secure withdrawals come out of the user's Lighter account balance; the // L1 owner of that account is the recipient (Secure locks to connected wallet). sourceOfFund: `balance|lighter-secure|${recipientAddr}`, clientMetadata: { id: uuid(), startTimestampMs: Date.now(), finalDollarValue: estTotalUsd, latestQuote: null, depositAddress: null, initSettings: { config: { targetAsset: mainnetTokenAddress, targetChain: mainnet3.id.toString(), targetAssetTicker: symbol } }, selectedSourceAssetInfo: { address: fromTokenAddress, symbol, chainId: LIGHTER_CHAIN_ID.toString(), iconSrc: null }, selectedPaymentMethodInfo: { paymentMethod: "LIGHTER_SECURE_WITHDRAWAL", title: "", description: "" }, isWithdrawal: true } }); } function buildSecureWithdrawalCallback({ exec }) { return async (param) => { const { targetAssetAddress, destinationAddress } = param; const customerData = param.customerData?.customerName === "lighter" ? param.customerData : void 0; if (!customerData) { logger.error( "lighter:withdrawal:secure:missingCustomerData", new Error("Missing Lighter customerData for Secure withdrawal"), { targetAssetAddress, destinationAddress } ); return; } const { assetIndex, routeType } = customerData; const amountTokenUnits = param.userInputAmount; if (!amountTokenUnits) { logger.error( "lighter:withdrawal:secure:missingAmount", new Error("Missing userInputAmount for Secure withdrawal"), { assetIndex, targetAssetAddress, destinationAddress } ); return; } const withdrawalUSD = param.withdrawalUSD ?? "0"; logger.info("lighter:withdrawal:secure:start", { assetIndex, routeType, amountTokenUnits, targetAssetAddress, destinationAddress }); const txHash = await exec({ amountTokenUnits, assetIndex, routeType }); logger.info("lighter:withdrawal:secure:submitted", { amountTokenUnits, assetIndex, routeType, txHash }); void trackLighterSecureWithdrawal({ apiKey: LIGHTERXYZ_API_KEY2, // Secure locks the destination to the connected wallet, so the L1 // recipient address doubles as the userId for tracking purposes. userId: destinationAddress, txHash, recipientAddr: destinationAddress, assetIndex, mainnetTokenAddress: targetAssetAddress, amountTokenUnits, withdrawalUSD }).catch((err) => { logger.warn("lighter:withdrawal:secure:trackingFailed", { error: err instanceof Error ? err.message : String(err), txHash, assetIndex, amountTokenUnits }); }); return txHash; }; } function createLighterSecureWithdrawalConfig(config) { return { modalTitle: config.modalTitle ?? i18next.t("withdrawal.withdraw"), disableConnectedWallet: config.disableConnectedWallet, onBeforeSign: config.onBeforeSign, withdrawCallback: buildSecureWithdrawalCallback({ exec: config.sendLighterSecureWithdrawal }), sourceChainId: mainnet3.id.toString(), sourceTokenSymbol: "USDC", defaultReceiveToken: "USDC", sourceTokenAddress: MAINNET_USDC, iconSrc: config.iconSrc ?? "https://sdk-cdn.fun.xyz/images/logos/lighter.png", getMinWithdrawalUSD: () => 0, getMinWithdrawalAmount: config.getMinWithdrawalAmount }; } function buildLighterFastWalletWithdrawalConfig({ signerClient, accountIndex, modalTitle, disableConnectedWallet, iconSrc, onBeforeSign }) { const adaptedWallet = createLighterAdaptedWallet({ signerClient, accountIndex }); const wallet = { _type: "lighter", adaptedWallet, address: () => String(accountIndex), getChainId: async () => Number(RELAY_LIGHTER_CHAIN_ID2), // biome-ignore lint/suspicious/noEmptyBlockStatements: no-op — Lighter has no EVM chain switching switchChain: async () => { }, sendTransaction: async () => { throw new Error("Use adaptedWallet for Lighter execution"); }, confirmTransaction: async () => { throw new Error("Use adaptedWallet for Lighter execution"); } }; return { modalTitle: modalTitle ?? i18next.t("withdrawal.withdraw"), disableConnectedWallet, onBeforeSign, wallet, sourceChainId: RELAY_LIGHTER_CHAIN_ID2, sourceTokenSymbol: "USDC", defaultReceiveToken: "USDC", sourceTokenAddress: LIGHTER_USDC_PERPS_ADDRESS, iconSrc: iconSrc ?? "https://sdk-cdn.fun.xyz/images/logos/lighter.png", getMinWithdrawalUSD: () => 4, getMaxWithdrawalUSD: () => LIGHTER_FAST_MAX_WITHDRAWAL_USD }; } function ChainIconStack({ chainIds }) { return /* @__PURE__ */ React3.createElement(Box, { display: "flex", alignItems: "center", justifyContent: "flex-end" }, chainIds.map((chainId, index) => { const metadata = chainMetadataById[chainId]; return /* @__PURE__ */ React3.createElement( Box, { key: chainId, position: "relative", style: { right: `${(chainIds.length - index - 1) * -2}px` } }, /* @__PURE__ */ React3.createElement( AsyncImage, { alt: metadata?.name ?? "", borderRadius: "full", src: metadata?.iconUrl ?? "", height: 16, width: 16 } ) ); })); } var LIGHTER_FAST_PREVIEW_CHAIN_IDS = [ arbitrum.id, base.id, optimism.id, bsc.id, SOLANA_MAINNET_CHAIN_ID ]; var LIGHTER_SECURE_PREVIEW_CHAIN_IDS = [mainnet3.id]; function buildLighterMultiMethodConfig({ config, accountIndex, t, secureDelayText, isUnifiedTradingMode }) { const fastConfig = buildLighterFastWalletWithdrawalConfig({ signerClient: config.signerClient, accountIndex: Number(accountIndex), modalTitle: config.modalTitle, disableConnectedWallet: config.disableConnectedWallet, iconSrc: config.fastIconSrc, onBeforeSign: config.onBeforeSign }); const secureConfig = createLighterSecureWithdrawalConfig({ accountIndex, modalTitle: config.modalTitle, disableConnectedWallet: config.disableConnectedWallet, iconSrc: config.secureIconSrc, sendLighterSecureWithdrawal: config.sendLighterSecureWithdrawal, onBeforeSign: config.onBeforeSign, getMinWithdrawalAmount: (symbol) => { const userMin = config.getSecureMinWithdrawalAmount?.(symbol); if (userMin !== void 0) { return userMin; } return LIGHTER_DEFAULT_SECURE_MIN_WITHDRAWAL_AMOUNTS[getLighterBaseSymbol(symbol).toUpperCase()] ?? 0; } }); const fast = { id: LIGHTER_FAST_METHOD_ID, keyText: t("withdrawal.methodFast"), subtitleText: t( isUnifiedTradingMode ? "withdrawal.fastDisclaimerUnified" : "withdrawal.fastDisclaimer" ), icon: /* @__PURE__ */ React3.createElement(EvmWallet, { size: 20 }), valueIcon: /* @__PURE__ */ React3.createElement(ChainIconStack, { chainIds: LIGHTER_FAST_PREVIEW_CHAIN_IDS }), config: fastConfig }; const secure = { id: LIGHTER_SECURE_METHOD_ID, keyText: t("withdrawal.methodSecure"), subtitleText: t("withdrawal.secureDisclaimer", { // Until the live value loads, show the localized "60+ minutes" // historical estimate. The "+" lives in the per-locale fallback string // so it doesn't bleed into live values, which are exact. delay: secureDelayText ?? t("withdrawal.secureFallbackDelay") }), icon: /* @__PURE__ */ React3.createElement(EvmWallet, { size: 20 }), valueIcon: /* @__PURE__ */ React3.createElement(ChainIconStack, { chainIds: LIGHTER_SECURE_PREVIEW_CHAIN_IDS }), config: secureConfig }; return { modalTitle: config.modalTitle ?? t("withdrawal.withdraw"), sectionTitle: t("withdrawal.sectionCrypto"), methods: [fast, secure] }; } function useLighterWithdrawalConfig(config) { const { t } = useFunkitTranslation(); const { mainAccountIndex, isUnifiedTradingMode } = useLighterAccounts({ address: config?.l1Address }); const accountIndex = mainAccountIndex && /^\d+$/.test(mainAccountIndex) ? mainAccountIndex : void 0; const secureDelayText = useLighterSecureDelayText(); return useMemo2(() => { if (!config || !accountIndex) { return null; } return buildLighterMultiMethodConfig({ config, accountIndex, t, secureDelayText, isUnifiedTradingMode }); }, [config, accountIndex, t, secureDelayText, isUnifiedTradingMode]); } export { LIGHTER_DEFAULT_SECURE_MIN_WITHDRAWAL_AMOUNTS, LIGHTER_ROUTE_TYPE, createLighterSecureWithdrawalConfig, trackLighterSecureWithdrawal, useLighterWithdrawalConfig };