Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b7d1d04
feat: add a smart contract wallet warning to layout
kyrers Aug 24, 2025
6a0aa99
feat: customize alert to match styling but also include link to docum…
kyrers Aug 25, 2025
ec914de
refactor: enhance local storage handling and improve smart contract w…
kyrers Aug 28, 2025
5e7f466
feat: add KlerosLiquid ABI and generate wagmi hooks
kyrers Sep 2, 2025
105b9a0
feat: fetch arbitrator address, extra data, and arbitration cost
kyrers Sep 2, 2025
b2a03f2
feat: add RaiseDispute modal
kyrers Sep 2, 2025
75f44d6
feat: allow one party to pay the arbitration cost and start the proce…
kyrers Sep 3, 2025
3f9df5b
refactor: minor update to transaction action components
kyrers Sep 4, 2025
0814c85
feat: update transaction status to include arbitration fee requirements
kyrers Sep 4, 2025
1f5bb9b
feat: integrate countdown timer for arbitration fee deposit
kyrers Sep 4, 2025
491d4f7
feat: implement withdraw functionality for both parties
kyrers Sep 5, 2025
b6cac45
feat: enhance transaction status and amount tags responsiveness
kyrers Sep 5, 2025
678029e
feat: display dispute details and links
kyrers Sep 8, 2025
3427d72
feat: fetch dispute info and add it to the transaction object
kyrers Sep 11, 2025
f0b4493
feat: enhance ongoing dispute information display with ruling details
kyrers Sep 11, 2025
659e78c
feat: implement dispute ruling display
kyrers Sep 12, 2025
2b413d9
feat: implement appeal functionality
kyrers Sep 12, 2025
ef6b11e
fix: correctly fetch appeal decision events
kyrers Sep 15, 2025
bd99b9d
feat: handle scenario where appeal is no longer possible, but the rul…
kyrers Sep 15, 2025
c1afe27
style: update ActionsContainer layout for improved responsiveness and…
kyrers Sep 15, 2025
813e23d
Merge pull request #18 from kleros/feat/raise-dispute-and-appeal
kyrers Sep 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@tanstack/react-query": "^5.76.1",
"alchemy-sdk": "^3.6.1",
"react": "^18.2.55",
"react-countdown": "^2.3.6",
"react-dom": "^18.2.55",
"react-router": "^7.6.0",
"styled-components": "^6.1.18",
Expand Down
10 changes: 10 additions & 0 deletions src/assets/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions src/assets/warning-circle-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions src/components/Transactions/TransactionCard/TransactionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ const AmountTag = styled(Tag)`
pointer-events: none;
text-transform: capitalize;
font-weight: bold;
max-width: 50%;

p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

@media (max-width: ${({ theme }) => theme.breakpoints.xs}) {
width: 100%;
max-width: 100%;
}
`;

