@trustless-work/blocks 1.1.7 → 1.2.0
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/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/initialize-escrow/shared/useInitializeEscrow.ts +16 -1
- 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/single-release/initialize-escrow/shared/useInitializeEscrow.ts +16 -1
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
|
@@ -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
|
+
|
|
@@ -17,7 +17,9 @@ import {
|
|
|
17
17
|
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
18
18
|
import { trustlineOptions } from "@/components/tw-blocks/wallet-kit/trustlines";
|
|
19
19
|
|
|
20
|
-
export function useInitializeEscrow({
|
|
20
|
+
export function useInitializeEscrow({
|
|
21
|
+
onSuccess,
|
|
22
|
+
}: { onSuccess?: () => void } = {}) {
|
|
21
23
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
22
24
|
|
|
23
25
|
const { getMultiReleaseFormSchema } = useInitializeEscrowSchema();
|
|
@@ -122,6 +124,15 @@ export function useInitializeEscrow({ onSuccess }: { onSuccess?: () => void } =
|
|
|
122
124
|
try {
|
|
123
125
|
setIsSubmitting(true);
|
|
124
126
|
|
|
127
|
+
const trustline = trustlineOptions.find(
|
|
128
|
+
(t) => t.value === payload.trustline.address
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (!trustline) {
|
|
132
|
+
toast.error("Invalid trustline address");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
/**
|
|
126
137
|
* Create the final payload for the initialize escrow mutation
|
|
127
138
|
*
|
|
@@ -142,6 +153,10 @@ export function useInitializeEscrow({ onSuccess }: { onSuccess?: () => void } =
|
|
|
142
153
|
? Number(milestone.amount)
|
|
143
154
|
: milestone.amount,
|
|
144
155
|
})),
|
|
156
|
+
trustline: {
|
|
157
|
+
address: trustline.value,
|
|
158
|
+
symbol: trustline.label,
|
|
159
|
+
},
|
|
145
160
|
};
|
|
146
161
|
|
|
147
162
|
/**
|
|
@@ -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
|
+
|
|
@@ -17,7 +17,9 @@ import {
|
|
|
17
17
|
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
18
18
|
import { trustlineOptions } from "@/components/tw-blocks/wallet-kit/trustlines";
|
|
19
19
|
|
|
20
|
-
export function useInitializeEscrow({
|
|
20
|
+
export function useInitializeEscrow({
|
|
21
|
+
onSuccess,
|
|
22
|
+
}: { onSuccess?: () => void } = {}) {
|
|
21
23
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
22
24
|
|
|
23
25
|
const { getSingleReleaseFormSchema } = useInitializeEscrowSchema();
|
|
@@ -108,6 +110,15 @@ export function useInitializeEscrow({ onSuccess }: { onSuccess?: () => void } =
|
|
|
108
110
|
try {
|
|
109
111
|
setIsSubmitting(true);
|
|
110
112
|
|
|
113
|
+
const trustline = trustlineOptions.find(
|
|
114
|
+
(t) => t.value === payload.trustline.address
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!trustline) {
|
|
118
|
+
toast.error("Invalid trustline address");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
/**
|
|
112
123
|
* Create the final payload for the initialize escrow mutation
|
|
113
124
|
*
|
|
@@ -126,6 +137,10 @@ export function useInitializeEscrow({ onSuccess }: { onSuccess?: () => void } =
|
|
|
126
137
|
: payload.platformFee,
|
|
127
138
|
signer: walletAddress || "",
|
|
128
139
|
milestones: payload.milestones,
|
|
140
|
+
trustline: {
|
|
141
|
+
address: trustline.value,
|
|
142
|
+
symbol: trustline.label,
|
|
143
|
+
},
|
|
129
144
|
};
|
|
130
145
|
|
|
131
146
|
/**
|