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.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/app/db.ts +8 -0
- package/app/lib/llm-client.ts +265 -0
- package/app/lib/paths.ts +11 -0
- package/app/lib/publish.ts +204 -0
- package/app/lib/writer-prompt.ts +44 -0
- package/app/node_modules/.prisma/local-client/client.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/client.js +5 -0
- package/app/node_modules/.prisma/local-client/default.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/default.js +5 -0
- package/app/node_modules/.prisma/local-client/edge.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/edge.js +184 -0
- package/app/node_modules/.prisma/local-client/index-browser.js +173 -0
- package/app/node_modules/.prisma/local-client/index.d.ts +3304 -0
- package/app/node_modules/.prisma/local-client/index.js +207 -0
- package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/app/node_modules/.prisma/local-client/package.json +183 -0
- package/app/node_modules/.prisma/local-client/query_engine_bg.js +2 -0
- package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
- package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +35 -0
- package/app/node_modules/.prisma/local-client/runtime/edge.js +35 -0
- package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +370 -0
- package/app/node_modules/.prisma/local-client/runtime/index-browser.js +17 -0
- package/app/node_modules/.prisma/local-client/runtime/library.d.ts +3982 -0
- package/app/node_modules/.prisma/local-client/runtime/library.js +147 -0
- package/app/node_modules/.prisma/local-client/runtime/react-native.js +84 -0
- package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +85 -0
- package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +38 -0
- package/app/node_modules/.prisma/local-client/schema.prisma +21 -0
- package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +5 -0
- package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +5 -0
- package/app/node_modules/.prisma/local-client/wasm.d.ts +1 -0
- package/app/node_modules/.prisma/local-client/wasm.js +191 -0
- package/app/prisma/schema.prisma +57 -0
- package/app/routes/auth.ts +173 -0
- package/app/routes/chat.ts +135 -0
- package/app/routes/config.ts +210 -0
- package/app/routes/dashboard.ts +186 -0
- package/app/routes/oauth.ts +150 -0
- package/app/routes/publish.ts +112 -0
- package/app/routes/wallet.ts +99 -0
- package/app/server.ts +154 -0
- package/app/vite.config.ts +19 -0
- package/app/web/App.tsx +102 -0
- package/app/web/components/Chat.tsx +272 -0
- package/app/web/components/Dashboard.tsx +222 -0
- package/app/web/components/LLMSetup.tsx +291 -0
- package/app/web/components/Layout.tsx +235 -0
- package/app/web/components/Login.tsx +62 -0
- package/app/web/components/Publish.tsx +245 -0
- package/app/web/components/Settings.tsx +175 -0
- package/app/web/components/Setup.tsx +84 -0
- package/app/web/components/WalletCard.tsx +117 -0
- package/app/web/dist/assets/index-C9kXlYO_.css +2 -0
- package/app/web/dist/assets/index-CJiiaLHs.js +9 -0
- package/app/web/dist/index.html +16 -0
- package/app/web/index.html +15 -0
- package/app/web/main.tsx +10 -0
- package/app/web/plotlink-logo.svg +5 -0
- package/app/web/styles.css +51 -0
- package/bin/plotlink-ows.js +394 -0
- package/lib/ows/index.ts +3 -0
- package/lib/ows/policy.ts +68 -0
- package/lib/ows/types.ts +14 -0
- package/lib/ows/wallet.ts +70 -0
- package/package.json +79 -0
- package/packages/cli/node_modules/commander/LICENSE +22 -0
- package/packages/cli/node_modules/commander/Readme.md +1149 -0
- package/packages/cli/node_modules/commander/esm.mjs +16 -0
- package/packages/cli/node_modules/commander/index.js +24 -0
- package/packages/cli/node_modules/commander/lib/argument.js +149 -0
- package/packages/cli/node_modules/commander/lib/command.js +2662 -0
- package/packages/cli/node_modules/commander/lib/error.js +39 -0
- package/packages/cli/node_modules/commander/lib/help.js +709 -0
- package/packages/cli/node_modules/commander/lib/option.js +367 -0
- package/packages/cli/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/packages/cli/node_modules/commander/package-support.json +16 -0
- package/packages/cli/node_modules/commander/package.json +82 -0
- package/packages/cli/node_modules/commander/typings/esm.d.mts +3 -0
- package/packages/cli/node_modules/commander/typings/index.d.ts +1045 -0
- package/packages/cli/node_modules/resolve-from/index.d.ts +31 -0
- package/packages/cli/node_modules/resolve-from/index.js +47 -0
- package/packages/cli/node_modules/resolve-from/license +9 -0
- package/packages/cli/node_modules/resolve-from/package.json +36 -0
- package/packages/cli/node_modules/resolve-from/readme.md +72 -0
- package/packages/cli/node_modules/tsup/LICENSE +21 -0
- package/packages/cli/node_modules/tsup/README.md +75 -0
- package/packages/cli/node_modules/tsup/assets/cjs_shims.js +13 -0
- package/packages/cli/node_modules/tsup/assets/esm_shims.js +9 -0
- package/packages/cli/node_modules/tsup/assets/package.json +3 -0
- package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +153 -0
- package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +42 -0
- package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +6 -0
- package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +352 -0
- package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +203 -0
- package/packages/cli/node_modules/tsup/dist/cli-default.js +12 -0
- package/packages/cli/node_modules/tsup/dist/cli-main.js +8 -0
- package/packages/cli/node_modules/tsup/dist/cli-node.js +14 -0
- package/packages/cli/node_modules/tsup/dist/index.d.ts +511 -0
- package/packages/cli/node_modules/tsup/dist/index.js +1711 -0
- package/packages/cli/node_modules/tsup/dist/rollup.js +6949 -0
- package/packages/cli/node_modules/tsup/package.json +99 -0
- package/packages/cli/node_modules/tsup/schema.json +362 -0
- package/packages/cli/package.json +35 -0
- package/packages/cli/src/commands/agent-register.ts +77 -0
- package/packages/cli/src/commands/chain.ts +29 -0
- package/packages/cli/src/commands/claim.ts +70 -0
- package/packages/cli/src/commands/create.ts +34 -0
- package/packages/cli/src/commands/status.ts +201 -0
- package/packages/cli/src/config.ts +103 -0
- package/packages/cli/src/index.ts +21 -0
- package/packages/cli/src/sdk/abi.ts +222 -0
- package/packages/cli/src/sdk/client.ts +713 -0
- package/packages/cli/src/sdk/constants.ts +56 -0
- package/packages/cli/src/sdk/index.ts +46 -0
- package/packages/cli/src/sdk/ipfs.ts +88 -0
- package/packages/cli/src/sdk.ts +36 -0
- package/packages/cli/tsconfig.json +20 -0
- package/packages/cli/tsup.config.ts +14 -0
- package/public/.well-known/farcaster.json +38 -0
- package/public/basescan-icon.svg +4 -0
- package/public/embed-image.png +0 -0
- package/public/favicon.png +0 -0
- package/public/hunt-token.svg +11 -0
- package/public/icon-192.png +0 -0
- package/public/icon.png +0 -0
- package/public/manifest.json +26 -0
- package/public/mc-icon-light.svg +12 -0
- package/public/og-image.png +0 -0
- package/public/plotlink-logo-symbol.svg +5 -0
- package/public/plotlink-logo.svg +5 -0
- package/public/screenshot-1.png +0 -0
- package/public/screenshot-2.png +0 -0
- package/public/screenshot-3.png +0 -0
- package/public/splash.png +0 -0
- package/public/wide-banner.png +0 -0
- package/scripts/backfill-trade-prices.ts +97 -0
- package/scripts/backfill-usd-rates.ts +220 -0
- package/scripts/e2e-verify.ts +1100 -0
- package/scripts/ows-smoke-test.ts +37 -0
- 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
|
+
}
|