diff --git a/lib/contracts/constants.ts b/lib/contracts/constants.ts index 391d2c1f..61e1e2ad 100644 --- a/lib/contracts/constants.ts +++ b/lib/contracts/constants.ts @@ -33,10 +33,11 @@ export const STORY_FACTORY = (process.env.NEXT_PUBLIC_CONTRACT_ADDRESS ?? ? "0xfa5489b6710Ba2f8406b37fA8f8c3018e51FA229" : "0x337c5b96f03fB335b433291695A4171fd5dED8B0")) as `0x${string}`; -/** ZapPlotLink — one-click buy (ETH -> PLOT -> storyline token via Uniswap V4 + MCV2) */ +/** ZapPlotLinkV2 — one-click buy (ETH/USDC/HUNT -> PLOT -> storyline token via Uniswap V4 + MCV2) + * Testnet: disabled (V1 contract incompatible with V2 ABI) */ export const ZAP_PLOTLINK = (IS_TESTNET - ? "0xC7C47D820D2D5b09797be2F438Cf329Ad7315682" - : "0x0000000000000000000000000000000000000000") as `0x${string}`; + ? "0x0000000000000000000000000000000000000000" + : "0xEF6a8640c836b16Eb8cCD8016Ead4C8517aC3033") as `0x${string}`; /** $PLOT protocol token * Testnet: PL_TEST ERC-20 on Base Sepolia @@ -48,6 +49,26 @@ export const PLOT_TOKEN = (IS_TESTNET /** Human-readable label for the reserve token */ export const RESERVE_LABEL = IS_TESTNET ? "PL_TEST" : "PLOT"; +// --------------------------------------------------------------------------- +// Supported Zap input tokens (Base) +// --------------------------------------------------------------------------- + +/** USDC on Base */ +export const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as const; + +/** HUNT on Base */ +export const HUNT = "0x37f0c2915CeCC7e977183B8543Fc0864d03E064C" as const; + +/** ETH represented as address(0) in the Zap contract */ +export const ETH_ADDRESS = "0x0000000000000000000000000000000000000000" as const; + +/** Supported input tokens for the Zap UI selector */ +export const SUPPORTED_ZAP_TOKENS = [ + { symbol: "ETH", address: ETH_ADDRESS as `0x${string}`, decimals: 18 }, + { symbol: "USDC", address: USDC as `0x${string}`, decimals: 6 }, + { symbol: "HUNT", address: HUNT as `0x${string}`, decimals: 18 }, +] as const; + // --------------------------------------------------------------------------- // Mint Club V2 // --------------------------------------------------------------------------- diff --git a/lib/zap.ts b/lib/zap.ts index 3e3814c3..a8ed6d22 100644 --- a/lib/zap.ts +++ b/lib/zap.ts @@ -1,314 +1,141 @@ /** - * ZapPlotLink frontend wrappers. + * ZapPlotLinkV2 frontend wrappers. * - * Provides quote estimation and transaction helpers for the ZapPlotLink - * contract, which swaps ETH → PLOT via Uniswap V4 and mints storyline - * tokens on the MCV2 bonding curve in a single transaction. + * Multi-token zap: ETH/USDC/HUNT → PLOT → storyline token in one tx. + * The contract handles the full swap path internally — no V4 Quoter + * calls needed from the frontend. */ import { type Address, parseAbi } from "viem"; import { browserClient as publicClient } from "./rpc"; -import { ZAP_PLOTLINK, UNISWAP_V4_QUOTER, PLOT_TOKEN } from "./contracts/constants"; +import { ZAP_PLOTLINK, ETH_ADDRESS } from "./contracts/constants"; // --------------------------------------------------------------------------- -// ABI (only the functions we call) +// ABI — ZapPlotLinkV2 (multi-token interface) // --------------------------------------------------------------------------- -export const zapPlotLinkAbi = parseAbi([ - "function mint(address storylineToken, uint256 tokensToMint, address receiver) external payable returns (uint256 reserveUsed)", - "function mintReverse(address storylineToken, uint256 minTokensOut, address receiver) external payable returns (uint256 tokensMinted)", - "function estimateMintCostInPlot(address storylineToken, uint256 tokensToMint) external view returns (uint256 plotRequired)", - "function estimateMintReverseFromPlot(address storylineToken, uint256 plotAmount) external view returns (uint256 tokensOut)", +export const zapPlotLinkV2Abi = parseAbi([ + "function mint(address fromToken, address storylineToken, uint256 storylineAmount, uint256 maxFromTokenAmount) external payable returns (uint256 fromTokenUsed)", + "function mintReverse(address fromToken, address storylineToken, uint256 fromTokenAmount, uint256 minStorylineAmount) external payable returns (uint256 storylineAmount)", + "function estimateMint(address fromToken, address storylineToken, uint256 storylineAmount) external returns (uint256 fromTokenAmount, uint256 totalPlotRequired)", + "function estimateMintReverse(address fromToken, address storylineToken, uint256 fromTokenAmount) external returns (uint256 storylineAmount, uint256 plotAmount)", ]); -/** - * V4 Quoter ABI — quoteExactInputSingle and quoteExactOutputSingle. - * - * These functions are NOT view — they execute state changes internally and - * revert with the result. Must be called via eth_call (simulateContract). - */ -const quoterAbi = [ - { - name: "quoteExactInputSingle", - type: "function", - stateMutability: "nonpayable", - inputs: [ - { - name: "params", - type: "tuple", - components: [ - { - name: "poolKey", - type: "tuple", - components: [ - { name: "currency0", type: "address" }, - { name: "currency1", type: "address" }, - { name: "fee", type: "uint24" }, - { name: "tickSpacing", type: "int24" }, - { name: "hooks", type: "address" }, - ], - }, - { name: "zeroForOne", type: "bool" }, - { name: "exactAmount", type: "uint128" }, - { name: "hookData", type: "bytes" }, - ], - }, - ], - outputs: [ - { name: "amountOut", type: "uint256" }, - { name: "gasEstimate", type: "uint256" }, - ], - }, - { - name: "quoteExactOutputSingle", - type: "function", - stateMutability: "nonpayable", - inputs: [ - { - name: "params", - type: "tuple", - components: [ - { - name: "poolKey", - type: "tuple", - components: [ - { name: "currency0", type: "address" }, - { name: "currency1", type: "address" }, - { name: "fee", type: "uint24" }, - { name: "tickSpacing", type: "int24" }, - { name: "hooks", type: "address" }, - ], - }, - { name: "zeroForOne", type: "bool" }, - { name: "exactAmount", type: "uint128" }, - { name: "hookData", type: "bytes" }, - ], - }, - ], - outputs: [ - { name: "amountIn", type: "uint256" }, - { name: "gasEstimate", type: "uint256" }, - ], - }, -] as const; - -const WETH: Address = "0x4200000000000000000000000000000000000006"; -const POOL_FEE = 3000; // 0.30% — must match deployed pool -const TICK_SPACING = 60; -const HOOKS: Address = "0x0000000000000000000000000000000000000000"; -const SLIPPAGE_BPS = 50; // 0.5% slippage buffer - -// Pool key tokens sorted (currency0 < currency1) -function getPoolKey(): { currency0: Address; currency1: Address } { - const wethNum = BigInt(WETH); - const plotNum = BigInt(PLOT_TOKEN); - if (wethNum < plotNum) { - return { currency0: WETH, currency1: PLOT_TOKEN }; - } - return { currency0: PLOT_TOKEN, currency1: WETH }; -} +const SLIPPAGE_BPS = 300; // 3% slippage buffer for bonding curve // --------------------------------------------------------------------------- -// Quote helpers +// Quote types // --------------------------------------------------------------------------- export type ZapMode = "exact-output" | "exact-input"; export interface ZapQuote { - /** PLOT tokens needed/received (bonding curve side) */ + /** fromToken amount needed (exact-output) or spent (exact-input) */ + fromTokenAmount: bigint; + /** PLOT tokens involved in the bonding curve leg */ plotAmount: bigint; - /** Estimated ETH cost (including 0.5% swap slippage buffer) */ - ethCost: bigint; - /** For exact-input: estimated storyline tokens out */ + /** For exact-input: estimated storyline tokens received */ tokensOut?: bigint; mode: ZapMode; } -/** - * Quote how much PLOT is received for a given ETH input via Uniswap V4. - * Uses the V4 Quoter's quoteExactInputSingle (called via eth_call). - */ -async function quoteEthToPlot(ethAmount: bigint): Promise { - const { currency0, currency1 } = getPoolKey(); - const zeroForOne = currency0 === WETH; // true if WETH is currency0 - - try { - const { result } = await publicClient.simulateContract({ - address: UNISWAP_V4_QUOTER as Address, - abi: quoterAbi, - functionName: "quoteExactInputSingle", - args: [ - { - poolKey: { currency0, currency1, fee: POOL_FEE, tickSpacing: TICK_SPACING, hooks: HOOKS }, - zeroForOne, - exactAmount: ethAmount, - hookData: "0x", - }, - ], - }); - return result[0]; - } catch { - // Fallback: if quoter call fails, return 0 to indicate unavailable - return BigInt(0); - } -} - -/** - * Quote how much ETH is needed to buy a given amount of PLOT via Uniswap V4. - * Uses the V4 Quoter's quoteExactOutputSingle (called via eth_call). - */ -async function quotePlotToEth(plotAmount: bigint): Promise { - const { currency0, currency1 } = getPoolKey(); - const zeroForOne = currency0 === WETH; // swapping WETH in for PLOT out - - try { - const { result } = await publicClient.simulateContract({ - address: UNISWAP_V4_QUOTER as Address, - abi: quoterAbi, - functionName: "quoteExactOutputSingle", - args: [ - { - poolKey: { currency0, currency1, fee: POOL_FEE, tickSpacing: TICK_SPACING, hooks: HOOKS }, - zeroForOne, - exactAmount: plotAmount, - hookData: "0x", - }, - ], - }); - return result[0]; - } catch { - return BigInt(0); - } -} +// --------------------------------------------------------------------------- +// Quote +// --------------------------------------------------------------------------- /** - * Get a quote for a zap mint. - * - * - exact-output: "I want N storyline tokens — how much ETH?" - * - exact-input: "I have N ETH — how many storyline tokens?" + * Get a quote for a zap mint. Calls the contract's non-view estimate + * functions via simulateContract (eth_call). * - * @param tokenAddress Storyline token address - * @param amount Token amount (exact-output) or ETH amount in wei (exact-input) + * @param fromToken Input token address (address(0) for ETH) + * @param storylineToken Storyline token to mint + * @param amount Storyline tokens (exact-output) or fromToken amount (exact-input) * @param mode Quote mode */ export async function getZapQuote( - tokenAddress: Address, + fromToken: Address, + storylineToken: Address, amount: bigint, mode: ZapMode, ): Promise { if (mode === "exact-output") { - // Step 1: How much PLOT needed to mint `amount` storyline tokens? - const plotRequired = await publicClient.readContract({ + const { result } = await publicClient.simulateContract({ address: ZAP_PLOTLINK, - abi: zapPlotLinkAbi, - functionName: "estimateMintCostInPlot", - args: [tokenAddress, amount], + abi: zapPlotLinkV2Abi, + functionName: "estimateMint", + args: [fromToken, storylineToken, amount], }); - // Step 2: How much ETH to buy that much PLOT on Uniswap V4? - const ethNeeded = await quotePlotToEth(plotRequired); + const fromTokenAmount = result[0]; + const plotRequired = result[1]; - // Add 0.5% slippage buffer - const ethCost = ethNeeded > BigInt(0) - ? ethNeeded + (ethNeeded * BigInt(SLIPPAGE_BPS)) / BigInt(10000) - : BigInt(0); + // Add 3% slippage buffer to fromTokenAmount + const withSlippage = fromTokenAmount + (fromTokenAmount * BigInt(SLIPPAGE_BPS)) / BigInt(10000); - return { plotAmount: plotRequired, ethCost, mode }; + return { fromTokenAmount: withSlippage, plotAmount: plotRequired, mode }; } else { - // exact-input: user sends `amount` ETH - // Step 1: How much PLOT do we get for `amount` ETH via Uniswap V4? - const plotReceived = await quoteEthToPlot(amount); + const { result } = await publicClient.simulateContract({ + address: ZAP_PLOTLINK, + abi: zapPlotLinkV2Abi, + functionName: "estimateMintReverse", + args: [fromToken, storylineToken, amount], + }); - // Step 2: How many storyline tokens for that PLOT? - let tokensOut = BigInt(0); - if (plotReceived > BigInt(0)) { - tokensOut = await publicClient.readContract({ - address: ZAP_PLOTLINK, - abi: zapPlotLinkAbi, - functionName: "estimateMintReverseFromPlot", - args: [tokenAddress, plotReceived], - }); - } + const tokensOut = result[0]; + const plotAmount = result[1]; - return { plotAmount: plotReceived, ethCost: amount, tokensOut, mode }; + return { fromTokenAmount: amount, plotAmount, tokensOut, mode }; } } // --------------------------------------------------------------------------- -// Transaction helpers +// Transaction builder // --------------------------------------------------------------------------- /** - * Build the transaction parameters for a zap mint. - * Returns args suitable for wagmi's writeContract. + * Build wagmi-compatible transaction params for a zap mint. * - * @param tokenAddress Storyline token address - * @param amount Token amount (exact-output) or ETH wei (exact-input) + * - ETH: payable tx with msg.value, no prior approval needed + * - USDC/HUNT: non-payable tx, requires prior ERC-20 approval to ZAP_PLOTLINK + * + * @param fromToken Input token address (address(0) for ETH) + * @param storylineToken Storyline token to mint + * @param amount Storyline tokens (exact-output) or fromToken amount (exact-input) * @param mode Zap mode - * @param receiver Address to receive minted tokens - * @param ethValue ETH to send (from quote.ethCost) - * @param minTokensOut Minimum tokens for exact-input slippage protection + * @param quote The quote from getZapQuote */ export function buildZapMintTx( - tokenAddress: Address, + fromToken: Address, + storylineToken: Address, amount: bigint, mode: ZapMode, - receiver: Address, - ethValue: bigint, - minTokensOut?: bigint, + quote: ZapQuote, ) { + const isEth = fromToken === ETH_ADDRESS; + if (mode === "exact-output") { return { address: ZAP_PLOTLINK, - abi: zapPlotLinkAbi, + abi: zapPlotLinkV2Abi, functionName: "mint" as const, - args: [tokenAddress, amount, receiver] as const, - value: ethValue, + args: [fromToken, storylineToken, amount, quote.fromTokenAmount] as const, + value: isEth ? quote.fromTokenAmount : BigInt(0), gas: BigInt(3_000_000), }; } else { - // Apply 3% slippage to minTokensOut for exact-input protection - const minOut = minTokensOut ?? BigInt(0); + // Apply 3% slippage to minStorylineAmount + const minOut = quote.tokensOut ?? BigInt(0); const slippageProtected = minOut > BigInt(0) - ? minOut - (minOut * BigInt(300)) / BigInt(10000) + ? minOut - (minOut * BigInt(SLIPPAGE_BPS)) / BigInt(10000) : BigInt(0); return { address: ZAP_PLOTLINK, - abi: zapPlotLinkAbi, + abi: zapPlotLinkV2Abi, functionName: "mintReverse" as const, - args: [tokenAddress, slippageProtected, receiver] as const, - value: ethValue, + args: [fromToken, storylineToken, amount, slippageProtected] as const, + value: isEth ? amount : BigInt(0), gas: BigInt(3_000_000), }; } } - -/** - * Execute a zap mint end-to-end: get quote, then submit transaction. - * - * @param tokenAddress Storyline token address - * @param amount Token amount (exact-output) or ETH amount in wei (exact-input) - * @param mode Zap mode - * @param receiver Address to receive minted tokens - * @param writeContractAsync wagmi writeContractAsync function - * @returns Transaction hash - */ -export async function executeZapMint( - tokenAddress: Address, - amount: bigint, - mode: ZapMode, - receiver: Address, - writeContractAsync: (args: ReturnType) => Promise
, -): Promise
{ - const quote = await getZapQuote(tokenAddress, amount, mode); - const tx = buildZapMintTx( - tokenAddress, - amount, - mode, - receiver, - quote.ethCost, - quote.tokensOut, - ); - return writeContractAsync(tx); -} diff --git a/src/components/TradingWidget.tsx b/src/components/TradingWidget.tsx index f079c454..bcb396d4 100644 --- a/src/components/TradingWidget.tsx +++ b/src/components/TradingWidget.tsx @@ -6,12 +6,15 @@ import { useQuery } from "@tanstack/react-query"; import { parseUnits, formatUnits, type Address } from "viem"; import { browserClient as publicClient } from "../../lib/rpc"; import { mcv2BondAbi, erc20Abi } from "../../lib/price"; -import { MCV2_BOND, PLOT_TOKEN, RESERVE_LABEL, EXPLORER_URL, ZAP_PLOTLINK } from "../../lib/contracts/constants"; +import { + MCV2_BOND, PLOT_TOKEN, RESERVE_LABEL, EXPLORER_URL, + ZAP_PLOTLINK, SUPPORTED_ZAP_TOKENS, ETH_ADDRESS, +} from "../../lib/contracts/constants"; import { getZapQuote, buildZapMintTx } from "../../lib/zap"; type Tab = "buy" | "sell"; type TxState = "idle" | "approving" | "confirming" | "pending" | "done" | "error"; -type PayToken = "ETH" | "PLOT"; +type PayToken = "ETH" | "USDC" | "HUNT" | "PLOT"; const SLIPPAGE_BPS = 300; // 3% slippage tolerance @@ -24,6 +27,16 @@ function applySlippage(amount: bigint, isBuy: boolean): bigint { const isZapAvailable = ZAP_PLOTLINK !== "0x0000000000000000000000000000000000000000"; +function getTokenDecimals(payToken: PayToken): number { + if (payToken === "USDC") return 6; + return 18; +} + +function getTokenAddress(payToken: PayToken): Address { + const token = SUPPORTED_ZAP_TOKENS.find((t) => t.symbol === payToken); + return token?.address ?? ETH_ADDRESS as Address; +} + export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { const { address, isConnected } = useAccount(); const [tab, setTab] = useState("buy"); @@ -36,29 +49,58 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { const { writeContractAsync } = useWriteContract(); const { data: ethBalanceData, refetch: refetchEthBalance } = useBalance({ address }); + const isPlotMode = payToken === "PLOT" || !isZapAvailable; + const isEthMode = payToken === "ETH" && isZapAvailable; + const isErc20ZapMode = (payToken === "USDC" || payToken === "HUNT") && isZapAvailable; + const isZapMode = tab === "buy" && !isPlotMode && isZapAvailable; + const parsedAmount = amount && !isNaN(Number(amount)) && Number(amount) > 0 - ? parseUnits(amount, 18) + ? parseUnits(amount, 18) // storyline tokens are always 18 decimals : BigInt(0); - const isEthMode = tab === "buy" && payToken === "ETH" && isZapAvailable; - - // Batch balance + estimate into a single multicall (PLOT mode / sell) - const balanceToken = tab === "buy" ? PLOT_TOKEN : tokenAddress; const hasAmount = parsedAmount > BigInt(0); + // Balance token for PLOT mode / sell + const balanceToken = tab === "buy" && isPlotMode ? PLOT_TOKEN : tokenAddress; + // ERC-20 balance token for USDC/HUNT modes + const erc20BalanceToken = isErc20ZapMode ? getTokenAddress(payToken) : undefined; + const { data: tradeData, refetch: refetchTradeData } = useQuery({ - queryKey: ["trade-data", balanceToken, address, tab, tokenAddress, amount, payToken], + queryKey: ["trade-data", address, tab, tokenAddress, amount, payToken], queryFn: async () => { - if (isEthMode) { - // ETH mode: use zap quote instead of multicall + if (tab === "buy" && isZapMode) { + // Zap mode (ETH/USDC/HUNT): get quote from contract let zapQuote = null; + let erc20Balance: bigint | undefined; + if (hasAmount) { - zapQuote = await getZapQuote(tokenAddress, parsedAmount, "exact-output"); + try { + zapQuote = await getZapQuote( + getTokenAddress(payToken), + tokenAddress, + parsedAmount, + "exact-output", + ); + } catch { + zapQuote = null; + } + } + + // Fetch ERC-20 balance for USDC/HUNT + if (isErc20ZapMode && erc20BalanceToken && address) { + erc20Balance = await publicClient.readContract({ + address: erc20BalanceToken, + abi: erc20Abi, + functionName: "balanceOf", + args: [address], + }); } - return { balance: undefined, estimate: null, zapQuote }; + + return { balance: erc20Balance, estimate: null, zapQuote }; } + // PLOT mode or sell: existing multicall const contracts: Array<{ address: Address; abi: typeof erc20Abi | typeof mcv2BondAbi; functionName: string; args?: readonly unknown[] }> = [ { address: balanceToken, @@ -91,9 +133,17 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { refetchInterval: 60000, }); - const balance = isEthMode ? ethBalanceData?.value : tradeData?.balance; + // Resolve balance based on mode + const balance = (() => { + if (tab === "sell") return tradeData?.balance; + if (isEthMode) return ethBalanceData?.value; + if (isErc20ZapMode) return tradeData?.balance; + return tradeData?.balance; // PLOT mode + })(); + const estimate = tradeData?.estimate ?? null; const zapQuote = tradeData?.zapQuote ?? null; + const refetchBalance = useCallback(() => { refetchTradeData(); if (isEthMode) refetchEthBalance(); @@ -107,17 +157,39 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { setTxHash(null); let tradeHash: string | null = null; - if (isEthMode && zapQuote) { - // ETH mode: use ZapPlotLink + if (tab === "buy" && isZapMode && zapQuote) { + const fromToken = getTokenAddress(payToken); + + // ERC-20 zap tokens need approval to ZAP_PLOTLINK first + if (isErc20ZapMode) { + const allowance = await publicClient.readContract({ + address: fromToken, + abi: erc20Abi, + functionName: "allowance", + args: [address, ZAP_PLOTLINK], + }); + + if (allowance < zapQuote.fromTokenAmount) { + setTxState("approving"); + const approveHash = await writeContractAsync({ + address: fromToken, + abi: erc20Abi, + functionName: "approve", + args: [ZAP_PLOTLINK, zapQuote.fromTokenAmount], + }); + await publicClient.waitForTransactionReceipt({ hash: approveHash }); + } + } + setTxState("confirming"); - const tx = buildZapMintTx(tokenAddress, parsedAmount, "exact-output", address, zapQuote.ethCost); + const tx = buildZapMintTx(fromToken, tokenAddress, parsedAmount, "exact-output", zapQuote); const hash = await writeContractAsync(tx); setTxHash(hash); tradeHash = hash; setTxState("pending"); await publicClient.waitForTransactionReceipt({ hash }); - } else if (tab === "buy" && estimate) { - // PLOT mode: approve PLOT_TOKEN → mint + } else if (tab === "buy" && isPlotMode && estimate) { + // PLOT mode: approve PLOT_TOKEN → MCV2_Bond.mint const maxCost = applySlippage(estimate, true); const allowance = await publicClient.readContract({ @@ -204,7 +276,7 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { setError(err instanceof Error ? err.message : "Transaction failed"); setTxState("error"); } - }, [address, parsedAmount, estimate, zapQuote, tab, isEthMode, tokenAddress, writeContractAsync, refetchBalance]); + }, [address, parsedAmount, estimate, zapQuote, tab, payToken, isZapMode, isPlotMode, isErc20ZapMode, tokenAddress, writeContractAsync, refetchBalance]); const reset = useCallback(() => { setTxState("idle"); @@ -213,17 +285,21 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { }, []); // Pre-validate balance - const insufficientBalance = - balance !== undefined && - parsedAmount > BigInt(0) && - (isEthMode - ? zapQuote != null && zapQuote.ethCost > balance - : tab === "buy" - ? estimate != null && applySlippage(estimate, true) > balance - : parsedAmount > balance); + const insufficientBalance = (() => { + if (balance === undefined || parsedAmount <= BigInt(0)) return false; + if (tab === "sell") return parsedAmount > balance; + if (isZapMode && zapQuote) return zapQuote.fromTokenAmount > balance; + if (isPlotMode && estimate) return applySlippage(estimate, true) > balance; + return false; + })(); if (!isConnected) return null; + // Display helpers + const balanceDecimals = isEthMode ? 18 : isErc20ZapMode ? getTokenDecimals(payToken) : 18; + const balanceLabel = isZapMode ? payToken : tab === "buy" ? RESERVE_LABEL : "tokens"; + const estimateDecimals = isZapMode ? (isEthMode ? 18 : getTokenDecimals(payToken)) : 18; + return (

