diff --git a/README.md b/README.md index cc730c7..8b13789 100644 --- a/README.md +++ b/README.md @@ -1,5 +1 @@ -# Firebase Studio -This is a NextJS starter in Firebase Studio. - -To get started, take a look at src/app/page.tsx. diff --git a/src/ai/flows/transaction-risk-assessment.ts b/src/ai/flows/transaction-risk-assessment.ts index e726d52..f97000b 100644 --- a/src/ai/flows/transaction-risk-assessment.ts +++ b/src/ai/flows/transaction-risk-assessment.ts @@ -13,7 +13,8 @@ import {z} from 'genkit'; const TransactionRiskInputSchema = z.object({ recipientAddress: z.string().describe('The recipient wallet address.'), - amount: z.number().describe('The amount of funds to be sent (ETH/USDC).'), + amount: z.number().describe('The amount of funds to be sent.'), + currency: z.string().describe('The currency of the transaction (e.g., ETH, USDC).'), userAddress: z.string().describe('The user wallet address.'), }); export type TransactionRiskInput = z.infer; @@ -43,7 +44,7 @@ const assessTransactionRiskPrompt = ai.definePrompt({ Analyze the following transaction details to determine if the transaction is potentially malicious or erroneous. Recipient Address: {{{recipientAddress}}} - Amount: {{{amount}}} + Amount: {{{amount}}} {{{currency}}} User Address: {{{userAddress}}} Provide a risk assessment and indicate whether the transaction is considered safe. diff --git a/src/app/actions.ts b/src/app/actions.ts index 71f4b0f..9596256 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -1,8 +1,15 @@ "use server"; +import { ethers } from 'ethers'; import { assessTransactionRisk, type TransactionRiskInput } from '@/ai/flows/transaction-risk-assessment'; import type { Transaction } from '@/lib/types'; +type SendFundsData = { + recipientAddress: string; + amount: number; + currency: 'ETH' | 'USDC'; +} + type SendFundsResult = { success: boolean; isRisk?: boolean; @@ -11,13 +18,52 @@ type SendFundsResult = { error?: string; }; +// Initialize the Ethereum provider +const provider = new ethers.JsonRpcProvider(process.env.NEXT_PUBLIC_RPC_URL!); + +async function resolveAddress(addressOrEns: string): Promise { + if (addressOrEns.endsWith('.eth')) { + try { + const resolvedAddress = await provider.resolveName(addressOrEns); + return resolvedAddress; + } catch (error) { + console.error(`Failed to resolve ENS name ${addressOrEns}:`, error); + return null; + } + } + // It's already an address, return it + if (ethers.isAddress(addressOrEns)) { + return addressOrEns; + } + return null; +} + export async function sendFunds( - data: TransactionRiskInput[], + data: SendFundsData[], bypassRiskCheck = false ): Promise { try { + const resolvedData: TransactionRiskInput[] = []; + + // --- ENS Resolution Step --- + for (const recipient of data) { + const resolvedAddress = await resolveAddress(recipient.recipientAddress); + if (!resolvedAddress) { + return { + success: false, + error: `Invalid Ethereum address or unable to resolve ENS name: ${recipient.recipientAddress}`, + }; + } + resolvedData.push({ + ...recipient, + recipientAddress: resolvedAddress, + userAddress: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', // Mock user address + }); + } + // --- End ENS Resolution --- + if (!bypassRiskCheck) { - for (const recipientData of data) { + for (const recipientData of resolvedData) { const riskAssessment = await assessTransactionRisk(recipientData); if (!riskAssessment.isSafe) { return { @@ -32,12 +78,12 @@ export async function sendFunds( // Simulate sending funds await new Promise(resolve => setTimeout(resolve, 1500)); - const newTransactions: Transaction[] = data.map(recipientData => { + const newTransactions: Transaction[] = resolvedData.map(recipientData => { const mockTxHash = `0x${[...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join('')}`; return { recipient: recipientData.recipientAddress, amount: recipientData.amount, - currency: 'ETH', // For now, we assume ETH. + currency: recipientData.currency as 'ETH' | 'USDC', timestamp: new Date(), txHash: mockTxHash, }; diff --git a/src/app/page.tsx b/src/app/page.tsx index 2d8b0a5..772f5e1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,6 @@ - "use client"; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import type { Transaction } from '@/lib/types'; import { DUMMY_TRANSACTIONS } from '@/lib/dummy-data'; import WalletConnect from '@/components/WalletConnect'; @@ -11,7 +10,18 @@ import Logo from '@/components/Logo'; import { ThemeToggle } from '@/components/ThemeToggle'; export default function Home() { - const [transactions, setTransactions] = useState(DUMMY_TRANSACTIONS); + const [transactions, setTransactions] = useState([]); + const [isHistoryLoading, setIsHistoryLoading] = useState(true); + + useEffect(() => { + // Simulate fetching initial data + const timer = setTimeout(() => { + setTransactions(DUMMY_TRANSACTIONS); + setIsHistoryLoading(false); + }, 1500); // 1.5-second delay + + return () => clearTimeout(timer); + }, []); const addTransactions = (newTransactions: Transaction[]) => { setTransactions(prev => [...newTransactions, ...prev]); @@ -39,7 +49,7 @@ export default function Home() {
- +
diff --git a/src/components/FundDispersalForm.tsx b/src/components/FundDispersalForm.tsx index 7af9134..adfb8ea 100644 --- a/src/components/FundDispersalForm.tsx +++ b/src/components/FundDispersalForm.tsx @@ -11,6 +11,13 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" import { AlertDialog, AlertDialogAction, @@ -24,13 +31,18 @@ import { import { useToast } from '@/hooks/use-toast'; import { Loader2, Send, AlertTriangle, PlusCircle, XCircle } from 'lucide-react'; +// Updated schema to accept ENS names const recipientSchema = z.object({ - recipientAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, { - message: "Please enter a valid Ethereum wallet address.", - }), + recipientAddress: z.string().refine(value => + /^0x[a-fA-F0-9]{40}$/.test(value) || /^[a-zA-Z0-9-]+\.eth$/.test(value), + { + message: "Please enter a valid Ethereum address or ENS name.", + } + ), amount: z.coerce.number().positive({ message: "Amount must be a positive number.", }), + currency: z.enum(['ETH', 'USDC']), }); const formSchema = z.object({ @@ -51,7 +63,7 @@ export default function FundDispersalForm({ onTransactionsAdded }: FundDispersal const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { - recipients: [{ recipientAddress: '', amount: 0 }], + recipients: [{ recipientAddress: '', amount: 0, currency: 'ETH' }], }, }); @@ -62,8 +74,7 @@ export default function FundDispersalForm({ onTransactionsAdded }: FundDispersal const onSubmit = (values: FundDispersalFormValues) => { startTransition(async () => { - const recipientData = values.recipients.map(r => ({ ...r, userAddress: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B' })); - const result = await sendFunds(recipientData); + const result = await sendFunds(values.recipients); if (result.success && result.transactions) { onTransactionsAdded(result.transactions); @@ -73,7 +84,7 @@ export default function FundDispersalForm({ onTransactionsAdded }: FundDispersal }); form.reset(); remove(); - append({ recipientAddress: '', amount: 0 }); + append({ recipientAddress: '', amount: 0, currency: 'ETH' }); } else if (result.isRisk && result.assessment) { setRiskData({ assessment: result.assessment, values }); } else { @@ -90,8 +101,7 @@ export default function FundDispersalForm({ onTransactionsAdded }: FundDispersal if (!riskData) return; startTransition(async () => { - const recipientData = riskData.values.recipients.map(r => ({ ...r, userAddress: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B' })); - const result = await sendFunds(recipientData, true); // Bypass risk check + const result = await sendFunds(riskData.values.recipients, true); // Bypass risk check if (result.success && result.transactions) { onTransactionsAdded(result.transactions); toast({ @@ -100,7 +110,7 @@ export default function FundDispersalForm({ onTransactionsAdded }: FundDispersal }); form.reset(); remove(); - append({ recipientAddress: '', amount: 0 }); + append({ recipientAddress: '', amount: 0, currency: 'ETH' }); } else { toast({ variant: "destructive", @@ -141,34 +151,57 @@ export default function FundDispersalForm({ onTransactionsAdded }: FundDispersal name={`recipients.${index}.recipientAddress`} render={({ field }) => ( - Recipient Wallet Address - - - - - - )} - /> - ( - - Amount (ETH/USDC) + Recipient Wallet Address or ENS - + )} /> +
+ ( + + Amount + + + + + + )} + /> + ( + + Currency + + + + )} + /> +
))}