From c335bc166ffadd1753963507763d3ea9b0bc307b Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Mon, 16 Mar 2026 19:56:26 +0000 Subject: [PATCH 1/2] [#184] Add title param to chainPlot, fix 168h deadline, redeploy contract Contract changes (plotlink-contracts): - Added `string calldata title` to chainPlot function signature - Added `title` field to PlotChained event - Updated genesis emit in createStoryline to include title - Changed deadline from 72h to 168h (7 days) - All 15 tests pass - Deployed to Base Sepolia: 0x6B8d38af1773dd162Ebc6f4A8eb923F3c669605d - Verified on Sourcify (exact match) Frontend changes: - Updated PlotChained event ABI with title field (abi.ts + SDK abi.ts) - Updated chainPlot function ABI with title input param - Updated STORY_FACTORY address to new deployment - useChainPlot hook: added optional title param (defaults to "") - SDK client.chainPlot: added optional title param - CLI chain command: added --title flag Fixes #184 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/contracts/abi.ts | 2 ++ lib/contracts/constants.ts | 2 +- packages/cli/src/commands/chain.ts | 5 +++-- packages/sdk/src/abi.ts | 2 ++ packages/sdk/src/client.ts | 4 +++- packages/sdk/src/constants.ts | 2 +- src/hooks/useChainPlot.ts | 4 ++-- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/contracts/abi.ts b/lib/contracts/abi.ts index 9f8806de..923f8bde 100644 --- a/lib/contracts/abi.ts +++ b/lib/contracts/abi.ts @@ -16,6 +16,7 @@ export const plotChainedEvent = { { name: "storylineId", type: "uint256", indexed: true }, { name: "plotIndex", type: "uint256", indexed: true }, { name: "writer", type: "address", indexed: true }, + { name: "title", type: "string", indexed: false }, { name: "contentCID", type: "string", indexed: false }, { name: "contentHash", type: "bytes32", indexed: false }, ], @@ -68,6 +69,7 @@ export const chainPlotFunction = { stateMutability: "nonpayable", inputs: [ { name: "storylineId", type: "uint256" }, + { name: "title", type: "string" }, { name: "contentCID", type: "string" }, { name: "contentHash", type: "bytes32" }, ], diff --git a/lib/contracts/constants.ts b/lib/contracts/constants.ts index 462bea6d..a52e8719 100644 --- a/lib/contracts/constants.ts +++ b/lib/contracts/constants.ts @@ -27,7 +27,7 @@ export const EXPLORER_URL = IS_TESTNET /** StoryFactory — storyline + plot management */ export const STORY_FACTORY = (process.env.NEXT_PUBLIC_CONTRACT_ADDRESS ?? (IS_TESTNET - ? "0x05C4d59529807316D6fA09cdaA509adDfe85b474" + ? "0x6B8d38af1773dd162Ebc6f4A8eb923F3c669605d" : "0x0000000000000000000000000000000000000000")) as `0x${string}`; /** ZapPlotLinkMCV2 — one-click buy (ETH/USDC/HUNT -> storyline token) */ diff --git a/packages/cli/src/commands/chain.ts b/packages/cli/src/commands/chain.ts index b90d7a70..1a9ee68c 100644 --- a/packages/cli/src/commands/chain.ts +++ b/packages/cli/src/commands/chain.ts @@ -8,14 +8,15 @@ export function registerChain(program: Command): void { .description("Chain a new plot onto an existing storyline") .requiredOption("-s, --storyline ", "Storyline ID") .requiredOption("-f, --file ", "Path to content file (plain text)") - .action(async (opts: { storyline: string; file: string }) => { + .option("-t, --title ", "Chapter title", "") + .action(async (opts: { storyline: string; file: string; title: string }) => { try { const content = readFileSync(opts.file, "utf-8"); const storylineId = BigInt(opts.storyline); const client = buildClient({ ipfs: true }); console.log(`Chaining plot onto storyline ${storylineId}...`); - const result = await client.chainPlot(storylineId, content); + const result = await client.chainPlot(storylineId, content, opts.title); console.log("Plot chained!"); console.log(` TX: ${result.txHash}`); diff --git a/packages/sdk/src/abi.ts b/packages/sdk/src/abi.ts index 37475f2a..b2aa4c0c 100644 --- a/packages/sdk/src/abi.ts +++ b/packages/sdk/src/abi.ts @@ -18,6 +18,7 @@ export const storyFactoryAbi = [ { name: "storylineId", type: "uint256", indexed: true }, { name: "plotIndex", type: "uint256", indexed: true }, { name: "writer", type: "address", indexed: true }, + { name: "title", type: "string", indexed: false }, { name: "contentCID", type: "string", indexed: false }, { name: "contentHash", type: "bytes32", indexed: false }, ], @@ -63,6 +64,7 @@ export const storyFactoryAbi = [ stateMutability: "nonpayable", inputs: [ { name: "storylineId", type: "uint256" }, + { name: "title", type: "string" }, { name: "contentCID", type: "string" }, { name: "contentHash", type: "bytes32" }, ], diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index d0df7320..3c490677 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -254,11 +254,13 @@ export class PlotLink { * * @param storylineId - The storyline to chain onto * @param content - Plot content (plain text) + * @param title - Optional chapter title (defaults to empty string) * @returns Transaction hash and IPFS CID */ async chainPlot( storylineId: bigint, content: string, + title = "", ): Promise<ChainPlotResult> { this.requireFilebase(); validateNonEmpty("content", content); @@ -272,7 +274,7 @@ export class PlotLink { address: this.storyFactory, abi: storyFactoryAbi, functionName: "chainPlot", - args: [storylineId, contentCid, contentHash], + args: [storylineId, title, contentCid, contentHash], }); const txHash = await this.walletClient.writeContract(request); diff --git a/packages/sdk/src/constants.ts b/packages/sdk/src/constants.ts index 1af71604..b72f7cbb 100644 --- a/packages/sdk/src/constants.ts +++ b/packages/sdk/src/constants.ts @@ -31,7 +31,7 @@ export const SUPPORTED_CHAIN_IDS = new Set([BASE_SEPOLIA_CHAIN_ID, BASE_MAINNET_ /** StoryFactory — storyline + plot management. */ export const STORY_FACTORY_ADDRESS = - "0x05C4d59529807316D6fA09cdaA509adDfe85b474" as const; + "0x6B8d38af1773dd162Ebc6f4A8eb923F3c669605d" as const; /** MCV2_Bond — bonding curve trading, token creation, royalty distribution. */ export const MCV2_BOND_ADDRESS = diff --git a/src/hooks/useChainPlot.ts b/src/hooks/useChainPlot.ts index 9b350fd4..de72ab4c 100644 --- a/src/hooks/useChainPlot.ts +++ b/src/hooks/useChainPlot.ts @@ -13,7 +13,7 @@ export function useChainPlot() { const { state, error, txHash, execute, reset } = usePublish(); const chainPlot = useCallback( - async (storylineId: number, content: string) => { + async (storylineId: number, content: string, title = "") => { await execute({ content, uploadKeyPrefix: `plotlink/plots/${storylineId}`, @@ -22,7 +22,7 @@ export function useChainPlot() { address: STORY_FACTORY, abi: storyFactoryAbi as unknown as [], functionName: "chainPlot", - args: [BigInt(storylineId), cid, contentHash], + args: [BigInt(storylineId), title, cid, contentHash], }), }); }, From 0edb93830a2f483d15a89447cc395df67fa472c4 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi <cyh76507707@gmail.com> Date: Mon, 16 Mar 2026 20:00:10 +0000 Subject: [PATCH 2/2] [#184] Add chapter title input to chain plot page Adds an optional title field (max 100 chars) to the chain page UI, wired through to chainPlot(storylineId, content, title). Addresses T2a review feedback on PR #221. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- src/app/chain/page.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/app/chain/page.tsx b/src/app/chain/page.tsx index ba893481..1a13f2f0 100644 --- a/src/app/chain/page.tsx +++ b/src/app/chain/page.tsx @@ -41,6 +41,7 @@ async function fetchWriterStorylines(address: string): Promise<Storyline[]> { export default function ChainPlotPage() { const { address, isConnected } = useAccount(); const [storylineId, setStorylineId] = useState<number | null>(null); + const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const { data: storylines = [], isLoading: loadingStorylines } = useQuery({ @@ -103,7 +104,7 @@ export default function ChainPlotPage() { <form onSubmit={(e) => { e.preventDefault(); - if (canSubmit) chainPlot(storylineId, content); + if (canSubmit) chainPlot(storylineId, content, title); }} className="mt-8 space-y-6" > @@ -135,6 +136,23 @@ export default function ChainPlotPage() { )} </div> + {/* Chapter title */} + <div> + <label className="text-foreground mb-2 block text-sm"> + Chapter Title <span className="text-muted">(optional)</span> + </label> + <input + type="text" + value={title} + onChange={(e) => setTitle(e.target.value.slice(0, 100))} + disabled={busy || noStoryline} + placeholder={noStoryline ? "Select a storyline first" : "e.g. The Awakening"} + maxLength={100} + className="border-border bg-surface text-foreground placeholder:text-muted w-full rounded border px-3 py-2 text-sm focus:border-accent focus:outline-none disabled:opacity-50" + /> + <span className="text-muted mt-1 block text-xs">{title.length}/100</span> + </div> + {/* Content */} <div> <label className="text-foreground mb-2 block text-sm">