diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..fa148b8 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,138 @@ +# Ultracite Code Standards + +This project uses **Ultracite**, a zero-config preset that enforces strict code quality standards through automated formatting and linting. + +## Docs-First Rule (Required) + +Before making any code change, read these docs first: + +1. `README.md` +2. `docs/architecture.md` +3. `docs/adding-chains.md` +4. `docs/customization.md` + +If your change is chain-specific, also read: +- `apps//README.md` +- `apps//src/runtime.ts` + +Do not write code before completing this docs pass. If docs and code disagree, update docs and code together so they stay aligned. + +## Quick Reference + +- **Format code**: `pnpm dlx ultracite fix` +- **Check for issues**: `pnpm dlx ultracite check` +- **Diagnose setup**: `pnpm dlx ultracite doctor` + +Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable. + +--- + +## Core Principles + +Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity. + +### Type Safety & Explicitness + +- Use explicit types for function parameters and return values when they enhance clarity +- Prefer `unknown` over `any` when the type is genuinely unknown +- Use const assertions (`as const`) for immutable values and literal types +- Leverage TypeScript's type narrowing instead of type assertions +- Use meaningful variable names instead of magic numbers - extract constants with descriptive names + +### Modern JavaScript/TypeScript + +- Use arrow functions for callbacks and short functions +- Prefer `for...of` loops over `.forEach()` and indexed `for` loops +- Use optional chaining (`?.`) and nullish coalescing (`??`) for safer property access +- Prefer template literals over string concatenation +- Use destructuring for object and array assignments +- Use `const` by default, `let` only when reassignment is needed, never `var` + +### Async & Promises + +- Always `await` promises in async functions - don't forget to use the return value +- Use `async/await` syntax instead of promise chains for better readability +- Handle errors appropriately in async code with try-catch blocks +- Don't use async functions as Promise executors + +### React & JSX + +- Use function components over class components +- Call hooks at the top level only, never conditionally +- Specify all dependencies in hook dependency arrays correctly +- Use the `key` prop for elements in iterables (prefer unique IDs over array indices) +- Nest children between opening and closing tags instead of passing as props +- Don't define components inside other components +- Use semantic HTML and ARIA attributes for accessibility: + - Provide meaningful alt text for images + - Use proper heading hierarchy + - Add labels for form inputs + - Include keyboard event handlers alongside mouse events + - Use semantic elements (` - - - - ); -}; - -AllowanceModal.displayName = "AllowanceModal"; - -export default memo(AllowanceModal); diff --git a/apps/citrea/src/components/fast-bridge/components/amount-input.tsx b/apps/citrea/src/components/fast-bridge/components/amount-input.tsx deleted file mode 100644 index af8251f..0000000 --- a/apps/citrea/src/components/fast-bridge/components/amount-input.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { type FC, Fragment, useEffect, useRef } from "react"; -import { Input } from "../../ui/input"; -import { Button } from "../../ui/button"; -import { SUPPORTED_CHAINS, type UserAsset } from "@avail-project/nexus-core"; -import { useNexus } from "../../nexus/NexusProvider"; -import { type FastBridgeState } from "../hooks/useBridge"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../../ui/accordion"; -import { SHORT_CHAIN_NAME } from "../../common"; -import { LoaderCircle } from "lucide-react"; - -interface AmountInputProps { - amount?: string; - onChange: (value: string) => void; - bridgableBalance?: UserAsset; - onCommit?: (value: string) => void; - disabled?: boolean; - inputs: FastBridgeState; - showBalanceDetails?: boolean; -} - -const AmountInput: FC = ({ - amount, - onChange, - bridgableBalance, - onCommit, - disabled, - inputs, - showBalanceDetails = true, -}) => { - const { nexusSDK, loading } = useNexus(); - const commitTimerRef = useRef(null); - const showBalanceDivider = showBalanceDetails && Boolean(bridgableBalance); - - const scheduleCommit = (val: string) => { - if (!onCommit || disabled) return; - if (commitTimerRef.current) clearTimeout(commitTimerRef.current); - commitTimerRef.current = setTimeout(() => { - onCommit(val); - }, 800); - }; - - const onMaxClick = async () => { - if (!showBalanceDetails || !nexusSDK || !inputs) return; - const maxBalAvailable = await nexusSDK?.calculateMaxForBridge({ - token: inputs?.token, - toChainId: inputs?.chain, - recipient: inputs?.recipient, - }); - if (!maxBalAvailable) return; - onChange(maxBalAvailable.amount); - onCommit?.(maxBalAvailable.amount); - }; - - useEffect(() => { - return () => { - if (commitTimerRef.current) { - clearTimeout(commitTimerRef.current); - commitTimerRef.current = null; - } - }; - }, []); - - return ( -
-
- { - let next = e.target.value.replaceAll(/[^0-9.]/g, ""); - const parts = next.split("."); - if (parts.length > 2) - next = parts[0] + "." + parts.slice(1).join(""); - if (next === ".") next = "0."; - onChange(next); - scheduleCommit(next); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - if (commitTimerRef.current) { - clearTimeout(commitTimerRef.current); - commitTimerRef.current = null; - } - onCommit?.(amount ?? ""); - } - }} - className="w-full border-none bg-transparent rounded-r-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none py-0 px-3 h-12!" - aria-invalid={Boolean(amount) && Number.isNaN(Number(amount))} - disabled={disabled} - /> -
- {showBalanceDetails && bridgableBalance && ( -

- {nexusSDK?.utils?.formatTokenBalance(bridgableBalance?.balance, { - symbol: - bridgableBalance?.displaySymbol ?? bridgableBalance?.symbol, - decimals: bridgableBalance?.decimals, - })} -

- )} - {showBalanceDetails && loading && !bridgableBalance && ( - - )} - {showBalanceDetails && ( - - )} -
-
- {showBalanceDetails && ( - - - - View Balance Breakdown - - -
- {bridgableBalance?.breakdown.map((chain) => { - if (Number.parseFloat(chain.balance) === 0) return null; - if ( - bridgableBalance.symbol === "USDM" && - chain.chain.id === SUPPORTED_CHAINS.MEGAETH - ) - return null; - return ( - -
-
-
- {chain.chain.name} -
- - {SHORT_CHAIN_NAME[chain.chain.id]} - -
-

- {nexusSDK?.utils?.formatTokenBalance(chain.balance, { - symbol: - bridgableBalance?.displaySymbol ?? - bridgableBalance?.symbol, - decimals: bridgableBalance?.decimals, - })} -

-
-
- ); - })} -
-
-
-
- )} -
- ); -}; - -export default AmountInput; diff --git a/apps/citrea/src/components/fast-bridge/components/balance-breakdown.tsx b/apps/citrea/src/components/fast-bridge/components/balance-breakdown.tsx deleted file mode 100644 index 6cdeec3..0000000 --- a/apps/citrea/src/components/fast-bridge/components/balance-breakdown.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { type UserAsset } from "@avail-project/nexus-core"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../../ui/accordion"; - -interface BalanceBreakdownProps { - assetBalances: UserAsset; -} - -const BalanceBreakdown = ({ assetBalances }: BalanceBreakdownProps) => { - return ( - - -
- {assetBalances && ( -
- -

View Balance Breakdown

-
-
- )} -
- {assetBalances && ( - -
- {assetBalances?.breakdown?.map((individualBalance) => ( -
-
- {individualBalance.chain.name} -

- {individualBalance.chain.name} -

-
- -

- {individualBalance.balance} {assetBalances.symbol} -

-
- ))} -
-
- )} -
-
- ); -}; - -export default BalanceBreakdown; diff --git a/apps/citrea/src/components/fast-bridge/components/chain-select.tsx b/apps/citrea/src/components/fast-bridge/components/chain-select.tsx deleted file mode 100644 index a5c175b..0000000 --- a/apps/citrea/src/components/fast-bridge/components/chain-select.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { type FC, useMemo } from "react"; -import { Label } from "../../ui/label"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "../../ui/select"; -import { type SUPPORTED_CHAINS_IDS } from "@avail-project/nexus-core"; -import { cn } from "@/lib/utils"; -import { useNexus } from "../../nexus/NexusProvider"; - -interface ChainSelectProps { - selectedChain: number; - disabled?: boolean; - hidden?: boolean; - className?: string; - label?: string; - handleSelect: (chainId: SUPPORTED_CHAINS_IDS) => void; -} - -const ChainSelect: FC = ({ - selectedChain, - disabled, - hidden = false, - className, - label, - handleSelect, -}) => { - const { supportedChainsAndTokens } = useNexus(); - - const selectedChainData = useMemo(() => { - if (!supportedChainsAndTokens) return null; - return supportedChainsAndTokens.find((c) => c.id === selectedChain); - }, [selectedChain, supportedChainsAndTokens]); - - console.log("SELECTED CHAIN DATA:", selectedChainData); - - if (hidden) return null; - return ( - - ); -}; - -export default ChainSelect; diff --git a/apps/citrea/src/components/fast-bridge/components/fee-breakdown.tsx b/apps/citrea/src/components/fast-bridge/components/fee-breakdown.tsx deleted file mode 100644 index 93655ba..0000000 --- a/apps/citrea/src/components/fast-bridge/components/fee-breakdown.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { type FC } from "react"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../../ui/accordion"; -import { type ReadableIntent } from "@avail-project/nexus-core"; -import { Skeleton } from "../../ui/skeleton"; -import { useNexus } from "../../nexus/NexusProvider"; -import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip"; -import { MessageCircleQuestion } from "lucide-react"; - -interface FeeBreakdownProps { - intent: ReadableIntent; - isLoading?: boolean; -} - -const FeeBreakdown: FC = ({ intent, isLoading = false }) => { - const { nexusSDK } = useNexus(); - const feeSymbol = - intent.token?.displaySymbol ?? intent.token?.symbol ?? "USDC"; - - const feeRows = [ - { - key: "caGas", - label: "Fast Bridge Gas Fees", - value: intent?.fees?.caGas, - description: - "The gas fee required for executing the fast bridge transaction on the destination chain.", - }, - { - key: "gasSupplied", - label: "Gas Supplied", - value: intent?.fees?.gasSupplied, - description: - "The amount of gas tokens supplied to cover transaction costs on the destination chain.", - }, - { - key: "solver", - label: "Solver Fees", - value: intent?.fees?.solver, - description: - "Fees paid to the solver that executes the bridge transaction and ensures fast completion.", - }, - { - key: "protocol", - label: "Protocol Fees", - value: intent?.fees?.protocol, - description: - "Fees collected by the protocol for maintaining and operating the bridge infrastructure.", - }, - ]; - - return ( - - -
-

Total fees

- -
- {isLoading ? ( - - ) : ( -

- {nexusSDK?.utils?.formatTokenBalance(intent.fees?.total, { - symbol: feeSymbol, - decimals: intent?.token?.decimals, - })} -

- )} - -

View Breakup

-
-
-
- -
- {feeRows.map(({ key, label, value, description }) => { - // if (Number.parseFloat(value ?? "0") <= 0) return null; - return ( - -
-
-

{label}

- - - -
- {isLoading ? ( - - ) : ( -

- {nexusSDK?.utils?.formatTokenBalance(value, { - symbol: feeSymbol, - decimals: intent?.token?.decimals, - })} -

- )} -
- -

{description}

-
-
- ); - })} -
-
-
-
- ); -}; - -export default FeeBreakdown; diff --git a/apps/citrea/src/components/fast-bridge/components/receipient-address.tsx b/apps/citrea/src/components/fast-bridge/components/receipient-address.tsx deleted file mode 100644 index 0bb4192..0000000 --- a/apps/citrea/src/components/fast-bridge/components/receipient-address.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; -import * as React from "react"; -import { Input } from "../../ui/input"; -import { Check, Edit } from "lucide-react"; -import { Button } from "../../ui/button"; -import { useNexus } from "../../nexus/NexusProvider"; -import { type Address } from "viem"; - -interface ReceipientAddressProps { - address?: Address; - onChange: (address: string) => void; -} - -const ReceipientAddress: React.FC = ({ - address, - onChange, -}) => { - const { nexusSDK } = useNexus(); - const [isEditing, setIsEditing] = React.useState(false); - const fallbackTruncate = (value: string, head = 6, tail = 6) => { - if (!value) return ""; - if (value.length <= head + tail) return value; - return `${value.slice(0, head)}...${value.slice(-tail)}`; - }; - const displayAddress = - address && nexusSDK?.utils?.truncateAddress - ? nexusSDK.utils.truncateAddress(address, 6, 6) - : address - ? fallbackTruncate(address, 6, 6) - : ""; - return ( -
- {isEditing ? ( -
- onChange(e.target.value)} - className="w-full text-base font-medium" - /> - -
- ) : ( -
-

Recipient Address

-
-

{displayAddress}

- - -
-
- )} -
- ); -}; - -export default ReceipientAddress; diff --git a/apps/citrea/src/components/fast-bridge/components/recipient-address.tsx b/apps/citrea/src/components/fast-bridge/components/recipient-address.tsx deleted file mode 100644 index 5a7fa6f..0000000 --- a/apps/citrea/src/components/fast-bridge/components/recipient-address.tsx +++ /dev/null @@ -1,79 +0,0 @@ -"use client"; -import { type FC, useState } from "react"; -import { Input } from "../../ui/input"; -import { Check, Edit } from "lucide-react"; -import { Button } from "../../ui/button"; -import { useNexus } from "../../nexus/NexusProvider"; -import { type Address } from "viem"; - -interface RecipientAddressProps { - address?: Address; - onChange: (address: string) => void; - disabled?: boolean; -} - -const RecipientAddress: FC = ({ - address, - onChange, - disabled, -}) => { - const { nexusSDK } = useNexus(); - const [isEditing, setIsEditing] = useState(false); - const fallbackTruncate = (value: string, head = 6, tail = 6) => { - if (!value) return ""; - if (value.length <= head + tail) return value; - return `${value.slice(0, head)}...${value.slice(-tail)}`; - }; - const displayAddress = - address && nexusSDK?.utils?.truncateAddress - ? nexusSDK.utils.truncateAddress(address, 6, 6) - : address - ? fallbackTruncate(address, 6, 6) - : ""; - return ( -
- {isEditing ? ( -
- onChange(e.target.value)} - className="w-full" - /> - -
- ) : ( -
-

Recipient Address

-
- {address && ( -

{displayAddress}

- )} - - -
-
- )} -
- ); -}; - -export default RecipientAddress; diff --git a/apps/citrea/src/components/fast-bridge/components/source-breakdown.tsx b/apps/citrea/src/components/fast-bridge/components/source-breakdown.tsx deleted file mode 100644 index bbe5c84..0000000 --- a/apps/citrea/src/components/fast-bridge/components/source-breakdown.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { - type ReadableIntent, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../../ui/accordion"; -import { Skeleton } from "../../ui/skeleton"; -import { useNexus } from "../../nexus/NexusProvider"; - -interface SourceBreakdownProps { - intent?: ReadableIntent; - tokenSymbol: SUPPORTED_TOKENS; - isLoading?: boolean; -} - -const SourceBreakdown = ({ - intent, - tokenSymbol, - isLoading = false, -}: SourceBreakdownProps) => { - const { nexusSDK } = useNexus(); - const spendSymbol = - intent?.token.displaySymbol ?? intent?.token.symbol ?? tokenSymbol; - return ( - - -
- {isLoading ? ( - <> -
-

You Spend

- -
-
- -
- -
-
- - ) : ( - intent?.sources && ( - <> -
-

You Spend

-

- {`${spendSymbol.toUpperCase()} on ${ - intent?.sources?.length - } ${intent?.sources?.length > 1 ? "chains" : "chain"}`} -

-
- -
-

- {nexusSDK?.utils?.formatTokenBalance(intent?.sourcesTotal, { - symbol: spendSymbol, - decimals: intent?.token?.decimals, - })} -

- -

View Sources

-
-
- - ) - )} -
- {!isLoading && intent?.sources && ( - -
- {intent?.sources?.map((source) => ( -
-
- {source?.chainName} -

{source.chainName}

-
- -

- {nexusSDK?.utils?.formatTokenBalance(source.amount, { - symbol: spendSymbol, - decimals: intent?.token?.decimals, - })} -

-
- ))} -
-
- )} -
-
- ); -}; - -export default SourceBreakdown; diff --git a/apps/citrea/src/components/fast-bridge/components/transaction-progress.tsx b/apps/citrea/src/components/fast-bridge/components/transaction-progress.tsx deleted file mode 100644 index f877513..0000000 --- a/apps/citrea/src/components/fast-bridge/components/transaction-progress.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { Check, Circle, LoaderPinwheel } from "lucide-react"; -import { type FC, memo, useMemo } from "react"; -import { - type BridgeStepType, - type SwapStepType, -} from "@avail-project/nexus-core"; - -type ProgressStep = BridgeStepType | SwapStepType; - -interface TransactionProgressProps { - timer: number; - steps: Array<{ id: number; completed: boolean; step: ProgressStep }>; - viewIntentUrl?: string; - operationType?: string; - completed?: boolean; -} - -export const getOperationText = (type: string) => { - switch (type) { - case "bridge": - return "Transaction"; - case "transfer": - return "Transferring"; - case "bridgeAndExecute": - return "Bridge & Execute"; - case "swap": - return "Swapping"; - default: - return "Processing"; - } -}; - -type DisplayStep = { id: string; label: string; completed: boolean }; - -const StepList: FC<{ steps: DisplayStep[]; currentIndex: number }> = memo( - ({ steps, currentIndex }) => { - return ( -
- {steps.map((s, idx) => { - const isCompleted = !!s.completed; - const isCurrent = currentIndex === -1 ? false : idx === currentIndex; - - let rightIcon = ; - if (isCompleted) { - rightIcon = ; - } else if (isCurrent) { - rightIcon = ( - - ); - } - - return ( -
-
- {s.label} -
- {rightIcon} -
- ); - })} -
- ); - } -); -StepList.displayName = "StepList"; - -const TransactionProgress: FC = ({ - timer, - steps, - viewIntentUrl, - operationType = "bridge", - completed = false, -}) => { - const totalSteps = Array.isArray(steps) ? steps.length : 0; - const completedSteps = Array.isArray(steps) - ? steps.reduce((acc, s) => acc + (s?.completed ? 1 : 0), 0) - : 0; - const rawPercent = totalSteps > 0 ? completedSteps / totalSteps : 0; - const percent = completed ? 1 : rawPercent; - const allCompleted = completed || percent >= 1; - const opText = getOperationText(operationType); - const headerText = allCompleted - ? `${opText} Completed` - : `${opText} In Progress...`; - const ctaText = allCompleted ? `View Explorer` : "View Intent"; - - const { effectiveSteps, currentIndex } = useMemo(() => { - const milestones = [ - "Intent verified", - "Collected on sources", - "Filled on destination", - ]; - const thresholds = milestones.map( - (_, idx) => (idx + 1) / milestones.length - ); - const displaySteps: DisplayStep[] = milestones.map((label, idx) => ({ - id: `M${idx}`, - label, - completed: idx === 0 ? timer > 0 : percent >= thresholds[idx], - })); - const current = displaySteps.findIndex((st) => !st.completed); - return { effectiveSteps: displaySteps, currentIndex: current }; - }, [percent, timer, completed]); - - return ( -
-
- {allCompleted ? ( - - ) : ( - - )} -

{headerText}

-
- - {Math.floor(timer)} - - - . - - - {String(Math.floor((timer % 1) * 1000)).padStart(3, "0")}s - -
-
- - - - {viewIntentUrl && ( - - {ctaText} - - )} -
- ); -}; - -export default TransactionProgress; diff --git a/apps/citrea/src/components/fast-bridge/fast-bridge.tsx b/apps/citrea/src/components/fast-bridge/fast-bridge.tsx deleted file mode 100644 index 6d6e107..0000000 --- a/apps/citrea/src/components/fast-bridge/fast-bridge.tsx +++ /dev/null @@ -1,470 +0,0 @@ -"use client"; -import { type FC, useEffect, useMemo, useRef, useState } from "react"; -import { Card, CardContent } from "../ui/card"; -import ChainSelect from "./components/chain-select"; -import TokenSelect from "./components/token-select"; -import { Button } from "../ui/button"; -import { X, CheckCircle2 } from "lucide-react"; -import { useNexus } from "../nexus/NexusProvider"; -import AmountInput from "./components/amount-input"; -import FeeBreakdown from "./components/fee-breakdown"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "../ui/dialog"; -import TransactionProgress from "./components/transaction-progress"; -import AllowanceModal from "./components/allowance-modal"; -import useBridge from "./hooks/useBridge"; -import SourceBreakdown from "./components/source-breakdown"; -import { - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; -import { type Address } from "viem"; -import { Skeleton } from "../ui/skeleton"; -import RecipientAddress from "./components/recipient-address"; -import ViewHistory from "../view-history/view-history"; -import { toast } from "sonner"; -import Decimal from "decimal.js"; - -interface FastBridgeProps { - connectedAddress?: Address; - isWalletConnected?: boolean; - onConnectWallet?: () => void; - mockIntent?: { - totalAmount?: string; - receiveAmount?: string; - totalGas?: string; - }; - prefill?: { - token: SUPPORTED_TOKENS; - chainId: SUPPORTED_CHAINS_IDS; - amount?: string; - recipient?: Address; - }; - onComplete?: () => void; - onStart?: () => void; - onError?: (message: string) => void; -} - -const FastBridge: FC = ({ - connectedAddress, - isWalletConnected, - onConnectWallet, - mockIntent, - onComplete, - onStart, - onError, - prefill, -}) => { - const { - nexusSDK, - intent, - bridgableBalance, - allowance, - network, - fetchBridgableBalance, - } = useNexus(); - const [historyRefreshNonce, setHistoryRefreshNonce] = useState(0); - - const { - inputs, - setInputs, - timer, - loading, - refreshing, - isDialogOpen, - txError, - setTxError, - handleTransaction, - reset, - filteredBridgableBalance, - startTransaction, - setIsDialogOpen, - commitAmount, - lastExplorerUrl, - steps, - status, - areInputsValid, - } = useBridge({ - prefill, - network: network ?? "mainnet", - connectedAddress, - nexusSDK, - intent, - bridgableBalance, - allowance, - onComplete: async () => { - if (onComplete) { - onComplete(); - } - const sourcesText = - intent.current?.intent?.sources?.length && - intent.current?.intent.sources.length > 0 - ? intent.current.intent.sources.map((s) => s.chainName).join(", ") - : "N/A"; - toast.success( -
-
- - Bridge Successful! -
-
-
- Source(s): {sourcesText} -
-
- Destination:{" "} - {intent.current?.intent?.destination?.chainName || "Unknown"} -
-
- Asset:{" "} - {intent.current?.intent?.token.symbol || "Unknown"} -
-
- Amount Spent:{" "} - {intent.current?.intent?.sourcesTotal - ? new Decimal(intent.current?.intent?.sourcesTotal).toFixed() - : "NaN"}{" "} - {intent.current?.intent?.token.symbol || "Unknown"} -
-
- Amount Received:{" "} - {intent.current?.intent?.destination?.amount - ? new Decimal( - intent.current?.intent?.destination?.amount, - ).toFixed() - : "NaN"}{" "} - {intent.current?.intent?.token.symbol || "Unknown"} -
-
- Total Fees:{" "} - {intent.current?.intent?.fees.total - ? new Decimal(intent.current?.intent?.fees.total).toFixed() - : "NaN"}{" "} - {intent.current?.intent?.token.symbol || "Unknown"} -
- {lastExplorerUrl ? ( - - ) : null} -
-
, - { - duration: Infinity, // Stay until dismissed - closeButton: true, - icon: null, // Remove default icon since we're adding our own - }, - ); - setHistoryRefreshNonce((prev) => prev + 1); - }, - onStart, - onError: (message) => { - toast.error(message); - if (onError) { - onError(message); - } - }, - fetchBalance: fetchBridgableBalance, - }); - const isConnected = isWalletConnected ?? Boolean(connectedAddress); - const isSdkReady = Boolean(nexusSDK); - const showSdkDetails = isSdkReady; - const receiveSymbol = - intent?.current?.intent?.token.symbol ?? - // @ts-expect-error - not possible - intent?.current?.intent?.token.displaySymbol ?? - filteredBridgableBalance?.symbol; - - const amountValue = useMemo(() => { - if (!inputs?.amount) return null; - const parsed = Number.parseFloat(inputs.amount); - return Number.isFinite(parsed) ? parsed : null; - }, [inputs?.amount]); - - const hasValidAmount = useMemo(() => { - if (amountValue === null) return false; - return amountValue > 0; - }, [amountValue]); - - const formatMockNumber = (value: number) => { - if (!Number.isFinite(value)) return "--"; - const fixed = value.toFixed(6); - return fixed.replace(/\.?0+$/, ""); - }; - - const formatWithToken = (value: string, token?: string) => { - if (!token) return value; - return `${value} ${token}`.trim(); - }; - - const tokenSuffix = - inputs?.token ?? filteredBridgableBalance?.symbol ?? "USDC"; - - const mockPreview = useMemo(() => { - if (!hasValidAmount || amountValue === null) return null; - if (mockIntent) { - return { - totalAmount: mockIntent.totalAmount ?? "--", - receiveAmount: mockIntent.receiveAmount ?? "--", - totalGas: mockIntent.totalGas ?? "--", - }; - } - const totalGas = amountValue * 0.001; - const totalAmount = amountValue + totalGas; - return { - totalAmount: formatWithToken(formatMockNumber(totalAmount), tokenSuffix), - receiveAmount: formatWithToken( - formatMockNumber(amountValue), - tokenSuffix, - ), - totalGas: formatWithToken(formatMockNumber(totalGas), tokenSuffix), - }; - }, [amountValue, hasValidAmount, mockIntent, tokenSuffix]); - - const showMockPreview = !isConnected && hasValidAmount && mockPreview; - const autoIntentTriggered = useRef(false); - - useEffect(() => { - autoIntentTriggered.current = false; - }, [inputs?.amount, inputs?.chain, inputs?.token, inputs?.recipient]); - - useEffect(() => { - if (!isConnected || !isSdkReady) return; - if (!areInputsValid) return; - if (intent.current) return; - // if (loading) return; // Removed to allow "10" fetch even if "1" is loading - if (autoIntentTriggered.current) return; - autoIntentTriggered.current = true; - void handleTransaction(); - }, [ - areInputsValid, - handleTransaction, - intent, - isConnected, - isSdkReady, - loading, - ]); - return ( - - - {showSdkDetails && ( - - )} - - setInputs({ - ...inputs, - chain, - }) - } - label="To" - disabled={!!prefill?.chainId} - /> - setInputs({ ...inputs, token })} - disabled={!!prefill?.token} - /> - setInputs({ ...inputs, amount })} - bridgableBalance={filteredBridgableBalance} - onCommit={() => void commitAmount()} - disabled={refreshing || !!prefill?.amount} - inputs={inputs} - showBalanceDetails={showSdkDetails} - /> - - setInputs({ ...inputs, recipient: address as `0x${string}` }) - } - disabled={!!prefill?.recipient} - /> - {showMockPreview && ( -
-
-

You spend

-

{mockPreview?.totalAmount}

-
-
-

You receive

-

- {mockPreview?.receiveAmount} -

-
-
-

Total gas

-

{mockPreview?.totalGas}

-
-
- )} - - {showSdkDetails && intent?.current?.intent && ( - <> - - -
-

You receive

-
- {refreshing ? ( - - ) : ( -

- {`${ - connectedAddress === inputs?.recipient - ? intent?.current?.intent?.destination?.amount - : inputs.amount - } ${receiveSymbol}`} -

- )} - {refreshing ? ( - - ) : ( -

- on {intent?.current?.intent?.destination?.chainName} -

- )} -
-
- - - )} - - {!intent.current && ( - - )} - - { - if (loading) return; - setIsDialogOpen(open); - }} - > - {intent.current && !isDialogOpen && ( -
- - - - -
- )} - - - - Transaction Progress - - {allowance.current ? ( - - ) : ( - - )} - -
- - {txError && ( -
- {txError} - -
- )} -
-
- ); -}; - -export default FastBridge; diff --git a/apps/citrea/src/components/fast-bridge/hooks/useBridge.ts b/apps/citrea/src/components/fast-bridge/hooks/useBridge.ts deleted file mode 100644 index 34f66bd..0000000 --- a/apps/citrea/src/components/fast-bridge/hooks/useBridge.ts +++ /dev/null @@ -1,386 +0,0 @@ -import { - type BridgeStepType, - NEXUS_EVENTS, - type NexusNetwork, - NexusSDK, - type OnAllowanceHookData, - type OnIntentHookData, - // SUPPORTED_CHAINS, - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, - type UserAsset, -} from "@avail-project/nexus-core"; -import { - useEffect, - useMemo, - useRef, - useState, - useReducer, - type RefObject, -} from "react"; -import { type Address, isAddress } from "viem"; -import { - useStopwatch, - usePolling, - useNexusError, - useTransactionSteps, - type TransactionStatus, -} from "../../common"; -import config from "../../../../config"; -import { trackBridgeSubmit } from "../../../lib/posthog"; -import { SHORT_CHAIN_NAME } from "../../common/utils/constant"; - -export interface FastBridgeState { - chain: SUPPORTED_CHAINS_IDS; - token: SUPPORTED_TOKENS; - amount?: string; - recipient?: `0x${string}`; -} - -const ALLOWED_TOKENS = new Set([ - "USDC", - "USDT", - "USDM", -]) as Set; - -interface UseBridgeProps { - network: NexusNetwork; - connectedAddress?: Address; - nexusSDK: NexusSDK | null; - intent: RefObject; - allowance: RefObject; - bridgableBalance: UserAsset[] | null; - prefill?: { - token: string; - chainId: number; - amount?: string; - recipient?: Address; - }; - onComplete?: () => void; - onStart?: () => void; - onError?: (message: string) => void; - fetchBalance: () => Promise; -} - -type BridgeState = { - inputs: FastBridgeState; - status: TransactionStatus; -}; - -type Action = - | { type: "setInputs"; payload: Partial } - | { type: "resetInputs" } - | { type: "setStatus"; payload: TransactionStatus }; - -const buildInitialInputs = ( - connectedAddress?: Address, - prefill?: { - token: string; - chainId: number; - amount?: string; - recipient?: Address; - }, -): FastBridgeState => { - const validToken = - prefill?.token && - ALLOWED_TOKENS.has(prefill.token.toUpperCase() as SUPPORTED_TOKENS) - ? (prefill.token.toUpperCase() as SUPPORTED_TOKENS) - : config.nexusPrimaryToken || "USDC"; - - const validAmount = prefill?.amount - ? (() => { - const sanitized = prefill.amount.trim(); - if (!sanitized || sanitized === "." || !/^\d*\.?\d*$/.test(sanitized)) - return undefined; - const num = Number.parseFloat(sanitized); - return Number.isNaN(num) || num <= 0 || num > 1e9 - ? undefined - : sanitized; - })() - : undefined; - - const validRecipient = - prefill?.recipient && isAddress(prefill.recipient) - ? (prefill.recipient as `0x${string}`) - : connectedAddress; - - return { - chain: config.chainId as SUPPORTED_CHAINS_IDS, - token: validToken as SUPPORTED_TOKENS, - amount: validAmount, - recipient: validRecipient, - }; -}; - -const useBridge = ({ - connectedAddress, - nexusSDK, - intent, - bridgableBalance, - prefill, - onComplete, - onStart, - onError, - fetchBalance, - allowance, -}: UseBridgeProps) => { - const handleNexusError = useNexusError(); - const initialState: BridgeState = { - inputs: buildInitialInputs(connectedAddress, prefill), - status: "idle", - }; - function reducer(state: BridgeState, action: Action): BridgeState { - switch (action.type) { - case "setInputs": - return { ...state, inputs: { ...state.inputs, ...action.payload } }; - case "resetInputs": - return { - ...state, - inputs: buildInitialInputs(connectedAddress, prefill), - }; - case "setStatus": - return { ...state, status: action.payload }; - default: - return state; - } - } - const [state, dispatch] = useReducer(reducer, initialState); - const inputs = state.inputs; - const setInputs = (next: FastBridgeState | Partial) => { - dispatch({ type: "setInputs", payload: next as Partial }); - }; - - const loading = state.status === "executing"; - const [refreshing, setRefreshing] = useState(false); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [txError, setTxError] = useState(null); - const [lastExplorerUrl, setLastExplorerUrl] = useState(""); - const commitLockRef = useRef(false); - const txnIdRef = useRef(0); - const { - steps, - onStepsList, - onStepComplete, - reset: resetSteps, - } = useTransactionSteps(); - - const areInputsValid = useMemo(() => { - const hasToken = inputs?.token !== undefined && inputs?.token !== null; - const hasChain = inputs?.chain !== undefined && inputs?.chain !== null; - const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0; - const hasValidrecipient = - Boolean(inputs?.recipient) && isAddress(inputs?.recipient as string); - return hasToken && hasChain && hasAmount && hasValidrecipient; - }, [inputs]); - - const handleTransaction = async () => { - if ( - !inputs?.amount || - !inputs?.recipient || - !inputs?.chain || - !inputs?.token - ) { - console.error("Missing required inputs"); - return; - } - - if (Number(inputs.amount) > 550) { - setTxError("Amount exceeds maximum limit of 550"); - return; - } - const currentTxnId = ++txnIdRef.current; - dispatch({ type: "setStatus", payload: "executing" }); - setTxError(null); - onStart?.(); - - // Track bridge submit event with PostHog - trackBridgeSubmit({ - chain: inputs.chain, - chainName: SHORT_CHAIN_NAME[inputs.chain] || `Chain ${inputs.chain}`, - tokenSymbol: inputs.token, - amount: inputs.amount, - fast_bridge: 'citrea', - }); - - try { - if (!nexusSDK) { - throw new Error("Nexus SDK not initialized"); - } - const formattedAmount = nexusSDK.convertTokenReadableAmountToBigInt( - inputs?.amount, - inputs?.token, - inputs?.chain, - ); - setLastExplorerUrl(""); - const bridgeTxn = await nexusSDK.bridge( - { - token: inputs?.token, - amount: formattedAmount, - toChainId: inputs?.chain, - recipient: inputs?.recipient, - }, - { - onEvent: (event) => { - if (currentTxnId !== txnIdRef.current) return; - if (event.name === NEXUS_EVENTS.STEPS_LIST) { - const list = Array.isArray(event.args) ? event.args : []; - onStepsList(list); - } - if (event.name === NEXUS_EVENTS.STEP_COMPLETE) { - console.log("STEP_EVENT", event); - if (event.args.type === "INTENT_HASH_SIGNED") { - stopwatch.start(); - } - onStepComplete(event.args); - } - }, - }, - ); - if (currentTxnId !== txnIdRef.current) return; - - if (!bridgeTxn) { - throw new Error("Transaction rejected by user"); - } - if (bridgeTxn) { - setLastExplorerUrl(bridgeTxn.explorerUrl); - await onSuccess(); - } - } catch (error) { - if (currentTxnId !== txnIdRef.current) return; - const { message } = handleNexusError(error); - intent.current?.deny(); - intent.current = null; - allowance.current = null; - setTxError(message); - onError?.(message); - setIsDialogOpen(false); - dispatch({ type: "setStatus", payload: "error" }); - } - }; - - const onSuccess = async () => { - // Close dialog and stop timer on success - stopwatch.stop(); - dispatch({ type: "setStatus", payload: "success" }); - onComplete?.(); - intent.current = null; - allowance.current = null; - dispatch({ type: "resetInputs" }); - setRefreshing(false); - await fetchBalance(); - }; - - const filteredBridgableBalance = useMemo(() => { - return bridgableBalance?.find((bal) => bal?.symbol === inputs?.token); - }, [bridgableBalance, inputs?.token]); - - const refreshIntent = async () => { - setRefreshing(true); - try { - await intent.current?.refresh([]); - } catch (error) { - console.error("Transaction failed:", error); - } finally { - setRefreshing(false); - } - }; - - const reset = () => { - intent.current?.deny(); - intent.current = null; - allowance.current = null; - dispatch({ type: "resetInputs" }); - dispatch({ type: "setStatus", payload: "idle" }); - setRefreshing(false); - stopwatch.stop(); - stopwatch.reset(); - resetSteps(); - }; - - const startTransaction = () => { - // Reset timer for a fresh run - intent.current?.allow(); - setIsDialogOpen(true); - setTxError(null); - }; - - const commitAmount = async () => { - if (commitLockRef.current) return; - if (loading || txError || !areInputsValid) return; - - // Validate amount before proceeding - if (inputs?.amount) { - const amountStr = inputs.amount.trim(); - if (!amountStr) return; - - const amount = Number.parseFloat(amountStr); - if (Number.isNaN(amount) || amount <= 0) return; - } - - commitLockRef.current = true; - try { - await handleTransaction(); - } finally { - commitLockRef.current = false; - } - }; - - usePolling(Boolean(intent.current) && !isDialogOpen, refreshIntent, 15000); - - const stopwatch = useStopwatch({ intervalMs: 100 }); - - useEffect(() => { - if (intent.current) { - // intent.current.deny(); - intent.current = null; - } - }, [inputs]); - - useEffect(() => { - if (!isDialogOpen) { - stopwatch.stop(); - stopwatch.reset(); - // Reset all transaction state when dialog closes - if (state.status === "success" || state.status === "error") { - resetSteps(); - setLastExplorerUrl(""); - dispatch({ type: "setStatus", payload: "idle" }); - } - } - }, [isDialogOpen, stopwatch, state.status]); - - useEffect(() => { - if (txError) { - setTxError(null); - } - }, [inputs]); - - useEffect(() => { - if (connectedAddress && !inputs?.recipient) { - setInputs({ recipient: connectedAddress as `0x${string}` }); - } - }, [connectedAddress, inputs?.recipient]); - - return { - inputs, - setInputs, - timer: stopwatch.seconds, - setIsDialogOpen, - setTxError, - loading, - refreshing, - isDialogOpen, - txError, - handleTransaction, - reset, - filteredBridgableBalance, - startTransaction, - commitAmount, - lastExplorerUrl, - steps, - status: state.status, - areInputsValid, - }; -}; - -export default useBridge; diff --git a/apps/citrea/src/components/hero-section.tsx b/apps/citrea/src/components/hero-section.tsx deleted file mode 100644 index 89ca4ff..0000000 --- a/apps/citrea/src/components/hero-section.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function HeroSection() { - return ( -
- Bridge your unified USDC/USDT balances across 12+ chains, to Avalanche, instantly! -
- ); -} diff --git a/apps/citrea/src/components/nexus/NexusProvider.tsx b/apps/citrea/src/components/nexus/NexusProvider.tsx deleted file mode 100644 index 8fde99e..0000000 --- a/apps/citrea/src/components/nexus/NexusProvider.tsx +++ /dev/null @@ -1,328 +0,0 @@ -"use client"; -import { - type EthereumProvider, - type NexusNetwork, - NexusSDK, - type OnAllowanceHookData, - type OnIntentHookData, - type OnSwapIntentHookData, - type SupportedChainsAndTokensResult, - type SupportedChainsResult, - type UserAsset, -} from "@avail-project/nexus-core"; - -import { - createContext, - type RefObject, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { useAccountEffect } from "wagmi"; - -interface NexusContextType { - nexusSDK: NexusSDK | null; - bridgableBalance: UserAsset[] | null; - swapBalance: UserAsset[] | null; - intent: RefObject; - allowance: RefObject; - swapIntent: RefObject; - exchangeRate: Record | null; - supportedChainsAndTokens: SupportedChainsAndTokensResult | null; - swapSupportedChainsAndTokens: SupportedChainsResult | null; - network?: NexusNetwork; - loading: boolean; - handleInit: (provider: EthereumProvider) => Promise; - fetchBridgableBalance: () => Promise; - fetchSwapBalance: () => Promise; - getFiatValue: (amount: number, token: string) => number; - initializeNexus: (provider: EthereumProvider) => Promise; - deinitializeNexus: () => Promise; - attachEventHooks: () => void; - setIntent: (data: OnIntentHookData | null) => void; - setAllowance: (data: OnAllowanceHookData | null) => void; -} - -const NexusContext = createContext(undefined); - -type NexusProviderProps = { - children: React.ReactNode; - config?: { - network?: NexusNetwork; - debug?: boolean; - }; -}; - -const defaultConfig: Required = { - network: "mainnet", - debug: false, -}; - -const NexusProvider = ({ - children, - config = defaultConfig, -}: NexusProviderProps) => { - const stableConfig = useMemo( - () => ({ ...defaultConfig, ...config }), - [config], - ); - - const sdkRef = useRef(null); - sdkRef.current ??= new NexusSDK({ - ...stableConfig, - }); - const sdk = sdkRef.current; - - const [nexusSDK, setNexusSDK] = useState(null); - const [loading, setLoading] = useState(false); - const [supportedChainsAndTokens, setSupportedChainsAndTokens] = - useState( - sdk.utils.getSupportedChains( - stableConfig.network === "testnet" ? 0 : undefined, - ) ?? null, - ); - const [swapSupportedChainsAndTokens, setSwapSupportedChainsAndTokens] = - useState( - sdk.utils.getSwapSupportedChainsAndTokens() ?? null, - ); - const [bridgableBalance, setBridgableBalance] = useState( - null, - ); - const [swapBalance, setSwapBalance] = useState(null); - const exchangeRate = useRef | null>(null); - - const intent = useRef(null); - const allowance = useRef(null); - const swapIntent = useRef(null); - - useEffect(() => { - const list = sdk.utils.getSupportedChains( - stableConfig.network === "testnet" ? 0 : undefined, - ); - setSupportedChainsAndTokens(list ?? null); - const swapList = sdk.utils.getSwapSupportedChainsAndTokens(); - setSwapSupportedChainsAndTokens(swapList ?? null); - }, [sdk, stableConfig.network]); - - const setupNexus = useCallback(async () => { - const list = sdk.utils.getSupportedChains( - stableConfig.network === "testnet" ? 0 : undefined, - ); - setSupportedChainsAndTokens(list ?? null); - const swapList = sdk.utils.getSwapSupportedChainsAndTokens(); - setSwapSupportedChainsAndTokens(swapList ?? null); - const [bridgeAbleBalanceResult, rates] = await Promise.allSettled([ - sdk.getBalancesForBridge(), - sdk.utils.getCoinbaseRates(), - ]); - console.log("bridgeAbleBalanceResult", bridgeAbleBalanceResult); - - if (bridgeAbleBalanceResult.status === "fulfilled") { - setBridgableBalance(bridgeAbleBalanceResult.value); - } - - if (rates?.status === "fulfilled") { - // Coinbase returns "units per USD" (e.g., 1 USD = 0.00028 ETH). - // Convert to "USD per unit" (e.g., 1 ETH = ~$3514) for straightforward UI calculations. - const usdPerUnit: Record = {}; - - for (const [symbol, value] of Object.entries(rates.value)) { - const unitsPerUsd = Number.parseFloat(String(value)); - if (Number.isFinite(unitsPerUsd) && unitsPerUsd > 0) { - usdPerUnit[symbol.toUpperCase()] = 1 / unitsPerUsd; - } - } - exchangeRate.current = usdPerUnit; - } - }, [sdk, stableConfig.network]); - - const initializeNexus = async (provider: EthereumProvider) => { - setLoading(true); - try { - if (sdk.isInitialized()) throw new Error("Nexus is already initialized"); - await sdk.initialize(provider); - setNexusSDK(sdk); - } catch (error) { - console.error("Error initializing Nexus:", error); - } finally { - setLoading(false); - } - }; - - const deinitializeNexus = async () => { - try { - if (!nexusSDK) return; - await nexusSDK.deinit(); - setNexusSDK(null); - setBridgableBalance(null); - setSwapBalance(null); - exchangeRate.current = null; - intent.current = null; - swapIntent.current = null; - allowance.current = null; - setLoading(false); - } catch (error) { - console.error("Error deinitializing Nexus:", error); - } - }; - - const attachEventHooks = () => { - sdk.setOnAllowanceHook((data: OnAllowanceHookData) => { - /** - * Useful when you want the user to select, min, max or a custom value - * Can use this to capture data and then show it on the UI - * @see - always call data.allow() to progress the flow, otherwise it will stay stuck here. - * const {allow, sources, deny} = data - * @example allow(['min', 'max', '0.5']), the array in allow function should match number of sources. - * You can skip setting this hook if you want, sdk will auto progress if this hook is not attached - */ - allowance.current = data; - }); - - sdk.setOnIntentHook((data: OnIntentHookData) => { - /** - * Useful when you want to capture the intent, and display it on the UI (bridge, bridgeAndTransfer, bridgeAndExecute) - * const {allow, deny, intent, refresh} = data - * @see - always call data.allow() to progress the flow, otherwise it will stay stuck here. - * deny() to reject the intent - * refresh() to refresh the intent, best to call refresh in 15 second intervals - * data.intent -> details about the intent, useful when wanting to display info on UI - * You can skip setting this hook if you want, sdk will auto progress if this hook is not attached - */ - intent.current = data; - }); - - sdk.setOnSwapIntentHook((data: OnSwapIntentHookData) => { - /** - * Same behaviour and function as setOnIntentHook, except this one is for swaps exclusively - */ - swapIntent.current = data; - }); - }; - - const handleInit = async (provider: EthereumProvider) => { - console.log("[NexusProvider] handleInit called"); - console.log("[NexusProvider] SDK isInitialized:", sdk.isInitialized()); - console.log("[NexusProvider] Loading:", loading); - - if (sdk.isInitialized() || loading) { - console.log( - "[NexusProvider] Skipping init - already initialized or loading", - ); - return; - } - - if (!provider || typeof provider.request !== "function") { - console.error("[NexusProvider] Invalid provider:", provider); - throw new Error("Invalid EIP-1193 provider"); - } - - console.log("[NexusProvider] Calling initializeNexus..."); - await initializeNexus(provider); - - console.log("[NexusProvider] Calling setupNexus..."); - await setupNexus(); - - console.log("[NexusProvider] Calling attachEventHooks..."); - attachEventHooks(); - - console.log("[NexusProvider] handleInit complete!"); - }; - - const fetchBridgableBalance = async () => { - try { - const updatedBalance = await sdk.getBalancesForBridge(); - console.log("bridgeAbleBalanceResult", updatedBalance); - setBridgableBalance(updatedBalance); - } catch (error) { - console.error("Error fetching bridgable balance:", error); - } - }; - - const fetchSwapBalance = async () => { - try { - const updatedBalance = await sdk.getBalancesForSwap(); - console.log("swapBalance", updatedBalance); - setSwapBalance(updatedBalance); - } catch (error) { - console.error("Error fetching swap balance:", error); - } - }; - - function getFiatValue(amount: number, token: string) { - const key = token.toUpperCase(); - const rate = exchangeRate.current?.[key] ?? 1; - return rate * amount; - } - - useAccountEffect({ - onDisconnect() { - deinitializeNexus(); - }, - }); - - const setIntent = (data: OnIntentHookData | null) => { - intent.current = data; - }; - - const setAllowance = (data: OnAllowanceHookData | null) => { - allowance.current = data; - }; - - const value = useMemo( - () => ({ - nexusSDK, - initializeNexus, - deinitializeNexus, - attachEventHooks, - intent, - allowance, - handleInit, - supportedChainsAndTokens, - swapSupportedChainsAndTokens, - bridgableBalance, - swapBalance: swapBalance, - network: config?.network, - loading, - fetchBridgableBalance, - fetchSwapBalance, - swapIntent, - exchangeRate: exchangeRate.current, - getFiatValue, - setIntent, - setAllowance, - }), - [ - nexusSDK, - initializeNexus, - deinitializeNexus, - attachEventHooks, - handleInit, - swapBalance, - config, - loading, - fetchBridgableBalance, - fetchSwapBalance, - setIntent, - setAllowance, - supportedChainsAndTokens, - swapSupportedChainsAndTokens, - ], - ); - return ( - {children} - ); -}; - -export function useNexus() { - const context = useContext(NexusContext); - if (!context) { - throw new Error("useNexus must be used within a NexusProvider"); - } - return context; -} - -export default NexusProvider; diff --git a/apps/citrea/src/components/nexus/useNexus.ts b/apps/citrea/src/components/nexus/useNexus.ts deleted file mode 100644 index 2dcd3f9..0000000 --- a/apps/citrea/src/components/nexus/useNexus.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useContext } from "react"; -import { NexusContext } from "./NexusProvider"; - -export function useNexus() { - const context = useContext(NexusContext); - if (!context) { - throw new Error("useNexus must be used within a NexusProvider"); - } - return context; -} diff --git a/apps/citrea/src/components/ui/accordion.tsx b/apps/citrea/src/components/ui/accordion.tsx deleted file mode 100644 index 75b4f79..0000000 --- a/apps/citrea/src/components/ui/accordion.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDownIcon } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -function Accordion({ - ...props -}: React.ComponentProps) { - return ; -} - -function AccordionItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AccordionTrigger({ - className, - children, - hideChevron = true, - containerClassName = "w-full", - ...props -}: React.ComponentProps & { - hideChevron?: boolean; - containerClassName?: string; -}) { - return ( - - svg]:rotate-180", - className, - )} - {...props} - > - {children} - {!hideChevron && ( - - )} - - - ); -} - -function AccordionContent({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - -
{children}
-
- ); -} - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/apps/citrea/src/components/ui/badge.tsx b/apps/citrea/src/components/ui/badge.tsx deleted file mode 100644 index fd3a406..0000000 --- a/apps/citrea/src/components/ui/badge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" - - return ( - - ) -} - -export { Badge, badgeVariants } diff --git a/apps/citrea/src/components/ui/button.tsx b/apps/citrea/src/components/ui/button.tsx deleted file mode 100644 index 71ab81f..0000000 --- a/apps/citrea/src/components/ui/button.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-sm": "size-8", - "icon-lg": "size-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -); - -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean; - }) { - const Comp = asChild ? Slot : "button"; - - return ( - - ); -} - -export { Button, buttonVariants }; diff --git a/apps/citrea/src/components/ui/card.tsx b/apps/citrea/src/components/ui/card.tsx deleted file mode 100644 index d05bbc6..0000000 --- a/apps/citrea/src/components/ui/card.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -function Card({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} diff --git a/apps/citrea/src/components/ui/dialog.tsx b/apps/citrea/src/components/ui/dialog.tsx deleted file mode 100644 index d9ccec9..0000000 --- a/apps/citrea/src/components/ui/dialog.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client" - -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Dialog({ - ...props -}: React.ComponentProps) { - return -} - -function DialogTrigger({ - ...props -}: React.ComponentProps) { - return -} - -function DialogPortal({ - ...props -}: React.ComponentProps) { - return -} - -function DialogClose({ - ...props -}: React.ComponentProps) { - return -} - -function DialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function DialogContent({ - className, - children, - showCloseButton = true, - ...props -}: React.ComponentProps & { - showCloseButton?: boolean -}) { - return ( - - - - {children} - {showCloseButton && ( - - - Close - - )} - - - ) -} - -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function DialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -} diff --git a/apps/citrea/src/components/ui/input.tsx b/apps/citrea/src/components/ui/input.tsx deleted file mode 100644 index 0316cc4..0000000 --- a/apps/citrea/src/components/ui/input.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ); -} - -export { Input }; diff --git a/apps/citrea/src/components/ui/label.tsx b/apps/citrea/src/components/ui/label.tsx deleted file mode 100644 index fb5fbc3..0000000 --- a/apps/citrea/src/components/ui/label.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client" - -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" - -import { cn } from "@/lib/utils" - -function Label({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { Label } diff --git a/apps/citrea/src/components/ui/select.tsx b/apps/citrea/src/components/ui/select.tsx deleted file mode 100644 index 91c5bdf..0000000 --- a/apps/citrea/src/components/ui/select.tsx +++ /dev/null @@ -1,185 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as SelectPrimitive from "@radix-ui/react-select"; -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -function Select({ - ...props -}: React.ComponentProps) { - return ; -} - -function SelectGroup({ - ...props -}: React.ComponentProps) { - return ; -} - -function SelectValue({ - ...props -}: React.ComponentProps) { - return ; -} - -function SelectTrigger({ - className, - size = "default", - children, - ...props -}: React.ComponentProps & { - size?: "sm" | "default"; -}) { - return ( - - {children} - - - - - ); -} - -function SelectContent({ - className, - children, - position = "popper", - ...props -}: React.ComponentProps) { - return ( - - - - - {children} - - - - - ); -} - -function SelectLabel({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function SelectItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ); -} - -function SelectSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function SelectScrollUpButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -function SelectScrollDownButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -}; diff --git a/apps/citrea/src/components/ui/separator.tsx b/apps/citrea/src/components/ui/separator.tsx deleted file mode 100644 index be47ba4..0000000 --- a/apps/citrea/src/components/ui/separator.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Separator({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -export { Separator }; diff --git a/apps/citrea/src/components/ui/skeleton.tsx b/apps/citrea/src/components/ui/skeleton.tsx deleted file mode 100644 index 32ea0ef..0000000 --- a/apps/citrea/src/components/ui/skeleton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { cn } from "@/lib/utils" - -function Skeleton({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { Skeleton } diff --git a/apps/citrea/src/components/ui/tooltip.tsx b/apps/citrea/src/components/ui/tooltip.tsx deleted file mode 100644 index 99aefa0..0000000 --- a/apps/citrea/src/components/ui/tooltip.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; - -import { cn } from "@/lib/utils"; - -function TooltipProvider({ - delayDuration = 0, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function Tooltip({ - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -function TooltipTrigger({ - ...props -}: React.ComponentProps) { - return ; -} - -function TooltipContent({ - className, - sideOffset = 0, - children, - ...props -}: React.ComponentProps) { - return ( - - - {children} - - - - ); -} - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/apps/citrea/src/components/view-history/hooks/useViewHistory.ts b/apps/citrea/src/components/view-history/hooks/useViewHistory.ts deleted file mode 100644 index ea4cece..0000000 --- a/apps/citrea/src/components/view-history/hooks/useViewHistory.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { type RFF } from "@avail-project/nexus-core"; -import { useNexus } from "../../nexus/NexusProvider"; -import { useCallback, useEffect, useState } from "react"; - -const ITEMS_PER_PAGE = 10; - -function formatExpiryDate(timestamp: number) { - const date = new Date(timestamp * 1000); - const formatted = date.toLocaleString("en-US", { - month: "short", - day: "2-digit", - year: "numeric", - }); - return formatted.replace(" ", ", "); -} - -const useViewHistory = () => { - const { nexusSDK } = useNexus(); - const [history, setHistory] = useState(null); - const [displayedHistory, setDisplayedHistory] = useState([]); - const [page, setPage] = useState(0); - const [hasMore, setHasMore] = useState(true); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const [sentinelNode, setSentinelNode] = useState(null); - - const observerTarget = useCallback((node: HTMLDivElement | null) => { - setSentinelNode(node); - }, []); - - const fetchIntentHistory = useCallback(async () => { - if (!nexusSDK) return; - - try { - const fetchedHistory = await nexusSDK.getMyIntents(); - const normalizedHistory = fetchedHistory ?? []; - - setHistory(normalizedHistory); - setDisplayedHistory(normalizedHistory.slice(0, ITEMS_PER_PAGE)); - setPage(0); - setHasMore(normalizedHistory.length > ITEMS_PER_PAGE); - setIsLoadingMore(false); - } catch (error) { - console.error("Error fetching intent history:", error); - } - }, [nexusSDK]); - - useEffect(() => { - if (!history) { - void fetchIntentHistory(); - } - }, [history, fetchIntentHistory]); - - const loadMore = useCallback(() => { - if (!history || isLoadingMore || !hasMore) return; - setIsLoadingMore(true); - - setTimeout(() => { - const nextPage = page + 1; - const startIndex = nextPage * ITEMS_PER_PAGE; - const endIndex = startIndex + ITEMS_PER_PAGE; - const newItems = history.slice(startIndex, endIndex); - - if (newItems.length > 0) { - setDisplayedHistory((prev) => [...prev, ...newItems]); - setPage(nextPage); - setHasMore(endIndex < history.length); - } else { - setHasMore(false); - } - - setIsLoadingMore(false); - }, 300); - }, [history, page, isLoadingMore, hasMore]); - - useEffect(() => { - if (!sentinelNode) { - return; - } - - const rootElement = sentinelNode.parentElement; - - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore && !isLoadingMore) { - loadMore(); - } - }, - { threshold: 0.1, root: rootElement ?? null }, - ); - - observer.observe(sentinelNode); - - return () => { - observer.disconnect(); - }; - }, [sentinelNode, loadMore, hasMore, isLoadingMore, displayedHistory.length]); - - const getStatus = (pastIntent: RFF) => { - if (pastIntent?.fulfilled) { - return "Fulfilled"; - } else if (pastIntent?.deposited) { - return "Deposited"; - } else if (pastIntent?.refunded) { - return "Refunded"; - } else { - return "Failed"; - } - }; - - return { - history, - displayedHistory, - page, - hasMore, - isLoadingMore, - getStatus, - observerTarget, - ITEMS_PER_PAGE, - formatExpiryDate, - fetchIntentHistory, - }; -}; - -export default useViewHistory; diff --git a/apps/citrea/src/components/walletConnect.tsx b/apps/citrea/src/components/walletConnect.tsx deleted file mode 100644 index 2dcb18f..0000000 --- a/apps/citrea/src/components/walletConnect.tsx +++ /dev/null @@ -1,161 +0,0 @@ -"use client"; -import * as React from "react"; -import type { EthereumProvider } from "@avail-project/nexus-core"; -import { useAccount } from "wagmi"; -import { useNexus } from "./nexus/NexusProvider"; -import { toast } from "sonner"; - -interface PreviewPanelProps { - children: React.ReactNode; -} - -export function PreviewPanel({ children }: Readonly) { - const [loading, setLoading] = React.useState(false); - const [initError, setInitError] = React.useState(null); - const { status, connector, address } = useAccount(); - const { nexusSDK, handleInit, deinitializeNexus, setIntent, setAllowance } = - useNexus(); - const prevAddressRef = React.useRef(address); - - const initializeNexus = React.useCallback(async () => { - if (loading || nexusSDK) return; // Prevent multiple calls - - console.log("[Nexus Init] Starting initialization..."); - console.log("[Nexus Init] Connector:", connector); - console.log("[Nexus Init] Connector name:", connector?.name); - console.log("[Nexus Init] Connector type:", connector?.type); - - setLoading(true); - setInitError(null); - - // Create a timeout promise - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error("Nexus initialization timed out after 30 seconds")); - }, 30000); // 30 second timeout - }); - - try { - if (!connector) { - throw new Error("No connector available"); - } - - console.log("[Nexus Init] Getting provider from connector..."); - const provider = (await connector.getProvider()) as EthereumProvider; - - console.log("[Nexus Init] Provider:", provider); - console.log("[Nexus Init] Provider type:", typeof provider); - console.log( - "[Nexus Init] Provider has request:", - typeof provider?.request === "function", - ); - - if (!provider) { - throw new Error("No provider available from connector"); - } - - if (typeof provider.request !== "function") { - throw new Error( - "Provider does not have a request method (not EIP-1193 compliant)", - ); - } - - console.log("[Nexus Init] Provider validated, calling handleInit..."); - - // Race between initialization and timeout - await Promise.race([handleInit(provider), timeoutPromise]); - - console.log("[Nexus Init] Initialization successful!"); - } catch (error) { - console.error("[Nexus Init] Initialization failed:", error); - const errorMessage = (error as Error)?.message || "Unknown error"; - setInitError(errorMessage); - toast.error(`Failed to initialize Nexus: ${errorMessage}`); - } finally { - setLoading(false); - } - }, [connector, handleInit, loading, nexusSDK]); - - // Handle wallet disconnection - clear Nexus state and balances - React.useEffect(() => { - if (status === "disconnected" && nexusSDK) { - deinitializeNexus(); - setIntent(null); - setAllowance(null); - prevAddressRef.current = undefined; - } - if (status === "disconnected") { - setInitError(null); - } - }, [status, nexusSDK, deinitializeNexus, setIntent, setAllowance]); - - // Handle account change - reinitialize Nexus when account address changes - React.useEffect(() => { - if ( - status === "connected" && - address && - address !== prevAddressRef.current - ) { - const previousAddress = prevAddressRef.current; - const currentAddress = address; - prevAddressRef.current = address; - - // If account changed and Nexus is initialized, reinitialize with new account - if (nexusSDK && previousAddress !== undefined) { - // Account changed - deinitialize and reinitialize - deinitializeNexus().then(() => { - // Small delay to ensure deinit completes, then reinitialize - setTimeout(() => { - // Check if still connected and address hasn't changed again - if ( - currentAddress === prevAddressRef.current && - !loading && - !initError - ) { - initializeNexus(); - } - }, 100); - }); - } - } else if (status === "connected" && address && !prevAddressRef.current) { - // First connection - set the address - prevAddressRef.current = address; - } - }, [ - status, - nexusSDK, - address, - initError, - initializeNexus, - deinitializeNexus, - loading, - ]); - - // Auto-initialize Nexus when wallet is connected and address is available - React.useEffect(() => { - if ( - status === "connected" && - !nexusSDK && - !loading && - !initError && - address && - connector - ) { - initializeNexus(); - } - }, [ - status, - nexusSDK, - initError, - address, - connector, - initializeNexus, - loading, - ]); - - return ( -
- {children} -
- ); -} diff --git a/apps/citrea/src/index.css b/apps/citrea/src/index.css deleted file mode 100644 index 6a02ea1..0000000 --- a/apps/citrea/src/index.css +++ /dev/null @@ -1,152 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -body, -#root { - height: 100svh; - width: 100svw; - overflow-x: hidden; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - /* button { - background-color: #f9f9f9; - } */ -} - -@theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/apps/citrea/src/lib/button-variants.ts b/apps/citrea/src/lib/button-variants.ts deleted file mode 100644 index d87142c..0000000 --- a/apps/citrea/src/lib/button-variants.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { cva } from "class-variance-authority"; - -const buttonVariants = cva( - "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -export { buttonVariants }; diff --git a/apps/citrea/src/lib/posthog.ts b/apps/citrea/src/lib/posthog.ts deleted file mode 100644 index aa51bd9..0000000 --- a/apps/citrea/src/lib/posthog.ts +++ /dev/null @@ -1,76 +0,0 @@ -import posthog from 'posthog-js'; - -const DEFAULT_POSTHOG_KEY = 'phc_UD6lQU3PEw1d8oo8E17rJLmRAR7kxJbQ5OseHuCvi7N'; -const DEFAULT_POSTHOG_HOST = 'https://us.i.posthog.com'; - -let isInitialized = false; - -/** - * Initialize PostHog analytics - */ -export function initPostHog(options?: { - apiKey?: string; - apiHost?: string; - debug?: boolean; -}): void { - if (isInitialized) return; - - const apiKey = options?.apiKey || DEFAULT_POSTHOG_KEY; - const apiHost = options?.apiHost || DEFAULT_POSTHOG_HOST; - const debug = options?.debug ?? true; // Always enable debug for troubleshooting - - console.log('[PostHog] Initializing with host:', apiHost); - - posthog.init(apiKey, { - api_host: apiHost, - person_profiles: 'identified_only', - autocapture: false, - capture_pageview: false, - capture_pageleave: true, - persistence: 'localStorage', // Use localStorage for persistence - loaded: (ph) => { - console.log('[PostHog] Loaded successfully'); - if (debug) { - ph.debug(); - } - }, - }); - - isInitialized = true; -} - -/** - * Capture a bridge submit event - */ -export interface BridgeSubmitEventProperties { - chain: string | number; - chainName: string; - tokenSymbol: string; - amount: string; - fast_bridge: 'megaeth' | 'citrea' | 'monad'; -} - -export function trackBridgeSubmit(properties: BridgeSubmitEventProperties): void { - if (!isInitialized) { - console.warn('[PostHog] Not initialized, initializing now...'); - initPostHog(); - } - - console.log('[PostHog] Capturing event: nexus_fast_bridge_demo_submit', properties); - posthog.capture('nexus_fast_bridge_demo_submit', properties); -} - -/** - * Generic capture wrapper - */ -export function capture(event: string, properties?: Record): void { - if (!isInitialized) { - console.warn('[PostHog] Not initialized, initializing now...'); - initPostHog(); - } - - console.log('[PostHog] Capturing event:', event, properties); - posthog.capture(event, properties); -} - -export { posthog }; diff --git a/apps/citrea/src/lib/url-params.ts b/apps/citrea/src/lib/url-params.ts deleted file mode 100644 index 2fb5962..0000000 --- a/apps/citrea/src/lib/url-params.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { type SUPPORTED_CHAINS_IDS, type SUPPORTED_TOKENS } from "@avail-project/nexus-core"; -import { isAddress } from "viem"; - -const ALLOWED_TOKENS = new Set(["USDC", "USDT"]) as Set; -const FILTERED_CHAIN_ID = 728126428; - -interface BridgeParams { - to?: SUPPORTED_CHAINS_IDS; - token?: SUPPORTED_TOKENS; - recipient?: `0x${string}`; - amount?: string; -} - -function isValidToken(token: string | null): token is SUPPORTED_TOKENS { - if (!token) return false; - const upperToken = token.toUpperCase().trim(); - return ALLOWED_TOKENS.has(upperToken as SUPPORTED_TOKENS); -} - -function isValidChain(chainStr: string | null): boolean { - if (!chainStr) return false; - const chainId = Number.parseInt(chainStr, 10); - if (!Number.isInteger(chainId) || chainId <= 0 || chainId > Number.MAX_SAFE_INTEGER) return false; - if (chainId === FILTERED_CHAIN_ID) return false; - return true; -} - -function sanitizeAmount(amount: string | null): string | undefined { - if (!amount) return undefined; - const sanitized = amount.trim(); - if (sanitized === "" || sanitized === ".") return undefined; - if (!/^\d*\.?\d*$/.test(sanitized)) return undefined; - const num = Number.parseFloat(sanitized); - if (Number.isNaN(num) || num <= 0) return undefined; - if (num > 1e9) return undefined; - return sanitized; -} - -export function readBridgeParams(): BridgeParams { - const params = new URLSearchParams(window.location.search); - const toStr = params.get("to"); - const tokenStr = params.get("token"); - const recipient = params.get("recipient"); - const amountStr = params.get("amount"); - - const to = toStr && toStr !== "self" && isValidChain(toStr) ? (Number.parseInt(toStr, 10) as SUPPORTED_CHAINS_IDS) : undefined; - const token = isValidToken(tokenStr) ? (tokenStr!.toUpperCase() as SUPPORTED_TOKENS) : undefined; - const sanitizedAmount = sanitizeAmount(amountStr); - const recipientAddress = recipient && isAddress(recipient) ? recipient : undefined; - - return { - to, - token, - recipient: recipientAddress, - amount: sanitizedAmount, - }; -} - -export function writeBridgeParams(params: BridgeParams): void { - const url = new URL(window.location.href); - - url.searchParams.delete("to"); - url.searchParams.delete("token"); - url.searchParams.delete("recipient"); - url.searchParams.delete("amount"); - - if (params.to && isValidChain(String(params.to))) { - url.searchParams.set("to", String(params.to)); - } - if (params.token && ALLOWED_TOKENS.has(params.token)) { - url.searchParams.set("token", params.token); - } - if (params.recipient && isAddress(params.recipient)) { - url.searchParams.set("recipient", params.recipient); - } - const sanitizedAmount = sanitizeAmount(params.amount ?? null); - if (sanitizedAmount) { - url.searchParams.set("amount", sanitizedAmount); - } - - window.history.replaceState({}, "", url.toString()); -} diff --git a/apps/citrea/src/lib/utils.ts b/apps/citrea/src/lib/utils.ts deleted file mode 100644 index f9923e8..0000000 --- a/apps/citrea/src/lib/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} -export function absoluteUrl(path: string) { - return `${process.env.NEXT_PUBLIC_BASE_URL}${path}`; -} diff --git a/apps/citrea/src/main.tsx b/apps/citrea/src/main.tsx index e98387c..0802495 100644 --- a/apps/citrea/src/main.tsx +++ b/apps/citrea/src/main.tsx @@ -1,46 +1,3 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import "./index.css"; -import App from "./App"; -import { initPostHog } from "./lib/posthog"; +import { bootstrapApp } from "@/bootstrap"; -// Initialize PostHog analytics -initPostHog(); - -// Clean up WalletConnect IndexedDB before app loads to prevent structure errors -const cleanupWalletConnectSubscription = () => { - try { - const dbName = "WALLET_CONNECT_V2_INDEXED_DB"; - - // Delete the entire database to prevent structure errors - const deleteRequest = indexedDB.deleteDatabase(dbName); - - deleteRequest.onsuccess = () => { - console.log( - "[WalletConnect] Database deleted successfully, will be recreated fresh", - ); - }; - - deleteRequest.onerror = () => { - console.debug( - "[WalletConnect] Database deletion failed or DB doesn't exist", - ); - }; - - deleteRequest.onblocked = () => { - console.debug("[WalletConnect] Database deletion blocked, may be in use"); - }; - } catch (error) { - // Ignore errors - this is a cleanup operation - console.debug("[WalletConnect] Cleanup skipped:", error); - } -}; - -// Run cleanup before rendering -cleanupWalletConnectSubscription(); - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); +bootstrapApp(); diff --git a/apps/citrea/src/providers/web3Provider.tsx b/apps/citrea/src/providers/web3Provider.tsx deleted file mode 100644 index f913430..0000000 --- a/apps/citrea/src/providers/web3Provider.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import React from "react"; -import { ConnectKitProvider, getDefaultConfig } from "connectkit"; -import { WagmiProvider, createConfig, http } from "wagmi"; -import { - mainnet, - scroll, - polygon, - optimism, - arbitrum, - base, - avalanche, - sophon, - kaia, - monad, - type Chain, -} from "wagmi/chains"; -import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; -import config from "../../config"; - -const walletConnectProjectId = import.meta.env.VITE_WALLET_CONNECT_ID; - -const chain: Chain = { - id: config.chainId, - name: config.chainName, - nativeCurrency: { - name: config.chainNativeCurrency.name, - symbol: config.chainNativeCurrency.symbol, - decimals: config.chainNativeCurrency.decimals, - }, - rpcUrls: { - default: { http: [config.chainRpcUrl] }, - }, - blockExplorers: { - default: { name: "Explorer", url: config.chainBlockExplorerUrl }, - }, - testnet: config.chainTestnet, -}; - -const megaeth: Chain = { - id: 4326, - name: "MegaETH Mainnet", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - rpcUrls: { - default: { http: [import.meta.env.VITE_MEGAETH_RPC] }, - }, - blockExplorers: { - default: { name: "Explorer", url: "https://megaeth.blockscout.com" }, - }, - testnet: false, -}; - -//ideally we should add private rpcs for each, for now it fallbacks to default rpcs -const transports = { - [mainnet.id]: http(import.meta.env.VITE_MAINNET_RPC), - [base.id]: http(import.meta.env.VITE_BASE_RPC), - [arbitrum.id]: http(import.meta.env.VITE_ARBITRUM_RPC), - [optimism.id]: http(import.meta.env.VITE_OPTIMISM_RPC), - [polygon.id]: http(import.meta.env.VITE_POLYGON_RPC), - [scroll.id]: http(import.meta.env.VITE_SCROLL_RPC), - [avalanche.id]: http(import.meta.env.VITE_AVALANCHE_RPC), - [sophon.id]: http(import.meta.env.VITE_SOPHON_RPC), - [kaia.id]: http(import.meta.env.VITE_KAIA_RPC), - [chain.id]: http(config.chainRpcUrl), - [monad.id]: http(import.meta.env.VITE_MONAD_RPC), - [megaeth.id]: http(import.meta.env.VITE_MEGAETH_RPC), -}; - -const defaultConfigParams = getDefaultConfig({ - appName: config.appTitle ?? "Nexus Elements", - walletConnectProjectId, - chains: [ - chain, - mainnet, - base, - sophon, - kaia, - arbitrum, - avalanche, - optimism, - polygon, - scroll, - monad, - megaeth, - ], - transports, - enableFamily: false, -}); - -const wagmiConfig = createConfig(defaultConfigParams); -const queryClient = new QueryClient(); - -const Web3Provider = ({ children }: { children: React.ReactNode }) => { - return ( - - - {children} - - - ); -}; - -export default Web3Provider; diff --git a/apps/citrea/src/runtime.ts b/apps/citrea/src/runtime.ts new file mode 100644 index 0000000..b03b1ac --- /dev/null +++ b/apps/citrea/src/runtime.ts @@ -0,0 +1,35 @@ +import { SUPPORTED_CHAINS } from "@avail-project/nexus-core"; +import type { ChainFeatures } from "@/types/runtime"; +import config from "../config"; + +export const appConfig = config; + +export const chainFeatures: ChainFeatures = { + slug: "citrea", + analyticsFastBridgeKey: "citrea", + maxBridgeAmount: 550, + walletInitDelayMs: 0, + showFluffeyMascot: false, + showPromoBanner: false, + pageDescription: + "Bridge USDC to Citrea instantly with Avail Fast Bridge. Leveraging Avail Nexus infrastructure, we provide a secure, decentralized path to move liquidity from 12+ chains including Ethereum and major L2s directly to Citrea.", + mapUsdmDisplaySymbolToUsdc: false, + mapUsdmToUsdcBalance: false, + tokenLogoOverrideBySymbol: { + USDM: "https://raw.githubusercontent.com/availproject/nexus-assets/refs/heads/main/tokens/usdm/logo.png", + }, + denyIntentOnReset: true, + tokenDenyListByChainId: { + [SUPPORTED_CHAINS.CITREA]: ["cBTC"], + }, + allowanceLogoOverrideByChainId: { + [SUPPORTED_CHAINS.CITREA]: "/citrea-chain-logo.webp", + }, + amountInputUseCalculatedMaxHeader: false, + amountInputShowDestinationBadge: false, + amountInputUseSourceSymbolInBreakdown: false, + hideMegaethSourceForUsdm: true, + feeBreakdownHideGasSupplied: false, + feeBreakdownKeepZeroRows: false, + dialogShowCloseButton: true, +}; diff --git a/apps/citrea/src/vite-env.d.ts b/apps/citrea/src/vite-env.d.ts index 04436df..1a8b0eb 100644 --- a/apps/citrea/src/vite-env.d.ts +++ b/apps/citrea/src/vite-env.d.ts @@ -3,57 +3,56 @@ import { SUPPORTED_CHAINS } from "@avail-project/nexus-core"; export interface AppEnv { - readonly VITE_APP_BASE_PATH?: string; - readonly VITE_CONFIG_CHAIN_ID: number; - readonly VITE_CONFIG_CHAIN_NAME: string; - readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_NAME: string; - readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_SYMBOL: string; - readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_DECIMALS: number; - readonly VITE_CONFIG_CHAIN_RPC_URL: string; - readonly VITE_CONFIG_CHAIN_BLOCK_EXPLORER_URL: string; - readonly VITE_CONFIG_CHAIN_TESTNET: boolean; - readonly VITE_CONFIG_CHAIN_USE_CHAIN_LOGO: boolean; - readonly VITE_CONFIG_CHAIN_ICON_URL: string; - readonly VITE_CONFIG_CHAIN_LOGO_URL: string; - readonly VITE_CONFIG_CHAIN_GIF_URL: string; - readonly VITE_CONFIG_CHAIN_GIF_ALT: string; - readonly VITE_CONFIG_CHAIN_HERO_TEXT: string; - readonly VITE_CONFIG_APP_TITLE: string; - readonly VITE_CONFIG_APP_DESCRIPTION: string; - readonly VITE_CONFIG_PRIMARY_COLOR: string; - readonly VITE_CONFIG_SECONDARY_COLOR: string; - readonly VITE_CONFIG_NEXUS_NETWORK: 'mainnet' | 'testnet' | 'devnet'; - readonly VITE_CONFIG_NEXUS_SUPPORTED_CHAIN: SUPPORTED_CHAINS; - readonly VITE_CONFIG_NEXUS_PRIMARY_TOKEN: string; + readonly VITE_APP_BASE_PATH?: string; + readonly VITE_CONFIG_APP_DESCRIPTION: string; + readonly VITE_CONFIG_APP_META_BACKGROUND_COLOR: string; + readonly VITE_CONFIG_APP_META_CANONICAL_URL: string; + readonly VITE_CONFIG_APP_META_DESCRIPTION: string; + readonly VITE_CONFIG_APP_META_FAVICON_URL: string; + readonly VITE_CONFIG_APP_META_IMAGE_URL: string; + readonly VITE_CONFIG_APP_META_THEME_COLOR: string; - readonly VITE_CONFIG_APP_META_TITLE: string; - readonly VITE_CONFIG_APP_META_DESCRIPTION: string; - readonly VITE_CONFIG_APP_META_CANONICAL_URL: string; - readonly VITE_CONFIG_APP_META_FAVICON_URL: string; - readonly VITE_CONFIG_APP_META_THEME_COLOR: string; - readonly VITE_CONFIG_APP_META_IMAGE_URL: string; - readonly VITE_CONFIG_APP_META_BACKGROUND_COLOR: string; + readonly VITE_CONFIG_APP_META_TITLE: string; + readonly VITE_CONFIG_APP_TITLE: string; + readonly VITE_CONFIG_CHAIN_BLOCK_EXPLORER_URL: string; + readonly VITE_CONFIG_CHAIN_GIF_ALT: string; + readonly VITE_CONFIG_CHAIN_GIF_URL: string; + readonly VITE_CONFIG_CHAIN_HERO_TEXT: string; + readonly VITE_CONFIG_CHAIN_ICON_URL: string; + readonly VITE_CONFIG_CHAIN_ID: number; + readonly VITE_CONFIG_CHAIN_LOGO_URL: string; + readonly VITE_CONFIG_CHAIN_NAME: string; + readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_DECIMALS: number; + readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_NAME: string; + readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_SYMBOL: string; + readonly VITE_CONFIG_CHAIN_RPC_URL: string; + readonly VITE_CONFIG_CHAIN_TESTNET: boolean; + readonly VITE_CONFIG_CHAIN_USE_CHAIN_LOGO: boolean; + readonly VITE_CONFIG_NEXUS_NETWORK: "mainnet" | "testnet" | "devnet"; + readonly VITE_CONFIG_NEXUS_PRIMARY_TOKEN: string; + readonly VITE_CONFIG_NEXUS_SUPPORTED_CHAIN: SUPPORTED_CHAINS; + readonly VITE_CONFIG_PRIMARY_COLOR: string; + readonly VITE_CONFIG_SECONDARY_COLOR: string; } declare global { + interface ImportMetaEnv extends AppEnv { + readonly VITE_ARBITRUM_RPC: string; + readonly VITE_AVALANCHE_RPC: string; + readonly VITE_BASE: string; + readonly VITE_BASE_RPC: string; + readonly VITE_KAIA_RPC: string; + readonly VITE_MAINNET_RPC: string; + readonly VITE_MEGAETH_RPC: string; + readonly VITE_MONAD_RPC: string; + readonly VITE_OPTIMISM_RPC: string; + readonly VITE_POLYGON_RPC: string; + readonly VITE_SCROLL_RPC: string; + readonly VITE_SOPHON_RPC: string; + readonly VITE_WALLET_CONNECT_ID: string; + } - interface ImportMetaEnv extends AppEnv { - readonly VITE_WALLET_CONNECT_ID: string; - readonly VITE_MAINNET_RPC: string; - readonly VITE_BASE_RPC: string; - readonly VITE_ARBITRUM_RPC: string; - readonly VITE_OPTIMISM_RPC: string; - readonly VITE_POLYGON_RPC: string; - readonly VITE_SCROLL_RPC: string; - readonly VITE_AVALANCHE_RPC: string; - readonly VITE_SOPHON_RPC: string; - readonly VITE_KAIA_RPC: string; - readonly VITE_MONAD_RPC: string; - readonly VITE_MEGAETH_RPC: string; - readonly VITE_BASE: string; - } - - interface ImportMeta { - readonly env: ImportMetaEnv; - } + interface ImportMeta { + readonly env: ImportMetaEnv; + } } diff --git a/apps/citrea/tsconfig.app.json b/apps/citrea/tsconfig.app.json index afe17f6..c6a071f 100644 --- a/apps/citrea/tsconfig.app.json +++ b/apps/citrea/tsconfig.app.json @@ -24,7 +24,8 @@ "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": ["../../packages/fast-bridge-app/src/*"], + "@fastbridge/runtime": ["./src/runtime.ts"] } }, "include": ["src"] diff --git a/apps/citrea/tsconfig.json b/apps/citrea/tsconfig.json index fec8c8e..5bfdf37 100644 --- a/apps/citrea/tsconfig.json +++ b/apps/citrea/tsconfig.json @@ -8,6 +8,7 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"] - } + }, + "strictNullChecks": true } } diff --git a/apps/citrea/vite.config.ts b/apps/citrea/vite.config.ts index 3ee235e..d3e526f 100644 --- a/apps/citrea/vite.config.ts +++ b/apps/citrea/vite.config.ts @@ -1,12 +1,12 @@ +import { writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import tailwindcss from "@tailwindcss/vite"; -import { nodePolyfills } from "vite-plugin-node-polyfills"; -import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; -import { writeFileSync } from "node:fs"; +import { defineConfig, loadEnv } from "vite"; +import { nodePolyfills } from "vite-plugin-node-polyfills"; +import { getConfig } from "./get-config"; import type { AppEnv } from "./src/vite-env.d"; -import { getConfig } from "./getConfig"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -18,31 +18,31 @@ export default defineConfig(({ mode }) => { const config = getConfig(env); const manifestContent = { - "name": config.meta.title, - "short_name": config.appTitle, - "description": config.meta.description, - "start_url": base, - "display": "standalone", - "background_color": config.meta.backgroundColor, - "theme_color": config.meta.themeColor, - "orientation": "portrait-primary", - "icons": [ + name: config.meta.title, + short_name: config.appTitle, + description: config.meta.description, + start_url: base, + display: "standalone", + background_color: config.meta.backgroundColor, + theme_color: config.meta.themeColor, + orientation: "portrait-primary", + icons: [ { - "src": config.meta.faviconUrl, - "sizes": "31x32", - "type": "image/png", - "purpose": "any" + src: config.meta.faviconUrl, + sizes: "31x32", + type: "image/png", + purpose: "any", }, { - "src": config.meta.imageUrl, - "sizes": "2444x1256", - "type": "image/png", - "purpose": "any" - } - ] + src: config.meta.imageUrl, + sizes: "2444x1256", + type: "image/png", + purpose: "any", + }, + ], }; - const outputPath = path.resolve(__dirname, 'dist', 'manifest.json'); + const outputPath = path.resolve(__dirname, "dist", "manifest.json"); return { base, @@ -55,19 +55,19 @@ export default defineConfig(({ mode }) => { }), // Create manifest.json at build { - name: 'generate-manifest', + name: "generate-manifest", writeBundle() { try { writeFileSync(outputPath, JSON.stringify(manifestContent, null, 2)); console.log(`Generated dynamic manifest.json at ${outputPath}`); } catch (err) { - console.error('Error generating manifest.json', err); + console.error("Error generating manifest.json", err); } - } + }, }, // Update title and meta of index.html at build { - name: 'html-transform', + name: "html-transform", transformIndexHtml(html) { // Replace placeholders with actual values return html @@ -77,13 +77,14 @@ export default defineConfig(({ mode }) => { .replaceAll(/%= APP_FAVICON_URL =%/g, config.meta.faviconUrl) .replaceAll(/%= APP_THEME_COLOR =%/g, config.meta.themeColor) .replaceAll(/%= APP_META_IMAGE_URL =%/g, config.meta.imageUrl) - .replaceAll(/%= MANIFEST_URL =%/g, `${base}manifest.json`) - } - } + .replaceAll(/%= MANIFEST_URL =%/g, `${base}manifest.json`); + }, + }, ], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + "@": path.resolve(__dirname, "../../packages/fast-bridge-app/src"), + "@fastbridge/runtime": path.resolve(__dirname, "./src/runtime.ts"), buffer: "vite-plugin-node-polyfills/shims/buffer", global: "vite-plugin-node-polyfills/shims/global", process: "vite-plugin-node-polyfills/shims/process", @@ -91,7 +92,7 @@ export default defineConfig(({ mode }) => { }, envPrefix: ["VITE_"], build: { - emptyOutDir: true - } - } + emptyOutDir: true, + }, + }; }); diff --git a/apps/megaeth/README.md b/apps/megaeth/README.md index 6438e99..cfdc4d4 100644 --- a/apps/megaeth/README.md +++ b/apps/megaeth/README.md @@ -1,13 +1,15 @@ -# Nexus fast bridge +# Chain App Wrapper -### Run locally +This app is a thin wrapper around shared code in `packages/fast-bridge-app`. -`npm run dev` to run locally. +For architecture, onboarding, and customization: +- `README.md` (repo root) +- `docs/architecture.md` +- `docs/adding-chains.md` +- `docs/customization.md` -### Build project - -`npm run build` to build the project. - -### Preview - -`npm run start` to preview and run +Primary chain-specific files in this app: +- `get-config.ts` +- `src/runtime.ts` +- `.env.megaeth` +- `public/*` assets diff --git a/apps/megaeth/config.ts b/apps/megaeth/config.ts index b1e41eb..392c7b1 100644 --- a/apps/megaeth/config.ts +++ b/apps/megaeth/config.ts @@ -1,4 +1,4 @@ -import { getConfig } from "./getConfig"; +import { getConfig } from "./get-config"; const config = getConfig(import.meta.env); diff --git a/apps/megaeth/eslint.config.js b/apps/megaeth/eslint.config.js index a6d1076..8401724 100644 --- a/apps/megaeth/eslint.config.js +++ b/apps/megaeth/eslint.config.js @@ -1,9 +1,9 @@ import js from "@eslint/js"; -import globals from "globals"; +import { defineConfig, globalIgnores } from "eslint/config"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; import tseslint from "typescript-eslint"; -import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ globalIgnores(["dist"]), @@ -18,6 +18,9 @@ export default defineConfig([ languageOptions: { ecmaVersion: 2020, globals: globals.browser, + parserOptions: { + tsconfigRootDir: import.meta.dirname, + }, }, }, ]); diff --git a/apps/megaeth/get-config.ts b/apps/megaeth/get-config.ts new file mode 100644 index 0000000..86a7ad6 --- /dev/null +++ b/apps/megaeth/get-config.ts @@ -0,0 +1,87 @@ +import type { AppEnv } from "./src/vite-env.d"; + +const withFallback = (value: T, fallbackValue: T): T => + value || fallbackValue; + +export function getConfig(env: AppEnv) { + return { + chainId: Number(withFallback(env.VITE_CONFIG_CHAIN_ID, 1337)), + chainName: withFallback(env.VITE_CONFIG_CHAIN_NAME, "MegaETH"), + chainNativeCurrency: { + name: withFallback(env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_NAME, "MegaETH"), + symbol: withFallback(env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_SYMBOL, "ETH"), + decimals: Number( + withFallback(env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_DECIMALS, 18) + ), + }, + chainRpcUrl: withFallback( + env.VITE_CONFIG_CHAIN_RPC_URL, + "https://rpcs.avail.so/megaeth" + ), + chainBlockExplorerUrl: withFallback( + env.VITE_CONFIG_CHAIN_BLOCK_EXPLORER_URL, + "https://explorer.megaeth.systems/" + ), + chainTestnet: env.VITE_CONFIG_CHAIN_TESTNET, + useChainLogo: env.VITE_CONFIG_CHAIN_USE_CHAIN_LOGO, + chainIconUrl: withFallback( + env.VITE_CONFIG_CHAIN_ICON_URL, + "/megaeth-favicon.svg" + ), + chainLogoUrl: withFallback( + env.VITE_CONFIG_CHAIN_LOGO_URL, + "/megaeth-logo.svg" + ), + chainGifUrl: withFallback(env.VITE_CONFIG_CHAIN_GIF_URL, "/megaeth.gif"), + chainGifAlt: withFallback(env.VITE_CONFIG_CHAIN_GIF_ALT, "Fast MegaETH"), + heroText: withFallback( + env.VITE_CONFIG_CHAIN_HERO_TEXT, + "Move your assets to MegaETH faster than ever!" + ), + + appTitle: withFallback(env.VITE_CONFIG_APP_TITLE, "MegaETH Fast Bridge"), + appDescription: withFallback( + env.VITE_CONFIG_APP_DESCRIPTION, + "MegaETH Fast Bridge" + ), + + primaryColor: withFallback(env.VITE_CONFIG_PRIMARY_COLOR, "#19191A"), + secondaryColor: withFallback(env.VITE_CONFIG_SECONDARY_COLOR, "#ECE8E8"), + nexusNetwork: withFallback(env.VITE_CONFIG_NEXUS_NETWORK, "mainnet"), + nexusSupportedChain: Number( + withFallback(env.VITE_CONFIG_NEXUS_SUPPORTED_CHAIN, 1337) + ), + nexusPrimaryToken: withFallback( + env.VITE_CONFIG_NEXUS_PRIMARY_TOKEN, + "USDC" + ), + + meta: { + title: withFallback( + env.VITE_CONFIG_APP_META_TITLE, + "MegaETH Fast Bridge - Powered by Avail Nexus" + ), + description: withFallback( + env.VITE_CONFIG_APP_META_DESCRIPTION, + "Move your unified USDC and USDT from 12 chains to MegaETH, faster than ever." + ), + canonicalUrl: withFallback( + env.VITE_CONFIG_APP_META_CANONICAL_URL, + "https://fastbridge.availproject.org/megaeth/" + ), + imageUrl: withFallback( + env.VITE_CONFIG_APP_META_IMAGE_URL, + "https://fastbridge.availproject.org/megaeth/megaeth-meta.png" + ), + faviconUrl: withFallback( + env.VITE_CONFIG_APP_META_FAVICON_URL, + "https://fastbridge.availproject.org/megaeth/faviconV2.png" + ), + themeColor: withFallback(env.VITE_CONFIG_APP_META_THEME_COLOR, "#19191A"), + backgroundColor: withFallback( + env.VITE_CONFIG_APP_META_BACKGROUND_COLOR, + "#ECE8E8" + ), + }, + }; +} diff --git a/apps/megaeth/getConfig.ts b/apps/megaeth/getConfig.ts deleted file mode 100644 index 709f7f6..0000000 --- a/apps/megaeth/getConfig.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { AppEnv } from "./src/vite-env.d"; - -export function getConfig(env: AppEnv) { - const config = { - chainId: Number(env.VITE_CONFIG_CHAIN_ID || 143), - chainName: env.VITE_CONFIG_CHAIN_NAME || "Monad", - chainNativeCurrency: { - name: env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_NAME || "Monad", - symbol: env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_SYMBOL || "MON", - decimals: Number(env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_DECIMALS || 18), - }, - chainRpcUrl: env.VITE_CONFIG_CHAIN_RPC_URL || "https://rpcs.avail.so/monad", - chainBlockExplorerUrl: - env.VITE_CONFIG_CHAIN_BLOCK_EXPLORER_URL || "https://monadvision.com/", - chainTestnet: env.VITE_CONFIG_CHAIN_TESTNET || false, - useChainLogo: env.VITE_CONFIG_CHAIN_USE_CHAIN_LOGO || false, - chainIconUrl: env.VITE_CONFIG_CHAIN_ICON_URL || "/Monad_Logomark.svg", - chainLogoUrl: env.VITE_CONFIG_CHAIN_LOGO_URL || "/Monad_Logo.svg", - chainGifUrl: env.VITE_CONFIG_CHAIN_GIF_URL || "/salmonad.gif", - chainGifAlt: env.VITE_CONFIG_CHAIN_GIF_ALT || "Fast Salmonad", - heroText: - env.VITE_CONFIG_CHAIN_HERO_TEXT || - "Move your assets to Monad faster than ever!", - - appTitle: env.VITE_CONFIG_APP_TITLE || "Monad Fast Bridge", - appDescription: env.VITE_CONFIG_APP_DESCRIPTION || "Monad Fast Bridge", - - primaryColor: env.VITE_CONFIG_PRIMARY_COLOR || "#19191A", - secondaryColor: env.VITE_CONFIG_SECONDARY_COLOR || "#ECE8E8", - nexusNetwork: env.VITE_CONFIG_NEXUS_NETWORK || "mainnet", - nexusSupportedChain: - Number(env.VITE_CONFIG_NEXUS_SUPPORTED_CHAIN) || 143, - nexusPrimaryToken: env.VITE_CONFIG_NEXUS_PRIMARY_TOKEN || "USDC", - - meta: { - title: - env.VITE_CONFIG_APP_META_TITLE || - "Monad Fast Bridge - Powered by Avail Nexus", - description: - env.VITE_CONFIG_APP_META_DESCRIPTION || - "Move your unified USDC and USDT from 12 chains to Monad, faster than ever.", - canonicalUrl: - env.VITE_CONFIG_APP_META_CANONICAL_URL || "https://monadfastbridge.com", - imageUrl: - env.VITE_CONFIG_APP_META_IMAGE_URL || - "https://monadfastbridge.com/MonadFBMeta.png", - faviconUrl: - env.VITE_CONFIG_APP_META_FAVICON_URL || - "https://monadfastbridge.com/faviconV2.png", - themeColor: env.VITE_CONFIG_APP_META_THEME_COLOR || "#19191A", - backgroundColor: env.VITE_CONFIG_APP_META_BACKGROUND_COLOR || "#ECE8E8", - }, - }; - - return config; -} \ No newline at end of file diff --git a/apps/megaeth/index.html b/apps/megaeth/index.html index 66cdf7a..87dc80c 100644 --- a/apps/megaeth/index.html +++ b/apps/megaeth/index.html @@ -1,77 +1,77 @@ - - + + - - - - - + + + + + %= APP_TITLE =% - - + + + > + > - - - - - - - - - + + + + + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - - + + + + + + - - + + + >
diff --git a/apps/megaeth/package.json b/apps/megaeth/package.json index f3961f1..6b87c63 100644 --- a/apps/megaeth/package.json +++ b/apps/megaeth/package.json @@ -11,8 +11,9 @@ "preview": "vite preview --port 5173" }, "dependencies": { - "@avail-project/nexus-core": "github:availproject/nexus-sdk#cca99c1a570a8598334e3e89453d39a27d70ede7", + "@avail-project/nexus-core": "1.1.2", "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", diff --git a/apps/megaeth/src/App.tsx b/apps/megaeth/src/App.tsx deleted file mode 100644 index 6e18e78..0000000 --- a/apps/megaeth/src/App.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import Web3Provider from "@/providers/web3Provider"; -import { Toaster } from "@/components/ui/sonner"; -import Navbar from "@/components/navbar"; -import HeroSection from "@/components/hero-section"; -import FastBridgeShowcase from "@/components/fast-bridge-showcase"; -import NexusProvider from "@/components/nexus/NexusProvider"; -import config from "../config"; - -// @ts-expect-error - Environment is not exported from @avail-project/nexus-core -enum Environment { - FOLLY, // Dev with test-net tokens - CERISE, // Dev with main-net tokens - CORAL, // Test-net with main-net tokens - JADE, // Main-net with main-net tokens -} - -export default function App() { - return ( - - -
- -
-
-
- - -
-
-
- - - - ); -} diff --git a/apps/megaeth/src/components/common/hooks/useDebouncedCallback.ts b/apps/megaeth/src/components/common/hooks/useDebouncedCallback.ts deleted file mode 100644 index d969b5a..0000000 --- a/apps/megaeth/src/components/common/hooks/useDebouncedCallback.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useEffect, useMemo, useRef } from "react"; -import { useStableCallback } from "./useStableCallback"; - -type AnyFn = (...args: any[]) => any; - -export interface Debounced { - (...args: Parameters): void; - cancel: () => void; - flush: () => void; -} - -/** - * Returns a debounced function that delays invoking `fn` until after `delay` - * milliseconds have elapsed since the last call. - */ -export function useDebouncedCallback( - fn: T, - delay: number -): Debounced { - const latest = useStableCallback(fn); - const timerRef = useRef(null); - const lastArgsRef = useRef | null>(null); - - const cancel = () => { - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = null; - } - }; - - const flush = () => { - if (timerRef.current && lastArgsRef.current) { - clearTimeout(timerRef.current); - timerRef.current = null; - - latest(...lastArgsRef.current); - lastArgsRef.current = null; - } - }; - - // cancel when delay changes/unmounts - useEffect(() => cancel, [delay]); - - return useMemo(() => { - const debounced = ((...args: Parameters) => { - lastArgsRef.current = args; - cancel(); - timerRef.current = setTimeout(() => { - - latest(...lastArgsRef.current!); - lastArgsRef.current = null; - timerRef.current = null; - }, delay); - }) as Debounced; - debounced.cancel = cancel; - debounced.flush = flush; - return debounced; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [delay, latest]); -} diff --git a/apps/megaeth/src/components/common/hooks/useDebouncedValue.ts b/apps/megaeth/src/components/common/hooks/useDebouncedValue.ts deleted file mode 100644 index 3df2f0b..0000000 --- a/apps/megaeth/src/components/common/hooks/useDebouncedValue.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useEffect, useState } from "react"; -import { useDebouncedCallback } from "./useDebouncedCallback"; - -/** - * Derives a debounced value from an input value and delay. - */ -export function useDebouncedValue(value: T, delay: number): T { - const [debounced, setDebounced] = useState(value); - const setter = useDebouncedCallback((v: T) => setDebounced(v), delay); - - useEffect(() => { - setter(value); - return setter.cancel; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, delay]); - - return debounced; -} diff --git a/apps/megaeth/src/components/common/hooks/useInterval.ts b/apps/megaeth/src/components/common/hooks/useInterval.ts deleted file mode 100644 index 2287075..0000000 --- a/apps/megaeth/src/components/common/hooks/useInterval.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useRef } from "react"; -import { useStableCallback } from "./useStableCallback"; - -interface UseIntervalOptions { - enabled?: boolean; - immediate?: boolean; -} - -/** - * Declarative setInterval with pause/resume and latest-callback semantics. - * Pass delay=null to pause. - */ -export function useInterval( - callback: () => void, - delay: number | null, - options: UseIntervalOptions = {} -) { - const { enabled = true, immediate = false } = options; - const savedCallback = useStableCallback(callback); - const intervalRef = useRef(null); - - useEffect(() => { - if (!enabled || delay == null) return; - if (immediate) { - savedCallback(); - } - intervalRef.current = setInterval(savedCallback, delay); - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - }; - }, [delay, enabled, immediate, savedCallback]); -} diff --git a/apps/megaeth/src/components/common/hooks/useNexusError.ts b/apps/megaeth/src/components/common/hooks/useNexusError.ts deleted file mode 100644 index 3276d17..0000000 --- a/apps/megaeth/src/components/common/hooks/useNexusError.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NexusError } from "@avail-project/nexus-core"; - -function handler(err: unknown) { - console.log("NEXUS-ERROR-OBJECT", err) - if (err instanceof NexusError) { - return { - code: err?.code, - message: err?.message, - context: err?.data?.context, - details: err?.data?.details, - }; - } else { - console.error("Unexpected error:", err); - return { - code: "unexpected_error", - message: "Oops! Something went wrong. Please try again.", - context: undefined, - details: undefined, - }; - } -} -export function useNexusError() { - return handler; -} diff --git a/apps/megaeth/src/components/common/hooks/usePolling.ts b/apps/megaeth/src/components/common/hooks/usePolling.ts deleted file mode 100644 index 8b35ed8..0000000 --- a/apps/megaeth/src/components/common/hooks/usePolling.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useRef } from "react"; -import { useInterval } from "./useInterval"; -import { useStableCallback } from "./useStableCallback"; - -/** - * Declarative polling with in-flight protection (no overlap). - * When enabled becomes true, an immediate run is executed, - * followed by interval-based runs. - */ -export function usePolling( - enabled: boolean, - fn: () => Promise | void, - intervalMs: number -) { - const inFlightRef = useRef(false); - const wrapped = useStableCallback(async () => { - if (inFlightRef.current) return; - try { - inFlightRef.current = true; - await fn(); - } catch (error) { - console.error(error); - } finally { - inFlightRef.current = false; - } - }); - - useInterval(wrapped, enabled ? intervalMs : null, { - enabled, - immediate: enabled, - }); -} diff --git a/apps/megaeth/src/components/common/hooks/useStopwatch.ts b/apps/megaeth/src/components/common/hooks/useStopwatch.ts deleted file mode 100644 index 9350047..0000000 --- a/apps/megaeth/src/components/common/hooks/useStopwatch.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useRef, useState } from "react"; - -interface UseStopwatchOptions { - running?: boolean; - intervalMs?: number; -} - -/** - * Simple stopwatch that increments elapsed seconds while running. - * Designed to replace scattered timer effects. - */ -export function useStopwatch(options: UseStopwatchOptions = {}) { - const { running = false, intervalMs = 100 } = options; - const [elapsedSeconds, setElapsedSeconds] = useState(0); - const timerRef = useRef | null>(null); - - const reset = () => { - setElapsedSeconds(0); - }; - - const stop = () => { - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } - }; - - const start = () => { - if (timerRef.current) return; - timerRef.current = setInterval(() => { - // 1s == 1000ms; we add fractional seconds per tick - setElapsedSeconds((prev) => prev + intervalMs / 1000); - }, intervalMs); - }; - - useEffect(() => { - if (running) { - start(); - } else { - stop(); - } - return stop; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [running, intervalMs]); - - return { - seconds: elapsedSeconds, - start, - stop, - reset, - running: Boolean(timerRef.current), - }; -} diff --git a/apps/megaeth/src/components/common/index.ts b/apps/megaeth/src/components/common/index.ts deleted file mode 100644 index f5a855e..0000000 --- a/apps/megaeth/src/components/common/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./hooks/useStopwatch"; -export * from "./hooks/usePolling"; -export * from "./hooks/useInterval"; -export * from "./hooks/useStableCallback"; -export * from "./hooks/useDebouncedValue"; -export * from "./hooks/useDebouncedCallback"; -export * from "./hooks/useNexusError"; -export * from "./tx/types"; -export * from "./tx/steps"; -export * from "./tx/useTransactionSteps"; -export * from "./utils/constant"; diff --git a/apps/megaeth/src/components/common/tx/steps.ts b/apps/megaeth/src/components/common/tx/steps.ts deleted file mode 100644 index 9d44a88..0000000 --- a/apps/megaeth/src/components/common/tx/steps.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { SwapStepType } from "@avail-project/nexus-core"; -import type { GenericStep } from "./types"; -import { getStepKey } from "./types"; - -/** - * Predefined expected steps for swaps to seed UI before events arrive. - * Kept here to avoid duplication across exact-in and exact-out hooks. - */ -export const SWAP_EXPECTED_STEPS: SwapStepType[] = [ - { type: "SWAP_START", typeID: "SWAP_START" } as SwapStepType, - { type: "DETERMINING_SWAP", typeID: "DETERMINING_SWAP" } as SwapStepType, - { - type: "CREATE_PERMIT_FOR_SOURCE_SWAP", - typeID: - "CREATE_PERMIT_FOR_SOURCE_SWAP" as unknown as SwapStepType["typeID"], - } as SwapStepType, - { - type: "SOURCE_SWAP_BATCH_TX", - typeID: "SOURCE_SWAP_BATCH_TX", - } as SwapStepType, - { - type: "SOURCE_SWAP_HASH", - typeID: "SOURCE_SWAP_HASH" as unknown as SwapStepType["typeID"], - } as SwapStepType, - { type: "RFF_ID", typeID: "RFF_ID" } as SwapStepType, - { - type: "DESTINATION_SWAP_BATCH_TX", - typeID: "DESTINATION_SWAP_BATCH_TX", - } as SwapStepType, - { - type: "DESTINATION_SWAP_HASH", - typeID: "DESTINATION_SWAP_HASH" as unknown as SwapStepType["typeID"], - } as SwapStepType, - { type: "SWAP_COMPLETE", typeID: "SWAP_COMPLETE" } as SwapStepType, -]; - -export function seedSteps(expected: T[]): Array> { - return expected.map((st, index) => ({ - id: index, - completed: false, - step: st, - })); -} - -export function computeAllCompleted(steps: Array>): boolean { - return steps.length > 0 && steps.every((s) => s.completed); -} - -/** - * Replace the current list of steps with a new list, preserving completion - * for any steps that were already marked completed (matched by key). - */ -export function mergeStepsList( - prev: Array>, - list: T[] -): Array> { - const completedKeys = new Set(); - for (const prevStep of prev) { - if (prevStep.completed) { - completedKeys.add(getStepKey(prevStep.step)); - } - } - const next: Array> = []; - for (let index = 0; index < list.length; index++) { - const step = list[index]; - const key = getStepKey(step); - next.push({ - id: index, - completed: completedKeys.has(key), - step, - }); - } - return next; -} - -/** - * Mark a step complete in-place; if the step doesn't yet exist, append it. - */ -export function mergeStepComplete( - prev: Array>, - step: T -): Array> { - const key = getStepKey(step); - const updated: Array> = []; - let found = false; - for (const s of prev) { - if (getStepKey(s.step) === key) { - updated.push({ ...s, completed: true, step: { ...s.step, ...step } }); - found = true; - } else { - updated.push(s); - } - } - if (!found) { - updated.push({ - id: updated.length, - completed: true, - step, - }); - } - return updated; -} diff --git a/apps/megaeth/src/components/common/tx/types.ts b/apps/megaeth/src/components/common/tx/types.ts deleted file mode 100644 index 7a2a7b1..0000000 --- a/apps/megaeth/src/components/common/tx/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -export type TransactionStatus = - | "idle" - | "preview" - | "awaiting-approval" - | "executing" - | "success" - | "error"; - -export type GenericStep = { - id: number; - completed: boolean; - step: TStep; -}; - -/** - * Normalizes a step to a stable key. Prefers typeID, then type, otherwise JSON. - */ -export function getStepKey(step: any): string { - if (!step) return ""; - if (typeof step.typeID === "string" && step.typeID.length > 0) { - return step.typeID; - } - if (typeof step.type === "string" && step.type.length > 0) { - return step.type; - } - try { - return JSON.stringify(step); - } catch { - return String(step); - } -} diff --git a/apps/megaeth/src/components/common/tx/useTransactionSteps.ts b/apps/megaeth/src/components/common/tx/useTransactionSteps.ts deleted file mode 100644 index eb74bd6..0000000 --- a/apps/megaeth/src/components/common/tx/useTransactionSteps.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useMemo, useRef, useState } from "react"; -import type { GenericStep } from "./types"; -import { getStepKey } from "./types"; -import { - computeAllCompleted, - mergeStepComplete, - mergeStepsList, - seedSteps, -} from "./steps"; - -interface UseTransactionStepsOptions { - expected?: T[]; -} - -/** - * Manages transaction steps with utilities to seed from expected steps, - * replace the list on "steps list" events, and mark individual steps complete. - */ -export function useTransactionSteps< - T extends { typeID?: string; type?: string } ->(options: UseTransactionStepsOptions = {}) { - const { expected } = options; - const [steps, setSteps] = useState>>(() => - expected ? seedSteps(expected) : [] - ); - const lastSignatureRef = useRef(""); - - const onStepsList = (list: T[]) => { - const signature = list.map((step) => getStepKey(step)).join("|"); - if (lastSignatureRef.current === signature) { - setSteps((prev) => mergeStepsList(prev, list)); - return; - } - lastSignatureRef.current = signature; - setSteps((prev) => mergeStepsList(prev, list)); - }; - - const onStepComplete = (step: T) => { - setSteps((prev) => mergeStepComplete(prev, step)); - }; - - const seed = (expectedSteps: T[]) => { - setSteps(seedSteps(expectedSteps)); - }; - - const reset = () => { - setSteps(expected ? seedSteps(expected) : []); - lastSignatureRef.current = ""; - }; - - const allCompleted = useMemo(() => computeAllCompleted(steps), [steps]); - - return { - steps, - allCompleted, - onStepsList, - onStepComplete, - seed, - reset, - }; -} diff --git a/apps/megaeth/src/components/common/utils/constant.ts b/apps/megaeth/src/components/common/utils/constant.ts deleted file mode 100644 index 8bc57a9..0000000 --- a/apps/megaeth/src/components/common/utils/constant.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { SUPPORTED_CHAINS } from "@avail-project/nexus-core"; -import { formatUnits, parseUnits } from "viem"; - -export const SHORT_CHAIN_NAME: Record = { - [SUPPORTED_CHAINS.ETHEREUM]: "Ethereum", - [SUPPORTED_CHAINS.BASE]: "Base", - [SUPPORTED_CHAINS.ARBITRUM]: "Arbitrum", - [SUPPORTED_CHAINS.OPTIMISM]: "Optimism", - [SUPPORTED_CHAINS.POLYGON]: "Polygon", - [SUPPORTED_CHAINS.AVALANCHE]: "Avalanche", - [SUPPORTED_CHAINS.SCROLL]: "Scroll", - [SUPPORTED_CHAINS.KAIA]: "Kaia", - [SUPPORTED_CHAINS.BNB]: "BNB", - [SUPPORTED_CHAINS.MONAD]: "Monad", - [SUPPORTED_CHAINS.HYPEREVM]: "HyperEVM", - [SUPPORTED_CHAINS.MEGAETH]: "MegaETH", - 4114: "Citrea", - - [SUPPORTED_CHAINS.SEPOLIA]: "Sepolia", - [SUPPORTED_CHAINS.BASE_SEPOLIA]: "Base Sepolia", - [SUPPORTED_CHAINS.ARBITRUM_SEPOLIA]: "Arbitrum Sepolia", - [SUPPORTED_CHAINS.OPTIMISM_SEPOLIA]: "Optimism Sepolia", - [SUPPORTED_CHAINS.POLYGON_AMOY]: "Polygon Amoy", - [SUPPORTED_CHAINS.MONAD_TESTNET]: "Monad Testnet", -} as const; - -export const TOKEN_IMAGES: Record = { - USDC: "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png", - USDT: "https://coin-images.coingecko.com/coins/images/35023/large/USDT.png", - "USD₮0": - "https://coin-images.coingecko.com/coins/images/35023/large/USDT.png", - USDM: "https://assets.coingecko.com/coins/images/31719/large/usdm.png", - WETH: "https://assets.coingecko.com/coins/images/279/large/ethereum.png?1595348880", - USDS: "https://assets.coingecko.com/coins/images/39926/standard/usds.webp?1726666683", - SOPH: "https://assets.coingecko.com/coins/images/38680/large/sophon_logo_200.png", - KAIA: "https://assets.coingecko.com/asset_platforms/images/9672/large/kaia.png", - BNB: "https://assets.coingecko.com/coins/images/825/large/bnb-icon2_2x.png", - // Add ETH as fallback for any ETH-related tokens - ETH: "https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628", - // Add common token fallbacks - POL: "https://coin-images.coingecko.com/coins/images/32440/standard/polygon.png", - AVAX: "https://assets.coingecko.com/coins/images/12559/standard/Avalanche_Circle_RedWhite_Trans.png", - FUEL: "https://coin-images.coingecko.com/coins/images/279/large/ethereum.png", - HYPE: "https://assets.coingecko.com/asset_platforms/images/243/large/hyperliquid.png", - // Popular swap tokens - DAI: "https://coin-images.coingecko.com/coins/images/9956/large/Badge_Dai.png?1696509996", - UNI: "https://coin-images.coingecko.com/coins/images/12504/large/uni.jpg?1696512319", - AAVE: "https://coin-images.coingecko.com/coins/images/12645/large/AAVE.png?1696512452", - LDO: "https://coin-images.coingecko.com/coins/images/13573/large/Lido_DAO.png?1696513326", - PEPE: "https://coin-images.coingecko.com/coins/images/29850/large/pepe-token.jpeg?1696528776", - OP: "https://coin-images.coingecko.com/coins/images/25244/large/Optimism.png?1696524385", - ZRO: "https://coin-images.coingecko.com/coins/images/28206/large/ftxG9_TJ_400x400.jpeg?1696527208", - OM: "https://assets.coingecko.com/coins/images/12151/standard/OM_Token.png?1696511991", - KAITO: - "https://assets.coingecko.com/coins/images/54411/standard/Qm4DW488_400x400.jpg", -}; - -const DEFAULT_SAFETY_MARGIN = 0.01; // 1% - -/** - * Compute an amount string for fraction buttons (25%, 50%, 75%, 100%). - * - * @param balanceStr - user's balance as a human decimal string (e.g. "12.345") OR as base-unit integer string if `balanceIsBaseUnits` true - * @param fraction - fraction e.g. 0.25, 0.5, 0.75, 1 - * @param decimals - token decimals (6 for USDC/USDT, 18 for ETH) - * @param safetyMargin - 0.01 for 1% default - * @param balanceIsBaseUnits - if true, balanceStr is already base units integer string (wei / smallest unit) - * @returns decimal string clipped to token decimals (rounded down) - */ -export function computeAmountFromFraction( - balanceStr: string, - fraction: number, - decimals: number, - safetyMargin = DEFAULT_SAFETY_MARGIN, - balanceIsBaseUnits = false, -): string { - if (!balanceStr) return "0"; - - // parse balance into base units (BigInt) - const balanceUnits: bigint = balanceIsBaseUnits - ? BigInt(balanceStr) - : parseUnits(balanceStr, decimals); - - if (balanceUnits === BigInt(0)) return "0"; - - // Use an integer precision multiplier to avoid FP issues - const PREC = 1_000_000; // 1e6 precision for fraction & safety margin - const safetyMul = BigInt(Math.max(0, Math.floor((1 - safetyMargin) * PREC))); // (1 - safetyMargin) * PREC - const fractionMul = BigInt(Math.max(0, Math.floor(fraction * PREC))); // fraction * PREC - - // Apply safety margin: floor(balance * (1 - safetyMargin)) - const maxAfterSafety = (balanceUnits * safetyMul) / BigInt(PREC); - - // Apply fraction and floor: floor(maxAfterSafety * fraction) - let desiredUnits = (maxAfterSafety * fractionMul) / BigInt(PREC); - - // Extra clamp just in case - if (desiredUnits > balanceUnits) desiredUnits = balanceUnits; - if (desiredUnits < BigInt(0)) desiredUnits = BigInt(0); - - // format back to human readable decimal string with token decimals (formatUnits truncates/keeps decimals) - // formatUnits will produce exactly decimals digits if fractional part exists; we'll strip trailing zeros. - const raw = formatUnits(desiredUnits, decimals); - // strip trailing zeros and possible trailing dot - return raw - .replace(/(\.\d*?[1-9])0+$/u, "$1") - .replace(/\.0+$/u, "") - .replace(/^\.$/u, "0"); -} - -export const usdFormatter = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: 2, - maximumFractionDigits: 2, -}); diff --git a/apps/megaeth/src/components/countdown-timer.tsx b/apps/megaeth/src/components/countdown-timer.tsx deleted file mode 100644 index 5da085c..0000000 --- a/apps/megaeth/src/components/countdown-timer.tsx +++ /dev/null @@ -1,84 +0,0 @@ -"use client"; -import * as React from "react"; - -interface CountdownTimerProps { - targetDate: Date; -} - -export const CountdownTimer: React.FC = ({ targetDate }) => { - const [timeLeft, setTimeLeft] = React.useState({ - days: 0, - hours: 0, - minutes: 0, - seconds: 0, - }); - - React.useEffect(() => { - if (!targetDate || isNaN(targetDate.getTime())) { - console.error("Invalid target date provided to CountdownTimer"); - return; - } - - const calculateTimeLeft = () => { - const now = new Date().getTime(); - const target = targetDate.getTime(); - const difference = target - now; - - if (difference > 0) { - setTimeLeft({ - days: Math.floor(difference / (1000 * 60 * 60 * 24)), - hours: Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)), - minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)), - seconds: Math.floor((difference % (1000 * 60)) / 1000), - }); - } else { - setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0 }); - } - }; - - calculateTimeLeft(); - const interval = setInterval(calculateTimeLeft, 1000); - - return () => clearInterval(interval); - }, [targetDate]); - - return ( -
-
-
-
- {String(timeLeft.days).padStart(2, "0")} -
-
- Days -
-
-
-
- {String(timeLeft.hours).padStart(2, "0")} -
-
- Hours -
-
-
-
- {String(timeLeft.minutes).padStart(2, "0")} -
-
- Minutes -
-
-
-
- {String(timeLeft.seconds).padStart(2, "0")} -
-
- Seconds -
-
-
-
- ); -}; - diff --git a/apps/megaeth/src/components/fast-bridge-showcase.tsx b/apps/megaeth/src/components/fast-bridge-showcase.tsx deleted file mode 100644 index 0ec7ddd..0000000 --- a/apps/megaeth/src/components/fast-bridge-showcase.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; -import { useAccount } from "wagmi"; -import { ConnectKitButton } from "connectkit"; -import FastBridge from "./fast-bridge/fast-bridge"; -import { PreviewPanel } from "./walletConnect"; - -const FastBridgeShowcase = () => { - const { address, isConnected } = useAccount(); - - return ( - - - {({ show }) => ( - - )} - - - ); -}; - -export default FastBridgeShowcase; diff --git a/apps/megaeth/src/components/fast-bridge/components/allowance-modal.tsx b/apps/megaeth/src/components/fast-bridge/components/allowance-modal.tsx deleted file mode 100644 index 75e38cf..0000000 --- a/apps/megaeth/src/components/fast-bridge/components/allowance-modal.tsx +++ /dev/null @@ -1,345 +0,0 @@ -"use client"; -import React, { - type FC, - memo, - type RefObject, - useEffect, - useMemo, - useState, -} from "react"; -import { Button } from "../../ui/button"; -import { Input } from "../../ui/input"; -import { Label } from "../../ui/label"; -import { - type AllowanceHookSource, - type OnAllowanceHookData, - SUPPORTED_CHAINS, -} from "@avail-project/nexus-core"; -import { useNexus } from "../../nexus/NexusProvider"; - -interface AllowanceModalProps { - allowance: RefObject; - callback?: () => void; - onCloseCallback?: () => void; -} - -type AllowanceChoice = "min" | "max" | "custom"; - -interface AllowanceOptionProps { - index: number; - name: string; - choice: AllowanceChoice; - selectedChoice?: AllowanceChoice; - onSelect: (index: number, choice: AllowanceChoice) => void; - title: string; - description?: string; - children?: React.ReactNode; - allowanceValue?: string; -} - -const ALLOWANCE_CHOICES: Array<{ - choice: AllowanceChoice; - title: string; - description: string; -}> = [ - { - choice: "min", - title: "Minimum", - description: "Grant the lowest allowance required for this action.", - }, - { - choice: "max", - title: "Maximum", - description: "Approve once and skip future approvals for this token.", - }, - { - choice: "custom", - title: "Custom amount", - description: "Specify an allowance that fits your threshold.", - }, -]; - -const AllowanceOption: FC = ({ - index, - name, - choice, - selectedChoice, - onSelect, - title, - description, - children, - allowanceValue, -}) => { - const isActive = selectedChoice === choice; - - return ( - - ); -}; - -const AllowanceModal: FC = ({ - allowance, - callback, - onCloseCallback, -}) => { - const { nexusSDK } = useNexus(); - const [selectedOption, setSelectedOption] = useState([]); - const [customValues, setCustomValues] = useState([]); - - const { sources, allow, deny } = allowance.current ?? { - sources: [], - allow: () => {}, - deny: () => {}, - }; - - const defaultChoices = useMemo( - () => Array.from({ length: sources.length }, () => "min"), - [sources.length], - ); - - const isCustomValueValid = ( - value: string, - minimumRaw: bigint, - decimals: number, - ): boolean => { - if (!value || value.trim() === "") return false; - try { - const parsedValue = nexusSDK?.utils?.parseUnits(value, decimals); - if (parsedValue === undefined) return false; - return parsedValue >= minimumRaw; - } catch { - return false; - } - }; - - const hasValidationErrors = useMemo(() => { - return sources.some((source, index) => { - if (selectedOption[index] !== "custom") return false; - const value = customValues[index]; - if (!value || value.trim() === "") return false; - return !isCustomValueValid( - value, - source.allowance.minimumRaw, - source.token.decimals, - ); - }); - }, [sources, selectedOption, customValues]); - - const onClose = () => { - deny(); - allowance.current = null; - onCloseCallback?.(); - }; - - const onApprove = () => { - const processed = sources.map((_, i) => { - const opt = selectedOption[i]; - if (opt === "min" || opt === "max") return opt; - const rawValue = customValues[i]?.trim(); - if (!rawValue) return "min"; - const parsed = Number(rawValue); - if (!Number.isFinite(parsed) || parsed < 0) return "min"; - return rawValue; - }); - try { - allow(processed); - allowance.current = null; - callback?.(); - } catch (error) { - console.error("AllowanceModal onApprove error", error); - allowance.current = null; - onCloseCallback?.(); - } - }; - - const handleChoiceChange = (index: number, value: AllowanceChoice) => { - setSelectedOption((prev) => { - const next = [...(prev.length ? prev : defaultChoices)]; - next[index] = value; - return next; - }); - }; - - const formatAmount = (value: string | bigint, source: AllowanceHookSource) => - nexusSDK?.utils?.formatTokenBalance(value, { - symbol: source.token.symbol, - decimals: source.token.decimals, - }) ?? "—"; - - useEffect(() => { - setSelectedOption(defaultChoices); - }, [defaultChoices]); - - useEffect(() => { - setCustomValues(Array.from({ length: sources.length }, () => "")); - }, [sources.length]); - - return ( - <> -
-

- Set Token Allowances -

-

- Review every required token and choose the minimum, an unlimited max, - or define a custom amount before approving. -

-
- -
- {sources?.map((source: AllowanceHookSource, index: number) => ( -
-
-
-
- {source.chain.name} -
-
-

- {source.token.symbol} -

-

- {source.chain.name} -

-
-
- -
-

- Current allowance -

-

- {formatAmount(source.allowance.currentRaw, source)} -

-
-
- -
- {ALLOWANCE_CHOICES.map((choice) => { - if (choice.choice === "custom") { - const customValue = customValues[index] ?? ""; - const isCustomSelected = selectedOption[index] === "custom"; - const showError = - isCustomSelected && - customValue.trim() !== "" && - !isCustomValueValid( - customValue, - source.allowance.minimumRaw, - source.token.decimals, - ); - return ( - -
- { - const next = [...customValues]; - next[index] = e.target.value; - setCustomValues(next); - }} - maxLength={source.token.decimals} - className={`h-9 w-40 rounded-lg border bg-background/80 text-sm disabled:opacity-60 ${ - showError ? "border-destructive" : "" - }`} - disabled={!isCustomSelected} - /> - {showError && ( -

- Min: {source.allowance.minimum} -

- )} -
-
- ); - } - return ( - - ); - })} -
-
- ))} -
- -
- - -
- - ); -}; - -AllowanceModal.displayName = "AllowanceModal"; - -export default memo(AllowanceModal); diff --git a/apps/megaeth/src/components/fast-bridge/components/amount-input.tsx b/apps/megaeth/src/components/fast-bridge/components/amount-input.tsx deleted file mode 100644 index e0fac43..0000000 --- a/apps/megaeth/src/components/fast-bridge/components/amount-input.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { type FC, Fragment, useEffect, useRef, useState } from "react"; -import { Input } from "../../ui/input"; -import { Button } from "../../ui/button"; -import { SUPPORTED_CHAINS, type UserAsset } from "@avail-project/nexus-core"; -import { useNexus } from "../../nexus/NexusProvider"; -import { type FastBridgeState } from "../hooks/useBridge"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../../ui/accordion"; -import { SHORT_CHAIN_NAME } from "../../common"; -import { LoaderCircle } from "lucide-react"; - -interface AmountInputProps { - amount?: string; - onChange: (value: string) => void; - bridgableBalance?: UserAsset; - onCommit?: (value: string) => void; - disabled?: boolean; - inputs: FastBridgeState; - showBalanceDetails?: boolean; -} - -const AmountInput: FC = ({ - amount, - onChange, - bridgableBalance, - onCommit, - disabled, - inputs, - showBalanceDetails = true, -}) => { - const { nexusSDK, loading } = useNexus(); - const commitTimerRef = useRef(null); - const showBalanceDivider = showBalanceDetails && Boolean(bridgableBalance); - const [maxVal, setMaxVal] = useState("0"); - - const scheduleCommit = (val: string) => { - if (!onCommit || disabled) return; - if (commitTimerRef.current) clearTimeout(commitTimerRef.current); - commitTimerRef.current = setTimeout(() => { - onCommit(val); - }, 800); - }; - - const getMaxVal = async () => { - if (!showBalanceDetails || !nexusSDK || !inputs) return; - const maxBalAvailable = await nexusSDK?.calculateMaxForBridge({ - token: inputs?.token, - toChainId: inputs?.chain, - }); - if (!maxBalAvailable) return; - return maxBalAvailable.amount; - }; - - const onMaxClick = async () => { - if (!showBalanceDetails || !nexusSDK || !inputs) return; - const maxBalAvailable = await nexusSDK?.calculateMaxForBridge({ - token: inputs?.token, - toChainId: inputs?.chain, - recipient: inputs?.recipient, - }); - if (!maxBalAvailable) return; - onChange(maxBalAvailable.amount); - onCommit?.(maxBalAvailable.amount); - }; - - useEffect(() => { - const initMaxVal = async function () { - const v = await getMaxVal(); - console.log("got max val in useeffect", v); - if (v) setMaxVal(v); - }; - - initMaxVal(); - }, [nexusSDK, loading, inputs]); - - useEffect(() => { - return () => { - if (commitTimerRef.current) { - clearTimeout(commitTimerRef.current); - commitTimerRef.current = null; - } - }; - }, []); - - return ( -
-
- { - let next = e.target.value.replaceAll(/[^0-9.]/g, ""); - const parts = next.split("."); - if (parts.length > 2) - next = parts[0] + "." + parts.slice(1).join(""); - if (next === ".") next = "0."; - onChange(next); - scheduleCommit(next); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - if (commitTimerRef.current) { - clearTimeout(commitTimerRef.current); - commitTimerRef.current = null; - } - onCommit?.(amount ?? ""); - } - }} - className="w-full border-none bg-transparent rounded-r-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none py-0 px-3 h-12!" - aria-invalid={Boolean(amount) && Number.isNaN(Number(amount))} - disabled={disabled} - /> -
- {showBalanceDetails && bridgableBalance && ( -

- {nexusSDK?.utils?.formatTokenBalance(maxVal, { - symbol: - bridgableBalance?.displaySymbol ?? bridgableBalance?.symbol, - decimals: bridgableBalance?.decimals, - })} -

- )} - {showBalanceDetails && loading && !bridgableBalance && ( - - )} - {showBalanceDetails && ( - - )} -
-
- {showBalanceDetails && ( - - - - View Balance Breakdown - - -
- {bridgableBalance?.breakdown.map((chain) => { - if (Number.parseFloat(chain.balance) === 0) return null; - // if ( - // bridgableBalance.symbol === "USDC" && - // chain.chain.id === SUPPORTED_CHAINS.MEGAETH - // ) - // return null; - return ( - -
-
-
- {chain.chain.name} -
- - {SHORT_CHAIN_NAME[chain.chain.id]} - - {chain.chain.id === inputs.chain && ( -
- Destination -
- )} -
-

- {nexusSDK?.utils?.formatTokenBalance(chain.balance, { - symbol: chain.symbol, - decimals: bridgableBalance?.decimals, - })} -

-
-
- ); - })} -
-
-
-
- )} -
- ); -}; - -export default AmountInput; diff --git a/apps/megaeth/src/components/fast-bridge/components/fee-breakdown.tsx b/apps/megaeth/src/components/fast-bridge/components/fee-breakdown.tsx deleted file mode 100644 index 63b17f0..0000000 --- a/apps/megaeth/src/components/fast-bridge/components/fee-breakdown.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { type FC } from "react"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../../ui/accordion"; -import { - SUPPORTED_CHAINS, - type ReadableIntent, -} from "@avail-project/nexus-core"; -import { Skeleton } from "../../ui/skeleton"; -import { useNexus } from "../../nexus/NexusProvider"; -import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip"; -import { MessageCircleQuestion } from "lucide-react"; - -interface FeeBreakdownProps { - intent: ReadableIntent; - isLoading?: boolean; -} - -const FeeBreakdown: FC = ({ intent, isLoading = false }) => { - const { nexusSDK } = useNexus(); - const feeSymbol = - intent.token?.displaySymbol ?? intent.token?.symbol ?? "USDC"; - - const feeRows = [ - { - key: "caGas", - label: "Fast Bridge Gas Fees", - value: intent?.fees.caGas, - description: - "The gas fee required for executing the fast bridge transaction on the destination chain.", - }, - // { - // key: "gasSupplied", - // label: "Gas Supplied", - // value: intent?.fees?.gasSupplied, - // description: - // "The amount of gas tokens supplied to cover transaction costs on the destination chain.", - // }, - { - key: "solver", - label: "Solver Fees", - value: intent?.fees?.solver, - description: - "Fees paid to the solver that executes the bridge transaction and ensures fast completion.", - }, - { - key: "protocol", - label: "Protocol Fees", - value: intent?.fees?.protocol, - description: - "Fees collected by the protocol for maintaining and operating the bridge infrastructure.", - }, - ]; - - return ( - - -
-

Total fees

- -
- {isLoading ? ( - - ) : ( -
- { - // intent.destination.chainID === SUPPORTED_CHAINS.MEGAETH ? ( - //

0 Fees

- // ) : -

- {nexusSDK?.utils?.formatTokenBalance(intent.fees?.total, { - symbol: - intent.token?.symbol === "USDM" - ? "USDC" - : intent.token?.symbol, - decimals: intent?.token?.decimals, - })} -

- } -
- )} - -

View Breakup

-
-
-
- -
- {feeRows.map(({ key, label, value, description }) => { - // if (Number.parseFloat(value ?? "0") <= 0) return null; - return ( - -
-
-

{label}

- - - -
- {isLoading ? ( - - ) : ( -
- {intent.destination.chainID === - SUPPORTED_CHAINS.MEGAETH && key !== "caGas" ? ( -

- 0 Fees -

- ) : ( -

- {nexusSDK?.utils?.formatTokenBalance(value, { - symbol: - intent.token?.symbol === "USDM" - ? "USDC" - : intent.token?.symbol, - decimals: intent?.token?.decimals, - })} -

- )} -
- )} -
- -

{description}

-
-
- ); - })} -
-
-
-
- ); -}; - -export default FeeBreakdown; diff --git a/apps/megaeth/src/components/fast-bridge/components/receipient-address.tsx b/apps/megaeth/src/components/fast-bridge/components/receipient-address.tsx deleted file mode 100644 index 0bb4192..0000000 --- a/apps/megaeth/src/components/fast-bridge/components/receipient-address.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; -import * as React from "react"; -import { Input } from "../../ui/input"; -import { Check, Edit } from "lucide-react"; -import { Button } from "../../ui/button"; -import { useNexus } from "../../nexus/NexusProvider"; -import { type Address } from "viem"; - -interface ReceipientAddressProps { - address?: Address; - onChange: (address: string) => void; -} - -const ReceipientAddress: React.FC = ({ - address, - onChange, -}) => { - const { nexusSDK } = useNexus(); - const [isEditing, setIsEditing] = React.useState(false); - const fallbackTruncate = (value: string, head = 6, tail = 6) => { - if (!value) return ""; - if (value.length <= head + tail) return value; - return `${value.slice(0, head)}...${value.slice(-tail)}`; - }; - const displayAddress = - address && nexusSDK?.utils?.truncateAddress - ? nexusSDK.utils.truncateAddress(address, 6, 6) - : address - ? fallbackTruncate(address, 6, 6) - : ""; - return ( -
- {isEditing ? ( -
- onChange(e.target.value)} - className="w-full text-base font-medium" - /> - -
- ) : ( -
-

Recipient Address

-
-

{displayAddress}

- - -
-
- )} -
- ); -}; - -export default ReceipientAddress; diff --git a/apps/megaeth/src/components/fast-bridge/components/recipient-address.tsx b/apps/megaeth/src/components/fast-bridge/components/recipient-address.tsx deleted file mode 100644 index 5a7fa6f..0000000 --- a/apps/megaeth/src/components/fast-bridge/components/recipient-address.tsx +++ /dev/null @@ -1,79 +0,0 @@ -"use client"; -import { type FC, useState } from "react"; -import { Input } from "../../ui/input"; -import { Check, Edit } from "lucide-react"; -import { Button } from "../../ui/button"; -import { useNexus } from "../../nexus/NexusProvider"; -import { type Address } from "viem"; - -interface RecipientAddressProps { - address?: Address; - onChange: (address: string) => void; - disabled?: boolean; -} - -const RecipientAddress: FC = ({ - address, - onChange, - disabled, -}) => { - const { nexusSDK } = useNexus(); - const [isEditing, setIsEditing] = useState(false); - const fallbackTruncate = (value: string, head = 6, tail = 6) => { - if (!value) return ""; - if (value.length <= head + tail) return value; - return `${value.slice(0, head)}...${value.slice(-tail)}`; - }; - const displayAddress = - address && nexusSDK?.utils?.truncateAddress - ? nexusSDK.utils.truncateAddress(address, 6, 6) - : address - ? fallbackTruncate(address, 6, 6) - : ""; - return ( -
- {isEditing ? ( -
- onChange(e.target.value)} - className="w-full" - /> - -
- ) : ( -
-

Recipient Address

-
- {address && ( -

{displayAddress}

- )} - - -
-
- )} -
- ); -}; - -export default RecipientAddress; diff --git a/apps/megaeth/src/components/fast-bridge/components/source-breakdown.tsx b/apps/megaeth/src/components/fast-bridge/components/source-breakdown.tsx deleted file mode 100644 index 22eb114..0000000 --- a/apps/megaeth/src/components/fast-bridge/components/source-breakdown.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { - SUPPORTED_CHAINS, - type ReadableIntent, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../../ui/accordion"; -import { Skeleton } from "../../ui/skeleton"; -import { useNexus } from "../../nexus/NexusProvider"; - -interface SourceBreakdownProps { - intent?: ReadableIntent; - tokenSymbol: SUPPORTED_TOKENS; - isLoading?: boolean; -} - -const SourceBreakdown = ({ - intent, - tokenSymbol, - isLoading = false, -}: SourceBreakdownProps) => { - const { nexusSDK } = useNexus(); - // const spendSymbol = - // intent?.token.displaySymbol ?? intent?.token.symbol ?? tokenSymbol; - return ( - - -
- {isLoading ? ( - <> -
-

You Spend

- -
-
- -
- -
-
- - ) : ( - intent?.sources && ( - <> -
-

You Spend

-

- {`${intent.token.symbol.toUpperCase()} on ${ - intent?.sources?.length - } ${intent?.sources?.length > 1 ? "chains" : "chain"}`} -

-
- -
-

- {nexusSDK?.utils?.formatTokenBalance(intent?.sourcesTotal, { - symbol: - intent.token.symbol === "USDM" - ? "USDC" - : intent.token.symbol, - decimals: intent.token.decimals, - })} -

- -

View Sources

-
-
- - ) - )} -
- {!isLoading && intent?.sources && ( - -
- {intent?.sources?.map((source) => ( -
-
- {source?.chain.name} -

{source.chain.name}

-
- -

- {nexusSDK?.utils?.formatTokenBalance(source.amount, { - symbol: source.token.symbol, - decimals: source.token.decimals, - })} -

-
- ))} -
-
- )} -
-
- ); -}; - -export default SourceBreakdown; diff --git a/apps/megaeth/src/components/fast-bridge/components/token-select.tsx b/apps/megaeth/src/components/fast-bridge/components/token-select.tsx deleted file mode 100644 index 8679b53..0000000 --- a/apps/megaeth/src/components/fast-bridge/components/token-select.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { - SUPPORTED_CHAINS, - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "../../ui/select"; -import { Label } from "../../ui/label"; -import { useNexus } from "../../nexus/NexusProvider"; -import { useMemo } from "react"; - -interface TokenSelectProps { - selectedToken?: SUPPORTED_TOKENS; - selectedChain: SUPPORTED_CHAINS_IDS; - handleTokenSelect: (token: SUPPORTED_TOKENS) => void; - isTestnet?: boolean; - disabled?: boolean; - label?: string; -} - -const TokenSelect = ({ - selectedToken, - selectedChain, - handleTokenSelect, - isTestnet = false, - disabled = false, - label, -}: TokenSelectProps) => { - const { supportedChainsAndTokens } = useNexus(); - const tokenData = useMemo(() => { - return supportedChainsAndTokens - ?.filter((chain) => chain.id === selectedChain) - .flatMap((chain) => chain.tokens); - }, [selectedChain, supportedChainsAndTokens]); - - const selectedTokenData = tokenData?.find((token) => { - return token.symbol === selectedToken; - }); - - return ( - - ); -}; - -export default TokenSelect; diff --git a/apps/megaeth/src/components/fast-bridge/fast-bridge.tsx b/apps/megaeth/src/components/fast-bridge/fast-bridge.tsx deleted file mode 100644 index 24cd529..0000000 --- a/apps/megaeth/src/components/fast-bridge/fast-bridge.tsx +++ /dev/null @@ -1,526 +0,0 @@ -import { type FC, useEffect, useMemo, useRef, useState } from "react"; -import { Card, CardContent } from "../ui/card"; -import ChainSelect from "./components/chain-select"; -import TokenSelect from "./components/token-select"; -import { Button } from "../ui/button"; -import { X, CheckCircle2 } from "lucide-react"; -import { useNexus } from "../nexus/NexusProvider"; -import AmountInput from "./components/amount-input"; -import FeeBreakdown from "./components/fee-breakdown"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "../ui/dialog"; -import TransactionProgress from "./components/transaction-progress"; -import AllowanceModal from "./components/allowance-modal"; -import useBridge from "./hooks/useBridge"; -import SourceBreakdown from "./components/source-breakdown"; -import { - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; -import { type Address } from "viem"; -import { Skeleton } from "../ui/skeleton"; -import RecipientAddress from "./components/recipient-address"; -import ViewHistory from "../view-history/view-history"; -import { toast } from "sonner"; -import Decimal from "decimal.js"; -import { EncryptedText } from "../ui/encrypted-text"; -import FluffeyMascot from "./components/fluffey-mascot"; - -interface FastBridgeProps { - connectedAddress?: Address; - isWalletConnected?: boolean; - onConnectWallet?: () => void; - mockIntent?: { - totalAmount?: string; - receiveAmount?: string; - totalGas?: string; - }; - prefill?: { - token: SUPPORTED_TOKENS; - chainId: SUPPORTED_CHAINS_IDS; - amount?: string; - recipient?: Address; - }; - onComplete?: () => void; - onStart?: () => void; - onError?: (message: string) => void; -} - -const FastBridge: FC = ({ - connectedAddress, - isWalletConnected, - onConnectWallet, - mockIntent, - onComplete, - onStart, - onError, - prefill, -}) => { - const { - nexusSDK, - intent, - bridgableBalance, - allowance, - network, - fetchBridgableBalance, - } = useNexus(); - const [historyRefreshNonce, setHistoryRefreshNonce] = useState(0); - - const { - inputs, - setInputs, - timer, - loading, - refreshing, - isDialogOpen, - txError, - setTxError, - handleTransaction, - reset, - filteredBridgableBalance, - startTransaction, - setIsDialogOpen, - commitAmount, - lastExplorerUrl, - steps, - status, - areInputsValid, - } = useBridge({ - prefill, - network: network ?? "mainnet", - connectedAddress, - nexusSDK, - intent, - bridgableBalance, - allowance, - onComplete: async () => { - if (onComplete) { - onComplete(); - } - - const sourcesText = - intent.current?.intent?.sources?.length && - intent.current?.intent.sources.length > 0 - ? intent.current.intent.sources.map((s) => s.chainName).join(", ") - : "N/A"; - toast.success( -
-
- - Bridge Successful! -
-
-
- Source(s): {sourcesText} -
-
- Destination:{" "} - {intent.current?.intent?.destination?.chainName || "Unknown"} -
-
- Asset:{" "} - {intent.current?.intent?.token.symbol || "Unknown"} -
-
- Amount Spent:{" "} - {intent.current?.intent?.sourcesTotal - ? new Decimal(intent.current?.intent?.sourcesTotal).toFixed() - : "NaN"}{" "} - {intent.current?.intent?.token.symbol - ? intent.current?.intent?.token.symbol.toUpperCase() === "USDM" - ? "USDC" - : intent.current?.intent?.token.symbol - : "Unknown"} -
-
- Amount Received:{" "} - {intent.current?.intent?.destination?.amount - ? new Decimal( - intent.current?.intent?.destination?.amount, - ).toFixed() - : "NaN"}{" "} - {intent.current?.intent?.token.symbol || "Unknown"} -
-
- Total Fees:{" "} - {intent.current?.intent?.fees.total - ? new Decimal(intent.current?.intent?.fees.total).toFixed() - : "NaN"}{" "} - {intent.current?.intent?.token.symbol - ? intent.current?.intent?.token.symbol.toUpperCase() === "USDM" - ? "USDC" - : intent.current?.intent?.token.symbol - : "Unknown"} -
- {lastExplorerUrl ? ( - - ) : null} -
-
, - { - duration: Infinity, // Stay until dismissed - closeButton: true, - icon: null, // Remove default icon since we're adding our own - }, - ); - setHistoryRefreshNonce((prev) => prev + 1); - }, - onStart, - onError: (message) => { - toast.error(message); - if (onError) { - onError(message); - } - }, - fetchBalance: fetchBridgableBalance, - }); - const isConnected = isWalletConnected ?? Boolean(connectedAddress); - const isSdkReady = Boolean(nexusSDK); - const showSdkDetails = isSdkReady; - const receiveSymbol = - intent?.current?.intent?.token.symbol ?? - intent?.current?.intent?.token.displaySymbol ?? - filteredBridgableBalance?.symbol; - - const amountValue = useMemo(() => { - if (!inputs?.amount) return null; - const parsed = Number.parseFloat(inputs.amount); - return Number.isFinite(parsed) ? parsed : null; - }, [inputs?.amount]); - - const hasValidAmount = useMemo(() => { - if (amountValue === null) return false; - return amountValue > 0; - }, [amountValue]); - - const formatMockNumber = (value: number) => { - if (!Number.isFinite(value)) return "--"; - const fixed = value.toFixed(6); - return fixed.replace(/\.?0+$/, ""); - }; - - const formatWithToken = (value: string, token?: string) => { - if (!token) return value; - return `${value} ${token}`.trim(); - }; - - const tokenSuffix = - inputs?.token ?? filteredBridgableBalance?.symbol ?? "USDC"; - - const mockPreview = useMemo(() => { - if (!hasValidAmount || amountValue === null) return null; - if (mockIntent) { - return { - totalAmount: mockIntent.totalAmount ?? "--", - receiveAmount: mockIntent.receiveAmount ?? "--", - totalGas: mockIntent.totalGas ?? "--", - }; - } - const totalGas = amountValue * 0.001; - const totalAmount = amountValue + totalGas; - return { - totalAmount: formatWithToken(formatMockNumber(totalAmount), tokenSuffix), - receiveAmount: formatWithToken( - formatMockNumber(amountValue), - tokenSuffix, - ), - totalGas: formatWithToken(formatMockNumber(totalGas), tokenSuffix), - }; - }, [amountValue, hasValidAmount, mockIntent, tokenSuffix]); - - const showMockPreview = !isConnected && hasValidAmount && mockPreview; - const autoIntentTriggered = useRef(false); - - useEffect(() => { - autoIntentTriggered.current = false; - }, [inputs?.amount, inputs?.chain, inputs?.token, inputs?.recipient]); - - useEffect(() => { - if (!isConnected || !isSdkReady) return; - if (!areInputsValid) return; - if (intent.current) return; - // if (loading) return; // Removed to allow "10" fetch even if "1" is loading - if (autoIntentTriggered.current) return; - autoIntentTriggered.current = true; - void handleTransaction(); - }, [ - areInputsValid, - handleTransaction, - intent, - isConnected, - isSdkReady, - loading, - ]); - return ( -
- -
-
- -

- -
- -

-
-
- - - {showSdkDetails && ( - - )} - - setInputs({ - ...inputs, - chain, - }) - } - label="To" - disabled={!!prefill?.chainId} - /> - setInputs({ ...inputs, token })} - disabled={!!prefill?.token} - /> - setInputs({ ...inputs, amount })} - bridgableBalance={filteredBridgableBalance} - onCommit={() => void commitAmount()} - disabled={refreshing || !!prefill?.amount} - inputs={inputs} - showBalanceDetails={showSdkDetails} - /> - - setInputs({ ...inputs, recipient: address as `0x${string}` }) - } - disabled={!!prefill?.recipient} - /> - {showMockPreview && ( -
-
-

You spend

-

- {mockPreview?.totalAmount} -

-
-
-

You receive

-

- {mockPreview?.receiveAmount} -

-
-
-

Total gas

-

{mockPreview?.totalGas}

-
-
- )} - - {showSdkDetails && intent?.current?.intent && ( - <> - - -
-

You receive

-
- {refreshing ? ( - - ) : ( -

- {`${ - connectedAddress === inputs?.recipient - ? intent?.current?.intent?.destination?.amount - : inputs.amount - } ${receiveSymbol}`} -

- )} - {refreshing ? ( - - ) : ( -

- on {intent?.current?.intent?.destination?.chainName} -

- )} -
-
- - - )} - - {!intent.current && ( - - )} - - { - if (loading) return; - setIsDialogOpen(open); - }} - > - {intent.current && !isDialogOpen && ( -
- - - - -
- )} - - - - Transaction Progress - - {allowance.current ? ( - - ) : ( - - )} - -
- - {txError && ( -
- {txError} - -
- )} -
-
-
- ); -}; - -export default FastBridge; diff --git a/apps/megaeth/src/components/fast-bridge/hooks/useBridge.ts b/apps/megaeth/src/components/fast-bridge/hooks/useBridge.ts deleted file mode 100644 index cd48904..0000000 --- a/apps/megaeth/src/components/fast-bridge/hooks/useBridge.ts +++ /dev/null @@ -1,401 +0,0 @@ -import { - type BridgeStepType, - NEXUS_EVENTS, - type NexusNetwork, - NexusSDK, - type OnAllowanceHookData, - type OnIntentHookData, - // SUPPORTED_CHAINS, - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, - type UserAsset, -} from "@avail-project/nexus-core"; -import { - useEffect, - useMemo, - useRef, - useState, - useReducer, - type RefObject, -} from "react"; -import { type Address, isAddress } from "viem"; -import { - useStopwatch, - usePolling, - useNexusError, - useTransactionSteps, - type TransactionStatus, -} from "../../common"; -import config from "../../../../config"; -import { trackBridgeSubmit } from "../../../lib/posthog"; -import { SHORT_CHAIN_NAME } from "../../common/utils/constant"; - -export interface FastBridgeState { - chain: SUPPORTED_CHAINS_IDS; - token: SUPPORTED_TOKENS; - amount?: string; - recipient?: `0x${string}`; -} - -const ALLOWED_TOKENS = new Set([ - "USDC", - "USDT", - "USDM", -]) as Set; - -interface UseBridgeProps { - network: NexusNetwork; - connectedAddress?: Address; - nexusSDK: NexusSDK | null; - intent: RefObject; - allowance: RefObject; - bridgableBalance: UserAsset[] | null; - prefill?: { - token: string; - chainId: number; - amount?: string; - recipient?: Address; - }; - onComplete?: () => void; - onStart?: () => void; - onError?: (message: string) => void; - fetchBalance: () => Promise; -} - -type BridgeState = { - inputs: FastBridgeState; - status: TransactionStatus; -}; - -type Action = - | { type: "setInputs"; payload: Partial } - | { type: "resetInputs" } - | { type: "setStatus"; payload: TransactionStatus }; - -const buildInitialInputs = ( - connectedAddress?: Address, - prefill?: { - token: string; - chainId: number; - amount?: string; - recipient?: Address; - }, -): FastBridgeState => { - const validToken = - prefill?.token && - ALLOWED_TOKENS.has(prefill.token.toUpperCase() as SUPPORTED_TOKENS) - ? (prefill.token.toUpperCase() as SUPPORTED_TOKENS) - : config.nexusPrimaryToken || "USDC"; - - const validAmount = prefill?.amount - ? (() => { - const sanitized = prefill.amount.trim(); - if (!sanitized || sanitized === "." || !/^\d*\.?\d*$/.test(sanitized)) - return undefined; - const num = Number.parseFloat(sanitized); - return Number.isNaN(num) || num <= 0 || num > 1e9 - ? undefined - : sanitized; - })() - : undefined; - - const validRecipient = - prefill?.recipient && isAddress(prefill.recipient) - ? (prefill.recipient as `0x${string}`) - : connectedAddress; - - return { - chain: config.chainId as SUPPORTED_CHAINS_IDS, - token: validToken as SUPPORTED_TOKENS, - amount: validAmount, - recipient: validRecipient, - }; -}; - -const useBridge = ({ - connectedAddress, - nexusSDK, - intent, - bridgableBalance, - prefill, - onComplete, - onStart, - onError, - fetchBalance, - allowance, -}: UseBridgeProps) => { - const handleNexusError = useNexusError(); - const initialState: BridgeState = { - inputs: buildInitialInputs(connectedAddress, prefill), - status: "idle", - }; - function reducer(state: BridgeState, action: Action): BridgeState { - switch (action.type) { - case "setInputs": - return { ...state, inputs: { ...state.inputs, ...action.payload } }; - case "resetInputs": - return { - ...state, - inputs: buildInitialInputs(connectedAddress, prefill), - }; - case "setStatus": - return { ...state, status: action.payload }; - default: - return state; - } - } - const [state, dispatch] = useReducer(reducer, initialState); - const inputs = state.inputs; - const setInputs = (next: FastBridgeState | Partial) => { - dispatch({ type: "setInputs", payload: next as Partial }); - }; - - const loading = state.status === "executing"; - const [refreshing, setRefreshing] = useState(false); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [txError, setTxError] = useState(null); - const [lastExplorerUrl, setLastExplorerUrl] = useState(""); - const commitLockRef = useRef(false); - const txnIdRef = useRef(0); - const { - steps, - onStepsList, - onStepComplete, - reset: resetSteps, - } = useTransactionSteps(); - - const areInputsValid = useMemo(() => { - const hasToken = inputs?.token !== undefined && inputs?.token !== null; - const hasChain = inputs?.chain !== undefined && inputs?.chain !== null; - const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0; - const hasValidrecipient = - Boolean(inputs?.recipient) && isAddress(inputs?.recipient as string); - return hasToken && hasChain && hasAmount && hasValidrecipient; - }, [inputs]); - - const resetIntent = () => { - intent.current = null; - allowance.current = null; - }; - - const handleTransaction = async () => { - const currentTxnId = ++txnIdRef.current; - if (!inputs.amount) { - setTxError("Amount is required"); - return; - } - if (Number(inputs.amount) === 0) { - setTxError("Amount should be greater than 0"); - return; - } - if ( - !inputs?.amount || - !inputs?.recipient || - !inputs?.chain || - !inputs?.token - ) { - console.error("Missing required inputs"); - return; - } - - if (Number(inputs.amount) > 5000) { - setTxError("Amount exceeds maximum limit of 5000"); - return; - } - dispatch({ type: "setStatus", payload: "executing" }); - setTxError(null); - onStart?.(); - - // Track bridge submit event with PostHog - trackBridgeSubmit({ - chain: inputs.chain, - chainName: SHORT_CHAIN_NAME[inputs.chain] || `Chain ${inputs.chain}`, - tokenSymbol: inputs.token, - amount: inputs.amount, - fast_bridge: "megaeth", - }); - - try { - if (!nexusSDK) { - throw new Error("Nexus SDK not initialized"); - } - const formattedAmount = nexusSDK.convertTokenReadableAmountToBigInt( - inputs?.amount, - inputs?.token, - inputs?.chain, - ); - setLastExplorerUrl(""); - const bridgeTxn = await nexusSDK.bridge( - { - token: inputs?.token, - amount: formattedAmount, - toChainId: inputs?.chain, - recipient: inputs?.recipient, - }, - { - onEvent: (event) => { - if (currentTxnId !== txnIdRef.current) return; - if (event.name === NEXUS_EVENTS.STEPS_LIST) { - const list = Array.isArray(event.args) ? event.args : []; - onStepsList(list); - } - if (event.name === NEXUS_EVENTS.STEP_COMPLETE) { - console.log("STEP_EVENT", event); - if (event.args.type === "INTENT_HASH_SIGNED") { - stopwatch.start(); - } - onStepComplete(event.args); - } - }, - }, - ); - if (currentTxnId !== txnIdRef.current) return; - - if (!bridgeTxn) { - throw new Error("Something went wrong, please try again"); - } - if (bridgeTxn) { - setLastExplorerUrl(bridgeTxn.explorerUrl); - await onSuccess(); - } - } catch (error) { - if (currentTxnId !== txnIdRef.current) return; - const { message } = handleNexusError(error); - // intent.current?.deny(); - intent.current = null; - allowance.current = null; - console.log("NEXUS-ERROR-MESSAGE", message) - setTxError(message); - onError?.(message); - setIsDialogOpen(false); - dispatch({ type: "setStatus", payload: "error" }); - } - }; - - const onSuccess = async () => { - // Close dialog and stop timer on success - stopwatch.stop(); - dispatch({ type: "setStatus", payload: "success" }); - onComplete?.(); - intent.current = null; - allowance.current = null; - dispatch({ type: "resetInputs" }); - setRefreshing(false); - await fetchBalance(); - }; - - const filteredBridgableBalance = useMemo(() => { - return bridgableBalance?.find((bal) => inputs.token === 'USDM' ? bal?.symbol === 'USDC' : bal?.symbol === inputs?.token); - }, [bridgableBalance, inputs?.token]); - - const refreshIntent = async () => { - setRefreshing(true); - try { - await intent.current?.refresh([]); - } catch (error) { - console.error("Transaction failed:", error); - } finally { - setRefreshing(false); - } - }; - - const reset = () => { - // intent.current?.deny(); - intent.current = null; - allowance.current = null; - dispatch({ type: "resetInputs" }); - dispatch({ type: "setStatus", payload: "idle" }); - setRefreshing(false); - stopwatch.stop(); - stopwatch.reset(); - resetSteps(); - }; - - const startTransaction = () => { - // Reset timer for a fresh run - intent.current?.allow(); - setIsDialogOpen(true); - setTxError(null); - }; - - const commitAmount = async () => { - if (commitLockRef.current) return; - if (loading || txError || !areInputsValid) return resetIntent(); - - // Validate amount before proceeding - if (inputs?.amount) { - const amountStr = inputs.amount.trim(); - if (!amountStr) return resetIntent(); - - const amount = Number.parseFloat(amountStr); - if (Number.isNaN(amount) || amount <= 0) return resetIntent(); - } - - commitLockRef.current = true; - try { - await handleTransaction(); - } finally { - commitLockRef.current = false; - } - }; - - usePolling(Boolean(intent.current) && !isDialogOpen, refreshIntent, 15000); - - const stopwatch = useStopwatch({ intervalMs: 100 }); - - useEffect(() => { - if (intent.current) { - // intent.current.deny(); - intent.current = null; - } - }, [inputs]); - - useEffect(() => { - if (!isDialogOpen) { - stopwatch.stop(); - stopwatch.reset(); - // Reset all transaction state when dialog closes - if (state.status === "success" || state.status === "error") { - resetSteps(); - setLastExplorerUrl(""); - dispatch({ type: "setStatus", payload: "idle" }); - } - } - }, [isDialogOpen, stopwatch, state.status]); - - useEffect(() => { - if (txError) { - setTxError(null); - } - }, [inputs]); - - useEffect(() => { - if (connectedAddress && !inputs?.recipient) { - setInputs({ recipient: connectedAddress as `0x${string}` }); - } - }, [connectedAddress, inputs?.recipient]); - - return { - inputs, - setInputs, - timer: stopwatch.seconds, - setIsDialogOpen, - setTxError, - loading, - refreshing, - isDialogOpen, - txError, - handleTransaction, - reset, - filteredBridgableBalance, - startTransaction, - commitAmount, - lastExplorerUrl, - steps, - status: state.status, - areInputsValid, - resetIntent - }; -}; - -export default useBridge; diff --git a/apps/megaeth/src/components/hero-section.tsx b/apps/megaeth/src/components/hero-section.tsx deleted file mode 100644 index 89ca4ff..0000000 --- a/apps/megaeth/src/components/hero-section.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function HeroSection() { - return ( -
- Bridge your unified USDC/USDT balances across 12+ chains, to Avalanche, instantly! -
- ); -} diff --git a/apps/megaeth/src/components/navbar.tsx b/apps/megaeth/src/components/navbar.tsx deleted file mode 100644 index 22253e1..0000000 --- a/apps/megaeth/src/components/navbar.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; -import { ConnectKitButton } from "connectkit"; -import config from "../../config"; -import AvailLogo from "/avail_logo.svg"; - -export default function Navbar() { - return ( - - ); -} diff --git a/apps/megaeth/src/components/nexus/NexusProvider.tsx b/apps/megaeth/src/components/nexus/NexusProvider.tsx deleted file mode 100644 index 8fde99e..0000000 --- a/apps/megaeth/src/components/nexus/NexusProvider.tsx +++ /dev/null @@ -1,328 +0,0 @@ -"use client"; -import { - type EthereumProvider, - type NexusNetwork, - NexusSDK, - type OnAllowanceHookData, - type OnIntentHookData, - type OnSwapIntentHookData, - type SupportedChainsAndTokensResult, - type SupportedChainsResult, - type UserAsset, -} from "@avail-project/nexus-core"; - -import { - createContext, - type RefObject, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { useAccountEffect } from "wagmi"; - -interface NexusContextType { - nexusSDK: NexusSDK | null; - bridgableBalance: UserAsset[] | null; - swapBalance: UserAsset[] | null; - intent: RefObject; - allowance: RefObject; - swapIntent: RefObject; - exchangeRate: Record | null; - supportedChainsAndTokens: SupportedChainsAndTokensResult | null; - swapSupportedChainsAndTokens: SupportedChainsResult | null; - network?: NexusNetwork; - loading: boolean; - handleInit: (provider: EthereumProvider) => Promise; - fetchBridgableBalance: () => Promise; - fetchSwapBalance: () => Promise; - getFiatValue: (amount: number, token: string) => number; - initializeNexus: (provider: EthereumProvider) => Promise; - deinitializeNexus: () => Promise; - attachEventHooks: () => void; - setIntent: (data: OnIntentHookData | null) => void; - setAllowance: (data: OnAllowanceHookData | null) => void; -} - -const NexusContext = createContext(undefined); - -type NexusProviderProps = { - children: React.ReactNode; - config?: { - network?: NexusNetwork; - debug?: boolean; - }; -}; - -const defaultConfig: Required = { - network: "mainnet", - debug: false, -}; - -const NexusProvider = ({ - children, - config = defaultConfig, -}: NexusProviderProps) => { - const stableConfig = useMemo( - () => ({ ...defaultConfig, ...config }), - [config], - ); - - const sdkRef = useRef(null); - sdkRef.current ??= new NexusSDK({ - ...stableConfig, - }); - const sdk = sdkRef.current; - - const [nexusSDK, setNexusSDK] = useState(null); - const [loading, setLoading] = useState(false); - const [supportedChainsAndTokens, setSupportedChainsAndTokens] = - useState( - sdk.utils.getSupportedChains( - stableConfig.network === "testnet" ? 0 : undefined, - ) ?? null, - ); - const [swapSupportedChainsAndTokens, setSwapSupportedChainsAndTokens] = - useState( - sdk.utils.getSwapSupportedChainsAndTokens() ?? null, - ); - const [bridgableBalance, setBridgableBalance] = useState( - null, - ); - const [swapBalance, setSwapBalance] = useState(null); - const exchangeRate = useRef | null>(null); - - const intent = useRef(null); - const allowance = useRef(null); - const swapIntent = useRef(null); - - useEffect(() => { - const list = sdk.utils.getSupportedChains( - stableConfig.network === "testnet" ? 0 : undefined, - ); - setSupportedChainsAndTokens(list ?? null); - const swapList = sdk.utils.getSwapSupportedChainsAndTokens(); - setSwapSupportedChainsAndTokens(swapList ?? null); - }, [sdk, stableConfig.network]); - - const setupNexus = useCallback(async () => { - const list = sdk.utils.getSupportedChains( - stableConfig.network === "testnet" ? 0 : undefined, - ); - setSupportedChainsAndTokens(list ?? null); - const swapList = sdk.utils.getSwapSupportedChainsAndTokens(); - setSwapSupportedChainsAndTokens(swapList ?? null); - const [bridgeAbleBalanceResult, rates] = await Promise.allSettled([ - sdk.getBalancesForBridge(), - sdk.utils.getCoinbaseRates(), - ]); - console.log("bridgeAbleBalanceResult", bridgeAbleBalanceResult); - - if (bridgeAbleBalanceResult.status === "fulfilled") { - setBridgableBalance(bridgeAbleBalanceResult.value); - } - - if (rates?.status === "fulfilled") { - // Coinbase returns "units per USD" (e.g., 1 USD = 0.00028 ETH). - // Convert to "USD per unit" (e.g., 1 ETH = ~$3514) for straightforward UI calculations. - const usdPerUnit: Record = {}; - - for (const [symbol, value] of Object.entries(rates.value)) { - const unitsPerUsd = Number.parseFloat(String(value)); - if (Number.isFinite(unitsPerUsd) && unitsPerUsd > 0) { - usdPerUnit[symbol.toUpperCase()] = 1 / unitsPerUsd; - } - } - exchangeRate.current = usdPerUnit; - } - }, [sdk, stableConfig.network]); - - const initializeNexus = async (provider: EthereumProvider) => { - setLoading(true); - try { - if (sdk.isInitialized()) throw new Error("Nexus is already initialized"); - await sdk.initialize(provider); - setNexusSDK(sdk); - } catch (error) { - console.error("Error initializing Nexus:", error); - } finally { - setLoading(false); - } - }; - - const deinitializeNexus = async () => { - try { - if (!nexusSDK) return; - await nexusSDK.deinit(); - setNexusSDK(null); - setBridgableBalance(null); - setSwapBalance(null); - exchangeRate.current = null; - intent.current = null; - swapIntent.current = null; - allowance.current = null; - setLoading(false); - } catch (error) { - console.error("Error deinitializing Nexus:", error); - } - }; - - const attachEventHooks = () => { - sdk.setOnAllowanceHook((data: OnAllowanceHookData) => { - /** - * Useful when you want the user to select, min, max or a custom value - * Can use this to capture data and then show it on the UI - * @see - always call data.allow() to progress the flow, otherwise it will stay stuck here. - * const {allow, sources, deny} = data - * @example allow(['min', 'max', '0.5']), the array in allow function should match number of sources. - * You can skip setting this hook if you want, sdk will auto progress if this hook is not attached - */ - allowance.current = data; - }); - - sdk.setOnIntentHook((data: OnIntentHookData) => { - /** - * Useful when you want to capture the intent, and display it on the UI (bridge, bridgeAndTransfer, bridgeAndExecute) - * const {allow, deny, intent, refresh} = data - * @see - always call data.allow() to progress the flow, otherwise it will stay stuck here. - * deny() to reject the intent - * refresh() to refresh the intent, best to call refresh in 15 second intervals - * data.intent -> details about the intent, useful when wanting to display info on UI - * You can skip setting this hook if you want, sdk will auto progress if this hook is not attached - */ - intent.current = data; - }); - - sdk.setOnSwapIntentHook((data: OnSwapIntentHookData) => { - /** - * Same behaviour and function as setOnIntentHook, except this one is for swaps exclusively - */ - swapIntent.current = data; - }); - }; - - const handleInit = async (provider: EthereumProvider) => { - console.log("[NexusProvider] handleInit called"); - console.log("[NexusProvider] SDK isInitialized:", sdk.isInitialized()); - console.log("[NexusProvider] Loading:", loading); - - if (sdk.isInitialized() || loading) { - console.log( - "[NexusProvider] Skipping init - already initialized or loading", - ); - return; - } - - if (!provider || typeof provider.request !== "function") { - console.error("[NexusProvider] Invalid provider:", provider); - throw new Error("Invalid EIP-1193 provider"); - } - - console.log("[NexusProvider] Calling initializeNexus..."); - await initializeNexus(provider); - - console.log("[NexusProvider] Calling setupNexus..."); - await setupNexus(); - - console.log("[NexusProvider] Calling attachEventHooks..."); - attachEventHooks(); - - console.log("[NexusProvider] handleInit complete!"); - }; - - const fetchBridgableBalance = async () => { - try { - const updatedBalance = await sdk.getBalancesForBridge(); - console.log("bridgeAbleBalanceResult", updatedBalance); - setBridgableBalance(updatedBalance); - } catch (error) { - console.error("Error fetching bridgable balance:", error); - } - }; - - const fetchSwapBalance = async () => { - try { - const updatedBalance = await sdk.getBalancesForSwap(); - console.log("swapBalance", updatedBalance); - setSwapBalance(updatedBalance); - } catch (error) { - console.error("Error fetching swap balance:", error); - } - }; - - function getFiatValue(amount: number, token: string) { - const key = token.toUpperCase(); - const rate = exchangeRate.current?.[key] ?? 1; - return rate * amount; - } - - useAccountEffect({ - onDisconnect() { - deinitializeNexus(); - }, - }); - - const setIntent = (data: OnIntentHookData | null) => { - intent.current = data; - }; - - const setAllowance = (data: OnAllowanceHookData | null) => { - allowance.current = data; - }; - - const value = useMemo( - () => ({ - nexusSDK, - initializeNexus, - deinitializeNexus, - attachEventHooks, - intent, - allowance, - handleInit, - supportedChainsAndTokens, - swapSupportedChainsAndTokens, - bridgableBalance, - swapBalance: swapBalance, - network: config?.network, - loading, - fetchBridgableBalance, - fetchSwapBalance, - swapIntent, - exchangeRate: exchangeRate.current, - getFiatValue, - setIntent, - setAllowance, - }), - [ - nexusSDK, - initializeNexus, - deinitializeNexus, - attachEventHooks, - handleInit, - swapBalance, - config, - loading, - fetchBridgableBalance, - fetchSwapBalance, - setIntent, - setAllowance, - supportedChainsAndTokens, - swapSupportedChainsAndTokens, - ], - ); - return ( - {children} - ); -}; - -export function useNexus() { - const context = useContext(NexusContext); - if (!context) { - throw new Error("useNexus must be used within a NexusProvider"); - } - return context; -} - -export default NexusProvider; diff --git a/apps/megaeth/src/components/nexus/useNexus.ts b/apps/megaeth/src/components/nexus/useNexus.ts deleted file mode 100644 index 2dcd3f9..0000000 --- a/apps/megaeth/src/components/nexus/useNexus.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useContext } from "react"; -import { NexusContext } from "./NexusProvider"; - -export function useNexus() { - const context = useContext(NexusContext); - if (!context) { - throw new Error("useNexus must be used within a NexusProvider"); - } - return context; -} diff --git a/apps/megaeth/src/components/ui/accordion.tsx b/apps/megaeth/src/components/ui/accordion.tsx deleted file mode 100644 index 75b4f79..0000000 --- a/apps/megaeth/src/components/ui/accordion.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDownIcon } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -function Accordion({ - ...props -}: React.ComponentProps) { - return ; -} - -function AccordionItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AccordionTrigger({ - className, - children, - hideChevron = true, - containerClassName = "w-full", - ...props -}: React.ComponentProps & { - hideChevron?: boolean; - containerClassName?: string; -}) { - return ( - - svg]:rotate-180", - className, - )} - {...props} - > - {children} - {!hideChevron && ( - - )} - - - ); -} - -function AccordionContent({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - -
{children}
-
- ); -} - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/apps/megaeth/src/components/ui/badge.tsx b/apps/megaeth/src/components/ui/badge.tsx deleted file mode 100644 index fd3a406..0000000 --- a/apps/megaeth/src/components/ui/badge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" - - return ( - - ) -} - -export { Badge, badgeVariants } diff --git a/apps/megaeth/src/components/ui/bubbles-animation.tsx b/apps/megaeth/src/components/ui/bubbles-animation.tsx deleted file mode 100644 index 6c4b772..0000000 --- a/apps/megaeth/src/components/ui/bubbles-animation.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client"; - -import React, { useEffect, useRef } from "react"; - -/** - * BubbleAnimation - * Minimal React component that implements ONLY the floating bubble animation - * from the referenced CodePen. Modified to render oblong (wider) bubbles. - */ -export default function BubbleAnimation({ - bubbleColor = "rgb(33, 150, 243)", - durationMs = 2000, - width = "max(500px, 50vw)", // wider - height = "max(200px, 20vw)", // shorter -}) { - const wrapperRef = useRef(null); - - useEffect(() => { - const wrapper = wrapperRef.current as HTMLElement | null; - if (!wrapper) return; - - const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); - const reduced = mediaQuery.matches; - - const timeouts = new Set(); - - const animateBubble = (x: number) => { - if (reduced) return; - const bubble = document.createElement("div"); - bubble.className = "bubble"; - bubble.style.left = `${x}px`; - bubble.style.backgroundColor = bubbleColor; - bubble.style.width = width; - bubble.style.height = height; - wrapper.appendChild(bubble); - const to = window.setTimeout(() => { - bubble.remove(); - timeouts.delete(to); - }, durationMs); - timeouts.add(to); - }; - - const handleMove = (e: MouseEvent) => animateBubble(e.clientX); - window.addEventListener("mousemove", handleMove); - - return () => { - window.removeEventListener("mousemove", handleMove); - timeouts.forEach((id) => window.clearTimeout(id)); - timeouts.clear(); - Array.from(wrapper.children).forEach((el) => el.remove()); - }; - }, [bubbleColor, durationMs, width, height]); - - return ( -
- {/* Animation layer only */} - - ); -} - diff --git a/apps/megaeth/src/components/ui/card.tsx b/apps/megaeth/src/components/ui/card.tsx deleted file mode 100644 index d05bbc6..0000000 --- a/apps/megaeth/src/components/ui/card.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -function Card({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} diff --git a/apps/megaeth/src/components/ui/dialog.tsx b/apps/megaeth/src/components/ui/dialog.tsx deleted file mode 100644 index d9ccec9..0000000 --- a/apps/megaeth/src/components/ui/dialog.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client" - -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Dialog({ - ...props -}: React.ComponentProps) { - return -} - -function DialogTrigger({ - ...props -}: React.ComponentProps) { - return -} - -function DialogPortal({ - ...props -}: React.ComponentProps) { - return -} - -function DialogClose({ - ...props -}: React.ComponentProps) { - return -} - -function DialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function DialogContent({ - className, - children, - showCloseButton = true, - ...props -}: React.ComponentProps & { - showCloseButton?: boolean -}) { - return ( - - - - {children} - {showCloseButton && ( - - - Close - - )} - - - ) -} - -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function DialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -} diff --git a/apps/megaeth/src/components/ui/input.tsx b/apps/megaeth/src/components/ui/input.tsx deleted file mode 100644 index 0316cc4..0000000 --- a/apps/megaeth/src/components/ui/input.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ); -} - -export { Input }; diff --git a/apps/megaeth/src/components/ui/label.tsx b/apps/megaeth/src/components/ui/label.tsx deleted file mode 100644 index fb5fbc3..0000000 --- a/apps/megaeth/src/components/ui/label.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client" - -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" - -import { cn } from "@/lib/utils" - -function Label({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { Label } diff --git a/apps/megaeth/src/components/ui/select.tsx b/apps/megaeth/src/components/ui/select.tsx deleted file mode 100644 index 91c5bdf..0000000 --- a/apps/megaeth/src/components/ui/select.tsx +++ /dev/null @@ -1,185 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as SelectPrimitive from "@radix-ui/react-select"; -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -function Select({ - ...props -}: React.ComponentProps) { - return ; -} - -function SelectGroup({ - ...props -}: React.ComponentProps) { - return ; -} - -function SelectValue({ - ...props -}: React.ComponentProps) { - return ; -} - -function SelectTrigger({ - className, - size = "default", - children, - ...props -}: React.ComponentProps & { - size?: "sm" | "default"; -}) { - return ( - - {children} - - - - - ); -} - -function SelectContent({ - className, - children, - position = "popper", - ...props -}: React.ComponentProps) { - return ( - - - - - {children} - - - - - ); -} - -function SelectLabel({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function SelectItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ); -} - -function SelectSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function SelectScrollUpButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -function SelectScrollDownButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -}; diff --git a/apps/megaeth/src/components/ui/separator.tsx b/apps/megaeth/src/components/ui/separator.tsx deleted file mode 100644 index be47ba4..0000000 --- a/apps/megaeth/src/components/ui/separator.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Separator({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -export { Separator }; diff --git a/apps/megaeth/src/components/ui/skeleton.tsx b/apps/megaeth/src/components/ui/skeleton.tsx deleted file mode 100644 index 32ea0ef..0000000 --- a/apps/megaeth/src/components/ui/skeleton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { cn } from "@/lib/utils" - -function Skeleton({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { Skeleton } diff --git a/apps/megaeth/src/components/ui/sonner.tsx b/apps/megaeth/src/components/ui/sonner.tsx deleted file mode 100644 index f96a98d..0000000 --- a/apps/megaeth/src/components/ui/sonner.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import { useTheme } from "next-themes"; -import { Toaster as Sonner, type ToasterProps } from "sonner"; - -const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); - - return ( - - ); -}; - -export { Toaster }; diff --git a/apps/megaeth/src/components/ui/tooltip.tsx b/apps/megaeth/src/components/ui/tooltip.tsx deleted file mode 100644 index 99aefa0..0000000 --- a/apps/megaeth/src/components/ui/tooltip.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; - -import { cn } from "@/lib/utils"; - -function TooltipProvider({ - delayDuration = 0, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function Tooltip({ - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -function TooltipTrigger({ - ...props -}: React.ComponentProps) { - return ; -} - -function TooltipContent({ - className, - sideOffset = 0, - children, - ...props -}: React.ComponentProps) { - return ( - - - {children} - - - - ); -} - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/apps/megaeth/src/components/view-history/view-history.tsx b/apps/megaeth/src/components/view-history/view-history.tsx deleted file mode 100644 index b58dd82..0000000 --- a/apps/megaeth/src/components/view-history/view-history.tsx +++ /dev/null @@ -1,276 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTrigger, - DialogTitle, -} from "@/components/ui/dialog"; -import { Clock, LoaderPinwheel, SquareArrowOutUpRight } from "lucide-react"; -import { type RFF } from "@avail-project/nexus-core"; -import { cn } from "@/lib/utils"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; -import useViewHistory from "./hooks/useViewHistory"; -import { TOKEN_IMAGES } from "../common"; - -const SourceChains = ({ sources }: { sources: RFF["sources"] }) => { - return ( -
- {sources?.map((source, index) => ( -
0 && "-ml-2", - )} - style={{ zIndex: sources.length - index }} - > - {source?.chain?.name} -
- ))} -
- ); -}; - -const StatusBadge = ({ status }: { status: string }) => { - const getVariant = (status: string) => { - if (status === "Fulfilled") { - return "default"; - } else if (status === "Deposited") { - return "secondary"; - } else if (status === "Refunded") { - return "outline"; - } else if (status === "Failed") { - return "destructive"; - } else { - return "default"; - } - }; - - return ( - -

{status}

-
- ); -}; - -const DestinationToken = ({ - destination, -}: { - destination: RFF["destinations"]; -}) => { - return ( -
- {destination.map((dest, index) => ( -
0 && "-ml-2", - )} - style={{ zIndex: destination.length - index }} - > - {dest.token.symbol} -
- ))} -
- ); -}; - -const ViewHistory = ({ - viewAsModal = true, - className, - refreshNonce, -}: { - viewAsModal?: boolean; - className?: string; - refreshNonce?: number; -}) => { - const { - history, - displayedHistory, - hasMore, - isLoadingMore, - getStatus, - observerTarget, - ITEMS_PER_PAGE, - formatExpiryDate, - fetchIntentHistory, - } = useViewHistory(); - - useEffect(() => { - if (!refreshNonce) return; - void fetchIntentHistory(); - }, [refreshNonce, fetchIntentHistory]); - - const renderHistoryContent = () => { - if (displayedHistory.length > 0) { - return ( - <> - {displayedHistory?.map((pastIntent) => ( - -
-
- -
-

- {pastIntent?.destinations - .map((d) => d?.token?.symbol) - .join(", ")} -

-

- Intent #{pastIntent?.id} -

-
-
- -
- - - -
-
- -
-
- -
-
-
- {pastIntent?.destinationChain?.name} -
-
- -
-
-

Expiry

-

- {formatExpiryDate(pastIntent?.expiry)} -

-
- - - -
-
- - ))} - - {hasMore && ( -
- {isLoadingMore && ( -
- - Loading more... -
- )} -
- )} - - {!hasMore && displayedHistory?.length > ITEMS_PER_PAGE && ( -
-

- No more transactions to load -

-
- )} - - ); - } - - if (history === null) { - return ( -
-
-
- -
-
-

Loading your history

-

- Fetching your past transactions... -

-
-
- ); - } - - return ( -
- -
-

No history yet

-

- Your transaction history will appear here -

-
-
- ); - }; - - if (!viewAsModal) { - return ( -
- {renderHistoryContent()} -
- ); - } - - return ( - - - - - - - - Transaction History - - -
- {renderHistoryContent()} -
-
-
- ); -}; - -export default ViewHistory; diff --git a/apps/megaeth/src/favicon.ico b/apps/megaeth/src/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/apps/megaeth/src/favicon.ico and /dev/null differ diff --git a/apps/megaeth/src/lib/button-variants.ts b/apps/megaeth/src/lib/button-variants.ts deleted file mode 100644 index d87142c..0000000 --- a/apps/megaeth/src/lib/button-variants.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { cva } from "class-variance-authority"; - -const buttonVariants = cva( - "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -export { buttonVariants }; diff --git a/apps/megaeth/src/lib/posthog.ts b/apps/megaeth/src/lib/posthog.ts deleted file mode 100644 index aa51bd9..0000000 --- a/apps/megaeth/src/lib/posthog.ts +++ /dev/null @@ -1,76 +0,0 @@ -import posthog from 'posthog-js'; - -const DEFAULT_POSTHOG_KEY = 'phc_UD6lQU3PEw1d8oo8E17rJLmRAR7kxJbQ5OseHuCvi7N'; -const DEFAULT_POSTHOG_HOST = 'https://us.i.posthog.com'; - -let isInitialized = false; - -/** - * Initialize PostHog analytics - */ -export function initPostHog(options?: { - apiKey?: string; - apiHost?: string; - debug?: boolean; -}): void { - if (isInitialized) return; - - const apiKey = options?.apiKey || DEFAULT_POSTHOG_KEY; - const apiHost = options?.apiHost || DEFAULT_POSTHOG_HOST; - const debug = options?.debug ?? true; // Always enable debug for troubleshooting - - console.log('[PostHog] Initializing with host:', apiHost); - - posthog.init(apiKey, { - api_host: apiHost, - person_profiles: 'identified_only', - autocapture: false, - capture_pageview: false, - capture_pageleave: true, - persistence: 'localStorage', // Use localStorage for persistence - loaded: (ph) => { - console.log('[PostHog] Loaded successfully'); - if (debug) { - ph.debug(); - } - }, - }); - - isInitialized = true; -} - -/** - * Capture a bridge submit event - */ -export interface BridgeSubmitEventProperties { - chain: string | number; - chainName: string; - tokenSymbol: string; - amount: string; - fast_bridge: 'megaeth' | 'citrea' | 'monad'; -} - -export function trackBridgeSubmit(properties: BridgeSubmitEventProperties): void { - if (!isInitialized) { - console.warn('[PostHog] Not initialized, initializing now...'); - initPostHog(); - } - - console.log('[PostHog] Capturing event: nexus_fast_bridge_demo_submit', properties); - posthog.capture('nexus_fast_bridge_demo_submit', properties); -} - -/** - * Generic capture wrapper - */ -export function capture(event: string, properties?: Record): void { - if (!isInitialized) { - console.warn('[PostHog] Not initialized, initializing now...'); - initPostHog(); - } - - console.log('[PostHog] Capturing event:', event, properties); - posthog.capture(event, properties); -} - -export { posthog }; diff --git a/apps/megaeth/src/lib/transaction-utils.ts b/apps/megaeth/src/lib/transaction-utils.ts deleted file mode 100644 index 32eab01..0000000 --- a/apps/megaeth/src/lib/transaction-utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -export const getOperationText = (type: string) => { - switch (type) { - case "bridge": - return "Transaction"; - case "transfer": - return "Transferring"; - case "bridgeAndExecute": - return "Bridge & Execute"; - case "swap": - return "Swapping"; - default: - return "Processing"; - } -}; - -export const getStatusText = (type: string, operationType: string) => { - const opText = getOperationText(operationType); - - switch (type) { - case "INTENT_ACCEPTED": - return "Intent Accepted"; - case "INTENT_HASH_SIGNED": - return "Signing Transaction"; - case "INTENT_SUBMITTED": - return "Submitting Transaction"; - case "INTENT_COLLECTION": - return "Collecting Confirmations"; - case "INTENT_COLLECTION_COMPLETE": - return "Confirmations Complete"; - case "APPROVAL": - return "Approving"; - case "TRANSACTION_SENT": - return "Sending Transaction"; - case "RECEIPT_RECEIVED": - return "Receipt Received"; - case "TRANSACTION_CONFIRMED": - case "INTENT_FULFILLED": - return `${opText} Complete`; - default: - return `Processing ${opText}`; - } -}; diff --git a/apps/megaeth/src/lib/url-params.ts b/apps/megaeth/src/lib/url-params.ts deleted file mode 100644 index 2fb5962..0000000 --- a/apps/megaeth/src/lib/url-params.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { type SUPPORTED_CHAINS_IDS, type SUPPORTED_TOKENS } from "@avail-project/nexus-core"; -import { isAddress } from "viem"; - -const ALLOWED_TOKENS = new Set(["USDC", "USDT"]) as Set; -const FILTERED_CHAIN_ID = 728126428; - -interface BridgeParams { - to?: SUPPORTED_CHAINS_IDS; - token?: SUPPORTED_TOKENS; - recipient?: `0x${string}`; - amount?: string; -} - -function isValidToken(token: string | null): token is SUPPORTED_TOKENS { - if (!token) return false; - const upperToken = token.toUpperCase().trim(); - return ALLOWED_TOKENS.has(upperToken as SUPPORTED_TOKENS); -} - -function isValidChain(chainStr: string | null): boolean { - if (!chainStr) return false; - const chainId = Number.parseInt(chainStr, 10); - if (!Number.isInteger(chainId) || chainId <= 0 || chainId > Number.MAX_SAFE_INTEGER) return false; - if (chainId === FILTERED_CHAIN_ID) return false; - return true; -} - -function sanitizeAmount(amount: string | null): string | undefined { - if (!amount) return undefined; - const sanitized = amount.trim(); - if (sanitized === "" || sanitized === ".") return undefined; - if (!/^\d*\.?\d*$/.test(sanitized)) return undefined; - const num = Number.parseFloat(sanitized); - if (Number.isNaN(num) || num <= 0) return undefined; - if (num > 1e9) return undefined; - return sanitized; -} - -export function readBridgeParams(): BridgeParams { - const params = new URLSearchParams(window.location.search); - const toStr = params.get("to"); - const tokenStr = params.get("token"); - const recipient = params.get("recipient"); - const amountStr = params.get("amount"); - - const to = toStr && toStr !== "self" && isValidChain(toStr) ? (Number.parseInt(toStr, 10) as SUPPORTED_CHAINS_IDS) : undefined; - const token = isValidToken(tokenStr) ? (tokenStr!.toUpperCase() as SUPPORTED_TOKENS) : undefined; - const sanitizedAmount = sanitizeAmount(amountStr); - const recipientAddress = recipient && isAddress(recipient) ? recipient : undefined; - - return { - to, - token, - recipient: recipientAddress, - amount: sanitizedAmount, - }; -} - -export function writeBridgeParams(params: BridgeParams): void { - const url = new URL(window.location.href); - - url.searchParams.delete("to"); - url.searchParams.delete("token"); - url.searchParams.delete("recipient"); - url.searchParams.delete("amount"); - - if (params.to && isValidChain(String(params.to))) { - url.searchParams.set("to", String(params.to)); - } - if (params.token && ALLOWED_TOKENS.has(params.token)) { - url.searchParams.set("token", params.token); - } - if (params.recipient && isAddress(params.recipient)) { - url.searchParams.set("recipient", params.recipient); - } - const sanitizedAmount = sanitizeAmount(params.amount ?? null); - if (sanitizedAmount) { - url.searchParams.set("amount", sanitizedAmount); - } - - window.history.replaceState({}, "", url.toString()); -} diff --git a/apps/megaeth/src/lib/utils.ts b/apps/megaeth/src/lib/utils.ts deleted file mode 100644 index f9923e8..0000000 --- a/apps/megaeth/src/lib/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} -export function absoluteUrl(path: string) { - return `${process.env.NEXT_PUBLIC_BASE_URL}${path}`; -} diff --git a/apps/megaeth/src/main.tsx b/apps/megaeth/src/main.tsx index e98387c..0802495 100644 --- a/apps/megaeth/src/main.tsx +++ b/apps/megaeth/src/main.tsx @@ -1,46 +1,3 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import "./index.css"; -import App from "./App"; -import { initPostHog } from "./lib/posthog"; +import { bootstrapApp } from "@/bootstrap"; -// Initialize PostHog analytics -initPostHog(); - -// Clean up WalletConnect IndexedDB before app loads to prevent structure errors -const cleanupWalletConnectSubscription = () => { - try { - const dbName = "WALLET_CONNECT_V2_INDEXED_DB"; - - // Delete the entire database to prevent structure errors - const deleteRequest = indexedDB.deleteDatabase(dbName); - - deleteRequest.onsuccess = () => { - console.log( - "[WalletConnect] Database deleted successfully, will be recreated fresh", - ); - }; - - deleteRequest.onerror = () => { - console.debug( - "[WalletConnect] Database deletion failed or DB doesn't exist", - ); - }; - - deleteRequest.onblocked = () => { - console.debug("[WalletConnect] Database deletion blocked, may be in use"); - }; - } catch (error) { - // Ignore errors - this is a cleanup operation - console.debug("[WalletConnect] Cleanup skipped:", error); - } -}; - -// Run cleanup before rendering -cleanupWalletConnectSubscription(); - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); +bootstrapApp(); diff --git a/apps/megaeth/src/providers/web3Provider.tsx b/apps/megaeth/src/providers/web3Provider.tsx deleted file mode 100644 index f913430..0000000 --- a/apps/megaeth/src/providers/web3Provider.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import React from "react"; -import { ConnectKitProvider, getDefaultConfig } from "connectkit"; -import { WagmiProvider, createConfig, http } from "wagmi"; -import { - mainnet, - scroll, - polygon, - optimism, - arbitrum, - base, - avalanche, - sophon, - kaia, - monad, - type Chain, -} from "wagmi/chains"; -import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; -import config from "../../config"; - -const walletConnectProjectId = import.meta.env.VITE_WALLET_CONNECT_ID; - -const chain: Chain = { - id: config.chainId, - name: config.chainName, - nativeCurrency: { - name: config.chainNativeCurrency.name, - symbol: config.chainNativeCurrency.symbol, - decimals: config.chainNativeCurrency.decimals, - }, - rpcUrls: { - default: { http: [config.chainRpcUrl] }, - }, - blockExplorers: { - default: { name: "Explorer", url: config.chainBlockExplorerUrl }, - }, - testnet: config.chainTestnet, -}; - -const megaeth: Chain = { - id: 4326, - name: "MegaETH Mainnet", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18, - }, - rpcUrls: { - default: { http: [import.meta.env.VITE_MEGAETH_RPC] }, - }, - blockExplorers: { - default: { name: "Explorer", url: "https://megaeth.blockscout.com" }, - }, - testnet: false, -}; - -//ideally we should add private rpcs for each, for now it fallbacks to default rpcs -const transports = { - [mainnet.id]: http(import.meta.env.VITE_MAINNET_RPC), - [base.id]: http(import.meta.env.VITE_BASE_RPC), - [arbitrum.id]: http(import.meta.env.VITE_ARBITRUM_RPC), - [optimism.id]: http(import.meta.env.VITE_OPTIMISM_RPC), - [polygon.id]: http(import.meta.env.VITE_POLYGON_RPC), - [scroll.id]: http(import.meta.env.VITE_SCROLL_RPC), - [avalanche.id]: http(import.meta.env.VITE_AVALANCHE_RPC), - [sophon.id]: http(import.meta.env.VITE_SOPHON_RPC), - [kaia.id]: http(import.meta.env.VITE_KAIA_RPC), - [chain.id]: http(config.chainRpcUrl), - [monad.id]: http(import.meta.env.VITE_MONAD_RPC), - [megaeth.id]: http(import.meta.env.VITE_MEGAETH_RPC), -}; - -const defaultConfigParams = getDefaultConfig({ - appName: config.appTitle ?? "Nexus Elements", - walletConnectProjectId, - chains: [ - chain, - mainnet, - base, - sophon, - kaia, - arbitrum, - avalanche, - optimism, - polygon, - scroll, - monad, - megaeth, - ], - transports, - enableFamily: false, -}); - -const wagmiConfig = createConfig(defaultConfigParams); -const queryClient = new QueryClient(); - -const Web3Provider = ({ children }: { children: React.ReactNode }) => { - return ( - - - {children} - - - ); -}; - -export default Web3Provider; diff --git a/apps/megaeth/src/runtime.ts b/apps/megaeth/src/runtime.ts new file mode 100644 index 0000000..867f767 --- /dev/null +++ b/apps/megaeth/src/runtime.ts @@ -0,0 +1,49 @@ +import { SUPPORTED_CHAINS } from "@avail-project/nexus-core"; +import type { ChainFeatures } from "@/types/runtime"; +import config from "../config"; + +export const appConfig = config; + +export const chainFeatures: ChainFeatures = { + slug: "megaeth", + analyticsFastBridgeKey: "megaeth", + maxBridgeAmount: 5000, + walletInitDelayMs: 500, + showFluffeyMascot: true, + showPromoBanner: false, + promoBannerLine1: "Zero solver and protocol fees when bridging to MegaETH.", + promoBannerLine2: "Till Friday the 13th. Don't fade anon.", + promoBannerImageUrl: + "https://files.availproject.org/fastbridge/megaeth/megaeth-mascot-1.png", + pageDescription: + "Avail Fast Bridge for MegaETH enables instant, secure bridging of USDC and ETH to the MegaETH network. Powered by Avail Nexus, it supports seamless transfers from Ethereum, Base, Optimism, Arbitrum, and more, ensuring liquidity is unified without complex wrapping.", + showSupportCta: true, + supportCtaHref: "https://discord.com/invite/AvailProject", + supportCtaLine1: "Reach out to us if", + supportCtaLine2: "you face any issues", + mapUsdmDisplaySymbolToUsdc: true, + mapUsdmToUsdcBalance: true, + tokenLogoOverrideBySymbol: { + USDM: "https://mega.etherscan.io/token/images/usdm_32.png", + }, + postBridgeWatchAsset: { + destinationChainId: SUPPORTED_CHAINS.MEGAETH, + tokenSymbol: "USDM", + walletAsset: { + address: "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7", + symbol: "USDm", + decimals: 18, + image: "https://mega.etherscan.io/token/images/usdm_32.png", + }, + }, + denyIntentOnReset: false, + tokenDenyListByChainId: {}, + allowanceLogoOverrideByChainId: {}, + amountInputUseCalculatedMaxHeader: true, + amountInputShowDestinationBadge: true, + amountInputUseSourceSymbolInBreakdown: true, + hideMegaethSourceForUsdm: false, + feeBreakdownHideGasSupplied: true, + feeBreakdownKeepZeroRows: true, + dialogShowCloseButton: false, +}; diff --git a/apps/megaeth/src/vite-env.d.ts b/apps/megaeth/src/vite-env.d.ts index 04436df..1a8b0eb 100644 --- a/apps/megaeth/src/vite-env.d.ts +++ b/apps/megaeth/src/vite-env.d.ts @@ -3,57 +3,56 @@ import { SUPPORTED_CHAINS } from "@avail-project/nexus-core"; export interface AppEnv { - readonly VITE_APP_BASE_PATH?: string; - readonly VITE_CONFIG_CHAIN_ID: number; - readonly VITE_CONFIG_CHAIN_NAME: string; - readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_NAME: string; - readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_SYMBOL: string; - readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_DECIMALS: number; - readonly VITE_CONFIG_CHAIN_RPC_URL: string; - readonly VITE_CONFIG_CHAIN_BLOCK_EXPLORER_URL: string; - readonly VITE_CONFIG_CHAIN_TESTNET: boolean; - readonly VITE_CONFIG_CHAIN_USE_CHAIN_LOGO: boolean; - readonly VITE_CONFIG_CHAIN_ICON_URL: string; - readonly VITE_CONFIG_CHAIN_LOGO_URL: string; - readonly VITE_CONFIG_CHAIN_GIF_URL: string; - readonly VITE_CONFIG_CHAIN_GIF_ALT: string; - readonly VITE_CONFIG_CHAIN_HERO_TEXT: string; - readonly VITE_CONFIG_APP_TITLE: string; - readonly VITE_CONFIG_APP_DESCRIPTION: string; - readonly VITE_CONFIG_PRIMARY_COLOR: string; - readonly VITE_CONFIG_SECONDARY_COLOR: string; - readonly VITE_CONFIG_NEXUS_NETWORK: 'mainnet' | 'testnet' | 'devnet'; - readonly VITE_CONFIG_NEXUS_SUPPORTED_CHAIN: SUPPORTED_CHAINS; - readonly VITE_CONFIG_NEXUS_PRIMARY_TOKEN: string; + readonly VITE_APP_BASE_PATH?: string; + readonly VITE_CONFIG_APP_DESCRIPTION: string; + readonly VITE_CONFIG_APP_META_BACKGROUND_COLOR: string; + readonly VITE_CONFIG_APP_META_CANONICAL_URL: string; + readonly VITE_CONFIG_APP_META_DESCRIPTION: string; + readonly VITE_CONFIG_APP_META_FAVICON_URL: string; + readonly VITE_CONFIG_APP_META_IMAGE_URL: string; + readonly VITE_CONFIG_APP_META_THEME_COLOR: string; - readonly VITE_CONFIG_APP_META_TITLE: string; - readonly VITE_CONFIG_APP_META_DESCRIPTION: string; - readonly VITE_CONFIG_APP_META_CANONICAL_URL: string; - readonly VITE_CONFIG_APP_META_FAVICON_URL: string; - readonly VITE_CONFIG_APP_META_THEME_COLOR: string; - readonly VITE_CONFIG_APP_META_IMAGE_URL: string; - readonly VITE_CONFIG_APP_META_BACKGROUND_COLOR: string; + readonly VITE_CONFIG_APP_META_TITLE: string; + readonly VITE_CONFIG_APP_TITLE: string; + readonly VITE_CONFIG_CHAIN_BLOCK_EXPLORER_URL: string; + readonly VITE_CONFIG_CHAIN_GIF_ALT: string; + readonly VITE_CONFIG_CHAIN_GIF_URL: string; + readonly VITE_CONFIG_CHAIN_HERO_TEXT: string; + readonly VITE_CONFIG_CHAIN_ICON_URL: string; + readonly VITE_CONFIG_CHAIN_ID: number; + readonly VITE_CONFIG_CHAIN_LOGO_URL: string; + readonly VITE_CONFIG_CHAIN_NAME: string; + readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_DECIMALS: number; + readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_NAME: string; + readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_SYMBOL: string; + readonly VITE_CONFIG_CHAIN_RPC_URL: string; + readonly VITE_CONFIG_CHAIN_TESTNET: boolean; + readonly VITE_CONFIG_CHAIN_USE_CHAIN_LOGO: boolean; + readonly VITE_CONFIG_NEXUS_NETWORK: "mainnet" | "testnet" | "devnet"; + readonly VITE_CONFIG_NEXUS_PRIMARY_TOKEN: string; + readonly VITE_CONFIG_NEXUS_SUPPORTED_CHAIN: SUPPORTED_CHAINS; + readonly VITE_CONFIG_PRIMARY_COLOR: string; + readonly VITE_CONFIG_SECONDARY_COLOR: string; } declare global { + interface ImportMetaEnv extends AppEnv { + readonly VITE_ARBITRUM_RPC: string; + readonly VITE_AVALANCHE_RPC: string; + readonly VITE_BASE: string; + readonly VITE_BASE_RPC: string; + readonly VITE_KAIA_RPC: string; + readonly VITE_MAINNET_RPC: string; + readonly VITE_MEGAETH_RPC: string; + readonly VITE_MONAD_RPC: string; + readonly VITE_OPTIMISM_RPC: string; + readonly VITE_POLYGON_RPC: string; + readonly VITE_SCROLL_RPC: string; + readonly VITE_SOPHON_RPC: string; + readonly VITE_WALLET_CONNECT_ID: string; + } - interface ImportMetaEnv extends AppEnv { - readonly VITE_WALLET_CONNECT_ID: string; - readonly VITE_MAINNET_RPC: string; - readonly VITE_BASE_RPC: string; - readonly VITE_ARBITRUM_RPC: string; - readonly VITE_OPTIMISM_RPC: string; - readonly VITE_POLYGON_RPC: string; - readonly VITE_SCROLL_RPC: string; - readonly VITE_AVALANCHE_RPC: string; - readonly VITE_SOPHON_RPC: string; - readonly VITE_KAIA_RPC: string; - readonly VITE_MONAD_RPC: string; - readonly VITE_MEGAETH_RPC: string; - readonly VITE_BASE: string; - } - - interface ImportMeta { - readonly env: ImportMetaEnv; - } + interface ImportMeta { + readonly env: ImportMetaEnv; + } } diff --git a/apps/megaeth/tsconfig.app.json b/apps/megaeth/tsconfig.app.json index afe17f6..c6a071f 100644 --- a/apps/megaeth/tsconfig.app.json +++ b/apps/megaeth/tsconfig.app.json @@ -24,7 +24,8 @@ "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": ["../../packages/fast-bridge-app/src/*"], + "@fastbridge/runtime": ["./src/runtime.ts"] } }, "include": ["src"] diff --git a/apps/megaeth/tsconfig.json b/apps/megaeth/tsconfig.json index fec8c8e..5bfdf37 100644 --- a/apps/megaeth/tsconfig.json +++ b/apps/megaeth/tsconfig.json @@ -8,6 +8,7 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"] - } + }, + "strictNullChecks": true } } diff --git a/apps/megaeth/vite.config.ts b/apps/megaeth/vite.config.ts index 778a7b7..de764ab 100644 --- a/apps/megaeth/vite.config.ts +++ b/apps/megaeth/vite.config.ts @@ -1,12 +1,12 @@ +import { writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import tailwindcss from "@tailwindcss/vite"; -import { nodePolyfills } from "vite-plugin-node-polyfills"; -import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; -import { writeFileSync } from "node:fs"; +import { defineConfig, loadEnv } from "vite"; +import { nodePolyfills } from "vite-plugin-node-polyfills"; +import { getConfig } from "./get-config"; import type { AppEnv } from "./src/vite-env.d"; -import { getConfig } from "./getConfig"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -18,31 +18,31 @@ export default defineConfig(({ mode }) => { const config = getConfig(env); const manifestContent = { - "name": config.meta.title, - "short_name": config.appTitle, - "description": config.meta.description, - "start_url": base, - "display": "standalone", - "background_color": config.meta.backgroundColor, - "theme_color": config.meta.themeColor, - "orientation": "portrait-primary", - "icons": [ + name: config.meta.title, + short_name: config.appTitle, + description: config.meta.description, + start_url: base, + display: "standalone", + background_color: config.meta.backgroundColor, + theme_color: config.meta.themeColor, + orientation: "portrait-primary", + icons: [ { - "src": config.meta.faviconUrl, - "sizes": "31x32", - "type": "image/png", - "purpose": "any" + src: config.meta.faviconUrl, + sizes: "31x32", + type: "image/png", + purpose: "any", }, { - "src": config.meta.imageUrl, - "sizes": "2444x1256", - "type": "image/png", - "purpose": "any" - } - ] + src: config.meta.imageUrl, + sizes: "2444x1256", + type: "image/png", + purpose: "any", + }, + ], }; - const outputPath = path.resolve(__dirname, 'dist', 'manifest.json'); + const outputPath = path.resolve(__dirname, "dist", "manifest.json"); return { base, @@ -55,19 +55,19 @@ export default defineConfig(({ mode }) => { }), // Create manifest.json at build { - name: 'generate-manifest', + name: "generate-manifest", writeBundle() { try { writeFileSync(outputPath, JSON.stringify(manifestContent, null, 2)); console.log(`Generated dynamic manifest.json at ${outputPath}`); } catch (err) { - console.error('Error generating manifest.json', err); + console.error("Error generating manifest.json", err); } - } + }, }, // Update title and meta of index.html at build { - name: 'html-transform', + name: "html-transform", transformIndexHtml(html) { // Replace placeholders with actual values return html @@ -77,13 +77,14 @@ export default defineConfig(({ mode }) => { .replaceAll(/%= APP_FAVICON_URL =%/g, config.meta.faviconUrl) .replaceAll(/%= APP_THEME_COLOR =%/g, config.meta.themeColor) .replaceAll(/%= APP_META_IMAGE_URL =%/g, config.meta.imageUrl) - .replaceAll(/%= MANIFEST_URL =%/g, `${base}manifest.json`) - } - } + .replaceAll(/%= MANIFEST_URL =%/g, `${base}manifest.json`); + }, + }, ], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + "@": path.resolve(__dirname, "../../packages/fast-bridge-app/src"), + "@fastbridge/runtime": path.resolve(__dirname, "./src/runtime.ts"), buffer: "vite-plugin-node-polyfills/shims/buffer", global: "vite-plugin-node-polyfills/shims/global", process: "vite-plugin-node-polyfills/shims/process", @@ -91,7 +92,7 @@ export default defineConfig(({ mode }) => { }, envPrefix: ["VITE_"], build: { - emptyOutDir: true - } - } + emptyOutDir: true, + }, + }; }); diff --git a/apps/monad/README.md b/apps/monad/README.md index 6438e99..3700e78 100644 --- a/apps/monad/README.md +++ b/apps/monad/README.md @@ -1,13 +1,15 @@ -# Nexus fast bridge +# Chain App Wrapper -### Run locally +This app is a thin wrapper around shared code in `packages/fast-bridge-app`. -`npm run dev` to run locally. +For architecture, onboarding, and customization: +- `README.md` (repo root) +- `docs/architecture.md` +- `docs/adding-chains.md` +- `docs/customization.md` -### Build project - -`npm run build` to build the project. - -### Preview - -`npm run start` to preview and run +Primary chain-specific files in this app: +- `get-config.ts` +- `src/runtime.ts` +- `.env.monad` +- `public/*` assets diff --git a/apps/monad/config.ts b/apps/monad/config.ts index b1e41eb..392c7b1 100644 --- a/apps/monad/config.ts +++ b/apps/monad/config.ts @@ -1,4 +1,4 @@ -import { getConfig } from "./getConfig"; +import { getConfig } from "./get-config"; const config = getConfig(import.meta.env); diff --git a/apps/monad/eslint.config.js b/apps/monad/eslint.config.js index a6d1076..8401724 100644 --- a/apps/monad/eslint.config.js +++ b/apps/monad/eslint.config.js @@ -1,9 +1,9 @@ import js from "@eslint/js"; -import globals from "globals"; +import { defineConfig, globalIgnores } from "eslint/config"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; import tseslint from "typescript-eslint"; -import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ globalIgnores(["dist"]), @@ -18,6 +18,9 @@ export default defineConfig([ languageOptions: { ecmaVersion: 2020, globals: globals.browser, + parserOptions: { + tsconfigRootDir: import.meta.dirname, + }, }, }, ]); diff --git a/apps/monad/get-config.ts b/apps/monad/get-config.ts new file mode 100644 index 0000000..31044f2 --- /dev/null +++ b/apps/monad/get-config.ts @@ -0,0 +1,87 @@ +import type { AppEnv } from "./src/vite-env.d"; + +const withFallback = (value: T, fallbackValue: T): T => + value || fallbackValue; + +export function getConfig(env: AppEnv) { + return { + chainId: Number(withFallback(env.VITE_CONFIG_CHAIN_ID, 143)), + chainName: withFallback(env.VITE_CONFIG_CHAIN_NAME, "Monad"), + chainNativeCurrency: { + name: withFallback(env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_NAME, "Monad"), + symbol: withFallback(env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_SYMBOL, "MON"), + decimals: Number( + withFallback(env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_DECIMALS, 18) + ), + }, + chainRpcUrl: withFallback( + env.VITE_CONFIG_CHAIN_RPC_URL, + "https://rpcs.avail.so/monad" + ), + chainBlockExplorerUrl: withFallback( + env.VITE_CONFIG_CHAIN_BLOCK_EXPLORER_URL, + "https://monadvision.com/" + ), + chainTestnet: env.VITE_CONFIG_CHAIN_TESTNET, + useChainLogo: env.VITE_CONFIG_CHAIN_USE_CHAIN_LOGO, + chainIconUrl: withFallback( + env.VITE_CONFIG_CHAIN_ICON_URL, + "/Monad_Logomark.svg" + ), + chainLogoUrl: withFallback( + env.VITE_CONFIG_CHAIN_LOGO_URL, + "/Monad_Logo.svg" + ), + chainGifUrl: withFallback(env.VITE_CONFIG_CHAIN_GIF_URL, "/salmonad.gif"), + chainGifAlt: withFallback(env.VITE_CONFIG_CHAIN_GIF_ALT, "Fast Salmonad"), + heroText: withFallback( + env.VITE_CONFIG_CHAIN_HERO_TEXT, + "Move your assets to Monad faster than ever!" + ), + + appTitle: withFallback(env.VITE_CONFIG_APP_TITLE, "Monad Fast Bridge"), + appDescription: withFallback( + env.VITE_CONFIG_APP_DESCRIPTION, + "Monad Fast Bridge" + ), + + primaryColor: withFallback(env.VITE_CONFIG_PRIMARY_COLOR, "#836ef9"), + secondaryColor: withFallback(env.VITE_CONFIG_SECONDARY_COLOR, "#ffffff"), + nexusNetwork: withFallback(env.VITE_CONFIG_NEXUS_NETWORK, "mainnet"), + nexusSupportedChain: Number( + withFallback(env.VITE_CONFIG_NEXUS_SUPPORTED_CHAIN, 143) + ), + nexusPrimaryToken: withFallback( + env.VITE_CONFIG_NEXUS_PRIMARY_TOKEN, + "USDC" + ), + + meta: { + title: withFallback( + env.VITE_CONFIG_APP_META_TITLE, + "Monad Fast Bridge - Powered by Avail Nexus" + ), + description: withFallback( + env.VITE_CONFIG_APP_META_DESCRIPTION, + "Move your unified USDC and USDT from 12 chains to Monad, faster than ever." + ), + canonicalUrl: withFallback( + env.VITE_CONFIG_APP_META_CANONICAL_URL, + "https://fastbridge.availproject.org/monad/" + ), + imageUrl: withFallback( + env.VITE_CONFIG_APP_META_IMAGE_URL, + "https://fastbridge.availproject.org/monad/MonadFBMeta.png" + ), + faviconUrl: withFallback( + env.VITE_CONFIG_APP_META_FAVICON_URL, + "https://fastbridge.availproject.org/monad/faviconV2.png" + ), + themeColor: withFallback(env.VITE_CONFIG_APP_META_THEME_COLOR, "#836ef9"), + backgroundColor: withFallback( + env.VITE_CONFIG_APP_META_BACKGROUND_COLOR, + "#ffffff" + ), + }, + }; +} diff --git a/apps/monad/getConfig.ts b/apps/monad/getConfig.ts deleted file mode 100644 index f00eede..0000000 --- a/apps/monad/getConfig.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { AppEnv } from "./src/vite-env.d"; - -export function getConfig(env: AppEnv) { - const config = { - chainId: Number(env.VITE_CONFIG_CHAIN_ID || 143), - chainName: env.VITE_CONFIG_CHAIN_NAME || "Monad", - chainNativeCurrency: { - name: env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_NAME || "Monad", - symbol: env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_SYMBOL || "MON", - decimals: Number(env.VITE_CONFIG_CHAIN_NATIVE_CURRENCY_DECIMALS || 18), - }, - chainRpcUrl: env.VITE_CONFIG_CHAIN_RPC_URL || "https://rpcs.avail.so/monad", - chainBlockExplorerUrl: - env.VITE_CONFIG_CHAIN_BLOCK_EXPLORER_URL || "https://monadvision.com/", - chainTestnet: env.VITE_CONFIG_CHAIN_TESTNET || false, - useChainLogo: env.VITE_CONFIG_CHAIN_USE_CHAIN_LOGO || false, - chainIconUrl: env.VITE_CONFIG_CHAIN_ICON_URL || "/Monad_Logomark.svg", - chainLogoUrl: env.VITE_CONFIG_CHAIN_LOGO_URL || "/Monad_Logo.svg", - chainGifUrl: env.VITE_CONFIG_CHAIN_GIF_URL || "/salmonad.gif", - chainGifAlt: env.VITE_CONFIG_CHAIN_GIF_ALT || "Fast Salmonad", - heroText: - env.VITE_CONFIG_CHAIN_HERO_TEXT || - "Move your assets to Monad faster than ever!", - - appTitle: env.VITE_CONFIG_APP_TITLE || "Monad Fast Bridge", - appDescription: env.VITE_CONFIG_APP_DESCRIPTION || "Monad Fast Bridge", - - primaryColor: env.VITE_CONFIG_PRIMARY_COLOR || "#836ef9", - secondaryColor: env.VITE_CONFIG_SECONDARY_COLOR || "#ffffff", - nexusNetwork: env.VITE_CONFIG_NEXUS_NETWORK || "mainnet", - nexusSupportedChain: - Number(env.VITE_CONFIG_NEXUS_SUPPORTED_CHAIN) || 143, - nexusPrimaryToken: env.VITE_CONFIG_NEXUS_PRIMARY_TOKEN || "USDC", - - meta: { - title: - env.VITE_CONFIG_APP_META_TITLE || - "Monad Fast Bridge - Powered by Avail Nexus", - description: - env.VITE_CONFIG_APP_META_DESCRIPTION || - "Move your unified USDC and USDT from 12 chains to Monad, faster than ever.", - canonicalUrl: - env.VITE_CONFIG_APP_META_CANONICAL_URL || "https://monadfastbridge.com", - imageUrl: - env.VITE_CONFIG_APP_META_IMAGE_URL || - "https://monadfastbridge.com/MonadFBMeta.png", - faviconUrl: - env.VITE_CONFIG_APP_META_FAVICON_URL || - "https://monadfastbridge.com/faviconV2.png", - themeColor: env.VITE_CONFIG_APP_META_THEME_COLOR || "#836ef9", - backgroundColor: env.VITE_CONFIG_APP_META_BACKGROUND_COLOR || "#ffffff", - }, - }; - - return config; -} \ No newline at end of file diff --git a/apps/monad/index.html b/apps/monad/index.html index 66cdf7a..87dc80c 100644 --- a/apps/monad/index.html +++ b/apps/monad/index.html @@ -1,77 +1,77 @@ - - + + - - - - - + + + + + %= APP_TITLE =% - - + + + > + > - - - - - - - - - + + + + + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - - + + + + + + - - + + + >
diff --git a/apps/monad/package.json b/apps/monad/package.json index 5ec2a3a..99711d9 100644 --- a/apps/monad/package.json +++ b/apps/monad/package.json @@ -11,8 +11,9 @@ "preview": "vite preview --port 5173" }, "dependencies": { - "@avail-project/nexus-core": "github:availproject/nexus-sdk#cca99c1a570a8598334e3e89453d39a27d70ede7", + "@avail-project/nexus-core": "1.1.2", "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", @@ -27,6 +28,7 @@ "connectkit": "^1.9.1", "decimal.js": "^10.6.0", "lucide-react": "^0.544.0", + "motion": "^12.29.2", "next-themes": "^0.4.6", "posthog-js": "^1.336.4", "react": "19.2.2", diff --git a/apps/monad/src/App.tsx b/apps/monad/src/App.tsx deleted file mode 100644 index 6e18e78..0000000 --- a/apps/monad/src/App.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import Web3Provider from "@/providers/web3Provider"; -import { Toaster } from "@/components/ui/sonner"; -import Navbar from "@/components/navbar"; -import HeroSection from "@/components/hero-section"; -import FastBridgeShowcase from "@/components/fast-bridge-showcase"; -import NexusProvider from "@/components/nexus/NexusProvider"; -import config from "../config"; - -// @ts-expect-error - Environment is not exported from @avail-project/nexus-core -enum Environment { - FOLLY, // Dev with test-net tokens - CERISE, // Dev with main-net tokens - CORAL, // Test-net with main-net tokens - JADE, // Main-net with main-net tokens -} - -export default function App() { - return ( - - -
- -
-
-
- - -
-
-
- - - - ); -} diff --git a/apps/monad/src/components/common/hooks/useInterval.ts b/apps/monad/src/components/common/hooks/useInterval.ts deleted file mode 100644 index 2287075..0000000 --- a/apps/monad/src/components/common/hooks/useInterval.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useRef } from "react"; -import { useStableCallback } from "./useStableCallback"; - -interface UseIntervalOptions { - enabled?: boolean; - immediate?: boolean; -} - -/** - * Declarative setInterval with pause/resume and latest-callback semantics. - * Pass delay=null to pause. - */ -export function useInterval( - callback: () => void, - delay: number | null, - options: UseIntervalOptions = {} -) { - const { enabled = true, immediate = false } = options; - const savedCallback = useStableCallback(callback); - const intervalRef = useRef(null); - - useEffect(() => { - if (!enabled || delay == null) return; - if (immediate) { - savedCallback(); - } - intervalRef.current = setInterval(savedCallback, delay); - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - }; - }, [delay, enabled, immediate, savedCallback]); -} diff --git a/apps/monad/src/components/common/hooks/useNexusError.ts b/apps/monad/src/components/common/hooks/useNexusError.ts deleted file mode 100644 index b14851e..0000000 --- a/apps/monad/src/components/common/hooks/useNexusError.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NexusError } from "@avail-project/nexus-core"; - -function handler(err: unknown) { - if (err instanceof NexusError) { - return { - code: err?.code, - message: err?.message, - context: err?.data?.context, - details: err?.data?.details, - }; - } else { - console.error("Unexpected error:", err); - return { - code: "unexpected_error", - message: "Oops! Something went wrong. Please try again.", - context: undefined, - details: undefined, - }; - } -} -export function useNexusError() { - return handler; -} diff --git a/apps/monad/src/components/common/hooks/useStableCallback.ts b/apps/monad/src/components/common/hooks/useStableCallback.ts deleted file mode 100644 index b6d8cb2..0000000 --- a/apps/monad/src/components/common/hooks/useStableCallback.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useCallback, useRef } from "react"; - -/** - * Returns a stable function identity that always calls the latest implementation. - * Useful when passing callbacks to memoized children without re-creating handlers. - */ -export function useStableCallback( - fn: (...args: Args) => Return -): (...args: Args) => Return { - const fnRef = useRef<(...args: Args) => Return>(fn); - fnRef.current = fn; - - - const stable = useCallback( - ((...args: Args) => { - return fnRef.current(...args); - }) as (...args: Args) => Return, - [] - ); - - return stable; -} diff --git a/apps/monad/src/components/common/index.ts b/apps/monad/src/components/common/index.ts deleted file mode 100644 index f5a855e..0000000 --- a/apps/monad/src/components/common/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./hooks/useStopwatch"; -export * from "./hooks/usePolling"; -export * from "./hooks/useInterval"; -export * from "./hooks/useStableCallback"; -export * from "./hooks/useDebouncedValue"; -export * from "./hooks/useDebouncedCallback"; -export * from "./hooks/useNexusError"; -export * from "./tx/types"; -export * from "./tx/steps"; -export * from "./tx/useTransactionSteps"; -export * from "./utils/constant"; diff --git a/apps/monad/src/components/common/tx/types.ts b/apps/monad/src/components/common/tx/types.ts deleted file mode 100644 index 7a2a7b1..0000000 --- a/apps/monad/src/components/common/tx/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -export type TransactionStatus = - | "idle" - | "preview" - | "awaiting-approval" - | "executing" - | "success" - | "error"; - -export type GenericStep = { - id: number; - completed: boolean; - step: TStep; -}; - -/** - * Normalizes a step to a stable key. Prefers typeID, then type, otherwise JSON. - */ -export function getStepKey(step: any): string { - if (!step) return ""; - if (typeof step.typeID === "string" && step.typeID.length > 0) { - return step.typeID; - } - if (typeof step.type === "string" && step.type.length > 0) { - return step.type; - } - try { - return JSON.stringify(step); - } catch { - return String(step); - } -} diff --git a/apps/monad/src/components/fast-bridge-showcase.tsx b/apps/monad/src/components/fast-bridge-showcase.tsx deleted file mode 100644 index 0ec7ddd..0000000 --- a/apps/monad/src/components/fast-bridge-showcase.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; -import { useAccount } from "wagmi"; -import { ConnectKitButton } from "connectkit"; -import FastBridge from "./fast-bridge/fast-bridge"; -import { PreviewPanel } from "./walletConnect"; - -const FastBridgeShowcase = () => { - const { address, isConnected } = useAccount(); - - return ( - - - {({ show }) => ( - - )} - - - ); -}; - -export default FastBridgeShowcase; diff --git a/apps/monad/src/components/fast-bridge/components/amount-input.tsx b/apps/monad/src/components/fast-bridge/components/amount-input.tsx deleted file mode 100644 index af8251f..0000000 --- a/apps/monad/src/components/fast-bridge/components/amount-input.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { type FC, Fragment, useEffect, useRef } from "react"; -import { Input } from "../../ui/input"; -import { Button } from "../../ui/button"; -import { SUPPORTED_CHAINS, type UserAsset } from "@avail-project/nexus-core"; -import { useNexus } from "../../nexus/NexusProvider"; -import { type FastBridgeState } from "../hooks/useBridge"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../../ui/accordion"; -import { SHORT_CHAIN_NAME } from "../../common"; -import { LoaderCircle } from "lucide-react"; - -interface AmountInputProps { - amount?: string; - onChange: (value: string) => void; - bridgableBalance?: UserAsset; - onCommit?: (value: string) => void; - disabled?: boolean; - inputs: FastBridgeState; - showBalanceDetails?: boolean; -} - -const AmountInput: FC = ({ - amount, - onChange, - bridgableBalance, - onCommit, - disabled, - inputs, - showBalanceDetails = true, -}) => { - const { nexusSDK, loading } = useNexus(); - const commitTimerRef = useRef(null); - const showBalanceDivider = showBalanceDetails && Boolean(bridgableBalance); - - const scheduleCommit = (val: string) => { - if (!onCommit || disabled) return; - if (commitTimerRef.current) clearTimeout(commitTimerRef.current); - commitTimerRef.current = setTimeout(() => { - onCommit(val); - }, 800); - }; - - const onMaxClick = async () => { - if (!showBalanceDetails || !nexusSDK || !inputs) return; - const maxBalAvailable = await nexusSDK?.calculateMaxForBridge({ - token: inputs?.token, - toChainId: inputs?.chain, - recipient: inputs?.recipient, - }); - if (!maxBalAvailable) return; - onChange(maxBalAvailable.amount); - onCommit?.(maxBalAvailable.amount); - }; - - useEffect(() => { - return () => { - if (commitTimerRef.current) { - clearTimeout(commitTimerRef.current); - commitTimerRef.current = null; - } - }; - }, []); - - return ( -
-
- { - let next = e.target.value.replaceAll(/[^0-9.]/g, ""); - const parts = next.split("."); - if (parts.length > 2) - next = parts[0] + "." + parts.slice(1).join(""); - if (next === ".") next = "0."; - onChange(next); - scheduleCommit(next); - }} - onKeyDown={(e) => { - if (e.key === "Enter") { - if (commitTimerRef.current) { - clearTimeout(commitTimerRef.current); - commitTimerRef.current = null; - } - onCommit?.(amount ?? ""); - } - }} - className="w-full border-none bg-transparent rounded-r-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none py-0 px-3 h-12!" - aria-invalid={Boolean(amount) && Number.isNaN(Number(amount))} - disabled={disabled} - /> -
- {showBalanceDetails && bridgableBalance && ( -

- {nexusSDK?.utils?.formatTokenBalance(bridgableBalance?.balance, { - symbol: - bridgableBalance?.displaySymbol ?? bridgableBalance?.symbol, - decimals: bridgableBalance?.decimals, - })} -

- )} - {showBalanceDetails && loading && !bridgableBalance && ( - - )} - {showBalanceDetails && ( - - )} -
-
- {showBalanceDetails && ( - - - - View Balance Breakdown - - -
- {bridgableBalance?.breakdown.map((chain) => { - if (Number.parseFloat(chain.balance) === 0) return null; - if ( - bridgableBalance.symbol === "USDM" && - chain.chain.id === SUPPORTED_CHAINS.MEGAETH - ) - return null; - return ( - -
-
-
- {chain.chain.name} -
- - {SHORT_CHAIN_NAME[chain.chain.id]} - -
-

- {nexusSDK?.utils?.formatTokenBalance(chain.balance, { - symbol: - bridgableBalance?.displaySymbol ?? - bridgableBalance?.symbol, - decimals: bridgableBalance?.decimals, - })} -

-
-
- ); - })} -
-
-
-
- )} -
- ); -}; - -export default AmountInput; diff --git a/apps/monad/src/components/fast-bridge/components/balance-breakdown.tsx b/apps/monad/src/components/fast-bridge/components/balance-breakdown.tsx deleted file mode 100644 index 6cdeec3..0000000 --- a/apps/monad/src/components/fast-bridge/components/balance-breakdown.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { type UserAsset } from "@avail-project/nexus-core"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../../ui/accordion"; - -interface BalanceBreakdownProps { - assetBalances: UserAsset; -} - -const BalanceBreakdown = ({ assetBalances }: BalanceBreakdownProps) => { - return ( - - -
- {assetBalances && ( -
- -

View Balance Breakdown

-
-
- )} -
- {assetBalances && ( - -
- {assetBalances?.breakdown?.map((individualBalance) => ( -
-
- {individualBalance.chain.name} -

- {individualBalance.chain.name} -

-
- -

- {individualBalance.balance} {assetBalances.symbol} -

-
- ))} -
-
- )} -
-
- ); -}; - -export default BalanceBreakdown; diff --git a/apps/monad/src/components/fast-bridge/components/chain-select.tsx b/apps/monad/src/components/fast-bridge/components/chain-select.tsx deleted file mode 100644 index 3899cd2..0000000 --- a/apps/monad/src/components/fast-bridge/components/chain-select.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { type FC, useMemo } from "react"; -import { Label } from "../../ui/label"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "../../ui/select"; -import { type SUPPORTED_CHAINS_IDS } from "@avail-project/nexus-core"; -import { cn } from "@/lib/utils"; -import { useNexus } from "../../nexus/NexusProvider"; - -interface ChainSelectProps { - selectedChain: number; - disabled?: boolean; - hidden?: boolean; - className?: string; - label?: string; - handleSelect: (chainId: SUPPORTED_CHAINS_IDS) => void; -} - -const ChainSelect: FC = ({ - selectedChain, - disabled, - hidden = false, - className, - label, - handleSelect, -}) => { - const { supportedChainsAndTokens } = useNexus(); - - const selectedChainData = useMemo(() => { - if (!supportedChainsAndTokens) return null; - return supportedChainsAndTokens.find((c) => c.id === selectedChain); - }, [selectedChain, supportedChainsAndTokens]); - - if (hidden) return null; - return ( - - ); -}; - -export default ChainSelect; diff --git a/apps/monad/src/components/fast-bridge/components/receipient-address.tsx b/apps/monad/src/components/fast-bridge/components/receipient-address.tsx deleted file mode 100644 index 0bb4192..0000000 --- a/apps/monad/src/components/fast-bridge/components/receipient-address.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; -import * as React from "react"; -import { Input } from "../../ui/input"; -import { Check, Edit } from "lucide-react"; -import { Button } from "../../ui/button"; -import { useNexus } from "../../nexus/NexusProvider"; -import { type Address } from "viem"; - -interface ReceipientAddressProps { - address?: Address; - onChange: (address: string) => void; -} - -const ReceipientAddress: React.FC = ({ - address, - onChange, -}) => { - const { nexusSDK } = useNexus(); - const [isEditing, setIsEditing] = React.useState(false); - const fallbackTruncate = (value: string, head = 6, tail = 6) => { - if (!value) return ""; - if (value.length <= head + tail) return value; - return `${value.slice(0, head)}...${value.slice(-tail)}`; - }; - const displayAddress = - address && nexusSDK?.utils?.truncateAddress - ? nexusSDK.utils.truncateAddress(address, 6, 6) - : address - ? fallbackTruncate(address, 6, 6) - : ""; - return ( -
- {isEditing ? ( -
- onChange(e.target.value)} - className="w-full text-base font-medium" - /> - -
- ) : ( -
-

Recipient Address

-
-

{displayAddress}

- - -
-
- )} -
- ); -}; - -export default ReceipientAddress; diff --git a/apps/monad/src/components/fast-bridge/components/source-breakdown.tsx b/apps/monad/src/components/fast-bridge/components/source-breakdown.tsx deleted file mode 100644 index bbe5c84..0000000 --- a/apps/monad/src/components/fast-bridge/components/source-breakdown.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { - type ReadableIntent, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "../../ui/accordion"; -import { Skeleton } from "../../ui/skeleton"; -import { useNexus } from "../../nexus/NexusProvider"; - -interface SourceBreakdownProps { - intent?: ReadableIntent; - tokenSymbol: SUPPORTED_TOKENS; - isLoading?: boolean; -} - -const SourceBreakdown = ({ - intent, - tokenSymbol, - isLoading = false, -}: SourceBreakdownProps) => { - const { nexusSDK } = useNexus(); - const spendSymbol = - intent?.token.displaySymbol ?? intent?.token.symbol ?? tokenSymbol; - return ( - - -
- {isLoading ? ( - <> -
-

You Spend

- -
-
- -
- -
-
- - ) : ( - intent?.sources && ( - <> -
-

You Spend

-

- {`${spendSymbol.toUpperCase()} on ${ - intent?.sources?.length - } ${intent?.sources?.length > 1 ? "chains" : "chain"}`} -

-
- -
-

- {nexusSDK?.utils?.formatTokenBalance(intent?.sourcesTotal, { - symbol: spendSymbol, - decimals: intent?.token?.decimals, - })} -

- -

View Sources

-
-
- - ) - )} -
- {!isLoading && intent?.sources && ( - -
- {intent?.sources?.map((source) => ( -
-
- {source?.chainName} -

{source.chainName}

-
- -

- {nexusSDK?.utils?.formatTokenBalance(source.amount, { - symbol: spendSymbol, - decimals: intent?.token?.decimals, - })} -

-
- ))} -
-
- )} -
-
- ); -}; - -export default SourceBreakdown; diff --git a/apps/monad/src/components/fast-bridge/components/token-select.tsx b/apps/monad/src/components/fast-bridge/components/token-select.tsx deleted file mode 100644 index e869231..0000000 --- a/apps/monad/src/components/fast-bridge/components/token-select.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "../../ui/select"; -import { Label } from "../../ui/label"; -import { useNexus } from "../../nexus/NexusProvider"; -import { useMemo } from "react"; - -interface TokenSelectProps { - selectedToken?: SUPPORTED_TOKENS; - selectedChain: SUPPORTED_CHAINS_IDS; - handleTokenSelect: (token: SUPPORTED_TOKENS) => void; - isTestnet?: boolean; - disabled?: boolean; - label?: string; -} - -const TokenSelect = ({ - selectedToken, - selectedChain, - handleTokenSelect, - isTestnet = false, - disabled = false, - label, -}: TokenSelectProps) => { - const { supportedChainsAndTokens } = useNexus(); - const tokenData = useMemo(() => { - return supportedChainsAndTokens - ?.filter((chain) => chain.id === selectedChain) - .flatMap((chain) => chain.tokens); - }, [selectedChain, supportedChainsAndTokens]); - - const selectedTokenData = tokenData?.find((token) => { - return token.symbol === selectedToken; - }); - - return ( - - ); -}; - -export default TokenSelect; diff --git a/apps/monad/src/components/fast-bridge/components/transaction-progress.tsx b/apps/monad/src/components/fast-bridge/components/transaction-progress.tsx deleted file mode 100644 index f877513..0000000 --- a/apps/monad/src/components/fast-bridge/components/transaction-progress.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { Check, Circle, LoaderPinwheel } from "lucide-react"; -import { type FC, memo, useMemo } from "react"; -import { - type BridgeStepType, - type SwapStepType, -} from "@avail-project/nexus-core"; - -type ProgressStep = BridgeStepType | SwapStepType; - -interface TransactionProgressProps { - timer: number; - steps: Array<{ id: number; completed: boolean; step: ProgressStep }>; - viewIntentUrl?: string; - operationType?: string; - completed?: boolean; -} - -export const getOperationText = (type: string) => { - switch (type) { - case "bridge": - return "Transaction"; - case "transfer": - return "Transferring"; - case "bridgeAndExecute": - return "Bridge & Execute"; - case "swap": - return "Swapping"; - default: - return "Processing"; - } -}; - -type DisplayStep = { id: string; label: string; completed: boolean }; - -const StepList: FC<{ steps: DisplayStep[]; currentIndex: number }> = memo( - ({ steps, currentIndex }) => { - return ( -
- {steps.map((s, idx) => { - const isCompleted = !!s.completed; - const isCurrent = currentIndex === -1 ? false : idx === currentIndex; - - let rightIcon = ; - if (isCompleted) { - rightIcon = ; - } else if (isCurrent) { - rightIcon = ( - - ); - } - - return ( -
-
- {s.label} -
- {rightIcon} -
- ); - })} -
- ); - } -); -StepList.displayName = "StepList"; - -const TransactionProgress: FC = ({ - timer, - steps, - viewIntentUrl, - operationType = "bridge", - completed = false, -}) => { - const totalSteps = Array.isArray(steps) ? steps.length : 0; - const completedSteps = Array.isArray(steps) - ? steps.reduce((acc, s) => acc + (s?.completed ? 1 : 0), 0) - : 0; - const rawPercent = totalSteps > 0 ? completedSteps / totalSteps : 0; - const percent = completed ? 1 : rawPercent; - const allCompleted = completed || percent >= 1; - const opText = getOperationText(operationType); - const headerText = allCompleted - ? `${opText} Completed` - : `${opText} In Progress...`; - const ctaText = allCompleted ? `View Explorer` : "View Intent"; - - const { effectiveSteps, currentIndex } = useMemo(() => { - const milestones = [ - "Intent verified", - "Collected on sources", - "Filled on destination", - ]; - const thresholds = milestones.map( - (_, idx) => (idx + 1) / milestones.length - ); - const displaySteps: DisplayStep[] = milestones.map((label, idx) => ({ - id: `M${idx}`, - label, - completed: idx === 0 ? timer > 0 : percent >= thresholds[idx], - })); - const current = displaySteps.findIndex((st) => !st.completed); - return { effectiveSteps: displaySteps, currentIndex: current }; - }, [percent, timer, completed]); - - return ( -
-
- {allCompleted ? ( - - ) : ( - - )} -

{headerText}

-
- - {Math.floor(timer)} - - - . - - - {String(Math.floor((timer % 1) * 1000)).padStart(3, "0")}s - -
-
- - - - {viewIntentUrl && ( - - {ctaText} - - )} -
- ); -}; - -export default TransactionProgress; diff --git a/apps/monad/src/components/fast-bridge/fast-bridge.tsx b/apps/monad/src/components/fast-bridge/fast-bridge.tsx deleted file mode 100644 index 6d6e107..0000000 --- a/apps/monad/src/components/fast-bridge/fast-bridge.tsx +++ /dev/null @@ -1,470 +0,0 @@ -"use client"; -import { type FC, useEffect, useMemo, useRef, useState } from "react"; -import { Card, CardContent } from "../ui/card"; -import ChainSelect from "./components/chain-select"; -import TokenSelect from "./components/token-select"; -import { Button } from "../ui/button"; -import { X, CheckCircle2 } from "lucide-react"; -import { useNexus } from "../nexus/NexusProvider"; -import AmountInput from "./components/amount-input"; -import FeeBreakdown from "./components/fee-breakdown"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "../ui/dialog"; -import TransactionProgress from "./components/transaction-progress"; -import AllowanceModal from "./components/allowance-modal"; -import useBridge from "./hooks/useBridge"; -import SourceBreakdown from "./components/source-breakdown"; -import { - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, -} from "@avail-project/nexus-core"; -import { type Address } from "viem"; -import { Skeleton } from "../ui/skeleton"; -import RecipientAddress from "./components/recipient-address"; -import ViewHistory from "../view-history/view-history"; -import { toast } from "sonner"; -import Decimal from "decimal.js"; - -interface FastBridgeProps { - connectedAddress?: Address; - isWalletConnected?: boolean; - onConnectWallet?: () => void; - mockIntent?: { - totalAmount?: string; - receiveAmount?: string; - totalGas?: string; - }; - prefill?: { - token: SUPPORTED_TOKENS; - chainId: SUPPORTED_CHAINS_IDS; - amount?: string; - recipient?: Address; - }; - onComplete?: () => void; - onStart?: () => void; - onError?: (message: string) => void; -} - -const FastBridge: FC = ({ - connectedAddress, - isWalletConnected, - onConnectWallet, - mockIntent, - onComplete, - onStart, - onError, - prefill, -}) => { - const { - nexusSDK, - intent, - bridgableBalance, - allowance, - network, - fetchBridgableBalance, - } = useNexus(); - const [historyRefreshNonce, setHistoryRefreshNonce] = useState(0); - - const { - inputs, - setInputs, - timer, - loading, - refreshing, - isDialogOpen, - txError, - setTxError, - handleTransaction, - reset, - filteredBridgableBalance, - startTransaction, - setIsDialogOpen, - commitAmount, - lastExplorerUrl, - steps, - status, - areInputsValid, - } = useBridge({ - prefill, - network: network ?? "mainnet", - connectedAddress, - nexusSDK, - intent, - bridgableBalance, - allowance, - onComplete: async () => { - if (onComplete) { - onComplete(); - } - const sourcesText = - intent.current?.intent?.sources?.length && - intent.current?.intent.sources.length > 0 - ? intent.current.intent.sources.map((s) => s.chainName).join(", ") - : "N/A"; - toast.success( -
-
- - Bridge Successful! -
-
-
- Source(s): {sourcesText} -
-
- Destination:{" "} - {intent.current?.intent?.destination?.chainName || "Unknown"} -
-
- Asset:{" "} - {intent.current?.intent?.token.symbol || "Unknown"} -
-
- Amount Spent:{" "} - {intent.current?.intent?.sourcesTotal - ? new Decimal(intent.current?.intent?.sourcesTotal).toFixed() - : "NaN"}{" "} - {intent.current?.intent?.token.symbol || "Unknown"} -
-
- Amount Received:{" "} - {intent.current?.intent?.destination?.amount - ? new Decimal( - intent.current?.intent?.destination?.amount, - ).toFixed() - : "NaN"}{" "} - {intent.current?.intent?.token.symbol || "Unknown"} -
-
- Total Fees:{" "} - {intent.current?.intent?.fees.total - ? new Decimal(intent.current?.intent?.fees.total).toFixed() - : "NaN"}{" "} - {intent.current?.intent?.token.symbol || "Unknown"} -
- {lastExplorerUrl ? ( - - ) : null} -
-
, - { - duration: Infinity, // Stay until dismissed - closeButton: true, - icon: null, // Remove default icon since we're adding our own - }, - ); - setHistoryRefreshNonce((prev) => prev + 1); - }, - onStart, - onError: (message) => { - toast.error(message); - if (onError) { - onError(message); - } - }, - fetchBalance: fetchBridgableBalance, - }); - const isConnected = isWalletConnected ?? Boolean(connectedAddress); - const isSdkReady = Boolean(nexusSDK); - const showSdkDetails = isSdkReady; - const receiveSymbol = - intent?.current?.intent?.token.symbol ?? - // @ts-expect-error - not possible - intent?.current?.intent?.token.displaySymbol ?? - filteredBridgableBalance?.symbol; - - const amountValue = useMemo(() => { - if (!inputs?.amount) return null; - const parsed = Number.parseFloat(inputs.amount); - return Number.isFinite(parsed) ? parsed : null; - }, [inputs?.amount]); - - const hasValidAmount = useMemo(() => { - if (amountValue === null) return false; - return amountValue > 0; - }, [amountValue]); - - const formatMockNumber = (value: number) => { - if (!Number.isFinite(value)) return "--"; - const fixed = value.toFixed(6); - return fixed.replace(/\.?0+$/, ""); - }; - - const formatWithToken = (value: string, token?: string) => { - if (!token) return value; - return `${value} ${token}`.trim(); - }; - - const tokenSuffix = - inputs?.token ?? filteredBridgableBalance?.symbol ?? "USDC"; - - const mockPreview = useMemo(() => { - if (!hasValidAmount || amountValue === null) return null; - if (mockIntent) { - return { - totalAmount: mockIntent.totalAmount ?? "--", - receiveAmount: mockIntent.receiveAmount ?? "--", - totalGas: mockIntent.totalGas ?? "--", - }; - } - const totalGas = amountValue * 0.001; - const totalAmount = amountValue + totalGas; - return { - totalAmount: formatWithToken(formatMockNumber(totalAmount), tokenSuffix), - receiveAmount: formatWithToken( - formatMockNumber(amountValue), - tokenSuffix, - ), - totalGas: formatWithToken(formatMockNumber(totalGas), tokenSuffix), - }; - }, [amountValue, hasValidAmount, mockIntent, tokenSuffix]); - - const showMockPreview = !isConnected && hasValidAmount && mockPreview; - const autoIntentTriggered = useRef(false); - - useEffect(() => { - autoIntentTriggered.current = false; - }, [inputs?.amount, inputs?.chain, inputs?.token, inputs?.recipient]); - - useEffect(() => { - if (!isConnected || !isSdkReady) return; - if (!areInputsValid) return; - if (intent.current) return; - // if (loading) return; // Removed to allow "10" fetch even if "1" is loading - if (autoIntentTriggered.current) return; - autoIntentTriggered.current = true; - void handleTransaction(); - }, [ - areInputsValid, - handleTransaction, - intent, - isConnected, - isSdkReady, - loading, - ]); - return ( - - - {showSdkDetails && ( - - )} - - setInputs({ - ...inputs, - chain, - }) - } - label="To" - disabled={!!prefill?.chainId} - /> - setInputs({ ...inputs, token })} - disabled={!!prefill?.token} - /> - setInputs({ ...inputs, amount })} - bridgableBalance={filteredBridgableBalance} - onCommit={() => void commitAmount()} - disabled={refreshing || !!prefill?.amount} - inputs={inputs} - showBalanceDetails={showSdkDetails} - /> - - setInputs({ ...inputs, recipient: address as `0x${string}` }) - } - disabled={!!prefill?.recipient} - /> - {showMockPreview && ( -
-
-

You spend

-

{mockPreview?.totalAmount}

-
-
-

You receive

-

- {mockPreview?.receiveAmount} -

-
-
-

Total gas

-

{mockPreview?.totalGas}

-
-
- )} - - {showSdkDetails && intent?.current?.intent && ( - <> - - -
-

You receive

-
- {refreshing ? ( - - ) : ( -

- {`${ - connectedAddress === inputs?.recipient - ? intent?.current?.intent?.destination?.amount - : inputs.amount - } ${receiveSymbol}`} -

- )} - {refreshing ? ( - - ) : ( -

- on {intent?.current?.intent?.destination?.chainName} -

- )} -
-
- - - )} - - {!intent.current && ( - - )} - - { - if (loading) return; - setIsDialogOpen(open); - }} - > - {intent.current && !isDialogOpen && ( -
- - - - -
- )} - - - - Transaction Progress - - {allowance.current ? ( - - ) : ( - - )} - -
- - {txError && ( -
- {txError} - -
- )} -
-
- ); -}; - -export default FastBridge; diff --git a/apps/monad/src/components/fast-bridge/hooks/useBridge.ts b/apps/monad/src/components/fast-bridge/hooks/useBridge.ts deleted file mode 100644 index 27ee49f..0000000 --- a/apps/monad/src/components/fast-bridge/hooks/useBridge.ts +++ /dev/null @@ -1,387 +0,0 @@ -import { - type BridgeStepType, - NEXUS_EVENTS, - type NexusNetwork, - NexusSDK, - type OnAllowanceHookData, - type OnIntentHookData, - // SUPPORTED_CHAINS, - type SUPPORTED_CHAINS_IDS, - type SUPPORTED_TOKENS, - type UserAsset, -} from "@avail-project/nexus-core"; -import { - useEffect, - useMemo, - useRef, - useState, - useReducer, - type RefObject, -} from "react"; -import { type Address, isAddress } from "viem"; -import { - useStopwatch, - usePolling, - useNexusError, - useTransactionSteps, - type TransactionStatus, -} from "../../common"; -import config from "../../../../config"; -import { trackBridgeSubmit } from "../../../lib/posthog"; -import { SHORT_CHAIN_NAME } from "../../common/utils/constant"; - -export interface FastBridgeState { - chain: SUPPORTED_CHAINS_IDS; - token: SUPPORTED_TOKENS; - amount?: string; - recipient?: `0x${string}`; -} - -const ALLOWED_TOKENS = new Set([ - "USDC", - "USDT", - "USDM", -]) as Set; - -interface UseBridgeProps { - network: NexusNetwork; - connectedAddress?: Address; - nexusSDK: NexusSDK | null; - intent: RefObject; - allowance: RefObject; - bridgableBalance: UserAsset[] | null; - prefill?: { - token: string; - chainId: number; - amount?: string; - recipient?: Address; - }; - onComplete?: () => void; - onStart?: () => void; - onError?: (message: string) => void; - fetchBalance: () => Promise; -} - -type BridgeState = { - inputs: FastBridgeState; - status: TransactionStatus; -}; - -type Action = - | { type: "setInputs"; payload: Partial } - | { type: "resetInputs" } - | { type: "setStatus"; payload: TransactionStatus }; - -const buildInitialInputs = ( - connectedAddress?: Address, - prefill?: { - token: string; - chainId: number; - amount?: string; - recipient?: Address; - }, -): FastBridgeState => { - const validToken = - prefill?.token && - ALLOWED_TOKENS.has(prefill.token.toUpperCase() as SUPPORTED_TOKENS) - ? (prefill.token.toUpperCase() as SUPPORTED_TOKENS) - : config.nexusPrimaryToken || "USDC"; - - const validAmount = prefill?.amount - ? (() => { - const sanitized = prefill.amount.trim(); - if (!sanitized || sanitized === "." || !/^\d*\.?\d*$/.test(sanitized)) - return undefined; - const num = Number.parseFloat(sanitized); - return Number.isNaN(num) || num <= 0 || num > 1e9 - ? undefined - : sanitized; - })() - : undefined; - - const validRecipient = - prefill?.recipient && isAddress(prefill.recipient) - ? (prefill.recipient as `0x${string}`) - : connectedAddress; - - return { - chain: config.chainId as SUPPORTED_CHAINS_IDS, - token: validToken as SUPPORTED_TOKENS, - amount: validAmount, - recipient: validRecipient, - }; -}; - -const useBridge = ({ - connectedAddress, - nexusSDK, - intent, - bridgableBalance, - prefill, - onComplete, - onStart, - onError, - fetchBalance, - allowance, -}: UseBridgeProps) => { - const handleNexusError = useNexusError(); - const initialState: BridgeState = { - inputs: buildInitialInputs(connectedAddress, prefill), - status: "idle", - }; - function reducer(state: BridgeState, action: Action): BridgeState { - switch (action.type) { - case "setInputs": - return { ...state, inputs: { ...state.inputs, ...action.payload } }; - case "resetInputs": - return { - ...state, - inputs: buildInitialInputs(connectedAddress, prefill), - }; - case "setStatus": - return { ...state, status: action.payload }; - default: - return state; - } - } - const [state, dispatch] = useReducer(reducer, initialState); - const inputs = state.inputs; - const setInputs = (next: FastBridgeState | Partial) => { - dispatch({ type: "setInputs", payload: next as Partial }); - }; - - const loading = state.status === "executing"; - const [refreshing, setRefreshing] = useState(false); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [txError, setTxError] = useState(null); - const [lastExplorerUrl, setLastExplorerUrl] = useState(""); - const commitLockRef = useRef(false); - const txnIdRef = useRef(0); - const { - steps, - onStepsList, - onStepComplete, - reset: resetSteps, - } = useTransactionSteps(); - - const areInputsValid = useMemo(() => { - const hasToken = inputs?.token !== undefined && inputs?.token !== null; - const hasChain = inputs?.chain !== undefined && inputs?.chain !== null; - const hasAmount = Boolean(inputs?.amount) && Number(inputs?.amount) > 0; - const hasValidrecipient = - Boolean(inputs?.recipient) && isAddress(inputs?.recipient as string); - return hasToken && hasChain && hasAmount && hasValidrecipient; - }, [inputs]); - - const handleTransaction = async () => { - if ( - !inputs?.amount || - !inputs?.recipient || - !inputs?.chain || - !inputs?.token - ) { - console.error("Missing required inputs"); - return; - } - - if (Number(inputs.amount) > 550) { - setTxError("Amount exceeds maximum limit of 550"); - return; - } - const currentTxnId = ++txnIdRef.current; - dispatch({ type: "setStatus", payload: "executing" }); - setTxError(null); - onStart?.(); - setLastExplorerUrl(""); - - // Track bridge submit event with PostHog - trackBridgeSubmit({ - chain: inputs.chain, - chainName: SHORT_CHAIN_NAME[inputs.chain] || `Chain ${inputs.chain}`, - tokenSymbol: inputs.token, - amount: inputs.amount, - fast_bridge: 'monad', - }); - - try { - if (!nexusSDK) { - throw new Error("Nexus SDK not initialized"); - } - const formattedAmount = nexusSDK.convertTokenReadableAmountToBigInt( - inputs?.amount, - inputs?.token, - inputs?.chain, - ); - setLastExplorerUrl(""); - const bridgeTxn = await nexusSDK.bridge( - { - token: inputs?.token, - amount: formattedAmount, - toChainId: inputs?.chain, - recipient: inputs?.recipient, - }, - { - onEvent: (event) => { - if (currentTxnId !== txnIdRef.current) return; - if (event.name === NEXUS_EVENTS.STEPS_LIST) { - const list = Array.isArray(event.args) ? event.args : []; - onStepsList(list); - } - if (event.name === NEXUS_EVENTS.STEP_COMPLETE) { - console.log("STEP_EVENT", event); - if (event.args.type === "INTENT_HASH_SIGNED") { - stopwatch.start(); - } - onStepComplete(event.args); - } - }, - }, - ); - if (currentTxnId !== txnIdRef.current) return; - - if (!bridgeTxn) { - throw new Error("Transaction rejected by user"); - } - if (bridgeTxn) { - setLastExplorerUrl(bridgeTxn.explorerUrl); - await onSuccess(); - } - } catch (error) { - if (currentTxnId !== txnIdRef.current) return; - const { message } = handleNexusError(error); - intent.current?.deny(); - intent.current = null; - allowance.current = null; - setTxError(message); - onError?.(message); - setIsDialogOpen(false); - dispatch({ type: "setStatus", payload: "error" }); - } - }; - - const onSuccess = async () => { - // Close dialog and stop timer on success - stopwatch.stop(); - dispatch({ type: "setStatus", payload: "success" }); - onComplete?.(); - intent.current = null; - allowance.current = null; - dispatch({ type: "resetInputs" }); - setRefreshing(false); - await fetchBalance(); - }; - - const filteredBridgableBalance = useMemo(() => { - return bridgableBalance?.find((bal) => bal?.symbol === inputs?.token); - }, [bridgableBalance, inputs?.token]); - - const refreshIntent = async () => { - setRefreshing(true); - try { - await intent.current?.refresh([]); - } catch (error) { - console.error("Transaction failed:", error); - } finally { - setRefreshing(false); - } - }; - - const reset = () => { - intent.current?.deny(); - intent.current = null; - allowance.current = null; - dispatch({ type: "resetInputs" }); - dispatch({ type: "setStatus", payload: "idle" }); - setRefreshing(false); - stopwatch.stop(); - stopwatch.reset(); - resetSteps(); - }; - - const startTransaction = () => { - // Reset timer for a fresh run - intent.current?.allow(); - setIsDialogOpen(true); - setTxError(null); - }; - - const commitAmount = async () => { - if (commitLockRef.current) return; - if (loading || txError || !areInputsValid) return; - - // Validate amount before proceeding - if (inputs?.amount) { - const amountStr = inputs.amount.trim(); - if (!amountStr) return; - - const amount = Number.parseFloat(amountStr); - if (Number.isNaN(amount) || amount <= 0) return; - } - - commitLockRef.current = true; - try { - await handleTransaction(); - } finally { - commitLockRef.current = false; - } - }; - - usePolling(Boolean(intent.current) && !isDialogOpen, refreshIntent, 15000); - - const stopwatch = useStopwatch({ intervalMs: 100 }); - - useEffect(() => { - if (intent.current) { - // intent.current.deny(); - intent.current = null; - } - }, [inputs]); - - useEffect(() => { - if (!isDialogOpen) { - stopwatch.stop(); - stopwatch.reset(); - // Reset all transaction state when dialog closes - if (state.status === "success" || state.status === "error") { - resetSteps(); - setLastExplorerUrl(""); - dispatch({ type: "setStatus", payload: "idle" }); - } - } - }, [isDialogOpen, stopwatch, state.status]); - - useEffect(() => { - if (txError) { - setTxError(null); - } - }, [inputs]); - - useEffect(() => { - if (connectedAddress && !inputs?.recipient) { - setInputs({ recipient: connectedAddress as `0x${string}` }); - } - }, [connectedAddress, inputs?.recipient]); - - return { - inputs, - setInputs, - timer: stopwatch.seconds, - setIsDialogOpen, - setTxError, - loading, - refreshing, - isDialogOpen, - txError, - handleTransaction, - reset, - filteredBridgableBalance, - startTransaction, - commitAmount, - lastExplorerUrl, - steps, - status: state.status, - areInputsValid, - }; -}; - -export default useBridge; diff --git a/apps/monad/src/components/hero-section.tsx b/apps/monad/src/components/hero-section.tsx deleted file mode 100644 index 89ca4ff..0000000 --- a/apps/monad/src/components/hero-section.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function HeroSection() { - return ( -
- Bridge your unified USDC/USDT balances across 12+ chains, to Avalanche, instantly! -
- ); -} diff --git a/apps/monad/src/components/navbar.tsx b/apps/monad/src/components/navbar.tsx deleted file mode 100644 index 22253e1..0000000 --- a/apps/monad/src/components/navbar.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; -import { ConnectKitButton } from "connectkit"; -import config from "../../config"; -import AvailLogo from "/avail_logo.svg"; - -export default function Navbar() { - return ( - - ); -} diff --git a/apps/monad/src/components/ui/accordion.tsx b/apps/monad/src/components/ui/accordion.tsx deleted file mode 100644 index 75b4f79..0000000 --- a/apps/monad/src/components/ui/accordion.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDownIcon } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -function Accordion({ - ...props -}: React.ComponentProps) { - return ; -} - -function AccordionItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function AccordionTrigger({ - className, - children, - hideChevron = true, - containerClassName = "w-full", - ...props -}: React.ComponentProps & { - hideChevron?: boolean; - containerClassName?: string; -}) { - return ( - - svg]:rotate-180", - className, - )} - {...props} - > - {children} - {!hideChevron && ( - - )} - - - ); -} - -function AccordionContent({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - -
{children}
-
- ); -} - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/apps/monad/src/components/ui/bubbles-animation.tsx b/apps/monad/src/components/ui/bubbles-animation.tsx deleted file mode 100644 index 6c4b772..0000000 --- a/apps/monad/src/components/ui/bubbles-animation.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client"; - -import React, { useEffect, useRef } from "react"; - -/** - * BubbleAnimation - * Minimal React component that implements ONLY the floating bubble animation - * from the referenced CodePen. Modified to render oblong (wider) bubbles. - */ -export default function BubbleAnimation({ - bubbleColor = "rgb(33, 150, 243)", - durationMs = 2000, - width = "max(500px, 50vw)", // wider - height = "max(200px, 20vw)", // shorter -}) { - const wrapperRef = useRef(null); - - useEffect(() => { - const wrapper = wrapperRef.current as HTMLElement | null; - if (!wrapper) return; - - const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); - const reduced = mediaQuery.matches; - - const timeouts = new Set(); - - const animateBubble = (x: number) => { - if (reduced) return; - const bubble = document.createElement("div"); - bubble.className = "bubble"; - bubble.style.left = `${x}px`; - bubble.style.backgroundColor = bubbleColor; - bubble.style.width = width; - bubble.style.height = height; - wrapper.appendChild(bubble); - const to = window.setTimeout(() => { - bubble.remove(); - timeouts.delete(to); - }, durationMs); - timeouts.add(to); - }; - - const handleMove = (e: MouseEvent) => animateBubble(e.clientX); - window.addEventListener("mousemove", handleMove); - - return () => { - window.removeEventListener("mousemove", handleMove); - timeouts.forEach((id) => window.clearTimeout(id)); - timeouts.clear(); - Array.from(wrapper.children).forEach((el) => el.remove()); - }; - }, [bubbleColor, durationMs, width, height]); - - return ( -
- {/* Animation layer only */} - - ); -} - diff --git a/apps/monad/src/components/ui/button.tsx b/apps/monad/src/components/ui/button.tsx deleted file mode 100644 index 71ab81f..0000000 --- a/apps/monad/src/components/ui/button.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-sm": "size-8", - "icon-lg": "size-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -); - -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean; - }) { - const Comp = asChild ? Slot : "button"; - - return ( - - ); -} - -export { Button, buttonVariants }; diff --git a/apps/monad/src/components/ui/dialog.tsx b/apps/monad/src/components/ui/dialog.tsx deleted file mode 100644 index d9ccec9..0000000 --- a/apps/monad/src/components/ui/dialog.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client" - -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Dialog({ - ...props -}: React.ComponentProps) { - return -} - -function DialogTrigger({ - ...props -}: React.ComponentProps) { - return -} - -function DialogPortal({ - ...props -}: React.ComponentProps) { - return -} - -function DialogClose({ - ...props -}: React.ComponentProps) { - return -} - -function DialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function DialogContent({ - className, - children, - showCloseButton = true, - ...props -}: React.ComponentProps & { - showCloseButton?: boolean -}) { - return ( - - - - {children} - {showCloseButton && ( - - - Close - - )} - - - ) -} - -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function DialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -} diff --git a/apps/monad/src/components/ui/input.tsx b/apps/monad/src/components/ui/input.tsx deleted file mode 100644 index 0316cc4..0000000 --- a/apps/monad/src/components/ui/input.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ); -} - -export { Input }; diff --git a/apps/monad/src/components/ui/label.tsx b/apps/monad/src/components/ui/label.tsx deleted file mode 100644 index fb5fbc3..0000000 --- a/apps/monad/src/components/ui/label.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client" - -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" - -import { cn } from "@/lib/utils" - -function Label({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { Label } diff --git a/apps/monad/src/components/ui/select.tsx b/apps/monad/src/components/ui/select.tsx deleted file mode 100644 index 91c5bdf..0000000 --- a/apps/monad/src/components/ui/select.tsx +++ /dev/null @@ -1,185 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as SelectPrimitive from "@radix-ui/react-select"; -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -function Select({ - ...props -}: React.ComponentProps) { - return ; -} - -function SelectGroup({ - ...props -}: React.ComponentProps) { - return ; -} - -function SelectValue({ - ...props -}: React.ComponentProps) { - return ; -} - -function SelectTrigger({ - className, - size = "default", - children, - ...props -}: React.ComponentProps & { - size?: "sm" | "default"; -}) { - return ( - - {children} - - - - - ); -} - -function SelectContent({ - className, - children, - position = "popper", - ...props -}: React.ComponentProps) { - return ( - - - - - {children} - - - - - ); -} - -function SelectLabel({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function SelectItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ); -} - -function SelectSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function SelectScrollUpButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -function SelectScrollDownButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -}; diff --git a/apps/monad/src/components/ui/separator.tsx b/apps/monad/src/components/ui/separator.tsx deleted file mode 100644 index be47ba4..0000000 --- a/apps/monad/src/components/ui/separator.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Separator({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -export { Separator }; diff --git a/apps/monad/src/components/ui/sonner.tsx b/apps/monad/src/components/ui/sonner.tsx deleted file mode 100644 index f96a98d..0000000 --- a/apps/monad/src/components/ui/sonner.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import { useTheme } from "next-themes"; -import { Toaster as Sonner, type ToasterProps } from "sonner"; - -const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); - - return ( - - ); -}; - -export { Toaster }; diff --git a/apps/monad/src/components/ui/tooltip.tsx b/apps/monad/src/components/ui/tooltip.tsx deleted file mode 100644 index 99aefa0..0000000 --- a/apps/monad/src/components/ui/tooltip.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; - -import { cn } from "@/lib/utils"; - -function TooltipProvider({ - delayDuration = 0, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function Tooltip({ - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -function TooltipTrigger({ - ...props -}: React.ComponentProps) { - return ; -} - -function TooltipContent({ - className, - sideOffset = 0, - children, - ...props -}: React.ComponentProps) { - return ( - - - {children} - - - - ); -} - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/apps/monad/src/components/view-history/hooks/useViewHistory.ts b/apps/monad/src/components/view-history/hooks/useViewHistory.ts deleted file mode 100644 index ea4cece..0000000 --- a/apps/monad/src/components/view-history/hooks/useViewHistory.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { type RFF } from "@avail-project/nexus-core"; -import { useNexus } from "../../nexus/NexusProvider"; -import { useCallback, useEffect, useState } from "react"; - -const ITEMS_PER_PAGE = 10; - -function formatExpiryDate(timestamp: number) { - const date = new Date(timestamp * 1000); - const formatted = date.toLocaleString("en-US", { - month: "short", - day: "2-digit", - year: "numeric", - }); - return formatted.replace(" ", ", "); -} - -const useViewHistory = () => { - const { nexusSDK } = useNexus(); - const [history, setHistory] = useState(null); - const [displayedHistory, setDisplayedHistory] = useState([]); - const [page, setPage] = useState(0); - const [hasMore, setHasMore] = useState(true); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const [sentinelNode, setSentinelNode] = useState(null); - - const observerTarget = useCallback((node: HTMLDivElement | null) => { - setSentinelNode(node); - }, []); - - const fetchIntentHistory = useCallback(async () => { - if (!nexusSDK) return; - - try { - const fetchedHistory = await nexusSDK.getMyIntents(); - const normalizedHistory = fetchedHistory ?? []; - - setHistory(normalizedHistory); - setDisplayedHistory(normalizedHistory.slice(0, ITEMS_PER_PAGE)); - setPage(0); - setHasMore(normalizedHistory.length > ITEMS_PER_PAGE); - setIsLoadingMore(false); - } catch (error) { - console.error("Error fetching intent history:", error); - } - }, [nexusSDK]); - - useEffect(() => { - if (!history) { - void fetchIntentHistory(); - } - }, [history, fetchIntentHistory]); - - const loadMore = useCallback(() => { - if (!history || isLoadingMore || !hasMore) return; - setIsLoadingMore(true); - - setTimeout(() => { - const nextPage = page + 1; - const startIndex = nextPage * ITEMS_PER_PAGE; - const endIndex = startIndex + ITEMS_PER_PAGE; - const newItems = history.slice(startIndex, endIndex); - - if (newItems.length > 0) { - setDisplayedHistory((prev) => [...prev, ...newItems]); - setPage(nextPage); - setHasMore(endIndex < history.length); - } else { - setHasMore(false); - } - - setIsLoadingMore(false); - }, 300); - }, [history, page, isLoadingMore, hasMore]); - - useEffect(() => { - if (!sentinelNode) { - return; - } - - const rootElement = sentinelNode.parentElement; - - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore && !isLoadingMore) { - loadMore(); - } - }, - { threshold: 0.1, root: rootElement ?? null }, - ); - - observer.observe(sentinelNode); - - return () => { - observer.disconnect(); - }; - }, [sentinelNode, loadMore, hasMore, isLoadingMore, displayedHistory.length]); - - const getStatus = (pastIntent: RFF) => { - if (pastIntent?.fulfilled) { - return "Fulfilled"; - } else if (pastIntent?.deposited) { - return "Deposited"; - } else if (pastIntent?.refunded) { - return "Refunded"; - } else { - return "Failed"; - } - }; - - return { - history, - displayedHistory, - page, - hasMore, - isLoadingMore, - getStatus, - observerTarget, - ITEMS_PER_PAGE, - formatExpiryDate, - fetchIntentHistory, - }; -}; - -export default useViewHistory; diff --git a/apps/monad/src/components/view-history/view-history.tsx b/apps/monad/src/components/view-history/view-history.tsx deleted file mode 100644 index b58dd82..0000000 --- a/apps/monad/src/components/view-history/view-history.tsx +++ /dev/null @@ -1,276 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTrigger, - DialogTitle, -} from "@/components/ui/dialog"; -import { Clock, LoaderPinwheel, SquareArrowOutUpRight } from "lucide-react"; -import { type RFF } from "@avail-project/nexus-core"; -import { cn } from "@/lib/utils"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; -import useViewHistory from "./hooks/useViewHistory"; -import { TOKEN_IMAGES } from "../common"; - -const SourceChains = ({ sources }: { sources: RFF["sources"] }) => { - return ( -
- {sources?.map((source, index) => ( -
0 && "-ml-2", - )} - style={{ zIndex: sources.length - index }} - > - {source?.chain?.name} -
- ))} -
- ); -}; - -const StatusBadge = ({ status }: { status: string }) => { - const getVariant = (status: string) => { - if (status === "Fulfilled") { - return "default"; - } else if (status === "Deposited") { - return "secondary"; - } else if (status === "Refunded") { - return "outline"; - } else if (status === "Failed") { - return "destructive"; - } else { - return "default"; - } - }; - - return ( - -

{status}

-
- ); -}; - -const DestinationToken = ({ - destination, -}: { - destination: RFF["destinations"]; -}) => { - return ( -
- {destination.map((dest, index) => ( -
0 && "-ml-2", - )} - style={{ zIndex: destination.length - index }} - > - {dest.token.symbol} -
- ))} -
- ); -}; - -const ViewHistory = ({ - viewAsModal = true, - className, - refreshNonce, -}: { - viewAsModal?: boolean; - className?: string; - refreshNonce?: number; -}) => { - const { - history, - displayedHistory, - hasMore, - isLoadingMore, - getStatus, - observerTarget, - ITEMS_PER_PAGE, - formatExpiryDate, - fetchIntentHistory, - } = useViewHistory(); - - useEffect(() => { - if (!refreshNonce) return; - void fetchIntentHistory(); - }, [refreshNonce, fetchIntentHistory]); - - const renderHistoryContent = () => { - if (displayedHistory.length > 0) { - return ( - <> - {displayedHistory?.map((pastIntent) => ( - -
-
- -
-

- {pastIntent?.destinations - .map((d) => d?.token?.symbol) - .join(", ")} -

-

- Intent #{pastIntent?.id} -

-
-
- -
- - - -
-
- -
-
- -
-
-
- {pastIntent?.destinationChain?.name} -
-
- -
-
-

Expiry

-

- {formatExpiryDate(pastIntent?.expiry)} -

-
- - - -
-
- - ))} - - {hasMore && ( -
- {isLoadingMore && ( -
- - Loading more... -
- )} -
- )} - - {!hasMore && displayedHistory?.length > ITEMS_PER_PAGE && ( -
-

- No more transactions to load -

-
- )} - - ); - } - - if (history === null) { - return ( -
-
-
- -
-
-

Loading your history

-

- Fetching your past transactions... -

-
-
- ); - } - - return ( -
- -
-

No history yet

-

- Your transaction history will appear here -

-
-
- ); - }; - - if (!viewAsModal) { - return ( -
- {renderHistoryContent()} -
- ); - } - - return ( - - - - - - - - Transaction History - - -
- {renderHistoryContent()} -
-
-
- ); -}; - -export default ViewHistory; diff --git a/apps/monad/src/components/walletConnect.tsx b/apps/monad/src/components/walletConnect.tsx deleted file mode 100644 index 2dcb18f..0000000 --- a/apps/monad/src/components/walletConnect.tsx +++ /dev/null @@ -1,161 +0,0 @@ -"use client"; -import * as React from "react"; -import type { EthereumProvider } from "@avail-project/nexus-core"; -import { useAccount } from "wagmi"; -import { useNexus } from "./nexus/NexusProvider"; -import { toast } from "sonner"; - -interface PreviewPanelProps { - children: React.ReactNode; -} - -export function PreviewPanel({ children }: Readonly) { - const [loading, setLoading] = React.useState(false); - const [initError, setInitError] = React.useState(null); - const { status, connector, address } = useAccount(); - const { nexusSDK, handleInit, deinitializeNexus, setIntent, setAllowance } = - useNexus(); - const prevAddressRef = React.useRef(address); - - const initializeNexus = React.useCallback(async () => { - if (loading || nexusSDK) return; // Prevent multiple calls - - console.log("[Nexus Init] Starting initialization..."); - console.log("[Nexus Init] Connector:", connector); - console.log("[Nexus Init] Connector name:", connector?.name); - console.log("[Nexus Init] Connector type:", connector?.type); - - setLoading(true); - setInitError(null); - - // Create a timeout promise - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error("Nexus initialization timed out after 30 seconds")); - }, 30000); // 30 second timeout - }); - - try { - if (!connector) { - throw new Error("No connector available"); - } - - console.log("[Nexus Init] Getting provider from connector..."); - const provider = (await connector.getProvider()) as EthereumProvider; - - console.log("[Nexus Init] Provider:", provider); - console.log("[Nexus Init] Provider type:", typeof provider); - console.log( - "[Nexus Init] Provider has request:", - typeof provider?.request === "function", - ); - - if (!provider) { - throw new Error("No provider available from connector"); - } - - if (typeof provider.request !== "function") { - throw new Error( - "Provider does not have a request method (not EIP-1193 compliant)", - ); - } - - console.log("[Nexus Init] Provider validated, calling handleInit..."); - - // Race between initialization and timeout - await Promise.race([handleInit(provider), timeoutPromise]); - - console.log("[Nexus Init] Initialization successful!"); - } catch (error) { - console.error("[Nexus Init] Initialization failed:", error); - const errorMessage = (error as Error)?.message || "Unknown error"; - setInitError(errorMessage); - toast.error(`Failed to initialize Nexus: ${errorMessage}`); - } finally { - setLoading(false); - } - }, [connector, handleInit, loading, nexusSDK]); - - // Handle wallet disconnection - clear Nexus state and balances - React.useEffect(() => { - if (status === "disconnected" && nexusSDK) { - deinitializeNexus(); - setIntent(null); - setAllowance(null); - prevAddressRef.current = undefined; - } - if (status === "disconnected") { - setInitError(null); - } - }, [status, nexusSDK, deinitializeNexus, setIntent, setAllowance]); - - // Handle account change - reinitialize Nexus when account address changes - React.useEffect(() => { - if ( - status === "connected" && - address && - address !== prevAddressRef.current - ) { - const previousAddress = prevAddressRef.current; - const currentAddress = address; - prevAddressRef.current = address; - - // If account changed and Nexus is initialized, reinitialize with new account - if (nexusSDK && previousAddress !== undefined) { - // Account changed - deinitialize and reinitialize - deinitializeNexus().then(() => { - // Small delay to ensure deinit completes, then reinitialize - setTimeout(() => { - // Check if still connected and address hasn't changed again - if ( - currentAddress === prevAddressRef.current && - !loading && - !initError - ) { - initializeNexus(); - } - }, 100); - }); - } - } else if (status === "connected" && address && !prevAddressRef.current) { - // First connection - set the address - prevAddressRef.current = address; - } - }, [ - status, - nexusSDK, - address, - initError, - initializeNexus, - deinitializeNexus, - loading, - ]); - - // Auto-initialize Nexus when wallet is connected and address is available - React.useEffect(() => { - if ( - status === "connected" && - !nexusSDK && - !loading && - !initError && - address && - connector - ) { - initializeNexus(); - } - }, [ - status, - nexusSDK, - initError, - address, - connector, - initializeNexus, - loading, - ]); - - return ( -
- {children} -
- ); -} diff --git a/apps/monad/src/favicon.ico b/apps/monad/src/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/apps/monad/src/favicon.ico and /dev/null differ diff --git a/apps/monad/src/index.css b/apps/monad/src/index.css deleted file mode 100644 index 6a02ea1..0000000 --- a/apps/monad/src/index.css +++ /dev/null @@ -1,152 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -body, -#root { - height: 100svh; - width: 100svw; - overflow-x: hidden; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - /* button { - background-color: #f9f9f9; - } */ -} - -@theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/apps/monad/src/lib/posthog.ts b/apps/monad/src/lib/posthog.ts deleted file mode 100644 index aa51bd9..0000000 --- a/apps/monad/src/lib/posthog.ts +++ /dev/null @@ -1,76 +0,0 @@ -import posthog from 'posthog-js'; - -const DEFAULT_POSTHOG_KEY = 'phc_UD6lQU3PEw1d8oo8E17rJLmRAR7kxJbQ5OseHuCvi7N'; -const DEFAULT_POSTHOG_HOST = 'https://us.i.posthog.com'; - -let isInitialized = false; - -/** - * Initialize PostHog analytics - */ -export function initPostHog(options?: { - apiKey?: string; - apiHost?: string; - debug?: boolean; -}): void { - if (isInitialized) return; - - const apiKey = options?.apiKey || DEFAULT_POSTHOG_KEY; - const apiHost = options?.apiHost || DEFAULT_POSTHOG_HOST; - const debug = options?.debug ?? true; // Always enable debug for troubleshooting - - console.log('[PostHog] Initializing with host:', apiHost); - - posthog.init(apiKey, { - api_host: apiHost, - person_profiles: 'identified_only', - autocapture: false, - capture_pageview: false, - capture_pageleave: true, - persistence: 'localStorage', // Use localStorage for persistence - loaded: (ph) => { - console.log('[PostHog] Loaded successfully'); - if (debug) { - ph.debug(); - } - }, - }); - - isInitialized = true; -} - -/** - * Capture a bridge submit event - */ -export interface BridgeSubmitEventProperties { - chain: string | number; - chainName: string; - tokenSymbol: string; - amount: string; - fast_bridge: 'megaeth' | 'citrea' | 'monad'; -} - -export function trackBridgeSubmit(properties: BridgeSubmitEventProperties): void { - if (!isInitialized) { - console.warn('[PostHog] Not initialized, initializing now...'); - initPostHog(); - } - - console.log('[PostHog] Capturing event: nexus_fast_bridge_demo_submit', properties); - posthog.capture('nexus_fast_bridge_demo_submit', properties); -} - -/** - * Generic capture wrapper - */ -export function capture(event: string, properties?: Record): void { - if (!isInitialized) { - console.warn('[PostHog] Not initialized, initializing now...'); - initPostHog(); - } - - console.log('[PostHog] Capturing event:', event, properties); - posthog.capture(event, properties); -} - -export { posthog }; diff --git a/apps/monad/src/lib/transaction-utils.ts b/apps/monad/src/lib/transaction-utils.ts deleted file mode 100644 index 32eab01..0000000 --- a/apps/monad/src/lib/transaction-utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -export const getOperationText = (type: string) => { - switch (type) { - case "bridge": - return "Transaction"; - case "transfer": - return "Transferring"; - case "bridgeAndExecute": - return "Bridge & Execute"; - case "swap": - return "Swapping"; - default: - return "Processing"; - } -}; - -export const getStatusText = (type: string, operationType: string) => { - const opText = getOperationText(operationType); - - switch (type) { - case "INTENT_ACCEPTED": - return "Intent Accepted"; - case "INTENT_HASH_SIGNED": - return "Signing Transaction"; - case "INTENT_SUBMITTED": - return "Submitting Transaction"; - case "INTENT_COLLECTION": - return "Collecting Confirmations"; - case "INTENT_COLLECTION_COMPLETE": - return "Confirmations Complete"; - case "APPROVAL": - return "Approving"; - case "TRANSACTION_SENT": - return "Sending Transaction"; - case "RECEIPT_RECEIVED": - return "Receipt Received"; - case "TRANSACTION_CONFIRMED": - case "INTENT_FULFILLED": - return `${opText} Complete`; - default: - return `Processing ${opText}`; - } -}; diff --git a/apps/monad/src/lib/utils.ts b/apps/monad/src/lib/utils.ts deleted file mode 100644 index f9923e8..0000000 --- a/apps/monad/src/lib/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} -export function absoluteUrl(path: string) { - return `${process.env.NEXT_PUBLIC_BASE_URL}${path}`; -} diff --git a/apps/monad/src/main.tsx b/apps/monad/src/main.tsx index e98387c..0802495 100644 --- a/apps/monad/src/main.tsx +++ b/apps/monad/src/main.tsx @@ -1,46 +1,3 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import "./index.css"; -import App from "./App"; -import { initPostHog } from "./lib/posthog"; +import { bootstrapApp } from "@/bootstrap"; -// Initialize PostHog analytics -initPostHog(); - -// Clean up WalletConnect IndexedDB before app loads to prevent structure errors -const cleanupWalletConnectSubscription = () => { - try { - const dbName = "WALLET_CONNECT_V2_INDEXED_DB"; - - // Delete the entire database to prevent structure errors - const deleteRequest = indexedDB.deleteDatabase(dbName); - - deleteRequest.onsuccess = () => { - console.log( - "[WalletConnect] Database deleted successfully, will be recreated fresh", - ); - }; - - deleteRequest.onerror = () => { - console.debug( - "[WalletConnect] Database deletion failed or DB doesn't exist", - ); - }; - - deleteRequest.onblocked = () => { - console.debug("[WalletConnect] Database deletion blocked, may be in use"); - }; - } catch (error) { - // Ignore errors - this is a cleanup operation - console.debug("[WalletConnect] Cleanup skipped:", error); - } -}; - -// Run cleanup before rendering -cleanupWalletConnectSubscription(); - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); +bootstrapApp(); diff --git a/apps/monad/src/runtime.ts b/apps/monad/src/runtime.ts new file mode 100644 index 0000000..8489e92 --- /dev/null +++ b/apps/monad/src/runtime.ts @@ -0,0 +1,30 @@ +import type { ChainFeatures } from "@/types/runtime"; +import config from "../config"; + +export const appConfig = config; + +export const chainFeatures: ChainFeatures = { + slug: "monad", + analyticsFastBridgeKey: "monad", + maxBridgeAmount: 550, + walletInitDelayMs: 0, + showFluffeyMascot: false, + showPromoBanner: false, + pageDescription: + "Experience the fastest way to bridge assets to Monad. Avail Fast Bridge facilitates instant stablecoin and token transfers from Ethereum, Base, Arbitrum, and Optimism. Built on Avail Nexus, it ensures your transition to the Monad ecosystem is secure and seamless.", + mapUsdmDisplaySymbolToUsdc: false, + mapUsdmToUsdcBalance: false, + tokenLogoOverrideBySymbol: { + USDM: "https://raw.githubusercontent.com/availproject/nexus-assets/refs/heads/main/tokens/usdm/logo.png", + }, + denyIntentOnReset: true, + tokenDenyListByChainId: {}, + allowanceLogoOverrideByChainId: {}, + amountInputUseCalculatedMaxHeader: false, + amountInputShowDestinationBadge: false, + amountInputUseSourceSymbolInBreakdown: false, + hideMegaethSourceForUsdm: true, + feeBreakdownHideGasSupplied: false, + feeBreakdownKeepZeroRows: false, + dialogShowCloseButton: true, +}; diff --git a/apps/monad/src/vite-env.d.ts b/apps/monad/src/vite-env.d.ts index 04436df..1a8b0eb 100644 --- a/apps/monad/src/vite-env.d.ts +++ b/apps/monad/src/vite-env.d.ts @@ -3,57 +3,56 @@ import { SUPPORTED_CHAINS } from "@avail-project/nexus-core"; export interface AppEnv { - readonly VITE_APP_BASE_PATH?: string; - readonly VITE_CONFIG_CHAIN_ID: number; - readonly VITE_CONFIG_CHAIN_NAME: string; - readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_NAME: string; - readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_SYMBOL: string; - readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_DECIMALS: number; - readonly VITE_CONFIG_CHAIN_RPC_URL: string; - readonly VITE_CONFIG_CHAIN_BLOCK_EXPLORER_URL: string; - readonly VITE_CONFIG_CHAIN_TESTNET: boolean; - readonly VITE_CONFIG_CHAIN_USE_CHAIN_LOGO: boolean; - readonly VITE_CONFIG_CHAIN_ICON_URL: string; - readonly VITE_CONFIG_CHAIN_LOGO_URL: string; - readonly VITE_CONFIG_CHAIN_GIF_URL: string; - readonly VITE_CONFIG_CHAIN_GIF_ALT: string; - readonly VITE_CONFIG_CHAIN_HERO_TEXT: string; - readonly VITE_CONFIG_APP_TITLE: string; - readonly VITE_CONFIG_APP_DESCRIPTION: string; - readonly VITE_CONFIG_PRIMARY_COLOR: string; - readonly VITE_CONFIG_SECONDARY_COLOR: string; - readonly VITE_CONFIG_NEXUS_NETWORK: 'mainnet' | 'testnet' | 'devnet'; - readonly VITE_CONFIG_NEXUS_SUPPORTED_CHAIN: SUPPORTED_CHAINS; - readonly VITE_CONFIG_NEXUS_PRIMARY_TOKEN: string; + readonly VITE_APP_BASE_PATH?: string; + readonly VITE_CONFIG_APP_DESCRIPTION: string; + readonly VITE_CONFIG_APP_META_BACKGROUND_COLOR: string; + readonly VITE_CONFIG_APP_META_CANONICAL_URL: string; + readonly VITE_CONFIG_APP_META_DESCRIPTION: string; + readonly VITE_CONFIG_APP_META_FAVICON_URL: string; + readonly VITE_CONFIG_APP_META_IMAGE_URL: string; + readonly VITE_CONFIG_APP_META_THEME_COLOR: string; - readonly VITE_CONFIG_APP_META_TITLE: string; - readonly VITE_CONFIG_APP_META_DESCRIPTION: string; - readonly VITE_CONFIG_APP_META_CANONICAL_URL: string; - readonly VITE_CONFIG_APP_META_FAVICON_URL: string; - readonly VITE_CONFIG_APP_META_THEME_COLOR: string; - readonly VITE_CONFIG_APP_META_IMAGE_URL: string; - readonly VITE_CONFIG_APP_META_BACKGROUND_COLOR: string; + readonly VITE_CONFIG_APP_META_TITLE: string; + readonly VITE_CONFIG_APP_TITLE: string; + readonly VITE_CONFIG_CHAIN_BLOCK_EXPLORER_URL: string; + readonly VITE_CONFIG_CHAIN_GIF_ALT: string; + readonly VITE_CONFIG_CHAIN_GIF_URL: string; + readonly VITE_CONFIG_CHAIN_HERO_TEXT: string; + readonly VITE_CONFIG_CHAIN_ICON_URL: string; + readonly VITE_CONFIG_CHAIN_ID: number; + readonly VITE_CONFIG_CHAIN_LOGO_URL: string; + readonly VITE_CONFIG_CHAIN_NAME: string; + readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_DECIMALS: number; + readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_NAME: string; + readonly VITE_CONFIG_CHAIN_NATIVE_CURRENCY_SYMBOL: string; + readonly VITE_CONFIG_CHAIN_RPC_URL: string; + readonly VITE_CONFIG_CHAIN_TESTNET: boolean; + readonly VITE_CONFIG_CHAIN_USE_CHAIN_LOGO: boolean; + readonly VITE_CONFIG_NEXUS_NETWORK: "mainnet" | "testnet" | "devnet"; + readonly VITE_CONFIG_NEXUS_PRIMARY_TOKEN: string; + readonly VITE_CONFIG_NEXUS_SUPPORTED_CHAIN: SUPPORTED_CHAINS; + readonly VITE_CONFIG_PRIMARY_COLOR: string; + readonly VITE_CONFIG_SECONDARY_COLOR: string; } declare global { + interface ImportMetaEnv extends AppEnv { + readonly VITE_ARBITRUM_RPC: string; + readonly VITE_AVALANCHE_RPC: string; + readonly VITE_BASE: string; + readonly VITE_BASE_RPC: string; + readonly VITE_KAIA_RPC: string; + readonly VITE_MAINNET_RPC: string; + readonly VITE_MEGAETH_RPC: string; + readonly VITE_MONAD_RPC: string; + readonly VITE_OPTIMISM_RPC: string; + readonly VITE_POLYGON_RPC: string; + readonly VITE_SCROLL_RPC: string; + readonly VITE_SOPHON_RPC: string; + readonly VITE_WALLET_CONNECT_ID: string; + } - interface ImportMetaEnv extends AppEnv { - readonly VITE_WALLET_CONNECT_ID: string; - readonly VITE_MAINNET_RPC: string; - readonly VITE_BASE_RPC: string; - readonly VITE_ARBITRUM_RPC: string; - readonly VITE_OPTIMISM_RPC: string; - readonly VITE_POLYGON_RPC: string; - readonly VITE_SCROLL_RPC: string; - readonly VITE_AVALANCHE_RPC: string; - readonly VITE_SOPHON_RPC: string; - readonly VITE_KAIA_RPC: string; - readonly VITE_MONAD_RPC: string; - readonly VITE_MEGAETH_RPC: string; - readonly VITE_BASE: string; - } - - interface ImportMeta { - readonly env: ImportMetaEnv; - } + interface ImportMeta { + readonly env: ImportMetaEnv; + } } diff --git a/apps/monad/tsconfig.app.json b/apps/monad/tsconfig.app.json index afe17f6..c6a071f 100644 --- a/apps/monad/tsconfig.app.json +++ b/apps/monad/tsconfig.app.json @@ -24,7 +24,8 @@ "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": ["../../packages/fast-bridge-app/src/*"], + "@fastbridge/runtime": ["./src/runtime.ts"] } }, "include": ["src"] diff --git a/apps/monad/tsconfig.json b/apps/monad/tsconfig.json index fec8c8e..5bfdf37 100644 --- a/apps/monad/tsconfig.json +++ b/apps/monad/tsconfig.json @@ -8,6 +8,7 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"] - } + }, + "strictNullChecks": true } } diff --git a/apps/monad/vite.config.ts b/apps/monad/vite.config.ts index 34caa41..f2aae83 100644 --- a/apps/monad/vite.config.ts +++ b/apps/monad/vite.config.ts @@ -1,12 +1,12 @@ +import { writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import tailwindcss from "@tailwindcss/vite"; -import { nodePolyfills } from "vite-plugin-node-polyfills"; -import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; -import { writeFileSync } from "node:fs"; +import { defineConfig, loadEnv } from "vite"; +import { nodePolyfills } from "vite-plugin-node-polyfills"; +import { getConfig } from "./get-config"; import type { AppEnv } from "./src/vite-env.d"; -import { getConfig } from "./getConfig"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -18,31 +18,31 @@ export default defineConfig(({ mode }) => { const config = getConfig(env); const manifestContent = { - "name": config.meta.title, - "short_name": config.appTitle, - "description": config.meta.description, - "start_url": base, - "display": "standalone", - "background_color": config.meta.backgroundColor, - "theme_color": config.meta.themeColor, - "orientation": "portrait-primary", - "icons": [ + name: config.meta.title, + short_name: config.appTitle, + description: config.meta.description, + start_url: base, + display: "standalone", + background_color: config.meta.backgroundColor, + theme_color: config.meta.themeColor, + orientation: "portrait-primary", + icons: [ { - "src": config.meta.faviconUrl, - "sizes": "31x32", - "type": "image/png", - "purpose": "any" + src: config.meta.faviconUrl, + sizes: "31x32", + type: "image/png", + purpose: "any", }, { - "src": config.meta.imageUrl, - "sizes": "2444x1256", - "type": "image/png", - "purpose": "any" - } - ] + src: config.meta.imageUrl, + sizes: "2444x1256", + type: "image/png", + purpose: "any", + }, + ], }; - const outputPath = path.resolve(__dirname, 'dist', 'manifest.json'); + const outputPath = path.resolve(__dirname, "dist", "manifest.json"); return { base, @@ -55,19 +55,19 @@ export default defineConfig(({ mode }) => { }), // Create manifest.json at build { - name: 'generate-manifest', + name: "generate-manifest", writeBundle() { try { writeFileSync(outputPath, JSON.stringify(manifestContent, null, 2)); console.log(`Generated dynamic manifest.json at ${outputPath}`); } catch (err) { - console.error('Error generating manifest.json', err); + console.error("Error generating manifest.json", err); } - } + }, }, // Update title and meta of index.html at build { - name: 'html-transform', + name: "html-transform", transformIndexHtml(html) { // Replace placeholders with actual values return html @@ -77,13 +77,14 @@ export default defineConfig(({ mode }) => { .replaceAll(/%= APP_FAVICON_URL =%/g, config.meta.faviconUrl) .replaceAll(/%= APP_THEME_COLOR =%/g, config.meta.themeColor) .replaceAll(/%= APP_META_IMAGE_URL =%/g, config.meta.imageUrl) - .replaceAll(/%= MANIFEST_URL =%/g, `${base}manifest.json`) - } - } + .replaceAll(/%= MANIFEST_URL =%/g, `${base}manifest.json`); + }, + }, ], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + "@": path.resolve(__dirname, "../../packages/fast-bridge-app/src"), + "@fastbridge/runtime": path.resolve(__dirname, "./src/runtime.ts"), buffer: "vite-plugin-node-polyfills/shims/buffer", global: "vite-plugin-node-polyfills/shims/global", process: "vite-plugin-node-polyfills/shims/process", @@ -91,7 +92,7 @@ export default defineConfig(({ mode }) => { }, envPrefix: ["VITE_"], build: { - emptyOutDir: true - } - } + emptyOutDir: true, + }, + }; }); diff --git a/apps/root/index.html b/apps/root/index.html index db822aa..4ec40d7 100644 --- a/apps/root/index.html +++ b/apps/root/index.html @@ -1,10 +1,55 @@ - - - Fast Bridge - + + + Avail Fast Bridge | Bridge USDC & Stablecoins Across Chains + + + + + + + + + + + + + + +
diff --git a/apps/root/public/metaimage.png b/apps/root/public/metaimage.png new file mode 100644 index 0000000..266d943 Binary files /dev/null and b/apps/root/public/metaimage.png differ diff --git a/apps/root/public/robots.txt b/apps/root/public/robots.txt new file mode 100644 index 0000000..25a6b1a --- /dev/null +++ b/apps/root/public/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Sitemap: https://fastbridge.availproject.org/sitemap.xml diff --git a/apps/root/public/sitemap.xml b/apps/root/public/sitemap.xml new file mode 100644 index 0000000..31c98e6 --- /dev/null +++ b/apps/root/public/sitemap.xml @@ -0,0 +1,23 @@ + + + + https://fastbridge.availproject.org/ + 2026-02-18 + 1.0 + + + https://fastbridge.availproject.org/megaeth/ + 2026-02-18 + 0.8 + + + https://fastbridge.availproject.org/citrea/ + 2026-02-18 + 0.8 + + + https://fastbridge.availproject.org/monad/ + 2026-02-18 + 0.8 + + diff --git a/apps/root/src/App.tsx b/apps/root/src/app.tsx similarity index 57% rename from apps/root/src/App.tsx rename to apps/root/src/app.tsx index 74d51df..a9766ea 100644 --- a/apps/root/src/App.tsx +++ b/apps/root/src/app.tsx @@ -1,14 +1,13 @@ -import { useState, useEffect, useRef } from "react"; -import { motion } from "motion/react"; import { ExternalLink } from "lucide-react"; -import { chains } from "./chains"; +import { motion } from "motion/react"; +import { useEffect, useRef, useState } from "react"; import AvailLogo from "/avail_logo.svg"; -import FloatingLogos from "./components/floating-logos"; -import AnimatedTitle from "./components/animated-title"; +import { chains } from "./chains"; import ChainCard from "./components/chain-card"; -import { SOCIAL_LINKS } from "./lib/constant"; +import FloatingLogos from "./components/floating-logos"; import SocialGlyph from "./components/social-glyph"; import { AuroraText } from "./components/ui/aurora-text"; +import { SOCIAL_LINKS } from "./lib/constant"; const socialLinkClassName = "inline-flex h-11 w-11 items-center justify-center rounded-full bg-[rgb(32,34,36)] text-white transition-[transform,background-color,box-shadow] duration-200 [transition-timing-function:cubic-bezier(0.34,1.56,0.64,1)] hover:-translate-y-0.5 hover:bg-black hover:shadow-[0_4px_12px_rgba(32,34,36,0.35)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--blue-500)] md:h-8 md:w-8"; @@ -36,84 +35,165 @@ export default function App() {
{/* Navbar */} - Avail + Avail
- - Nexus Fast Bridge + + Avail FastBridge {/* Hero Section */} {/* Floating logos background */} -
-

- Avail Fast Bridge +
+

+ Avail FastBridge

+

+ Fast Crypto Bridge for Stables Across Chains +

- Bridge assets across chains with Avail's unified - infrastructure. Fast, secure, and seamless cross-chain transfers. + Avail FastBridge is fast.
+ Bridge stablecoins like USDC, USDT and others across 15+ chains in + seconds with no wrapping and no complexity. It's a secure, + multi-chain bridge powered by Avail Nexus.
{/* Chain Grid */}
-
+

+ Bridge Stablecoins to Any Chain +

+
{chains.map((chain, index) => { - return ; + return ; })}
+ +
+
+

+ Why Use Avail FastBridge as Your Multi-Chain Bridge +

+ +
+

+ Avail FastBridge is the quickest way to move your assets between + chains. In a single transaction you can instantly{" "} + bridge USDC across multiple chains. +

+ +

+ It’s one of the few{" "} + multi-chain, and multi-transaction bridges{" "} + enabling you to move assets freely in a single transaction + between major ecosystems like Ethereum, Base, Arbitrum, + Optimism, and emerging L2s such as MegaETH, Citrea, and Monad. +

+ +

+ Avail FastBridge specializes in{" "} + cross-chain bridging for quick and seamless{" "} + stablecoin transfers, ensuring your liquidity + is never fragmented. Experience the speed of a true{" "} + fast crypto bridge that eliminates the + complexities of wrapping and waiting. +

+ +

+ Powered by Avail Nexus, providing uncompromised security and + seamless interoperability for the decentralized age. +

+
+
+
+
+

+ Supported Chains +

+

+ MegaETH, Citrea, Monad, Ethereum, Arbitrum, Optimism, Base, + Scroll, Polygon, Avalanche, and more. +

+
+
+

+ Supported Tokens +

+

+ USDC, USDT, USDM, ETH and native assets. +

+
+
+

+ Key Features +

+
    +
  • Atomic settlement speed
  • +
  • Avail Nexus security
  • +
  • Zero slippage on stablecoins
  • +
+
+
+
{/* Footer */}
Powered by - Avail + Avail
Discord {SOCIAL_LINKS.map((social) => ( @@ -174,12 +262,12 @@ export default function App() {
Discord
Powered by - Avail + Avail
- + Socials
{SOCIAL_LINKS.map((social) => ( diff --git a/apps/root/src/chains.ts b/apps/root/src/chains.ts index 803398f..79a824c 100644 --- a/apps/root/src/chains.ts +++ b/apps/root/src/chains.ts @@ -1,15 +1,15 @@ +import chainList from "../../../chains.config.json"; + export interface ChainConfig { - slug: string; - name: string; + appDir: string; + basePath: string; description?: string; + iconUrl?: string; + logoUrl?: string; + name: string; primaryColor?: string; secondaryColor?: string; - basePath: string; - appDir: string; - logoUrl?: string; - iconUrl?: string; + slug: string; } -import chainList from "../../../chains.config.json" assert { type: "json" }; - export const chains = chainList as ChainConfig[]; diff --git a/apps/root/src/components/animated-title.tsx b/apps/root/src/components/animated-title.tsx index eaf978c..58e2160 100644 --- a/apps/root/src/components/animated-title.tsx +++ b/apps/root/src/components/animated-title.tsx @@ -1,20 +1,30 @@ import { motion } from "motion/react"; + function AnimatedTitle({ text }: { text: string }) { const letters = text.split(""); + const letterOccurrenceByValue = new Map(); + const letterEntries = letters.map((letter) => { + const occurrence = (letterOccurrenceByValue.get(letter) ?? 0) + 1; + letterOccurrenceByValue.set(letter, occurrence); + return { + key: `${letter}-${occurrence}`, + letter, + }; + }); return ( -

- {letters.map((letter, index) => ( +

+ {letterEntries.map(({ key, letter }, index) => ( (null); const primaryColor = chain.primaryColor ?? "#2563eb"; @@ -23,15 +23,17 @@ function ChainCard({ const springConfig = { damping: 25, stiffness: 200 }; const rotateX = useSpring( useTransform(mouseY, [-0.5, 0.5], [8, -8]), - springConfig, + springConfig ); const rotateY = useSpring( useTransform(mouseX, [-0.5, 0.5], [-8, 8]), - springConfig, + springConfig ); const handleMouseMove = (e: React.MouseEvent) => { - if (!cardRef.current) return; + if (!cardRef.current) { + return; + } const rect = cardRef.current.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width - 0.5; const y = (e.clientY - rect.top) / rect.height - 0.5; @@ -45,12 +47,45 @@ function ChainCard({ setIsHovered(false); }; + useEffect(() => { + if (!chain.logoUrl) { + setImageError(true); + return; + } + + let canceled = false; + const image = new Image(); + image.onload = () => { + if (!canceled) { + setImageError(false); + } + }; + image.onerror = () => { + if (!canceled) { + setImageError(true); + } + }; + image.src = chain.logoUrl; + + return () => { + canceled = true; + }; + }, [chain.logoUrl]); + // Determine if color is light or dark for fallback text const isLightColor = (hex: string) => { - const rgb = parseInt(hex.slice(1), 16); - const r = (rgb >> 16) & 0xff; - const g = (rgb >> 8) & 0xff; - const b = (rgb >> 0) & 0xff; + const normalized = hex.startsWith("#") ? hex.slice(1) : hex; + if (normalized.length !== 6) { + return false; + } + + const r = Number.parseInt(normalized.slice(0, 2), 16); + const g = Number.parseInt(normalized.slice(2, 4), 16); + const b = Number.parseInt(normalized.slice(4, 6), 16); + if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) { + return false; + } + const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; return luma > 128; }; @@ -59,48 +94,48 @@ function ChainCard({ return ( setIsHovered(true)} + onMouseLeave={handleMouseLeave} + onMouseMove={handleMouseMove} ref={cardRef} style={{ rotateX, rotateY, transformStyle: "preserve-3d", }} - onMouseMove={handleMouseMove} - onMouseEnter={() => setIsHovered(true)} - onMouseLeave={handleMouseLeave} > {/* Animated border glow */}
{/* Chain Logo with bounce animation */} {!imageError && chain.logoUrl ? ( {`${chain.name} setImageError(true)} + height={72} + src={chain.logoUrl} + width={72} /> ) : (
{chain.name} {chain.description && ( {chain.description} @@ -152,24 +188,24 @@ function ChainCard({
{/* Card Footer */} -
+
{chain.basePath} diff --git a/apps/root/src/components/floating-logos.tsx b/apps/root/src/components/floating-logos.tsx index 402da6f..68cf500 100644 --- a/apps/root/src/components/floating-logos.tsx +++ b/apps/root/src/components/floating-logos.tsx @@ -1,21 +1,122 @@ -import { TOKEN_IMAGES } from "@/lib/constant"; import { motion } from "motion/react"; import { useCallback, useEffect, useRef, useState } from "react"; +import { TOKEN_IMAGES } from "@/lib/constant"; interface FloatingLogoData { + homeX?: number; + homeY?: number; id: number; - x: number; - y: number; - size: number; - vx: number; - vy: number; imageUrl: string; name: string; + size: number; type: "chain" | "token"; - homeX?: number; - homeY?: number; + vx: number; + vy: number; + x: number; + y: number; } +const CURSOR_RADIUS = 60; +const REPEL_FORCE = 2; +const FRICTION = 0.96; +const MAX_SPEED = 0.8; +const RETURN_FORCE = 0.005; +const MIN_LOGO_BOUNDARY = 5; +const MAX_LOGO_BOUNDARY = 95; +const BOUNDARY_PUSH = 0.02; + +const setLogoHomePosition = (logo: FloatingLogoData) => { + if (logo.homeX === undefined) { + logo.homeX = logo.x; + } + if (logo.homeY === undefined) { + logo.homeY = logo.y; + } +}; + +const applyCursorRepel = ( + logo: FloatingLogoData, + mousePosition: { x: number; y: number }, + width: number, + height: number +) => { + const logoPx = (logo.x / 100) * width; + const logoPy = (logo.y / 100) * height; + const dx = logoPx - mousePosition.x; + const dy = logoPy - mousePosition.y; + const distance = Math.sqrt(dx * dx + dy * dy); + const influenceRadius = CURSOR_RADIUS + logo.size / 2; + + if (distance >= influenceRadius || distance === 0) { + return; + } + + const force = (influenceRadius - distance) / influenceRadius; + const angle = Math.atan2(dy, dx); + logo.vx += Math.cos(angle) * force * REPEL_FORCE; + logo.vy += Math.sin(angle) * force * REPEL_FORCE; +}; + +const applyAmbientDrift = ( + logo: FloatingLogoData, + index: number, + time: number +) => { + const noiseX = Math.sin(time + index * 2) * Math.cos(time * 0.7 + index); + const noiseY = + Math.cos(time * 0.8 + index * 1.5) * Math.sin(time * 0.6 + index * 0.8); + const jitter = (Math.random() - 0.5) * 0.03; + + logo.vx += noiseX * 0.008 + jitter; + logo.vy += noiseY * 0.008 + jitter; +}; + +const applyReturnForce = (logo: FloatingLogoData) => { + const homeDx = (logo.homeX ?? logo.x) - logo.x; + const homeDy = (logo.homeY ?? logo.y) - logo.y; + logo.vx += homeDx * RETURN_FORCE; + logo.vy += homeDy * RETURN_FORCE; +}; + +const applyVelocity = ( + logo: FloatingLogoData, + width: number, + height: number +) => { + logo.x += (logo.vx / width) * 100; + logo.y += (logo.vy / height) * 100; +}; + +const applyFriction = (logo: FloatingLogoData) => { + logo.vx *= FRICTION; + logo.vy *= FRICTION; +}; + +const clampLogoSpeed = (logo: FloatingLogoData) => { + const speed = Math.sqrt(logo.vx * logo.vx + logo.vy * logo.vy); + if (speed <= MAX_SPEED) { + return; + } + + logo.vx = (logo.vx / speed) * MAX_SPEED; + logo.vy = (logo.vy / speed) * MAX_SPEED; +}; + +const applyBoundaryForce = (logo: FloatingLogoData) => { + if (logo.x < MIN_LOGO_BOUNDARY) { + logo.vx += BOUNDARY_PUSH; + } + if (logo.x > MAX_LOGO_BOUNDARY) { + logo.vx -= BOUNDARY_PUSH; + } + if (logo.y < MIN_LOGO_BOUNDARY) { + logo.vy += BOUNDARY_PUSH; + } + if (logo.y > MAX_LOGO_BOUNDARY) { + logo.vy -= BOUNDARY_PUSH; + } +}; + function FloatingLogos({ mousePosition, }: { @@ -38,9 +139,9 @@ function FloatingLogos({ ...tokenSymbols, ...tokenSymbols, ].sort(() => Math.random() - 0.5); - shuffledTokens.forEach((symbol) => { + for (const symbol of shuffledTokens) { logos.push({ - id: id++, + id, x: Math.random() * 110 - 15, y: Math.random() * 110 - 20, size: 24 + Math.random() * 28, // 24-52px, slightly smaller than chains @@ -50,7 +151,8 @@ function FloatingLogos({ name: symbol, type: "token", }); - }); + id += 1; + } logosRef.current = logos; forceUpdate({}); @@ -59,87 +161,24 @@ function FloatingLogos({ // Animation loop with cursor collision and gentle random movement const animate = useCallback(() => { const container = containerRef.current; - if (!container) return; + if (!container) { + return; + } const rect = container.getBoundingClientRect(); const logos = logosRef.current; - const cursorRadius = 60; - const repelForce = 2; - const friction = 0.96; - const maxSpeed = 0.8; - const returnForce = 0.005; const time = Date.now() * 0.0005; - logos.forEach((logo, index) => { - // Store original position as "home" - if (logo.homeX === undefined) logo.homeX = logo.x; - if (logo.homeY === undefined) logo.homeY = logo.y; - - // Convert percentage to pixels for collision - const logoPx = (logo.x / 100) * rect.width; - const logoPy = (logo.y / 100) * rect.height; - - // Calculate distance to cursor - const dx = logoPx - mousePosition.x; - const dy = logoPy - mousePosition.y; - const distance = Math.sqrt(dx * dx + dy * dy); - - // Cursor collision/repulsion (gentler) - if (distance < cursorRadius + logo.size / 2 && distance > 0) { - const force = - (cursorRadius + logo.size / 2 - distance) / - (cursorRadius + logo.size / 2); - const angle = Math.atan2(dy, dx); - logo.vx += Math.cos(angle) * force * repelForce; - logo.vy += Math.sin(angle) * force * repelForce; - } - - // Gentle drift movement - very slow and subtle - const noiseX = Math.sin(time + index * 2) * Math.cos(time * 0.7 + index); - const noiseY = - Math.cos(time * 0.8 + index * 1.5) * Math.sin(time * 0.6 + index * 0.8); - - // Minimal random jitter - const jitter = (Math.random() - 0.5) * 0.03; - - logo.vx += noiseX * 0.008 + jitter; - logo.vy += noiseY * 0.008 + jitter; - - // Gentle return-to-home force (keeps logos in view) - const homeDx = logo.homeX - logo.x; - const homeDy = logo.homeY - logo.y; - logo.vx += homeDx * returnForce; - logo.vy += homeDy * returnForce; - - // Apply velocity - logo.x += (logo.vx / rect.width) * 100; - logo.y += (logo.vy / rect.height) * 100; - - // Apply friction - logo.vx *= friction; - logo.vy *= friction; - - // Clamp speed - const speed = Math.sqrt(logo.vx * logo.vx + logo.vy * logo.vy); - if (speed > maxSpeed) { - logo.vx = (logo.vx / speed) * maxSpeed; - logo.vy = (logo.vy / speed) * maxSpeed; - } - - // Soft boundary constraints (gentle push back instead of bounce) - if (logo.x < 5) { - logo.vx += 0.02; - } - if (logo.x > 95) { - logo.vx -= 0.02; - } - if (logo.y < 5) { - logo.vy += 0.02; - } - if (logo.y > 95) { - logo.vy -= 0.02; - } - }); + for (const [index, logo] of logos.entries()) { + setLogoHomePosition(logo); + applyCursorRepel(logo, mousePosition, rect.width, rect.height); + applyAmbientDrift(logo, index, time); + applyReturnForce(logo); + applyVelocity(logo, rect.width, rect.height); + applyFriction(logo); + clampLogoSpeed(logo); + applyBoundaryForce(logo); + } forceUpdate({}); animationRef.current = requestAnimationFrame(animate); @@ -156,32 +195,31 @@ function FloatingLogos({ return (
{logosRef.current.map((logo) => ( {logo.name} { - (e.target as HTMLImageElement).style.display = "none"; - }} + height={Math.round(logo.size)} + src={logo.imageUrl} + width={Math.round(logo.size)} /> ))} diff --git a/apps/root/src/components/social-glyph.tsx b/apps/root/src/components/social-glyph.tsx index 1a6e657..3eb335c 100644 --- a/apps/root/src/components/social-glyph.tsx +++ b/apps/root/src/components/social-glyph.tsx @@ -4,11 +4,11 @@ function SocialGlyph({ id }: { id: SocialId }) { if (id === "telegram") { return ( {children} - ) + ); } -) +); -AuroraText.displayName = "AuroraText" +AuroraText.displayName = "AuroraText"; diff --git a/apps/root/src/index.css b/apps/root/src/index.css index 73dd3af..c135cbe 100644 --- a/apps/root/src/index.css +++ b/apps/root/src/index.css @@ -1,5 +1,5 @@ /* Nexus Fast Bridge Hub - Avail Design System (Light Theme) */ -@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"); @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; @@ -7,40 +7,40 @@ @custom-variant dark (&:is(.dark *)); @font-face { - font-family: 'Delight'; + font-family: "Delight"; src: - url('/delight/Web-TT/Delight-Regular.woff2') format('woff2'), - url('/delight/Web-TT/Delight-Regular.woff') format('woff'); + url("/delight/Web-TT/Delight-Regular.woff2") format("woff2"), + url("/delight/Web-TT/Delight-Regular.woff") format("woff"); font-weight: 400; font-style: normal; font-display: swap; } @font-face { - font-family: 'Delight'; + font-family: "Delight"; src: - url('/delight/Web-TT/Delight-Medium.woff2') format('woff2'), - url('/delight/Web-TT/Delight-Medium.woff') format('woff'); + url("/delight/Web-TT/Delight-Medium.woff2") format("woff2"), + url("/delight/Web-TT/Delight-Medium.woff") format("woff"); font-weight: 500; font-style: normal; font-display: swap; } @font-face { - font-family: 'Delight'; + font-family: "Delight"; src: - url('/delight/Web-TT/Delight-SemiBold.woff2') format('woff2'), - url('/delight/Web-TT/Delight-SemiBold.woff') format('woff'); + url("/delight/Web-TT/Delight-SemiBold.woff2") format("woff2"), + url("/delight/Web-TT/Delight-SemiBold.woff") format("woff"); font-weight: 600; font-style: normal; font-display: swap; } @font-face { - font-family: 'Delight'; + font-family: "Delight"; src: - url('/delight/Web-TT/Delight-Bold.woff2') format('woff2'), - url('/delight/Web-TT/Delight-Bold.woff') format('woff'); + url("/delight/Web-TT/Delight-Bold.woff2") format("woff2"), + url("/delight/Web-TT/Delight-Bold.woff") format("woff"); font-weight: 700; font-style: normal; font-display: swap; @@ -48,9 +48,9 @@ :root { /* Typography */ - --font-display: 'Delight', 'Geist', system-ui, -apple-system, sans-serif; - --font-body: 'Geist', system-ui, -apple-system, sans-serif; - --font-mono: 'JetBrains Mono', 'SF Mono', monospace; + --font-display: "Delight", "Geist", system-ui, -apple-system, sans-serif; + --font-body: "Geist", system-ui, -apple-system, sans-serif; + --font-mono: "JetBrains Mono", "SF Mono", monospace; /* Border Radius */ --radius-none-med: 0px; @@ -173,14 +173,14 @@ html { } body { - font-family: var(--font-body); - background: var(--background-secondary); - color: var(--foreground-primary); min-height: 100vh; - line-height: 1.6; + overflow-x: hidden; + font-family: var(--font-body); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - overflow-x: hidden; + line-height: 1.6; + color: var(--foreground-primary); + background: var(--background-secondary); } #root { @@ -188,8 +188,8 @@ body { } ::selection { - background: hsla(214, 92%, 48%, 0.2); color: var(--foreground-primary); + background: hsla(214, 92%, 48%, 0.2); } @theme inline { @@ -233,25 +233,25 @@ body { --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); @keyframes aurora { - 0% { - background-position: 0% 50%; - transform: rotate(-5deg) scale(0.9); + 0% { + background-position: 0% 50%; + transform: rotate(-5deg) scale(0.9); } - 25% { - background-position: 50% 100%; - transform: rotate(5deg) scale(1.1); + 25% { + background-position: 50% 100%; + transform: rotate(5deg) scale(1.1); } - 50% { - background-position: 100% 50%; - transform: rotate(-3deg) scale(0.95); + 50% { + background-position: 100% 50%; + transform: rotate(-3deg) scale(0.95); } - 75% { - background-position: 50% 0%; - transform: rotate(3deg) scale(1.05); + 75% { + background-position: 50% 0%; + transform: rotate(3deg) scale(1.05); } - 100% { - background-position: 0% 50%; - transform: rotate(-5deg) scale(0.9); + 100% { + background-position: 0% 50%; + transform: rotate(-5deg) scale(0.9); } } } @@ -297,4 +297,4 @@ body { body { @apply bg-background text-foreground; } -} \ No newline at end of file +} diff --git a/apps/root/src/lib/utils.ts b/apps/root/src/lib/utils.ts index bd0c391..365058c 100644 --- a/apps/root/src/lib/utils.ts +++ b/apps/root/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/apps/root/src/main.tsx b/apps/root/src/main.tsx index 7467839..5130dc6 100644 --- a/apps/root/src/main.tsx +++ b/apps/root/src/main.tsx @@ -1,9 +1,14 @@ import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; -import App from "./App"; +import App from "./app"; -ReactDOM.createRoot(document.getElementById("root")!).render( +const rootElement = document.getElementById("root"); +if (!rootElement) { + throw new Error("Root element with id 'root' was not found."); +} + +ReactDOM.createRoot(rootElement).render( diff --git a/apps/root/tsconfig.app.json b/apps/root/tsconfig.app.json index 2827d34..259782b 100644 --- a/apps/root/tsconfig.app.json +++ b/apps/root/tsconfig.app.json @@ -20,8 +20,8 @@ "resolveJsonModule": true, "baseUrl": ".", "paths": { - "@/*": ["./src/*"], - }, + "@/*": ["./src/*"] + } }, - "include": ["src"], + "include": ["src"] } diff --git a/apps/root/tsconfig.json b/apps/root/tsconfig.json index 59578c3..5bfdf37 100644 --- a/apps/root/tsconfig.json +++ b/apps/root/tsconfig.json @@ -2,12 +2,13 @@ "files": [], "references": [ { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.node.json" } ], "compilerOptions": { "baseUrl": ".", "paths": { - "@/*": ["./src/*"], + "@/*": ["./src/*"] }, - }, + "strictNullChecks": true + } } diff --git a/apps/root/vite.config.ts b/apps/root/vite.config.ts index 1f71a75..bb24aa1 100644 --- a/apps/root/vite.config.ts +++ b/apps/root/vite.config.ts @@ -1,13 +1,20 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const chainsConfigPath = path.resolve(__dirname, "..", "..", "chains.config.json"); -const chains = JSON.parse(fs.readFileSync(chainsConfigPath, "utf8")) as Array<{ slug: string }>; +const chainsConfigPath = path.resolve( + __dirname, + "..", + "..", + "chains.config.json" +); +const chains = JSON.parse(fs.readFileSync(chainsConfigPath, "utf8")) as Array<{ + slug: string; +}>; const baseDevPort = 5173; const proxy = Object.fromEntries( @@ -21,7 +28,7 @@ const proxy = Object.fromEntries( changeOrigin: true, }, ]; - }), + }) ); export default defineConfig(({ command }) => { diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000..7c9e23c --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,44 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["ultracite/biome/core", "ultracite/biome/react"], + "overrides": [ + { + "includes": ["packages/fast-bridge-app/src/**"], + "linter": { + "rules": { + "a11y": { + "useAriaPropsForRole": "warn", + "useFocusableInteractive": "warn", + "useSemanticElements": "warn" + }, + "complexity": { + "noExcessiveCognitiveComplexity": "warn", + "noForEach": "warn", + "noVoid": "warn" + }, + "correctness": { + "useExhaustiveDependencies": "warn", + "useImageSize": "warn" + }, + "performance": { + "noBarrelFile": "warn", + "noNamespaceImport": "warn", + "useTopLevelRegex": "warn" + }, + "style": { + "useConsistentArrayType": "warn", + "useConsistentTypeDefinitions": "warn", + "noEnum": "warn", + "noNestedTernary": "warn", + "noNonNullAssertion": "warn", + "useFilenamingConvention": "warn" + }, + "suspicious": { + "noArrayIndexKey": "warn", + "useIterableCallbackReturn": "warn" + } + } + } + } + ] +} diff --git a/chains.config.json b/chains.config.json index 0f68557..94186c8 100644 --- a/chains.config.json +++ b/chains.config.json @@ -7,7 +7,7 @@ "secondaryColor": "#ECE8E8", "basePath": "/megaeth/", "appDir": "apps/megaeth", - "logoUrl": "https://files.availproject.org/fastbridge/megaeth/megaeth-logo.svg", + "logoUrl": "https://files.availproject.org/fastbridge/megaeth/megaeth-favicon.svg", "iconUrl": "https://files.availproject.org/fastbridge/megaeth/megaeth-favicon.svg" }, { @@ -18,8 +18,8 @@ "secondaryColor": "#ffffff", "basePath": "/citrea/", "appDir": "apps/citrea", - "logoUrl": "https://files.availproject.org/fastbridge/citrea/citrea.svg", - "iconUrl": "https://files.availproject.org/fastbridge/citrea/citrea.svg" + "logoUrl": "https://files.availproject.org/fastbridge/citrea/citrea-favicon.svg", + "iconUrl": "https://files.availproject.org/fastbridge/citrea/citrea-favicon.svg" }, { "slug": "monad", diff --git a/docs/adding-chains.md b/docs/adding-chains.md new file mode 100644 index 0000000..6b79188 --- /dev/null +++ b/docs/adding-chains.md @@ -0,0 +1,88 @@ +# Adding a New Chain + +This guide is the canonical process for creating and shipping a new chain app. + +## Fast Path (Recommended) + +```bash +pnpm chain:add --name "Chain Name" +``` + +Example: + +```bash +pnpm chain:add sonic --name "Sonic" +``` + +### Useful options + +- `--description "..."` +- `--base-path "/sonic/"` +- `--primary "#hex"` +- `--secondary "#hex"` +- `--logo-url "https://..."` +- `--icon-url "https://..."` +- `--template monad` + +## What the Scaffold Command Does + +- Clones `apps/