Skip to content

feat: add section for claiming staking rewards in v2, tested in arb sepolia #1913

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
102 changes: 102 additions & 0 deletions web/src/pages/Profile/StakingRewardsClaimModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useState, useEffect } from "react";
import { useAccount, useWriteContract } from "wagmi";
import { formatEther, parseAbi } from "viem";
import { DEFAULT_CHAIN } from "consts/chains";

const ipfsEndpoint = "https://cdn.kleros.link";

const chainIdToParams = {
421614: {
contractAddress: "0x9DdAeD4e2Ba34d59025c1A549311F621a8ff9b7b",
snapshots: ["QmQBupnUD9zt2dzZcB6tNAENiWtmwfWeKDuZbWEWoKs7s2/arbSepolia-snapshot-2025-02.json"],
startMonth: 2,
},
42161: {
contractAddress: "",
snapshots: [],
startMonth: 2,
},
};
Comment on lines +8 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing contract configuration for production chain

The Arbitrum One (42161) configuration has empty values for contractAddress and snapshots. This will prevent the feature from working in production.

-    contractAddress: "",
-    snapshots: [],
+    contractAddress: "0xActualProductionAddress",
+    snapshots: ["actual/production/snapshot/path.json"],
    startMonth: 2,
  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const chainIdToParams = {
421614: {
contractAddress: "0x9DdAeD4e2Ba34d59025c1A549311F621a8ff9b7b",
snapshots: ["QmQBupnUD9zt2dzZcB6tNAENiWtmwfWeKDuZbWEWoKs7s2/arbSepolia-snapshot-2025-02.json"],
startMonth: 2,
},
42161: {
contractAddress: "",
snapshots: [],
startMonth: 2,
},
};
const chainIdToParams = {
421614: {
contractAddress: "0x9DdAeD4e2Ba34d59025c1A549311F621a8ff9b7b",
snapshots: ["QmQBupnUD9zt2dzZcB6tNAENiWtmwfWeKDuZbWEWoKs7s2/arbSepolia-snapshot-2025-02.json"],
startMonth: 2,
},
42161: {
contractAddress: "0xActualProductionAddress",
snapshots: ["actual/production/snapshot/path.json"],
startMonth: 2,
},
};


const claimMonthsAbi = parseAbi([
"function claimMonths(address _liquidityProvider, (uint256 month, uint256 balance, bytes32[] merkleProof)[] claims)",
]);

const ClaimModal = () => {
const { address: account } = useAccount();
const chainId = DEFAULT_CHAIN;
const chainParams = chainIdToParams[chainId] ?? chainIdToParams[DEFAULT_CHAIN];
Comment on lines +27 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Hardcoded chain ID needs to be dynamic

The component uses a hardcoded DEFAULT_CHAIN rather than the user's connected chain, which could lead to incorrect contract interactions if the user is connected to a different network.

- const chainId = DEFAULT_CHAIN;
+ const { chain } = useNetwork();
+ const chainId = chain?.id || DEFAULT_CHAIN;

Make sure to add useNetwork to your wagmi imports.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const chainId = DEFAULT_CHAIN;
const chainParams = chainIdToParams[chainId] ?? chainIdToParams[DEFAULT_CHAIN];
const { chain } = useNetwork();
const chainId = chain?.id || DEFAULT_CHAIN;
const chainParams = chainIdToParams[chainId] ?? chainIdToParams[DEFAULT_CHAIN];


const [claims, setClaims] = useState([]);
const [loading, setLoading] = useState(false);
const [claimed, setClaimed] = useState(false);

useEffect(() => {
const fetchClaims = async () => {
if (!account || !chainParams) return;

const userClaims = [];
for (let index = 0; index < chainParams.snapshots.length; index++) {
const response = await fetch(`${ipfsEndpoint}/ipfs/${chainParams.snapshots[index]}`);
const snapshot = await response.json();
const claim = snapshot.merkleTree.claims[account];

if (claim) {
userClaims.push({
month: chainParams.startMonth + index,
balance: BigInt(claim.value.hex),
merkleProof: claim.proof,
});
}
}
setClaims(userClaims);
};

fetchClaims();
}, [account, chainParams]);
Comment on lines +34 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for IPFS fetch operations

The function fetches data from IPFS without proper error handling, which could lead to unhandled promise rejections if the network request fails.

const fetchClaims = async () => {
  if (!account || !chainParams) return;

  const userClaims = [];
  for (let index = 0; index < chainParams.snapshots.length; index++) {
    try {
      const response = await fetch(`${ipfsEndpoint}/ipfs/${chainParams.snapshots[index]}`);
+     if (!response.ok) {
+       console.error(`Failed to fetch snapshot: ${response.status} ${response.statusText}`);
+       continue;
+     }
      const snapshot = await response.json();
      const claim = snapshot.merkleTree.claims[account];

      if (claim) {
        userClaims.push({
          month: chainParams.startMonth + index,
          balance: BigInt(claim.value.hex),
          merkleProof: claim.proof,
        });
      }
+   } catch (error) {
+     console.error(`Error fetching snapshot ${index}:`, error);
+   }
  }
  setClaims(userClaims);
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const fetchClaims = async () => {
if (!account || !chainParams) return;
const userClaims = [];
for (let index = 0; index < chainParams.snapshots.length; index++) {
const response = await fetch(`${ipfsEndpoint}/ipfs/${chainParams.snapshots[index]}`);
const snapshot = await response.json();
const claim = snapshot.merkleTree.claims[account];
if (claim) {
userClaims.push({
month: chainParams.startMonth + index,
balance: BigInt(claim.value.hex),
merkleProof: claim.proof,
});
}
}
setClaims(userClaims);
};
fetchClaims();
}, [account, chainParams]);
useEffect(() => {
const fetchClaims = async () => {
if (!account || !chainParams) return;
const userClaims = [];
for (let index = 0; index < chainParams.snapshots.length; index++) {
try {
const response = await fetch(`${ipfsEndpoint}/ipfs/${chainParams.snapshots[index]}`);
if (!response.ok) {
console.error(`Failed to fetch snapshot: ${response.status} ${response.statusText}`);
continue;
}
const snapshot = await response.json();
const claim = snapshot.merkleTree.claims[account];
if (claim) {
userClaims.push({
month: chainParams.startMonth + index,
balance: BigInt(claim.value.hex),
merkleProof: claim.proof,
});
}
} catch (error) {
console.error(`Error fetching snapshot ${index}:`, error);
}
}
setClaims(userClaims);
};
fetchClaims();
}, [account, chainParams]);


