plotlink-ows 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/app/db.ts +8 -0
  4. package/app/lib/llm-client.ts +265 -0
  5. package/app/lib/paths.ts +11 -0
  6. package/app/lib/publish.ts +204 -0
  7. package/app/lib/writer-prompt.ts +44 -0
  8. package/app/node_modules/.prisma/local-client/client.d.ts +1 -0
  9. package/app/node_modules/.prisma/local-client/client.js +5 -0
  10. package/app/node_modules/.prisma/local-client/default.d.ts +1 -0
  11. package/app/node_modules/.prisma/local-client/default.js +5 -0
  12. package/app/node_modules/.prisma/local-client/edge.d.ts +1 -0
  13. package/app/node_modules/.prisma/local-client/edge.js +184 -0
  14. package/app/node_modules/.prisma/local-client/index-browser.js +173 -0
  15. package/app/node_modules/.prisma/local-client/index.d.ts +3304 -0
  16. package/app/node_modules/.prisma/local-client/index.js +207 -0
  17. package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
  18. package/app/node_modules/.prisma/local-client/package.json +183 -0
  19. package/app/node_modules/.prisma/local-client/query_engine_bg.js +2 -0
  20. package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
  21. package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +35 -0
  22. package/app/node_modules/.prisma/local-client/runtime/edge.js +35 -0
  23. package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +370 -0
  24. package/app/node_modules/.prisma/local-client/runtime/index-browser.js +17 -0
  25. package/app/node_modules/.prisma/local-client/runtime/library.d.ts +3982 -0
  26. package/app/node_modules/.prisma/local-client/runtime/library.js +147 -0
  27. package/app/node_modules/.prisma/local-client/runtime/react-native.js +84 -0
  28. package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +85 -0
  29. package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +38 -0
  30. package/app/node_modules/.prisma/local-client/schema.prisma +21 -0
  31. package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +5 -0
  32. package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +5 -0
  33. package/app/node_modules/.prisma/local-client/wasm.d.ts +1 -0
  34. package/app/node_modules/.prisma/local-client/wasm.js +191 -0
  35. package/app/prisma/schema.prisma +57 -0
  36. package/app/routes/auth.ts +173 -0
  37. package/app/routes/chat.ts +135 -0
  38. package/app/routes/config.ts +210 -0
  39. package/app/routes/dashboard.ts +186 -0
  40. package/app/routes/oauth.ts +150 -0
  41. package/app/routes/publish.ts +112 -0
  42. package/app/routes/wallet.ts +99 -0
  43. package/app/server.ts +154 -0
  44. package/app/vite.config.ts +19 -0
  45. package/app/web/App.tsx +102 -0
  46. package/app/web/components/Chat.tsx +272 -0
  47. package/app/web/components/Dashboard.tsx +222 -0
  48. package/app/web/components/LLMSetup.tsx +291 -0
  49. package/app/web/components/Layout.tsx +235 -0
  50. package/app/web/components/Login.tsx +62 -0
  51. package/app/web/components/Publish.tsx +245 -0
  52. package/app/web/components/Settings.tsx +175 -0
  53. package/app/web/components/Setup.tsx +84 -0
  54. package/app/web/components/WalletCard.tsx +117 -0
  55. package/app/web/dist/assets/index-C9kXlYO_.css +2 -0
  56. package/app/web/dist/assets/index-CJiiaLHs.js +9 -0
  57. package/app/web/dist/index.html +16 -0
  58. package/app/web/index.html +15 -0
  59. package/app/web/main.tsx +10 -0
  60. package/app/web/plotlink-logo.svg +5 -0
  61. package/app/web/styles.css +51 -0
  62. package/bin/plotlink-ows.js +394 -0
  63. package/lib/ows/index.ts +3 -0
  64. package/lib/ows/policy.ts +68 -0
  65. package/lib/ows/types.ts +14 -0
  66. package/lib/ows/wallet.ts +70 -0
  67. package/package.json +79 -0
  68. package/packages/cli/node_modules/commander/LICENSE +22 -0
  69. package/packages/cli/node_modules/commander/Readme.md +1149 -0
  70. package/packages/cli/node_modules/commander/esm.mjs +16 -0
  71. package/packages/cli/node_modules/commander/index.js +24 -0
  72. package/packages/cli/node_modules/commander/lib/argument.js +149 -0
  73. package/packages/cli/node_modules/commander/lib/command.js +2662 -0
  74. package/packages/cli/node_modules/commander/lib/error.js +39 -0
  75. package/packages/cli/node_modules/commander/lib/help.js +709 -0
  76. package/packages/cli/node_modules/commander/lib/option.js +367 -0
  77. package/packages/cli/node_modules/commander/lib/suggestSimilar.js +101 -0
  78. package/packages/cli/node_modules/commander/package-support.json +16 -0
  79. package/packages/cli/node_modules/commander/package.json +82 -0
  80. package/packages/cli/node_modules/commander/typings/esm.d.mts +3 -0
  81. package/packages/cli/node_modules/commander/typings/index.d.ts +1045 -0
  82. package/packages/cli/node_modules/resolve-from/index.d.ts +31 -0
  83. package/packages/cli/node_modules/resolve-from/index.js +47 -0
  84. package/packages/cli/node_modules/resolve-from/license +9 -0
  85. package/packages/cli/node_modules/resolve-from/package.json +36 -0
  86. package/packages/cli/node_modules/resolve-from/readme.md +72 -0
  87. package/packages/cli/node_modules/tsup/LICENSE +21 -0
  88. package/packages/cli/node_modules/tsup/README.md +75 -0
  89. package/packages/cli/node_modules/tsup/assets/cjs_shims.js +13 -0
  90. package/packages/cli/node_modules/tsup/assets/esm_shims.js +9 -0
  91. package/packages/cli/node_modules/tsup/assets/package.json +3 -0
  92. package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +153 -0
  93. package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +42 -0
  94. package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +6 -0
  95. package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +352 -0
  96. package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +203 -0
  97. package/packages/cli/node_modules/tsup/dist/cli-default.js +12 -0
  98. package/packages/cli/node_modules/tsup/dist/cli-main.js +8 -0
  99. package/packages/cli/node_modules/tsup/dist/cli-node.js +14 -0
  100. package/packages/cli/node_modules/tsup/dist/index.d.ts +511 -0
  101. package/packages/cli/node_modules/tsup/dist/index.js +1711 -0
  102. package/packages/cli/node_modules/tsup/dist/rollup.js +6949 -0
  103. package/packages/cli/node_modules/tsup/package.json +99 -0
  104. package/packages/cli/node_modules/tsup/schema.json +362 -0
  105. package/packages/cli/package.json +35 -0
  106. package/packages/cli/src/commands/agent-register.ts +77 -0
  107. package/packages/cli/src/commands/chain.ts +29 -0
  108. package/packages/cli/src/commands/claim.ts +70 -0
  109. package/packages/cli/src/commands/create.ts +34 -0
  110. package/packages/cli/src/commands/status.ts +201 -0
  111. package/packages/cli/src/config.ts +103 -0
  112. package/packages/cli/src/index.ts +21 -0
  113. package/packages/cli/src/sdk/abi.ts +222 -0
  114. package/packages/cli/src/sdk/client.ts +713 -0
  115. package/packages/cli/src/sdk/constants.ts +56 -0
  116. package/packages/cli/src/sdk/index.ts +46 -0
  117. package/packages/cli/src/sdk/ipfs.ts +88 -0
  118. package/packages/cli/src/sdk.ts +36 -0
  119. package/packages/cli/tsconfig.json +20 -0
  120. package/packages/cli/tsup.config.ts +14 -0
  121. package/public/.well-known/farcaster.json +38 -0
  122. package/public/basescan-icon.svg +4 -0
  123. package/public/embed-image.png +0 -0
  124. package/public/favicon.png +0 -0
  125. package/public/hunt-token.svg +11 -0
  126. package/public/icon-192.png +0 -0
  127. package/public/icon.png +0 -0
  128. package/public/manifest.json +26 -0
  129. package/public/mc-icon-light.svg +12 -0
  130. package/public/og-image.png +0 -0
  131. package/public/plotlink-logo-symbol.svg +5 -0
  132. package/public/plotlink-logo.svg +5 -0
  133. package/public/screenshot-1.png +0 -0
  134. package/public/screenshot-2.png +0 -0
  135. package/public/screenshot-3.png +0 -0
  136. package/public/splash.png +0 -0
  137. package/public/wide-banner.png +0 -0
  138. package/scripts/backfill-trade-prices.ts +97 -0
  139. package/scripts/backfill-usd-rates.ts +220 -0
  140. package/scripts/e2e-verify.ts +1100 -0
  141. package/scripts/ows-smoke-test.ts +37 -0
  142. package/scripts/score-users.mjs +203 -0
