@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
package/templates/escrows/multi-release/withdraw-remaining-funds/dialog/WithdrawRemainingFunds.tsx
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Form,
|
|
4
|
+
FormField,
|
|
5
|
+
FormItem,
|
|
6
|
+
FormLabel,
|
|
7
|
+
FormControl,
|
|
8
|
+
FormMessage,
|
|
9
|
+
} from "__UI_BASE__/form";
|
|
10
|
+
import { Input } from "__UI_BASE__/input";
|
|
11
|
+
import { Button } from "__UI_BASE__/button";
|
|
12
|
+
import {
|
|
13
|
+
Dialog,
|
|
14
|
+
DialogContent,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
DialogTrigger,
|
|
18
|
+
} from "__UI_BASE__/dialog";
|
|
19
|
+
import { Loader2, Trash2 } from "lucide-react";
|
|
20
|
+
import { useWithdrawRemainingFunds } from "./useWithdrawRemainingFunds";
|
|
21
|
+
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
22
|
+
import { formatCurrency } from "../../../../helpers/format.helper";
|
|
23
|
+
|
|
24
|
+
export const WithdrawRemainingFundsDialog = () => {
|
|
25
|
+
const {
|
|
26
|
+
form,
|
|
27
|
+
handleSubmit,
|
|
28
|
+
isSubmitting,
|
|
29
|
+
distributions,
|
|
30
|
+
handleAddDistribution,
|
|
31
|
+
handleRemoveDistribution,
|
|
32
|
+
handleDistributionAddressChange,
|
|
33
|
+
handleDistributionAmountChange,
|
|
34
|
+
isAnyDistributionEmpty,
|
|
35
|
+
allowedAmount,
|
|
36
|
+
distributedSum,
|
|
37
|
+
isExactMatch,
|
|
38
|
+
difference,
|
|
39
|
+
} = useWithdrawRemainingFunds();
|
|
40
|
+
const { selectedEscrow } = useEscrowContext();
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Dialog>
|
|
44
|
+
<DialogTrigger asChild>
|
|
45
|
+
<Button type="button" className="cursor-pointer w-full">
|
|
46
|
+
Withdraw Remaining
|
|
47
|
+
</Button>
|
|
48
|
+
</DialogTrigger>
|
|
49
|
+
<DialogContent className="!w-full sm:!max-w-3xl max-h-[95vh] overflow-y-auto">
|
|
50
|
+
<DialogHeader>
|
|
51
|
+
<DialogTitle>Withdraw Remaining Funds</DialogTitle>
|
|
52
|
+
</DialogHeader>
|
|
53
|
+
<Form {...form}>
|
|
54
|
+
<form onSubmit={handleSubmit}>
|
|
55
|
+
<FormLabel className="flex items-center my-4">
|
|
56
|
+
Distributions<span className="text-destructive ml-1">*</span>
|
|
57
|
+
</FormLabel>
|
|
58
|
+
|
|
59
|
+
{distributions.map((d, idx) => (
|
|
60
|
+
<div
|
|
61
|
+
key={`dist-${idx}`}
|
|
62
|
+
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-[1fr_minmax(140px,220px)_auto] gap-3 sm:gap-4 items-end mb-2"
|
|
63
|
+
>
|
|
64
|
+
<FormField
|
|
65
|
+
control={form.control}
|
|
66
|
+
name={`distributions.${idx}.address` as const}
|
|
67
|
+
render={() => (
|
|
68
|
+
<FormItem className="sm:col-span-2 lg:col-span-1">
|
|
69
|
+
<FormLabel>Address</FormLabel>
|
|
70
|
+
<FormControl>
|
|
71
|
+
<Input
|
|
72
|
+
type="text"
|
|
73
|
+
placeholder="Receiver address"
|
|
74
|
+
value={d.address}
|
|
75
|
+
onChange={(e) =>
|
|
76
|
+
handleDistributionAddressChange(idx, e.target.value)
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
</FormControl>
|
|
80
|
+
<FormMessage />
|
|
81
|
+
</FormItem>
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
<FormField
|
|
86
|
+
control={form.control}
|
|
87
|
+
name={`distributions.${idx}.amount` as const}
|
|
88
|
+
render={() => (
|
|
89
|
+
<FormItem>
|
|
90
|
+
<FormLabel>Amount</FormLabel>
|
|
91
|
+
<FormControl>
|
|
92
|
+
<Input
|
|
93
|
+
type="text"
|
|
94
|
+
inputMode="decimal"
|
|
95
|
+
placeholder="0.00"
|
|
96
|
+
value={(d.amount as string) ?? ""}
|
|
97
|
+
onChange={(e) =>
|
|
98
|
+
handleDistributionAmountChange(idx, e)
|
|
99
|
+
}
|
|
100
|
+
/>
|
|
101
|
+
</FormControl>
|
|
102
|
+
<FormMessage />
|
|
103
|
+
</FormItem>
|
|
104
|
+
)}
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<Button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => handleRemoveDistribution(idx)}
|
|
110
|
+
className="justify-self-end self-end p-2 bg-transparent text-destructive rounded-md border-none shadow-none hover:bg-transparent hover:shadow-none hover:text-destructive focus:ring-0 active:ring-0"
|
|
111
|
+
disabled={distributions.length <= 2}
|
|
112
|
+
>
|
|
113
|
+
<Trash2 className="h-5 w-5" />
|
|
114
|
+
</Button>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
|
|
118
|
+
<div className="flex justify-between items-center mt-4">
|
|
119
|
+
<Button
|
|
120
|
+
type="button"
|
|
121
|
+
variant="outline"
|
|
122
|
+
onClick={handleAddDistribution}
|
|
123
|
+
disabled={isAnyDistributionEmpty}
|
|
124
|
+
className="cursor-pointer"
|
|
125
|
+
>
|
|
126
|
+
Add Item
|
|
127
|
+
</Button>
|
|
128
|
+
|
|
129
|
+
<div className="flex items-center gap-4">
|
|
130
|
+
<div className="text-xs text-muted-foreground">
|
|
131
|
+
<p>
|
|
132
|
+
<span className="font-bold">Total Amount: </span>
|
|
133
|
+
{distributedSum.toFixed(2)} / {allowedAmount.toFixed(2)}
|
|
134
|
+
</p>
|
|
135
|
+
{!isExactMatch && (
|
|
136
|
+
<p className="text-destructive">
|
|
137
|
+
<span className="font-bold">Difference: </span>
|
|
138
|
+
{difference.toFixed(2)}
|
|
139
|
+
</p>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<p className="text-xs text-muted-foreground">
|
|
144
|
+
<span className="font-bold">Total Balance: </span>
|
|
145
|
+
{formatCurrency(
|
|
146
|
+
selectedEscrow?.balance || 0,
|
|
147
|
+
selectedEscrow?.trustline.name || ""
|
|
148
|
+
)}
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div className="mt-4 flex justify-start items-center">
|
|
154
|
+
<Button
|
|
155
|
+
type="submit"
|
|
156
|
+
disabled={
|
|
157
|
+
isSubmitting || isAnyDistributionEmpty || !isExactMatch
|
|
158
|
+
}
|
|
159
|
+
className="cursor-pointer"
|
|
160
|
+
>
|
|
161
|
+
{isSubmitting ? (
|
|
162
|
+
<div className="flex items-center">
|
|
163
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
164
|
+
<span className="ml-2">Withdrawing...</span>
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
"Withdraw"
|
|
168
|
+
)}
|
|
169
|
+
</Button>
|
|
170
|
+
</div>
|
|
171
|
+
</form>
|
|
172
|
+
</Form>
|
|
173
|
+
</DialogContent>
|
|
174
|
+
</Dialog>
|
|
175
|
+
);
|
|
176
|
+
};
|
package/templates/escrows/multi-release/withdraw-remaining-funds/form/WithdrawRemainingFunds.tsx
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Form,
|
|
4
|
+
FormField,
|
|
5
|
+
FormItem,
|
|
6
|
+
FormLabel,
|
|
7
|
+
FormControl,
|
|
8
|
+
FormMessage,
|
|
9
|
+
} from "__UI_BASE__/form";
|
|
10
|
+
import { Input } from "__UI_BASE__/input";
|
|
11
|
+
import { Button } from "__UI_BASE__/button";
|
|
12
|
+
import { useWithdrawRemainingFunds } from "./useWithdrawRemainingFunds";
|
|
13
|
+
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
14
|
+
import { Loader2, Trash2 } from "lucide-react";
|
|
15
|
+
import { formatCurrency } from "../../../../helpers/format.helper";
|
|
16
|
+
|
|
17
|
+
export const WithdrawRemainingFundsForm = () => {
|
|
18
|
+
const {
|
|
19
|
+
form,
|
|
20
|
+
handleSubmit,
|
|
21
|
+
isSubmitting,
|
|
22
|
+
distributions,
|
|
23
|
+
handleAddDistribution,
|
|
24
|
+
handleRemoveDistribution,
|
|
25
|
+
handleDistributionAddressChange,
|
|
26
|
+
handleDistributionAmountChange,
|
|
27
|
+
isAnyDistributionEmpty,
|
|
28
|
+
allowedAmount,
|
|
29
|
+
distributedSum,
|
|
30
|
+
isExactMatch,
|
|
31
|
+
difference,
|
|
32
|
+
} = useWithdrawRemainingFunds();
|
|
33
|
+
const { selectedEscrow } = useEscrowContext();
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Form {...form}>
|
|
37
|
+
<form onSubmit={handleSubmit} className="flex flex-col space-y-6 w-full">
|
|
38
|
+
<FormLabel className="flex items-center my-4">
|
|
39
|
+
Distributions<span className="text-destructive ml-1">*</span>
|
|
40
|
+
</FormLabel>
|
|
41
|
+
|
|
42
|
+
{distributions.map((d, idx) => (
|
|
43
|
+
<div
|
|
44
|
+
key={`dist-${idx}`}
|
|
45
|
+
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"
|
|
46
|
+
>
|
|
47
|
+
<FormField
|
|
48
|
+
control={form.control}
|
|
49
|
+
name={`distributions.${idx}.address` as const}
|
|
50
|
+
render={() => (
|
|
51
|
+
<FormItem className="sm:col-span-2 lg:col-span-1">
|
|
52
|
+
<FormLabel>Address</FormLabel>
|
|
53
|
+
<FormControl>
|
|
54
|
+
<Input
|
|
55
|
+
type="text"
|
|
56
|
+
placeholder="Receiver address"
|
|
57
|
+
value={d.address}
|
|
58
|
+
onChange={(e) =>
|
|
59
|
+
handleDistributionAddressChange(idx, e.target.value)
|
|
60
|
+
}
|
|
61
|
+
/>
|
|
62
|
+
</FormControl>
|
|
63
|
+
<FormMessage />
|
|
64
|
+
</FormItem>
|
|
65
|
+
)}
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
<FormField
|
|
69
|
+
control={form.control}
|
|
70
|
+
name={`distributions.${idx}.amount` as const}
|
|
71
|
+
render={() => (
|
|
72
|
+
<FormItem>
|
|
73
|
+
<FormLabel>Amount</FormLabel>
|
|
74
|
+
<FormControl>
|
|
75
|
+
<Input
|
|
76
|
+
type="text"
|
|
77
|
+
inputMode="decimal"
|
|
78
|
+
placeholder="0.00"
|
|
79
|
+
value={(d.amount as string) ?? ""}
|
|
80
|
+
onChange={(e) => handleDistributionAmountChange(idx, e)}
|
|
81
|
+
/>
|
|
82
|
+
</FormControl>
|
|
83
|
+
<FormMessage />
|
|
84
|
+
</FormItem>
|
|
85
|
+
)}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<Button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={() => handleRemoveDistribution(idx)}
|
|
91
|
+
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"
|
|
92
|
+
disabled={distributions.length <= 2}
|
|
93
|
+
>
|
|
94
|
+
<Trash2 className="h-5 w-5" />
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
|
|
99
|
+
<div className="flex justify-between items-center">
|
|
100
|
+
<Button
|
|
101
|
+
type="button"
|
|
102
|
+
variant="outline"
|
|
103
|
+
onClick={handleAddDistribution}
|
|
104
|
+
disabled={isAnyDistributionEmpty}
|
|
105
|
+
className="cursor-pointer"
|
|
106
|
+
>
|
|
107
|
+
Add Item
|
|
108
|
+
</Button>
|
|
109
|
+
|
|
110
|
+
<div className="flex items-center gap-4">
|
|
111
|
+
<div className="text-xs text-muted-foreground">
|
|
112
|
+
<p>
|
|
113
|
+
<span className="font-bold">Total Amount: </span>
|
|
114
|
+
{distributedSum.toFixed(2)} / {allowedAmount.toFixed(2)}
|
|
115
|
+
</p>
|
|
116
|
+
{!isExactMatch && (
|
|
117
|
+
<p className="text-destructive">
|
|
118
|
+
<span className="font-bold">Difference: </span>
|
|
119
|
+
{difference.toFixed(2)}
|
|
120
|
+
</p>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<p className="text-xs text-muted-foreground">
|
|
125
|
+
<span className="font-bold">Total Balance: </span>
|
|
126
|
+
{formatCurrency(
|
|
127
|
+
selectedEscrow?.balance || 0,
|
|
128
|
+
selectedEscrow?.trustline.name || ""
|
|
129
|
+
)}
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div className="mt-4">
|
|
135
|
+
<Button
|
|
136
|
+
type="submit"
|
|
137
|
+
disabled={isSubmitting || isAnyDistributionEmpty || !isExactMatch}
|
|
138
|
+
className="cursor-pointer"
|
|
139
|
+
>
|
|
140
|
+
{isSubmitting ? (
|
|
141
|
+
<div className="flex items-center">
|
|
142
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
143
|
+
<span className="ml-2">Withdrawing...</span>
|
|
144
|
+
</div>
|
|
145
|
+
) : (
|
|
146
|
+
"Withdraw"
|
|
147
|
+
)}
|
|
148
|
+
</Button>
|
|
149
|
+
</div>
|
|
150
|
+
</form>
|
|
151
|
+
</Form>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isValidWallet } from "../../../../wallet-kit/validators";
|
|
3
|
+
|
|
4
|
+
export const getFormSchema = () => {
|
|
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
|
|
12
|
+
}
|
|
13
|
+
const numVal = Number(val);
|
|
14
|
+
return !isNaN(numVal) && numVal >= 0;
|
|
15
|
+
}
|
|
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
|
|
25
|
+
}
|
|
26
|
+
const numVal = Number(val);
|
|
27
|
+
if (isNaN(numVal)) return false;
|
|
28
|
+
const decimalPlaces = (numVal.toString().split(".")[1] || "").length;
|
|
29
|
+
return decimalPlaces <= 2;
|
|
30
|
+
}
|
|
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);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const withdrawRemainingFundsSchema = getFormSchema();
|
|
78
|
+
|
|
79
|
+
export type WithdrawRemainingFundsValues = z.infer<
|
|
80
|
+
typeof withdrawRemainingFundsSchema
|
|
81
|
+
>;
|
package/templates/escrows/multi-release/withdraw-remaining-funds/shared/useWithdrawRemainingFunds.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useForm } from "react-hook-form";
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import {
|
|
5
|
+
withdrawRemainingFundsSchema,
|
|
6
|
+
type WithdrawRemainingFundsValues,
|
|
7
|
+
} from "./schema";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
import { WithdrawRemainingFundsPayload } from "@trustless-work/escrow";
|
|
10
|
+
import { useEscrowContext } from "@/components/tw-blocks/providers/EscrowProvider";
|
|
11
|
+
import { useEscrowsMutations } from "@/components/tw-blocks/tanstack/useEscrowsMutations";
|
|
12
|
+
import {
|
|
13
|
+
ErrorResponse,
|
|
14
|
+
handleError,
|
|
15
|
+
} from "@/components/tw-blocks/handle-errors/handle";
|
|
16
|
+
import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
|
|
17
|
+
|
|
18
|
+
type DistributionInput = { address: string; amount: string | number };
|
|
19
|
+
|
|
20
|
+
export function useWithdrawRemainingFunds() {
|
|
21
|
+
const { withdrawRemainingFunds } = useEscrowsMutations();
|
|
22
|
+
const { selectedEscrow, updateEscrow } = useEscrowContext();
|
|
23
|
+
const { walletAddress } = useWalletContext();
|
|
24
|
+
|
|
25
|
+
const form = useForm<WithdrawRemainingFundsValues>({
|
|
26
|
+
resolver: zodResolver(withdrawRemainingFundsSchema),
|
|
27
|
+
defaultValues: {
|
|
28
|
+
distributions: [
|
|
29
|
+
{ address: "", amount: "" },
|
|
30
|
+
{ address: "", amount: "" },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
mode: "onChange",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const distributions = form.watch("distributions") as DistributionInput[];
|
|
37
|
+
|
|
38
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
39
|
+
|
|
40
|
+
const allowedAmount = React.useMemo(() => {
|
|
41
|
+
return Number(selectedEscrow?.balance || 0);
|
|
42
|
+
}, [selectedEscrow]);
|
|
43
|
+
|
|
44
|
+
const distributedSum = React.useMemo(() => {
|
|
45
|
+
return (distributions || []).reduce((acc, d) => {
|
|
46
|
+
const n = Number(d?.amount ?? 0);
|
|
47
|
+
return acc + (isNaN(n) ? 0 : n);
|
|
48
|
+
}, 0);
|
|
49
|
+
}, [distributions]);
|
|
50
|
+
|
|
51
|
+
const isExactMatch = React.useMemo(() => {
|
|
52
|
+
return Number(allowedAmount) === Number(distributedSum);
|
|
53
|
+
}, [allowedAmount, distributedSum]);
|
|
54
|
+
|
|
55
|
+
const difference = React.useMemo(() => {
|
|
56
|
+
return Math.abs(Number(allowedAmount) - Number(distributedSum));
|
|
57
|
+
}, [allowedAmount, distributedSum]);
|
|
58
|
+
|
|
59
|
+
const handleDistributionAddressChange = (index: number, value: string) => {
|
|
60
|
+
const updated = [...distributions];
|
|
61
|
+
updated[index] = { ...updated[index], address: value };
|
|
62
|
+
form.setValue("distributions", updated);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleDistributionAmountChange = (
|
|
66
|
+
index: number,
|
|
67
|
+
e: React.ChangeEvent<HTMLInputElement>
|
|
68
|
+
) => {
|
|
69
|
+
let rawValue = e.target.value;
|
|
70
|
+
rawValue = rawValue.replace(/[^0-9.]/g, "");
|
|
71
|
+
if (rawValue.split(".").length > 2) {
|
|
72
|
+
rawValue = rawValue.slice(0, -1);
|
|
73
|
+
}
|
|
74
|
+
if (rawValue.includes(".")) {
|
|
75
|
+
const parts = rawValue.split(".");
|
|
76
|
+
if (parts[1] && parts[1].length > 2) {
|
|
77
|
+
rawValue = parts[0] + "." + parts[1].slice(0, 2);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const updated = [...distributions];
|
|
81
|
+
updated[index] = { ...updated[index], amount: rawValue };
|
|
82
|
+
form.setValue("distributions", updated);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleAddDistribution = () => {
|
|
86
|
+
const updated = [...distributions, { address: "", amount: "" }];
|
|
87
|
+
form.setValue("distributions", updated);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleRemoveDistribution = (index: number) => {
|
|
91
|
+
if (distributions.length <= 2) return;
|
|
92
|
+
const updated = distributions.filter((_, i) => i !== index);
|
|
93
|
+
form.setValue("distributions", updated);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const isAnyDistributionEmpty = React.useMemo(() => {
|
|
97
|
+
if (!distributions.length) return true;
|
|
98
|
+
const last = distributions[distributions.length - 1];
|
|
99
|
+
return (last.address || "").trim() === "" || (last.amount ?? "") === "";
|
|
100
|
+
}, [distributions]);
|
|
101
|
+
|
|
102
|
+
const handleSubmit = form.handleSubmit(async (payload) => {
|
|
103
|
+
try {
|
|
104
|
+
setIsSubmitting(true);
|
|
105
|
+
|
|
106
|
+
if (!isExactMatch) {
|
|
107
|
+
toast.error("The total distributions must equal the remaining amount");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const finalPayload: WithdrawRemainingFundsPayload = {
|
|
112
|
+
contractId: selectedEscrow?.contractId || "",
|
|
113
|
+
disputeResolver: walletAddress || "",
|
|
114
|
+
distributions: payload.distributions.map((d) => ({
|
|
115
|
+
address: d.address,
|
|
116
|
+
amount: Number(d.amount || 0),
|
|
117
|
+
})) as [{ address: string; amount: number }],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
await withdrawRemainingFunds.mutateAsync({
|
|
121
|
+
payload: finalPayload,
|
|
122
|
+
type: "multi-release",
|
|
123
|
+
address: walletAddress || "",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
toast.success("Withdraw successful");
|
|
127
|
+
|
|
128
|
+
const sumDistributed = payload.distributions.reduce((acc, d) => {
|
|
129
|
+
const n = Number(d.amount || 0);
|
|
130
|
+
return acc + (isNaN(n) ? 0 : n);
|
|
131
|
+
}, 0);
|
|
132
|
+
|
|
133
|
+
updateEscrow({
|
|
134
|
+
...selectedEscrow,
|
|
135
|
+
balance: (selectedEscrow?.balance || 0) - sumDistributed || 0,
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
toast.error(handleError(error as ErrorResponse).message);
|
|
139
|
+
} finally {
|
|
140
|
+
setIsSubmitting(false);
|
|
141
|
+
form.reset();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
form,
|
|
147
|
+
handleSubmit,
|
|
148
|
+
isSubmitting,
|
|
149
|
+
distributions,
|
|
150
|
+
handleAddDistribution,
|
|
151
|
+
handleRemoveDistribution,
|
|
152
|
+
handleDistributionAddressChange,
|
|
153
|
+
handleDistributionAmountChange,
|
|
154
|
+
isAnyDistributionEmpty,
|
|
155
|
+
allowedAmount,
|
|
156
|
+
distributedSum,
|
|
157
|
+
isExactMatch,
|
|
158
|
+
difference,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -38,7 +38,6 @@ export function useInitializeEscrow() {
|
|
|
38
38
|
receiverMemo: "",
|
|
39
39
|
trustline: {
|
|
40
40
|
address: "",
|
|
41
|
-
decimals: 10000000,
|
|
42
41
|
},
|
|
43
42
|
roles: {
|
|
44
43
|
approver: "",
|
|
@@ -82,7 +81,6 @@ export function useInitializeEscrow() {
|
|
|
82
81
|
receiverMemo: "123",
|
|
83
82
|
trustline: {
|
|
84
83
|
address: usdc?.value || "",
|
|
85
|
-
decimals: 10000000,
|
|
86
84
|
},
|
|
87
85
|
roles: {
|
|
88
86
|
approver: walletAddress || "",
|