/ dapp / src / App.js
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;