Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
# Firebase Studio

This is a NextJS starter in Firebase Studio.

To get started, take a look at src/app/page.tsx.
5 changes: 3 additions & 2 deletions src/ai/flows/transaction-risk-assessment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof TransactionRiskInputSchema>;
Expand Down Expand Up @@ -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.
Expand Down
54 changes: 50 additions & 4 deletions src/app/actions.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string | null> {
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<SendFundsResult> {
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 {
Expand All @@ -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,
};
Expand Down
18 changes: 14 additions & 4 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,7 +10,18 @@ import Logo from '@/components/Logo';
import { ThemeToggle } from '@/components/ThemeToggle';

export default function Home() {
const [transactions, setTransactions] = useState<Transaction[]>(DUMMY_TRANSACTIONS);
const [transactions, setTransactions] = useState<Transaction[]>([]);
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]);
Expand Down Expand Up @@ -39,7 +49,7 @@ export default function Home() {
<FundDispersalForm onTransactionsAdded={addTransactions} />
</div>
<div className="lg:col-span-3">
<TransactionHistory transactions={transactions} />
<TransactionHistory transactions={transactions} isLoading={isHistoryLoading} />
</div>
</div>
</div>
Expand Down
85 changes: 59 additions & 26 deletions src/components/FundDispersalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -51,7 +63,7 @@ export default function FundDispersalForm({ onTransactionsAdded }: FundDispersal
const form = useForm<FundDispersalFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
recipients: [{ recipientAddress: '', amount: 0 }],
recipients: [{ recipientAddress: '', amount: 0, currency: 'ETH' }],
},
});

Expand All @@ -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);
Expand All @@ -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 {
Expand All @@ -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({
Expand All @@ -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",
Expand Down Expand Up @@ -141,34 +151,57 @@ export default function FundDispersalForm({ onTransactionsAdded }: FundDispersal
name={`recipients.${index}.recipientAddress`}
render={({ field }) => (
<FormItem>
<FormLabel>Recipient Wallet Address</FormLabel>
<FormControl>
<Input placeholder="0x..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`recipients.${index}.amount`}
render={({ field }) => (
<FormItem>
<FormLabel>Amount (ETH/USDC)</FormLabel>
<FormLabel>Recipient Wallet Address or ENS</FormLabel>
<FormControl>
<Input type="number" placeholder="0.1" {...field} step="0.01" />
<Input placeholder="0x... or vitalik.eth" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-start gap-2">
<FormField
control={form.control}
name={`recipients.${index}.amount`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Amount</FormLabel>
<FormControl>
<Input type="number" placeholder="0.1" {...field} step="0.01" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`recipients.${index}.currency`}
render={({ field }) => (
<FormItem className="w-[100px]">
<FormLabel>Currency</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select currency" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="ETH">ETH</SelectItem>
<SelectItem value="USDC">USDC</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
))}
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => append({ recipientAddress: '', amount: 0 })}
onClick={() => append({ recipientAddress: '', amount: 0, currency: 'ETH' })}
>
<PlusCircle className="mr-2 h-4 w-4" />
Add Recipient
Expand Down
Loading