diff --git a/lib/price.ts b/lib/price.ts index 64f124b4..d9d5a45f 100644 --- a/lib/price.ts +++ b/lib/price.ts @@ -53,6 +53,23 @@ export const mcv2BondAbi = [ ], outputs: [], }, + { + type: "function", + name: "getRoyaltyInfo", + stateMutability: "view", + inputs: [{ name: "token", type: "address" }], + outputs: [ + { name: "royalty", type: "uint256" }, + { name: "royaltyBeneficiary", type: "address" }, + ], + }, + { + type: "function", + name: "claimRoyalties", + stateMutability: "nonpayable", + inputs: [{ name: "token", type: "address" }], + outputs: [], + }, ] as const; export const erc20Abi = [ diff --git a/src/app/dashboard/writer/page.tsx b/src/app/dashboard/writer/page.tsx index 929a08c0..9c334d10 100644 --- a/src/app/dashboard/writer/page.tsx +++ b/src/app/dashboard/writer/page.tsx @@ -5,7 +5,9 @@ import { useQuery } from "@tanstack/react-query"; import { supabase, type Storyline } from "../../../../lib/supabase"; import { ConnectWallet } from "../../../components/ConnectWallet"; import { DeadlineCountdown } from "../../../components/DeadlineCountdown"; +import { ClaimRoyalties } from "../../../components/ClaimRoyalties"; import Link from "next/link"; +import { type Address } from "viem"; async function fetchWriterStorylines( address: string, @@ -126,6 +128,13 @@ function StorylineDetail({ storyline }: { storyline: Storyline }) { storyline.last_plot_time && ( )} + + {storyline.token_address && ( + + )} ); } diff --git a/src/components/ClaimRoyalties.tsx b/src/components/ClaimRoyalties.tsx new file mode 100644 index 00000000..30d40382 --- /dev/null +++ b/src/components/ClaimRoyalties.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { useWriteContract } from "wagmi"; +import { useQuery } from "@tanstack/react-query"; +import { formatUnits, type Address } from "viem"; +import { publicClient } from "../../lib/rpc"; +import { mcv2BondAbi } from "../../lib/price"; +import { MCV2_BOND, IS_TESTNET } from "../../lib/contracts/constants"; + +type TxState = "idle" | "confirming" | "pending" | "done" | "error"; + +interface ClaimRoyaltiesProps { + tokenAddress: Address; + plotCount: number; +} + +export function ClaimRoyalties({ tokenAddress, plotCount }: ClaimRoyaltiesProps) { + const [txState, setTxState] = useState("idle"); + const [error, setError] = useState(null); + const [claimedAmount, setClaimedAmount] = useState(BigInt(0)); + + const { writeContractAsync } = useWriteContract(); + + const reserveLabel = IS_TESTNET ? "WETH" : "$PLOT"; + + // Fetch unclaimed royalty balance + const { data: royaltyInfo, refetch } = useQuery({ + queryKey: ["royalty-info", tokenAddress], + queryFn: async () => { + const result = await publicClient.readContract({ + address: MCV2_BOND, + abi: mcv2BondAbi, + functionName: "getRoyaltyInfo", + args: [tokenAddress], + }); + return { + unclaimed: result[0], + beneficiary: result[1] as Address, + }; + }, + refetchInterval: 30000, + }); + + const unclaimed = royaltyInfo?.unclaimed ?? BigInt(0); + const eligible = plotCount >= 2; + + const executeClaim = useCallback(async () => { + try { + setError(null); + setClaimedAmount(unclaimed); + setTxState("confirming"); + + const hash = await writeContractAsync({ + address: MCV2_BOND, + abi: mcv2BondAbi, + functionName: "claimRoyalties", + args: [tokenAddress], + }); + + setTxState("pending"); + await publicClient.waitForTransactionReceipt({ hash }); + + setTxState("done"); + refetch(); + } catch (err) { + setError(err instanceof Error ? err.message : "Claim failed"); + setTxState("error"); + } + }, [tokenAddress, unclaimed, writeContractAsync, refetch]); + + const reset = useCallback(() => { + setTxState("idle"); + setError(null); + }, []); + + // Don't show if no royalties to claim + if (unclaimed === BigInt(0) && txState === "idle") return null; + + return ( + + + + + Unclaimed Royalties + + + {formatUnits(unclaimed, 18)} {reserveLabel} + + + {eligible ? ( + + {txState === "idle" && "Claim"} + {txState === "confirming" && "Confirm..."} + {txState === "pending" && "Pending..."} + {txState === "done" && `Claimed ${formatUnits(claimedAmount, 18)} ${reserveLabel}`} + {txState === "error" && "Retry"} + + ) : ( + + Chain plot #1 to unlock + + )} + + {error && {error}} + + ); +}
{error}