@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.
Files changed (36) hide show
  1. package/bin/index.js +78 -1
  2. package/package.json +1 -1
  3. package/templates/deps.json +1 -1
  4. package/templates/escrows/details/Actions.tsx +21 -1
  5. package/templates/escrows/indicators/balance-progress/bar/BalanceProgress.tsx +55 -0
  6. package/templates/escrows/indicators/balance-progress/donut/BalanceProgress.tsx +99 -0
  7. package/templates/escrows/multi-release/initialize-escrow/dialog/InitializeEscrow.tsx +1 -0
  8. package/templates/escrows/multi-release/initialize-escrow/form/InitializeEscrow.tsx +1 -0
  9. package/templates/escrows/multi-release/initialize-escrow/shared/schema.ts +0 -1
  10. package/templates/escrows/multi-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -2
  11. package/templates/escrows/multi-release/resolve-dispute/button/ResolveDispute.tsx +10 -20
  12. package/templates/escrows/multi-release/resolve-dispute/dialog/ResolveDispute.tsx +117 -60
  13. package/templates/escrows/multi-release/resolve-dispute/form/ResolveDispute.tsx +111 -55
  14. package/templates/escrows/multi-release/resolve-dispute/shared/schema.ts +68 -71
  15. package/templates/escrows/multi-release/resolve-dispute/shared/useResolveDispute.ts +107 -21
  16. package/templates/escrows/multi-release/update-escrow/shared/schema.ts +0 -1
  17. package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +0 -4
  18. package/templates/escrows/multi-release/withdraw-remaining-funds/button/WithdrawRemainingFunds.tsx +85 -0
  19. package/templates/escrows/multi-release/withdraw-remaining-funds/dialog/WithdrawRemainingFunds.tsx +176 -0
  20. package/templates/escrows/multi-release/withdraw-remaining-funds/form/WithdrawRemainingFunds.tsx +153 -0
  21. package/templates/escrows/multi-release/withdraw-remaining-funds/shared/schema.ts +81 -0
  22. package/templates/escrows/multi-release/withdraw-remaining-funds/shared/useWithdrawRemainingFunds.ts +160 -0
  23. package/templates/escrows/single-multi-release/approve-milestone/button/ApproveMilestone.tsx +0 -1
  24. package/templates/escrows/single-multi-release/approve-milestone/shared/useApproveMilestone.ts +0 -1
  25. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +0 -1
  26. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +0 -2
  27. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +15 -31
  28. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +116 -60
  29. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +98 -43
  30. package/templates/escrows/single-release/resolve-dispute/shared/schema.ts +65 -68
  31. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +100 -22
  32. package/templates/escrows/single-release/update-escrow/shared/schema.ts +0 -1
  33. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +0 -4
  34. package/templates/tanstack/useEscrowsMutations.ts +53 -0
  35. package/templates/tanstack/useGetMultipleEscrowBalances.ts +41 -0
  36. package/templates/wallet-kit/trustlines.ts +0 -4
@@ -12,6 +12,8 @@ import {
12
12
  } from "@/components/tw-blocks/handle-errors/handle";
13
13
  import { useWalletContext } from "@/components/tw-blocks/wallet-kit/WalletProvider";
14
14
 
