@trustless-work/blocks 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/bin/index.js +57 -0
  2. package/package.json +1 -1
  3. package/templates/deps.json +1 -1
  4. package/templates/escrows/details/Actions.tsx +21 -1
  5. package/templates/escrows/multi-release/initialize-escrow/dialog/InitializeEscrow.tsx +1 -0
  6. package/templates/escrows/multi-release/initialize-escrow/form/InitializeEscrow.tsx +1 -0
  7. package/templates/escrows/multi-release/initialize-escrow/shared/schema.ts +0 -1
  8. package/templates/escrows/multi-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -2
  9. package/templates/escrows/multi-release/resolve-dispute/button/ResolveDispute.tsx +10 -20
  10. package/templates/escrows/multi-release/resolve-dispute/dialog/ResolveDispute.tsx +117 -60
  11. package/templates/escrows/multi-release/resolve-dispute/form/ResolveDispute.tsx +111 -55
  12. package/templates/escrows/multi-release/resolve-dispute/shared/schema.ts +68 -71
  13. package/templates/escrows/multi-release/resolve-dispute/shared/useResolveDispute.ts +107 -21
  14. package/templates/escrows/multi-release/update-escrow/shared/schema.ts +0 -1
  15. package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +0 -4
  16. package/templates/escrows/multi-release/withdraw-remaining-funds/button/WithdrawRemainingFunds.tsx +85 -0
  17. package/templates/escrows/multi-release/withdraw-remaining-funds/dialog/WithdrawRemainingFunds.tsx +176 -0
  18. package/templates/escrows/multi-release/withdraw-remaining-funds/form/WithdrawRemainingFunds.tsx +153 -0
  19. package/templates/escrows/multi-release/withdraw-remaining-funds/shared/schema.ts +81 -0
  20. package/templates/escrows/multi-release/withdraw-remaining-funds/shared/useWithdrawRemainingFunds.ts +160 -0
  21. package/templates/escrows/single-multi-release/approve-milestone/button/ApproveMilestone.tsx +0 -1
  22. package/templates/escrows/single-multi-release/approve-milestone/shared/useApproveMilestone.ts +0 -1
  23. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +0 -1
  24. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -2
  25. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +15 -31
  26. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +116 -60
  27. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +98 -43
  28. package/templates/escrows/single-release/resolve-dispute/shared/schema.ts +65 -68
  29. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +100 -22
  30. package/templates/escrows/single-release/update-escrow/shared/schema.ts +0 -1
  31. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +0 -4
  32. package/templates/tanstack/useEscrowsMutations.ts +53 -0
  33. package/templates/wallet-kit/trustlines.ts +0 -4
@@ -1,83 +1,80 @@
1
1
  import { z } from "zod";
2
+ import { isValidWallet } from "../../../../wallet-kit/validators";
2
3
 