interface Props {
Expand All @@ -80,6 +92,7 @@ export default function TransactionCard({ transaction }: Props) {
active
status={transaction.formattedStatus}
text={transaction.formattedStatus}
forceMaxWidth
/>
<AmountTag
active
Expand Down
78 changes: 75 additions & 3 deletions src/components/Transactions/TransactionDetails/Actions/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import Reimburse from "./Reimburse/Reimburse";
import Execute from "./Execute/Execute";
import { useState } from "react";
import ErrorAlert from "./Common/ErrorAlert/ErrorAlert";
import RaiseDispute from "./RaiseDispute/RaiseDispute";
import Withdraw from "./Withdraw/Withdraw";
import OngoingDisputeInfo from "./OngoingDisputeInfo/OngoingDisputeInfo";
import { DisputeStatus } from "model/Dispute";

const Container = styled.div`
display: flex;
Expand All @@ -15,6 +19,12 @@ const Container = styled.div`
const ActionsContainer = styled.div`
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 2rem;

@media (max-width: ${({ theme }) => theme.breakpoints.xs}) {
gap: 1rem;
}
`;

interface Props {
Expand All @@ -23,7 +33,8 @@ interface Props {
}

export default function Actions({ transaction, isBuyer }: Props) {
const [isExecuteError, setIsExecuteError] = useState<boolean>(false);
//For actions that do not have their own modal to show an error
const [isActionError, setIsActionError] = useState<boolean>(false);

const currentTime = Date.now() / 1000;
const isInBufferPeriod =
Expand Down Expand Up @@ -59,9 +70,34 @@ export default function Actions({ transaction, isBuyer }: Props) {
transaction.status === TransactionStatus.NoDispute &&
hasTimedOut;

//Show raise dispute button if there is no dispute already and the transaction has not timed out
const showRaiseDisputeButton =
transaction.status === TransactionStatus.NoDispute && !hasTimedOut;

//Show deposit arbitration fee button (RaiseDispute component) to the user only if he is the party that needs to deposit the arbitration fee
const showDepositArbitrationFeeButton =
(transaction.status === TransactionStatus.WaitingSender && isBuyer) ||
(transaction.status === TransactionStatus.WaitingReceiver && !isBuyer);

const depositFeeDeadline =
transaction.lastInteraction + transaction.arbitrationInfo.feeTimeout;

//Show withdraw button if the user is the party that is waiting for the other to deposit the arbitration fee
const showWithdrawButton =
(transaction.status === TransactionStatus.WaitingSender && !isBuyer) ||
(transaction.status === TransactionStatus.WaitingReceiver && isBuyer);

const ongoingDispute =
transaction.status === TransactionStatus.DisputeCreated &&
transaction.disputeInfo.disputeStatus !== DisputeStatus.Solved;

const waitingRulingExecution =
transaction.status !== TransactionStatus.Resolved &&
transaction.disputeInfo.disputeStatus === DisputeStatus.Solved;

return (
<Container>
{isExecuteError && <ErrorAlert />}
{isActionError && <ErrorAlert />}

<ActionsContainer>
{showPayButton && (
Expand Down Expand Up @@ -90,7 +126,43 @@ export default function Actions({ transaction, isBuyer }: Props) {
transactionId={transaction.id}
contractAddress={transaction.arbitrableAddress}
isNative={transaction.metaEvidence.token?.ticker === "ETH"}
setIsExecuteError={setIsExecuteError}
setIsExecuteError={setIsActionError}
/>
)}

{(showRaiseDisputeButton || showDepositArbitrationFeeButton) && (
<RaiseDispute
transactionId={transaction.id}
contractAddress={transaction.arbitrableAddress}
arbitrationCost={transaction.arbitrationInfo.arbitrationCost}
isNative={transaction.metaEvidence.token?.ticker === "ETH"}
isBuyer={isBuyer}
hasToDepositFee={showDepositArbitrationFeeButton}
depositFeeDeadline={depositFeeDeadline}
/>
)}

{showWithdrawButton && (
<Withdraw
transactionId={transaction.id}
contractAddress={transaction.arbitrableAddress}
isNative={transaction.metaEvidence.token?.ticker === "ETH"}
isBuyer={isBuyer}
depositFeeDeadline={depositFeeDeadline}
setIsWithdrawError={setIsActionError}
/>
)}

{(ongoingDispute || waitingRulingExecution) && (
<OngoingDisputeInfo
transactionId={transaction.id}
contractAddress={transaction.arbitrableAddress}
isNative={transaction.metaEvidence.token?.ticker === "ETH"}
disputeId={transaction.disputeId}
disputeInfo={transaction.disputeInfo}
isBuyer={isBuyer}
isAwaitingRulingExecution={waitingRulingExecution}
setIsAppealError={setIsActionError}
/>
)}
</ActionsContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Countdown from "react-countdown";
import { StyledP } from "../StyledElements/StyledElements";

interface Props {
depositFeeDeadline: number;
}

export default function FeeDepositCoundown({ depositFeeDeadline }: Props) {
return (
<StyledP>
Time left:{" "}
<Countdown
date={depositFeeDeadline * 1000}
renderer={({ days, hours, minutes, seconds }) => {
return (
<span>
{days}d {hours}h {minutes}m {seconds}s
</span>
);
}}
/>
</StyledP>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,26 @@ export const StyledNumberField = styled(NumberField)`

export const StyledP = styled.p`
font-weight: bold;
text-align: justify;
`;

export const StyledH1 = styled.h1`
font-size: 1.5rem;
font-weight: bold;
`;

export const CustomActionButtonContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 16px;
`;

export const RulingContainer = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
text-align: justify;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,19 @@ export default function Execute({
return {
address: contractAddress as `0x${string}`,
args: [transactionId],
account: address,
query: { enabled: false }, //Only simulate when we want
} as const;
}, [transactionId, contractAddress]);
}, [contractAddress, transactionId, address]);

const { refetch: refetchNativeSimulateData } =
useSimulateMultipleArbitrableTransactionExecuteTransaction({
...transactionConfig,
account: address,
query: { enabled: false }, //Only simulate when we want
});

const { refetch: refetchTokenSimulateData } =
useSimulateMultipleArbitrableTokenTransactionExecuteTransaction({
...transactionConfig,
account: address,
query: { enabled: false }, //Only simulate when we want
});

const { writeContractAsync: executeNativeTransaction } =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useAccount } from "wagmi";
import { StyledP } from "../Common/StyledElements/StyledElements";
import styled from "styled-components";
import { DisputeRuling, DisputeStatus, type DisputeInfo } from "model/Dispute";
import AppealableDisputeInfo from "./Rulings/AppealableDisputeInfo/AppealableDisputeInfo";
import WinnerDisputeInfo from "./Rulings/WinnerDisputeInfo/WinnerDisputeInfo";

interface Props {
transactionId: bigint;
contractAddress: string;
isNative: boolean;
disputeId: bigint;
disputeInfo: DisputeInfo;
isBuyer: boolean;
isAwaitingRulingExecution: boolean;
setIsAppealError: (isError: boolean) => void;
}

const defaultLinkProps = {
target: "_blank",
rel: "noopener noreferrer",
};

const CustomP = styled(StyledP)`
text-align: center;
`;

const StyledA = styled.a`
color: ${({ theme }) => theme.colors.secondaryText};
text-decoration: underline;
`;

export default function OngoingDisputeInfo({
transactionId,
contractAddress,
isNative,
disputeId,
disputeInfo,
isBuyer,
isAwaitingRulingExecution,
setIsAppealError,
}: Props) {
const { chainId } = useAccount();

if (disputeInfo.disputeStatus === DisputeStatus.Waiting) {
return (
<CustomP>
Dispute ongoing.
<br />
You can follow the process and upload evidence using the{" "}
<StyledA
href={`https://resolve.kleros.io/${chainId}/cases/${disputeId}`}
{...defaultLinkProps}
>
Dispute Resolver.
</StyledA>
<br />
You can also view the case in the{" "}
<StyledA
href={`https://court.kleros.io/cases/${disputeId}`}
{...defaultLinkProps}
>
Court.
</StyledA>
</CustomP>
);
}

const showTiedInformation =
disputeInfo.currentRuling === DisputeRuling["Jurors refused to arbitrate"];

const showLoserInformation =
(isBuyer &&
disputeInfo.currentRuling ===
DisputeRuling["Jurors ruled in favor of the receiver"]) ||
(!isBuyer &&
disputeInfo.currentRuling ===
DisputeRuling["Jurors ruled in favor of the sender"]);

//In effect, both are the same. In both scenarios, appeal is possible.
if (showTiedInformation || showLoserInformation) {
return (
<AppealableDisputeInfo
transactionId={transactionId}
contractAddress={contractAddress}
isNative={isNative}
appealCost={disputeInfo.appealCost}
appealPeriod={disputeInfo.appealPeriod}
isLoser={showLoserInformation}
isAwaitingRulingExecution={isAwaitingRulingExecution}
setIsAppealError={setIsAppealError}
/>
);
}

return (
<WinnerDisputeInfo
appealPeriod={disputeInfo.appealPeriod}
isAwaitingRulingExecution={isAwaitingRulingExecution}
/>
);
}
Loading