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
17 changes: 17 additions & 0 deletions lib/price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
9 changes: 9 additions & 0 deletions src/app/dashboard/writer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -126,6 +128,13 @@ function StorylineDetail({ storyline }: { storyline: Storyline }) {
storyline.last_plot_time && (
<DeadlineCountdown lastPlotTime={storyline.last_plot_time} />
)}

{storyline.token_address && (
<ClaimRoyalties
tokenAddress={storyline.token_address as Address}
plotCount={storyline.plot_count}
/>
)}
</div>
);
}
115 changes: 115 additions & 0 deletions src/components/ClaimRoyalties.tsx
Original file line number Diff line number Diff line change
@@ -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<TxState>("idle");
const [error, setError] = useState<string | null>(null);
const [claimedAmount, setClaimedAmount] = useState<bigint>(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 (
<div className="mt-3">
<div className="flex items-center justify-between text-xs">
<div>
<span className="text-muted text-[10px] uppercase tracking-wider">
Unclaimed Royalties
</span>
<span className="text-foreground ml-2">
{formatUnits(unclaimed, 18)} {reserveLabel}
</span>
</div>
{eligible ? (
<button
onClick={txState === "done" || txState === "error" ? reset : executeClaim}
disabled={
(txState === "idle" && unclaimed === BigInt(0)) ||
(txState !== "idle" && txState !== "done" && txState !== "error")
}
className="bg-accent text-background rounded px-3 py-1 text-[10px] font-medium transition-opacity disabled:opacity-40"
>
{txState === "idle" && "Claim"}
{txState === "confirming" && "Confirm..."}
{txState === "pending" && "Pending..."}
{txState === "done" && `Claimed ${formatUnits(claimedAmount, 18)} ${reserveLabel}`}
{txState === "error" && "Retry"}
</button>
) : (
<span className="text-muted text-[10px]">
Chain plot #1 to unlock
</span>
)}
</div>
{error && <p className="mt-1 text-[10px] text-red-400">{error}</p>}
</div>
);
}
Loading