Skip to content
Merged
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
5 changes: 5 additions & 0 deletions apps/hyperdrive-trading/src/rewards/ClaimableReward.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Reward } from "src/rewards/generated/HyperdriveRewardsApi";

export interface ClaimableReward extends Reward {
merkleType: "HyperdriveMerkle" | "MerklXyz";
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import classNames from "classnames";
import { ReactElement } from "react";
import Skeleton from "react-loading-skeleton";
import { ClaimableReward } from "src/rewards/ClaimableReward";
import { Reward } from "src/rewards/generated/HyperdriveRewardsApi";
import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain";
import { Pagination } from "src/ui/base/components/Pagination";
Expand All @@ -28,7 +29,7 @@ export function RewardsTableDesktop({
rewards,
}: {
account: Address;
rewards: Reward[];
rewards: ClaimableReward[];
}): ReactElement {
const appConfig = useAppConfigForConnectedChain({ strict: false });
const tableInstance = useReactTable({
Expand Down Expand Up @@ -139,7 +140,7 @@ export function RewardsTableDesktop({
);
}

const columnHelper = createColumnHelper<Reward>();
const columnHelper = createColumnHelper<ClaimableReward>();

function getColumns({
account,
Expand Down Expand Up @@ -240,7 +241,7 @@ function ClaimRewardsButton({
reward,
}: {
account: Address | undefined;
reward: Reward;
reward: ClaimableReward;
}): ReactElement {
const connectedChainId = useChainId();
const { claimed } = useClaimedRewards({
Expand Down
137 changes: 86 additions & 51 deletions apps/hyperdrive-trading/src/ui/rewards/hooks/useClaimReward.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ import { usePublicClient, useWriteContract } from "wagmi";
import { getToken } from "@delvtech/hyperdrive-appconfig";
import { hyperdriveRewardsAbi } from "@delvtech/hyperdrive-js";
import { useCallback, useState } from "react";
import { assertNever } from "src/base/assertNever";
import { QueryStatusWithIdle } from "src/base/queryStatus";
import { Reward } from "src/rewards/generated/HyperdriveRewardsApi";
import { ClaimableReward } from "src/rewards/ClaimableReward";
import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain";
import { SUCCESS_TOAST_DURATION } from "src/ui/base/toasts";
import { merklDistributorAbi } from "src/ui/rewards/merklDistributorAbi";
import TransactionToast from "src/ui/transactions/TransactionToast";
import { Address } from "viem";
import { WriteContractVariables } from "wagmi/query";

export function useClaimReward({
account,
reward,
enabled = true,
}: {
account: Address | undefined;
reward: Reward;
reward: ClaimableReward;
enabled?: boolean;
}): {
claim: (() => void) | undefined;
Expand All @@ -44,63 +47,46 @@ export function useClaimReward({
return;
}

return writeContract(
{
abi: hyperdriveRewardsAbi,
address: reward.claimContractAddress,
chainId: reward.chainId,
functionName: "claim",
args: [
account,
reward.rewardTokenAddress,
BigInt(reward.claimableAmount), // must be the claimable amount, since it's baked into the merkle proof
reward.merkleProof,
],
},
{
onSuccess: async (hash) => {
addRecentTransaction({
hash,
description: "Claim Reward",
});
setIsTransactionMined(false);
toast.loading(
<TransactionToast
chainId={reward.chainId}
message={`Claiming ${token?.symbol} reward...`}
txHash={hash}
/>,
{ id: hash },
);
const claimArgs = getClaimArgs(account, reward);
writeContract(claimArgs as any, {
onSuccess: async (hash) => {
addRecentTransaction({
hash,
description: "Claim Reward",
});
setIsTransactionMined(false);
toast.loading(
<TransactionToast
chainId={reward.chainId}
message={`Claiming ${token?.symbol} reward...`}
txHash={hash}
/>,
{ id: hash },
);

await waitForTransactionAndInvalidateCache({
publicClient,
hash,
queryClient,
});
setIsTransactionMined(true);
await waitForTransactionAndInvalidateCache({
publicClient,
hash,
queryClient,
});
setIsTransactionMined(true);

toast.success(
<TransactionToast
chainId={reward.chainId}
message={`Claimed ${token?.symbol} reward`}
txHash={hash}
/>,
{ id: hash, duration: SUCCESS_TOAST_DURATION },
);
},
toast.success(
<TransactionToast
chainId={reward.chainId}
message={`Claimed ${token?.symbol} reward`}
txHash={hash}
/>,
{ id: hash, duration: SUCCESS_TOAST_DURATION },
);
},
);
});
}, [
account,
addRecentTransaction,
publicClient,
queryEnabled,
reward.chainId,
reward.claimContractAddress,
reward.claimableAmount,
reward.merkleProof,
reward.rewardTokenAddress,
reward,
token?.symbol,
writeContract,
]);
Expand All @@ -111,3 +97,52 @@ export function useClaimReward({
isTransactionMined,
};
}

function getClaimArgs(account: Address, reward: ClaimableReward) {
switch (reward.merkleType) {
case "HyperdriveMerkle": {
const claimArgs: WriteContractVariables<
typeof hyperdriveRewardsAbi,
"claim",
any,
any,
any
> = {
address: reward.claimContractAddress,
abi: hyperdriveRewardsAbi,
functionName: "claim",
args: [
account,
reward.rewardTokenAddress,
BigInt(reward.claimableAmount),
reward.merkleProof,
],
};

return claimArgs;
}
case "MerklXyz": {
const claimArgs: WriteContractVariables<
typeof merklDistributorAbi,
"claim",
any,
any,
any
> = {
address: reward.claimContractAddress,
abi: merklDistributorAbi,
functionName: "claim",
args: [
[account],
[reward.rewardTokenAddress],
[BigInt(reward.claimableAmount)],
[reward.merkleProof],
],
};

return claimArgs;
}
default:
assertNever(reward.merkleType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { MerklApi } from "@merkl/api";
import { useQuery } from "@tanstack/react-query";
import { makeQueryKey2 } from "src/base/makeQueryKey";
import { rewardsFork } from "src/chains/rewardsFork";
import {
HyperdriveRewardsApi,
Reward,
} from "src/rewards/generated/HyperdriveRewardsApi";
import { ClaimableReward } from "src/rewards/ClaimableReward";
import { HyperdriveRewardsApi } from "src/rewards/generated/HyperdriveRewardsApi";
import { Address, Hash } from "viem";
import { gnosis } from "viem/chains";
import { usePublicClient } from "wagmi";
Expand All @@ -16,7 +14,7 @@ export function useClaimableRewards({
}: {
account: Address | undefined;
}): {
rewards: Reward[] | undefined;
rewards: ClaimableReward[] | undefined;
rewardsStatus: "error" | "success" | "loading";
} {
const publicClient = usePublicClient();
Expand Down Expand Up @@ -49,7 +47,9 @@ export function useClaimableRewards({
* Rewards that come from the Hyperdrive Rewards API server. This server also
* defines the shape used for rewards everywhere else in the app.
*/
async function fetchHyperdriveRewardApi(account: Address): Promise<Reward[]> {
async function fetchHyperdriveRewardApi(
account: Address,
): Promise<ClaimableReward[]> {
const rewardsApi = new HyperdriveRewardsApi({
baseUrl: import.meta.env.VITE_REWARDS_BASE_URL,
});
Expand All @@ -60,6 +60,7 @@ async function fetchHyperdriveRewardApi(account: Address): Promise<Reward[]> {
return response.rewards.map((r) => ({
...r,
chainId: rewardsFork.id,
merkleType: "HyperdriveMerkle",
claimableAmount: parseFixed(r.claimableAmount).bigint.toString(),
}));
} catch (error: any) {
Expand Down Expand Up @@ -89,7 +90,7 @@ const MerklDistributorsByChain: Record<number, Address> = {
[gnosis.id]: "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae",
};

async function fetchMileRewards(account: Address): Promise<Reward[]> {
async function fetchMileRewards(account: Address): Promise<ClaimableReward[]> {
// Merkl.xyz accumulates Miles across all chains and hyperdrives onto Gnosis
// chain only. This makes things easier for turning them into HD later if
// they're all just on one chain.
Expand Down Expand Up @@ -121,15 +122,18 @@ async function fetchMileRewards(account: Address): Promise<Reward[]> {
// since we only request a single chain id, we can just grab the first
// data item
data[0].rewards.find(
// Merkl.xyz has something called HYPOINTS too, but we only care about
// Miles
(d) => d.token.symbol === "Miles" && !!Number(d.amount),
),
)
.map(({ data, chainId }) => {
.map(({ data, chainId }): ClaimableReward => {
const rewards = data![0].rewards.find(
(d) => d.token.symbol === "Miles" && !!Number(d.amount),
);
return {
chainId,
merkleType: "MerklXyz",
merkleProof: rewards?.proofs as Hash[],
claimableAmount: rewards?.amount.toString() || "0",
pendingAmount: rewards?.pending.toString() || "0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Abi for the Merkl Distributor contract: https://app.merkl.xyz/status

export const MerklDistributorAbi = [
export const merklDistributorAbi = [
{ inputs: [], stateMutability: "nonpayable", type: "constructor" },
{ inputs: [], name: "InvalidDispute", type: "error" },
{ inputs: [], name: "InvalidLengths", type: "error" },
Expand Down
Loading
Loading