Trade

@@ -253,7 +329,7 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { {tab === "buy" && isZapAvailable && (
Pay with - {(["ETH", "PLOT"] as const).map((t) => ( + {(["ETH", "USDC", "HUNT", "PLOT"] as const).map((t) => ( ))}
@@ -303,7 +379,7 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { {balance !== undefined && (

- Balance: {formatUnits(balance, 18)} {isEthMode ? "ETH" : tab === "buy" ? RESERVE_LABEL : "tokens"} + Balance: {formatUnits(balance, balanceDecimals)} {balanceLabel}

)} {insufficientBalance && ( @@ -312,16 +388,16 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { {/* Estimate */} - {isEthMode && zapQuote && parsedAmount > BigInt(0) && ( + {isZapMode && zapQuote && parsedAmount > BigInt(0) && (
Est. cost:{" "} - {formatUnits(zapQuote.ethCost, 18)} ETH + {formatUnits(zapQuote.fromTokenAmount, estimateDecimals)} {payToken} - (incl. 0.5% swap slippage) + (incl. 3% slippage)
)} - {!isEthMode && estimate != null && parsedAmount > BigInt(0) && ( + {!isZapMode && estimate != null && parsedAmount > BigInt(0) && (
{tab === "buy" ? "Max cost" : "Min return"}:{" "} @@ -337,14 +413,14 @@ export function TradingWidget({ tokenAddress }: { tokenAddress: Address }) { disabled={ (txState === "idle" && ( parsedAmount === BigInt(0) || - (isEthMode ? !zapQuote : !estimate) || + (isZapMode ? !zapQuote : !estimate) || insufficientBalance )) || (txState !== "idle" && txState !== "done" && txState !== "error") } className="bg-accent text-background mt-3 w-full rounded py-2 text-xs font-medium transition-opacity disabled:opacity-40" > - {txState === "idle" && (tab === "buy" ? `Buy with ${isEthMode ? "ETH" : RESERVE_LABEL}` : "Sell Tokens")} + {txState === "idle" && (tab === "buy" ? `Buy with ${payToken === "PLOT" ? RESERVE_LABEL : payToken}` : "Sell Tokens")} {txState === "approving" && "Approving..."} {txState === "confirming" && "Confirm in wallet..."} {txState === "pending" && "Pending..."}