const { writeContractAsync } = useWriteContract();

const handleClaim = async () => {
if (!claims.length || !account) return;
setLoading(true);

try {
await writeContractAsync({
abi: claimMonthsAbi,
address: chainParams.contractAddress,
functionName: "claimMonths",
args: [account, claims],
gasLimit: 500000,
});

setClaimed(true);
} catch (error) {
console.error("Transaction failed:", error);
setClaimed(false);
}

setLoading(false);
};
Comment on lines +60 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and user feedback for transactions

The current implementation logs errors to the console but doesn't provide meaningful feedback to the user when a transaction fails.

const handleClaim = async () => {
  if (!claims.length || !account) return;
  setLoading(true);
+ setError(null); // Add error state

  try {
    await writeContractAsync({
      abi: claimMonthsAbi,
      address: chainParams.contractAddress,
      functionName: "claimMonths",
      args: [account, claims],
      gasLimit: 500000,
    });

    setClaimed(true);
  } catch (error) {
    console.error("Transaction failed:", error);
+   // Provide a user-friendly error message
+   setError(error.message || "Transaction failed. Please try again.");
    setClaimed(false);
  }

  setLoading(false);
};

Also, don't forget to add the error state to your component:

const [error, setError] = useState(null);

And display it in your UI:

{error && <div className="error-message">{error}</div>}


const totalClaimableTokens = claims.reduce((acc, claim) => acc + claim.balance, BigInt(0));

return (
<div>
{loading && <div>Loading...</div>}

{!loading && !claimed && (
<>
<p>Claimable Tokens: {formatEther(totalClaimableTokens)} PNK</p>
<button onClick={handleClaim} disabled={!claims.length}>
Claim Tokens
</button>
</>
)}

{claimed && <p>🎉 Tokens Claimed 🎉</p>}
</div>
);
};

export default ClaimModal;
2 changes: 2 additions & 0 deletions web/src/pages/Profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import FavoriteCases from "components/FavoriteCases";
import ScrollTop from "components/ScrollTop";
import Courts from "./Courts";
import JurorInfo from "./JurorInfo";
import StakingRewardsClaimModal from "./StakingRewardsClaimModal";

const Container = styled.div`
width: 100%;
Expand Down Expand Up @@ -94,6 +95,7 @@ const Profile: React.FC = () => {
}
{...{ casesPerPage }}
/>
<StakingRewardsClaimModal />
</>
) : (
<ConnectWalletContainer>
Expand Down
Loading