import type { ProgramAccount } from "@project-serum/anchor"; import type { MergeMinerData, MergePoolData, MinerData, QuarryData, RewarderData, } from "@quarryprotocol/quarry-sdk"; import { findMergeMinerAddress, findMinerAddress, findPoolAddress, findQuarryAddress, findReplicaMintAddress, } from "@quarryprotocol/quarry-sdk"; import type { AssociatedTokenAccount } from "@saberhq/sail"; import { useToken, useTokens, useUserATAs } from "@saberhq/sail"; import type { Token } from "@saberhq/token-utils"; import { TokenAmount } from "@saberhq/token-utils"; import { useSolana } from "@saberhq/use-solana"; import type { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { zip } from "lodash-es"; import { useEffect, useMemo, useState } from "react"; import { createContainer } from "unstated-next"; import { useBatchedMiners, useBatchedQuarries, useBatchedRewarders, useMergeMinerData, useMergePoolData, } from "../.."; import { makeCalculateClaimableAmount } from "../../utils/makeCalculateClaimableAmounts"; export interface UseMergedQuarriesArgs { /** * Owner of the Merge Miner. Defaults to the currently connected user. */ owner?: PublicKey; /** * Primary token to stake. */ primaryToken?: Token; /** * Key of the primary rewarder. */ primaryRewarder?: PublicKey; /** * Keys of all replica rewarders. */ replicaRewarders?: PublicKey[]; } export interface UseMergedQuarries { primaryToken: Token; replicaToken?: Token | null; primaryRewarderKey: PublicKey; replicaRewarderKeys: PublicKey[]; userPrimaryTokenBalance?: AssociatedTokenAccount | null; mergePool?: ProgramAccount | null; mergeMiner?: ProgramAccount | null; loading: boolean; quarries: MergedQuarry[]; primaryQuarry?: MergedQuarry; } interface QuarryKeySet { rewarderKey: PublicKey; quarryKey: PublicKey; quarryMint: PublicKey; isReplica: boolean; /** * If null, the user's wallet is not connected. */ minerKey: PublicKey | null; } /** * A Quarry being mined alongside other quarries. */ export interface MergedQuarry { /** * If true, then the quarry is a replica. */ isReplica: boolean; /** * Reward token. */ rewardToken: Token; /** * The rewarder. */ rewarder: ProgramAccount; /** * The quarry. */ quarry: ProgramAccount; /** * The miner. */ miner: ProgramAccount | null; /** * Total deposits */ totalDeposits: TokenAmount | null; /** * Total pool rewards per day, in tokens */ rewardsPerDay: TokenAmount | null; /** * Gets the currently claimable amount of tokens. */ getClaimableAmount: () => number; /** * Amount of tokens the user has staked. */ userStakedAmount: TokenAmount | null; /** * Amount of user rewards tokens per day. */ userRewardsPerDay: TokenAmount | null; } const noReplicaRewarders: PublicKey[] = []; const useMergedQuarriesInternal = ({ owner: theOwner, primaryToken, primaryRewarder, replicaRewarders = noReplicaRewarders, }: UseMergedQuarriesArgs = {}): UseMergedQuarries => { if (!primaryToken || !primaryRewarder) { throw new Error("missing primary token or rewarder"); } const { wallet } = useSolana(); const owner = useMemo( () => theOwner ?? wallet?.publicKey, [theOwner, wallet] ); const [{ mergePoolKey, mergeMinerKey, replicaMint, quarryKeys }, setKeys] = useState<{ mergePoolKey?: PublicKey; mergeMinerKey?: PublicKey | null; replicaMint?: PublicKey; quarryKeys: QuarryKeySet[]; }>({ quarryKeys: [] }); useEffect(() => { let cancel = false; void (async () => { const [replicaMint] = await findReplicaMintAddress({ primaryMint: primaryToken.mintAccount, }); const [mergePool] = await findPoolAddress({ primaryMint: primaryToken.mintAccount, }); const [primaryQuarry] = await findQuarryAddress( primaryRewarder, primaryToken.mintAccount ); const replicaQuarries = await Promise.all( replicaRewarders.map(async (replicaRewarder) => { const [replicaQuarry] = await findQuarryAddress( replicaRewarder, replicaMint ); return replicaQuarry; }) ); let mergeMiner: PublicKey | null = null; if (owner) { const [theMergeMiner] = await findMergeMinerAddress({ pool: mergePool, owner, }); mergeMiner = theMergeMiner; } let primaryMiner: PublicKey | null = null; if (mergeMiner) { const [minerKey] = await findMinerAddress(primaryQuarry, mergeMiner); primaryMiner = minerKey; } const quarries: QuarryKeySet[] = []; quarries.push({ rewarderKey: primaryRewarder, quarryKey: primaryQuarry, quarryMint: primaryToken.mintAccount, isReplica: false, minerKey: primaryMiner, }); const replicas = zip(replicaRewarders, replicaQuarries) .filter((v): v is [PublicKey, PublicKey] => !!v[0] && !!v[1]) .map(([replicaRewarder, replicaQuarry]) => ({ rewarderKey: replicaRewarder, quarryKey: replicaQuarry, quarryMint: replicaMint, isReplica: true, minerKey: null, })); const mm = mergeMiner; if (mm) { quarries.push( ...(await Promise.all( replicas.map(async (replica) => { const [minerKey] = await findMinerAddress(replica.quarryKey, mm); return { ...replica, minerKey, }; }) )) ); } else { quarries.push(...replicas); } if (cancel) { return; } setKeys({ mergePoolKey: mergePool, mergeMinerKey: mergeMiner, replicaMint, quarryKeys: quarries, }); })(); return () => { cancel = true; }; }, [owner, primaryRewarder, primaryToken.mintAccount, replicaRewarders]); const [userPrimaryTokenBalance] = useUserATAs(primaryToken); const { data: mergePool } = useMergePoolData(mergePoolKey); const { data: mergeMiner } = useMergeMinerData(mergeMinerKey); const { data: replicaToken } = useToken(replicaMint); const { data: rewarders } = useBatchedRewarders( useMemo(() => quarryKeys.map((q) => q.rewarderKey), [quarryKeys]) ); const { data: quarries } = useBatchedQuarries( useMemo(() => quarryKeys.map((q) => q.quarryKey), [quarryKeys]) ); const { data: miners } = useBatchedMiners( useMemo(() => quarryKeys.map((q) => q.minerKey), [quarryKeys]) ); const rewardTokens = useTokens( useMemo( () => rewarders?.map((q) => q?.account.rewardsTokenMint) ?? [], [rewarders] ) ); const mergedQuarries = useMemo(() => { if (!rewarders || !quarries || !miners) { return []; } return zip(rewarders, quarries, miners, rewardTokens, quarryKeys) .map( ([ rewarder, quarry, miner, rewardTokenQuery, quarryKey, ]): MergedQuarry | null => { const rewardToken = rewardTokenQuery?.data; if (!rewarder || !quarry || !rewardToken || !quarryKey) { return null; } const stakedToken = quarryKey.isReplica ? replicaToken : primaryToken; const totalDeposits = stakedToken ? new TokenAmount(stakedToken, quarry.account.totalTokensDeposited) : null; const rewardsPerDay = rewardToken ? new TokenAmount( rewardToken, quarry.account.annualRewardsRate.div(new BN(365)) ) : null; const calculateClaimableAmount = miner ? makeCalculateClaimableAmount({ rewardsToken: rewardToken, quarryData: quarry.account, minerData: miner.account, }) : null; const userStakedAmount = stakedToken && miner ? new TokenAmount(stakedToken, miner.account.balance) : null; const userRewardsPerDay = calculateClaimableAmount ? TokenAmount.parse( rewardToken, (calculateClaimableAmount.rewardsPerSecond * 86_400).toString() ) : null; return { isReplica: quarryKey.isReplica, rewarder, rewardToken, quarry, miner: miner ?? null, getClaimableAmount: calculateClaimableAmount?.get ?? (() => 0), totalDeposits, rewardsPerDay, userStakedAmount, userRewardsPerDay, }; } ) .filter((m): m is MergedQuarry => !!m); }, [ miners, primaryToken, quarries, quarryKeys, replicaToken, rewardTokens, rewarders, ]); return { primaryToken, replicaToken, userPrimaryTokenBalance, mergePool, mergeMiner, loading: false, quarries: mergedQuarries, primaryRewarderKey: primaryRewarder, replicaRewarderKeys: replicaRewarders, primaryQuarry: mergedQuarries.find((mq) => !mq.isReplica), }; }; /** * @deprecated To be removed in v.0.14.0. */ export const { Provider: MergedQuarriesProvider, useContainer: useMergedQuarries, } = createContainer(useMergedQuarriesInternal);