Skip to content

Commit 8dd635c

Browse files
committed
Enhance deposit transaction logic and user balance fetching
- Implemented direct fetching of user balance from the blockchain, improving accuracy and reliability. - Added fallback logic to retrieve balance from user assets if blockchain data is unavailable. - Enforced ADA-only deposits by restricting asset selection and validating transaction inputs. - Updated UI components to reflect changes in asset handling and ensure consistent user experience. - Introduced an import transaction feature in the CardBalance component for enhanced transaction management.
1 parent 0067f77 commit 8dd635c

File tree

4 files changed

+550
-48
lines changed

4 files changed

+550
-48
lines changed

src/components/pages/wallet/new-transaction/deposit/index.tsx

Lines changed: 49 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { ToastAction } from "@/components/ui/toast";
2424
import { useToast } from "@/hooks/use-toast";
2525
import { useRouter } from "next/router";
2626
import { cn } from "@/lib/utils";
27+
import { getBalanceFromUtxos } from "@/utils/getBalance";
2728

2829
export default function PageNewTransaction() {
2930
const { connected, wallet } = useWallet();
@@ -42,16 +43,39 @@ export default function PageNewTransaction() {
4243
const router = useRouter();
4344
const userAssets = useUserStore((state) => state.userAssets);
4445
const userAssetMetadata = useUserStore((state) => state.userAssetMetadata);
46+
const [userBalance, setUserBalance] = useState<number>(0);
4547

4648
useEffect(() => {
4749
reset();
4850
}, []);
4951

50-
const userBalance = useMemo(() => {
51-
const lovelace =
52-
userAssets.find((asset) => asset.unit === "lovelace")?.quantity || 0;
53-
return Number(lovelace) / Math.pow(10, 6);
54-
}, [userAssets]);
52+
// Fetch user balance directly from blockchain
53+
useEffect(() => {
54+
async function fetchUserBalance() {
55+
if (!userAddress || network === undefined || network === null) {
56+
// Fallback to userAssets if address/network not available
57+
const lovelace =
58+
userAssets.find((asset) => asset.unit === "lovelace")?.quantity || 0;
59+
setUserBalance(Number(lovelace) / Math.pow(10, 6));
60+
return;
61+
}
62+
63+
try {
64+
const blockchainProvider = getProvider(network);
65+
const utxos = await blockchainProvider.fetchAddressUTxOs(userAddress);
66+
const balance = getBalanceFromUtxos(utxos);
67+
setUserBalance(balance ?? 0);
68+
} catch (error) {
69+
console.error("Failed to fetch user balance:", error);
70+
// Fallback to userAssets if available
71+
const lovelace =
72+
userAssets.find((asset) => asset.unit === "lovelace")?.quantity || 0;
73+
setUserBalance(Number(lovelace) / Math.pow(10, 6));
74+
}
75+
}
76+
77+
fetchUserBalance();
78+
}, [userAddress, network, userAssets]);
5579

5680
function reset() {
5781
setMetadata("");
@@ -82,9 +106,11 @@ export default function PageNewTransaction() {
82106
> = {};
83107

84108
// reduce assets and amounts to Asset: Amount object
109+
// Only ADA deposits are allowed
85110
for (let i = 0; i < assets.length; i++) {
86111
const unit = assets[i] ?? "";
87-
if (unit === "ADA") {
112+
// Only process ADA deposits
113+
if (unit === "ADA" || unit === "lovelace" || unit === "") {
88114
if (assetsAmounts.lovelace) {
89115
assetsAmounts.lovelace.amount += Number(amounts[i]) ?? 0;
90116
} else {
@@ -95,19 +121,8 @@ export default function PageNewTransaction() {
95121
unit: "lovelace",
96122
};
97123
}
98-
} else {
99-
if (assetsAmounts[unit]) {
100-
assetsAmounts[unit].amount += Number(amounts[i]) ?? 0;
101-
} else {
102-
const asset = userWalletAssets.find((asset) => asset.unit === unit);
103-
assetsAmounts[unit] = {
104-
amount: Number(amounts[i]) ?? 0,
105-
assetName: asset?.assetName ?? unit,
106-
decimals: userAssetMetadata[unit]?.decimals ?? 0,
107-
unit: asset?.unit ?? "",
108-
};
109-
}
110124
}
125+
// Ignore any non-ADA assets
111126
}
112127
return assetsAmounts;
113128
}, [amounts, assets, userWalletAssets, userAssetMetadata]);
@@ -149,17 +164,17 @@ export default function PageNewTransaction() {
149164
for (let i = 0; i < UTxoCount; i++) {
150165
if (address && address.startsWith("addr") && address.length > 0) {
151166
const rawUnit = assets[i];
152-
// Default to 'lovelace' if rawUnit is undefined or if it's 'ADA'
153-
const unit = rawUnit
154-
? rawUnit === "ADA"
155-
? "lovelace"
156-
: rawUnit
157-
: "lovelace";
158-
const assetMetadata = userAssetMetadata[unit];
159-
const multiplier =
160-
unit === "lovelace"
161-
? 1000000
162-
: Math.pow(10, assetMetadata?.decimals ?? 0);
167+
// Only allow ADA deposits - enforce lovelace unit
168+
const unit = "lovelace";
169+
170+
// Validate that only ADA is being deposited
171+
if (rawUnit && rawUnit !== "ADA" && rawUnit !== "lovelace") {
172+
setError("Only ADA can be deposited. Please select ADA for all UTxOs.");
173+
setLoading(false);
174+
return;
175+
}
176+
177+
const multiplier = 1000000; // ADA always uses 6 decimals
163178
const parsedAmount = parseFloat(amounts[i]!) || 0;
164179
const thisAmount = parsedAmount * multiplier;
165180
outputs.push({
@@ -242,7 +257,7 @@ export default function PageNewTransaction() {
242257
function addNewUTxO() {
243258
setUTxoCount(UTxoCount + 1);
244259
setAmounts([...amounts, "100"]);
245-
setAssets([...assets, "ADA"]);
260+
setAssets([...assets, "ADA"]); // Only ADA can be deposited
246261
}
247262

248263
return (
@@ -384,21 +399,6 @@ function UTxORow({
384399
});
385400
}, [userAssets, userAssetMetadata]);
386401

387-
const assetOptions = useMemo(() => {
388-
return (
389-
<>
390-
{userWalletAssets.map((userWalletAsset) => {
391-
return (
392-
<option key={userWalletAsset.unit} value={userWalletAsset.unit}>
393-
{userWalletAsset.unit === "lovelace"
394-
? "ADA"
395-
: userWalletAsset.assetName}
396-
</option>
397-
);
398-
})}
399-
</>
400-
);
401-
}, [userWalletAssets]);
402402

403403
return (
404404
<TableRow>
@@ -420,18 +420,19 @@ function UTxORow({
420420
</TableCell>
421421
<TableCell className="w-[240px]">
422422
<select
423-
value={assets[index]}
423+
value="ADA"
424424
onChange={(e) => {
425+
// Only allow ADA deposits - force to ADA
425426
const newAssets = [...assets];
426-
newAssets[index] = e.target.value;
427+
newAssets[index] = "ADA";
427428
setAssets(newAssets);
428429
}}
429430
disabled={disableAdaAmountInput}
430431
className={cn(
431432
"flex h-9 w-full rounded-md border border-zinc-200 bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:placeholder:text-zinc-400 dark:focus-visible:ring-zinc-300",
432433
)}
433434
>
434-
{assetOptions}
435+
<option value="ADA">ADA</option>
435436
</select>
436437
</TableCell>
437438
<TableCell>

src/components/pages/wallet/transactions/card-balance.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import type { Wallet } from "@/types/wallet";
77
import Link from "next/link";
88
import { useEffect, useState } from "react";
99
import { getBalanceFromUtxos } from "@/utils/getBalance";
10+
import { Upload } from "lucide-react";
11+
import ImportTransactionDialog from "./import-transaction-dialog";
1012

1113
export default function CardBalance({ appWallet }: { appWallet: Wallet }) {
1214
const walletsUtxos = useWalletsStore((state) => state.walletsUtxos);
1315
const walletAssets = useWalletsStore((state) => state.walletAssets);
1416
const utxos = walletsUtxos[appWallet.id];
1517
const [balance, setBalance] = useState<number>(0);
18+
const [importDialogOpen, setImportDialogOpen] = useState(false);
1619

1720
useEffect(() => {
1821
if(!utxos) return
@@ -56,7 +59,20 @@ export default function CardBalance({ appWallet }: { appWallet: Wallet }) {
5659
New Transaction
5760
</Button>
5861
</Link>
62+
<Button
63+
onClick={() => setImportDialogOpen(true)}
64+
size="sm"
65+
className="w-full sm:w-auto"
66+
>
67+
<Upload className="mr-2 h-4 w-4" />
68+
Import Transaction
69+
</Button>
5970
</div>
71+
<ImportTransactionDialog
72+
open={importDialogOpen}
73+
onOpenChange={setImportDialogOpen}
74+
walletId={appWallet.id}
75+
/>
6076

6177
{/* Suggesting to disable the button if the balance is less than 0, or no previous transactions */}
6278
{/* <Button
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { useState } from "react";
2+
import { Button } from "@/components/ui/button";
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
} from "@/components/ui/dialog";
11+
import { Textarea } from "@/components/ui/textarea";
12+
import { Input } from "@/components/ui/input";
13+
import { useToast } from "@/hooks/use-toast";
14+
import { api } from "@/utils/api";
15+
import { Loader } from "lucide-react";
16+
17+
interface ImportTransactionDialogProps {
18+
open: boolean;
19+
onOpenChange: (open: boolean) => void;
20+
walletId: string;
21+
}
22+
23+
export default function ImportTransactionDialog({
24+
open,
25+
onOpenChange,
26+
walletId,
27+
}: ImportTransactionDialogProps) {
28+
const [cbor, setCbor] = useState("");
29+
const [description, setDescription] = useState("");
30+
const { toast } = useToast();
31+
const ctx = api.useUtils();
32+
33+
const { mutate: importTransaction, isPending } =
34+
api.transaction.importTransaction.useMutation({
35+
onSuccess: () => {
36+
toast({
37+
title: "Transaction Imported",
38+
description: "Transaction has been imported successfully",
39+
});
40+
setCbor("");
41+
setDescription("");
42+
onOpenChange(false);
43+
void ctx.transaction.getPendingTransactions.invalidate({ walletId });
44+
void ctx.transaction.getAllTransactions.invalidate({ walletId });
45+
},
46+
onError: (error) => {
47+
toast({
48+
title: "Import Failed",
49+
description: error.message || "Failed to import transaction",
50+
variant: "destructive",
51+
});
52+
},
53+
});
54+
55+
const handleImport = () => {
56+
if (!cbor.trim()) {
57+
toast({
58+
title: "Invalid Input",
59+
description: "Please enter a valid CBOR hex string",
60+
variant: "destructive",
61+
});
62+
return;
63+
}
64+
65+
importTransaction({
66+
walletId,
67+
txCbor: cbor.trim(),
68+
description: description.trim() || undefined,
69+
});
70+
};
71+
72+
return (
73+
<Dialog open={open} onOpenChange={onOpenChange}>
74+
<DialogContent className="sm:max-w-[600px]">
75+
<DialogHeader>
76+
<DialogTitle>Import Transaction</DialogTitle>
77+
<DialogDescription>
78+
Paste the CBOR hex string of a signed transaction. The transaction
79+
will be verified to ensure it's signed by one of the wallet signers
80+
before being added as a pending transaction.
81+
</DialogDescription>
82+
</DialogHeader>
83+
<div className="space-y-4 py-4">
84+
<div className="space-y-2">
85+
<label className="text-sm font-medium">Description (Optional)</label>
86+
<Input
87+
value={description}
88+
onChange={(e) => setDescription(e.target.value)}
89+
placeholder="Add a description for this transaction..."
90+
disabled={isPending}
91+
maxLength={255}
92+
/>
93+
</div>
94+
<div className="space-y-2">
95+
<label className="text-sm font-medium">Transaction CBOR (Hex)</label>
96+
<Textarea
97+
value={cbor}
98+
onChange={(e) => setCbor(e.target.value)}
99+
placeholder="Paste CBOR hex string here..."
100+
className="min-h-[200px] font-mono text-xs"
101+
disabled={isPending}
102+
/>
103+
</div>
104+
</div>
105+
<DialogFooter>
106+
<Button
107+
variant="outline"
108+
onClick={() => onOpenChange(false)}
109+
disabled={isPending}
110+
>
111+
Cancel
112+
</Button>
113+
<Button onClick={handleImport} disabled={isPending || !cbor.trim()}>
114+
{isPending ? (
115+
<>
116+
<Loader className="mr-2 h-4 w-4 animate-spin" />
117+
Importing...
118+
</>
119+
) : (
120+
"Import Transaction"
121+
)}
122+
</Button>
123+
</DialogFooter>
124+
</DialogContent>
125+
</Dialog>
126+
);
127+
}
128+

0 commit comments

Comments
 (0)