@@ -0,0 +1,713 @@
1
+ import {
2
+ createPublicClient,
3
+ createWalletClient,
4
+ http,
5
+ fallback,
6
+ keccak256,
7
+ toHex,
8
+ decodeEventLog,
9
+ formatUnits,
10
+ type PublicClient,
11
+ type WalletClient,
12
+ type Address,
13
+ type Hex,
14
+ type Chain,
15
+ } from "viem";
16
+ import { privateKeyToAccount } from "viem/accounts";
17
+ import { base, baseSepolia } from "viem/chains";
18
+
19
+ import { storyFactoryAbi, erc8004Abi, mcv2BondAbi } from "./abi.js";
20
+
21
+ // Named ABI event references (avoid fragile array indexing)
22
+ const StorylineCreatedEvent = storyFactoryAbi.find(
23
+ (item) => item.type === "event" && item.name === "StorylineCreated",
24
+ )!;
25
+ const PlotChainedEvent = storyFactoryAbi.find(
26
+ (item) => item.type === "event" && item.name === "PlotChained",
27
+ )!;
28
+ import {
29
+ STORY_FACTORY_ADDRESS,
30
+ STORY_FACTORY_MAINNET_ADDRESS,
31
+ MCV2_BOND_ADDRESS,
32
+ MCV2_BOND_MAINNET_ADDRESS,
33
+ ERC8004_REGISTRY_ADDRESS,
34
+ BASE_SEPOLIA_CHAIN_ID,
35
+ BASE_MAINNET_CHAIN_ID,
36
+ DEPLOYMENT_BLOCK,
37
+ DEPLOYMENT_BLOCK_MAINNET,
38
+ SUPPORTED_CHAIN_IDS,
39
+ } from "./constants.js";
40
+ import { uploadWithRetry, type FilebaseConfig } from "./ipfs.js";
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Types
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Configuration for the PlotLink SDK client.
48
+ */
49
+ export interface PlotLinkConfig {
50
+ /** Hex-encoded private key (with or without 0x prefix). */
51
+ privateKey: string;
52
+ /** JSON-RPC URL for the Base chain. */
53
+ rpcUrl: string;
54
+ /** Optional additional RPC URLs for fallback rotation (tried in order after rpcUrl). */
55
+ rpcUrls?: string[];
56
+ /** Chain ID — defaults to 84532 (Base Sepolia). */
57
+ chainId?: number;
58
+ /** Override StoryFactory contract address. */
59
+ storyFactoryAddress?: Address;
60
+ /** Override MCV2_Bond contract address. */
61
+ mcv2BondAddress?: Address;
62
+ /** Override ERC-8004 Registry contract address. */
63
+ erc8004RegistryAddress?: Address;
64
+ /**
65
+ * Filebase credentials for IPFS uploads.
66
+ * Required for createStoryline() and chainPlot().
67
+ * If omitted, those methods will throw when called.
68
+ */
69
+ filebase?: FilebaseConfig;
70
+ }
71
+
72
+ export interface CreateStorylineResult {
73
+ storylineId: bigint;
74
+ txHash: Hex;
75
+ contentCid: string;
76
+ }
77
+
78
+ export interface ChainPlotResult {
79
+ txHash: Hex;
80
+ contentCid: string;
81
+ }
82
+
83
+ export interface StorylineInfo {
84
+ creator: Address;
85
+ tokenAddress: Address;
86
+ title: string;
87
+ hasDeadline: boolean;
88
+ openingCID: string;
89
+ openingHash: Hex;
90
+ }
91
+
92
+ export interface PlotInfo {
93
+ storylineId: bigint;
94
+ plotIndex: bigint;
95
+ writer: Address;
96
+ contentCID: string;
97
+ contentHash: Hex;
98
+ }
99
+
100
+ export interface RegisterAgentResult {
101
+ agentId: bigint;
102
+ txHash: Hex;
103
+ }
104
+
105
+ export interface SetAgentWalletResult {
106
+ txHash: Hex;
107
+ }
108
+
109
+ export interface RoyaltyInfo {
110
+ balance: bigint;
111
+ claimed: bigint;
112
+ }
113
+
114
+ export interface TokenPriceInfo {
115
+ /** Cost (in reserve token wei) to mint 1 unit of the storyline token. */
116
+ priceRaw: bigint;
117
+ /** priceRaw formatted with 18 decimals. */
118
+ priceFormatted: string;
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Client
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /**
126
+ * PlotLink SDK client for interacting with the PlotLink protocol on Base.
127
+ *
128
+ * Provides methods for storyline management, plot chaining, agent registration,
129
+ * and royalty claims. Uses viem for contract interactions and Filebase for
130
+ * IPFS content uploads.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * const client = new PlotLink({
135
+ * privateKey: "0x...",
136
+ * rpcUrl: "https://sepolia.base.org",
137
+ * filebase: { accessKey: "...", secretKey: "...", bucket: "my-bucket" },
138
+ * });
139
+ *
140
+ * const { storylineId } = await client.createStoryline(
141
+ * "My Story",
142
+ * "Once upon a time...",
143
+ * "Fantasy",
144
+ * );
145
+ * ```
146
+ */
147
+ export class PlotLink {
148
+ readonly publicClient: PublicClient;
149
+ readonly walletClient: WalletClient;
150
+ readonly address: Address;
151
+
152
+ readonly storyFactory: Address;
153
+ readonly mcv2Bond: Address;
154
+ private readonly erc8004Registry: Address;
155
+ private readonly filebase: FilebaseConfig | undefined;
156
+ private readonly chain: Chain;
157
+ private readonly deploymentBlock: bigint;
158
+
159
+ constructor(config: PlotLinkConfig) {
160
+ const chainId = config.chainId ?? BASE_SEPOLIA_CHAIN_ID;
161
+ if (!SUPPORTED_CHAIN_IDS.has(chainId)) {
162
+ throw new Error(
163
+ `Unsupported chainId: ${chainId}. PlotLink SDK supports Base (8453) and Base Sepolia (84532).`,
164
+ );
165
+ }
166
+ const isMainnet = chainId === BASE_MAINNET_CHAIN_ID;
167
+ this.chain = isMainnet ? base : baseSepolia;
168
+ this.deploymentBlock = isMainnet ? DEPLOYMENT_BLOCK_MAINNET : DEPLOYMENT_BLOCK;
169
+
170
+ const normalizedKey = config.privateKey.startsWith("0x")
171
+ ? config.privateKey
172
+ : `0x${config.privateKey}`;
173
+ const account = privateKeyToAccount(normalizedKey as Hex);
174
+
175
+ const allUrls = [config.rpcUrl, ...(config.rpcUrls ?? [])];
176
+ const transport =
177
+ allUrls.length > 1
178
+ ? fallback(allUrls.map((url) => http(url, { timeout: 10_000, retryCount: 1 })), { rank: false })
179
+ : http(config.rpcUrl);
180
+
181
+ this.publicClient = createPublicClient({
182
+ chain: this.chain,
183
+ transport,
184
+ });
185
+
186
+ this.walletClient = createWalletClient({
187
+ account,
188
+ chain: this.chain,
189
+ transport,
190
+ });
191
+
192
+ this.address = account.address;
193
+ this.storyFactory =
194
+ config.storyFactoryAddress ?? (isMainnet ? STORY_FACTORY_MAINNET_ADDRESS : STORY_FACTORY_ADDRESS);
195
+ this.mcv2Bond = config.mcv2BondAddress ?? (isMainnet ? MCV2_BOND_MAINNET_ADDRESS : MCV2_BOND_ADDRESS);
196
+ this.erc8004Registry =
197
+ config.erc8004RegistryAddress ?? ERC8004_REGISTRY_ADDRESS;
198
+ this.filebase = config.filebase;
199
+ }
200
+
201
+ // -------------------------------------------------------------------------
202
+ // Storyline methods
203
+ // -------------------------------------------------------------------------
204
+
205
+ /**
206
+ * Create a new storyline.
207
+ *
208
+ * Uploads the opening content to IPFS via Filebase, computes its keccak256
209
+ * hash, and calls StoryFactory.createStoryline() on-chain.
210
+ *
211
+ * @param title - Storyline title
212
+ * @param content - Opening plot content (plain text)
213
+ * @param genre - Genre label (stored off-chain; used for agent URI composition)
214
+ * @param hasDeadline - Whether the storyline has a sunset deadline (default: true, mandatory 7-day)
215
+ * @returns The storyline ID, transaction hash, and IPFS CID
216
+ */
217
+ async createStoryline(
218
+ title: string,
219
+ content: string,
220
+ genre: string,
221
+ hasDeadline = true,
222
+ ): Promise<CreateStorylineResult> {
223
+ this.requireFilebase();
224
+ validateNonEmpty("title", title);
225
+ validateNonEmpty("content", content);
226
+ validateNonEmpty("genre", genre);
227
+ validateTitle(title);
228
+ validateContentLength(content);
229
+
230
+ const metadata = JSON.stringify({ title, genre, content });
231
+ const key = `plotlink/storylines/${Date.now()}-${slugify(title)}.json`;
232
+ const contentCid = await uploadWithRetry(metadata, key, this.filebase!);
233
+ const contentHash = hashContent(content);
234
+
235
+ // MCV2_Bond requires a creation fee as msg.value when minting a new token
236
+ const creationFee = await this.publicClient.readContract({
237
+ address: this.mcv2Bond,
238
+ abi: mcv2BondAbi,
239
+ functionName: "creationFee",
240
+ }) as bigint;
241
+
242
+ const { request } = await this.publicClient.simulateContract({
243
+ account: this.walletClient.account!,
244
+ address: this.storyFactory,
245
+ abi: storyFactoryAbi,
246
+ functionName: "createStoryline",
247
+ args: [title, contentCid, contentHash, hasDeadline],
248
+ value: creationFee,
249
+ });
250
+
251
+ const txHash = await this.walletClient.writeContract(request);
252
+ const receipt = await this.publicClient.waitForTransactionReceipt({
253
+ hash: txHash,
254
+ });
255
+
256
+ // Decode StorylineCreated event to get the storylineId
257
+ let storylineId = BigInt(0);
258
+ for (const log of receipt.logs) {
259
+ try {
260
+ const decoded = decodeEventLog({
261
+ abi: storyFactoryAbi,
262
+ data: log.data,
263
+ topics: log.topics,
264
+ });
265
+ if (decoded.eventName === "StorylineCreated") {
266
+ storylineId = (decoded.args as { storylineId: bigint }).storylineId;
267
+ break;
268
+ }
269
+ } catch {
270
+ // Skip logs from other contracts
271
+ }
272
+ }
273
+
274
+ return { storylineId, txHash, contentCid };
275
+ }
276
+
277
+ /**
278
+ * Chain a new plot onto an existing storyline.
279
+ *
280
+ * Uploads content to IPFS and calls StoryFactory.chainPlot() on-chain.
281
+ *
282
+ * @param storylineId - The storyline to chain onto
283
+ * @param content - Plot content (plain text)
284
+ * @param title - Optional chapter title (defaults to empty string)
285
+ * @returns Transaction hash and IPFS CID
286
+ */
287
+ async chainPlot(
288
+ storylineId: bigint,
289
+ content: string,
290
+ title = "",
291
+ ): Promise<ChainPlotResult> {
292
+ this.requireFilebase();
293
+ validateNonEmpty("content", content);
294
+ validateContentLength(content);
295
+
296
+ const key = `plotlink/plots/${storylineId}-${Date.now()}.txt`;
297
+ const contentCid = await uploadWithRetry(content, key, this.filebase!);
298
+ const contentHash = hashContent(content);
299
+
300
+ const { request } = await this.publicClient.simulateContract({
301
+ account: this.walletClient.account!,
302
+ address: this.storyFactory,
303
+ abi: storyFactoryAbi,
304
+ functionName: "chainPlot",
305
+ args: [storylineId, title, contentCid, contentHash],
306
+ });
307
+
308
+ const txHash = await this.walletClient.writeContract(request);
309
+ await this.publicClient.waitForTransactionReceipt({ hash: txHash });
310
+
311
+ return { txHash, contentCid };
312
+ }
313
+
314
+ /**
315
+ * Read storyline data from the StorylineCreated event logs.
316
+ *
317
+ * Fetches the creation event for the given storyline ID to retrieve
318
+ * on-chain metadata (title, token address, opening CID, etc.).
319
+ *
320
+ * @param storylineId - The storyline ID to look up
321
+ * @returns Storyline info or null if not found
322
+ */
323
+ async getStoryline(storylineId: bigint): Promise<StorylineInfo | null> {
324
+ const logs = await this.getLogsPaginated({
325
+ address: this.storyFactory,
326
+ event: StorylineCreatedEvent,
327
+ args: { storylineId },
328
+ });
329
+
330
+ if (logs.length === 0) return null;
331
+
332
+ const log = logs[0] as { args: Record<string, unknown> };
333
+ const args = log.args as {
334
+ writer: Address;
335
+ tokenAddress: Address;
336
+ title: string;
337
+ hasDeadline: boolean;
338
+ openingCID: string;
339
+ openingHash: Hex;
340
+ };
341
+
342
+ return {
343
+ creator: args.writer,
344
+ tokenAddress: args.tokenAddress,
345
+ title: args.title,
346
+ hasDeadline: args.hasDeadline,
347
+ openingCID: args.openingCID,
348
+ openingHash: args.openingHash,
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Read all plots for a storyline from PlotChained event logs.
354
+ *
355
+ * @param storylineId - The storyline ID to query
356
+ * @returns Array of plot info objects, ordered by plot index
357
+ */
358
+ async getPlots(storylineId: bigint): Promise<PlotInfo[]> {
359
+ const logs = await this.getLogsPaginated({
360
+ address: this.storyFactory,
361
+ event: PlotChainedEvent,
362
+ args: { storylineId },
363
+ });
364
+
365
+ return logs.map((log) => {
366
+ const args = (log as { args: Record<string, unknown> }).args as {
367
+ storylineId: bigint;
368
+ plotIndex: bigint;
369
+ writer: Address;
370
+ contentCID: string;
371
+ contentHash: Hex;
372
+ };
373
+ return {
374
+ storylineId: args.storylineId,
375
+ plotIndex: args.plotIndex,
376
+ writer: args.writer,
377
+ contentCID: args.contentCID,
378
+ contentHash: args.contentHash,
379
+ };
380
+ });
381
+ }
382
+
383
+ /**
384
+ * Read storyline struct directly from contract storage.
385
+ * Uses readContract instead of getLogs, avoiding RPC block-range limits.
386
+ * Does not include title or openingCID (those are only in event logs).
387
+ */
388
+ async getStorylineStruct(storylineId: bigint): Promise<{
389
+ writer: Address;
390
+ token: Address;
391
+ plotCount: number;
392
+ lastPlotTime: number;
393
+ hasDeadline: boolean;
394
+ } | null> {
395
+ try {
396
+ const result = await this.publicClient.readContract({
397
+ address: this.storyFactory,
398
+ abi: storyFactoryAbi,
399
+ functionName: "storylines",
400
+ args: [storylineId],
401
+ });
402
+ const [writer, token, plotCount, lastPlotTime, hasDeadline] = result as [Address, Address, number, number, boolean];
403
+ return { writer, token, plotCount, lastPlotTime, hasDeadline };
404
+ } catch {
405
+ return null;
406
+ }
407
+ }
408
+
409
+ // -------------------------------------------------------------------------
410
+ // Agent methods
411
+ // -------------------------------------------------------------------------
412
+
413
+ /**
414
+ * Register an AI agent on the ERC-8004 Agent Identity Registry.
415
+ *
416
+ * Constructs a JSON agent URI from the provided metadata and calls
417
+ * `register(agentURI)` on the ERC-8004 registry contract.
418
+ *
419
+ * @param name - Agent display name
420
+ * @param description - Short description of the agent
421
+ * @param genre - Primary genre the agent writes in
422
+ * @param model - LLM model identifier (e.g. "Claude Opus 4")
423
+ * @returns Agent ID and transaction hash
424
+ */
425
+ async registerAgent(
426
+ name: string,
427
+ description: string,
428
+ genre: string,
429
+ model: string,
430
+ ): Promise<RegisterAgentResult> {
431
+ validateNonEmpty("name", name);
432
+ validateNonEmpty("description", description);
433
+ validateNonEmpty("genre", genre);
434
+ validateNonEmpty("model", model);
435
+
436
+ const agentURI = JSON.stringify({ name, description, genre, model });
437
+
438
+ const { request } = await this.publicClient.simulateContract({
439
+ account: this.walletClient.account!,
440
+ address: this.erc8004Registry,
441
+ abi: erc8004Abi,
442
+ functionName: "register",
443
+ args: [agentURI],
444
+ });
445
+
446
+ const txHash = await this.walletClient.writeContract(request);
447
+ const receipt = await this.publicClient.waitForTransactionReceipt({
448
+ hash: txHash,
449
+ });
450
+
451
+ // Decode Registered event to get the agentId
452
+ let agentId = BigInt(0);
453
+ for (const log of receipt.logs) {
454
+ try {
455
+ const decoded = decodeEventLog({
456
+ abi: erc8004Abi,
457
+ data: log.data,
458
+ topics: log.topics,
459
+ });
460
+ if (decoded.eventName === "Registered") {
461
+ agentId = (decoded.args as { agentId: bigint }).agentId;
462
+ break;
463
+ }
464
+ } catch {
465
+ // Skip logs from other contracts
466
+ }
467
+ }
468
+
469
+ return { agentId, txHash };
470
+ }
471
+
472
+ /**
473
+ * Set (or rotate) the wallet for a registered agent.
474
+ *
475
+ * Builds an EIP-712 `AgentWalletSet` signature using the agent wallet's
476
+ * private key and submits the `setAgentWallet` transaction from the SDK's
477
+ * configured (owner) wallet.
478
+ *
479
+ * @param agentId - The on-chain agent ID (from registerAgent)
480
+ * @param newWallet - The new wallet address to assign
481
+ * @param agentWalletPrivateKey - Hex-encoded private key of the agent wallet (signer)
482
+ * @returns Transaction hash
483
+ */
484
+ async setAgentWallet(
485
+ agentId: bigint,
486
+ newWallet: Address,
487
+ agentWalletPrivateKey: string,
488
+ ): Promise<SetAgentWalletResult> {
489
+ const normalizedKey = agentWalletPrivateKey.startsWith("0x")
490
+ ? agentWalletPrivateKey
491
+ : `0x${agentWalletPrivateKey}`;
492
+ const agentAccount = privateKeyToAccount(normalizedKey as Hex);
493
+
494
+ // Deadline: 1 hour from now
495
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
496
+
497
+ const domain = {
498
+ name: "ERC8004IdentityRegistry",
499
+ version: "1",
500
+ chainId: this.chain.id,
501
+ verifyingContract: this.erc8004Registry,
502
+ } as const;
503
+
504
+ const types = {
505
+ AgentWalletSet: [
506
+ { name: "agentId", type: "uint256" },
507
+ { name: "newWallet", type: "address" },
508
+ { name: "owner", type: "address" },
509
+ { name: "deadline", type: "uint256" },
510
+ ],
511
+ } as const;
512
+
513
+ const message = {
514
+ agentId,
515
+ newWallet,
516
+ owner: this.address,
517
+ deadline,
518
+ } as const;
519
+
520
+ const signature = await agentAccount.signTypedData({
521
+ domain,
522
+ types,
523
+ primaryType: "AgentWalletSet",
524
+ message,
525
+ });
526
+
527
+ const { request } = await this.publicClient.simulateContract({
528
+ account: this.walletClient.account!,
529
+ address: this.erc8004Registry,
530
+ abi: erc8004Abi,
531
+ functionName: "setAgentWallet",
532
+ args: [agentId, newWallet, deadline, signature],
533
+ });
534
+
535
+ const txHash = await this.walletClient.writeContract(request);
536
+ await this.publicClient.waitForTransactionReceipt({ hash: txHash });
537
+
538
+ return { txHash };
539
+ }
540
+
541
+ // -------------------------------------------------------------------------
542
+ // Royalty methods
543
+ // -------------------------------------------------------------------------
544
+
545
+ /**
546
+ * Get royalty info for a beneficiary on a given reserve token.
547
+ *
548
+ * @param beneficiary - The royalty beneficiary (usually the bond creator)
549
+ * @param reserveToken - The reserve token address (e.g. WETH on testnet, $PLOT on mainnet)
550
+ * @returns Balance (unclaimed) and claimed royalty amounts
551
+ */
552
+ async getRoyaltyInfo(beneficiary: Address, reserveToken: Address): Promise<RoyaltyInfo> {
553
+ const [balance, claimed] = await this.publicClient.readContract({
554
+ address: this.mcv2Bond,
555
+ abi: mcv2BondAbi,
556
+ functionName: "getRoyaltyInfo",
557
+ args: [beneficiary, reserveToken],
558
+ }) as [bigint, bigint];
559
+
560
+ return { balance, claimed };
561
+ }
562
+
563
+ /**
564
+ * Claim accumulated royalties for a reserve token from the MCV2_Bond
565
+ * bonding curve contract.
566
+ *
567
+ * @param reserveToken - The reserve token address (e.g. WETH on testnet, $PLOT on mainnet)
568
+ * @returns Transaction hash
569
+ */
570
+ async claimRoyalties(reserveToken: Address): Promise<Hex> {
571
+ const { request } = await this.publicClient.simulateContract({
572
+ account: this.walletClient.account!,
573
+ address: this.mcv2Bond,
574
+ abi: mcv2BondAbi,
575
+ functionName: "claimRoyalties",
576
+ args: [reserveToken],
577
+ });
578
+
579
+ const txHash = await this.walletClient.writeContract(request);
580
+ await this.publicClient.waitForTransactionReceipt({ hash: txHash });
581
+
582
+ return txHash;
583
+ }
584
+
585
+ // -------------------------------------------------------------------------
586
+ // Price methods
587
+ // -------------------------------------------------------------------------
588
+
589
+ /**
590
+ * Get the current bonding-curve price for a storyline token.
591
+ *
592
+ * Calls MCV2_Bond.priceForNextMint() to get the cost (in reserve token)
593
+ * to mint 1 unit of the given storyline token.
594
+ *
595
+ * @param tokenAddress - The storyline's ERC-20 token address
596
+ * @returns Price info or null if the token has no bond / query fails
597
+ */
598
+ async getTokenPrice(tokenAddress: Address): Promise<TokenPriceInfo | null> {
599
+ try {
600
+ const result = await this.publicClient.readContract({
601
+ address: this.mcv2Bond,
602
+ abi: mcv2BondAbi,
603
+ functionName: "priceForNextMint",
604
+ args: [tokenAddress],
605
+ });
606
+
607
+ const priceRaw = BigInt(result as bigint);
608
+ return {
609
+ priceRaw,
610
+ priceFormatted: formatUnits(priceRaw, 18),
611
+ };
612
+ } catch {
613
+ return null;
614
+ }
615
+ }
616
+
617
+ // -------------------------------------------------------------------------
618
+ // Internal helpers
619
+ // -------------------------------------------------------------------------
620
+
621
+ private requireFilebase(): void {
622
+ if (!this.filebase) {
623
+ throw new Error(
624
+ "Filebase config required for IPFS uploads. " +
625
+ "Pass { filebase: { accessKey, secretKey, bucket } } to the PlotLink constructor.",
626
+ );
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Paginated getLogs that chunks requests into RPC-safe ranges.
632
+ * Public RPCs typically limit eth_getLogs to 10,000 blocks per request.
633
+ */
634
+ private async getLogsPaginated(params: {
635
+ address: Address;
636
+ event: (typeof storyFactoryAbi)[number] & { type: "event" };
637
+ args?: Record<string, unknown>;
638
+ }): Promise<unknown[]> {
639
+ const MAX_RANGE = BigInt(9_999);
640
+ const latestBlock = await this.publicClient.getBlockNumber();
641
+ const from = this.deploymentBlock;
642
+ const allLogs: unknown[] = [];
643
+
644
+ for (let start = from; start <= latestBlock; start += MAX_RANGE + 1n) {
645
+ const end = start + MAX_RANGE > latestBlock ? latestBlock : start + MAX_RANGE;
646
+ const logs = await this.publicClient.getLogs({
647
+ address: params.address,
648
+ event: params.event,
649
+ args: params.args,
650
+ fromBlock: start,
651
+ toBlock: end,
652
+ } as Parameters<typeof this.publicClient.getLogs>[0]);
653
+ allLogs.push(...logs);
654
+ }
655
+
656
+ return allLogs;
657
+ }
658
+ }
659
+
660
+ // ---------------------------------------------------------------------------
661
+ // Utility functions
662
+ // ---------------------------------------------------------------------------
663
+
664
+ /**
665
+ * Validate that a required string parameter is non-empty.
666
+ */
667
+ function validateNonEmpty(name: string, value: string): void {
668
+ if (typeof value !== "string" || value.trim().length === 0) {
669
+ throw new Error(`"${name}" must be a non-empty string.`);
670
+ }
671
+ }
672
+
673
+ // Content limits — mirrored from lib/content.ts in the web app
674
+ const MAX_TITLE_LENGTH = 60;
675
+ const MIN_CONTENT_LENGTH = 500;
676
+ const MAX_CONTENT_LENGTH = 10_000;
677
+
678
+ function validateTitle(title: string): void {
679
+ const charCount = [...title].length;
680
+ if (charCount > MAX_TITLE_LENGTH) {
681
+ throw new Error(
682
+ `Title must be ${MAX_TITLE_LENGTH} characters or less (currently: ${charCount})`,
683
+ );
684
+ }
685
+ }
686
+
687
+ function validateContentLength(content: string): void {
688
+ const charCount = [...content].length;
689
+ if (charCount < MIN_CONTENT_LENGTH || charCount > MAX_CONTENT_LENGTH) {
690
+ throw new Error(
691
+ `Content must be between ${MIN_CONTENT_LENGTH} and ${MAX_CONTENT_LENGTH} characters (currently: ${charCount})`,
692
+ );
693
+ }
694
+ }
695
+
696
+ /**
697
+ * Compute keccak256 hash of content, matching the on-chain contentHash.
698
+ * Same encoding as the web app's hashContent (lib/content.ts).
699
+ */
700
+ function hashContent(content: string): Hex {
701
+ return keccak256(toHex(content));
702
+ }
703
+
704
+ /**
705
+ * Simple slugify for S3 keys.
706
+ */
707
+ function slugify(text: string): string {
708
+ return text
709
+ .toLowerCase()
710
+ .replace(/[^a-z0-9]+/g, "-")
711
+ .replace(/^-|-$/g, "")
712
+ .slice(0, 40);
713
+ }