15
+ type DistributionInput = { address: string; amount: string | number };
16
+
15
17
  export function useResolveDispute() {
16
18
  const { resolveDispute } = useEscrowsMutations();
17
19
  const { selectedEscrow, updateEscrow } = useEscrowContext();
@@ -20,38 +22,98 @@ export function useResolveDispute() {
20
22
  const form = useForm<ResolveDisputeValues>({
21
23
  resolver: zodResolver(resolveDisputeSchema),
22
24
  defaultValues: {
23
- approverFunds: 0,
24
- receiverFunds: 0,
25
+ distributions: [
26
+ { address: "", amount: "" },
27
+ { address: "", amount: "" },
28
+ ],
25
29
  },
26
30
  mode: "onChange",
27
31
  });
28
32
 
33
+ const distributions = form.watch("distributions") as DistributionInput[];
34
+
29
35
  const [isSubmitting, setIsSubmitting] = React.useState(false);
30
36
 
37
+ const allowedAmount = React.useMemo(() => {
38
+ return Number(selectedEscrow?.amount || 0);
39
+ }, [selectedEscrow]);
40
+
41
+ const distributedSum = React.useMemo(() => {
42
+ return (distributions || []).reduce((acc, d) => {
43
+ const n = Number(d?.amount ?? 0);
44
+ return acc + (isNaN(n) ? 0 : n);
45
+ }, 0);
46
+ }, [distributions]);
47
+
48
+ const isExactMatch = React.useMemo(() => {
49
+ return Number(allowedAmount) === Number(distributedSum);
50
+ }, [allowedAmount, distributedSum]);
51
+
52
+ const difference = React.useMemo(() => {
53
+ return Math.abs(Number(allowedAmount) - Number(distributedSum));
54
+ }, [allowedAmount, distributedSum]);
55
+
56
+ const handleDistributionAddressChange = (index: number, value: string) => {
57
+ const updated = [...distributions];
58
+ updated[index] = { ...updated[index], address: value };
59
+ form.setValue("distributions", updated);
60
+ };
61
+
62
+ const handleDistributionAmountChange = (
63
+ index: number,
64
+ e: React.ChangeEvent<HTMLInputElement>
65
+ ) => {
66
+ let rawValue = e.target.value;
67
+ rawValue = rawValue.replace(/[^0-9.]/g, "");
68
+ if (rawValue.split(".").length > 2) {
69
+ rawValue = rawValue.slice(0, -1);
70
+ }
71
+ if (rawValue.includes(".")) {
72
+ const parts = rawValue.split(".");
73
+ if (parts[1] && parts[1].length > 2) {
74
+ rawValue = parts[0] + "." + parts[1].slice(0, 2);
75
+ }
76
+ }
77
+ const updated = [...distributions];
78
+ updated[index] = { ...updated[index], amount: rawValue };
79
+ form.setValue("distributions", updated);
80
+ };
81
+
82
+ const handleAddDistribution = () => {
83
+ const updated = [...distributions, { address: "", amount: "" }];
84
+ form.setValue("distributions", updated);
85
+ };
86
+
87
+ const handleRemoveDistribution = (index: number) => {
88
+ if (distributions.length <= 2) return;
89
+ const updated = distributions.filter((_, i) => i !== index);
90
+ form.setValue("distributions", updated);
91
+ };
92
+
93
+ const isAnyDistributionEmpty = React.useMemo(() => {
94
+ if (!distributions.length) return true;
95
+ const last = distributions[distributions.length - 1];
96
+ return (last.address || "").trim() === "" || (last.amount ?? "") === "";
97
+ }, [distributions]);
98
+
31
99
  const handleSubmit = form.handleSubmit(async (payload) => {
32
100
  try {
33
101
  setIsSubmitting(true);
34
102
 
35
- /**
36
- * Create the final payload for the resolve dispute mutation
37
- *
38
- * @param payload - The payload from the form
39
- * @returns The final payload for the resolve dispute mutation
40
- */
103
+ if (!isExactMatch) {
104
+ toast.error("The total distributions must equal the escrow amount");
105
+ return;
106
+ }
107
+
41
108
  const finalPayload: SingleReleaseResolveDisputePayload = {
42
109
  contractId: selectedEscrow?.contractId || "",
43
110
  disputeResolver: walletAddress || "",
44
- approverFunds: Number(payload.approverFunds),
45
- receiverFunds: Number(payload.receiverFunds),
111
+ distributions: payload.distributions.map((d) => ({
112
+ address: d.address,
113
+ amount: Number(d.amount || 0),
114
+ })) as [{ address: string; amount: number }],
46
115
  };
47
116
 
48
- /**
49
- * Call the resolve dispute mutation
50
- *
51
- * @param payload - The final payload for the resolve dispute mutation
52
- * @param type - The type of the escrow
53
- * @param address - The address of the escrow
54
- */
55
117
  await resolveDispute.mutateAsync({
56
118
  payload: finalPayload,
57
119
  type: "single-release",
@@ -60,6 +122,11 @@ export function useResolveDispute() {
60
122
 
61
123
  toast.success("Dispute resolved successfully");
62
124
 
125
+ const sumDistributed = payload.distributions.reduce((acc, d) => {
126
+ const n = Number(d.amount || 0);
127
+ return acc + (isNaN(n) ? 0 : n);
128
+ }, 0);
129
+
63
130
  updateEscrow({
64
131
  ...selectedEscrow,
65
132
  flags: {
@@ -67,10 +134,7 @@ export function useResolveDispute() {
67
134
  disputed: false,
68
135
  resolved: true,
69
136
  },
70
- balance:
71
- (selectedEscrow?.balance || 0) -
72
- (Number(payload.approverFunds) + Number(payload.receiverFunds)) ||
73
- 0,
137
+ balance: (selectedEscrow?.balance || 0) - sumDistributed || 0,
74
138
  });
75
139
  } catch (error) {
76
140
  toast.error(handleError(error as ErrorResponse).message);
@@ -80,5 +144,19 @@ export function useResolveDispute() {
80
144
  }
81
145
  });
82
146
 
83
- return { form, handleSubmit, isSubmitting };
147
+ return {
148
+ form,
149
+ handleSubmit,
150
+ isSubmitting,
151
+ distributions,
152
+ handleAddDistribution,
153
+ handleRemoveDistribution,
154
+ handleDistributionAddressChange,
155
+ handleDistributionAmountChange,
156
+ isAnyDistributionEmpty,
157
+ allowedAmount,
158
+ distributedSum,
159
+ isExactMatch,
160
+ difference,
161
+ };
84
162
  }
@@ -8,7 +8,6 @@ export const useUpdateEscrowSchema = () => {
8
8
  address: z.string().min(1, {
9
9
  message: "Trustline address is required.",
10
10
  }),
11
- decimals: z.number().default(10000000),
12
11
  }),
13
12
  roles: z.object({
14
13
  approver: z
@@ -44,7 +44,6 @@ export function useUpdateEscrow() {
44
44
  : "",
45
45
  trustline: {
46
46
  address: selectedEscrow?.trustline?.address || "",
47
- decimals: 10000000,
48
47
  },
49
48
  roles: {
50
49
  approver: selectedEscrow?.roles?.approver || "",
@@ -82,7 +81,6 @@ export function useUpdateEscrow() {
82
81
  : "",
83
82
  trustline: {
84
83
  address: selectedEscrow?.trustline?.address || "",
85
- decimals: 10000000,
86
84
  },
87
85
  roles: {
88
86
  approver: selectedEscrow?.roles?.approver || "",
@@ -171,7 +169,6 @@ export function useUpdateEscrow() {
171
169
  : undefined,
172
170
  trustline: {
173
171
  address: payload.trustline.address,
174
- decimals: 10000000,
175
172
  },
176
173
  roles: payload.roles,
177
174
  milestones: payload.milestones,
@@ -202,7 +199,6 @@ export function useUpdateEscrow() {
202
199
  (selectedEscrow.trustline?.address as string) ||
203
200
  "",
204
201
  address: finalPayload.escrow.trustline.address,
205
- decimals: finalPayload.escrow.trustline.decimals,
206
202
  },
207
203
  };
208
204
 
@@ -23,6 +23,8 @@ import {
23
23
  SingleReleaseReleaseFundsPayload,
24
24
  MultiReleaseResolveDisputePayload,
25
25
  SingleReleaseResolveDisputePayload,
26
+ WithdrawRemainingFundsPayload,
27
+ useWithdrawRemainingFunds,
26
28
  } from "@trustless-work/escrow";
27
29
  import { signTransaction } from "../wallet-kit/wallet-kit";
28
30
 
@@ -49,6 +51,7 @@ export const useEscrowsMutations = () => {
49
51
  const { startDispute } = useStartDispute();
50
52
  const { releaseFunds } = useReleaseFunds();
51
53
  const { resolveDispute } = useResolveDispute();
54
+ const { withdrawRemainingFunds } = useWithdrawRemainingFunds();
52
55
 
53
56
  /**
54
57
  * Deploy Escrow
@@ -434,6 +437,55 @@ export const useEscrowsMutations = () => {
434
437
  },
435
438
  });
436
439
 
440
+ /**
441
+ * Withdraw Remaining Funds
442
+ */
443
+ const withdrawRemainingFundsMutation = useMutation({
444
+ mutationFn: async ({
445
+ payload,
446
+ type,
447
+ address,
448
+ }: {
449
+ payload: WithdrawRemainingFundsPayload;
450
+ type: EscrowType;
451
+ address: string;
452
+ }) => {
453
+ const { unsignedTransaction } = await withdrawRemainingFunds(
454
+ payload,
455
+ type
456
+ );
457
+
458
+ if (!unsignedTransaction) {
459
+ throw new Error(
460
+ "Unsigned transaction is missing from withdrawRemainingFunds response."
461
+ );
462
+ }
463
+
464
+ const signedTxXdr = await signTransaction({
465
+ unsignedTransaction,
466
+ address,
467
+ });
468
+
469
+ if (!signedTxXdr) {
470
+ throw new Error("Signed transaction is missing.");
471
+ }
472
+
473
+ const response = await sendTransaction(signedTxXdr);
474
+
475
+ if (response.status !== "SUCCESS") {
476
+ throw new Error("Transaction failed to send");
477
+ }
478
+
479
+ return response;
480
+ },
481
+ onSuccess: () => {
482
+ queryClient.invalidateQueries({ queryKey: ["escrows"] });
483
+ },
484
+ onError: (error) => {
485
+ console.error(error);
486
+ },
487
+ });
488
+
437
489
  return {
438
490
  deployEscrow: deployEscrowMutation,
439
491
  updateEscrow: updateEscrowMutation,
@@ -443,5 +495,6 @@ export const useEscrowsMutations = () => {
443
495
  startDispute: startDisputeMutation,
444
496
  releaseFunds: releaseFundsMutation,
445
497
  resolveDispute: resolveDisputeMutation,
498
+ withdrawRemainingFunds: withdrawRemainingFundsMutation,
446
499
  };
447
500
  };
@@ -0,0 +1,41 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import {
3
+ GetEscrowBalancesResponse,
4
+ GetBalanceParams,
5
+ } from "@trustless-work/escrow/types";
6
+ import { useGetMultipleEscrowBalances } from "@trustless-work/escrow/hooks";
7
+
8
+ /**
9
+ * Use the query to get the escrows balances
10
+ *
11
+ * @param params - The parameters for the query
12
+ * @returns The query result
13
+ */
14
+ export const useGetMultipleEscrowBalancesQuery = ({
15
+ addresses,
16
+ enabled = true,
17
+ }: GetBalanceParams & { enabled?: boolean }) => {
18
+ const { getMultipleBalances } = useGetMultipleEscrowBalances();
19
+
20
+ // Get the escrows by signer
21
+ return useQuery({
22
+ queryKey: ["escrows", addresses],
23
+ queryFn: async (): Promise<GetEscrowBalancesResponse[]> => {
24
+ /**
25
+ * Call the query to get the escrows from the Trustless Work Indexer
26
+ *
27
+ * @param params - The parameters for the query
28
+ * @returns The query result
29
+ */
30
+ const balances = await getMultipleBalances({ addresses });
31
+
32
+ if (!balances) {
33
+ throw new Error("Escrows not found");
34
+ }
35
+
36
+ return balances;
37
+ },
38
+ enabled: enabled,
39
+ staleTime: 1000 * 60 * 5, // 5 min
40
+ });
41
+ };
@@ -10,26 +10,22 @@ export const trustlines = [
10
10
  {
11
11
  name: "USDC",
12
12
  address: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
13
- decimals: 10000000,
14
13
  network: "testnet",
15
14
  },
16
15
  {
17
16
  name: "EURC",
18
17
  address: "GB3Q6QDZYTHWT7E5PVS3W7FUT5GVAFC5KSZFFLPU25GO7VTC3NM2ZTVO",
19
- decimals: 10000000,
20
18
  network: "testnet",
21
19
  },
22
20
  // MAINNET
23
21
  {
24
22
  name: "USDC",
25
23
  address: "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75",
26
- decimals: 10000000,
27
24
  network: "mainnet",
28
25
  },
29
26
  {
30
27
  name: "EURC",
31
28
  address: "GB3Q6QDZYTHWT7E5PVS3W7FUT5GVAFC5KSZFFLPU25GO7VTC3NM2ZTVO",
32
- decimals: 10000000,
33
29
  network: "mainnet",
34
30
  },
35
31
  ];