UNPKG

@rainbow-me/rainbowkit

Version:
439 lines (426 loc) 13.4 kB
"use client"; import { TxItem, chainToExplorerUrl } from "./chunk-VE6DHGRI.js"; import { ExternalLinkIcon } from "./chunk-2JJRYQVC.js"; import { AppContext } from "./chunk-FKYDNCKF.js"; import { I18nContext } from "./chunk-E5IRXM5F.js"; import { Text } from "./chunk-32RBZPUM.js"; import { touchableStyles } from "./chunk-2W63IDAD.js"; import { isMobile } from "./chunk-N6EWR2LO.js"; import { Box } from "./chunk-ZKEPQLOV.js"; // src/components/Txs/TxList.tsx import React2, { useContext } from "react"; import { useAccount as useAccount5 } from "wagmi"; // src/transactions/useClearRecentTransactions.ts import { useCallback } from "react"; import { useAccount as useAccount3 } from "wagmi"; // src/hooks/useChainId.ts import { useAccount } from "wagmi"; function useChainId() { const { chain: activeChain } = useAccount(); return activeChain?.id ?? null; } // src/transactions/TransactionStoreContext.tsx import React from "react"; import { useAccount as useAccount2, useBalance, usePublicClient } from "wagmi"; // src/transactions/transactionStore.ts import { waitForTransactionReceipt } from "viem/actions"; // src/transactions/getTransactionProvider.ts var transactionWatcherKey = "rk-transactions"; function getTransactionProvider(provider) { const uid = `${provider.uid}.${transactionWatcherKey}`; return { ...provider, uid }; } // src/transactions/transactionStore.ts var storageKey = "rk-transactions"; function safeParseJsonData(string) { try { const value = string ? JSON.parse(string) : {}; return typeof value === "object" ? value : {}; } catch { return {}; } } function loadData() { return safeParseJsonData( typeof window !== "undefined" ? window.localStorage.getItem(storageKey) : null ); } var transactionHashRegex = /^0x([A-Fa-f0-9]{64})$/; function validateTransaction(transaction) { const errors = []; if (!transactionHashRegex.test(transaction.hash)) { errors.push("Invalid transaction hash"); } if (typeof transaction.description !== "string") { errors.push("Transaction must have a description"); } if (typeof transaction.confirmations !== "undefined" && (!Number.isInteger(transaction.confirmations) || transaction.confirmations < 1)) { errors.push("Transaction confirmations must be a positiver integer"); } return errors; } function createTransactionStore({ provider: initialProvider }) { let data = loadData(); let transactionProvider; const listeners = /* @__PURE__ */ new Set(); const transactionListeners = /* @__PURE__ */ new Set(); const transactionRequestCache = /* @__PURE__ */ new Map(); function setProvider(newProvider) { transactionProvider = getTransactionProvider(newProvider); } setProvider(initialProvider); function getTransactions(account, chainId) { return data[account]?.[chainId] ?? []; } function addTransaction(account, chainId, transaction) { const errors = validateTransaction(transaction); if (errors.length > 0) { throw new Error(["Unable to add transaction", ...errors].join("\n")); } updateTransactions(account, chainId, (transactions) => { return [ { ...transaction, status: "pending" }, ...transactions.filter(({ hash }) => { return hash !== transaction.hash; }) ]; }); } function clearTransactions(account, chainId) { updateTransactions(account, chainId, () => { return []; }); } function setTransactionStatus(account, chainId, hash, status) { updateTransactions(account, chainId, (transactions) => { return transactions.map( (transaction) => transaction.hash === hash ? { ...transaction, status } : transaction ); }); } async function waitForPendingTransactions(account, chainId) { await Promise.all( getTransactions(account, chainId).filter((transaction) => transaction.status === "pending").map(async (transaction) => { const { confirmations, hash } = transaction; const existingRequest = transactionRequestCache.get(hash); if (existingRequest) { return await existingRequest; } const requestPromise = waitForTransactionReceipt( transactionProvider, { confirmations, hash, timeout: 3e5 // 5 minutes } ).then(({ status }) => { transactionRequestCache.delete(hash); if (status === void 0) { return; } setTransactionStatus( account, chainId, hash, // @ts-expect-error - types changed with viem@1.1.0 status === 0 || status === "reverted" ? "failed" : "confirmed" ); notifyTransactionListeners(status); }).catch(() => { transactionRequestCache.delete(hash); setTransactionStatus(account, chainId, hash, "failed"); }); transactionRequestCache.set(hash, requestPromise); return await requestPromise; }) ); } function updateTransactions(account, chainId, updateFn) { data = loadData(); data[account] = data[account] ?? {}; let completedTransactionCount = 0; const MAX_COMPLETED_TRANSACTIONS = 10; const transactions = updateFn(data[account][chainId] ?? []).filter(({ status }) => { return status === "pending" ? true : completedTransactionCount++ <= MAX_COMPLETED_TRANSACTIONS; }); data[account][chainId] = transactions.length > 0 ? transactions : void 0; persistData(); notifyListeners(); waitForPendingTransactions(account, chainId); } function persistData() { if (typeof window !== "undefined") { window.localStorage.setItem(storageKey, JSON.stringify(data)); } } function notifyListeners() { for (const listener of listeners) { listener(); } } function notifyTransactionListeners(txStatus) { for (const transactionListener of transactionListeners) { transactionListener(txStatus); } } function onChange(fn) { listeners.add(fn); return () => { listeners.delete(fn); }; } function onTransactionStatus(fn) { transactionListeners.add(fn); return () => { transactionListeners.delete(fn); }; } return { addTransaction, clearTransactions, getTransactions, onTransactionStatus, onChange, setProvider, waitForPendingTransactions }; } // src/transactions/TransactionStoreContext.tsx var storeSingleton; var TransactionStoreContext = React.createContext( null ); function TransactionStoreProvider({ children }) { const provider = usePublicClient(); const { address } = useAccount2(); const chainId = useChainId(); const { refetch } = useBalance({ address, query: { enabled: false } }); const [store] = React.useState(() => { if (!storeSingleton) { storeSingleton = createTransactionStore({ provider }); } return storeSingleton; }); const onTransactionStatus = React.useCallback( (txStatus) => { if (txStatus === "success") refetch(); }, [refetch] ); React.useEffect(() => { store.setProvider(provider); }, [store, provider]); React.useEffect(() => { if (address && chainId) { store.waitForPendingTransactions(address, chainId); } }, [store, address, chainId]); React.useEffect(() => { if (store && address && chainId) { return store.onTransactionStatus(onTransactionStatus); } }, [store, address, chainId, onTransactionStatus]); return /* @__PURE__ */ React.createElement(TransactionStoreContext.Provider, { value: store }, children); } function useTransactionStore() { const store = React.useContext(TransactionStoreContext); if (!store) { throw new Error("Transaction hooks must be used within RainbowKitProvider"); } return store; } // src/transactions/useClearRecentTransactions.ts function useClearRecentTransactions() { const store = useTransactionStore(); const { address } = useAccount3(); const chainId = useChainId(); return useCallback(() => { if (!address || !chainId) { throw new Error("No address or chain ID found"); } store.clearTransactions(address, chainId); }, [store, address, chainId]); } // src/transactions/useRecentTransactions.ts import { useEffect, useState } from "react"; import { useAccount as useAccount4 } from "wagmi"; function useRecentTransactions() { const store = useTransactionStore(); const { address } = useAccount4(); const chainId = useChainId(); const [transactions, setTransactions] = useState( () => store && address && chainId ? store.getTransactions(address, chainId) : [] ); useEffect(() => { if (store && address && chainId) { setTransactions(store.getTransactions(address, chainId)); return store.onChange(() => { setTransactions(store.getTransactions(address, chainId)); }); } }, [store, address, chainId]); return transactions; } // src/components/Txs/TxList.tsx var NUMBER_OF_VISIBLE_TXS = 3; function TxList({ address }) { const recentTransactions = useRecentTransactions(); const clearRecentTransactions = useClearRecentTransactions(); const { chain: activeChain } = useAccount5(); const explorerLink = chainToExplorerUrl(activeChain); const visibleTxs = recentTransactions.slice(0, NUMBER_OF_VISIBLE_TXS); const hasTransactions = visibleTxs.length > 0; const mobile = isMobile(); const { appName } = useContext(AppContext); const { i18n } = useContext(I18nContext); return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement( Box, { display: "flex", flexDirection: "column", gap: "10", paddingBottom: "2", paddingTop: "16", paddingX: mobile ? "8" : "18" }, hasTransactions && /* @__PURE__ */ React2.createElement( Box, { paddingBottom: mobile ? "4" : "0", paddingTop: "8", paddingX: mobile ? "12" : "6" }, /* @__PURE__ */ React2.createElement(Box, { display: "flex", justifyContent: "space-between" }, /* @__PURE__ */ React2.createElement( Text, { color: "modalTextSecondary", size: mobile ? "16" : "14", weight: "semibold" }, i18n.t("profile.transactions.recent.title") ), /* @__PURE__ */ React2.createElement( Box, { style: { marginBottom: -6, marginLeft: -10, marginRight: -10, marginTop: -6 } }, /* @__PURE__ */ React2.createElement( Box, { as: "button", background: { hover: "profileForeground" }, borderRadius: "actionButton", className: touchableStyles({ active: "shrink" }), onClick: clearRecentTransactions, paddingX: mobile ? "8" : "12", paddingY: mobile ? "4" : "5", transition: "default", type: "button" }, /* @__PURE__ */ React2.createElement( Text, { color: "modalTextSecondary", size: mobile ? "16" : "14", weight: "semibold" }, i18n.t("profile.transactions.clear.label") ) ) )) ), /* @__PURE__ */ React2.createElement(Box, { display: "flex", flexDirection: "column", gap: "4" }, hasTransactions ? visibleTxs.map((tx) => /* @__PURE__ */ React2.createElement(TxItem, { key: tx.hash, tx })) : /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(Box, { padding: mobile ? "12" : "8" }, /* @__PURE__ */ React2.createElement( Text, { color: "modalTextDim", size: mobile ? "16" : "14", weight: mobile ? "medium" : "bold" }, appName ? i18n.t("profile.transactions.description", { appName }) : i18n.t("profile.transactions.description_fallback") )), mobile && /* @__PURE__ */ React2.createElement( Box, { background: "generalBorderDim", height: "1", marginX: "12", marginY: "8" } ))) ), explorerLink && /* @__PURE__ */ React2.createElement(Box, { paddingBottom: "18", paddingX: mobile ? "8" : "18" }, /* @__PURE__ */ React2.createElement( Box, { alignItems: "center", as: "a", background: { hover: "profileForeground" }, borderRadius: "menuButton", className: touchableStyles({ active: "shrink" }), color: "modalTextDim", display: "flex", flexDirection: "row", href: `${explorerLink}/address/${address}`, justifyContent: "space-between", paddingX: "8", paddingY: "12", rel: "noreferrer noopener", style: { willChange: "transform" }, target: "_blank", transition: "default", width: "full", ...mobile ? { paddingLeft: "12" } : {} }, /* @__PURE__ */ React2.createElement( Text, { color: "modalText", font: "body", size: mobile ? "16" : "14", weight: mobile ? "semibold" : "bold" }, i18n.t("profile.explorer.label") ), /* @__PURE__ */ React2.createElement(ExternalLinkIcon, null) ))); } export { TransactionStoreProvider, useRecentTransactions, TxList };