@trustless-work/blocks 1.0.0 → 1.0.2
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.
- package/bin/index.js +78 -1
- package/package.json +1 -1
- package/templates/deps.json +1 -1
- package/templates/escrows/details/Actions.tsx +21 -1
- package/templates/escrows/indicators/balance-progress/bar/BalanceProgress.tsx +55 -0
- package/templates/escrows/indicators/balance-progress/donut/BalanceProgress.tsx +99 -0
- package/templates/escrows/multi-release/initialize-escrow/dialog/InitializeEscrow.tsx +1 -0
- package/templates/escrows/multi-release/initialize-escrow/form/InitializeEscrow.tsx +1 -0
- package/templates/escrows/multi-release/initialize-escrow/shared/schema.ts +0 -1
- package/templates/escrows/multi-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -2
- package/templates/escrows/multi-release/resolve-dispute/button/ResolveDispute.tsx +10 -20
- package/templates/escrows/multi-release/resolve-dispute/dialog/ResolveDispute.tsx +117 -60
- package/templates/escrows/multi-release/resolve-dispute/form/ResolveDispute.tsx +111 -55
- package/templates/escrows/multi-release/resolve-dispute/shared/schema.ts +68 -71
- package/templates/escrows/multi-release/resolve-dispute/shared/useResolveDispute.ts +107 -21
- package/templates/escrows/multi-release/update-escrow/shared/schema.ts +0 -1
- package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +0 -4
- package/templates/escrows/multi-release/withdraw-remaining-funds/button/WithdrawRemainingFunds.tsx +85 -0
- package/templates/escrows/multi-release/withdraw-remaining-funds/dialog/WithdrawRemainingFunds.tsx +176 -0
- package/templates/escrows/multi-release/withdraw-remaining-funds/form/WithdrawRemainingFunds.tsx +153 -0
- package/templates/escrows/multi-release/withdraw-remaining-funds/shared/schema.ts +81 -0
- package/templates/escrows/multi-release/withdraw-remaining-funds/shared/useWithdrawRemainingFunds.ts +160 -0
- package/templates/escrows/single-multi-release/approve-milestone/button/ApproveMilestone.tsx +0 -1
- package/templates/escrows/single-multi-release/approve-milestone/shared/useApproveMilestone.ts +0 -1
- package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +0 -1
- package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -2
- package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +15 -31
- package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +116 -60
- package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +98 -43
- package/templates/escrows/single-release/resolve-dispute/shared/schema.ts +65 -68
- package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +100 -22
- package/templates/escrows/single-release/update-escrow/shared/schema.ts +0 -1
- package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +0 -4
- package/templates/tanstack/useEscrowsMutations.ts +53 -0
- package/templates/tanstack/useGetMultipleEscrowBalances.ts +41 -0
- package/templates/wallet-kit/trustlines.ts +0 -4
|
@@ -11,7 +11,7 @@ import { Input } from "__UI_BASE__/input";
|
|
|
11
11
|
import { Button } from "__UI_BASE__/button";
|
|
12
12
|
import { useResolveDispute } from "./useResolveDispute";
|
|
13
13
|
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
14
|
-
import { Loader2 } from "lucide-react";
|
|
14
|
+
import { Loader2, Trash2 } from "lucide-react";
|
|
15
15
|
import {
|
|
16
16
|
Select,
|
|
17
17
|
SelectContent,
|
|
@@ -28,7 +28,22 @@ export const ResolveDisputeForm = ({
|
|
|
28
28
|
showSelectMilestone?: boolean;
|
|
29
29
|
milestoneIndex?: number | string;
|
|
30
30
|
}) => {
|
|
31
|
-
const {
|
|
31
|
+
const {
|
|
32
|
+
form,
|
|
33
|
+
handleSubmit,
|
|
34
|
+
isSubmitting,
|
|
35
|
+
totalAmount,
|
|
36
|
+
distributions,
|
|
37
|
+
handleAddDistribution,
|
|
38
|
+
handleRemoveDistribution,
|
|
39
|
+
handleDistributionAddressChange,
|
|
40
|
+
handleDistributionAmountChange,
|
|
41
|
+
isAnyDistributionEmpty,
|
|
42
|
+
allowedAmount,
|
|
43
|
+
distributedSum,
|
|
44
|
+
isExactMatch,
|
|
45
|
+
difference,
|
|
46
|
+
} = useResolveDispute();
|
|
32
47
|
const { selectedEscrow } = useEscrowContext();
|
|
33
48
|
|
|
34
49
|
React.useEffect(() => {
|
|
@@ -79,52 +94,106 @@ export const ResolveDisputeForm = ({
|
|
|
79
94
|
/>
|
|
80
95
|
)}
|
|
81
96
|
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
name="approverFunds"
|
|
86
|
-
render={({ field }) => (
|
|
87
|
-
<FormItem>
|
|
88
|
-
<FormLabel>Approver Funds</FormLabel>
|
|
89
|
-
<FormControl>
|
|
90
|
-
<Input
|
|
91
|
-
type="text"
|
|
92
|
-
inputMode="decimal"
|
|
93
|
-
placeholder="Enter approver funds"
|
|
94
|
-
value={field.value as unknown as string}
|
|
95
|
-
onChange={(e) => field.onChange(e.target.value)}
|
|
96
|
-
/>
|
|
97
|
-
</FormControl>
|
|
98
|
-
<FormMessage />
|
|
99
|
-
</FormItem>
|
|
100
|
-
)}
|
|
101
|
-
/>
|
|
97
|
+
<FormLabel className="flex items-center my-4">
|
|
98
|
+
Distributions<span className="text-destructive ml-1">*</span>
|
|
99
|
+
</FormLabel>
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
101
|
+
{distributions.map((d, idx) => (
|
|
102
|
+
<div
|
|
103
|
+
key={`dist-${idx}`}
|
|
104
|
+
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"
|
|
105
|
+
>
|
|
106
|
+
<FormField
|
|
107
|
+
control={form.control}
|
|
108
|
+
name={`distributions.${idx}.address` as const}
|
|
109
|
+
render={() => (
|
|
110
|
+
<FormItem className="sm:col-span-2 lg:col-span-1">
|
|
111
|
+
<FormLabel>Address</FormLabel>
|
|
112
|
+
<FormControl>
|
|
113
|
+
<Input
|
|
114
|
+
type="text"
|
|
115
|
+
placeholder="Receiver address"
|
|
116
|
+
value={d.address}
|
|
117
|
+
onChange={(e) =>
|
|
118
|
+
handleDistributionAddressChange(idx, e.target.value)
|
|
119
|
+
}
|
|
120
|
+
/>
|
|
121
|
+
</FormControl>
|
|
122
|
+
<FormMessage />
|
|
123
|
+
</FormItem>
|
|
124
|
+
)}
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
<FormField
|
|
128
|
+
control={form.control}
|
|
129
|
+
name={`distributions.${idx}.amount` as const}
|
|
130
|
+
render={() => (
|
|
131
|
+
<FormItem>
|
|
132
|
+
<FormLabel>Amount</FormLabel>
|
|
133
|
+
<FormControl>
|
|
134
|
+
<Input
|
|
135
|
+
type="text"
|
|
136
|
+
inputMode="decimal"
|
|
137
|
+
placeholder="0.00"
|
|
138
|
+
value={(d.amount as string) ?? ""}
|
|
139
|
+
onChange={(e) => handleDistributionAmountChange(idx, e)}
|
|
140
|
+
/>
|
|
141
|
+
</FormControl>
|
|
142
|
+
<FormMessage />
|
|
143
|
+
</FormItem>
|
|
144
|
+
)}
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
<Button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={() => handleRemoveDistribution(idx)}
|
|
150
|
+
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"
|
|
151
|
+
disabled={distributions.length <= 2}
|
|
152
|
+
>
|
|
153
|
+
<Trash2 className="h-5 w-5" />
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
|
|
158
|
+
<div className="flex justify-between items-center">
|
|
159
|
+
<Button
|
|
160
|
+
type="button"
|
|
161
|
+
variant="outline"
|
|
162
|
+
onClick={handleAddDistribution}
|
|
163
|
+
disabled={isAnyDistributionEmpty}
|
|
164
|
+
className="cursor-pointer"
|
|
165
|
+
>
|
|
166
|
+
Add Item
|
|
167
|
+
</Button>
|
|
168
|
+
|
|
169
|
+
<div className="flex items-center gap-4">
|
|
170
|
+
<div className="text-xs text-muted-foreground">
|
|
171
|
+
<p>
|
|
172
|
+
<span className="font-bold">Total Amount: </span>
|
|
173
|
+
{distributedSum.toFixed(2)} / {allowedAmount.toFixed(2)}
|
|
174
|
+
</p>
|
|
175
|
+
{!isExactMatch && (
|
|
176
|
+
<p className="text-destructive">
|
|
177
|
+
<span className="font-bold">Difference: </span>
|
|
178
|
+
{difference.toFixed(2)}
|
|
179
|
+
</p>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<p className="text-xs text-muted-foreground">
|
|
184
|
+
<span className="font-bold">Total Balance: </span>
|
|
185
|
+
{formatCurrency(
|
|
186
|
+
selectedEscrow?.balance || 0,
|
|
187
|
+
selectedEscrow?.trustline.name || ""
|
|
188
|
+
)}
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
122
191
|
</div>
|
|
123
192
|
|
|
124
193
|
<div className="mt-4">
|
|
125
194
|
<Button
|
|
126
195
|
type="submit"
|
|
127
|
-
disabled={isSubmitting}
|
|
196
|
+
disabled={isSubmitting || isAnyDistributionEmpty || !isExactMatch}
|
|
128
197
|
className="cursor-pointer"
|
|
129
198
|
>
|
|
130
199
|
{isSubmitting ? (
|
|
@@ -136,19 +205,6 @@ export const ResolveDisputeForm = ({
|
|
|
136
205
|
"Resolve"
|
|
137
206
|
)}
|
|
138
207
|
</Button>
|
|
139
|
-
|
|
140
|
-
<p className="text-xs text-muted-foreground">
|
|
141
|
-
<span className="font-bold">Total Amount: </span>
|
|
142
|
-
{formatCurrency(totalAmount, selectedEscrow?.trustline.name || "")}
|
|
143
|
-
</p>
|
|
144
|
-
|
|
145
|
-
<p className="text-xs text-muted-foreground">
|
|
146
|
-
<span className="font-bold">Total Balance: </span>
|
|
147
|
-
{formatCurrency(
|
|
148
|
-
selectedEscrow?.balance || 0,
|
|
149
|
-
selectedEscrow?.trustline.name || ""
|
|
150
|
-
)}
|
|
151
|
-
</p>
|
|
152
208
|
</div>
|
|
153
209
|
</form>
|
|
154
210
|
</Form>
|
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
(val)
|
|
9
|
-
if (
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 {
|
|
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
|
}
|
|
@@ -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
|
|
package/templates/escrows/multi-release/withdraw-remaining-funds/button/WithdrawRemainingFunds.tsx
ADDED
|
@@ -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
|
+
};
|