diff --git a/front/package.json b/front/package.json index 8ce38fa..63b4144 100644 --- a/front/package.json +++ b/front/package.json @@ -13,6 +13,7 @@ "@emotion/styled": "^11.11.0", "@peculiar/asn1-ecc": "2.3.6", "@peculiar/asn1-schema": "2.3.6", + "@radix-ui/react-form": "^0.0.3", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/themes": "^2.0.0", "@rainbow-me/rainbowkit": "^1.0.0", diff --git a/front/pnpm-lock.yaml b/front/pnpm-lock.yaml index f8448f9..48bb795 100644 --- a/front/pnpm-lock.yaml +++ b/front/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@peculiar/asn1-schema': specifier: 2.3.6 version: 2.3.6 + '@radix-ui/react-form': + specifier: ^0.0.3 + version: 0.0.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.2.0) diff --git a/front/src/app/api/users/save/route.ts b/front/src/app/api/users/save/route.ts index 6a9acb9..b960370 100644 --- a/front/src/app/api/users/save/route.ts +++ b/front/src/app/api/users/save/route.ts @@ -25,9 +25,6 @@ export async function POST(req: Request) { args: [BigInt(id)], }); - console.log("id", id); - console.log("user", user); - if (user.account !== zeroAddress) { return Response.json(undefined); } @@ -48,8 +45,6 @@ export async function POST(req: Request) { args: [BigInt(id)], }); - console.log("createdUser", createdUser); - await publicClient.waitForTransactionReceipt({ hash }); return Response.json({ ...createdUser, id: toHex(createdUser.id) }); } diff --git a/front/src/components/Balance/index.tsx b/front/src/components/Balance/index.tsx index 6ef1214..ea6b3d0 100644 --- a/front/src/components/Balance/index.tsx +++ b/front/src/components/Balance/index.tsx @@ -9,7 +9,7 @@ const css: CSSProperties = { }; export default function Balance() { - const { balance } = useBalance(); + const { balance, refreshBalance } = useBalance(); let [intBalance, decimals] = balance.toFixed(2).split("."); return ( diff --git a/front/src/components/History/index.tsx b/front/src/components/History/index.tsx index a7331b4..8b27896 100644 --- a/front/src/components/History/index.tsx +++ b/front/src/components/History/index.tsx @@ -1,47 +1,56 @@ "use client"; import { useTransaction } from "@/providers/TransactionProvider"; -import { Badge, Box, Flex, Link, Separator, Text } from "@radix-ui/themes"; +import { Badge, Box, Button, Flex, Link, Separator, Text } from "@radix-ui/themes"; import { Log } from "viem"; export default function History() { - const { loading, txs } = useTransaction(); + const { loading, txs, getLastTxs, unwatchLogs } = useTransaction(); + + if (loading) + return ( + + Fetching latest transactions... + + ); return ( - - History - + + + History + + {!loading && Array.isArray(txs) && - txs - .sort((a: Log, b: Log) => { - return Number(b.blockNumber) - Number(a.blockNumber); - }) - .map((tx: Log) => { - return ( - - - - - {tx?.transactionHash?.toString().slice(0, 4)}... - {tx?.transactionHash?.toString().slice(-4)} - - - - {(tx as unknown as { args: { status: boolean } }).args.status} - + txs.map((tx: Log) => { + return ( + + + + + {tx?.transactionHash?.toString().slice(0, 4)}... + {tx?.transactionHash?.toString().slice(-4)} + + + + {(tx as unknown as { args: { status: boolean } }).args.status} + + {(tx as unknown as { args: { success: boolean } })?.args.success ? ( Complete - - - ); - })} + ) : ( + Failed + )} + + + ); + })} ); diff --git a/front/src/components/SendTransaction.tsx b/front/src/components/SendTransaction.tsx index 77e5f96..e6e0ef9 100644 --- a/front/src/components/SendTransaction.tsx +++ b/front/src/components/SendTransaction.tsx @@ -16,8 +16,14 @@ import { } from "@radix-ui/themes"; import { UserOpBuilder, emptyHex } from "@/libs/smart-wallet/service/userOps"; import { useBalance } from "@/providers/BalanceProvider"; -import { CheckCircledIcon, CheckIcon, ExternalLinkIcon, ReloadIcon } from "@radix-ui/react-icons"; +import { + CheckCircledIcon, + CrossCircledIcon, + ExternalLinkIcon, + ReloadIcon, +} from "@radix-ui/react-icons"; import { useMe } from "@/providers/MeProvider"; +import * as Form from "@radix-ui/react-form"; import Spinner from "./Spinner"; smartWallet.init(); @@ -26,37 +32,51 @@ const builder = new UserOpBuilder(smartWallet.client.chain as Chain); export function SendTransaction() { const [txReceipt, setTxReceipt] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); const { me } = useMe(); - const { balance } = useBalance(); + const { balance, refreshBalance } = useBalance(); const submitTx = async (e: any) => { setIsLoading(true); + setError(null); e.preventDefault(); - const formData = new FormData(e.target as HTMLFormElement); - const address = formData?.get("address") as Hex; - const value = formData?.get("value") as `${number}`; - const { maxFeePerGas, maxPriorityFeePerGas }: EstimateFeesPerGasReturnType = - await smartWallet.client.estimateFeesPerGas(); + try { + const formData = new FormData(e.target as HTMLFormElement); + const address = formData?.get("address") as Hex; + const usdAmount = formData?.get("amount") as `${number}`; + + const price: { ethereum: { usd: number } } = await ( + await fetch("/api/price?ids=ethereum¤cies=usd") + ).json(); - const userOp = await builder.buildUserOp({ - calls: [ - { - dest: address || "0x1878EA9134D500A3cEF3E89589ECA3656EECf48f", - value: BigInt(value) || BigInt(11), - data: emptyHex, - }, - ], - maxFeePerGas: maxFeePerGas as bigint, - maxPriorityFeePerGas: maxPriorityFeePerGas as bigint, - keyId: me?.keyId as Hex, - }); + const { maxFeePerGas, maxPriorityFeePerGas }: EstimateFeesPerGasReturnType = + await smartWallet.client.estimateFeesPerGas(); - const hash = await smartWallet.sendUserOperation({ userOp }); - const receipt = await smartWallet.waitForUserOperationReceipt({ hash }); - setTxReceipt(receipt); + const userOp = await builder.buildUserOp({ + calls: [ + { + dest: address.toLowerCase() as Hex, + value: + (BigInt(usdAmount) * BigInt(1e18)) / (BigInt(price.ethereum.usd * 100) / BigInt(100)), // 100 is the price precision + data: emptyHex, + }, + ], + maxFeePerGas: maxFeePerGas as bigint, + maxPriorityFeePerGas: maxPriorityFeePerGas as bigint, + keyId: me?.keyId as Hex, + }); - setIsLoading(false); + const hash = await smartWallet.sendUserOperation({ userOp }); + const receipt = await smartWallet.waitForUserOperationReceipt({ hash }); + setTxReceipt(receipt); + } catch (e) { + console.error(e); + setError("Something went wrong!"); + } finally { + setIsLoading(false); + refreshBalance(); + } }; return ( @@ -67,7 +87,7 @@ export function SendTransaction() { )} {!txReceipt && !isLoading && ( -
await submitTx(e)} > - - - + + - - - - - - USD + style={{ + paddingInline: "0.5rem", + }} + > + + + To + + Please enter a recipient address! + + + Please provide a valid address! + + + + + + + + + + + Amount (USD) + + Please enter a value! + + + Please provide a valid value! + + + Insufficient balance! + + + + + + + + ${balance.toString().slice(0, 4)} available - - - - - ${balance.toString().slice(0, 4)} available - + + + + {error && ( + + {error} + + )} + -
+ )} {isLoading && ( @@ -120,18 +175,35 @@ export function SendTransaction() { {txReceipt && !isLoading && ( <> - - - - See transaction - - - + {txReceipt.success ? ( + <> + + + + See transaction + + + + + ) : ( + <> + + + + Transaction failed! + + + + + )} )} diff --git a/front/src/libs/smart-wallet/hook/useSmartWalletHook.tsx b/front/src/libs/smart-wallet/hook/useSmartWalletHook.tsx index e41c029..b005a0b 100644 --- a/front/src/libs/smart-wallet/hook/useSmartWalletHook.tsx +++ b/front/src/libs/smart-wallet/hook/useSmartWalletHook.tsx @@ -15,9 +15,7 @@ export function useSmartWalletHook() { smartWallet.client.watchEvent({ address: address, - onLogs: (logs: any) => { - console.log("logs", logs); - }, + onLogs: (logs: any) => {}, }); }, [address]); diff --git a/front/src/providers/BalanceProvider/index.tsx b/front/src/providers/BalanceProvider/index.tsx index 38fcbb9..d45fafb 100644 --- a/front/src/providers/BalanceProvider/index.tsx +++ b/front/src/providers/BalanceProvider/index.tsx @@ -8,6 +8,7 @@ import { Hex } from "viem"; function useBalanceHook() { // balance in usd const [balance, setBalance] = useState(0); + const [increment, setIncrement] = useState(0); const { me } = useMe(); const getBalance = useCallback(async (keyId: Hex) => { @@ -18,14 +19,19 @@ function useBalanceHook() { setBalance(ethBalance * price); }, []); + const refreshBalance = useCallback(() => { + setIncrement((prev) => prev + 1); + }, []); + useEffect(() => { if (!me?.keyId) return; getBalance(me?.keyId); - }, [me, getBalance]); + }, [me, getBalance, increment]); return { balance, getBalance, + refreshBalance, }; } diff --git a/front/src/providers/TransactionProvider/index.tsx b/front/src/providers/TransactionProvider/index.tsx index 7c1187c..abb3b10 100644 --- a/front/src/providers/TransactionProvider/index.tsx +++ b/front/src/providers/TransactionProvider/index.tsx @@ -1,8 +1,9 @@ "use client"; -import { useMe } from "@/providers/MeProvider"; +import { PUBLIC_CLIENT, ENTRYPOINT_ADDRESS } from "@/constants"; +import { Me, useMe } from "@/providers/MeProvider"; import { createContext, useCallback, useContext, useEffect, useState } from "react"; -import { GetLogsReturnType, Hex, Log } from "viem"; +import { Hex, Log } from "viem"; const useTxHook = () => { const [txs, setTxs] = useState | null>(null); @@ -14,21 +15,96 @@ const useTxHook = () => { setLoading(true); const res = await fetch(`/api/users/${keyId}/txs`); const resJson = await res.json(); - setTxs(resJson.logs); + setTxs((prev) => { + const logs = resJson.logs.sort((a: Log, b: Log) => { + return Number(b.blockNumber) - Number(a.blockNumber); + }); + if (!prev) return logs; + return [...prev, ...logs]; + }); setLoading(false); }, []); - useEffect(() => { - if (!me?.keyId) return; - getLastTxs(me.keyId); - }, [me?.keyId, getLastTxs]); + const unwatchLogs = PUBLIC_CLIENT.watchContractEvent({ + address: ENTRYPOINT_ADDRESS, + abi: [ + { + inputs: [ + { + internalType: "bytes32", + name: "userOpHash", + type: "bytes32", + indexed: true, + }, + { + internalType: "address", + name: "sender", + type: "address", + indexed: true, + }, + { + internalType: "address", + name: "paymaster", + type: "address", + indexed: true, + }, + { + internalType: "uint256", + name: "nonce", + type: "uint256", + indexed: false, + }, + { + internalType: "bool", + name: "success", + type: "bool", + indexed: false, + }, + { + internalType: "uint256", + name: "actualGasCost", + type: "uint256", + indexed: false, + }, + { + internalType: "uint256", + name: "actualGasUsed", + type: "uint256", + indexed: false, + }, + ], + type: "event", + name: "UserOperationEvent", + anonymous: false, + }, + ], + eventName: "UserOperationEvent", + args: { sender: me?.account }, + onLogs: (logs) => { + setLoading(true); + setTxs((prev) => { + if (!prev) return logs; + return [ + ...prev, + ...logs.sort((a: Log, b: Log) => { + return Number(b.blockNumber) - Number(a.blockNumber); + }), + ]; + }); + setLoading(false); + }, + }); - useEffect(() => {}, [txs]); + useEffect(() => { + if (!me) return; + getLastTxs(me?.keyId); + }, [me, getLastTxs]); return { loading, txs, getLastTxs, + unwatchLogs, }; };