Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
at main 280 lines 7.9 kB view raw
1import { BASE_RPC_URL } from "@hey/data/constants"; 2import type { GetCoinResponse } from "@zoralabs/coins-sdk"; 3import { 4 createTradeCall, 5 type TradeParameters, 6 tradeCoin 7} from "@zoralabs/coins-sdk"; 8import { useEffect, useMemo, useState } from "react"; 9import { toast } from "sonner"; 10import type { Address } from "viem"; 11import { 12 createPublicClient, 13 erc20Abi, 14 formatEther, 15 formatUnits, 16 http, 17 parseEther, 18 parseUnits 19} from "viem"; 20import { base } from "viem/chains"; 21import { useAccount, useConfig, useWalletClient } from "wagmi"; 22import { getWalletClient } from "wagmi/actions"; 23import { Button, Image, Input, Tabs, Tooltip } from "@/components/Shared/UI"; 24import useHandleWrongNetwork from "@/hooks/useHandleWrongNetwork"; 25 26interface TradeModalProps { 27 coin: NonNullable<GetCoinResponse["zora20Token"]>; 28 onClose: () => void; 29} 30 31type Mode = "buy" | "sell"; 32 33const Trade = ({ coin, onClose }: TradeModalProps) => { 34 const { address } = useAccount(); 35 const config = useConfig(); 36 const { data: walletClient } = useWalletClient({ chainId: base.id }); 37 const publicClient = useMemo( 38 () => 39 createPublicClient({ 40 chain: base, 41 transport: http(BASE_RPC_URL, { batch: { batchSize: 30 } }) 42 }), 43 [] 44 ); 45 const handleWrongNetwork = useHandleWrongNetwork(); 46 47 const [mode, setMode] = useState<Mode>("buy"); 48 const [amount, setAmount] = useState(""); 49 const [loading, setLoading] = useState(false); 50 const [ethBalance, setEthBalance] = useState<bigint>(0n); 51 const [tokenBalance, setTokenBalance] = useState<bigint>(0n); 52 const [estimatedOut, setEstimatedOut] = useState<string>(""); 53 54 useEffect(() => { 55 (async () => { 56 if (!address) return; 57 try { 58 const [eth, token] = await Promise.all([ 59 publicClient.getBalance({ address }), 60 publicClient.readContract({ 61 abi: erc20Abi, 62 address: coin.address as Address, 63 args: [address], 64 functionName: "balanceOf" 65 }) 66 ]); 67 setEthBalance(eth); 68 setTokenBalance(token as bigint); 69 } catch {} 70 })(); 71 }, [address, coin.address, publicClient]); 72 73 const tokenDecimals = 18; 74 75 const setPercentAmount = (pct: number) => { 76 const decimals = 6; 77 if (mode === "buy") { 78 const available = Number(formatEther(ethBalance)); 79 const gasReserve = 0.0002; 80 const baseAmt = (available * pct) / 100; 81 const amt = pct === 100 ? Math.max(baseAmt - gasReserve, 0) : baseAmt; 82 setAmount(amt.toFixed(decimals)); 83 } else { 84 const available = Number(formatUnits(tokenBalance, tokenDecimals)); 85 const amt = Math.max((available * pct) / 100, 0); 86 setAmount(amt.toFixed(decimals)); 87 } 88 }; 89 90 const makeParams = (address: Address): TradeParameters | null => { 91 if (!amount || Number(amount) <= 0) return null; 92 93 if (mode === "buy") { 94 return { 95 amountIn: parseEther(amount), 96 buy: { address: coin.address as Address, type: "erc20" }, 97 sell: { type: "eth" }, 98 sender: address, 99 slippage: 0.1 100 }; 101 } 102 103 return { 104 amountIn: parseUnits(amount, tokenDecimals), 105 buy: { type: "eth" }, 106 sell: { address: coin.address as Address, type: "erc20" }, 107 sender: address, 108 slippage: 0.1 109 }; 110 }; 111 112 const handleSubmit = async () => { 113 if (!address) { 114 return toast.error("Connect a wallet to trade"); 115 } 116 117 const params = makeParams(address); 118 if (!params) return; 119 120 try { 121 setLoading(true); 122 await handleWrongNetwork({ chainId: base.id }); 123 const client = 124 (await getWalletClient(config, { chainId: base.id })) || walletClient; 125 if (!client) { 126 setLoading(false); 127 return toast.error("Please switch to Base network"); 128 } 129 130 await tradeCoin({ 131 account: client.account, 132 publicClient, 133 tradeParameters: params, 134 validateTransaction: false, 135 walletClient: client 136 }); 137 toast.success("Trade submitted"); 138 onClose(); 139 } catch { 140 toast.error("Trade failed"); 141 } finally { 142 setLoading(false); 143 } 144 }; 145 146 useEffect(() => { 147 let cancelled = false; 148 let intervalId: ReturnType<typeof setInterval> | undefined; 149 let timeoutId: ReturnType<typeof setTimeout> | undefined; 150 151 const run = async () => { 152 const sender = (address as Address) || undefined; 153 if (!sender || !amount) { 154 setEstimatedOut(""); 155 return; 156 } 157 158 const params: TradeParameters = 159 mode === "buy" 160 ? { 161 amountIn: parseEther(amount), 162 buy: { address: coin.address as Address, type: "erc20" }, 163 sell: { type: "eth" }, 164 sender, 165 slippage: 0.1 166 } 167 : { 168 amountIn: parseUnits(amount, tokenDecimals), 169 buy: { type: "eth" }, 170 sell: { address: coin.address as Address, type: "erc20" }, 171 sender, 172 slippage: 0.1 173 }; 174 175 try { 176 const q = await createTradeCall(params); 177 if (!cancelled) { 178 const out = q.quote.amountOut || "0"; 179 setEstimatedOut(out); 180 } 181 } catch { 182 if (!cancelled) setEstimatedOut(""); 183 } 184 }; 185 186 timeoutId = setTimeout(() => { 187 void run(); 188 }, 300); 189 190 intervalId = setInterval(() => { 191 void run(); 192 }, 8000); 193 194 return () => { 195 cancelled = true; 196 if (intervalId) clearInterval(intervalId); 197 if (timeoutId) clearTimeout(timeoutId); 198 }; 199 }, [address, amount, coin.address, mode]); 200 201 const symbol = coin.symbol || ""; 202 203 const balanceLabel = 204 mode === "buy" 205 ? `Balance: ${Number(formatEther(ethBalance)).toFixed(6)}` 206 : `Balance: ${Number(formatUnits(tokenBalance, tokenDecimals)).toFixed(3)}`; 207 208 return ( 209 <div className="p-5"> 210 <Tabs 211 active={mode} 212 className="mb-4" 213 layoutId="trade-mode" 214 setActive={(t) => setMode(t as Mode)} 215 tabs={[ 216 { name: "Buy", type: "buy" }, 217 { name: "Sell", type: "sell" } 218 ]} 219 /> 220 <div className="relative mb-2"> 221 <Input 222 inputMode="decimal" 223 label="Amount" 224 onChange={(e) => setAmount(e.target.value)} 225 placeholder={mode === "buy" ? "0.01" : "0"} 226 prefix={ 227 mode === "buy" ? ( 228 "ETH" 229 ) : ( 230 <Tooltip content={`$${symbol}`}> 231 <Image 232 alt={coin.name} 233 className="size-5 rounded-full" 234 height={20} 235 src={coin.mediaContent?.previewImage?.small} 236 width={20} 237 /> 238 </Tooltip> 239 ) 240 } 241 value={amount} 242 /> 243 </div> 244 <div className="mb-3 flex items-center justify-between text-gray-500 text-xs dark:text-gray-400"> 245 <div> 246 Estimated amount:{" "} 247 {estimatedOut 248 ? mode === "buy" 249 ? `${Number( 250 formatUnits(BigInt(estimatedOut), tokenDecimals) 251 ).toFixed(0)}` 252 : `${Number(formatEther(BigInt(estimatedOut))).toFixed(6)} ETH` 253 : "-"} 254 </div> 255 <div>{balanceLabel}</div> 256 </div> 257 <div className="mb-3 grid grid-cols-4 gap-2"> 258 {[25, 50, 75].map((p) => ( 259 <Button key={p} onClick={() => setPercentAmount(p)} outline> 260 {p}% 261 </Button> 262 ))} 263 <Button onClick={() => setPercentAmount(100)} outline> 264 Max 265 </Button> 266 </div> 267 <Button 268 className="mt-4 w-full" 269 disabled={!amount || !address} 270 loading={loading} 271 onClick={handleSubmit} 272 size="lg" 273 > 274 {mode === "buy" ? "Buy" : "Sell"} 275 </Button> 276 </div> 277 ); 278}; 279 280export default Trade;