@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,14 +11,14 @@ import {
|
|
|
11
11
|
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
12
12
|
import { Loader2 } from "lucide-react";
|
|
13
13
|
|
|
14
|
+
type Distribution = { address: string; amount: number };
|
|
15
|
+
|
|
14
16
|
type ResolveDisputeButtonProps = {
|
|
15
|
-
|
|
16
|
-
receiverFunds: number;
|
|
17
|
+
distributions: Distribution[];
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export const ResolveDisputeButton = ({
|
|
20
|
-
|
|
21
|
-
receiverFunds,
|
|
21
|
+
distributions,
|
|
22
22
|
}: ResolveDisputeButtonProps) => {
|
|
23
23
|
const { resolveDispute } = useEscrowsMutations();
|
|
24
24
|
const { selectedEscrow, updateEscrow } = useEscrowContext();
|
|
@@ -27,42 +27,22 @@ export const ResolveDisputeButton = ({
|
|
|
27
27
|
|
|
28
28
|
async function handleClick() {
|
|
29
29
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
) {
|
|
36
|
-
toast.error("Both amounts are required");
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (approverFunds < 0 || receiverFunds < 0) {
|
|
41
|
-
toast.error("Amounts must be >= 0");
|
|
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");
|
|
42
35
|
return;
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
setIsSubmitting(true);
|
|
46
39
|
|
|
47
|
-
/**
|
|
48
|
-
* Create the payload for the resolve dispute mutation
|
|
49
|
-
*
|
|
50
|
-
* @returns The payload for the resolve dispute mutation
|
|
51
|
-
*/
|
|
52
40
|
const payload: SingleReleaseResolveDisputePayload = {
|
|
53
41
|
contractId: selectedEscrow?.contractId || "",
|
|
54
42
|
disputeResolver: walletAddress || "",
|
|
55
|
-
|
|
56
|
-
receiverFunds: Number(receiverFunds),
|
|
43
|
+
distributions: distributions as [Distribution],
|
|
57
44
|
};
|
|
58
45
|
|
|
59
|
-
/**
|
|
60
|
-
* Call the resolve dispute mutation
|
|
61
|
-
*
|
|
62
|
-
* @param payload - The payload for the resolve dispute mutation
|
|
63
|
-
* @param type - The type of the escrow
|
|
64
|
-
* @param address - The address of the escrow
|
|
65
|
-
*/
|
|
66
46
|
await resolveDispute.mutateAsync({
|
|
67
47
|
payload,
|
|
68
48
|
type: "single-release",
|
|
@@ -70,6 +50,10 @@ export const ResolveDisputeButton = ({
|
|
|
70
50
|
});
|
|
71
51
|
|
|
72
52
|
toast.success("Dispute resolved successfully");
|
|
53
|
+
const sumDistributed = distributions.reduce(
|
|
54
|
+
(acc, d) => acc + Number(d.amount || 0),
|
|
55
|
+
0
|
|
56
|
+
);
|
|
73
57
|
updateEscrow({
|
|
74
58
|
...selectedEscrow,
|
|
75
59
|
flags: {
|
|
@@ -77,7 +61,7 @@ export const ResolveDisputeButton = ({
|
|
|
77
61
|
disputed: false,
|
|
78
62
|
resolved: true,
|
|
79
63
|
},
|
|
80
|
-
balance: selectedEscrow?.balance || 0,
|
|
64
|
+
balance: (selectedEscrow?.balance || 0) - sumDistributed || 0,
|
|
81
65
|
});
|
|
82
66
|
} catch (error) {
|
|
83
67
|
toast.error(handleError(error as ErrorResponse).message);
|
|
@@ -16,13 +16,27 @@ import {
|
|
|
16
16
|
DialogTitle,
|
|
17
17
|
DialogTrigger,
|
|
18
18
|
} from "__UI_BASE__/dialog";
|
|
19
|
-
import { Loader2 } from "lucide-react";
|
|
19
|
+
import { Loader2, Trash2 } from "lucide-react";
|
|
20
20
|
import { useResolveDispute } from "./useResolveDispute";
|
|
21
21
|
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
22
22
|
import { formatCurrency } from "../../../../helpers/format.helper";
|
|
23
23
|
|
|
24
24
|
export const ResolveDisputeDialog = () => {
|
|
25
|
-
const {
|
|
25
|
+
const {
|
|
26
|
+
form,
|
|
27
|
+
handleSubmit,
|
|
28
|
+
isSubmitting,
|
|
29
|
+
distributions,
|
|
30
|
+
handleAddDistribution,
|
|
31
|
+
handleRemoveDistribution,
|
|
32
|
+
handleDistributionAddressChange,
|
|
33
|
+
handleDistributionAmountChange,
|
|
34
|
+
isAnyDistributionEmpty,
|
|
35
|
+
allowedAmount,
|
|
36
|
+
distributedSum,
|
|
37
|
+
isExactMatch,
|
|
38
|
+
difference,
|
|
39
|
+
} = useResolveDispute();
|
|
26
40
|
const { selectedEscrow } = useEscrowContext();
|
|
27
41
|
|
|
28
42
|
return (
|
|
@@ -32,58 +46,116 @@ export const ResolveDisputeDialog = () => {
|
|
|
32
46
|
Resolve Dispute
|
|
33
47
|
</Button>
|
|
34
48
|
</DialogTrigger>
|
|
35
|
-
<DialogContent>
|
|
49
|
+
<DialogContent className="!w-full sm:!max-w-3xl max-h-[95vh] overflow-y-auto">
|
|
36
50
|
<DialogHeader>
|
|
37
51
|
<DialogTitle>Resolve Dispute</DialogTitle>
|
|
38
52
|
</DialogHeader>
|
|
39
53
|
<Form {...form}>
|
|
40
54
|
<form onSubmit={handleSubmit}>
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
name="approverFunds"
|
|
45
|
-
render={({ field }) => (
|
|
46
|
-
<FormItem>
|
|
47
|
-
<FormLabel>Approver Funds</FormLabel>
|
|
48
|
-
<FormControl>
|
|
49
|
-
<Input
|
|
50
|
-
type="text"
|
|
51
|
-
inputMode="decimal"
|
|
52
|
-
placeholder="Enter approver funds"
|
|
53
|
-
value={field.value as unknown as string}
|
|
54
|
-
onChange={(e) => field.onChange(e.target.value)}
|
|
55
|
-
/>
|
|
56
|
-
</FormControl>
|
|
57
|
-
<FormMessage />
|
|
58
|
-
</FormItem>
|
|
59
|
-
)}
|
|
60
|
-
/>
|
|
55
|
+
<FormLabel className="flex items-center my-4">
|
|
56
|
+
Distributions<span className="text-destructive ml-1">*</span>
|
|
57
|
+
</FormLabel>
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
{distributions.map((d, idx) => (
|
|
60
|
+
<div
|
|
61
|
+
key={`dist-${idx}`}
|
|
62
|
+
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-[1fr_minmax(140px,220px)_auto] gap-3 sm:gap-4 items-end mb-2"
|
|
63
|
+
>
|
|
64
|
+
<FormField
|
|
65
|
+
control={form.control}
|
|
66
|
+
name={`distributions.${idx}.address` as const}
|
|
67
|
+
render={() => (
|
|
68
|
+
<FormItem className="sm:col-span-2 lg:col-span-1">
|
|
69
|
+
<FormLabel>Address</FormLabel>
|
|
70
|
+
<FormControl>
|
|
71
|
+
<Input
|
|
72
|
+
type="text"
|
|
73
|
+
placeholder="Receiver address"
|
|
74
|
+
value={d.address}
|
|
75
|
+
onChange={(e) =>
|
|
76
|
+
handleDistributionAddressChange(idx, e.target.value)
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
</FormControl>
|
|
80
|
+
<FormMessage />
|
|
81
|
+
</FormItem>
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
<FormField
|
|
86
|
+
control={form.control}
|
|
87
|
+
name={`distributions.${idx}.amount` as const}
|
|
88
|
+
render={() => (
|
|
89
|
+
<FormItem>
|
|
90
|
+
<FormLabel>Amount</FormLabel>
|
|
91
|
+
<FormControl>
|
|
92
|
+
<Input
|
|
93
|
+
type="text"
|
|
94
|
+
inputMode="decimal"
|
|
95
|
+
placeholder="0.00"
|
|
96
|
+
value={(d.amount as string) ?? ""}
|
|
97
|
+
onChange={(e) =>
|
|
98
|
+
handleDistributionAmountChange(idx, e)
|
|
99
|
+
}
|
|
100
|
+
/>
|
|
101
|
+
</FormControl>
|
|
102
|
+
<FormMessage />
|
|
103
|
+
</FormItem>
|
|
104
|
+
)}
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<Button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => handleRemoveDistribution(idx)}
|
|
110
|
+
className="justify-self-end self-end p-2 bg-transparent text-destructive rounded-md border-none shadow-none hover:bg-transparent hover:shadow-none hover:text-destructive focus:ring-0 active:ring-0"
|
|
111
|
+
disabled={distributions.length <= 2}
|
|
112
|
+
>
|
|
113
|
+
<Trash2 className="h-5 w-5" />
|
|
114
|
+
</Button>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
|
|
118
|
+
<div className="flex justify-between items-center mt-4">
|
|
119
|
+
<Button
|
|
120
|
+
type="button"
|
|
121
|
+
variant="outline"
|
|
122
|
+
onClick={handleAddDistribution}
|
|
123
|
+
disabled={isAnyDistributionEmpty}
|
|
124
|
+
className="cursor-pointer"
|
|
125
|
+
>
|
|
126
|
+
Add Item
|
|
127
|
+
</Button>
|
|
128
|
+
|
|
129
|
+
<div className="flex items-center gap-4">
|
|
130
|
+
<div className="text-xs text-muted-foreground">
|
|
131
|
+
<p>
|
|
132
|
+
<span className="font-bold">Total Amount: </span>
|
|
133
|
+
{distributedSum.toFixed(2)} / {allowedAmount.toFixed(2)}
|
|
134
|
+
</p>
|
|
135
|
+
{!isExactMatch && (
|
|
136
|
+
<p className="text-destructive">
|
|
137
|
+
<span className="font-bold">Difference: </span>
|
|
138
|
+
{difference.toFixed(2)}
|
|
139
|
+
</p>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<p className="text-xs text-muted-foreground">
|
|
144
|
+
<span className="font-bold">Total Balance: </span>
|
|
145
|
+
{formatCurrency(
|
|
146
|
+
selectedEscrow?.balance || 0,
|
|
147
|
+
selectedEscrow?.trustline.name || ""
|
|
148
|
+
)}
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
81
151
|
</div>
|
|
82
152
|
|
|
83
|
-
<div className="mt-4 flex justify-
|
|
153
|
+
<div className="mt-4 flex justify-start items-center">
|
|
84
154
|
<Button
|
|
85
155
|
type="submit"
|
|
86
|
-
disabled={
|
|
156
|
+
disabled={
|
|
157
|
+
isSubmitting || isAnyDistributionEmpty || !isExactMatch
|
|
158
|
+
}
|
|
87
159
|
className="cursor-pointer"
|
|
88
160
|
>
|
|
89
161
|
{isSubmitting ? (
|
|
@@ -95,22 +167,6 @@ export const ResolveDisputeDialog = () => {
|
|
|
95
167
|
"Resolve"
|
|
96
168
|
)}
|
|
97
169
|
</Button>
|
|
98
|
-
|
|
99
|
-
<p className="text-xs text-muted-foreground">
|
|
100
|
-
<span className="font-bold">Total Amount: </span>
|
|
101
|
-
{formatCurrency(
|
|
102
|
-
selectedEscrow?.amount || 0,
|
|
103
|
-
selectedEscrow?.trustline.name || ""
|
|
104
|
-
)}
|
|
105
|
-
</p>
|
|
106
|
-
|
|
107
|
-
<p className="text-xs text-muted-foreground">
|
|
108
|
-
<span className="font-bold">Total Balance: </span>
|
|
109
|
-
{formatCurrency(
|
|
110
|
-
selectedEscrow?.balance || 0,
|
|
111
|
-
selectedEscrow?.trustline.name || ""
|
|
112
|
-
)}
|
|
113
|
-
</p>
|
|
114
170
|
</div>
|
|
115
171
|
</form>
|
|
116
172
|
</Form>
|
|
@@ -10,60 +10,115 @@ import {
|
|
|
10
10
|
import { Input } from "__UI_BASE__/input";
|
|
11
11
|
import { Button } from "__UI_BASE__/button";
|
|
12
12
|
import { useResolveDispute } from "./useResolveDispute";
|
|
13
|
-
import { Loader2 } from "lucide-react";
|
|
13
|
+
import { Loader2, Trash2 } from "lucide-react";
|
|
14
14
|
|
|
15
15
|
export const ResolveDisputeForm = () => {
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
form,
|
|
18
|
+
handleSubmit,
|
|
19
|
+
isSubmitting,
|
|
20
|
+
distributions,
|
|
21
|
+
handleAddDistribution,
|
|
22
|
+
handleRemoveDistribution,
|
|
23
|
+
handleDistributionAddressChange,
|
|
24
|
+
handleDistributionAmountChange,
|
|
25
|
+
isAnyDistributionEmpty,
|
|
26
|
+
allowedAmount,
|
|
27
|
+
distributedSum,
|
|
28
|
+
isExactMatch,
|
|
29
|
+
difference,
|
|
30
|
+
} = useResolveDispute();
|
|
17
31
|
|
|
18
32
|
return (
|
|
19
33
|
<Form {...form}>
|
|
20
34
|
<form onSubmit={handleSubmit} className="flex flex-col space-y-6 w-full">
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
name="approverFunds"
|
|
25
|
-
render={({ field }) => (
|
|
26
|
-
<FormItem>
|
|
27
|
-
<FormLabel>Approver Funds</FormLabel>
|
|
28
|
-
<FormControl>
|
|
29
|
-
<Input
|
|
30
|
-
type="text"
|
|
31
|
-
inputMode="decimal"
|
|
32
|
-
placeholder="Enter approver funds"
|
|
33
|
-
value={field.value as unknown as string}
|
|
34
|
-
onChange={(e) => field.onChange(e.target.value)}
|
|
35
|
-
/>
|
|
36
|
-
</FormControl>
|
|
37
|
-
<FormMessage />
|
|
38
|
-
</FormItem>
|
|
39
|
-
)}
|
|
40
|
-
/>
|
|
35
|
+
<FormLabel className="flex items-center my-4">
|
|
36
|
+
Distributions<span className="text-destructive ml-1">*</span>
|
|
37
|
+
</FormLabel>
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
39
|
+
{distributions.map((d, idx) => (
|
|
40
|
+
<div
|
|
41
|
+
key={`dist-${idx}`}
|
|
42
|
+
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"
|
|
43
|
+
>
|
|
44
|
+
<FormField
|
|
45
|
+
control={form.control}
|
|
46
|
+
name={`distributions.${idx}.address` as const}
|
|
47
|
+
render={() => (
|
|
48
|
+
<FormItem className="sm:col-span-2 lg:col-span-1">
|
|
49
|
+
<FormLabel>Address</FormLabel>
|
|
50
|
+
<FormControl>
|
|
51
|
+
<Input
|
|
52
|
+
type="text"
|
|
53
|
+
placeholder="Receiver address"
|
|
54
|
+
value={d.address}
|
|
55
|
+
onChange={(e) =>
|
|
56
|
+
handleDistributionAddressChange(idx, e.target.value)
|
|
57
|
+
}
|
|
58
|
+
/>
|
|
59
|
+
</FormControl>
|
|
60
|
+
<FormMessage />
|
|
61
|
+
</FormItem>
|
|
62
|
+
)}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
<FormField
|
|
66
|
+
control={form.control}
|
|
67
|
+
name={`distributions.${idx}.amount` as const}
|
|
68
|
+
render={() => (
|
|
69
|
+
<FormItem>
|
|
70
|
+
<FormLabel>Amount</FormLabel>
|
|
71
|
+
<FormControl>
|
|
72
|
+
<Input
|
|
73
|
+
type="text"
|
|
74
|
+
inputMode="decimal"
|
|
75
|
+
placeholder="0.00"
|
|
76
|
+
value={(d.amount as string) ?? ""}
|
|
77
|
+
onChange={(e) => handleDistributionAmountChange(idx, e)}
|
|
78
|
+
/>
|
|
79
|
+
</FormControl>
|
|
80
|
+
<FormMessage />
|
|
81
|
+
</FormItem>
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
<Button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={() => handleRemoveDistribution(idx)}
|
|
88
|
+
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"
|
|
89
|
+
disabled={distributions.length <= 2}
|
|
90
|
+
>
|
|
91
|
+
<Trash2 className="h-5 w-5" />
|
|
92
|
+
</Button>
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
|
|
96
|
+
<div className="flex justify-end">
|
|
97
|
+
<Button
|
|
98
|
+
type="button"
|
|
99
|
+
variant="outline"
|
|
100
|
+
onClick={handleAddDistribution}
|
|
101
|
+
disabled={isAnyDistributionEmpty}
|
|
102
|
+
className="cursor-pointer"
|
|
103
|
+
>
|
|
104
|
+
Add Item
|
|
105
|
+
</Button>
|
|
61
106
|
</div>
|
|
62
107
|
|
|
63
|
-
<div className="mt-4">
|
|
108
|
+
<div className="mt-4 space-y-2">
|
|
109
|
+
<p className="text-xs text-muted-foreground">
|
|
110
|
+
<span className="font-bold">Total Amount: </span>
|
|
111
|
+
{distributedSum.toFixed(2)} / {allowedAmount.toFixed(2)}
|
|
112
|
+
</p>
|
|
113
|
+
{!isExactMatch && (
|
|
114
|
+
<p className="text-xs text-destructive">
|
|
115
|
+
<span className="font-bold">Difference: </span>
|
|
116
|
+
{difference.toFixed(2)}
|
|
117
|
+
</p>
|
|
118
|
+
)}
|
|
64
119
|
<Button
|
|
65
120
|
type="submit"
|
|
66
|
-
disabled={isSubmitting}
|
|
121
|
+
disabled={isSubmitting || isAnyDistributionEmpty || !isExactMatch}
|
|
67
122
|
className="cursor-pointer"
|
|
68
123
|
>
|
|
69
124
|
{isSubmitting ? (
|
|
@@ -1,80 +1,77 @@
|
|
|
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
|
+
})
|
|
53
|
+
.superRefine((data, ctx) => {
|
|
54
|
+
const seen = new Map<string, number>();
|
|
55
|
+
data.distributions.forEach((item, idx) => {
|
|
56
|
+
const key = (item.address || "").trim().toUpperCase();
|
|
57
|
+
if (!key) return;
|
|
58
|
+
if (seen.has(key)) {
|
|
59
|
+
const firstIdx = seen.get(key)!;
|
|
60
|
+
ctx.addIssue({
|
|
61
|
+
code: z.ZodIssueCode.custom,
|
|
62
|
+
path: ["distributions", idx, "address"],
|
|
63
|
+
message: "Duplicate address. Each recipient must be unique.",
|
|
64
|
+
});
|
|
65
|
+
ctx.addIssue({
|
|
66
|
+
code: z.ZodIssueCode.custom,
|
|
67
|
+
path: ["distributions", firstIdx, "address"],
|
|
68
|
+
message: "Duplicate address. Each recipient must be unique.",
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
seen.set(key, idx);
|
|
75
72
|
}
|
|
76
|
-
)
|
|
77
|
-
|
|
73
|
+
});
|
|
74
|
+
});
|
|
78
75
|
};
|
|
79
76
|
|
|
80
77
|
export const resolveDisputeSchema = getFormSchema();
|