@trustless-work/blocks 1.1.8 → 1.2.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.
- package/bin/index.js +88 -0
- package/package.json +1 -1
- package/templates/escrows/details/GeneralInformation.tsx +10 -7
- package/templates/escrows/details/MilestoneCard.tsx +4 -1
- package/templates/escrows/escrows-by-role/cards/EscrowsCards.tsx +4 -4
- package/templates/escrows/escrows-by-role/table/EscrowsTable.tsx +2 -2
- package/templates/escrows/escrows-by-signer/cards/EscrowsCards.tsx +4 -4
- package/templates/escrows/escrows-by-signer/table/EscrowsTable.tsx +2 -2
- package/templates/escrows/load-escrow/dialog/LoadEscrow.tsx +3 -1
- package/templates/escrows/load-escrow/shared/useLoadEscrow.ts +5 -1
- package/templates/escrows/multi-release/dispute-milestone/dialog/DisputeMilestone.tsx +103 -0
- package/templates/escrows/multi-release/dispute-milestone/form/DisputeMilestone.tsx +81 -0
- package/templates/escrows/multi-release/dispute-milestone/shared/schema.ts +9 -0
- package/templates/escrows/multi-release/dispute-milestone/shared/useDisputeMilestone.ts +99 -0
- package/templates/escrows/multi-release/release-milestone/dialog/ReleaseMilestone.tsx +103 -0
- package/templates/escrows/multi-release/release-milestone/form/ReleaseMilestone.tsx +81 -0
- package/templates/escrows/multi-release/release-milestone/shared/schema.ts +10 -0
- package/templates/escrows/multi-release/release-milestone/shared/useReleaseMilestone.ts +121 -0
- package/templates/escrows/multi-release/resolve-dispute/dialog/ResolveDispute.tsx +1 -1
- package/templates/escrows/multi-release/resolve-dispute/form/ResolveDispute.tsx +1 -1
- package/templates/escrows/multi-release/resolve-dispute/shared/useResolveDispute.ts +22 -4
- package/templates/escrows/multi-release/update-escrow/shared/schema.ts +3 -0
- package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +3 -7
- package/templates/escrows/multi-release/withdraw-remaining-funds/dialog/WithdrawRemainingFunds.tsx +1 -1
- package/templates/escrows/multi-release/withdraw-remaining-funds/form/WithdrawRemainingFunds.tsx +1 -1
- package/templates/escrows/single-release/initialize-escrow/dialog/InitializeEscrow.tsx +1 -0
- package/templates/escrows/single-release/initialize-escrow/form/InitializeEscrow.tsx +1 -0
- package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +1 -1
- package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +2 -6
- package/templates/wallet-kit/trustlines.ts +5 -5
package/bin/index.js
CHANGED
|
@@ -865,6 +865,90 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
|
|
|
865
865
|
);
|
|
866
866
|
}
|
|
867
867
|
|
|
868
|
+
try {
|
|
869
|
+
const isMultiDisputeRoot =
|
|
870
|
+
name === "escrows/multi-release/dispute-milestone";
|
|
871
|
+
const isMultiDisputeDialog =
|
|
872
|
+
name === "escrows/multi-release/dispute-milestone/dialog";
|
|
873
|
+
const isMultiDisputeForm =
|
|
874
|
+
name === "escrows/multi-release/dispute-milestone/form";
|
|
875
|
+
|
|
876
|
+
const srcSharedDir = path.join(
|
|
877
|
+
TEMPLATES_DIR,
|
|
878
|
+
"escrows",
|
|
879
|
+
"multi-release",
|
|
880
|
+
"dispute-milestone",
|
|
881
|
+
"shared"
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
function copyMultiDisputeSharedInto(targetDir) {
|
|
885
|
+
if (!fs.existsSync(srcSharedDir)) return;
|
|
886
|
+
const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
|
|
887
|
+
for (const entry of entries) {
|
|
888
|
+
if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
|
|
889
|
+
const entrySrc = path.join(srcSharedDir, entry.name);
|
|
890
|
+
const entryDest = path.join(targetDir, entry.name);
|
|
891
|
+
writeTransformed(entrySrc, entryDest);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (isMultiDisputeRoot) {
|
|
896
|
+
copyMultiDisputeSharedInto(path.join(destDir, "dialog"));
|
|
897
|
+
copyMultiDisputeSharedInto(path.join(destDir, "form"));
|
|
898
|
+
} else if (isMultiDisputeDialog) {
|
|
899
|
+
copyMultiDisputeSharedInto(destDir);
|
|
900
|
+
} else if (isMultiDisputeForm) {
|
|
901
|
+
copyMultiDisputeSharedInto(destDir);
|
|
902
|
+
}
|
|
903
|
+
} catch (e) {
|
|
904
|
+
console.warn(
|
|
905
|
+
"⚠️ Failed to materialize shared multi-release dispute-milestone files:",
|
|
906
|
+
e?.message || e
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
try {
|
|
911
|
+
const isMultiReleaseRoot =
|
|
912
|
+
name === "escrows/multi-release/release-milestone";
|
|
913
|
+
const isMultiReleaseDialog =
|
|
914
|
+
name === "escrows/multi-release/release-milestone/dialog";
|
|
915
|
+
const isMultiReleaseForm =
|
|
916
|
+
name === "escrows/multi-release/release-milestone/form";
|
|
917
|
+
|
|
918
|
+
const srcSharedDir = path.join(
|
|
919
|
+
TEMPLATES_DIR,
|
|
920
|
+
"escrows",
|
|
921
|
+
"multi-release",
|
|
922
|
+
"release-milestone",
|
|
923
|
+
"shared"
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
function copyMultiReleaseSharedInto(targetDir) {
|
|
927
|
+
if (!fs.existsSync(srcSharedDir)) return;
|
|
928
|
+
const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
|
|
929
|
+
for (const entry of entries) {
|
|
930
|
+
if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
|
|
931
|
+
const entrySrc = path.join(srcSharedDir, entry.name);
|
|
932
|
+
const entryDest = path.join(targetDir, entry.name);
|
|
933
|
+
writeTransformed(entrySrc, entryDest);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (isMultiReleaseRoot) {
|
|
938
|
+
copyMultiReleaseSharedInto(path.join(destDir, "dialog"));
|
|
939
|
+
copyMultiReleaseSharedInto(path.join(destDir, "form"));
|
|
940
|
+
} else if (isMultiReleaseDialog) {
|
|
941
|
+
copyMultiReleaseSharedInto(destDir);
|
|
942
|
+
} else if (isMultiReleaseForm) {
|
|
943
|
+
copyMultiReleaseSharedInto(destDir);
|
|
944
|
+
}
|
|
945
|
+
} catch (e) {
|
|
946
|
+
console.warn(
|
|
947
|
+
"⚠️ Failed to materialize shared multi-release release-milestone files:",
|
|
948
|
+
e?.message || e
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
868
952
|
try {
|
|
869
953
|
const isMultiUpdateRoot = name === "escrows/multi-release/update-escrow";
|
|
870
954
|
const isMultiUpdateDialog =
|
|
@@ -1124,6 +1208,8 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
|
|
|
1124
1208
|
"resolve-dispute",
|
|
1125
1209
|
"update-escrow",
|
|
1126
1210
|
"withdraw-remaining-funds",
|
|
1211
|
+
"dispute-milestone",
|
|
1212
|
+
"release-milestone",
|
|
1127
1213
|
];
|
|
1128
1214
|
|
|
1129
1215
|
for (const mod of modules) {
|
|
@@ -1255,6 +1341,8 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
|
|
|
1255
1341
|
"resolve-dispute",
|
|
1256
1342
|
"update-escrow",
|
|
1257
1343
|
"withdraw-remaining-funds",
|
|
1344
|
+
"dispute-milestone",
|
|
1345
|
+
"release-milestone",
|
|
1258
1346
|
];
|
|
1259
1347
|
|
|
1260
1348
|
const baseTarget = path.join(destDir, "multi-release");
|
package/package.json
CHANGED
|
@@ -96,7 +96,10 @@ export const GeneralInformation = ({
|
|
|
96
96
|
<StatisticsCard
|
|
97
97
|
title="Amount"
|
|
98
98
|
icon={CircleDollarSign}
|
|
99
|
-
value={formatCurrency(
|
|
99
|
+
value={formatCurrency(
|
|
100
|
+
totalAmount,
|
|
101
|
+
selectedEscrow.trustline?.symbol
|
|
102
|
+
)}
|
|
100
103
|
/>
|
|
101
104
|
|
|
102
105
|
<StatisticsCard
|
|
@@ -104,7 +107,7 @@ export const GeneralInformation = ({
|
|
|
104
107
|
icon={Wallet}
|
|
105
108
|
value={formatCurrency(
|
|
106
109
|
selectedEscrow.balance ?? 0,
|
|
107
|
-
selectedEscrow.trustline?.
|
|
110
|
+
selectedEscrow.trustline?.symbol
|
|
108
111
|
)}
|
|
109
112
|
/>
|
|
110
113
|
</div>
|
|
@@ -136,7 +139,7 @@ export const GeneralInformation = ({
|
|
|
136
139
|
<div className="flex-1 min-w-0">
|
|
137
140
|
<div className="flex items-center justify-between mb-2">
|
|
138
141
|
<span className="text-sm font-medium text-muted-foreground">
|
|
139
|
-
{selectedEscrow.trustline?.
|
|
142
|
+
{selectedEscrow.trustline?.symbol || "No Trustline"} |
|
|
140
143
|
Escrow ID
|
|
141
144
|
</span>
|
|
142
145
|
<button
|
|
@@ -237,7 +240,7 @@ export const GeneralInformation = ({
|
|
|
237
240
|
<span className="font-medium">
|
|
238
241
|
{formatCurrency(
|
|
239
242
|
selectedEscrow.amount,
|
|
240
|
-
selectedEscrow.trustline?.
|
|
243
|
+
selectedEscrow.trustline?.symbol
|
|
241
244
|
)}
|
|
242
245
|
</span>
|
|
243
246
|
</div>
|
|
@@ -256,7 +259,7 @@ export const GeneralInformation = ({
|
|
|
256
259
|
<span className="font-medium">
|
|
257
260
|
{formatCurrency(
|
|
258
261
|
Number(receiverAmount.toFixed(2)),
|
|
259
|
-
selectedEscrow.trustline?.
|
|
262
|
+
selectedEscrow.trustline?.symbol
|
|
260
263
|
)}
|
|
261
264
|
</span>
|
|
262
265
|
</div>
|
|
@@ -276,7 +279,7 @@ export const GeneralInformation = ({
|
|
|
276
279
|
<span className="font-medium">
|
|
277
280
|
{formatCurrency(
|
|
278
281
|
Number(platformFeeAmount.toFixed(2)),
|
|
279
|
-
selectedEscrow.trustline?.
|
|
282
|
+
selectedEscrow.trustline?.symbol
|
|
280
283
|
)}
|
|
281
284
|
</span>
|
|
282
285
|
</div>
|
|
@@ -297,7 +300,7 @@ export const GeneralInformation = ({
|
|
|
297
300
|
<span className="font-medium">
|
|
298
301
|
{formatCurrency(
|
|
299
302
|
Number(trustlessWorkAmount.toFixed(2)),
|
|
300
|
-
selectedEscrow.trustline?.
|
|
303
|
+
selectedEscrow.trustline?.symbol
|
|
301
304
|
)}
|
|
302
305
|
</span>
|
|
303
306
|
</div>
|
|
@@ -202,7 +202,10 @@ const MilestoneCardComponent = ({
|
|
|
202
202
|
{"amount" in milestone && (
|
|
203
203
|
<div className="flex items-center gap-2 py-2">
|
|
204
204
|
<span className="text-2xl font-bold text-foreground">
|
|
205
|
-
{formatCurrency(
|
|
205
|
+
{formatCurrency(
|
|
206
|
+
milestone.amount,
|
|
207
|
+
selectedEscrow.trustline?.symbol
|
|
208
|
+
)}
|
|
206
209
|
</span>
|
|
207
210
|
</div>
|
|
208
211
|
)}
|
|
@@ -293,7 +293,7 @@ export const EscrowsByRoleCards = () => {
|
|
|
293
293
|
{escrow.type === "single-release"
|
|
294
294
|
? formatCurrency(
|
|
295
295
|
escrow.amount,
|
|
296
|
-
escrow.trustline.
|
|
296
|
+
escrow.trustline.symbol
|
|
297
297
|
)
|
|
298
298
|
: formatCurrency(
|
|
299
299
|
escrow.milestones.reduce(
|
|
@@ -303,7 +303,7 @@ export const EscrowsByRoleCards = () => {
|
|
|
303
303
|
.amount,
|
|
304
304
|
0
|
|
305
305
|
),
|
|
306
|
-
escrow.trustline.
|
|
306
|
+
escrow.trustline.symbol
|
|
307
307
|
)}
|
|
308
308
|
</span>
|
|
309
309
|
</div>
|
|
@@ -316,7 +316,7 @@ export const EscrowsByRoleCards = () => {
|
|
|
316
316
|
<span className="font-medium text-green-800 dark:text-green-600">
|
|
317
317
|
{formatCurrency(
|
|
318
318
|
escrow.balance,
|
|
319
|
-
escrow.trustline.
|
|
319
|
+
escrow.trustline.symbol
|
|
320
320
|
)}
|
|
321
321
|
</span>
|
|
322
322
|
</div>
|
|
@@ -362,7 +362,7 @@ export const EscrowsByRoleCards = () => {
|
|
|
362
362
|
<span className="text-muted-foreground">
|
|
363
363
|
{formatCurrency(
|
|
364
364
|
milestone.amount,
|
|
365
|
-
escrow.trustline.
|
|
365
|
+
escrow.trustline.symbol
|
|
366
366
|
)}
|
|
367
367
|
</span>
|
|
368
368
|
|
|
@@ -160,9 +160,9 @@ export const EscrowsByRoleTable = () => {
|
|
|
160
160
|
cell: ({ row }) => (
|
|
161
161
|
<span
|
|
162
162
|
className="max-w-[220px] truncate block"
|
|
163
|
-
title={`${row.original.trustline.
|
|
163
|
+
title={`${row.original.trustline.symbol} (${row.original.trustline.address})`}
|
|
164
164
|
>
|
|
165
|
-
{row.original.trustline.
|
|
165
|
+
{row.original.trustline.symbol}
|
|
166
166
|
</span>
|
|
167
167
|
),
|
|
168
168
|
},
|
|
@@ -281,7 +281,7 @@ export const EscrowsBySignerCards = () => {
|
|
|
281
281
|
{escrow.type === "single-release"
|
|
282
282
|
? formatCurrency(
|
|
283
283
|
escrow.amount,
|
|
284
|
-
escrow.trustline.
|
|
284
|
+
escrow.trustline.symbol
|
|
285
285
|
)
|
|
286
286
|
: formatCurrency(
|
|
287
287
|
escrow.milestones.reduce(
|
|
@@ -291,7 +291,7 @@ export const EscrowsBySignerCards = () => {
|
|
|
291
291
|
.amount,
|
|
292
292
|
0
|
|
293
293
|
),
|
|
294
|
-
escrow.trustline.
|
|
294
|
+
escrow.trustline.symbol
|
|
295
295
|
)}
|
|
296
296
|
</span>
|
|
297
297
|
</div>
|
|
@@ -304,7 +304,7 @@ export const EscrowsBySignerCards = () => {
|
|
|
304
304
|
<span className="font-medium text-green-800 dark:text-green-600">
|
|
305
305
|
{formatCurrency(
|
|
306
306
|
escrow.balance,
|
|
307
|
-
escrow.trustline.
|
|
307
|
+
escrow.trustline.symbol
|
|
308
308
|
)}
|
|
309
309
|
</span>
|
|
310
310
|
</div>
|
|
@@ -350,7 +350,7 @@ export const EscrowsBySignerCards = () => {
|
|
|
350
350
|
<span className="text-muted-foreground">
|
|
351
351
|
{formatCurrency(
|
|
352
352
|
milestone.amount,
|
|
353
|
-
escrow.trustline.
|
|
353
|
+
escrow.trustline.symbol
|
|
354
354
|
)}
|
|
355
355
|
</span>
|
|
356
356
|
|
|
@@ -150,9 +150,9 @@ export const EscrowsBySignerTable = () => {
|
|
|
150
150
|
cell: ({ row }) => (
|
|
151
151
|
<span
|
|
152
152
|
className="max-w-[220px] truncate block"
|
|
153
|
-
title={`${row.original.trustline.
|
|
153
|
+
title={`${row.original.trustline.symbol} (${row.original.trustline.address})`}
|
|
154
154
|
>
|
|
155
|
-
{row.original.trustline.
|
|
155
|
+
{row.original.trustline.symbol}
|
|
156
156
|
</span>
|
|
157
157
|
),
|
|
158
158
|
},
|
|
@@ -22,7 +22,9 @@ import * as React from "react";
|
|
|
22
22
|
|
|
23
23
|
export function LoadEscrowDialog() {
|
|
24
24
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
25
|
-
const { form, isSubmitting, onSubmit } = useLoadEscrow(
|
|
25
|
+
const { form, isSubmitting, onSubmit } = useLoadEscrow({
|
|
26
|
+
onSuccess: () => setIsOpen(false),
|
|
27
|
+
});
|
|
26
28
|
|
|
27
29
|
return (
|
|
28
30
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
@@ -12,7 +12,9 @@ import { GetEscrowsFromIndexerResponse } from "@trustless-work/escrow/types";
|
|
|
12
12
|
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
13
13
|
import { formSchema } from "./schema";
|
|
14
14
|
|
|
15
|
-
export const useLoadEscrow = (
|
|
15
|
+
export const useLoadEscrow = ({
|
|
16
|
+
onSuccess,
|
|
17
|
+
}: { onSuccess?: () => void } = {}) => {
|
|
16
18
|
const { setSelectedEscrow } = useEscrowContext();
|
|
17
19
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
18
20
|
const { getEscrowByContractIds } = useGetEscrowFromIndexerByContractIds();
|
|
@@ -46,6 +48,8 @@ export const useLoadEscrow = () => {
|
|
|
46
48
|
toast.success(
|
|
47
49
|
"Escrow data fetched successfully. Now you can use the selectedEscrow state"
|
|
48
50
|
);
|
|
51
|
+
|
|
52
|
+
onSuccess?.();
|
|
49
53
|
} catch (error) {
|
|
50
54
|
toast.error(handleError(error as ErrorResponse).message);
|
|
51
55
|
} finally {
|
|
@@ -0,0 +1,103 @@
|
|
|
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 { Button } from "__UI_BASE__/button";
|
|
11
|
+
import {
|
|
12
|
+
Dialog,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
DialogTrigger,
|
|
17
|
+
} from "__UI_BASE__/dialog";
|
|
18
|
+
import { Loader2 } from "lucide-react";
|
|
19
|
+
import { useDisputeMilestone } from "./useDisputeMilestone";
|
|
20
|
+
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
21
|
+
import {
|
|
22
|
+
Select,
|
|
23
|
+
SelectContent,
|
|
24
|
+
SelectItem,
|
|
25
|
+
SelectTrigger,
|
|
26
|
+
SelectValue,
|
|
27
|
+
} from "__UI_BASE__/select";
|
|
28
|
+
|
|
29
|
+
export const DisputeMilestoneDialog = () => {
|
|
30
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
31
|
+
const { form, handleSubmit, isSubmitting } = useDisputeMilestone({
|
|
32
|
+
onSuccess: () => setIsOpen(false),
|
|
33
|
+
});
|
|
34
|
+
const { selectedEscrow } = useEscrowContext();
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
38
|
+
<DialogTrigger asChild>
|
|
39
|
+
<Button type="button" className="cursor-pointer w-full">
|
|
40
|
+
Dispute Milestone
|
|
41
|
+
</Button>
|
|
42
|
+
</DialogTrigger>
|
|
43
|
+
<DialogContent className="!w-full sm:!max-w-md">
|
|
44
|
+
<DialogHeader>
|
|
45
|
+
<DialogTitle>Dispute Milestone</DialogTitle>
|
|
46
|
+
</DialogHeader>
|
|
47
|
+
<Form {...form}>
|
|
48
|
+
<form onSubmit={handleSubmit}>
|
|
49
|
+
<FormField
|
|
50
|
+
control={form.control}
|
|
51
|
+
name="milestoneIndex"
|
|
52
|
+
render={({ field }) => (
|
|
53
|
+
<FormItem>
|
|
54
|
+
<FormLabel className="flex items-center">
|
|
55
|
+
Milestone
|
|
56
|
+
<span className="text-destructive ml-1">*</span>
|
|
57
|
+
</FormLabel>
|
|
58
|
+
<FormControl>
|
|
59
|
+
<Select
|
|
60
|
+
value={field.value}
|
|
61
|
+
onValueChange={(e) => {
|
|
62
|
+
field.onChange(e);
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<SelectTrigger className="w-full">
|
|
66
|
+
<SelectValue placeholder="Select milestone" />
|
|
67
|
+
</SelectTrigger>
|
|
68
|
+
<SelectContent>
|
|
69
|
+
{(selectedEscrow?.milestones || []).map((m, idx) => (
|
|
70
|
+
<SelectItem key={`ms-${idx}`} value={String(idx)}>
|
|
71
|
+
{m?.description || `Milestone ${idx + 1}`}
|
|
72
|
+
</SelectItem>
|
|
73
|
+
))}
|
|
74
|
+
</SelectContent>
|
|
75
|
+
</Select>
|
|
76
|
+
</FormControl>
|
|
77
|
+
<FormMessage />
|
|
78
|
+
</FormItem>
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<div className="mt-4 flex justify-start items-center">
|
|
83
|
+
<Button
|
|
84
|
+
type="submit"
|
|
85
|
+
disabled={isSubmitting || !selectedEscrow?.balance}
|
|
86
|
+
className="cursor-pointer"
|
|
87
|
+
>
|
|
88
|
+
{isSubmitting ? (
|
|
89
|
+
<div className="flex items-center">
|
|
90
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
91
|
+
<span className="ml-2">Disputing...</span>
|
|
92
|
+
</div>
|
|
93
|
+
) : (
|
|
94
|
+
"Dispute Milestone"
|
|
95
|
+
)}
|
|
96
|
+
</Button>
|
|
97
|
+
</div>
|
|
98
|
+
</form>
|
|
99
|
+
</Form>
|
|
100
|
+
</DialogContent>
|
|
101
|
+
</Dialog>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
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 { Button } from "__UI_BASE__/button";
|
|
11
|
+
import { Loader2 } from "lucide-react";
|
|
12
|
+
import { useDisputeMilestone } from "./useDisputeMilestone";
|
|
13
|
+
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from "__UI_BASE__/select";
|
|
21
|
+
|
|
22
|
+
export const DisputeMilestoneForm = () => {
|
|
23
|
+
const { form, handleSubmit, isSubmitting } = useDisputeMilestone();
|
|
24
|
+
const { selectedEscrow } = useEscrowContext();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Form {...form}>
|
|
28
|
+
<form onSubmit={handleSubmit} className="flex flex-col space-y-6 w-full">
|
|
29
|
+
<FormField
|
|
30
|
+
control={form.control}
|
|
31
|
+
name="milestoneIndex"
|
|
32
|
+
render={({ field }) => (
|
|
33
|
+
<FormItem>
|
|
34
|
+
<FormLabel className="flex items-center">
|
|
35
|
+
Milestone
|
|
36
|
+
<span className="text-destructive ml-1">*</span>
|
|
37
|
+
</FormLabel>
|
|
38
|
+
<FormControl>
|
|
39
|
+
<Select
|
|
40
|
+
value={field.value}
|
|
41
|
+
onValueChange={(e) => {
|
|
42
|
+
field.onChange(e);
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<SelectTrigger className="w-full">
|
|
46
|
+
<SelectValue placeholder="Select milestone" />
|
|
47
|
+
</SelectTrigger>
|
|
48
|
+
<SelectContent>
|
|
49
|
+
{(selectedEscrow?.milestones || []).map((m, idx) => (
|
|
50
|
+
<SelectItem key={`ms-${idx}`} value={String(idx)}>
|
|
51
|
+
{m?.description || `Milestone ${idx + 1}`}
|
|
52
|
+
</SelectItem>
|
|
53
|
+
))}
|
|
54
|
+
</SelectContent>
|
|
55
|
+
</Select>
|
|
56
|
+
</FormControl>
|
|
57
|
+
<FormMessage />
|
|
58
|
+
</FormItem>
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<div className="mt-4">
|
|
63
|
+
<Button
|
|
64
|
+
type="submit"
|
|
65
|
+
disabled={isSubmitting || !selectedEscrow?.balance}
|
|
66
|
+
className="cursor-pointer"
|
|
67
|
+
>
|
|
68
|
+
{isSubmitting ? (
|
|
69
|
+
<div className="flex items-center">
|
|
70
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
71
|
+
<span className="ml-2">Disputing...</span>
|
|
72
|
+
</div>
|
|
73
|
+
) : (
|
|
74
|
+
"Dispute Milestone"
|
|
75
|
+
)}
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</form>
|
|
79
|
+
</Form>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const disputeMilestoneSchema = z.object({
|
|
4
|
+
milestoneIndex: z
|
|
5
|
+
.string({ required_error: "Milestone is required" })
|
|
6
|
+
.min(1, { message: "Milestone is required" }),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export type DisputeMilestoneValues = z.infer<typeof disputeMilestoneSchema>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useForm } from "react-hook-form";
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import {
|
|
5
|
+
disputeMilestoneSchema,
|
|
6
|
+
type DisputeMilestoneValues,
|
|
7
|
+
} from "./schema";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
import {
|
|
10
|
+
MultiReleaseStartDisputePayload,
|
|
11
|
+
MultiReleaseMilestone,
|
|
12
|
+
} from "@trustless-work/escrow";
|
|
13
|
+
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
14
|
+
import { useEscrowsMutations } from "@/components/tw-blocks/tanstack/useEscrowsMutations";
|
|
15
|
+
import {
|
|
16
|
+
ErrorResponse,
|
|
17
|
+
handleError,
|
|
18
|
+
} from "@/components/tw-blocks/handle-errors/handle";
|
|
19
|
+
import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
|
|
20
|
+
|
|
21
|
+
export function useDisputeMilestone({
|
|
22
|
+
onSuccess,
|
|
23
|
+
}: { onSuccess?: () => void } = {}) {
|
|
24
|
+
const { startDispute } = useEscrowsMutations();
|
|
25
|
+
const { selectedEscrow, updateEscrow } = useEscrowContext();
|
|
26
|
+
const { walletAddress } = useWalletContext();
|
|
27
|
+
|
|
28
|
+
const form = useForm<DisputeMilestoneValues>({
|
|
29
|
+
resolver: zodResolver(disputeMilestoneSchema),
|
|
30
|
+
defaultValues: {
|
|
31
|
+
milestoneIndex: "0",
|
|
32
|
+
},
|
|
33
|
+
mode: "onChange",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
37
|
+
|
|
38
|
+
const handleSubmit = form.handleSubmit(async (payload) => {
|
|
39
|
+
try {
|
|
40
|
+
setIsSubmitting(true);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create the payload for the dispute escrow mutation
|
|
44
|
+
*
|
|
45
|
+
* @returns The payload for the dispute escrow mutation
|
|
46
|
+
*/
|
|
47
|
+
const finalPayload: MultiReleaseStartDisputePayload = {
|
|
48
|
+
contractId: selectedEscrow?.contractId || "",
|
|
49
|
+
signer: walletAddress || "",
|
|
50
|
+
milestoneIndex: String(payload.milestoneIndex),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Call the dispute escrow mutation
|
|
55
|
+
*
|
|
56
|
+
* @param payload - The payload for the dispute escrow mutation
|
|
57
|
+
* @param type - The type of the escrow
|
|
58
|
+
* @param address - The address of the escrow
|
|
59
|
+
*/
|
|
60
|
+
await startDispute.mutateAsync({
|
|
61
|
+
payload: finalPayload,
|
|
62
|
+
type: "multi-release",
|
|
63
|
+
address: walletAddress || "",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
toast.success("Milestone disputed successfully");
|
|
67
|
+
|
|
68
|
+
updateEscrow({
|
|
69
|
+
...selectedEscrow,
|
|
70
|
+
milestones: selectedEscrow?.milestones.map((milestone, index) => {
|
|
71
|
+
if (index === Number(payload.milestoneIndex)) {
|
|
72
|
+
return {
|
|
73
|
+
...milestone,
|
|
74
|
+
flags: {
|
|
75
|
+
...(milestone as MultiReleaseMilestone).flags,
|
|
76
|
+
disputed: true,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return milestone;
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
onSuccess?.();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
toast.error(handleError(error as ErrorResponse).message);
|
|
87
|
+
} finally {
|
|
88
|
+
setIsSubmitting(false);
|
|
89
|
+
form.reset();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
form,
|
|
95
|
+
handleSubmit,
|
|
96
|
+
isSubmitting,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
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 { Button } from "__UI_BASE__/button";
|
|
11
|
+
import {
|
|
12
|
+
Dialog,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
DialogTrigger,
|
|
17
|
+
} from "__UI_BASE__/dialog";
|
|
18
|
+
import { Loader2 } from "lucide-react";
|
|
19
|
+
import { useReleaseMilestone } from "./useReleaseMilestone";
|
|
20
|
+
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
21
|
+
import {
|
|
22
|
+
Select,
|
|
23
|
+
SelectContent,
|
|
24
|
+
SelectItem,
|
|
25
|
+
SelectTrigger,
|
|
26
|
+
SelectValue,
|
|
27
|
+
} from "__UI_BASE__/select";
|
|
28
|
+
|
|
29
|
+
export const ReleaseMilestoneDialog = () => {
|
|
30
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
31
|
+
const { form, handleSubmit, isSubmitting } = useReleaseMilestone({
|
|
32
|
+
onSuccess: () => setIsOpen(false),
|
|
33
|
+
});
|
|
34
|
+
const { selectedEscrow } = useEscrowContext();
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
38
|
+
<DialogTrigger asChild>
|
|
39
|
+
<Button type="button" className="cursor-pointer w-full">
|
|
40
|
+
Release Milestone
|
|
41
|
+
</Button>
|
|
42
|
+
</DialogTrigger>
|
|
43
|
+
<DialogContent className="!w-full sm:!max-w-md">
|
|
44
|
+
<DialogHeader>
|
|
45
|
+
<DialogTitle>Release Milestone</DialogTitle>
|
|
46
|
+
</DialogHeader>
|
|
47
|
+
<Form {...form}>
|
|
48
|
+
<form onSubmit={handleSubmit}>
|
|
49
|
+
<FormField
|
|
50
|
+
control={form.control}
|
|
51
|
+
name="milestoneIndex"
|
|
52
|
+
render={({ field }) => (
|
|
53
|
+
<FormItem>
|
|
54
|
+
<FormLabel className="flex items-center">
|
|
55
|
+
Milestone
|
|
56
|
+
<span className="text-destructive ml-1">*</span>
|
|
57
|
+
</FormLabel>
|
|
58
|
+
<FormControl>
|
|
59
|
+
<Select
|
|
60
|
+
value={field.value}
|
|
61
|
+
onValueChange={(e) => {
|
|
62
|
+
field.onChange(e);
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<SelectTrigger className="w-full">
|
|
66
|
+
<SelectValue placeholder="Select milestone" />
|
|
67
|
+
</SelectTrigger>
|
|
68
|
+
<SelectContent>
|
|
69
|
+
{(selectedEscrow?.milestones || []).map((m, idx) => (
|
|
70
|
+
<SelectItem key={`ms-${idx}`} value={String(idx)}>
|
|
71
|
+
{m?.description || `Milestone ${idx + 1}`}
|
|
72
|
+
</SelectItem>
|
|
73
|
+
))}
|
|
74
|
+
</SelectContent>
|
|
75
|
+
</Select>
|
|
76
|
+
</FormControl>
|
|
77
|
+
<FormMessage />
|
|
78
|
+
</FormItem>
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<div className="mt-4 flex justify-start items-center">
|
|
83
|
+
<Button
|
|
84
|
+
type="submit"
|
|
85
|
+
disabled={isSubmitting}
|
|
86
|
+
className="cursor-pointer"
|
|
87
|
+
>
|
|
88
|
+
{isSubmitting ? (
|
|
89
|
+
<div className="flex items-center">
|
|
90
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
91
|
+
<span className="ml-2">Releasing...</span>
|
|
92
|
+
</div>
|
|
93
|
+
) : (
|
|
94
|
+
"Release Milestone"
|
|
95
|
+
)}
|
|
96
|
+
</Button>
|
|
97
|
+
</div>
|
|
98
|
+
</form>
|
|
99
|
+
</Form>
|
|
100
|
+
</DialogContent>
|
|
101
|
+
</Dialog>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
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 { Button } from "__UI_BASE__/button";
|
|
11
|
+
import { Loader2 } from "lucide-react";
|
|
12
|
+
import { useReleaseMilestone } from "./useReleaseMilestone";
|
|
13
|
+
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from "__UI_BASE__/select";
|
|
21
|
+
|
|
22
|
+
export const ReleaseMilestoneForm = () => {
|
|
23
|
+
const { form, handleSubmit, isSubmitting } = useReleaseMilestone();
|
|
24
|
+
const { selectedEscrow } = useEscrowContext();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Form {...form}>
|
|
28
|
+
<form onSubmit={handleSubmit} className="flex flex-col space-y-6 w-full">
|
|
29
|
+
<FormField
|
|
30
|
+
control={form.control}
|
|
31
|
+
name="milestoneIndex"
|
|
32
|
+
render={({ field }) => (
|
|
33
|
+
<FormItem>
|
|
34
|
+
<FormLabel className="flex items-center">
|
|
35
|
+
Milestone
|
|
36
|
+
<span className="text-destructive ml-1">*</span>
|
|
37
|
+
</FormLabel>
|
|
38
|
+
<FormControl>
|
|
39
|
+
<Select
|
|
40
|
+
value={field.value}
|
|
41
|
+
onValueChange={(e) => {
|
|
42
|
+
field.onChange(e);
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<SelectTrigger className="w-full">
|
|
46
|
+
<SelectValue placeholder="Select milestone" />
|
|
47
|
+
</SelectTrigger>
|
|
48
|
+
<SelectContent>
|
|
49
|
+
{(selectedEscrow?.milestones || []).map((m, idx) => (
|
|
50
|
+
<SelectItem key={`ms-${idx}`} value={String(idx)}>
|
|
51
|
+
{m?.description || `Milestone ${idx + 1}`}
|
|
52
|
+
</SelectItem>
|
|
53
|
+
))}
|
|
54
|
+
</SelectContent>
|
|
55
|
+
</Select>
|
|
56
|
+
</FormControl>
|
|
57
|
+
<FormMessage />
|
|
58
|
+
</FormItem>
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<div className="mt-4">
|
|
63
|
+
<Button
|
|
64
|
+
type="submit"
|
|
65
|
+
disabled={isSubmitting}
|
|
66
|
+
className="cursor-pointer"
|
|
67
|
+
>
|
|
68
|
+
{isSubmitting ? (
|
|
69
|
+
<div className="flex items-center">
|
|
70
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
71
|
+
<span className="ml-2">Releasing...</span>
|
|
72
|
+
</div>
|
|
73
|
+
) : (
|
|
74
|
+
"Release Milestone"
|
|
75
|
+
)}
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</form>
|
|
79
|
+
</Form>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const releaseMilestoneSchema = z.object({
|
|
4
|
+
milestoneIndex: z
|
|
5
|
+
.string({ required_error: "Milestone is required" })
|
|
6
|
+
.min(1, { message: "Milestone is required" }),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export type ReleaseMilestoneValues = z.infer<typeof releaseMilestoneSchema>;
|
|
10
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useForm } from "react-hook-form";
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import {
|
|
5
|
+
releaseMilestoneSchema,
|
|
6
|
+
type ReleaseMilestoneValues,
|
|
7
|
+
} from "./schema";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
import {
|
|
10
|
+
MultiReleaseReleaseFundsPayload,
|
|
11
|
+
MultiReleaseMilestone,
|
|
12
|
+
} from "@trustless-work/escrow";
|
|
13
|
+
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
14
|
+
import { useEscrowsMutations } from "@/components/tw-blocks/tanstack/useEscrowsMutations";
|
|
15
|
+
import {
|
|
16
|
+
ErrorResponse,
|
|
17
|
+
handleError,
|
|
18
|
+
} from "@/components/tw-blocks/handle-errors/handle";
|
|
19
|
+
import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
|
|
20
|
+
import { useEscrowDialogs } from "@/components/tw-blocks/providers/EscrowDialogsProvider";
|
|
21
|
+
import { useEscrowAmountContext } from "@/components/tw-blocks/providers/EscrowAmountProvider";
|
|
22
|
+
|
|
23
|
+
export function useReleaseMilestone({
|
|
24
|
+
onSuccess,
|
|
25
|
+
}: { onSuccess?: () => void } = {}) {
|
|
26
|
+
const { releaseFunds } = useEscrowsMutations();
|
|
27
|
+
const { selectedEscrow, updateEscrow } = useEscrowContext();
|
|
28
|
+
const dialogStates = useEscrowDialogs();
|
|
29
|
+
const { setAmounts, setLastReleasedMilestoneIndex } =
|
|
30
|
+
useEscrowAmountContext();
|
|
31
|
+
const { walletAddress } = useWalletContext();
|
|
32
|
+
|
|
33
|
+
const form = useForm<ReleaseMilestoneValues>({
|
|
34
|
+
resolver: zodResolver(releaseMilestoneSchema),
|
|
35
|
+
defaultValues: {
|
|
36
|
+
milestoneIndex: "0",
|
|
37
|
+
},
|
|
38
|
+
mode: "onChange",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
42
|
+
|
|
43
|
+
const handleSubmit = form.handleSubmit(async (payload) => {
|
|
44
|
+
try {
|
|
45
|
+
setIsSubmitting(true);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create the payload for the release escrow mutation
|
|
49
|
+
*
|
|
50
|
+
* @returns The payload for the release escrow mutation
|
|
51
|
+
*/
|
|
52
|
+
const finalPayload: MultiReleaseReleaseFundsPayload = {
|
|
53
|
+
contractId: selectedEscrow?.contractId || "",
|
|
54
|
+
releaseSigner: walletAddress || "",
|
|
55
|
+
milestoneIndex: String(payload.milestoneIndex),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Call the release escrow mutation
|
|
60
|
+
*
|
|
61
|
+
* @param payload - The payload for the release escrow mutation
|
|
62
|
+
* @param type - The type of the escrow
|
|
63
|
+
* @param address - The address of the escrow
|
|
64
|
+
*/
|
|
65
|
+
await releaseFunds.mutateAsync({
|
|
66
|
+
payload: finalPayload,
|
|
67
|
+
type: "multi-release",
|
|
68
|
+
address: walletAddress || "",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
toast.success("Milestone released successfully");
|
|
72
|
+
|
|
73
|
+
// Ensure amounts are up to date for the success dialog
|
|
74
|
+
if (selectedEscrow) {
|
|
75
|
+
const milestone = selectedEscrow.milestones?.[Number(payload.milestoneIndex)];
|
|
76
|
+
const releasedAmount = Number(
|
|
77
|
+
(milestone as MultiReleaseMilestone | undefined)?.amount || 0
|
|
78
|
+
);
|
|
79
|
+
const platformFee = Number(selectedEscrow.platformFee || 0);
|
|
80
|
+
setAmounts(releasedAmount, platformFee);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
updateEscrow({
|
|
84
|
+
...selectedEscrow,
|
|
85
|
+
milestones: selectedEscrow?.milestones.map((milestone, index) => {
|
|
86
|
+
if (index === Number(payload.milestoneIndex)) {
|
|
87
|
+
return {
|
|
88
|
+
...milestone,
|
|
89
|
+
flags: {
|
|
90
|
+
...(milestone as MultiReleaseMilestone).flags,
|
|
91
|
+
released: true,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return milestone;
|
|
96
|
+
}),
|
|
97
|
+
balance: (selectedEscrow?.balance || 0) - (selectedEscrow?.amount || 0),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Remember which milestone was released for the success dialog
|
|
101
|
+
setLastReleasedMilestoneIndex(Number(payload.milestoneIndex));
|
|
102
|
+
|
|
103
|
+
// Open success dialog
|
|
104
|
+
dialogStates.successRelease.setIsOpen(true);
|
|
105
|
+
|
|
106
|
+
onSuccess?.();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
toast.error(handleError(error as ErrorResponse).message);
|
|
109
|
+
} finally {
|
|
110
|
+
setIsSubmitting(false);
|
|
111
|
+
form.reset();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
form,
|
|
117
|
+
handleSubmit,
|
|
118
|
+
isSubmitting,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
@@ -204,7 +204,7 @@ export const ResolveDisputeDialog = ({
|
|
|
204
204
|
<span className="font-bold">Total Balance: </span>
|
|
205
205
|
{formatCurrency(
|
|
206
206
|
selectedEscrow?.balance || 0,
|
|
207
|
-
selectedEscrow?.trustline.
|
|
207
|
+
selectedEscrow?.trustline.symbol || ""
|
|
208
208
|
)}
|
|
209
209
|
</p>
|
|
210
210
|
</div>
|
|
@@ -184,7 +184,7 @@ export const ResolveDisputeForm = ({
|
|
|
184
184
|
<span className="font-bold">Total Balance: </span>
|
|
185
185
|
{formatCurrency(
|
|
186
186
|
selectedEscrow?.balance || 0,
|
|
187
|
-
selectedEscrow?.trustline.
|
|
187
|
+
selectedEscrow?.trustline.symbol || ""
|
|
188
188
|
)}
|
|
189
189
|
</p>
|
|
190
190
|
</div>
|
|
@@ -17,7 +17,9 @@ import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvid
|
|
|
17
17
|
|
|
18
18
|
type DistributionInput = { address: string; amount: string | number };
|
|
19
19
|
|
|
20
|
-
export function useResolveDispute({
|
|
20
|
+
export function useResolveDispute({
|
|
21
|
+
onSuccess,
|
|
22
|
+
}: { onSuccess?: () => void } = {}) {
|
|
21
23
|
const { resolveDispute } = useEscrowsMutations();
|
|
22
24
|
const { selectedEscrow, updateEscrow } = useEscrowContext();
|
|
23
25
|
const { walletAddress } = useWalletContext();
|
|
@@ -50,7 +52,12 @@ export function useResolveDispute({ onSuccess }: { onSuccess?: () => void } = {}
|
|
|
50
52
|
const idx = Number(milestoneIndexWatch);
|
|
51
53
|
const milestones = selectedEscrow.milestones as MultiReleaseMilestone[];
|
|
52
54
|
const m = milestones?.[idx];
|
|
53
|
-
|
|
55
|
+
const amount = m?.amount;
|
|
56
|
+
// Handle both string and number types
|
|
57
|
+
if (amount === undefined || amount === null) return 0;
|
|
58
|
+
const numAmount =
|
|
59
|
+
typeof amount === "string" ? Number(amount) : Number(amount);
|
|
60
|
+
return isNaN(numAmount) ? 0 : numAmount;
|
|
54
61
|
}, [selectedEscrow, milestoneIndexWatch]);
|
|
55
62
|
|
|
56
63
|
const distributions = form.watch("distributions") as DistributionInput[];
|
|
@@ -63,11 +70,22 @@ export function useResolveDispute({ onSuccess }: { onSuccess?: () => void } = {}
|
|
|
63
70
|
}, [distributions]);
|
|
64
71
|
|
|
65
72
|
const isExactMatch = React.useMemo(() => {
|
|
66
|
-
|
|
73
|
+
const allowed = Number(allowedAmount);
|
|
74
|
+
const distributed = Number(distributedSum);
|
|
75
|
+
// Use epsilon comparison for floating point numbers
|
|
76
|
+
// Round to 2 decimal places to avoid precision issues
|
|
77
|
+
const roundedAllowed = Math.round(allowed * 100) / 100;
|
|
78
|
+
const roundedDistributed = Math.round(distributed * 100) / 100;
|
|
79
|
+
return Math.abs(roundedAllowed - roundedDistributed) < 0.01;
|
|
67
80
|
}, [allowedAmount, distributedSum]);
|
|
68
81
|
|
|
69
82
|
const difference = React.useMemo(() => {
|
|
70
|
-
|
|
83
|
+
const allowed = Number(allowedAmount);
|
|
84
|
+
const distributed = Number(distributedSum);
|
|
85
|
+
// Round to 2 decimal places to avoid precision issues
|
|
86
|
+
const roundedAllowed = Math.round(allowed * 100) / 100;
|
|
87
|
+
const roundedDistributed = Math.round(distributed * 100) / 100;
|
|
88
|
+
return Math.abs(roundedAllowed - roundedDistributed);
|
|
71
89
|
}, [allowedAmount, distributedSum]);
|
|
72
90
|
|
|
73
91
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
@@ -47,7 +47,6 @@ export function useUpdateEscrow({
|
|
|
47
47
|
| undefined,
|
|
48
48
|
trustline: {
|
|
49
49
|
address: selectedEscrow?.trustline?.address || "",
|
|
50
|
-
symbol: selectedEscrow?.trustline?.name || "",
|
|
51
50
|
},
|
|
52
51
|
roles: {
|
|
53
52
|
approver: selectedEscrow?.roles?.approver || "",
|
|
@@ -87,7 +86,6 @@ export function useUpdateEscrow({
|
|
|
87
86
|
| undefined) || "",
|
|
88
87
|
trustline: {
|
|
89
88
|
address: selectedEscrow?.trustline?.address || "",
|
|
90
|
-
symbol: selectedEscrow?.trustline?.name || "",
|
|
91
89
|
},
|
|
92
90
|
roles: {
|
|
93
91
|
approver: selectedEscrow?.roles?.approver || "",
|
|
@@ -207,9 +205,10 @@ export function useUpdateEscrow({
|
|
|
207
205
|
: payload.platformFee,
|
|
208
206
|
trustline: {
|
|
209
207
|
address: payload.trustline.address,
|
|
208
|
+
symbol: payload.trustline.symbol,
|
|
210
209
|
},
|
|
211
210
|
roles: payload.roles,
|
|
212
|
-
milestones: payload.milestones.map((milestone, index) => ({
|
|
211
|
+
milestones: payload.milestones.map((milestone, index: number) => ({
|
|
213
212
|
...milestone,
|
|
214
213
|
amount:
|
|
215
214
|
typeof milestone.amount === "string"
|
|
@@ -240,10 +239,7 @@ export function useUpdateEscrow({
|
|
|
240
239
|
...selectedEscrow,
|
|
241
240
|
...finalPayload.escrow,
|
|
242
241
|
trustline: {
|
|
243
|
-
|
|
244
|
-
selectedEscrow.trustline?.name ||
|
|
245
|
-
(selectedEscrow.trustline?.address as string) ||
|
|
246
|
-
"",
|
|
242
|
+
symbol: selectedEscrow?.trustline?.symbol || "",
|
|
247
243
|
address: finalPayload.escrow.trustline.address,
|
|
248
244
|
},
|
|
249
245
|
};
|
package/templates/escrows/multi-release/withdraw-remaining-funds/dialog/WithdrawRemainingFunds.tsx
CHANGED
|
@@ -145,7 +145,7 @@ export const WithdrawRemainingFundsDialog = () => {
|
|
|
145
145
|
<span className="font-bold">Total Balance: </span>
|
|
146
146
|
{formatCurrency(
|
|
147
147
|
selectedEscrow?.balance || 0,
|
|
148
|
-
selectedEscrow?.trustline.
|
|
148
|
+
selectedEscrow?.trustline.symbol || ""
|
|
149
149
|
)}
|
|
150
150
|
</p>
|
|
151
151
|
</div>
|
package/templates/escrows/multi-release/withdraw-remaining-funds/form/WithdrawRemainingFunds.tsx
CHANGED
|
@@ -125,7 +125,7 @@ export const WithdrawRemainingFundsForm = () => {
|
|
|
125
125
|
<span className="font-bold">Total Balance: </span>
|
|
126
126
|
{formatCurrency(
|
|
127
127
|
selectedEscrow?.balance || 0,
|
|
128
|
-
selectedEscrow?.trustline.
|
|
128
|
+
selectedEscrow?.trustline.symbol || ""
|
|
129
129
|
)}
|
|
130
130
|
</p>
|
|
131
131
|
</div>
|
|
@@ -466,6 +466,7 @@ export const InitializeEscrowDialog = () => {
|
|
|
466
466
|
onClick={() => handleRemoveMilestone(index)}
|
|
467
467
|
className="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 self-start sm:self-center cursor-pointer"
|
|
468
468
|
disabled={milestones.length === 1}
|
|
469
|
+
type="button"
|
|
469
470
|
>
|
|
470
471
|
<Trash2 className="h-5 w-5" />
|
|
471
472
|
</Button>
|
|
@@ -445,6 +445,7 @@ export const InitializeEscrowForm = () => {
|
|
|
445
445
|
onClick={() => handleRemoveMilestone(index)}
|
|
446
446
|
className="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 self-start sm:self-center cursor-pointer"
|
|
447
447
|
disabled={milestones.length === 1}
|
|
448
|
+
type="button"
|
|
448
449
|
>
|
|
449
450
|
<Trash2 className="h-5 w-5" />
|
|
450
451
|
</Button>
|
|
@@ -145,7 +145,7 @@ export const ResolveDisputeDialog = () => {
|
|
|
145
145
|
<span className="font-bold">Total Balance: </span>
|
|
146
146
|
{formatCurrency(
|
|
147
147
|
selectedEscrow?.balance || 0,
|
|
148
|
-
selectedEscrow?.trustline.
|
|
148
|
+
selectedEscrow?.trustline.symbol || ""
|
|
149
149
|
)}
|
|
150
150
|
</p>
|
|
151
151
|
</div>
|
|
@@ -50,7 +50,6 @@ export function useUpdateEscrow({
|
|
|
50
50
|
amount: selectedEscrow?.amount as unknown as number | string | undefined,
|
|
51
51
|
trustline: {
|
|
52
52
|
address: selectedEscrow?.trustline?.address || "",
|
|
53
|
-
symbol: selectedEscrow?.trustline?.name || "",
|
|
54
53
|
},
|
|
55
54
|
roles: {
|
|
56
55
|
approver: selectedEscrow?.roles?.approver || "",
|
|
@@ -87,7 +86,6 @@ export function useUpdateEscrow({
|
|
|
87
86
|
"",
|
|
88
87
|
trustline: {
|
|
89
88
|
address: selectedEscrow?.trustline?.address || "",
|
|
90
|
-
symbol: selectedEscrow?.trustline?.name || "",
|
|
91
89
|
},
|
|
92
90
|
roles: {
|
|
93
91
|
approver: selectedEscrow?.roles?.approver || "",
|
|
@@ -181,6 +179,7 @@ export function useUpdateEscrow({
|
|
|
181
179
|
: payload.amount,
|
|
182
180
|
trustline: {
|
|
183
181
|
address: payload.trustline.address,
|
|
182
|
+
symbol: selectedEscrow?.trustline?.symbol || "",
|
|
184
183
|
},
|
|
185
184
|
roles: payload.roles,
|
|
186
185
|
milestones: payload.milestones.map((milestone, index) => ({
|
|
@@ -210,10 +209,7 @@ export function useUpdateEscrow({
|
|
|
210
209
|
...selectedEscrow,
|
|
211
210
|
...finalPayload.escrow,
|
|
212
211
|
trustline: {
|
|
213
|
-
|
|
214
|
-
selectedEscrow.trustline?.name ||
|
|
215
|
-
(selectedEscrow.trustline?.address as string) ||
|
|
216
|
-
"",
|
|
212
|
+
symbol: selectedEscrow.trustline?.address,
|
|
217
213
|
address: finalPayload.escrow.trustline.address,
|
|
218
214
|
},
|
|
219
215
|
};
|
|
@@ -8,23 +8,23 @@
|
|
|
8
8
|
export const trustlines = [
|
|
9
9
|
// TESTNET
|
|
10
10
|
{
|
|
11
|
-
|
|
11
|
+
symbol: "USDC",
|
|
12
12
|
address: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
|
|
13
13
|
network: "testnet",
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
|
|
16
|
+
symbol: "EURC",
|
|
17
17
|
address: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
|
|
18
18
|
network: "testnet",
|
|
19
19
|
},
|
|
20
20
|
// MAINNET
|
|
21
21
|
{
|
|
22
|
-
|
|
22
|
+
symbol: "USDC",
|
|
23
23
|
address: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
|
|
24
24
|
network: "mainnet",
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
|
-
|
|
27
|
+
symbol: "EURC",
|
|
28
28
|
address: "GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2",
|
|
29
29
|
network: "mainnet",
|
|
30
30
|
},
|
|
@@ -37,7 +37,7 @@ export const trustlineOptions = Array.from(
|
|
|
37
37
|
.filter((trustline) => trustline.network === "testnet")
|
|
38
38
|
.map((trustline) => [
|
|
39
39
|
trustline.address,
|
|
40
|
-
{ value: trustline.address, label: trustline.
|
|
40
|
+
{ value: trustline.address, label: trustline.symbol },
|
|
41
41
|
])
|
|
42
42
|
).values()
|
|
43
43
|
);
|