Skip to content

Commit ca5a459

Browse files
authored
chore(tnt-core-v0.13.0): regenerate ABIs + slashing UX polish (#3198)
1 parent f9801eb commit ca5a459

15 files changed

Lines changed: 1105 additions & 19 deletions

File tree

apps/tangle-cloud/src/pages/instances/Instances/UpdateBlueprintModel/ServiceRequestDetailModal.tsx

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { type ServiceRequest } from '@tangle-network/tangle-shared-ui/data/graph
1616
import {
1717
useServiceRequestDetails,
1818
useTokenMetadata,
19+
useExpireServiceRequestTx,
20+
isServiceRequestExpired,
1921
} from '@tangle-network/tangle-shared-ui/data/services';
2022
import type { Blueprint } from '@tangle-network/tangle-shared-ui/types/blueprint';
2123
import { TxStatus } from '@tangle-network/tangle-shared-ui/hooks/useContractWrite';
@@ -75,6 +77,43 @@ const ServiceRequestDetailModal: FC<Props> = ({
7577
enabled: selectedRequest !== null,
7678
});
7779

80+
// Permissionless cleanup. Available once `now > createdAt + grace` and the
81+
// request has not already been activated or rejected. The contract
82+
// re-validates these conditions, but we gate the button to avoid wasting a
83+
// user's gas on a guaranteed revert.
84+
const {
85+
execute: expireServiceRequest,
86+
error: expireError,
87+
reset: resetExpire,
88+
isPending: isExpiring,
89+
isSuccess: isExpireSuccess,
90+
} = useExpireServiceRequestTx();
91+
92+
const canExpireRequest = useMemo(() => {
93+
if (!contractDetails || !selectedRequest) {
94+
return false;
95+
}
96+
if (contractDetails.rejected) {
97+
return false;
98+
}
99+
return isServiceRequestExpired(contractDetails.createdAt);
100+
}, [contractDetails, selectedRequest]);
101+
102+
const handleExpireRequest = useCallback(async () => {
103+
if (!selectedRequest || !canExpireRequest) {
104+
return;
105+
}
106+
await expireServiceRequest?.({ requestId: selectedRequest.requestId });
107+
}, [canExpireRequest, expireServiceRequest, selectedRequest]);
108+
109+
// After a successful expire the request is gone — close the modal so the
110+
// parent list refetches against an invalidated `serviceRequestDetails` cache.
111+
useEffect(() => {
112+
if (isExpireSuccess) {
113+
onClose();
114+
}
115+
}, [isExpireSuccess, onClose]);
116+
78117
const { data: tokenMetadata, isLoading: isLoadingToken } = useTokenMetadata(
79118
contractDetails?.paymentToken,
80119
{
@@ -271,27 +310,74 @@ const ServiceRequestDetailModal: FC<Props> = ({
271310
/>
272311
</ModalBody>
273312

313+
{expireError ? (
314+
<div className="px-6 pt-2">
315+
<div className="rounded-lg border border-destructive/30 bg-destructive/10 p-3 space-y-2">
316+
<Text variant="body3" className="text-destructive">
317+
{expireError.message ||
318+
'Failed to expire the service request. Please try again.'}
319+
</Text>
320+
<button
321+
type="button"
322+
className="text-xs underline text-destructive"
323+
onClick={resetExpire}
324+
>
325+
Dismiss
326+
</button>
327+
</div>
328+
</div>
329+
) : null}
330+
274331
{!viewOnly && (
275-
<div className="flex justify-end gap-3 p-6 pt-4 shrink-0 bg-background">
332+
<div className="flex flex-wrap justify-end gap-3 p-6 pt-4 shrink-0 bg-background">
333+
{canExpireRequest ? (
334+
<Button
335+
variant="secondary"
336+
onClick={() => void handleExpireRequest()}
337+
isLoading={isExpiring}
338+
isDisabled={
339+
isExpiring || isApproving || isRejecting || !canExpireRequest
340+
}
341+
title="Refunds the requester and frees the operator candidates. Anyone can call this once the grace period has passed."
342+
>
343+
Expire request
344+
</Button>
345+
) : null}
346+
276347
<Button
277348
variant="secondary"
278349
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
279350
onClick={onReject}
280351
isLoading={isRejecting}
281-
isDisabled={isRejecting}
352+
isDisabled={isRejecting || isExpiring}
282353
>
283354
Reject
284355
</Button>
285356

286-
<Button variant="primary" onClick={handleApproveClick}>
357+
<Button
358+
variant="primary"
359+
onClick={handleApproveClick}
360+
isDisabled={isExpiring}
361+
>
287362
Approve
288363
</Button>
289364
</div>
290365
)}
291366

292367
{viewOnly && (
293-
<div className="flex justify-end gap-3 p-6 pt-4 shrink-0 bg-background">
294-
<Button variant="secondary" onClick={onClose}>
368+
<div className="flex flex-wrap justify-end gap-3 p-6 pt-4 shrink-0 bg-background">
369+
{canExpireRequest ? (
370+
<Button
371+
variant="secondary"
372+
onClick={() => void handleExpireRequest()}
373+
isLoading={isExpiring}
374+
isDisabled={isExpiring || !canExpireRequest}
375+
title="Refunds the requester and frees the operator candidates. Anyone can call this once the grace period has passed."
376+
>
377+
Expire request
378+
</Button>
379+
) : null}
380+
<Button variant="secondary" onClick={onClose} isDisabled={isExpiring}>
295381
Close
296382
</Button>
297383
</div>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* Read-only summary of the protocol-level slashing parameters returned by
3+
* `getSlashConfig`. Shipped with tnt-core v0.13.0 — exposes the fields that
4+
* govern dispute lifecycle (disputeBond, disputeResolutionDeadline) and the
5+
* caps that gate proposal flow (maxSlashBps, maxPendingSlashesPerOperator).
6+
*
7+
* Operators and slash proposers both benefit from seeing these up front so
8+
* they don't waste a simulation on a guaranteed revert.
9+
*/
10+
11+
import { Card } from '@tangle-network/sandbox-ui/primitives';
12+
import { formatUnits } from 'viem';
13+
import { Text } from '../../../../components/Text';
14+
15+
const SECONDS_PER_HOUR = 60 * 60;
16+
const SECONDS_PER_DAY = SECONDS_PER_HOUR * 24;
17+
18+
const formatDuration = (seconds: bigint): string => {
19+
const total = Number(seconds);
20+
if (!Number.isFinite(total) || total <= 0) {
21+
return '—';
22+
}
23+
24+
if (total >= SECONDS_PER_DAY) {
25+
const days = total / SECONDS_PER_DAY;
26+
const rounded = Math.round(days * 10) / 10;
27+
return `${rounded.toLocaleString(undefined, {
28+
maximumFractionDigits: 1,
29+
})} day${rounded === 1 ? '' : 's'}`;
30+
}
31+
32+
if (total >= SECONDS_PER_HOUR) {
33+
const hours = total / SECONDS_PER_HOUR;
34+
const rounded = Math.round(hours * 10) / 10;
35+
return `${rounded.toLocaleString(undefined, {
36+
maximumFractionDigits: 1,
37+
})} hour${rounded === 1 ? '' : 's'}`;
38+
}
39+
40+
return `${total.toLocaleString()} second${total === 1 ? '' : 's'}`;
41+
};
42+
43+
// Trim trailing zeros so we don't show "0.000000000000000000 ETH".
44+
const formatEthAmount = (wei: bigint): string => {
45+
const formatted = formatUnits(wei, 18);
46+
if (!formatted.includes('.')) return formatted;
47+
return formatted.replace(/\.?0+$/, '');
48+
};
49+
50+
const formatBps = (bps: number): string => {
51+
const percent = bps / 100;
52+
return `${bps.toLocaleString()} bps (${percent.toLocaleString(undefined, {
53+
minimumFractionDigits: 0,
54+
maximumFractionDigits: 2,
55+
})}%)`;
56+
};
57+
58+
export interface SlashingParametersCardProps {
59+
/**
60+
* Active SlashConfig from `getSlashConfig`. Undefined while the read is in
61+
* flight; rendered as a skeleton in that case.
62+
*/
63+
config:
64+
| {
65+
disputeWindow: bigint;
66+
instantSlashEnabled: boolean;
67+
maxSlashBps: number;
68+
disputeResolutionDeadline: bigint;
69+
disputeBond: bigint;
70+
maxPendingSlashesPerOperator: number;
71+
}
72+
| undefined;
73+
isLoading: boolean;
74+
}
75+
76+
const SlashingParametersCard = ({
77+
config,
78+
isLoading,
79+
}: SlashingParametersCardProps) => {
80+
if (isLoading || !config) {
81+
return (
82+
<Card className="p-4">
83+
<Text variant="body3" className="text-muted-foreground">
84+
Loading slashing parameters...
85+
</Text>
86+
</Card>
87+
);
88+
}
89+
90+
return (
91+
<Card className="p-4 space-y-3">
92+
<div>
93+
<Text variant="body2" fw="bold">
94+
Slashing parameters
95+
</Text>
96+
<Text variant="body3" className="text-muted-foreground">
97+
Protocol-wide settings from the active SlashConfig. Proposals,
98+
disputes, and execution all enforce these.
99+
</Text>
100+
</div>
101+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-2">
102+
<div>
103+
<Text variant="body3" className="text-muted-foreground">
104+
Maximum slash per proposal
105+
</Text>
106+
<Text variant="body2" fw="semibold">
107+
{formatBps(config.maxSlashBps)}
108+
</Text>
109+
</div>
110+
<div>
111+
<Text variant="body3" className="text-muted-foreground">
112+
Dispute window
113+
</Text>
114+
<Text variant="body2" fw="semibold">
115+
{formatDuration(config.disputeWindow)}
116+
</Text>
117+
</div>
118+
<div>
119+
<Text variant="body3" className="text-muted-foreground">
120+
Dispute resolution deadline
121+
</Text>
122+
<Text variant="body2" fw="semibold">
123+
{formatDuration(config.disputeResolutionDeadline)}
124+
</Text>
125+
</div>
126+
<div>
127+
<Text variant="body3" className="text-muted-foreground">
128+
Required dispute bond
129+
</Text>
130+
<Text variant="body2" fw="semibold">
131+
{config.disputeBond > BigInt(0)
132+
? `${formatEthAmount(config.disputeBond)} ETH`
133+
: 'None'}
134+
</Text>
135+
</div>
136+
<div>
137+
<Text variant="body3" className="text-muted-foreground">
138+
Max pending slashes per operator
139+
</Text>
140+
<Text variant="body2" fw="semibold">
141+
{config.maxPendingSlashesPerOperator > 0
142+
? config.maxPendingSlashesPerOperator.toLocaleString()
143+
: 'Unlimited'}
144+
</Text>
145+
</div>
146+
<div>
147+
<Text variant="body3" className="text-muted-foreground">
148+
Instant slash
149+
</Text>
150+
<Text variant="body2" fw="semibold">
151+
{config.instantSlashEnabled ? 'Enabled' : 'Disabled'}
152+
</Text>
153+
</div>
154+
</div>
155+
</Card>
156+
);
157+
};
158+
159+
export default SlashingParametersCard;

apps/tangle-cloud/src/pages/operators/manage/components/modals/DisputeSlashModal.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ChangeEvent } from 'react';
2+
import { formatUnits, zeroAddress } from 'viem';
23
import {
34
SlashActionPermissions,
45
SlashDisputeEligibility,
@@ -24,6 +25,14 @@ import {
2425
const shortenHex = (value: string) =>
2526
value.length <= 12 ? value : `${value.slice(0, 6)}...${value.slice(-4)}`;
2627

28+
// Trim trailing zeros so we don't show "0.000000000000000000 ETH" or
29+
// "1.500000000000000000 ETH" in the bond row.
30+
const formatEthAmount = (wei: bigint): string => {
31+
const formatted = formatUnits(wei, 18);
32+
if (!formatted.includes('.')) return formatted;
33+
return formatted.replace(/\.?0+$/, '');
34+
};
35+
2736
interface DisputeSlashModalProps {
2837
open: boolean;
2938
onOpenChange: (open: boolean) => void;
@@ -39,6 +48,17 @@ interface DisputeSlashModalProps {
3948
onConfirm: () => void;
4049
errorMessage: string | null;
4150
onDismissError: () => void;
51+
/**
52+
* msg.value the contract will require for disputeSlash. Read from the active
53+
* SlashConfig and surfaced here so the user knows what bond they are posting
54+
* before signing.
55+
*/
56+
disputeBond: bigint;
57+
/**
58+
* Seconds remaining until the dispute resolution deadline. Only meaningful
59+
* when the slash is already in `Disputed` status; null otherwise.
60+
*/
61+
disputeResolutionSecondsRemaining: number | null;
4262
}
4363

4464
const DisputeSlashModal = ({
@@ -56,7 +76,14 @@ const DisputeSlashModal = ({
5676
onConfirm,
5777
errorMessage,
5878
onDismissError,
79+
disputeBond,
80+
disputeResolutionSecondsRemaining,
5981
}: DisputeSlashModalProps) => {
82+
const isAlreadyDisputed = selectedSlash?.status === 'Disputed';
83+
const hasKnownDisputer =
84+
!!selectedSlash &&
85+
selectedSlash.disputer.toLowerCase() !== zeroAddress.toLowerCase();
86+
6087
return (
6188
<Modal open={open} onOpenChange={onOpenChange}>
6289
<ModalContent>
@@ -130,6 +157,41 @@ const DisputeSlashModal = ({
130157
<Text variant="body3" className="font-mono break-all">
131158
{selectedSlash?.evidence ?? '-'}
132159
</Text>
160+
<Text variant="body3" className="text-muted-foreground">
161+
Required Dispute Bond:
162+
</Text>
163+
<Text variant="body3">
164+
{disputeBond > BigInt(0)
165+
? `${formatEthAmount(disputeBond)} ETH (refunded if dispute upheld)`
166+
: 'No bond required'}
167+
</Text>
168+
{isAlreadyDisputed && hasKnownDisputer ? (
169+
<>
170+
<Text variant="body3" className="text-muted-foreground">
171+
Disputer:
172+
</Text>
173+
<Text
174+
variant="body3"
175+
className="font-mono"
176+
title={selectedSlash?.disputer ?? undefined}
177+
>
178+
{selectedSlash ? shortenHex(selectedSlash.disputer) : '-'}
179+
</Text>
180+
</>
181+
) : null}
182+
{isAlreadyDisputed &&
183+
disputeResolutionSecondsRemaining !== null ? (
184+
<>
185+
<Text variant="body3" className="text-muted-foreground">
186+
Resolution Deadline:
187+
</Text>
188+
<Text variant="body3">
189+
{disputeResolutionSecondsRemaining > 0
190+
? formatTimeRemaining(disputeResolutionSecondsRemaining)
191+
: 'Deadline passed'}
192+
</Text>
193+
</>
194+
) : null}
133195
</div>
134196
</div>
135197
<div>

0 commit comments

Comments
 (0)