diff --git a/contracts/src/Chainvoice.sol b/contracts/src/Chainvoice.sol index a729f8e8..662a07ad 100644 --- a/contracts/src/Chainvoice.sol +++ b/contracts/src/Chainvoice.sol @@ -1,13 +1,28 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.13; +interface IERC20 { + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function allowance( + address owner, + address spender + ) external view returns (uint256); +} + contract Chainvoice { struct InvoiceDetails { uint256 id; address from; address to; uint256 amountDue; + address tokenAddress; bool isPaid; + bool isCancelled; string encryptedInvoiceData; // Base64-encoded ciphertext string encryptedHash; } @@ -25,14 +40,23 @@ contract Chainvoice { event InvoiceCreated( uint256 indexed id, address indexed from, - address indexed to + address indexed to, + address tokenAddress ); event InvoicePaid( uint256 indexed id, address indexed from, address indexed to, - uint256 amount + uint256 amount, + address tokenAddress + ); + + event InvoiceCancelled( + uint256 indexed id, + address indexed from, + address indexed to, + address tokenAddress ); constructor() { @@ -48,12 +72,20 @@ contract Chainvoice { function createInvoice( address to, uint256 amountDue, + address tokenAddress, string memory encryptedInvoiceData, string memory encryptedHash ) external { require(to != address(0), "Recipient address is zero"); require(to != msg.sender, "Self-invoicing not allowed"); + if (tokenAddress != address(0)) { + require(tokenAddress.code.length > 0, "Not a contract address"); + (bool success, ) = tokenAddress.staticcall( + abi.encodeWithSignature("balanceOf(address)", address(this)) + ); + require(success, "Not an ERC20 token"); + } uint256 invoiceId = invoices.length; invoices.push( @@ -62,7 +94,9 @@ contract Chainvoice { from: msg.sender, to: to, amountDue: amountDue, + tokenAddress: tokenAddress, isPaid: false, + isCancelled: false, encryptedInvoiceData: encryptedInvoiceData, encryptedHash: encryptedHash }) @@ -71,7 +105,20 @@ contract Chainvoice { sentInvoices[msg.sender].push(invoiceId); receivedInvoices[to].push(invoiceId); - emit InvoiceCreated(invoiceId, msg.sender, to); + emit InvoiceCreated(invoiceId, msg.sender, to, tokenAddress); + } + + function cancelInvoice(uint256 invoiceId) external { + require(invoiceId < invoices.length, "Invalid invoice ID"); + InvoiceDetails storage invoice = invoices[invoiceId]; + + require(msg.sender == invoice.from, "Only invoice creator can cancel"); + require( + !invoice.isPaid && !invoice.isCancelled, + "Invoice not cancellable" + ); + invoice.isCancelled = true; + emit InvoiceCancelled(invoiceId, invoice.from, invoice.to, invoice.tokenAddress); } function payInvoice(uint256 invoiceId) external payable { @@ -80,16 +127,84 @@ contract Chainvoice { InvoiceDetails storage invoice = invoices[invoiceId]; require(msg.sender == invoice.to, "Not authorized"); require(!invoice.isPaid, "Already paid"); - require(msg.value == invoice.amountDue + fee, "Incorrect payment amount"); + require(!invoice.isCancelled, "Invoice is cancelled"); + + if (invoice.tokenAddress == address(0)) { + // Native token (ETH) payment + require( + msg.value == invoice.amountDue + fee, + "Incorrect payment amount" + ); + accumulatedFees += fee; + + uint256 amountToSender = msg.value - fee; + (bool sent, ) = payable(invoice.from).call{value: amountToSender}( + "" + ); + require(sent, "Transfer failed"); + } else { + // ERC20 token payment + require(msg.value == fee, "Must pay fee in native token"); + require( + IERC20(invoice.tokenAddress).allowance( + msg.sender, + address(this) + ) >= invoice.amountDue, + "Insufficient allowance" + ); + + accumulatedFees += fee; + bool transferSuccess = IERC20(invoice.tokenAddress).transferFrom( + msg.sender, + invoice.from, + invoice.amountDue + ); + require(transferSuccess, "Token transfer failed"); + } - accumulatedFees += fee; invoice.isPaid = true; + emit InvoicePaid( + invoiceId, + invoice.from, + invoice.to, + invoice.amountDue, + invoice.tokenAddress + ); + } - uint256 amountToSender = msg.value - fee; - (bool sent, ) = payable(invoice.from).call{value: amountToSender}(""); - require(sent, "Transfer failed"); + function getPaymentStatus( + uint256 invoiceId, + address payer + ) + external + view + returns (bool canPay, uint256 payerBalance, uint256 allowanceAmount) + { + require(invoiceId < invoices.length, "Invalid invoice ID"); + InvoiceDetails memory invoice = invoices[invoiceId]; + if (invoice.isCancelled) { + return (false, (payer).balance, 0); + } - emit InvoicePaid(invoiceId, invoice.from, invoice.to, msg.value); + if (invoice.tokenAddress == address(0)) { + return ( + payer.balance >= invoice.amountDue + fee, + payer.balance, + type(uint256).max // Native token has no allowance + ); + } else { + return ( + IERC20(invoice.tokenAddress).balanceOf(payer) >= + invoice.amountDue && + IERC20(invoice.tokenAddress).allowance( + payer, + address(this) + ) >= + invoice.amountDue, + IERC20(invoice.tokenAddress).balanceOf(payer), + IERC20(invoice.tokenAddress).allowance(payer, address(this)) + ); + } } function getSentInvoices( diff --git a/frontend/package.json b/frontend/package.json index 84beb46c..4abc1634 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@mui/material": "^6.4.6", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-popover": "^1.1.5", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.1.1", "@rainbow-me/rainbowkit": "^2.2.2", "@tanstack/react-query": "^5.64.1", @@ -42,6 +43,7 @@ "react-icons": "^5.5.0", "react-router-dom": "^7.1.1", "react-to-print": "^3.0.5", + "react-toastify": "^11.0.5", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "viem": "^2.22.9", diff --git a/frontend/public/dashboard.png b/frontend/public/dashboard.png new file mode 100644 index 00000000..e2145846 Binary files /dev/null and b/frontend/public/dashboard.png differ diff --git a/frontend/public/lit-protocol-diagram.png b/frontend/public/lit-protocol-diagram.png new file mode 100644 index 00000000..5430ef88 Binary files /dev/null and b/frontend/public/lit-protocol-diagram.png differ diff --git a/frontend/public/lit-protocol-logo.png b/frontend/public/lit-protocol-logo.png new file mode 100644 index 00000000..c01a5487 Binary files /dev/null and b/frontend/public/lit-protocol-logo.png differ diff --git a/frontend/public/token-select.png b/frontend/public/token-select.png new file mode 100644 index 00000000..c73e61ba Binary files /dev/null and b/frontend/public/token-select.png differ diff --git a/frontend/public/tokenImages/cin.png b/frontend/public/tokenImages/cin.png new file mode 100644 index 00000000..6a822a0d Binary files /dev/null and b/frontend/public/tokenImages/cin.png differ diff --git a/frontend/public/tokenImages/eth.png b/frontend/public/tokenImages/eth.png new file mode 100644 index 00000000..45002fe3 Binary files /dev/null and b/frontend/public/tokenImages/eth.png differ diff --git a/frontend/public/tokenImages/generic.png b/frontend/public/tokenImages/generic.png new file mode 100644 index 00000000..8214bc90 Binary files /dev/null and b/frontend/public/tokenImages/generic.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6a9ab5a3..2e40df4b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -18,7 +18,7 @@ import Feature from "./page/Feature"; import About from "./page/About"; import Working from "./page/Working"; import Treasure from "./page/Treasure"; -import CreateInvoice from "./components/CreateInvoice"; +import CreateInvoice from "./page/CreateInvoice"; import SentInvoice from "./page/SentInvoice"; import ReceivedInvoice from "./page/ReceivedInvoice"; @@ -41,7 +41,7 @@ function App() { */} -

+ {/*

© {new Date().getFullYear()} Chainvoice. All rights reserved. -

+

*/} diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index a88bc0fe..e53e3a9d 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -75,28 +75,34 @@ function Navbar() { } }; - const navItems = [ + // const navItems = [ + // { + // name: "Home", + // icon: , + // action: () => handleScroll("home-section"), + // path: "/", + // }, + // { + // name: "Features", + // icon: , + // action: () => handleScroll("feature-section"), + // path: "/#feature-section", + // }, + // { + // name: "Services", + // icon: , + // action: () => handleScroll("service-section"), + // path: "/#service-section", + // }, + // ]; + + const appItems = [ { name: "Home", icon: , action: () => handleScroll("home-section"), path: "/", }, - { - name: "Features", - icon: , - action: () => handleScroll("feature-section"), - path: "/#feature-section", - }, - { - name: "Services", - icon: , - action: () => handleScroll("service-section"), - path: "/#service-section", - }, - ]; - - const appItems = [ { name: "Dashboard", icon: , @@ -152,7 +158,7 @@ function Navbar() { {/* Desktop Navigation */}
- {navItems.map((item) => ( + {/* {navItems.map((item) => ( {item.icon} {item.name} - ))} - + ))} */} {isConnected && appItems.map((item) => (
{/* Navigation Links */} - {navItems.map((item) => ( + {/* {navItems.map((item) => ( {item.icon} {item.name} - ))} - + ))} */} {isConnected && appItems.map((item) => ( { + const carouselRef = useRef(); + const duplicatedTokens = [...TOKEN_PRESETS, ...TOKEN_PRESETS]; // Double the tokens for seamless loop + + useEffect(() => { + const carousel = carouselRef.current; + let animationFrame; + let speed = 1; // Pixels per frame + let position = 0; + + const animate = () => { + position -= speed; + if (position <= -carousel.scrollWidth / 2) { + position = 0; + } + carousel.style.transform = `translateX(${position}px)`; + animationFrame = requestAnimationFrame(animate); + }; + + animate(); + + // Pause on hover + const pause = () => cancelAnimationFrame(animationFrame); + const resume = () => { + cancelAnimationFrame(animationFrame); + animate(); + }; + + carousel.addEventListener("mouseenter", pause); + carousel.addEventListener("mouseleave", resume); + + return () => { + cancelAnimationFrame(animationFrame); + carousel.removeEventListener("mouseenter", pause); + carousel.removeEventListener("mouseleave", resume); + }; + }, []); + + return ( +
+
+ + {duplicatedTokens.map((token, index) => ( + +
+
+ {token.symbol} { + e.target.src = "/tokenImages/default.png"; + }} + /> + {token.address === + "0x0000000000000000000000000000000000000000" && ( +
+ +
+ )} +
+
+

{token.symbol}

+

{token.name}

+
+
+
+ ))} +
+
+
+ ); +}; + +export default TokenCarousel; diff --git a/frontend/src/components/TokenIntegrationRequest.jsx b/frontend/src/components/TokenIntegrationRequest.jsx new file mode 100644 index 00000000..63f146da --- /dev/null +++ b/frontend/src/components/TokenIntegrationRequest.jsx @@ -0,0 +1,16 @@ +function TokenIntegrationRequest({ address }) { + return ( +
+ Using this token frequently?{" "} + + Request adding to default list + +
+ ); +} + +export default TokenIntegrationRequest; diff --git a/frontend/src/components/TokenSelector.jsx b/frontend/src/components/TokenSelector.jsx new file mode 100644 index 00000000..4fbcf087 --- /dev/null +++ b/frontend/src/components/TokenSelector.jsx @@ -0,0 +1,331 @@ +import React, { useRef, useState } from "react"; +import { TOKEN_PRESETS } from "@/utils/erc20_token"; +import { Label } from "@radix-ui/react-label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "./ui/input"; +import { PlusIcon } from "lucide-react"; +import { Button } from "./ui/button"; +const POPULAR_TOKENS = [ + "0x0000000000000000000000000000000000000000", // ETH + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // USDC + "0xdac17f958d2ee523a2206206994597c13d831ec7", // USDT + "0x6b175474e89094c44da98b954eedeac495271d0f", // DAI +]; + +function TokenSelector() { + const [loading, setLoading] = useState(false); + const [selectedToken, setSelectedToken] = useState(TOKEN_PRESETS[0]); + const [customTokenAddress, setCustomTokenAddress] = useState(""); + const [useCustomToken, setUseCustomToken] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const inputRef = useRef(null); + const filteredTokens = TOKEN_PRESETS.filter( + (token) => + token.name.toLowerCase().includes(searchTerm.toLowerCase()) || + token.symbol.toLowerCase().includes(searchTerm.toLowerCase()) + ); + return ( + <> +
+

+ + + + + + + Payment Currency +

+ +
+
+
+ + setSearchTerm(e.target.value)} + /> +
+ {!searchTerm && ( +
+
+ Popular +
+ {TOKEN_PRESETS.filter((token) => + POPULAR_TOKENS.includes(token.address) + ).map((token) => ( + +
+
+ {token.name} { + e.currentTarget.src = + "/tokenImages/generic.png"; + }} + /> +
+
+ + {token.name} + + + {token.symbol} + +
+
+
+ ))} +
+ )} +
+
+ {searchTerm ? "Search Results" : "All Tokens"} +
+
+ {filteredTokens + .filter( + (token) => !POPULAR_TOKENS.includes(token.address) + ) + .map((token) => ( + +
+
+ {token.name} { + e.currentTarget.src = + "/tokenImages/generic.png"; + }} + /> +
+
+ + {token.name} + + + {token.symbol} + +
+
+
+ ))} +
+ {filteredTokens.length === 0 && ( +
+ No tokens found +
+ )} +
+ + +
+
+ +
+ + Custom Token + +
+
+ + +
+ + {!useCustomToken && ( +
+ +
+
+
+ {selectedToken.name} +
+
+ + {selectedToken.name} + + + {selectedToken.symbol} + +
+ {selectedToken.address} +
+
+
+
+
+ )} +
+ + {useCustomToken && ( +
+
+
+ + setCustomTokenAddress(e.target.value)} + disabled={loading} + /> +

+ Enter a valid ERC-20 token contract address. Make sure to + verify the token details before proceeding. +

+
+ {customTokenAddress && ( +
+
+
+
+ + + + + +
+
+

+ Custom Token Selected +

+

+ {customTokenAddress} +

+
+
+
+ +
+ )} +
+
+ )} + +
+

+ {useCustomToken ? ( + "Please verify the token contract address before creating the invoice." + ) : ( + <> + Note:{" "} + Payments will be processed in {selectedToken.symbol}. Ensure + your client has sufficient balance of this token. + + )} +

+
+
+
+ + ); +} + +export default TokenSelector; diff --git a/frontend/src/components/ui/select.jsx b/frontend/src/components/ui/select.jsx new file mode 100644 index 00000000..b1367bf2 --- /dev/null +++ b/frontend/src/components/ui/select.jsx @@ -0,0 +1,119 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props}> + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/frontend/src/contractsABI/ChainvoiceABI.js b/frontend/src/contractsABI/ChainvoiceABI.js index 9baa2a01..2639dae6 100644 --- a/frontend/src/contractsABI/ChainvoiceABI.js +++ b/frontend/src/contractsABI/ChainvoiceABI.js @@ -17,6 +17,19 @@ export const ChainvoiceABI = [ ], stateMutability: "view", }, + { + type: "function", + name: "cancelInvoice", + inputs: [ + { + name: "invoiceId", + type: "uint256", + internalType: "uint256", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, { type: "function", name: "createInvoice", @@ -31,6 +44,11 @@ export const ChainvoiceABI = [ type: "uint256", internalType: "uint256", }, + { + name: "tokenAddress", + type: "address", + internalType: "address", + }, { name: "encryptedInvoiceData", type: "string", @@ -94,11 +112,21 @@ export const ChainvoiceABI = [ type: "uint256", internalType: "uint256", }, + { + name: "tokenAddress", + type: "address", + internalType: "address", + }, { name: "isPaid", type: "bool", internalType: "bool", }, + { + name: "isCancelled", + type: "bool", + internalType: "bool", + }, { name: "encryptedInvoiceData", type: "string", @@ -114,6 +142,40 @@ export const ChainvoiceABI = [ ], stateMutability: "view", }, + { + type: "function", + name: "getPaymentStatus", + inputs: [ + { + name: "invoiceId", + type: "uint256", + internalType: "uint256", + }, + { + name: "payer", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "canPay", + type: "bool", + internalType: "bool", + }, + { + name: "payerBalance", + type: "uint256", + internalType: "uint256", + }, + { + name: "allowanceAmount", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, { type: "function", name: "getReceivedInvoices", @@ -150,11 +212,21 @@ export const ChainvoiceABI = [ type: "uint256", internalType: "uint256", }, + { + name: "tokenAddress", + type: "address", + internalType: "address", + }, { name: "isPaid", type: "bool", internalType: "bool", }, + { + name: "isCancelled", + type: "bool", + internalType: "bool", + }, { name: "encryptedInvoiceData", type: "string", @@ -206,11 +278,21 @@ export const ChainvoiceABI = [ type: "uint256", internalType: "uint256", }, + { + name: "tokenAddress", + type: "address", + internalType: "address", + }, { name: "isPaid", type: "bool", internalType: "bool", }, + { + name: "isCancelled", + type: "bool", + internalType: "bool", + }, { name: "encryptedInvoiceData", type: "string", @@ -257,11 +339,21 @@ export const ChainvoiceABI = [ type: "uint256", internalType: "uint256", }, + { + name: "tokenAddress", + type: "address", + internalType: "address", + }, { name: "isPaid", type: "bool", internalType: "bool", }, + { + name: "isCancelled", + type: "bool", + internalType: "bool", + }, { name: "encryptedInvoiceData", type: "string", @@ -395,6 +487,37 @@ export const ChainvoiceABI = [ outputs: [], stateMutability: "nonpayable", }, + { + type: "event", + name: "InvoiceCancelled", + inputs: [ + { + name: "id", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "from", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "to", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "tokenAddress", + type: "address", + indexed: false, + internalType: "address", + }, + ], + anonymous: false, + }, { type: "event", name: "InvoiceCreated", @@ -417,6 +540,12 @@ export const ChainvoiceABI = [ indexed: true, internalType: "address", }, + { + name: "tokenAddress", + type: "address", + indexed: false, + internalType: "address", + }, ], anonymous: false, }, @@ -448,6 +577,12 @@ export const ChainvoiceABI = [ indexed: false, internalType: "uint256", }, + { + name: "tokenAddress", + type: "address", + indexed: false, + internalType: "address", + }, ], anonymous: false, }, diff --git a/frontend/src/contractsABI/ERC20_ABI.js b/frontend/src/contractsABI/ERC20_ABI.js new file mode 100644 index 00000000..b1f283c8 --- /dev/null +++ b/frontend/src/contractsABI/ERC20_ABI.js @@ -0,0 +1,10 @@ +export const ERC20_ABI = [ + "function approve(address spender, uint256 amount) external returns (bool)", + "function transfer(address recipient, uint256 amount) external returns (bool)", + "function transferFrom(address sender, address recipient, uint256 amount) external returns (bool)", + "function balanceOf(address account) external view returns (uint256)", + "function allowance(address owner, address spender) external view returns (uint256)", + "function name() view returns (string)", + "function symbol() view returns (string)", + "function decimals() view returns (uint8)", +]; diff --git a/frontend/src/page/Applayout.jsx b/frontend/src/page/Applayout.jsx index 8b459d0c..f2886e90 100644 --- a/frontend/src/page/Applayout.jsx +++ b/frontend/src/page/Applayout.jsx @@ -6,9 +6,9 @@ import { Outlet } from "react-router-dom"; function Applayout() { return ( -
+
-
+
diff --git a/frontend/src/page/CreateInvoice.jsx b/frontend/src/page/CreateInvoice.jsx new file mode 100644 index 00000000..d994bd95 --- /dev/null +++ b/frontend/src/page/CreateInvoice.jsx @@ -0,0 +1,1073 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Input } from "../components/ui/input"; +import { Button } from "../components/ui/button"; +import { + BrowserProvider, + Contract, + ethers, + formatUnits, + parseUnits, +} from "ethers"; +import { useAccount, useWalletClient } from "wagmi"; +import { ChainvoiceABI } from "../contractsABI/ChainvoiceABI"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Calendar } from "@/components/ui/calendar"; +import { + CalendarIcon, + CheckCircle2, + Loader2, + PlusIcon, + XCircle, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { format } from "date-fns"; +import { Label } from "../components/ui/label"; +import { useNavigate } from "react-router-dom"; + +import { LitNodeClient } from "@lit-protocol/lit-node-client"; +import { encryptString } from "@lit-protocol/encryption/src/lib/encryption.js"; +import { LIT_ABILITY, LIT_NETWORK } from "@lit-protocol/constants"; +import { + createSiweMessageWithRecaps, + generateAuthSig, + LitAccessControlConditionResource, +} from "@lit-protocol/auth-helpers"; + +// Add this near the top with other imports +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { TOKEN_PRESETS } from "@/utils/erc20_token"; +import TokenIntegrationRequest from "@/components/TokenIntegrationRequest"; +import { ERC20_ABI } from "@/contractsABI/ERC20_ABI"; +function CreateInvoice() { + const { data: walletClient } = useWalletClient(); + const { isConnected } = useAccount(); + const account = useAccount(); + const [dueDate, setDueDate] = useState(new Date()); + const [issueDate, setIssueDate] = useState(new Date()); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const litClientRef = useRef(null); + + // Add these state variables + const [selectedToken, setSelectedToken] = useState(TOKEN_PRESETS[0]); + const [customTokenAddress, setCustomTokenAddress] = useState(""); + const [useCustomToken, setUseCustomToken] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const inputRef = useRef(null); + const [tokenVerificationState, setTokenVerificationState] = useState("idle"); + const [verifiedToken, setVerifiedToken] = useState(null); + const filteredTokens = TOKEN_PRESETS.filter( + (token) => + token.name.toLowerCase().includes(searchTerm.toLowerCase()) || + token.symbol.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const POPULAR_TOKENS = [ + "0x0000000000000000000000000000000000000000", // ETH + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // USDC + "0xdac17f958d2ee523a2206206994597c13d831ec7", // USDT + "0x6b175474e89094c44da98b954eedeac495271d0f", // DAI + ]; + const [itemData, setItemData] = useState([ + { + description: "", + qty: "", + unitPrice: "", + discount: "", + tax: "", + amount: "", + }, + ]); + + const [totalAmountDue, setTotalAmountDue] = useState(0); + + useEffect(() => { + const total = itemData.reduce((sum, item) => { + const qty = parseUnits(item.qty || "0", 18); + const unitPrice = parseUnits(item.unitPrice || "0", 18); + const discount = parseUnits(item.discount || "0", 18); + const tax = parseUnits(item.tax || "0", 18); + // qty * price (then divide by 1e18 to cancel double scaling) + const lineTotal = (qty * unitPrice) / parseUnits("1", 18); + const adjusted = lineTotal - discount + tax; + + return sum + adjusted; + }, 0n); + + setTotalAmountDue(formatUnits(total, 18)); + }, [itemData]); + + useEffect(() => { + const initLit = async () => { + if (!litClientRef.current) { + const client = new LitNodeClient({ + litNetwork: LIT_NETWORK.DatilDev, + debug: false, + }); + await client.connect(); + litClientRef.current = client; + console.log(litClientRef.current); + } + }; + initLit(); + }, []); + + const handleItemData = (e, index) => { + const { name, value } = e.target; + + setItemData((prevItemData) => + prevItemData.map((item, i) => { + if (i === index) { + const updatedItem = { ...item, [name]: value }; + if ( + name === "qty" || + name === "unitPrice" || + name === "discount" || + name === "tax" + ) { + const qty = parseUnits(updatedItem.qty || "0", 18); + const unitPrice = parseUnits(updatedItem.unitPrice || "0", 18); + const discount = parseUnits(updatedItem.discount || "0", 18); + const tax = parseUnits(updatedItem.tax || "0", 18); + + const lineTotal = (qty * unitPrice) / parseUnits("1", 18); + const finalAmount = lineTotal - discount + tax; + + updatedItem.amount = formatUnits(finalAmount, 18); + } + return updatedItem; + } + return item; + }) + ); + }; + + const addItem = () => { + setItemData((prev) => [ + ...prev, + { + description: "", + qty: "", + unitPrice: "", + discount: "", + tax: "", + amount: "", + }, + ]); + }; + + const verifyToken = async (address) => { + setTokenVerificationState("verifying"); + + try { + const provider = new BrowserProvider(walletClient); + const contract = new ethers.Contract(address, ERC20_ABI, provider); + + const [symbol, name, decimals] = await Promise.all([ + contract.symbol().catch(() => "UNKNOWN"), + contract.name().catch(() => "Unknown Token"), + contract.decimals().catch(() => 18), + ]); + setVerifiedToken({ address, symbol, name, decimals }); + setTokenVerificationState("success"); + } catch (error) { + console.error("Verification failed:", error); + setTokenVerificationState("error"); + } + }; + + const createInvoiceRequest = async (data) => { + if (!isConnected || !walletClient) { + alert("Please connect your wallet"); + return; + } + + try { + setLoading(true); + const provider = new BrowserProvider(walletClient); + const signer = await provider.getSigner(); + + // Determine the token to use + const paymentToken = useCustomToken ? verifiedToken : selectedToken; + + // 1. Prepare invoice data + const invoicePayload = { + amountDue: totalAmountDue.toString(), + dueDate, + issueDate, + paymentToken: { + address: paymentToken.address, + symbol: paymentToken.symbol, + decimals: Number(paymentToken.decimals), + }, + user: { + address: account?.address.toString(), + fname: data.userFname, + lname: data.userLname, + email: data.userEmail, + country: data.userCountry, + city: data.userCity, + postalcode: data.userPostalcode, + }, + client: { + address: data.clientAddress, + fname: data.clientFname, + lname: data.clientLname, + email: data.clientEmail, + country: data.clientCountry, + city: data.clientCity, + postalcode: data.clientPostalcode, + }, + items: itemData, + }; + + const invoiceString = JSON.stringify(invoicePayload); + + // 2. Setup Lit + const litNodeClient = litClientRef.current; + if (!litNodeClient) { + alert("Lit client not initialized"); + return; + } + const accessControlConditions = [ + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: account.address.toLowerCase(), + }, + }, + { operator: "or" }, + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: data.clientAddress.toLowerCase(), + }, + }, + ]; + + // 3. Encrypt + const { ciphertext, dataToEncryptHash } = await encryptString( + { + accessControlConditions, + dataToEncrypt: invoiceString, + }, + litNodeClient + ); + + const sessionSigs = await litNodeClient.getSessionSigs({ + chain: "ethereum", + resourceAbilityRequests: [ + { + resource: new LitAccessControlConditionResource("*"), + ability: LIT_ABILITY.AccessControlConditionDecryption, + }, + ], + authNeededCallback: async ({ + uri, + expiration, + resourceAbilityRequests, + }) => { + const nonce = await litNodeClient.getLatestBlockhash(); + const toSign = await createSiweMessageWithRecaps({ + uri, + expiration, + resources: resourceAbilityRequests, + walletAddress: account.address, + nonce, + litNodeClient, + }); + + return await generateAuthSig({ + signer, + toSign, + }); + }, + }); + + const encryptedStringBase64 = btoa(ciphertext); + + // 4. Send to contract + const contract = new Contract( + import.meta.env.VITE_CONTRACT_ADDRESS, + ChainvoiceABI, + signer + ); + const tx = await contract.createInvoice( + data.clientAddress, + ethers.parseUnits(totalAmountDue.toString(), paymentToken.decimals), + paymentToken.address, + encryptedStringBase64, + dataToEncryptHash + ); + + const receipt = await tx.wait(); + setTimeout(() => navigate("/dashboard/sent"), 4000); + } catch (err) { + console.error("Encryption or transaction failed:", err); + alert("Failed to create invoice."); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + const formData = new FormData(e.target); + + // User detail + const userAddress = formData.get("userAddress"); + const userFname = formData.get("userFname"); + const userLname = formData.get("userLname"); + const userEmail = formData.get("userEmail"); + const userCountry = formData.get("userCountry"); + const userCity = formData.get("userCity"); + const userPostalcode = formData.get("userPostalcode"); + + // Client detail + const clientAddress = formData.get("clientAddress"); + const clientFname = formData.get("clientFname"); + const clientLname = formData.get("clientLname"); + const clientEmail = formData.get("clientEmail"); + const clientCountry = formData.get("clientCountry"); + const clientCity = formData.get("clientCity"); + const clientPostalcode = formData.get("clientPostalcode"); + + const data = { + userAddress, + userFname, + userLname, + userEmail, + userCountry, + userCity, + userPostalcode, + clientAddress, + clientFname, + clientLname, + clientEmail, + clientCountry, + clientCity, + clientPostalcode, + itemData, + }; + console.log(data); + await createInvoiceRequest(data); + }; + + return ( +
+

+ Create New Invoice Request +

+ +
+
+ + +
+ +
+ + +
+ +
+ + + + + + + { + if (date) { + setDueDate(date); + document.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape" }) + ); + } + }} + initialFocus + disabled={(date) => date < new Date()} + /> + + +
+
+ +
+
+ {/* Your Information */} +
+

+ From (Your Information) +

+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+
+ {/* Client Information */} +
+

+ Client Information +

+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+
+
+ +
+

+ + + + + + + Payment Currency +

+ +
+
+
+ + setSearchTerm(e.target.value)} + /> +
+ {!searchTerm && ( +
+
+ Popular +
+ {TOKEN_PRESETS.filter((token) => + POPULAR_TOKENS.includes(token.address) + ).map((token) => ( + +
+
+ {token.name} { + e.currentTarget.src = + "/tokenImages/generic.png"; + }} + /> +
+
+ + {token.name} + + + {token.symbol} + +
+
+
+ ))} +
+ )} +
+
+ {searchTerm ? "Search Results" : "All Tokens"} +
+
+ {filteredTokens + .filter( + (token) => !POPULAR_TOKENS.includes(token.address) + ) + .map((token) => ( + +
+
+ {token.name} { + e.currentTarget.src = + "/tokenImages/generic.png"; + }} + /> +
+
+ + {token.name} + + + {token.symbol} + +
+
+
+ ))} +
+ {filteredTokens.length === 0 && ( +
+ No tokens found +
+ )} +
+ + +
+
+ +
+ + Custom Token + +
+
+ + +
+ + {!useCustomToken && ( +
+ +
+
+
+ {selectedToken.name} +
+
+ + {selectedToken.name} + + + {selectedToken.symbol} + +
+ {selectedToken.address} +
+
+
+
+
+ )} +
+ + {useCustomToken && ( +
+
+ + { + const address = e.target.value; + setCustomTokenAddress(address); + if (!address || !ethers.isAddress(address)) { + setTokenVerificationState("idle"); + setVerifiedToken(null); + } else if (ethers.isAddress(address)) { + verifyToken(address); + } + }} + className="h-10 bg-gray-50 text-gray-700 border-gray-200 focus:ring-2 focus:ring-blue-100" + disabled={loading} + /> +

+ Enter a valid ERC-20 token contract address +

+
+ + {tokenVerificationState === "verifying" && ( +
+ + Verifying token... +
+ )} + + {tokenVerificationState === "success" && verifiedToken && ( +
+
+
+ +
+

+ {verifiedToken.name} ({verifiedToken.symbol}) +

+

+ {verifiedToken.address} +

+

+

Decimals: {String(verifiedToken.decimals)}

+

+
+
+
+ +
+ )} + + {tokenVerificationState === "error" && ( +
+
+ +

+ Failed to verify token. Please check the address. +

+
+
+ )} +
+ )} + +
+

+ {useCustomToken ? ( + verifiedToken ? ( + <> + Note:{" "} + Payments will be processed in {verifiedToken?.symbol}. + Ensure your client has sufficient balance of this token. + + ) : ( + "" + ) + ) : ( + <> + Note:{" "} + Payments will be processed in {selectedToken.symbol}. Ensure + your client has sufficient balance of this token. + + )} +

+
+
+
+ {/* Invoice Items Section */} +
+
+
DESCRIPTION
+
QTY
+
UNIT PRICE
+
DISCOUNT
+
TAX(%)
+
AMOUNT
+ {/*
AMOUNT
*/} +
+ +
+ {itemData.map((_, index) => ( +
+ {/* Item Fields */} +
+ handleItemData(e, index)} + /> +
+
+ handleItemData(e, index)} + /> +
+
+ handleItemData(e, index)} + /> +
+
+ handleItemData(e, index)} + /> +
+
+ handleItemData(e, index)} + /> +
+
+ +
+ + {index > 0 && ( + + )} +
+ ))} +
+
+ + +
+
+ Total: + + {totalAmountDue}{" "} + {useCustomToken ? verifiedToken?.symbol : selectedToken.symbol} + +
+
+
+
+ + {/* Form Actions */} +
+ +
+ +
+ ); +} + +export default CreateInvoice; diff --git a/frontend/src/page/Home.jsx b/frontend/src/page/Home.jsx index cc80256b..b6dbdebf 100644 --- a/frontend/src/page/Home.jsx +++ b/frontend/src/page/Home.jsx @@ -39,8 +39,8 @@ export default function Home() { return (
-
-

+
+

Welcome Back!

@@ -129,7 +129,7 @@ export default function Home() { component="main" sx={{ flexGrow: 1, - p: 3, + px: 1, maxHeight: "calc(100vh - 180px)", overflowY: "auto", scrollbarWidth: "none", @@ -137,7 +137,7 @@ export default function Home() { display: "none", }, transition: "all 0.3s ease", - borderLeft: { lg: "2px solid #1f2937" }, // Only show border on desktop + borderLeft: { lg: "2px solid #1f2937" }, }} className="text-white" > diff --git a/frontend/src/page/Landing.jsx b/frontend/src/page/Landing.jsx index 4f87f6a5..34054e44 100644 --- a/frontend/src/page/Landing.jsx +++ b/frontend/src/page/Landing.jsx @@ -1,221 +1,406 @@ import { ConnectButton } from "@rainbow-me/rainbowkit"; import React, { useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import { useAccount } from "wagmi"; -import ShieldIcon from "@mui/icons-material/Shield"; -import EmailIcon from "@mui/icons-material/Email"; -import GavelIcon from "@mui/icons-material/Gavel"; -import LeaderboardIcon from "@mui/icons-material/Leaderboard"; -function Landing() { - const Account = useAccount(); - const navigate = useNavigate(); +import { motion } from "framer-motion"; +import { + FiShield, + FiMail, + FiCode, + FiTrendingUp, + FiLock, + FiZap, +} from "react-icons/fi"; +import { SiEthereum } from "react-icons/si"; +import { LockIcon } from "lucide-react"; +import TokenCarousel from "@/components/TokenCrousel"; - // useEffect(() => { - // if (Account.address) navigate("/home/sent"); - // }, [Account, Account.address]); +function Landing() { + useEffect(() => { + // Smooth scroll for anchor links + document.querySelectorAll('a[href^="#"]').forEach((anchor) => { + anchor.addEventListener("click", function (e) { + e.preventDefault(); + document.querySelector(this.getAttribute("href")).scrollIntoView({ + behavior: "smooth", + }); + }); + }); + }, []); return ( - <> -
-
-

- Decentralized Payment Requests & -
- Invoice Automation -

-

- One click to effortless invoicing — process payments in any ERC20 - token with transparency and trustless efficiency! -

- {/*

Only with

-
Chainvoice
*/} - -

- Connect with your wallet and Get Started! -

-
- -
+
+ {/* Hero Section */} +
+
+
+
-
-

- - Trusted by 1000+ users -

-

- - Smart Contract Driven 100% Secure -

+
+
+
+ + + Web3 Invoicing + {" "} +
+ Made Simple +
+ +

+ End-to-end encrypted, multi-chain invoicing with Lit Protocol + and support for 1000+ ERC20 tokens. +

+ +
+
+
+ Lit Protocol + Lit Protocol Encrypted +
+
+ + Multi-chain Support +
+
+ + End-to-End Security +
+
+
+
+ +
+ + Secure Invoice Dashboard +
+
+ Lit Protocol +

+ Encrypted with Lit Protocol +

+
+
+
+
-
- -
-
-
-
-

- A powerful and secure{" "} - - invoicing solution - {" "} - designed for growing businesses. -

-
+
+ + {/* Security Section */} +
+
+
+ + Military-Grade Security{" "} + Powered by Lit Protocol + +
+ +
{[ { - title: "Secure and Transparent Transactions", + icon: , + title: "Decentralized Encryption", description: - "Leverage blockchain technology to ensure encrypted, tamper-proof, and immutable transactions. Provide complete transparency for invoice verification.", - icon: , + "Invoice data is encrypted using Lit Protocol's distributed key management system, ensuring no single party can access sensitive information without proper authorization.", }, { - title: "Send and Receive Invoices", + icon: , + title: "Conditional Access", description: - "Effortlessly create and manage invoices with a few clicks. Track real-time status and maintain a comprehensive invoice dashboard.", - icon: , + "Define precise access conditions using blockchain parameters. Payments automatically decrypt invoice details when conditions are met.", }, { - title: "Smart Contract Integration", + icon: , + title: "Cross-Chain Compatibility", description: - "Automate payment processes with secure smart contracts. Ensure funds are released only when invoice conditions are met, reducing intermediary dependencies.", - icon: , - }, - { - title: "Comprehensive Invoice Tracking", - description: - "Gain complete visibility into your invoice lifecycle. Monitor payment statuses, track financial performance, and manage all transactions seamlessly.", - icon: , + "Our Lit Protocol integration works seamlessly across all supported chains, maintaining security consistency throughout the ecosystem.", }, ].map((feature, index) => ( -
-
- {feature.icon} +
+ {feature.icon}
-

- {feature.title} -

-

{feature.description}

-
+

{feature.title}

+

+ {feature.description} +

+ ))}
-
- Invoice Illustration -
-
-
- Aeroplane 2 -
-

- {" "} - Start Sending Your -

-

- Invoice Today! + + {/* Token Support Section */} +

+
+
+ + Universal Token Support + +

+ Accept payments in any ERC20 token while maintaining full + encryption and security through Lit Protocol

- Aeroplane 1 + + + +
+
+ + Seamless Multi-Token{" "} + Payments + + +

+ Chainvoice's smart contract architecture automatically handles + token conversions and verifications, while Lit Protocol ensures + all payment details remain encrypted until settlement. Our + system supports: +

+ +
    +
  • + ✓ + All standard ERC20 tokens across EVM chains +
  • +
  • + ✓ + Native chain currencies (ETH, MATIC, etc.) +
  • +
  • + ✓ + Stablecoins with automatic price feeds +
  • +
  • + ✓ + Custom token whitelisting for enterprise clients +
  • +
+
+ + + Token Payment Flow +
+ 1000+ ERC20 Token +
+
+
+
+
+ + {/* CTA Section */} +
+
+
+
+ +
+ +

+ Ready to Experience{" "} + Next-Gen Invoicing? +

+ +

+ Join thousands of Web3-native businesses already using Chainvoice + for secure, encrypted invoicing with support for all major ERC20 + tokens across multiple chains. +

+ +
+
+ +
+
+
+
-
-
+ {/* Footer */} +
+
+
-
- logo +
+ Chainvoice

Chain voice

-

- Secure & Smart Invoicing +

+ The most secure Web3 invoicing platform powered by Lit + Protocol's decentralized encryption technology.

-
-
-

Quick Links

-
    - {["Home", "Feature", "Treasure", "Service", "Invoice"].map( - (link) => ( -
  • - - {link} - -
  • - ) - )} -
-
-
-

Services

-
    - {[ - "Blog & Article", - "Terms and Conditions", - "Privacy Policy", - "Contact Us", - "Invoice", - ].map((link) => ( -
  • - - {link} - -
  • - ))} -
-
-
-

Contact

-
    - {["chainvoice@gmail.com"].map((link) => ( -
  • - - {link} - -
  • - ))} -
-
+ +
+

+ Product +

+
    + {[ + "Features", + "Security", + "Token Support", + "Pricing", + "API", + ].map((item) => ( +
  • + + {item} + +
  • + ))} +
+
+ +
+

+ Resources +

+
    + {[ + "Documentation", + "Developers", + "GitHub", + "Blog", + "Status", + ].map((item) => ( +
  • + + {item} + +
  • + ))} +
+
+ +
+

+ Technology +

+
-
-
- + +
+

+ © {new Date().getFullYear()} Chainvoice. All rights reserved. +

+ +
+
+ +
); } + export default Landing; diff --git a/frontend/src/page/ReceivedInvoice.jsx b/frontend/src/page/ReceivedInvoice.jsx index daade8b6..054cf636 100644 --- a/frontend/src/page/ReceivedInvoice.jsx +++ b/frontend/src/page/ReceivedInvoice.jsx @@ -22,17 +22,33 @@ import { generateAuthSig, LitAccessControlConditionResource, } from "@lit-protocol/auth-helpers"; +import { ERC20_ABI } from "@/contractsABI/ERC20_ABI"; +import { toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import CancelIcon from "@mui/icons-material/Cancel"; + +import { + CircularProgress, + Skeleton, + Chip, + Avatar, + Tooltip, + IconButton, + Typography, +} from "@mui/material"; +import PaidIcon from "@mui/icons-material/CheckCircle"; +import UnpaidIcon from "@mui/icons-material/Pending"; +import DownloadIcon from "@mui/icons-material/Download"; +import CurrencyExchangeIcon from "@mui/icons-material/CurrencyExchange"; +import { TOKEN_PRESETS } from "@/utils/erc20_token"; const columns = [ - { id: "fname", label: "First Name", minWidth: 100 }, - { id: "lname", label: "Last Name", minWidth: 100 }, - { id: "to", label: "Sender's address", minWidth: 200 }, - { id: "email", label: "Email", minWidth: 170 }, - // { id: 'country', label: 'Country', minWidth: 100 }, - { id: "amountDue", label: "Total Amount", minWidth: 100, align: "right" }, - { id: "isPaid", label: "Status", minWidth: 100 }, - { id: "detail", label: "Detail Invoice", minWidth: 100 }, - { id: "pay", label: "Pay / Paid", minWidth: 100 }, + { id: "fname", label: "Client", minWidth: 120 }, + { id: "to", label: "Sender", minWidth: 150 }, + { id: "amountDue", label: "Amount", minWidth: 100, align: "right" }, + { id: "status", label: "Status", minWidth: 100 }, + { id: "date", label: "Date", minWidth: 100 }, + { id: "actions", label: "Actions", minWidth: 150 }, ]; function ReceivedInvoice() { @@ -40,12 +56,14 @@ function ReceivedInvoice() { const [rowsPerPage, setRowsPerPage] = useState(10); const { data: walletClient } = useWalletClient(); const { address } = useAccount(); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const [receivedInvoices, setReceivedInvoice] = useState([]); const [fee, setFee] = useState(0); const [error, setError] = useState(null); const [litReady, setLitReady] = useState(false); const litClientRef = useRef(null); + const [paymentLoading, setPaymentLoading] = useState({}); + const [networkLoading, setNetworkLoading] = useState(false); const handleChangePage = (event, newPage) => { setPage(newPage); @@ -68,10 +86,9 @@ function ReceivedInvoice() { await client.connect(); litClientRef.current = client; setLitReady(true); - console.log(litClientRef.current); } } catch (error) { - console.error("Error while lit client initialization:", error); + console.error("Error initializing Lit client:", error); } finally { setLoading(false); } @@ -90,23 +107,19 @@ function ReceivedInvoice() { const signer = await provider.getSigner(); const network = await provider.getNetwork(); - if (network.chainId != 5115) { + if (network.chainId != 11155111) { setError( - `Failed to load invoices. You're connected to the "${network.name}" network, but your invoices are on the "Citrea" testnet. Please switch to Sepolia and try again.` + `You're connected to ${network.name}. Please switch to Sepolia network to view your invoices.` ); - setLoading(false); return; } - // 1. Setup Lit Node - const litNodeClient = litClientRef.current; if (!litNodeClient) { alert("Lit client not initialized"); return; } - // 2. Get data from contract const contract = new Contract( import.meta.env.VITE_CONTRACT_ADDRESS, ChainvoiceABI, @@ -114,7 +127,14 @@ function ReceivedInvoice() { ); const res = await contract.getReceivedInvoices(address); - console.log("getReceivedInvoices raw response:", res); + console.log("Raw invoices data:", res); + + if (!res || !Array.isArray(res) || res.length === 0) { + console.warn("No invoices found."); + setReceivedInvoice([]); + setLoading(false); + return; + } // First check if user has any invoices // if (!res || !Array.isArray(res) || res.length === 0) { @@ -128,111 +148,132 @@ function ReceivedInvoice() { const decryptedInvoices = []; for (const invoice of res) { - const id = invoice[0]; - const from = invoice[1].toLowerCase(); - const to = invoice[2].toLowerCase(); - const isPaid = invoice[4]; - const encryptedStringBase64 = invoice[5]; // encryptedData - const dataToEncryptHash = invoice[6]; - - if (!encryptedStringBase64 || !dataToEncryptHash) continue; - const currentUserAddress = address.toLowerCase(); - if (currentUserAddress !== from && currentUserAddress !== to) { - console.warn( - `User ${currentUserAddress} not authorized to decrypt invoice ${id}` - ); - continue; - } - const ciphertext = atob(encryptedStringBase64); - const accessControlConditions = [ - { - contractAddress: "", - standardContractType: "", - chain: "ethereum", - method: "", - parameters: [":userAddress"], - returnValueTest: { - comparator: "=", - value: invoice[1].toLowerCase(), // from - }, - }, - { operator: "or" }, - { - contractAddress: "", - standardContractType: "", - chain: "ethereum", - method: "", - parameters: [":userAddress"], - returnValueTest: { - comparator: "=", - value: invoice[2].toLowerCase(), // to + try { + const id = invoice[0]; + const from = invoice[1].toLowerCase(); + const to = invoice[2].toLowerCase(); + const isPaid = invoice[5]; + const isCancelled = invoice[6]; + const encryptedStringBase64 = invoice[7]; + const dataToEncryptHash = invoice[8]; + + if (!encryptedStringBase64 || !dataToEncryptHash) continue; + + const currentUserAddress = address.toLowerCase(); + if (currentUserAddress !== from && currentUserAddress !== to) { + console.warn(`Unauthorized access attempt for invoice ${id}`); + continue; + } + const ciphertext = atob(encryptedStringBase64); + const accessControlConditions = [ + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: from, + }, }, - }, - ]; - - const sessionSigs = await litNodeClient.getSessionSigs({ - chain: "ethereum", - resourceAbilityRequests: [ + { operator: "or" }, { - resource: new LitAccessControlConditionResource("*"), - ability: LIT_ABILITY.AccessControlConditionDecryption, + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: to, + }, }, - ], - authNeededCallback: async ({ - uri, - expiration, - resourceAbilityRequests, - }) => { - const nonce = await litNodeClient.getLatestBlockhash(); - const toSign = await createSiweMessageWithRecaps({ + ]; + const sessionSigs = await litNodeClient.getSessionSigs({ + chain: "ethereum", + resourceAbilityRequests: [ + { + resource: new LitAccessControlConditionResource("*"), + ability: LIT_ABILITY.AccessControlConditionDecryption, + }, + ], + authNeededCallback: async ({ uri, expiration, - resources: resourceAbilityRequests, - walletAddress: address, - nonce, - litNodeClient, - }); - return await generateAuthSig({ signer, toSign }); - }, - }); - - const decryptedString = await decryptToString( - { - accessControlConditions, - chain: "ethereum", - ciphertext, - dataToEncryptHash, - sessionSigs, - }, - litNodeClient - ); - - const parsed = JSON.parse(decryptedString); - parsed["id"] = id; - parsed["isPaid"] = isPaid; - decryptedInvoices.push(parsed); + resourceAbilityRequests, + }) => { + const nonce = await litNodeClient.getLatestBlockhash(); + const toSign = await createSiweMessageWithRecaps({ + uri, + expiration, + resources: resourceAbilityRequests, + walletAddress: address, + nonce, + litNodeClient, + }); + return await generateAuthSig({ signer, toSign }); + }, + }); + const decryptedString = await decryptToString( + { + accessControlConditions, + chain: "ethereum", + ciphertext, + dataToEncryptHash, + sessionSigs, + }, + litNodeClient + ); + const parsed = JSON.parse(decryptedString); + parsed["id"] = id; + parsed["isPaid"] = isPaid; + parsed["isCancelled"] = isCancelled; + + // Enhance with token details + if (parsed.paymentToken?.address) { + const tokenInfo = TOKEN_PRESETS.find( + (t) => + t.address.toLowerCase() === + parsed.paymentToken.address.toLowerCase() + ); + if (tokenInfo) { + parsed.paymentToken = { + ...parsed.paymentToken, + logo: tokenInfo.logo, + decimals: tokenInfo.decimals, + }; + } + } + + decryptedInvoices.push(parsed); + } catch (err) { + console.error(`Error processing invoice ${invoice[0]}:`, err); + } } - console.log("decrypted : ", decryptedInvoices); setReceivedInvoice(decryptedInvoices); - const fee = await contract.fee(); setFee(fee); } catch (error) { - console.error("Decryption error:", error); - alert("Failed to fetch or decrypt received invoices."); + console.error("Fetch error:", error); } finally { setLoading(false); } }; fetchReceivedInvoices(); - console.log("invoices : ", receivedInvoices); - }, [walletClient, litReady]); + }, [walletClient, litReady, address]); + + const payInvoice = async (invoiceId, amountDue, tokenAddress) => { + if (!walletClient) { + console.error("Wallet not connected"); + return; + } + + setPaymentLoading((prev) => ({ ...prev, [invoiceId]: true })); - const payInvoice = async (id, amountDue) => { try { - if (!walletClient) return; const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); const contract = new Contract( @@ -240,18 +281,81 @@ function ReceivedInvoice() { ChainvoiceABI, signer ); - console.log(ethers.parseUnits(String(amountDue), 18)); + const invoice = receivedInvoices.find((inv) => inv.id === invoiceId); + if (invoice?.isCancelled) { + throw new Error("Cannot pay a cancelled invoice"); + } const fee = await contract.fee(); - console.log(fee); - const amountDueInWei = ethers.parseUnits(String(amountDue), 18); - const feeInWei = BigInt(fee); - const total = amountDueInWei + feeInWei; + const isNativeToken = tokenAddress === ethers.ZeroAddress; - const res = await contract.payInvoice(BigInt(id), { - value: total, - }); + if (!ethers.isAddress(tokenAddress)) { + throw new Error(`Invalid token address: ${tokenAddress}`); + } + + const tokenInfo = TOKEN_PRESETS.find( + (t) => t.address.toLowerCase() === tokenAddress.toLowerCase() + ); + const tokenSymbol = tokenInfo?.symbol || "Token"; + + if (!isNativeToken) { + const tokenContract = new Contract(tokenAddress, ERC20_ABI, signer); + + const currentAllowance = await tokenContract.allowance( + await signer.getAddress(), + import.meta.env.VITE_CONTRACT_ADDRESS + ); + + const decimals = await tokenContract.decimals(); + const amountDueInWei = ethers.parseUnits(String(amountDue), decimals); + + if (currentAllowance < amountDueInWei) { + const approveTx = await tokenContract.approve( + import.meta.env.VITE_CONTRACT_ADDRESS, + amountDueInWei + ); + + await approveTx.wait(); + alert( + `Approval for ${tokenSymbol} completed! Now processing payment...` + ); + } + + const tx = await contract.payInvoice(BigInt(invoiceId), { + value: fee, + }); + + await tx.wait(); + alert(`Payment successful in ${tokenSymbol}!`); + } else { + const amountDueInWei = ethers.parseUnits(String(amountDue), 18); + const total = amountDueInWei + BigInt(fee); + + const tx = await contract.payInvoice(BigInt(invoiceId), { + value: total, + }); + + await tx.wait(); + alert("Payment successful in ETH!"); + } + + // Refresh invoice status + const updatedInvoices = receivedInvoices.map((inv) => + inv.id === invoiceId ? { ...inv, isPaid: true } : inv + ); + setReceivedInvoice(updatedInvoices); } catch (error) { - console.log(error); + console.error("Payment failed:", error); + if (error.code === "ACTION_REJECTED") { + toast.error("Transaction was rejected by user"); + } else if (error.message.includes("insufficient balance")) { + toast.error("Insufficient balance for this transaction"); + } else if (error.message.includes("cancelled")) { + toast.error("Cannot pay a cancelled invoice"); + } else { + toast.error(`Payment failed: ${error.reason || error.message}`); + } + } finally { + setPaymentLoading((prev) => ({ ...prev, [invoiceId]: false })); } }; @@ -274,344 +378,617 @@ function ReceivedInvoice() { }); }; - const contentRef = useRef(); const handlePrint = async () => { - const element = contentRef.current; - if (!element) { - return; - } + const element = document.getElementById("invoice-print"); + if (!element) return; - const canvas = await html2canvas(element, { - scale: 2, - }); + const canvas = await html2canvas(element, { scale: 2 }); const data = canvas.toDataURL("image/png"); - // download feature (implement later on) - // const pdf = new jsPDF({ - // orientation: "portrait", - // unit: "px", - // format: "a4", - // }); + const link = document.createElement("a"); + link.download = `invoice-${drawerState.selectedInvoice.id}.png`; + link.href = data; + link.click(); + }; - // const imgProperties = pdf.getImageProperties(data); - // const pdfWidth = pdf.internal.pageSize.getWidth(); + const switchNetwork = async () => { + try { + setNetworkLoading(true); + await window.ethereum.request({ + method: "wallet_switchEthereumChain", + params: [{ chainId: "0xaa36a7" }], // Sepolia chain ID + }); + setError(null); + } catch (error) { + console.error("Network switch failed:", error); + alert("Failed to switch network. Please switch to Sepolia manually."); + } finally { + setNetworkLoading(false); + } + }; - // const pdfHeight = (imgProperties.height * pdfWidth) / imgProperties.width; + const formatAddress = (address) => { + return `${address.substring(0, 10)}...${address.substring( + address.length - 10 + )}`; + }; - // pdf.addImage(data, "PNG", 0, 0, pdfWidth, pdfHeight); - // pdf.save("invoice.pdf"); + const formatDate = (issueDate) => { + const date = new Date(issueDate); + return date.toLocaleString(); }; + return ( -
-

Received Invoice Request

-

Pay to your client request

- - {loading ? ( -

Loading invoices...

- ) : error ? ( -

{error}

- ) : receivedInvoices.length === 0 ? ( -

No invoices found

- ) : ( - <> - - - - - {columns.map((column) => ( - - {column.label} - - ))} - - - - {receivedInvoices - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((invoice, index) => ( - - {columns.map((column) => { - - const value = invoice?.user[column.id] || ""; - - if (column.id === "to") { - return ( - - {invoice.user?.address - ? `${invoice.user.address.substring( - 0, - 10 - )}...${invoice.user.address.substring( - invoice.user.address.length - 10 - )}` - : "N/A"} - - ); - } - if (column.id === "amountDue") { - return ( - - {/* {ethers.formatUnits(invoice.amountDue)} ETH */} - {invoice.amountDue} ETH - - ); - } - if (column.id === "isPaid") { - return ( - - - - ); - } - if (column.id === "detail") { - return ( - +
+
+
+

Received Invoices

+

+ Manage and pay your incoming invoices +

+
+ {error && ( + + )} +
+ + + {loading ? ( +
+
+ + +
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ ) : error ? ( +
+
+

{error}

+
+
+ ) : receivedInvoices.length === 0 ? ( +
+
+ +

+ No Invoices Found +

+

+ You don't have any received invoices yet. +

+
+
+ ) : ( + <> + +
+ + + {columns.map((column) => ( + + {column.label} + + ))} + + + + {receivedInvoices + .slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ) + .map((invoice) => ( + + {/* Client Column */} + +
+ -
+
+ + {/* Sender Column */} + + + + {formatAddress(invoice.user?.address)} + + + + + {/* Amount Column */} + +
+ {invoice.paymentToken?.logo ? ( + {invoice.paymentToken.symbol} + ) : ( + + )} + + {invoice.amountDue}{" "} + {invoice.paymentToken?.symbol} + +
+
+ + {/* Status Column */} + + {invoice.isCancelled ? ( + } + label="Cancelled" + color="error" + size="small" + variant="outlined" + /> + ) : invoice.isPaid ? ( + } + label="Paid" + color="success" + size="small" + variant="outlined" + /> + ) : ( + } + /> + )} + + {/* Date Column */} + + + + {formatDate(invoice.issueDate)} + + + + +
+ + - - - - ); - } - if (column.id === "pay" && !invoice.isPaid) { - return ( - + + + + + {!invoice.isPaid && !invoice.isCancelled && ( - - ); - } - if (column.id === "pay" && invoice.isPaid) { - return ( - + )} + {invoice.isCancelled && ( - - ); - } - return ( - - {value} - - ); - })} - - ))} - -
-
- + + + ))} + + + + - - )} -
- + "& .MuiSelect-icon": { + color: "#64748b", + }, + }} + /> + + )} + +
+ + {/* Invoice Detail Drawer */} {drawerState.selectedInvoice && ( -
-
-
- none -
-

- Issued by {drawerState.selectedInvoice.issueDate} -

-

- Payment Due by {drawerState.selectedInvoice.dueDate} +

+
+
+
+ Chainvoice +

+ Cha + in + voice

+ +

+ Powered by Chainvoice +

-
-

- Invoice # {drawerState.selectedInvoice.id.toString()} -

+
+

INVOICE

+

+ #{drawerState.selectedInvoice.id.toString().padStart(6, "0")} +

+
+ {drawerState.selectedInvoice.isCancelled ? ( + } + /> + ) : drawerState.selectedInvoice.isPaid ? ( + } + /> + ) : ( + } + /> + )} +
+
+ {drawerState.selectedInvoice.isCancelled && ( +
+
+
+ + Invoice Cancelled by{" "} + {drawerState.selectedInvoice.user?.fname || "The sender"}{" "} + {drawerState.selectedInvoice.user?.lname || ""}{" "} + + + You no longer need to make payment for this invoice. + +
+
-
-

From

-

+ {!drawerState.selectedInvoice.isPaid && ( +

+ + Note: This invoice was cancelled before payment was + completed + +
+ )} +
+ )} +
+
+

+ From +

+

+ {drawerState.selectedInvoice.user.fname}{" "} + {drawerState.selectedInvoice.user.lname} +

+

{drawerState.selectedInvoice.user.address}

-

{`${drawerState.selectedInvoice.user.fname} ${drawerState.selectedInvoice.user.lname}`}

-

+

+ {drawerState.selectedInvoice.user.city},{" "} + {drawerState.selectedInvoice.user.country},{" "} + {drawerState.selectedInvoice.user.postalcode} +

+

{drawerState.selectedInvoice.user.email}

-

{`${drawerState.selectedInvoice.user.city}, ${drawerState.selectedInvoice.user.country} (${drawerState.selectedInvoice.user.postalcode})`}

-
-

Billed to

-

+

+

+ Bill To +

+

+ {drawerState.selectedInvoice.client.fname}{" "} + {drawerState.selectedInvoice.client.lname} +

+

{drawerState.selectedInvoice.client.address}

-

{`${drawerState.selectedInvoice.client.fname} ${drawerState.selectedInvoice.client.lname}`}

-

+

+ {drawerState.selectedInvoice.client.city},{" "} + {drawerState.selectedInvoice.client.country},{" "} + {drawerState.selectedInvoice.client.postalcode} +

+

{drawerState.selectedInvoice.client.email}

-

{`${drawerState.selectedInvoice.client.city}, ${drawerState.selectedInvoice.client.country} (${drawerState.selectedInvoice.client.postalcode})`}

- - - - - - - - - + +
+

+ Payment Currency +

+
+ {drawerState.selectedInvoice.paymentToken?.logo ? ( + {drawerState.selectedInvoice.paymentToken.symbol} + ) : ( +
+ +
+ )} +
+

+ {drawerState.selectedInvoice.paymentToken?.name || "Ether "} + {"("} + {drawerState.selectedInvoice.paymentToken?.symbol || "ETH"} + {")"} +

+

+ {drawerState.selectedInvoice.paymentToken?.address + ? `${drawerState.selectedInvoice.paymentToken.address.substring( + 0, + 10 + )}......${drawerState.selectedInvoice.paymentToken.address.substring( + 33 + )}` + : "Native Currency"} +

+
+
+ {drawerState.selectedInvoice.paymentToken?.address && ( +
+

+ Decimals:{" "} + {drawerState.selectedInvoice.paymentToken.decimals || 18} +

+

Chain: Sepolia Testnet

+
+ )} +
+
+
+ + Issued:{" "} + {new Date( + drawerState.selectedInvoice.issueDate + ).toLocaleDateString()} + + + Due:{" "} + {new Date( + drawerState.selectedInvoice.dueDate + ).toLocaleDateString()} + +
+
+
+
DescriptionQTYUnit PriceDiscountTaxAmount
+ + + + + + + + - {drawerState.selectedInvoice?.items?.map((item, index) => ( - - - - - - + {drawerState.selectedInvoice.items?.map((item, index) => ( + + + + + - - - + - - ))} + ))} +
DescriptionQtyPriceDiscountTaxAmount
{item.description}{item.qty.toString()} - {/* {ethers.formatUnits(item.unitPrice)} */} - {item.unitPrice} +
{item.description}{item.qty} + {item.unitPrice}{" "} + {drawerState.selectedInvoice.paymentToken?.symbol} + + {item.discount || "0"} {item.discount.toString()}{item.tax.toString()} - {item.amount} ETH - {/* {ethers.formatUnits(item.amount)} ETH */} + + {item.tax || "0%"} + + {item.amount}{" "} + {drawerState.selectedInvoice.paymentToken?.symbol}
-
-

- {/* Fee for invoice pay : {ethers.formatUnits(fee)} ETH */} +

- Fee for invoice pay : {parseFloat(ethers.formatUnits(fee))}{" "} - ETH -

-

- {" "} - Amount: {drawerState.selectedInvoice.amountDue}{" "} - {/* {ethers.formatUnits(drawerState.selectedInvoice.amountDue)}{" "} */} - ETH -

-

- Total Amount:{" "} - {parseFloat(drawerState.selectedInvoice.amountDue) + - parseFloat(ethers.formatUnits(fee))}{" "} - ETH -

+
+
+ Subtotal: + + {drawerState.selectedInvoice.amountDue}{" "} + {drawerState.selectedInvoice.paymentToken?.symbol} + +
+
+ Network Fee: + + {ethers.formatUnits(fee)} ETH +
-
-

Powered by

- +
+ Total Amount: + + {drawerState.selectedInvoice.paymentToken?.symbol === "ETH" + ? `${( + parseFloat(drawerState.selectedInvoice.amountDue) + + parseFloat(ethers.formatUnits(fee)) + ).toFixed(6)} ETH` + : `${drawerState.selectedInvoice.amountDue} ${ + drawerState.selectedInvoice.paymentToken?.symbol + } + ${ethers.formatUnits(fee)} ETH`} +
+ +
+ + +
)} diff --git a/frontend/src/page/SentInvoice.jsx b/frontend/src/page/SentInvoice.jsx index f24eb7f4..95f5a188 100644 --- a/frontend/src/page/SentInvoice.jsx +++ b/frontend/src/page/SentInvoice.jsx @@ -6,15 +6,14 @@ import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TablePagination from "@mui/material/TablePagination"; import TableRow from "@mui/material/TableRow"; +import { ChainvoiceABI } from "@/contractsABI/ChainvoiceABI"; import { BrowserProvider, Contract, ethers } from "ethers"; -import React, { useEffect, useState, useRef } from "react"; +import React, { useEffect, useState } from "react"; import { useAccount, useWalletClient } from "wagmi"; -import { ChainvoiceABI } from "../contractsABI/ChainvoiceABI"; import DescriptionIcon from "@mui/icons-material/Description"; - import SwipeableDrawer from "@mui/material/SwipeableDrawer"; +import { useRef } from "react"; import html2canvas from "html2canvas"; - import { LitNodeClient } from "@lit-protocol/lit-node-client"; import { decryptToString } from "@lit-protocol/encryption/src/lib/encryption.js"; import { LIT_ABILITY, LIT_NETWORK } from "@lit-protocol/constants"; @@ -23,22 +22,54 @@ import { generateAuthSig, LitAccessControlConditionResource, } from "@lit-protocol/auth-helpers"; +import { ERC20_ABI } from "@/contractsABI/ERC20_ABI"; +import { + CircularProgress, + Skeleton, + Chip, + Avatar, + Tooltip, + IconButton, + Typography, + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions, + Button, + Alert, +} from "@mui/material"; +import PaidIcon from "@mui/icons-material/CheckCircle"; +import UnpaidIcon from "@mui/icons-material/Pending"; +import DownloadIcon from "@mui/icons-material/Download"; +import CancelIcon from "@mui/icons-material/Cancel"; +import CurrencyExchangeIcon from "@mui/icons-material/CurrencyExchange"; +import { TOKEN_PRESETS } from "@/utils/erc20_token"; const columns = [ - { id: "fname", label: "First Name", minWidth: 100 }, - { id: "lname", label: "Last Name", minWidth: 100 }, - { id: "to", label: "Receiver's Address", minWidth: 200 }, - { id: "email", label: "Email", minWidth: 170 }, - { id: "country", label: "Country", minWidth: 100 }, - { id: "city", label: "City", minWidth: 100 }, - { id: "amountDue", label: "Total Amount", minWidth: 100, align: "right" }, - { id: "isPaid", label: "Status", minWidth: 100 }, - { id: "detail", label: "Detail Invoice", minWidth: 100 }, + { id: "fname", label: "Client", minWidth: 120 }, + { id: "to", label: "Receiver", minWidth: 150 }, + { id: "amountDue", label: "Amount", minWidth: 100, align: "right" }, + { id: "status", label: "Status", minWidth: 120 }, + { id: "date", label: "Date", minWidth: 100 }, + { id: "actions", label: "Actions", minWidth: 150 }, ]; function SentInvoice() { const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); + const { data: walletClient } = useWalletClient(); + const { address } = useAccount(); + const [loading, setLoading] = useState(true); + const [sentInvoices, setSentInvoices] = useState([]); + const [fee, setFee] = useState(0); + const [error, setError] = useState(null); + const [litReady, setLitReady] = useState(false); + const litClientRef = useRef(null); + const [paymentLoading, setPaymentLoading] = useState({}); + const [networkLoading, setNetworkLoading] = useState(false); + const [cancelConfirmOpen, setCancelConfirmOpen] = useState(false); + const [invoiceToCancel, setInvoiceToCancel] = useState(null); const handleChangePage = (event, newPage) => { setPage(newPage); }; @@ -46,17 +77,6 @@ function SentInvoice() { setRowsPerPage(+event.target.value); setPage(0); }; - - const { data: walletClient } = useWalletClient(); - const [sentInvoices, setSentInvoices] = useState([]); - const [invoiceItems, setInvoiceItems] = useState([]); - const [loading, setLoading] = useState(false); - const [fee, setFee] = useState(0); - const [error, setError] = useState(null); - const { address } = useAccount(); - const [litReady, setLitReady] = useState(false); - const litClientRef = useRef(null); - useEffect(() => { const initLit = async () => { try { @@ -69,10 +89,9 @@ function SentInvoice() { await client.connect(); litClientRef.current = client; setLitReady(true); - console.log(litClientRef.current); } } catch (error) { - console.error("Error while lit client initialization:", error); + console.error("Error initializing Lit client:", error); } finally { setLoading(false); } @@ -81,16 +100,23 @@ function SentInvoice() { }, []); useEffect(() => { - if (!walletClient || !litReady) return; + if (!walletClient || !address || !litReady) return; const fetchSentInvoices = async () => { try { setLoading(true); - - // 1. Setup signer + setError(null); const provider = new BrowserProvider(walletClient); const signer = await provider.getSigner(); + const network = await provider.getNetwork(); + if (network.chainId != 11155111) { + setError( + `You're connected to ${network.name}. Please switch to Sepolia network to view your invoices.` + ); + setLoading(false); + return; + } // 2. Connect to Lit Node const litNodeClient = litClientRef.current; @@ -107,12 +133,11 @@ function SentInvoice() { ); const res = await contract.getSentInvoices(address); - console.log(res); + console.log("Raw invoices data:", res); if (!res || !Array.isArray(res) || res.length === 0) { console.warn("No invoices found."); setSentInvoices([]); - setInvoiceItems([]); setLoading(false); return; } @@ -120,89 +145,108 @@ function SentInvoice() { const decryptedInvoices = []; for (const invoice of res) { - const id = invoice[0]; - const from = invoice[1].toLowerCase(); - const to = invoice[2].toLowerCase(); - const isPaid = invoice[4]; - const encryptedStringBase64 = invoice[5]; // encryptedData - const dataToEncryptHash = invoice[6]; - - if (!encryptedStringBase64 || !dataToEncryptHash) continue; - const currentUserAddress = address.toLowerCase(); - if (currentUserAddress !== from && currentUserAddress !== to) { - console.warn( - `User ${currentUserAddress} not authorized to decrypt invoice ${id}` - ); - continue; - } - const ciphertext = atob(encryptedStringBase64); - const accessControlConditions = [ - { - contractAddress: "", - standardContractType: "", - chain: "ethereum", - method: "", - parameters: [":userAddress"], - returnValueTest: { - comparator: "=", - value: invoice[1].toLowerCase(), // from - }, - }, - { operator: "or" }, - { - contractAddress: "", - standardContractType: "", - chain: "ethereum", - method: "", - parameters: [":userAddress"], - returnValueTest: { - comparator: "=", - value: invoice[2].toLowerCase(), // to + try { + const id = invoice[0]; + const from = invoice[1].toLowerCase(); + const to = invoice[2].toLowerCase(); + const isPaid = invoice[5]; + const isCancelled = invoice[6]; + const encryptedStringBase64 = invoice[7]; + const dataToEncryptHash = invoice[8]; + + if (!encryptedStringBase64 || !dataToEncryptHash) continue; + + const currentUserAddress = address.toLowerCase(); + if (currentUserAddress !== from && currentUserAddress !== to) { + console.warn(`Unauthorized access attempt for invoice ${id}`); + continue; + } + const ciphertext = atob(encryptedStringBase64); + const accessControlConditions = [ + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: from, + }, }, - }, - ]; - - const sessionSigs = await litNodeClient.getSessionSigs({ - chain: "ethereum", - resourceAbilityRequests: [ + { operator: "or" }, { - resource: new LitAccessControlConditionResource("*"), - ability: LIT_ABILITY.AccessControlConditionDecryption, + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: to, + }, }, - ], - authNeededCallback: async ({ - uri, - expiration, - resourceAbilityRequests, - }) => { - const nonce = await litNodeClient.getLatestBlockhash(); - const toSign = await createSiweMessageWithRecaps({ + ]; + + const sessionSigs = await litNodeClient.getSessionSigs({ + chain: "ethereum", + resourceAbilityRequests: [ + { + resource: new LitAccessControlConditionResource("*"), + ability: LIT_ABILITY.AccessControlConditionDecryption, + }, + ], + authNeededCallback: async ({ uri, expiration, - resources: resourceAbilityRequests, - walletAddress: address, - nonce, - litNodeClient, - }); - return await generateAuthSig({ signer, toSign }); - }, - }); + resourceAbilityRequests, + }) => { + const nonce = await litNodeClient.getLatestBlockhash(); + const toSign = await createSiweMessageWithRecaps({ + uri, + expiration, + resources: resourceAbilityRequests, + walletAddress: address, + nonce, + litNodeClient, + }); + return await generateAuthSig({ signer, toSign }); + }, + }); - const decryptedString = await decryptToString( - { - accessControlConditions, - chain: "ethereum", - ciphertext, - dataToEncryptHash, - sessionSigs, - }, - litNodeClient - ); + const decryptedString = await decryptToString( + { + accessControlConditions, + chain: "ethereum", + ciphertext, + dataToEncryptHash, + sessionSigs, + }, + litNodeClient + ); - const parsed = JSON.parse(decryptedString); - parsed["id"] = id; - parsed["isPaid"] = isPaid; - decryptedInvoices.push(parsed); + const parsed = JSON.parse(decryptedString); + parsed["id"] = id; + parsed["isPaid"] = isPaid; + parsed["isCancelled"] = isCancelled; + if (parsed.paymentToken?.address) { + const tokenInfo = TOKEN_PRESETS.find( + (t) => + t.address.toLowerCase() === + parsed.paymentToken.address.toLowerCase() + ); + if (tokenInfo) { + parsed.paymentToken = { + ...parsed.paymentToken, + logo: tokenInfo.logo, + decimals: tokenInfo.decimals, + }; + } + } + decryptedInvoices.push(parsed); + } catch (err) { + console.error(`Error processing invoice ${invoice[0]}:`, err); + } } setSentInvoices(decryptedInvoices); @@ -210,14 +254,14 @@ function SentInvoice() { setFee(fee); } catch (error) { console.error("Decryption error:", error); - alert("Failed to decrypt invoice."); } finally { + console.log(sentInvoices); setLoading(false); } }; fetchSentInvoices(); - }, [walletClient, litReady]); + }, [walletClient, litReady, address]); const [drawerState, setDrawerState] = useState({ open: false, @@ -225,7 +269,6 @@ function SentInvoice() { }); const toggleDrawer = (invoice) => (event) => { - console.log(invoice); if ( event && event.type === "keydown" && @@ -239,313 +282,666 @@ function SentInvoice() { }); }; - const contentRef = useRef(); const handlePrint = async () => { - const element = contentRef.current; - if (!element) { - return; - } + const element = document.getElementById("invoice-print"); + if (!element) return; - const canvas = await html2canvas(element, { - scale: 2, - }); + const canvas = await html2canvas(element, { scale: 2 }); const data = canvas.toDataURL("image/png"); - // download feature (implement later on) - // const pdf = new jsPDF({ - // orientation: "portrait", - // unit: "px", - // format: "a4", - // }); - - // const imgProperties = pdf.getImageProperties(data); - // const pdfWidth = pdf.internal.pageSize.getWidth(); + const link = document.createElement("a"); + link.download = `invoice-${drawerState.selectedInvoice.id}.png`; + link.href = data; + link.click(); + }; + const handleCancelInvoice = async (invoiceId) => { + try { + setPaymentLoading((prev) => ({ ...prev, [invoiceId]: true })); + const provider = new BrowserProvider(walletClient); + const signer = await provider.getSigner(); + const contract = new Contract( + import.meta.env.VITE_CONTRACT_ADDRESS, + ChainvoiceABI, + signer + ); + + const tx = await contract.cancelInvoice(invoiceId); + await tx.wait(); + setSentInvoices((prev) => + prev.map((inv) => + inv.id === invoiceId ? { ...inv, isCancelled: true } : inv + ) + ); + + toast.success("Invoice cancelled successfully"); + } catch (error) { + console.error("Cancellation failed:", error); + toast.error("Failed to cancel invoice"); + } finally { + setPaymentLoading((prev) => ({ ...prev, [invoiceId]: false })); + } + }; + const switchNetwork = async () => { + try { + setNetworkLoading(true); + await window.ethereum.request({ + method: "wallet_switchEthereumChain", + params: [{ chainId: "0xaa36a7" }], // Sepolia chain ID + }); + setError(null); + } catch (error) { + console.error("Network switch failed:", error); + alert("Failed to switch network. Please switch to Sepolia manually."); + } finally { + setNetworkLoading(false); + } + }; - // const pdfHeight = (imgProperties.height * pdfWidth) / imgProperties.width; + const formatAddress = (address) => { + return `${address.substring(0, 10)}...${address.substring( + address.length - 10 + )}`; + }; - // pdf.addImage(data, "PNG", 0, 0, pdfWidth, pdfHeight); - // pdf.save("invoice.pdf"); + const formatDate = (issueDate) => { + const date = new Date(issueDate); + return date.toLocaleString(); }; return ( -
-

Your Sent Invoice Request

- - {loading ? ( -

Loading invoices...

- ) : error ? ( -

{error}

- ) : sentInvoices.length === 0 ? ( -

No invoices found

- ) : ( - <> - - - - - {columns.map((column) => ( - - {column.label} - - ))} - - - - {sentInvoices - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((invoice, index) => ( - - {columns.map((column) => { - const value = invoice?.client[column.id]; - if (column.id === "to") { - return ( - - {invoice.client.address - ? `${invoice.client.address.substring( - 0, - 10 - )}...${invoice.client.address.substring( - invoice.client.address.length - 10 - )}` - : "N/A"} - - ); - } - if (column.id === "amountDue") { - return ( - - {invoice.amountDue} ETH - - ); - } - if (column.id === "isPaid") { - return ( - - - - ); - } - if (column.id === "detail") { - return ( - +
+
+
+

Sent Invoices

+
+ {error && ( + + )} +
+ + + {loading ? ( +
+
+ + +
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ ) : error ? ( +
+
+

{error}

+
+
+ ) : sentInvoices.length === 0 ? ( +
+
+ +

+ No Invoices Found +

+

+ You haven't sent any invoices yet. +

+
+
+ ) : ( + <> + +
+ + + {columns.map((column) => ( + + {column.label} + + ))} + + + + {sentInvoices + .slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ) + .map((invoice) => ( + + {/* Client Column */} + +
+ -
+
+ + {/* Sender Column */} + + + + {formatAddress(invoice.client?.address)} + + + + + {/* Amount Column */} + +
+ {invoice.paymentToken?.logo ? ( + {invoice.paymentToken.symbol} + ) : ( + + )} + + {invoice.amountDue}{" "} + {invoice.paymentToken?.symbol} + +
+
+ + {/* Status Column */} + + {invoice.isCancelled ? ( + } + label="Cancelled" + color="error" + size="small" + variant="outlined" + /> + ) : invoice.isPaid ? ( + } + label="Paid" + color="success" + size="small" + variant="outlined" + /> + ) : ( + } + label="Pending" + color="warning" + size="small" + variant="outlined" + /> + )} + + + {/* Date Column */} + + + {formatDate(invoice.issueDate)} + + + + +
+ {!invoice.isPaid && !invoice.isCancelled && ( + + { + setInvoiceToCancel(invoice); + setCancelConfirmOpen(true); + }} + sx={{ + backgroundColor: "#fee2e2", + "&:hover": { backgroundColor: "#fecaca" }, + }} + > + + + + )} + + - - - - ); - } - return ( - - {value} - - ); - })} - - ))} - -
-
- - + + +
+ + + ))} + + + + - - )} - - + "& .MuiSelect-icon": { + color: "#64748b", + }, + }} + /> + + )} + +
+ {/* Invoice Detail Drawer */} {drawerState.selectedInvoice && ( -
-
-
- none -
-

- Issued by {drawerState.selectedInvoice.issueDate} -

-

- Payment Due by {drawerState.selectedInvoice.dueDate} +

+
+
+ Powered by +
+ Chainvoice +

+ Cha + in + voice

-
-

- Invoice # {drawerState.selectedInvoice.id.toString()} -

+
+

INVOICE

+

+ #{drawerState.selectedInvoice.id.toString().padStart(6, "0")} +

+
+ {drawerState.selectedInvoice.isCancelled ? ( + } + /> + ) : drawerState.selectedInvoice.isPaid ? ( + } + /> + ) : ( + } + /> + )} +
+
+ + {drawerState.selectedInvoice.isCancelled && ( +
+
+
+ + You Cancelled This Invoice + + + {drawerState.selectedInvoice.client?.fname || + "The recipient"}{" "} + {drawerState.selectedInvoice.client?.lname || ""}{" "} + has been notified and cannot pay this invoice. + +
+
-
-

From

-

+ {drawerState.selectedInvoice.isPaid && ( +

+ + Note: Payment was already completed before cancellation + +
+ )} +
+ )} +
+
+

+ From +

+

+ {drawerState.selectedInvoice.user.fname}{" "} + {drawerState.selectedInvoice.user.lname} +

+

{drawerState.selectedInvoice.user.address}

-

{`${drawerState.selectedInvoice.user.fname} ${drawerState.selectedInvoice.user.lname}`}

-

+

+ {drawerState.selectedInvoice.user.city},{" "} + {drawerState.selectedInvoice.user.country},{" "} + {drawerState.selectedInvoice.user.postalcode} +

+

{drawerState.selectedInvoice.user.email}

-

{`${drawerState.selectedInvoice.user.city}, ${drawerState.selectedInvoice.user.country} (${drawerState.selectedInvoice.user.postalcode})`}

-
-

Billed to

-

+

+

+ Bill To +

+

+ {drawerState.selectedInvoice.client.fname}{" "} + {drawerState.selectedInvoice.client.lname} +

+

{drawerState.selectedInvoice.client.address}

-

{`${drawerState.selectedInvoice.client.fname} ${drawerState.selectedInvoice.client.lname}`}

-

+

+ {drawerState.selectedInvoice.client.city},{" "} + {drawerState.selectedInvoice.client.country},{" "} + {drawerState.selectedInvoice.client.postalcode} +

+

{drawerState.selectedInvoice.client.email}

-

{`${drawerState.selectedInvoice.client.city}, ${drawerState.selectedInvoice.client.country} (${drawerState.selectedInvoice.client.postalcode})`}

- - - - - - - - - + +
+

+ Payment Currency +

+
+ {drawerState.selectedInvoice.paymentToken?.logo ? ( + {drawerState.selectedInvoice.paymentToken.symbol} + ) : ( +
+ +
+ )} +
+

+ {drawerState.selectedInvoice.paymentToken?.name || "Ether "} + {"("} + {drawerState.selectedInvoice.paymentToken?.symbol || "ETH"} + {")"} +

+

+ {drawerState.selectedInvoice.paymentToken?.address + ? `${drawerState.selectedInvoice.paymentToken.address.substring( + 0, + 10 + )}......${drawerState.selectedInvoice.paymentToken.address.substring( + 33 + )}` + : "Native Currency"} +

+
+
+ {drawerState.selectedInvoice.paymentToken?.address && ( +
+

Chain: Sepolia Testnet

+
+ )} +
+
+
+ + Issued:{" "} + {new Date( + drawerState.selectedInvoice.issueDate + ).toLocaleDateString()} + + + Due:{" "} + {new Date( + drawerState.selectedInvoice.dueDate + ).toLocaleDateString()} + +
+
+ +
+
DescriptionQTYUnit PriceDiscountTaxAmount
+ + + + + + + + - {drawerState.selectedInvoice?.items?.map((item, index) => ( - - - - - + {drawerState.selectedInvoice.items?.map((item, index) => ( + + + + + - - - + - - ))} + ))} +
DescriptionQtyPriceDiscountTaxAmount
{item.description}{item.qty.toString()} - {item.unitPrice} +
{item.description}{item.qty} + {item.unitPrice}{" "} + {drawerState.selectedInvoice.paymentToken?.symbol} + + {item.discount || "0"} {item.discount.toString()}{item.tax.toString()} - {item.amount} + + {item.tax || "0%"} + + {item.amount}{" "} + {drawerState.selectedInvoice.paymentToken?.symbol}
-
-

- Fee for invoice pay : {parseFloat(ethers.formatUnits(fee))}{" "} - ETH -

-

- {" "} - Amount:{" "} - {drawerState.selectedInvoice.amountDue} ETH -

-

- Total Amount:{" "} - {parseFloat(drawerState.selectedInvoice.amountDue) + - parseFloat(ethers.formatUnits(fee))}{" "} - ETH -

+
+ +
+
+ Subtotal: + + {drawerState.selectedInvoice.amountDue}{" "} + {drawerState.selectedInvoice.paymentToken?.symbol} + +
+
+ Network Fee: + + {ethers.formatUnits(fee)} ETH +
-
-

Powered by

- +
+ Total Amount: + + {drawerState.selectedInvoice.paymentToken?.symbol === "ETH" + ? `${( + parseFloat(drawerState.selectedInvoice.amountDue) + + parseFloat(ethers.formatUnits(fee)) + ).toFixed(6)} ETH` + : `${drawerState.selectedInvoice.amountDue} ${ + drawerState.selectedInvoice.paymentToken?.symbol + } + ${ethers.formatUnits(fee)} ETH`} +
+ +
+ + +
)} + setCancelConfirmOpen(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + + Confirm Invoice Cancellation + + + + + You're about to cancel this invoice sent to{" "} + + {invoiceToCancel?.client.fname} {invoiceToCancel?.client.lname} + + . + + {invoiceToCancel?.isPaid ? ( + + Payment was already received - cancelling will not reverse the + transaction + + ) : ( + + This action cannot be undone + + )} + + + + + + +
); } diff --git a/frontend/src/page/Treasure.jsx b/frontend/src/page/Treasure.jsx index e2cbaf7b..78549639 100644 --- a/frontend/src/page/Treasure.jsx +++ b/frontend/src/page/Treasure.jsx @@ -10,8 +10,11 @@ import { Banknote, Key, Wallet, - DollarSignIcon, + DollarSign, + Settings, + ChevronRight, } from "lucide-react"; +import { motion } from "framer-motion"; const Treasure = () => { const [treasureAmount, setTreasureAmount] = useState(0); @@ -21,9 +24,11 @@ const Treasure = () => { fetch: false, setAddress: false, withdraw: false, + feeUpdate: false, }); const [treasuryAddress, setTreasuryAddress] = useState(""); const [newTreasuryAddress, setNewTreasuryAddress] = useState(""); + const [newFee, setNewFee] = useState(""); useEffect(() => { const fetchTreasureAmount = async () => { @@ -107,77 +112,129 @@ const Treasure = () => { } }; + const handleUpdateFee = async () => { + if (!newFee || isNaN(newFee)) { + alert("Please enter a valid fee amount"); + return; + } + try { + if (!walletClient) return; + setLoading((prev) => ({ ...prev, feeUpdate: true })); + const provider = new BrowserProvider(walletClient); + const signer = await provider.getSigner(); + const contract = new Contract( + import.meta.env.VITE_CONTRACT_ADDRESS, + ChainvoiceABI, + signer + ); + const tx = await contract.setFeeAmount( + ethers.parseUnits(newFee, "ether") + ); + await tx.wait(); + const updatedFee = await contract.fee(); + setFee(ethers.formatUnits(updatedFee)); + setNewFee(""); + alert("Fee updated successfully!"); + } catch (error) { + console.error("Error updating fee:", error); + alert(error.message || "Failed to update fee"); + } finally { + setLoading((prev) => ({ ...prev, feeUpdate: false })); + } + }; + return (
-
-
+ + {/* Treasury Overview Card */} +
-
-
+
+
-
+
-

+

Treasury Vault

-
-
- - - Current Balance: - - + +
+
+
+ + Current Balance +
+ {loading.fetch ? ( ) : ( - `${treasureAmount} cBTC` + `${treasureAmount} ETH` )}
-
- - - Fee Per Transaction: - - + +
+
+ + Transaction Fee +
+ {loading.fetch ? ( ) : ( - `${fee} cBTC` + `${fee} ETH` )}
-
- - - Admin Access Only - + +
+ + Admin Access Only
-
+ - {/* Content */} -
-

- Treasury Controls -

+ {/* Control Panel */} + +
+ +

+ Treasury Controls +

+

- Secure management of platform funds and treasury settings + Manage platform funds and configuration settings

-
-
- -

Current Treasury

+ {/* Treasury Address Section */} +
+
+ +

+ Treasury Address +

-
+

{loading.fetch ? ( @@ -188,61 +245,93 @@ const Treasure = () => { )}

+
+ setNewTreasuryAddress(e.target.value)} + className="bg-gray-800 border-gray-700 text-white font-mono" + /> + +

+ Requires contract owner privileges +

+
+ {/* Fee Adjustment Section */}
- -

- Update Treasury Address + +

+ Transaction Fee

-
+
setNewTreasuryAddress(e.target.value)} - className="flex-1 bg-gray-800 border-gray-700 text-white font-mono text-sm" + type="number" + placeholder={`Current: ${fee} ETH`} + value={newFee} + onChange={(e) => setNewFee(e.target.value)} + className="bg-gray-800 border-gray-700 text-white font-mono" /> +

+ Applies to all future transactions +

-

- Requires contract owner privileges -

- {/* Withdraw */} + {/* Withdrawal Section */}
- -

Funds Withdrawal

+ +

Withdraw Funds

- Will be sent to the current treasury address + Funds will be sent to the current treasury address

-
-
+ +
); }; diff --git a/frontend/src/utils/erc20_token.js b/frontend/src/utils/erc20_token.js new file mode 100644 index 00000000..c347db7c --- /dev/null +++ b/frontend/src/utils/erc20_token.js @@ -0,0 +1,709 @@ +// { +// name: "Chainvoice", +// symbol: "CIN", +// address: "0xB5E9C6e57C9d312937A059089B547d0036c155C7", +// decimals: 18, +// }, + +export const TOKEN_PRESETS = [ + { + name: "Ethereum Mainnet", + symbol: "ETH", + address: "0x0000000000000000000000000000000000000000", + decimals: 18, + logo: "/tokenImages/eth.png", + }, + { + name: "Tether USD", + symbol: "USDT", + address: "0xdac17f958d2ee523a2206206994597c13d831ec7", + decimal: "6", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x636c00eefcf239bf56cc07dfc8ec2a0d26ecc765805f8d9e62c866b0164ccb62.png", + }, + { + name: "BNB", + symbol: "BNB", + address: "0xb8c77482e45f1f44de1745f52c74426c631bdd52", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x533cc6c47bad45528f615b1f66023b1f7c429d4304e2d1ddb84918077e8ead8b.png", + }, + { + name: "USD Coin", + symbol: "USDC", + address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimal: "6", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xda4a7aa3c2c6966c74c4a8446b6348c3e397491e14b2874719192fa5a4c71cab.png", + }, + { + name: "Liquid staked Ether 2.0", + symbol: "stETH", + address: "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x4e05bd4f46441ab8812874ddb4a8e6ace80ab398e09c07086a93e60ff4b7f4bb.png", + }, + { + name: "Wrapped BTC", + symbol: "WBTC", + address: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + decimal: "8", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xbd2860a79e7bc89d86922c8a80cdeb9a32a0082045fb0da24c31e3871916e0d3.png", + }, + { + name: "Wrapped liquid staked Ether 2.0", + symbol: "wstETH", + address: "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x3454daef9545c526928d9a01030752ff1995f65f4ec044a0b127b16f913e4065.png", + }, + { + name: "ChainLink Token", + symbol: "LINK", + address: "0x514910771af9ca656af840dff83e8264ecf986ca", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x0d5bccf38448bc46b1a49419cfa43d1d569dbce172c214bacde2d2e22b005dc0.png", + }, + { + name: "Bitfinex LEO Token", + symbol: "LEO", + address: "0x2af5d2ad76741191d15dfe7bf6ac92d4bd912ca3", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x0f5926985ff75e8c8caaa95501ea575923c317bb847421d4d00dbf5964a1feea.png", + }, + { + name: "USDS Stablecoin", + symbol: "USDS", + address: "0xdc035d45d973e3ec169d2276ddab16f1e407384f", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xb547d1b9a9e8f112d440784ffaed3dbdc433a70820a359614664ec981752ae4e.png", + }, + { + name: "SHIBA INU", + symbol: "SHIB", + address: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x934042eb1c76a54280d40bebcc15138690c0b719eaaa4bd07529ce5d8dede7e6.png", + }, + { + name: "EtherFi wrapped ETH", + symbol: "weETH", + address: "0xcd5fe23c85820f7b72d0926fc9b05b43e359b7ee", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x8a7377a984da522f6099b1a7d8314a6fb527d07cfd637ababfbab4b94bfe693a.png", + }, + { + name: "Wrapped TON Coin", + symbol: "TONCOIN", + address: "0x582d872a1b094fc48f5de31d3b73f2d9be47def1", + decimal: "9", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x5b41e3f6b7eaa6d3ae160a0d3525678c720b1948ca6734dd4d998f2d6310ff1b.png", + }, + { + name: "Wrapped Ether", + symbol: "WETH", + address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x7f71adae0bcc9186e966ba6fe0be5dfd303f6ccba90512991f91babf1f333625.png", + }, + { + name: "WBT", + symbol: "WBT", + address: "0x925206b8a707096ed26ae47c84747fe0bb734f59", + decimal: "8", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xdeea9fdad65441cbb8ed321961502ca3f60ce0e6939d6f3761a6c1d4989a61ec.png", + }, + { + name: "Coinbase Wrapped BTC", + symbol: "cbBTC", + address: "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf", + decimal: "8", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x12601a638d95d1bed9ee4d1973daad3d89925f85ea42a09951daceb9494645a6.png", + }, + { + name: "USDe", + symbol: "USDe", + address: "0x4c9edd5852cd905f086c759e8383e09bff1e68b3", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x26c646b32ea6353a192f6c760b97beb706772d0e28099f504520f70a31e3a466.png", + }, + { + name: "BitgetToken", + symbol: "BGB", + address: "0x19de6b897ed14a376dda0fe53a5420d2ac828a28", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xef3652281dd019ae4dd24ec58d13d40c6d0115eb6ed972108808e361bd9db3e1.png", + }, + { + name: "Uniswap", + symbol: "UNI", + address: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xc53061256eb8145b0a928ac83ee76b055f7c563ad686521367bd16dd20ec3bed.png", + }, + { + name: "Aave Token", + symbol: "AAVE", + address: "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xeab69f3d8298f4b4b826af873d42063dbb05350742033769837aa0be6fbb9b64.png", + }, + { + name: "Pepe", + symbol: "PEPE", + address: "0x6982508145454ce325ddbe47a25d4ec3d2311933", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x673e4e814b3ed3008059e214b80fba443dae310acc14e66f707360067b4424a9.png", + }, + { + name: "Dai Stablecoin", + symbol: "DAI", + address: "0x6b175474e89094c44da98b954eedeac495271d0f", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xea5be93d23c21846ac28d0096ce859aceaac4471a4492818d06c0c5a5e540644.png", + }, + { + name: "Staked USDe", + symbol: "sUSDe", + address: "0x9d39a5de30e57443bff2a8307a4256c8797a3497", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x3abf76c30b11bc875bc5460a1f9b52e9d8a203f93c1983eb395afc5a8ba7027f.png", + }, + { + name: "CRO", + symbol: "CRO", + address: "0xa0b73e1ff0b80914ab6fe0444e65848c4c34450b", + decimal: "8", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x25095b588aae7d80740f215b6e69966926d3a8994f141e1593a5c3294f042297.png", + }, + { + name: "OKB", + symbol: "OKB", + address: "0x75231f58b43240c9718dd58b4967c5114342a86c", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x80ca2e5c6480ba7147551c1327ac2bfd1214607e90348784d37d84d724b8c5e5.png", + }, + { + name: "BlackRock USD Institutional Digital Liquidity Fund", + symbol: "BUIDL", + address: "0x7712c34205737192402172409a8f7ccef8aa2aec", + decimal: "6", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xe7568c777cc874c0f00c7528f1c03bc2d9c51ede922116ce57e0941c8b0100b8.png", + }, + { + name: "NEAR", + symbol: "NEAR", + address: "0x85f17cf997934a597031b2e18a9ab6ebd4b9f6a4", + decimal: "24", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xc75188cae4539f032ee7e1174ba4c7c9154e1dfbfb855c861f96efaab6ec7258.png", + }, + { + name: "Ondo", + symbol: "ONDO", + address: "0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x1fb261c892aa9b241652d883258be36cc4b87a6f34674e7ea7dc334523b80121.png", + }, + { + name: "Savings USDS", + symbol: "sUSDS", + address: "0xa3931d71877c0e7a3148cb7eb4463524fec27fbd", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xd3a649f8632bc78ce570ce63e898c6b1b0e13082bdfd528f82d46d27b8c09114.png", + }, + { + name: "World Liberty Financial USD", + symbol: "USD1", + address: "0x8d0d000ee44948fc98c9b98a4fa4921476f08b0d", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xf53fefeddd8477c2879e714bfae64d77fd05a5099c91d3a88502f4b404b5a710.png", + }, + { + name: "Mantle", + symbol: "MNT", + address: "0x3c3a81e81dc49a522a592e7622a7e711c06bf354", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x22f5817a14887c89254d451c8f2fbcd605d6c9b922fa1897bd0b83f9b335a56b.png", + }, + { + name: "Fasttoken", + symbol: "FTN", + address: "0xaedf386b755465871ff874e3e37af5976e247064", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x53ff0289d0573bc4b047b0fd00e4d7a0b724533af5400f544009f4c503308b5e.png", + }, + { + name: "GateChainToken", + symbol: "GT", + address: "0xe66747a101bff2dba3697199dcce5b743b454759", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x28dee88c251fffa5e1c340e8ddd0689c0d1d0b5d225d47ef709a30aed05879c9.png", + }, + { + name: "Polygon Ecosystem Token", + symbol: "POL", + address: "0x455e53cbb86018ac2b8092fdcd39d8444affc3f6", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xf5dfa373e5ab92008519c13cddc2505a8679c49cf490ba06ba8365936a06701b.png", + }, + { + name: "Fetch", + symbol: "FET", + address: "0xaea46a60368a7bd060eec7df8cba43b7ef41ad85", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x86707a37ebedf7ec6fc725027f04d0c38cd720d0dc38795877093b97613afbd8.png", + }, + { + name: "ENA", + symbol: "ENA", + address: "0x57e114b691db790c35207b2e685d4a43181e6061", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x90dde53b21e0318435509c0e32190cf31da0971ba874f695b02107f992c9fdc5.png", + }, + { + name: "SKY Governance Token", + symbol: "SKY", + address: "0x56072c95faa701256059aa122697b133aded9279", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x1f6ec6ef067dc18c953e2e43c86923a53b9c9a966166502b3f3c4bb463eed240.png", + }, + { + name: "Render Token", + symbol: "RNDR", + address: "0x6de037ef9ad2725eb40118bb1702ebb27e4aeb24", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x6e69899378586303df5fcb70a6488e60828c68706fd1b83a8954853fe40222f2.png", + }, + { + name: "Arbitrum", + symbol: "ARB", + address: "0xb50721bcf8d664c30412cfbc6cf7a15145234ad1", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xa11a48d3dac41fa9f07bee709903fbf6405f7ce48c6e6dc797e532efeda337d9.png", + }, + { + name: "Lombard Staked Bitcoin", + symbol: "LBTC", + address: "0x8236a87084f8b84306f72007f36f2618a5634494", + decimal: "8", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x3e5e975c15cd33f005ecaa28f7e23cddb1af50d3f85090fdf28e80c77e089a29.png", + }, + { + name: "Quant", + symbol: "QNT", + address: "0x4a220e6096b25eadb88358cb44068a3248254675", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xae8315d6bbe7c190cc699b64db3ecdbc0bce3db790732e7bb67718b58e3c0426.png", + }, + { + name: "Bonk", + symbol: "Bonk", + address: "0x1151cb3d861920e07a38e03eead12c32178567f6", + decimal: "5", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x35fcbf9794295309281d4c0cc759e240a586111e49cd6cdf0cb82499c37dacc0.png", + }, + { + name: "Worldcoin", + symbol: "WLD", + address: "0x163f8c2467924be0ae7b5347228cabf260318753", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xb444942fc451b3f471944b51d9be5ada722671cba9b9bce652f95ed1d01ef387.png", + }, + { + name: "USDtb", + symbol: "USDtb", + address: "0xc139190f447e929f090edeb554d95abb8b18ac1c", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x80db354588d993be46ec08fdadf0fe3954140e29d185b5c75217ac9556f06420.png", + }, + { + name: "First Digital USD", + symbol: "FDUSD", + address: "0xc5f0f7b66764f6ec8c8dff7ba683102295e16409", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x3dba518552999dd3a3b923b18ce39deba017998d7862bffaaabf10f3ebb74470.png", + }, + { + name: "SPX6900", + symbol: "SPX", + address: "0xe0f63a424a4439cbe457d80e4f4b51ad25b2c56c", + decimal: "8", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x7c69f4632587621b26bd220686a2bd668f1fdf1937b28167d08b075e9d9da281.png", + }, + { + name: "rsETH", + symbol: "rsETH", + address: "0xa1290d69c65a6fe4df752f95823fae25cb99e5a7", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x4d7847e6cfdf7a7b5d1b9d6c7bfc63e067d14ff2dfc6aac731302dec0f16f5a0.png", + }, + { + name: "Rocket Pool ETH", + symbol: "rETH", + address: "0xae78736cd615f374d3085123a210448e74fc6393", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x9ae7abade50f86dc63e72cf2add44c92ab2d3950f42245cb76ffe7dc2e77cb6d.png", + }, + { + name: "Nexo", + symbol: "NEXO", + address: "0xb62132e35a6c13ee1ee0f84dc5d40bad8d815206", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xaf742d00a2e8d58b2c9341fd73f42cfc2f2e5ae39005533c2aa8d1eeed0a489d.png", + }, + { + name: "Injective Token", + symbol: "INJ", + address: "0xe28b3b32b6c345a34ff64674606124dd5aceca30", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x5aabea3f7de09d6b2a3e861cd97902ba0d341e8f5daa71c9e9ede234d56cf9a5.png", + }, + { + name: "mETH", + symbol: "mETH", + address: "0xd5f7838f5c461feff7fe49ea5ebaf7728bb0adfa", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x65a213ed5544f36d32261e2f1b45a2aa8ce18e674c788687bafbe1d7b303acac.png", + }, + { + name: "Staked ETH", + symbol: "osETH", + address: "0xf1c9acdc66974dfb6decb12aa385b9cd01190e38", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xfb9cdd2c50e3cf5ad5452714caaa98a8076975a9bd07d69068f6be235fe814aa.png", + }, + { + name: "Solv BTC", + symbol: "SolvBTC", + address: "0x7a56e1c57c7475ccf742a1832b028f0456652f97", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xaab141f44b3f0d76f3315866e19ef7631d74f58c22916af091896824b59e3496.png", + }, + { + name: "Tokenize Emblem", + symbol: "TKX", + address: "0x667102bd3413bfeaa3dffb48fa8288819e480a88", + decimal: "8", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xf7fed58cf6ffda940c013bbb37bfbf319c0c155f54a2a9073156735c0c55a3f0.png", + }, + { + name: "Virtual Protocol", + symbol: "VIRTUAL", + address: "0x44ff8620b8ca30902395a7bd3f2407e1a091bf73", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xab89b951d480f6a2548f6fd2abfe8f13ecc4189fd811cd44c7ff41fa8b5cb1c6.png", + }, + { + name: "Syrup USDC", + symbol: "syrupUSDC", + address: "0x80ac24aa929eaf5013f6436cda2a7ba190f5cc0b", + decimal: "6", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xb7f804c79a04a9b48473c29cdfc51e9780a52623c5672abb8a48d4056d17beb6.png", + }, + { + name: "Paxos Gold", + symbol: "PAXG", + address: "0x45804880de22913dafe09f4980848ece6ecbaf78", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xb95043411156e0cf0a718f9938a17c09a077d3bb7c2b23e87086388a50e0ad69.png", + }, + { + name: "PayPal USD", + symbol: "PYUSD", + address: "0x6c3ea9036406852006290770bedfcaba0e23a0e8", + decimal: "6", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x61d41978836bfe9cd6810f33bc4833cdde140fbfe1ddd5d5a0cf23d80d9e3c61.png", + }, + { + name: "FLOKI", + symbol: "FLOKI", + address: "0xcf0c122c6b73ff809c693db761e7baebe62b6a2e", + decimal: "9", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xef24c2a03a5e2659fe073768e7b266215411dee6bb50bfa6ae88e0b0b3ed5915.png", + }, + { + name: "Graph Token", + symbol: "GRT", + address: "0xc944e90c64b2c07662a292be6244bdf05cda44a7", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xbcf1ff7bcf39ea3fdc2944b48e335f8a016ca1390c2f9ab3375d592c2a7a284e.png", + }, + { + name: "clBTC", + symbol: "clBTC", + address: "0xe7ae30c03395d66f30a26c49c91edae151747911", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xf5890b52fc0e86a586337ccc8e49e4f5f512ab5c00778f5642b3de7cdcf66d4b.png", + }, + { + name: "Tether Gold", + symbol: "XAUt", + address: "0x68749665ff8d2d112fa859aa293f07a622782f38", + decimal: "6", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xeb1e86b05e387d0ba5081ba78b6d6e42043b36555e443a1293f0bc476afa124c.png", + }, + { + name: "Immutable X", + symbol: "IMX", + address: "0xf57e7e7c23978c3caec3c3548e3d615c346e79ff", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x582820f7558c1a76a1edf7e58df994e265f323bf4020d62d344093e9c176154b.png", + }, + { + name: "PancakeSwap Token", + symbol: "Cake", + address: "0x152649ea73beab28c5b49b26eb48f7ead6d4c898", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x11d48fd825024aa8cfa4d43cc6bd330b0e6a21d5e265a0fbd3d5bb255eb9e69d.png", + }, + { + name: "Liquid Staked ETH", + symbol: "LsETH", + address: "0x8c1bed5b9a0928467c9b1341da1d7bd5e10b6549", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x0d8f81393bb459317dac5773b83bb2d790b8a4567b58daf941ea06ff69a56fa9.png", + }, + { + name: "Curve DAO Token", + symbol: "CRV", + address: "0xd533a949740bb3306d119cc777fa900ba034cd52", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xc6a36535595fc2a85d6c076ec06df02d3cc28d2399b409c7a7ae58e04367d976.png", + }, + { + name: "Ondo Short-Term U.S. Government Bond Fund", + symbol: "OUSG", + address: "0x1b19c19393e2d034d8ff31ff34c81252fcbbee92", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x368c45405136c537b8cd38c87e5495d88399b388e15a8c05065b548b8de5c7a0.png", + }, + { + name: "Superstate Short Duration US Government Securities Fund", + symbol: "USTB", + address: "0x43415eb6ff9db7e26a15b704e7a3edce97d31c4e", + decimal: "6", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x3a42a31b349dbcc4545784c9083717db6eb96117bd5e046e9c90f8a5121d3fb5.png", + }, + { + name: "USDX", + symbol: "USDX", + address: "0xf3527ef8de265eaa3716fb312c12847bfba66cef", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xaebcf05a1df94e754c2f21331f1b10056204bd810e2a015c2ec6e78ba2ba25e0.png", + }, + { + name: "Lido DAO Token", + symbol: "LDO", + address: "0x5a98fcbea516cf06857215779fd812ca3bef1b32", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xb003c5c36c3bc6ab6a04cf816bcadf7c69763f51e95169652e8dbd51c4bf09ef.png", + }, + { + name: "Gala", + symbol: "GALA", + address: "0xd1d2eb1b1e90b638588728b4130137d262c87cae", + decimal: "8", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x4031e94d1b2221342425981938d79c0824065b9e0ca604da7594c2ffeb66fe52.png", + }, + { + name: "Ethereum Name Service", + symbol: "ENS", + address: "0xc18360217d8f7ab5e7c516566761ea12ce7f9d72", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x8b1881f819ba5e5e8b986832b569d7c2e0ce1453572ce65d8d41709bec018d94.png", + }, + { + name: "Ondo U.S. Dollar Yield", + symbol: "USDY", + address: "0x96f6ef951840721adbf46ac996b59e0235cb985c", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xfd4029f0337c74afcab7b96cf7491132f4653636b3d7944acae9cc27ee23e776.png", + }, + { + name: "SAND", + symbol: "SAND", + address: "0x3845badade8e6dff049820680d1f14bd3903a5d0", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x3d3aa642e221f1362236429e6c61d46559bb680fe5e30891e8495471a1c9cb6d.png", + }, + { + name: "BitTorrent", + symbol: "BTT", + address: "0xc669928185dbce49d2230cc9b0979be6dc797957", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xc4ba29a5dc7368f01ce67ed8ebc9019f0f67bde7cbfabe8ff6157da67aeb99e0.png", + }, + { + name: "JasmyCoin", + symbol: "JASMY", + address: "0x7420b4b9a0110cdc71fb720908340c03f9bc03ec", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xf02f7e6ab17a0563cb628067a587959fa6875b4c489a004ba078030583e19ad7.png", + }, + { + name: "Pendle", + symbol: "PENDLE", + address: "0x808507121b80c02388fad14726482e061b8da827", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x93bfdf82043b3dcfd0ecbde84ec9ec332b832168f479c17ceda39ee9429e0338.png", + }, + { + name: "Usual USD", + symbol: "USD0", + address: "0x73a15fed60bf67631dc6cd7bc5b6e8da8190acf5", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x0e1df7dd5a2d894870a16cd5ccaeb51a240c1f473761915219565a6a993857e5.png", + }, + { + name: "cmETH", + symbol: "cmETH", + address: "0xe6829d9a7ee3040e1276fa75293bde931859e8fa", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x2db801d03445f6a78a4eaa4c5f1ac075c66e89fb42f1312d454a7e759df774d5.png", + }, + { + name: "SolvBTC Babylon", + symbol: "SolvBTC.BBN", + address: "0xd9d920aa40f578ab794426f5c90f6c731d159def", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x07bd4c851a9dd9bf2731542a39e6986017f2dc41350a6ff4447d84e36655fe1a.png", + }, + { + name: "tBTC v2", + symbol: "tBTC", + address: "0x18084fba666a33d37592fa2633fd49a74dd93a88", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x374c55831315eece6acdf73dd0ed4342ca11ba7a43146a2814f75b3cd47f02b4.png", + }, + { + name: "cgETH Hashkey Cloud", + symbol: "cgETH.hashkey", + address: "0xc60a9145d9e9f1152218e7da6df634b7a74ae444", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x518d0de9c0610837259b1bafb438bec7e0d08d6d50472fd1f16fe150c9c686b9.png", + }, + { + name: "Syrup Token", + symbol: "SYRUP", + address: "0x643c4e15d7d62ad0abec4a9bd4b001aa3ef52d66", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x53a946528e8af04d1cf5aeb25bff5b60cb3d619a771a47ae8573de1c52fcff54.png", + }, + { + name: "Decentraland MANA", + symbol: "MANA", + address: "0x0f5d2fb29fb7d3cfee444a200298f468908cc942", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x3de2fd9036d3330aa44d05bed8e7d42c78763c78b7e9c67ec9e57763cfe007ab.png", + }, + { + name: "", + symbol: "", + address: "0xcfd748b9de538c9f5b1805e8db9e1d4671f7f2ec", + decimal: "0", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x11e6eff08e149134d03f121f37913a360168286298c72c99aa2c7a6e78182205.png", + }, + { + name: "TrueUSD", + symbol: "TUSD", + address: "0x0000000000085d4780b73119b644ae5ecd22b376", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xfdee06ccdc1b94b47884cb37f623663824e23ad500f601cc50c7419b266b68d9.png", + }, + { + name: "ApeCoin", + symbol: "APE", + address: "0x4d224452801aced8b2f0aebe155379bb5d594381", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x97863e7e4f8a71de7dffecb21913a33f7af57a7170fbfeff05870f2acb645da2.png", + }, + { + name: "Chain", + symbol: "XCN", + address: "0xa2cd3d43c775978a96bdbf12d733d5a1ed94fb18", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x321ed57969e51fa8315f370d76414baa8fe2fb9b079ce3ca5bfaccead729fca5.png", + }, + { + name: "Morpho Token", + symbol: "MORPHO", + address: "0x9994e35db50125e0df82e4c2dde62496ce330999", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xe90be6230ef4ccee86e8b87c0ca8d24f8cf14bead8f1621ce3456135ffd3cc85.png", + }, + { + name: "Decentralized USD", + symbol: "USDD", + address: "0x0c10bf8fcb7bf5412187a595ab97a3609160b5c6", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xac24f1bff2cf09d84ce133faf5e5428994ad07efc23b65d8779c09dd8c0f7cff.png", + }, + { + name: "Dexe", + symbol: "DEXE", + address: "0xde4ee8057785a7e8e800db58f9784845a5c2cbd6", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x95ccb485493b37dbf2e6a86496688d00ef15614710b2841904dd1a9371fbfd66.png", + }, + { + name: "Mog Coin", + symbol: "Mog", + address: "0xaaee1a9723aadb7afa2810263653a34ba2c21c7a", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x2901a28b39db8fc38d87ca82a3a54efcfa312e737501dd4ca68dda7cf8fbd275.png", + }, + { + name: "APENFT", + symbol: "NFT", + address: "0x198d14f2ad9ce69e76ea330b374de4957c3f850a", + decimal: "6", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0xf48a34674e86da1610277474d53e92a21d74b5385215c84dc874c6098cd41f80.png", + }, + { + name: "Reserve Rights", + symbol: "RSR", + address: "0x320623b8e4ff03373931769a31fc52a4e78b5d70", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x79fb26c2f551cc14b32575f693298d3d30e2a87c242344cca29e527fa39a7786.png", + }, + { + name: "StarkNet Token", + symbol: "STRK", + address: "0xca14007eff0db1f8135f4c25b34de49ab0d42766", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x9a122beff8685e1b1ccd7a2b55b1f923b74eb7c37a77e58fca918937fe72c4ae.png", + }, + { + name: "ETHx", + symbol: "ETHx", + address: "0xa35b1b31ce002fbf2058d22f30f95d405200a15b", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x7ecb25bb3cb2e648b6d601ab906e5bc27a43709ff0818d471043828f7738b98f.png", + }, + { + name: "US Yield Coin", + symbol: "USYC", + address: "0x136471a34f6ef19fe571effc1ca711fdb8e49f2b", + decimal: "6", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x3fc57e0034e59dfc92e4a39bb7deb3238d876295a1a565b1985fb9b69ec261d2.png", + }, + { + name: "Compound", + symbol: "COMP", + address: "0xc00e94cb662c3520282e6f5717214004a7f26888", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x6bfc3cb11dcccb2a934dbad676e90523308cf1a9e07402f6850d6c1ee84d967b.png", + }, + { + name: "Movement", + symbol: "MOVE", + address: "0x3073f7aaa4db83f95e9fff17424f71d4751a3073", + decimal: "8", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x54afe78e77ea2aeac01a26c6e24da8a0d8829c4b11385df5e1b71db741c40bad.png", + }, + { + name: "ether.fi ETH", + symbol: "eETH", + address: "0x35fa164735182de50811e8e2e824cfb9b6118ac2", + decimal: "18", + logo: "https://market-data-images.s3.us-east-1.amazonaws.com/tokenImages/0x02eb37d0c375737c34d9057a6c89d563d7cb96ea30f155bcecea5040bf92a640.png", + }, +];