@rainbow-me/rainbowkit
Version:
The best way to connect a wallet
439 lines (426 loc) • 13.4 kB
JavaScript
"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
};