App.js
1 import { useState, useEffect, useRef, useCallback } from "react"; 2 import { ethers, JsonRpcProvider } from "ethers"; 3 import Chart from "chart.js/auto"; 4 import "chartjs-adapter-date-fns"; 5 import contractConfig from "./contract-address.json"; 6 7 function App() { 8 const [orderType, setOrderType] = useState("buy"); 9 const [tradeType, setTradeType] = useState("limit"); 10 const [price, setPrice] = useState(""); 11 const [quantity, setQuantity] = useState(""); 12 const [balances, setBalances] = useState({ 0: "0", 1: "0", 2: "0" }); 13 const [orders, setOrders] = useState({}); 14 const [walletAddress, setWalletAddress] = useState(""); 15 const [selectedSecurityId, setSelectedSecurityId] = useState(0); 16 const [chartCache, setChartCache] = useState({}); 17 const [loadingCharts, setLoadingCharts] = useState({}); 18 const [loadedSecurities, setLoadedSecurities] = useState([0]); 19 const chartRef = useRef(null); 20 const chartInstance = useRef(null); 21 22 const contractAddress = contractConfig.address; 23 const rpcUrl = "https://sepolia.base.org"; 24 const alphaVantageKey = "YOUR_API_KEY"; // Replace with your Alpha Vantage API key 25 26 const securities = [ 27 { id: 0, symbol: "AAPL", name: "Apple" }, 28 { id: 1, symbol: "TSLA", name: "Tesla" }, 29 { id: 2, symbol: "NVDA", name: "Nvidia" } 30 ]; 31 32 const abi = [ 33 "function placeOrder(uint256 _securityId, bool _isBuy, uint256 _price, uint256 _quantity) external", 34 "function getOrderCount(uint256 _securityId) external view returns (uint256)", 35 "function getOrder(uint256 _securityId, uint256 _orderId) external view returns (address,bool,uint256,uint256,bool)", 36 "function getBalance(address _user, uint256 _securityId) external view returns (int256)", 37 "function cancelOrder(uint256 _securityId, uint256 _orderId) external", 38 "function cancelAllOrders(uint256 _securityId) external", 39 "event OrderPlaced(address indexed user, uint256 indexed securityId, bool isBuy, uint256 price, uint256 quantity, uint256 orderId)", 40 "event OrderMatched(address indexed buyer, address indexed seller, uint256 indexed securityId, uint256 price, uint256 quantity)" 41 ]; 42 43 const fetchChartData = useCallback(async (symbol) => { 44 try { 45 const response = await fetch( 46 `https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=${symbol}&interval=1min&apikey=${alphaVantageKey}` 47 ); 48 const data = await response.json(); 49 const timeSeries = data["Time Series (1min)"]; 50 if (!timeSeries) { 51 console.error("No time series data from Alpha Vantage:", data); 52 return null; 53 } 54 const chartData = Object.entries(timeSeries) 55 .map(([time, values]) => ({ 56 time, 57 price: parseFloat(values["4. close"]) 58 })) 59 .slice(0, 50) 60 .reverse(); 61 return chartData; 62 } catch (err) { 63 console.error(`Error fetching ${symbol} data:`, err); 64 return null; 65 } 66 }, [alphaVantageKey]); 67 68 const fetchOrders = useCallback(async (securityId) => { 69 try { 70 const provider = new JsonRpcProvider(rpcUrl); 71 const contract = new ethers.Contract(contractAddress, abi, provider); 72 const count = await contract.getOrderCount(securityId); 73 const activeOrders = []; 74 75 for (let i = 0; i < count; i++) { 76 const [user, isBuy, rawPrice, rawQty, active] = await contract.getOrder(securityId, i); 77 if (!active) continue; 78 const priceFloat = Number(ethers.formatUnits(rawPrice, 18)); 79 const qtyInt = rawQty.toString(); 80 activeOrders.push({ orderId: i, user, isBuy, price: priceFloat, quantity: qtyInt }); 81 console.log(`Order ${i} for ${securities[securityId].symbol}: ${isBuy ? "Buy" : "Sell"} ${qtyInt} @ ${priceFloat}`); 82 } 83 setOrders(prev => ({ ...prev, [securityId]: activeOrders })); 84 } catch (err) { 85 console.error(`Error fetching orders for security ${securityId}:`, err); 86 } 87 }, [securities]); 88 89 const fetchBalances = useCallback(async (addr) => { 90 try { 91 const provider = new JsonRpcProvider(rpcUrl); 92 const contract = new ethers.Contract(contractAddress, abi, provider); 93 const newBalances = {}; 94 for (let security of securities) { 95 const bal = await contract.getBalance(addr, security.id); 96 newBalances[security.id] = bal.toString(); 97 } 98 setBalances(newBalances); 99 } catch (err) { 100 console.error("Error fetching balances:", err); 101 } 102 }, [securities]); 103 104 const loadChartData = useCallback(async (securityId) => { 105 const symbol = securities.find(s => s.id === securityId).symbol; 106 107 if (chartCache[securityId]) { 108 return; 109 } 110 111 setLoadingCharts(prev => ({ ...prev, [securityId]: true })); 112 const timeout = setTimeout(() => { 113 if (!chartCache[securityId]) { 114 setLoadingCharts(prev => ({ ...prev, [securityId]: false })); 115 } 116 }, 10000); 117 118 const data = await fetchChartData(symbol); 119 clearTimeout(timeout); 120 121 if (data) { 122 setChartCache(prev => ({ ...prev, [securityId]: data })); 123 } 124 setLoadingCharts(prev => ({ ...prev, [securityId]: false })); 125 }, [chartCache, fetchChartData, securities]); 126 127 const setupChart = useCallback((data, symbol) => { 128 if (chartInstance.current) { 129 chartInstance.current.destroy(); 130 } 131 const ctx = chartRef.current.getContext("2d"); 132 chartInstance.current = new Chart(ctx, { 133 type: "line", 134 data: { 135 datasets: [{ 136 label: `${symbol} Price (USD)`, 137 data: data.map(d => ({ x: new Date(d.time), y: d.price })), 138 borderColor: "#00ff66", 139 fill: false, 140 tension: 0.1 141 }] 142 }, 143 options: { 144 responsive: true, 145 scales: { 146 x: { type: "time", time: { unit: "minute" }, title: { display: true, text: "Time" } }, 147 y: { title: { display: true, text: "Price (USD)" } } 148 } 149 } 150 }); 151 }, []); 152 153 const getWalletAddress = useCallback(async () => { 154 if (window.ethereum) { 155 const browserProvider = new ethers.BrowserProvider(window.ethereum); 156 await browserProvider.send("eth_requestAccounts", []); 157 const signer = await browserProvider.getSigner(); 158 const addr = await signer.getAddress(); 159 setWalletAddress(addr); 160 fetchBalances(addr); 161 } 162 }, [fetchBalances]); 163 164 const fetchAllData = useCallback(async () => { 165 for (let security of securities) { 166 await fetchOrders(security.id); 167 } 168 if (walletAddress) await fetchBalances(walletAddress); 169 }, [securities, walletAddress, fetchOrders, fetchBalances]); 170 171 // Initial setup 172 // eslint-disable-next-line react-hooks/exhaustive-deps 173 useEffect(() => { 174 getWalletAddress(); 175 fetchAllData(); 176 loadChartData(selectedSecurityId); 177 }, []); 178 179 // Security selection 180 useEffect(() => { 181 if (!loadedSecurities.includes(selectedSecurityId)) { 182 setLoadedSecurities(prev => [...prev, selectedSecurityId]); 183 loadChartData(selectedSecurityId); 184 } 185 fetchOrders(selectedSecurityId); 186 }, [selectedSecurityId, loadedSecurities, fetchOrders, loadChartData]); 187 188 // Chart rendering 189 // eslint-disable-next-line react-hooks/exhaustive-deps 190 useEffect(() => { 191 const symbol = securities.find(s => s.id === selectedSecurityId).symbol; 192 if (chartCache[selectedSecurityId] && chartRef.current) { 193 setupChart(chartCache[selectedSecurityId], symbol); 194 } 195 }, [chartCache, selectedSecurityId, setupChart]); 196 197 const handleOrderSelect = (selectedOrder) => { 198 const asks = (orders[selectedSecurityId] || []).filter(o => !o.isBuy).sort((a, b) => a.price - b.price); // Ascending for buys 199 const bids = (orders[selectedSecurityId] || []).filter(o => o.isBuy).sort((a, b) => b.price - a.price); // Descending for sells 200 201 let totalQuantity = 0; 202 if (!selectedOrder.isBuy) { // Selected an ask (sell order) → Buy order 203 setOrderType("buy"); 204 setTradeType("limit"); 205 setPrice(selectedOrder.price.toString()); 206 totalQuantity = asks 207 .filter(o => o.price <= selectedOrder.price) 208 .reduce((sum, o) => sum + parseInt(o.quantity, 10), 0); 209 } else { // Selected a bid (buy order) → Sell order 210 setOrderType("sell"); 211 setTradeType("limit"); 212 setPrice(selectedOrder.price.toString()); 213 totalQuantity = bids 214 .filter(o => o.price >= selectedOrder.price) 215 .reduce((sum, o) => sum + parseInt(o.quantity, 10), 0); 216 } 217 setQuantity(totalQuantity.toString()); 218 }; 219 220 const placeOrder = async () => { 221 try { 222 if (!quantity) { 223 alert("Please fill in Quantity."); 224 return; 225 } 226 const qtyInt = parseInt(quantity, 10); 227 if (isNaN(qtyInt) || qtyInt <= 0) { 228 alert("Quantity must be a positive integer."); 229 return; 230 } 231 if (!window.ethereum) { 232 alert("No wallet detected."); 233 return; 234 } 235 const browserProvider = new ethers.BrowserProvider(window.ethereum); 236 await browserProvider.send("eth_requestAccounts", []); 237 const signer = await browserProvider.getSigner(); 238 const contract = new ethers.Contract(contractAddress, abi, signer); 239 240 let priceWei; 241 if (tradeType === "limit") { 242 if (!price) { 243 alert("Please fill in Limit Price."); 244 return; 245 } 246 priceWei = ethers.parseUnits(price.toString(), 18); 247 console.log(`Placing limit ${orderType} order: ${qtyInt} @ ${price} (${priceWei.toString()} wei) for ${securities[selectedSecurityId].symbol}`); 248 } else { 249 priceWei = orderType === "buy" ? ethers.parseUnits("999999", 18) : ethers.parseUnits("0.01", 18); 250 console.log(`Placing market ${orderType} order: ${qtyInt} units for ${securities[selectedSecurityId].symbol}`); 251 } 252 253 const tx = await contract.placeOrder(selectedSecurityId, orderType === "buy", priceWei, qtyInt); 254 await tx.wait(); 255 console.log("Transaction confirmed:", tx.hash); 256 alert("Order placed. If matched, your balance will update."); 257 await fetchOrders(selectedSecurityId); 258 await fetchBalances(await signer.getAddress()); 259 setPrice(""); 260 setQuantity(""); 261 } catch (err) { 262 console.error("Error placing order:", err); 263 alert("Failed to place order: " + (err.reason || err.message)); 264 } 265 }; 266 267 const cancelOrder = async (orderId) => { 268 try { 269 if (!window.ethereum) return; 270 const browserProvider = new ethers.BrowserProvider(window.ethereum); 271 await browserProvider.send("eth_requestAccounts", []); 272 const signer = await browserProvider.getSigner(); 273 const contract = new ethers.Contract(contractAddress, abi, signer); 274 const tx = await contract.cancelOrder(selectedSecurityId, orderId); 275 await tx.wait(); 276 alert("Order cancelled."); 277 await fetchOrders(selectedSecurityId); 278 await fetchBalances(await signer.getAddress()); 279 } catch (err) { 280 console.error("Error cancelling order:", err); 281 alert("Failed to cancel order: " + (err.reason || err.message)); 282 } 283 }; 284 285 const cancelAllOrders = async () => { 286 try { 287 if (!window.ethereum) return; 288 const browserProvider = new ethers.BrowserProvider(window.ethereum); 289 await browserProvider.send("eth_requestAccounts", []); 290 const signer = await browserProvider.getSigner(); 291 const contract = new ethers.Contract(contractAddress, abi, signer); 292 const tx = await contract.cancelAllOrders(selectedSecurityId); 293 await tx.wait(); 294 alert("All orders cancelled."); 295 await fetchOrders(selectedSecurityId); 296 await fetchBalances(await signer.getAddress()); 297 } catch (err) { 298 console.error("Error cancelling all orders:", err); 299 alert("Failed to cancel all orders: " + (err.reason || err.message)); 300 } 301 }; 302 303 const asks = (orders[selectedSecurityId] || []).filter(o => !o.isBuy).sort((a, b) => b.price - a.price); 304 const bids = (orders[selectedSecurityId] || []).filter(o => o.isBuy).sort((a, b) => b.price - a.price); 305 const myOrders = (orders[selectedSecurityId] || []).filter(o => o.user.toLowerCase() === walletAddress.toLowerCase()); 306 const nonZeroBalances = securities.filter(security => parseInt(balances[security.id], 10) !== 0); 307 308 return ( 309 <div style={{ display: "flex", backgroundColor: "#121212", color: "#ffffff", height: "100vh" }}> 310 <div style={{ width: "150px", borderRight: "1px solid #2f2f2f", padding: "10px", overflowY: "auto" }}> 311 <h2 style={{ marginBottom: "10px" }}>Securities</h2> 312 {securities.map(security => ( 313 <button 314 key={security.id} 315 style={{ 316 width: "100%", 317 padding: "8px", 318 marginBottom: "5px", 319 backgroundColor: selectedSecurityId === security.id ? "#2f2f2f" : "#1a1a1a", 320 border: "none", 321 color: "#fff", 322 textAlign: "left", 323 cursor: "pointer" 324 }} 325 onClick={() => setSelectedSecurityId(security.id)} 326 > 327 {security.name} ({security.symbol}) 328 </button> 329 ))} 330 </div> 331 332 <div style={{ width: "300px", borderRight: "1px solid #2f2f2f", padding: "10px", overflowY: "auto" }}> 333 <h2 style={{ marginBottom: "10px" }}>Order Book - {securities.find(s => s.id === selectedSecurityId).symbol}</h2> 334 <div style={{ marginBottom: "20px" }}> 335 <h3 style={{ margin: "0 0 5px 0" }}>Asks</h3> 336 {asks.length > 0 ? ( 337 asks.map((o, i) => ( 338 <div 339 key={`ask-${i}`} 340 style={{ 341 color: "red", 342 fontSize: "0.9em", 343 padding: "4px", 344 cursor: "pointer", 345 backgroundColor: "#1a1a1a", 346 marginBottom: "2px", 347 borderRadius: "3px" 348 }} 349 onClick={() => handleOrderSelect(o)} 350 > 351 {o.quantity} @ {o.price.toFixed(2)} 352 </div> 353 )) 354 ) : ( 355 <p style={{ fontSize: "0.8em" }}>No asks</p> 356 )} 357 </div> 358 <div> 359 <h3 style={{ margin: "0 0 5px 0" }}>Bids</h3> 360 {bids.length > 0 ? ( 361 bids.map((o, i) => ( 362 <div 363 key={`bid-${i}`} 364 style={{ 365 color: "green", 366 fontSize: "0.9em", 367 padding: "4px", 368 cursor: "pointer", 369 backgroundColor: "#1a1a1a", 370 marginBottom: "2px", 371 borderRadius: "3px" 372 }} 373 onClick={() => handleOrderSelect(o)} 374 > 375 {o.quantity} @ {o.price.toFixed(2)} 376 </div> 377 )) 378 ) : ( 379 <p style={{ fontSize: "0.8em" }}>No bids</p> 380 )} 381 </div> 382 </div> 383 384 <div style={{ flex: 1, padding: "10px", overflowY: "auto" }}> 385 <h2 style={{ marginBottom: "10px" }}>{securities.find(s => s.id === selectedSecurityId).symbol} Price Chart</h2> 386 {loadingCharts[selectedSecurityId] ? ( 387 <div style={{ height: "300px" }}></div> 388 ) : chartCache[selectedSecurityId] ? ( 389 <canvas ref={chartRef}></canvas> 390 ) : ( 391 <p style={{ textAlign: "center", color: "#ccc" }}>Live data is not available</p> 392 )} 393 </div> 394 395 <div style={{ width: "300px", display: "flex", flexDirection: "column", overflowY: "auto" }}> 396 <div style={{ padding: "10px", borderBottom: "1px solid #2f2f2f" }}> 397 <h2 style={{ marginBottom: "10px" }}>Order</h2> 398 <div style={{ marginBottom: "10px" }}> 399 <label style={{ marginRight: "10px" }}> 400 <input 401 type="radio" 402 value="limit" 403 checked={tradeType === "limit"} 404 onChange={() => setTradeType("limit")} 405 /> Limit 406 </label> 407 <label> 408 <input 409 type="radio" 410 value="market" 411 checked={tradeType === "market"} 412 onChange={() => setTradeType("market")} 413 /> Market 414 </label> 415 </div> 416 <div style={{ display: "flex", marginBottom: "8px" }}> 417 <button 418 style={{ 419 flex: 1, 420 backgroundColor: orderType === "buy" ? "#0f6" : "#2f2f2f", 421 color: "#000", 422 padding: "6px", 423 border: "1px solid #555" 424 }} 425 onClick={() => setOrderType("buy")} 426 > 427 Buy 428 </button> 429 <button 430 style={{ 431 flex: 1, 432 backgroundColor: orderType === "sell" ? "red" : "#2f2f2f", 433 color: "#000", 434 padding: "6px", 435 border: "1px solid #555" 436 }} 437 onClick={() => setOrderType("sell")} 438 > 439 Sell 440 </button> 441 </div> 442 <div style={{ marginBottom: "10px" }}> 443 {tradeType === "limit" && ( 444 <> 445 <label style={{ display: "block", marginBottom: 4 }}>Limit Price</label> 446 <input 447 type="number" 448 step="0.01" 449 placeholder="e.g. 271.95" 450 value={price} 451 onChange={(e) => setPrice(e.target.value)} 452 style={{ width: "100%", padding: 6, marginBottom: 8 }} 453 /> 454 </> 455 )} 456 <label style={{ display: "block", marginBottom: 4 }}>Quantity</label> 457 <input 458 type="number" 459 step="1" 460 placeholder="e.g. 5" 461 value={quantity} 462 onChange={(e) => setQuantity(e.target.value)} 463 style={{ width: "100%", padding: 6 }} 464 /> 465 </div> 466 <button 467 style={{ 468 width: "100%", 469 backgroundColor: orderType === "buy" ? "#0f6" : "red", 470 color: "#000", 471 padding: "8px", 472 border: "none", 473 cursor: "pointer" 474 }} 475 onClick={placeOrder} 476 > 477 {orderType === "buy" ? `Buy ${securities.find(s => s.id === selectedSecurityId).symbol}` : `Sell ${securities.find(s => s.id === selectedSecurityId).symbol}`} 478 </button> 479 </div> 480 481 <div style={{ padding: "10px", flexGrow: 1 }}> 482 <h2 style={{ marginBottom: "10px" }}>Your Positions</h2> 483 <div style={{ backgroundColor: "#1a1a1a", padding: "10px", borderRadius: "5px" }}> 484 {nonZeroBalances.length > 0 ? ( 485 nonZeroBalances.map(security => ( 486 <div 487 key={security.id} 488 style={{ 489 display: "flex", 490 justifyContent: "space-between", 491 marginBottom: "8px", 492 padding: "5px", 493 backgroundColor: "#2f2f2f", 494 borderRadius: "3px" 495 }} 496 > 497 <span>{security.name} ({security.symbol})</span> 498 <span>{balances[security.id]}</span> 499 </div> 500 )) 501 ) : ( 502 <p style={{ fontSize: "0.8em", textAlign: "center" }}>No positions</p> 503 )} 504 </div> 505 </div> 506 507 <div style={{ padding: "10px", borderTop: "1px solid #2f2f2f" }}> 508 <h3 style={{ margin: "0 0 5px 0" }}>My Orders</h3> 509 <button 510 style={{ 511 width: "100%", 512 backgroundColor: "#555", 513 color: "#fff", 514 padding: "4px", 515 border: "none", 516 cursor: "pointer", 517 marginBottom: "5px" 518 }} 519 onClick={cancelAllOrders} 520 > 521 Cancel all 522 </button> 523 {myOrders.length > 0 ? ( 524 myOrders.map((o) => ( 525 <div 526 key={o.orderId} 527 style={{ 528 display: "flex", 529 justifyContent: "space-between", 530 alignItems: "center", 531 backgroundColor: "#1a1a1a", 532 padding: "4px", 533 marginBottom: "3px", 534 fontSize: "0.9em" 535 }} 536 > 537 <span> 538 {o.isBuy ? "Buy" : "Sell"} {o.quantity} @ {o.price.toFixed(2)} 539 </span> 540 <span 541 style={{ 542 cursor: "pointer", 543 color: "#ff5555", 544 fontWeight: "bold", 545 marginLeft: "10px" 546 }} 547 onClick={() => cancelOrder(o.orderId)} 548 > 549 × 550 </span> 551 </div> 552 )) 553 ) : ( 554 <p style={{ fontSize: "0.8em" }}>No orders</p> 555 )} 556 </div> 557 </div> 558 </div> 559 ); 560 } 561 562 export default App;