3
4
  export const getFormSchema = () => {
4
- return z.object({
5
- approverFunds: z
6
- .union([z.string(), z.number()])
7
- .refine(
8
- (val) => {
9
- if (typeof val === "string") {
10
- if (val === "" || val === "." || val.endsWith(".")) {
11
- return true;
12
- }
13
- const numVal = Number(val);
14
- return !isNaN(numVal) && numVal >= 0;
5
+ const amountSchema = z
6
+ .union([z.string(), z.number()])
7
+ .refine(
8
+ (val) => {
9
+ if (typeof val === "string") {
10
+ if (val === "" || val === "." || val.endsWith(".")) {
11
+ return true; // Allow partial input
15
12
  }
16
- return val >= 0;
17
- },
18
- {
19
- message: "Approver funds must be 0 or greater.",
13
+ const numVal = Number(val);
14
+ return !isNaN(numVal) && numVal >= 0;
20
15
  }
21
- )
22
- .refine(
23
- (val) => {
24
- if (typeof val === "string") {
25
- if (val === "" || val === "." || val.endsWith(".")) {
26
- return true;
27
- }
28
- const numVal = Number(val);
29
- if (isNaN(numVal)) return false;
30
- const decimalPlaces = (numVal.toString().split(".")[1] || "")
31
- .length;
32
- return decimalPlaces <= 2;
16
+ return val >= 0;
17
+ },
18
+ { message: "Amount must be 0 or greater." }
19
+ )
20
+ .refine(
21
+ (val) => {
22
+ if (typeof val === "string") {
23
+ if (val === "" || val === "." || val.endsWith(".")) {
24
+ return true; // Allow partial input
33
25
  }
34
- const decimalPlaces = (val.toString().split(".")[1] || "").length;
26
+ const numVal = Number(val);
27
+ if (isNaN(numVal)) return false;
28
+ const decimalPlaces = (numVal.toString().split(".")[1] || "").length;
35
29
  return decimalPlaces <= 2;
36
- },
37
- {
38
- message: "Approver funds can have a maximum of 2 decimal places.",
39
30
  }
40
- ),
41
- receiverFunds: z
42
- .union([z.string(), z.number()])
43
- .refine(
44
- (val) => {
45
- if (typeof val === "string") {
46
- if (val === "" || val === "." || val.endsWith(".")) {
47
- return true;
48
- }
49
- const numVal = Number(val);
50
- return !isNaN(numVal) && numVal >= 0;
51
- }
52
- return val >= 0;
53
- },
54
- {
55
- message: "Receiver funds must be 0 or greater.",
56
- }
57
- )
58
- .refine(
59
- (val) => {
60
- if (typeof val === "string") {
61
- if (val === "" || val === "." || val.endsWith(".")) {
62
- return true;
63
- }
64
- const numVal = Number(val);
65
- if (isNaN(numVal)) return false;
66
- const decimalPlaces = (numVal.toString().split(".")[1] || "")
67
- .length;
68
- return decimalPlaces <= 2;
69
- }
70
- const decimalPlaces = (val.toString().split(".")[1] || "").length;
71
- return decimalPlaces <= 2;
72
- },
73
- {
74
- message: "Receiver funds can have a maximum of 2 decimal places.",
31
+ const decimalPlaces = (val.toString().split(".")[1] || "").length;
32
+ return decimalPlaces <= 2;
33
+ },
34
+ { message: "Amount can have a maximum of 2 decimal places." }
35
+ );
36
+
37
+ return z
38
+ .object({
39
+ distributions: z
40
+ .array(
41
+ z.object({
42
+ address: z
43
+ .string()
44
+ .min(1, { message: "Address is required." })
45
+ .refine((addr) => isValidWallet(addr), {
46
+ message: "Invalid Stellar address.",
47
+ }),
48
+ amount: amountSchema,
49
+ })
50
+ )
51
+ .min(2, { message: "At least two distributions are required." }),
52
+ milestoneIndex: z
53
+ .string({ required_error: "Milestone is required" })
54
+ .min(1, { message: "Milestone is required" }),
55
+ })
56
+ .superRefine((data, ctx) => {
57
+ const seen = new Map<string, number>();
58
+ data.distributions.forEach((item, idx) => {
59
+ const key = (item.address || "").trim().toUpperCase();
60
+ if (!key) return;
61
+ if (seen.has(key)) {
62
+ const firstIdx = seen.get(key)!;
63
+ ctx.addIssue({
64
+ code: z.ZodIssueCode.custom,
65
+ path: ["distributions", idx, "address"],
66
+ message: "Duplicate address. Each recipient must be unique.",
67
+ });
68
+ ctx.addIssue({
69
+ code: z.ZodIssueCode.custom,
70
+ path: ["distributions", firstIdx, "address"],
71
+ message: "Duplicate address. Each recipient must be unique.",
72
+ });
73
+ } else {
74
+ seen.set(key, idx);
75
75
  }
76
- ),
77
- milestoneIndex: z
78
- .string({ required_error: "Milestone is required" })
79
- .min(1, { message: "Milestone is required" }),
80
- });
76
+ });
77
+ });
81
78
  };
82
79
 
83
80
  export const resolveDisputeSchema = getFormSchema();
@@ -15,6 +15,8 @@ import {
15
15
  } from "@/components/tw-blocks/handle-errors/handle";
16
16
  import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
17
17
 
18
+ type DistributionInput = { address: string; amount: string | number };
19
+
18
20
  export function useResolveDispute() {
19
21
  const { resolveDispute } = useEscrowsMutations();
20
22
  const { selectedEscrow, updateEscrow } = useEscrowContext();
@@ -23,8 +25,10 @@ export function useResolveDispute() {
23
25
  const form = useForm<ResolveDisputeValues>({
24
26
  resolver: zodResolver(resolveDisputeSchema),
25
27
  defaultValues: {
26
- approverFunds: 0,
27
- receiverFunds: 0,
28
+ distributions: [
29
+ { address: "", amount: "" },
30
+ { address: "", amount: "" },
31
+ ],
28
32
  milestoneIndex: "0",
29
33
  },
30
34
  mode: "onChange",
@@ -39,33 +43,97 @@ export function useResolveDispute() {
39
43
  );
40
44
  }, [selectedEscrow]);
41
45
 
46
+ const milestoneIndexWatch = form.watch("milestoneIndex");
47
+
48
+ const allowedAmount = React.useMemo(() => {
49
+ if (selectedEscrow?.type !== "multi-release") return 0;
50
+ const idx = Number(milestoneIndexWatch);
51
+ const milestones = selectedEscrow.milestones as MultiReleaseMilestone[];
52
+ const m = milestones?.[idx];
53
+ return Number((m?.amount as unknown as number) || 0);
54
+ }, [selectedEscrow, milestoneIndexWatch]);
55
+
56
+ const distributions = form.watch("distributions") as DistributionInput[];
57
+
58
+ const distributedSum = React.useMemo(() => {
59
+ return (distributions || []).reduce((acc, d) => {
60
+ const n = Number(d?.amount ?? 0);
61
+ return acc + (isNaN(n) ? 0 : n);
62
+ }, 0);
63
+ }, [distributions]);
64
+
65
+ const isExactMatch = React.useMemo(() => {
66
+ return Number(allowedAmount) === Number(distributedSum);
67
+ }, [allowedAmount, distributedSum]);
68
+
69
+ const difference = React.useMemo(() => {
70
+ return Math.abs(Number(allowedAmount) - Number(distributedSum));
71
+ }, [allowedAmount, distributedSum]);
72
+
42
73
  const [isSubmitting, setIsSubmitting] = React.useState(false);
43
74
 
75
+ const handleDistributionAddressChange = (index: number, value: string) => {
76
+ const updated = [...distributions];
77
+ updated[index] = { ...updated[index], address: value };
78
+ form.setValue("distributions", updated);
79
+ };
80
+
81
+ const handleDistributionAmountChange = (
82
+ index: number,
83
+ e: React.ChangeEvent<HTMLInputElement>
84
+ ) => {
85
+ let rawValue = e.target.value;
86
+ rawValue = rawValue.replace(/[^0-9.]/g, "");
87
+ if (rawValue.split(".").length > 2) {
88
+ rawValue = rawValue.slice(0, -1);
89
+ }
90
+ if (rawValue.includes(".")) {
91
+ const parts = rawValue.split(".");
92
+ if (parts[1] && parts[1].length > 2) {
93
+ rawValue = parts[0] + "." + parts[1].slice(0, 2);
94
+ }
95
+ }
96
+ const updated = [...distributions];
97
+ updated[index] = { ...updated[index], amount: rawValue };
98
+ form.setValue("distributions", updated);
99
+ };
100
+
101
+ const handleAddDistribution = () => {
102
+ const updated = [...distributions, { address: "", amount: "" }];
103
+ form.setValue("distributions", updated);
104
+ };
105
+
106
+ const handleRemoveDistribution = (index: number) => {
107
+ if (distributions.length <= 2) return;
108
+ const updated = distributions.filter((_, i) => i !== index);
109
+ form.setValue("distributions", updated);
110
+ };
111
+
112
+ const isAnyDistributionEmpty = React.useMemo(() => {
113
+ if (!distributions.length) return true;
114
+ const last = distributions[distributions.length - 1];
115
+ return (last.address || "").trim() === "" || (last.amount ?? "") === "";
116
+ }, [distributions]);
117
+
44
118
  const handleSubmit = form.handleSubmit(async (payload) => {
45
119
  try {
46
120
  setIsSubmitting(true);
47
121
 
48
- /**
49
- * Create the final payload for the resolve dispute mutation
50
- *
51
- * @param payload - The payload from the form
52
- * @returns The final payload for the resolve dispute mutation
53
- */
122
+ if (!isExactMatch) {
123
+ toast.error("The total distributions must equal the milestone amount");
124
+ return;
125
+ }
126
+
54
127
  const finalPayload: MultiReleaseResolveDisputePayload = {
55
128
  contractId: selectedEscrow?.contractId || "",
56
129
  disputeResolver: walletAddress || "",
57
- approverFunds: Number(payload.approverFunds),
58
- receiverFunds: Number(payload.receiverFunds),
59
130
  milestoneIndex: String(payload.milestoneIndex),
131
+ distributions: payload.distributions.map((d) => ({
132
+ address: d.address,
133
+ amount: Number(d.amount || 0),
134
+ })) as [{ address: string; amount: number }],
60
135
  };
61
136
 
62
- /**
63
- * Call the resolve dispute mutation
64
- *
65
- * @param payload - The final payload for the resolve dispute mutation
66
- * @param type - The type of the escrow
67
- * @param address - The address of the escrow
68
- */
69
137
  await resolveDispute.mutateAsync({
70
138
  payload: finalPayload,
71
139
  type: "multi-release",
@@ -74,6 +142,11 @@ export function useResolveDispute() {
74
142
 
75
143
  toast.success("Dispute resolved successfully");
76
144
 
145
+ const sumDistributed = payload.distributions.reduce((acc, d) => {
146
+ const n = Number(d.amount || 0);
147
+ return acc + (isNaN(n) ? 0 : n);
148
+ }, 0);
149
+
77
150
  updateEscrow({
78
151
  ...selectedEscrow,
79
152
  milestones: selectedEscrow?.milestones.map((milestone, index) => {
@@ -89,9 +162,7 @@ export function useResolveDispute() {
89
162
  }
90
163
  return milestone;
91
164
  }),
92
- balance:
93
- selectedEscrow?.balance ||
94
- Number(payload.approverFunds) + Number(payload.receiverFunds),
165
+ balance: (selectedEscrow?.balance || 0) - sumDistributed || 0,
95
166
  });
96
167
  } catch (error) {
97
168
  toast.error(handleError(error as ErrorResponse).message);
@@ -101,5 +172,20 @@ export function useResolveDispute() {
101
172
  }
102
173
  });
103
174
 
104
- return { form, handleSubmit, isSubmitting, totalAmount };
175
+ return {
176
+ form,
177
+ handleSubmit,
178
+ isSubmitting,
179
+ totalAmount,
180
+ distributions,
181
+ handleAddDistribution,
182
+ handleRemoveDistribution,
183
+ handleDistributionAddressChange,
184
+ handleDistributionAmountChange,
185
+ isAnyDistributionEmpty,
186
+ allowedAmount,
187
+ distributedSum,
188
+ isExactMatch,
189
+ difference,
190
+ };
105
191
  }
@@ -8,7 +8,6 @@ export const useUpdateEscrowSchema = () => {
8
8
  address: z.string().min(1, {
9
9
  message: "Trustline address is required.",
10
10
  }),
11
- decimals: z.number().default(10000000),
12
11
  }),
13
12
  roles: z.object({
14
13
  approver: z
@@ -43,7 +43,6 @@ export function useUpdateEscrow() {
43
43
  : "",
44
44
  trustline: {
45
45
  address: selectedEscrow?.trustline?.address || "",
46
- decimals: 10000000,
47
46
  },
48
47
  roles: {
49
48
  approver: selectedEscrow?.roles?.approver || "",
@@ -84,7 +83,6 @@ export function useUpdateEscrow() {
84
83
  : "",
85
84
  trustline: {
86
85
  address: selectedEscrow?.trustline?.address || "",
87
- decimals: 10000000,
88
86
  },
89
87
  roles: {
90
88
  approver: selectedEscrow?.roles?.approver || "",
@@ -190,7 +188,6 @@ export function useUpdateEscrow() {
190
188
  : undefined,
191
189
  trustline: {
192
190
  address: payload.trustline.address,
193
- decimals: 10000000,
194
191
  },
195
192
  roles: payload.roles,
196
193
  milestones: payload.milestones.map((milestone) => ({
@@ -227,7 +224,6 @@ export function useUpdateEscrow() {
227
224
  (selectedEscrow.trustline?.address as string) ||
228
225
  "",
229
226
  address: finalPayload.escrow.trustline.address,
230
- decimals: finalPayload.escrow.trustline.decimals,
231
227
  },
232
228
  };
233
229
 
@@ -0,0 +1,85 @@
1
+ import * as React from "react";
2
+ import { Button } from "__UI_BASE__/button";
3
+ import { useEscrowsMutations } from "@/components/tw-blocks/tanstack/useEscrowsMutations";
4
+ import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
5
+ import { WithdrawRemainingFundsPayload } from "@trustless-work/escrow/types";
6
+ import { toast } from "sonner";
7
+ import {
8
+ ErrorResponse,
9
+ handleError,
10
+ } from "@/components/tw-blocks/handle-errors/handle";
11
+ import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
12
+ import { Loader2 } from "lucide-react";
13
+
14
+ type Distribution = { address: string; amount: number };
15
+
16
+ type WithdrawRemainingFundsButtonProps = {
17
+ distributions: Distribution[];
18
+ };
19
+
20
+ export const WithdrawRemainingFundsButton = ({
21
+ distributions,
22
+ }: WithdrawRemainingFundsButtonProps) => {
23
+ const { withdrawRemainingFunds } = useEscrowsMutations();
24
+ const { selectedEscrow, updateEscrow } = useEscrowContext();
25
+ const { walletAddress } = useWalletContext();
26
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
27
+
28
+ async function handleClick() {
29
+ try {
30
+ const hasInvalid = distributions.some(
31
+ (d) => !d.address || Number.isNaN(d.amount) || d.amount < 0
32
+ );
33
+ if (hasInvalid) {
34
+ toast.error("Invalid distributions");
35
+ return;
36
+ }
37
+
38
+ setIsSubmitting(true);
39
+
40
+ const payload: WithdrawRemainingFundsPayload = {
41
+ contractId: selectedEscrow?.contractId || "",
42
+ disputeResolver: walletAddress || "",
43
+ distributions: distributions as [{ address: string; amount: number }],
44
+ };
45
+
46
+ await withdrawRemainingFunds.mutateAsync({
47
+ payload,
48
+ type: "multi-release",
49
+ address: walletAddress || "",
50
+ });
51
+
52
+ toast.success("Withdraw successful");
53
+ const sumDistributed = distributions.reduce(
54
+ (acc, d) => acc + Number(d.amount || 0),
55
+ 0
56
+ );
57
+ updateEscrow({
58
+ ...selectedEscrow,
59
+ balance: (selectedEscrow?.balance || 0) - sumDistributed || 0,
60
+ });
61
+ } catch (error) {
62
+ toast.error(handleError(error as ErrorResponse).message);
63
+ } finally {
64
+ setIsSubmitting(false);
65
+ }
66
+ }
67
+
68
+ return (
69
+ <Button
70
+ type="button"
71
+ disabled={isSubmitting}
72
+ onClick={handleClick}
73
+ className="cursor-pointer w-full"
74
+ >
75
+ {isSubmitting ? (
76
+ <div className="flex items-center">
77
+ <Loader2 className="h-5 w-5 animate-spin" />
78
+ <span className="ml-2">Withdrawing...</span>
79
+ </div>
80
+ ) : (
81
+ "Withdraw Remaining"
82
+ )}
83
+ </Button>
84
+ );
85
+ };
@@ -0,0 +1,176 @@
1
+ import * as React from "react";
2
+ import {
3
+ Form,
4
+ FormField,
5
+ FormItem,
6
+ FormLabel,
7
+ FormControl,
8
+ FormMessage,
9
+ } from "__UI_BASE__/form";
10
+ import { Input } from "__UI_BASE__/input";
11
+ import { Button } from "__UI_BASE__/button";
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ DialogTrigger,
18
+ } from "__UI_BASE__/dialog";
19
+ import { Loader2, Trash2 } from "lucide-react";
20
+ import { useWithdrawRemainingFunds } from "./useWithdrawRemainingFunds";
21
+ import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
22
+ import { formatCurrency } from "../../../../helpers/format.helper";
23
+
24
+ export const WithdrawRemainingFundsDialog = () => {
25
+ const {
26
+ form,
27
+ handleSubmit,
28
+ isSubmitting,
29
+ distributions,
30
+ handleAddDistribution,
31
+ handleRemoveDistribution,
32
+ handleDistributionAddressChange,
33
+ handleDistributionAmountChange,
34
+ isAnyDistributionEmpty,
35
+ allowedAmount,
36
+ distributedSum,
37
+ isExactMatch,
38
+ difference,
39
+ } = useWithdrawRemainingFunds();
40
+ const { selectedEscrow } = useEscrowContext();
41
+
42
+ return (
43
+ <Dialog>
44
+ <DialogTrigger asChild>
45
+ <Button type="button" className="cursor-pointer w-full">
46
+ Withdraw Remaining
47
+ </Button>
48
+ </DialogTrigger>
49
+ <DialogContent className="!w-full sm:!max-w-3xl max-h-[95vh] overflow-y-auto">
50
+ <DialogHeader>
51
+ <DialogTitle>Withdraw Remaining Funds</DialogTitle>
52
+ </DialogHeader>
53
+ <Form {...form}>
54
+ <form onSubmit={handleSubmit}>
55
+ <FormLabel className="flex items-center my-4">
56
+ Distributions<span className="text-destructive ml-1">*</span>
57
+ </FormLabel>
58
+
59
+ {distributions.map((d, idx) => (
60
+ <div
61
+ key={`dist-${idx}`}
62
+ className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-[1fr_minmax(140px,220px)_auto] gap-3 sm:gap-4 items-end mb-2"
63
+ >
64
+ <FormField
65
+ control={form.control}
66
+ name={`distributions.${idx}.address` as const}
67
+ render={() => (
68
+ <FormItem className="sm:col-span-2 lg:col-span-1">
69
+ <FormLabel>Address</FormLabel>
70
+ <FormControl>
71
+ <Input
72
+ type="text"
73
+ placeholder="Receiver address"
74
+ value={d.address}
75
+ onChange={(e) =>
76
+ handleDistributionAddressChange(idx, e.target.value)
77
+ }
78
+ />
79
+ </FormControl>
80
+ <FormMessage />
81
+ </FormItem>
82
+ )}
83
+ />
84
+
85
+ <FormField
86
+ control={form.control}
87
+ name={`distributions.${idx}.amount` as const}
88
+ render={() => (
89
+ <FormItem>
90
+ <FormLabel>Amount</FormLabel>
91
+ <FormControl>
92
+ <Input
93
+ type="text"
94
+ inputMode="decimal"
95
+ placeholder="0.00"
96
+ value={(d.amount as string) ?? ""}
97
+ onChange={(e) =>
98
+ handleDistributionAmountChange(idx, e)
99
+ }
100
+ />
101
+ </FormControl>
102
+ <FormMessage />
103
+ </FormItem>
104
+ )}
105
+ />
106
+
107
+ <Button
108
+ type="button"
109
+ onClick={() => handleRemoveDistribution(idx)}
110
+ className="justify-self-end self-end p-2 bg-transparent text-destructive rounded-md border-none shadow-none hover:bg-transparent hover:shadow-none hover:text-destructive focus:ring-0 active:ring-0"
111
+ disabled={distributions.length <= 2}
112
+ >
113
+ <Trash2 className="h-5 w-5" />
114
+ </Button>
115
+ </div>
116
+ ))}
117
+
118
+ <div className="flex justify-between items-center mt-4">
119
+ <Button
120
+ type="button"
121
+ variant="outline"
122
+ onClick={handleAddDistribution}
123
+ disabled={isAnyDistributionEmpty}
124
+ className="cursor-pointer"
125
+ >
126
+ Add Item
127
+ </Button>
128
+
129
+ <div className="flex items-center gap-4">
130
+ <div className="text-xs text-muted-foreground">
131
+ <p>
132
+ <span className="font-bold">Total Amount: </span>
133
+ {distributedSum.toFixed(2)} / {allowedAmount.toFixed(2)}
134
+ </p>
135
+ {!isExactMatch && (
136
+ <p className="text-destructive">
137
+ <span className="font-bold">Difference: </span>
138
+ {difference.toFixed(2)}
139
+ </p>
140
+ )}
141
+ </div>
142
+
143
+ <p className="text-xs text-muted-foreground">
144
+ <span className="font-bold">Total Balance: </span>
145
+ {formatCurrency(
146
+ selectedEscrow?.balance || 0,
147
+ selectedEscrow?.trustline.name || ""
148
+ )}
149
+ </p>
150
+ </div>
151
+ </div>
152
+
153
+ <div className="mt-4 flex justify-start items-center">
154
+ <Button
155
+ type="submit"
156
+ disabled={
157
+ isSubmitting || isAnyDistributionEmpty || !isExactMatch
158
+ }
159
+ className="cursor-pointer"
160
+ >
161
+ {isSubmitting ? (
162
+ <div className="flex items-center">
163
+ <Loader2 className="h-5 w-5 animate-spin" />
164
+ <span className="ml-2">Withdrawing...</span>
165
+ </div>
166
+ ) : (
167
+ "Withdraw"
168
+ )}
169
+ </Button>
170
+ </div>
171
+ </form>
172
+ </Form>
173
+ </DialogContent>
174
+ </Dialog>
175
+ );
176
+ };