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,1100 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* E2E Indexer Verification Script
|
|
4
|
+
*
|
|
5
|
+
* Validates that the PlotLink web app correctly indexes every mainnet
|
|
6
|
+
* transaction produced by the contract E2E test (plotlink-contracts#27).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx tsx scripts/e2e-verify.ts --from-file ../plotlink-contracts/e2e-results.json
|
|
10
|
+
*
|
|
11
|
+
* Requires environment variables:
|
|
12
|
+
* NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
|
|
13
|
+
* NEXT_PUBLIC_APP_URL (defaults to http://localhost:3000)
|
|
14
|
+
* NEXT_PUBLIC_CHAIN_ID (defaults to 84532)
|
|
15
|
+
* NEXT_PUBLIC_RPC_URL (optional)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { resolve, dirname } from "node:path";
|
|
20
|
+
import { createClient } from "@supabase/supabase-js";
|
|
21
|
+
import { keccak256, toHex, formatUnits, decodeEventLog, type Address } from "viem";
|
|
22
|
+
import { base, baseSepolia } from "viem/chains";
|
|
23
|
+
import { publicClient } from "../lib/rpc";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// CLI args
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
const fromFileIdx = args.indexOf("--from-file");
|
|
31
|
+
if (fromFileIdx === -1 || !args[fromFileIdx + 1]) {
|
|
32
|
+
console.error("Usage: npx tsx scripts/e2e-verify.ts --from-file <path-to-e2e-results.json>");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const resultsPath = resolve(args[fromFileIdx + 1]);
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Config
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
|
42
|
+
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
|
43
|
+
const SUPABASE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || "";
|
|
44
|
+
|
|
45
|
+
if (!SUPABASE_URL || !SUPABASE_KEY) {
|
|
46
|
+
console.error("Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
|
|
51
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// MCV2 Bond ABI (minimal for price/TVL reads)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
const mcv2BondAbi = [
|
|
59
|
+
{
|
|
60
|
+
type: "function" as const,
|
|
61
|
+
name: "priceForNextMint" as const,
|
|
62
|
+
stateMutability: "view" as const,
|
|
63
|
+
inputs: [{ name: "token", type: "address" }],
|
|
64
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: "function" as const,
|
|
68
|
+
name: "tokenBond" as const,
|
|
69
|
+
stateMutability: "view" as const,
|
|
70
|
+
inputs: [{ name: "token", type: "address" }],
|
|
71
|
+
outputs: [
|
|
72
|
+
{ name: "creator", type: "address" },
|
|
73
|
+
{ name: "token", type: "address" },
|
|
74
|
+
{ name: "priceForNextMint_", type: "uint256" },
|
|
75
|
+
{ name: "mintRoyalty", type: "uint256" },
|
|
76
|
+
{ name: "reserveToken", type: "address" },
|
|
77
|
+
{ name: "reserveBalance", type: "uint256" },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
] as const;
|
|
81
|
+
|
|
82
|
+
const erc20Abi = [
|
|
83
|
+
{
|
|
84
|
+
type: "function" as const,
|
|
85
|
+
name: "totalSupply" as const,
|
|
86
|
+
stateMutability: "view" as const,
|
|
87
|
+
inputs: [],
|
|
88
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: "function" as const,
|
|
92
|
+
name: "decimals" as const,
|
|
93
|
+
stateMutability: "view" as const,
|
|
94
|
+
inputs: [],
|
|
95
|
+
outputs: [{ name: "", type: "uint8" }],
|
|
96
|
+
},
|
|
97
|
+
] as const;
|
|
98
|
+
|
|
99
|
+
const storylineCreatedAbi = [
|
|
100
|
+
{
|
|
101
|
+
type: "event" as const,
|
|
102
|
+
name: "StorylineCreated" as const,
|
|
103
|
+
inputs: [
|
|
104
|
+
{ name: "storylineId", type: "uint256", indexed: true },
|
|
105
|
+
{ name: "writer", type: "address", indexed: true },
|
|
106
|
+
{ name: "tokenAddress", type: "address", indexed: false },
|
|
107
|
+
{ name: "title", type: "string", indexed: false },
|
|
108
|
+
{ name: "hasDeadline", type: "bool", indexed: false },
|
|
109
|
+
{ name: "openingCID", type: "string", indexed: false },
|
|
110
|
+
{ name: "openingHash", type: "bytes32", indexed: false },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
] as const;
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Load e2e-results.json and broadcast artifact
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
interface E2EResults {
|
|
121
|
+
deployer: string;
|
|
122
|
+
donor: string;
|
|
123
|
+
factory: string;
|
|
124
|
+
plTest: string;
|
|
125
|
+
bond: string;
|
|
126
|
+
chainId: number;
|
|
127
|
+
broadcastArtifact: string;
|
|
128
|
+
scenariosPassed: number;
|
|
129
|
+
gasUsed: number;
|
|
130
|
+
storylineA1: { storylineId: number; token: string; plotCount: number; hasDeadline: boolean };
|
|
131
|
+
storylineA2: { storylineId: number; token: string; plotCount: number; hasDeadline: boolean };
|
|
132
|
+
storylineA3: { storylineId: number; token: string };
|
|
133
|
+
tradingB: {
|
|
134
|
+
b1Cost: number; b2Cost: number; b3Cost: number;
|
|
135
|
+
b4Refund: number; b5Refund: number;
|
|
136
|
+
};
|
|
137
|
+
edgeCasesF: { f1StorylineId: number; f1Token: string; f2StorylineId: number; f3StorylineId: number };
|
|
138
|
+
royaltiesClaimed?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface BroadcastTx {
|
|
142
|
+
hash: string;
|
|
143
|
+
transactionType: string;
|
|
144
|
+
contractName: string | null;
|
|
145
|
+
contractAddress: string | null;
|
|
146
|
+
function: string | null;
|
|
147
|
+
arguments: string[] | null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface BroadcastArtifact {
|
|
151
|
+
transactions: BroadcastTx[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const results: E2EResults = JSON.parse(readFileSync(resultsPath, "utf-8"));
|
|
155
|
+
const artifactPath = resolve(dirname(resultsPath), results.broadcastArtifact);
|
|
156
|
+
|
|
157
|
+
// Chain from e2e-results.json (for display only — publicClient uses env config)
|
|
158
|
+
const chainId = results.chainId;
|
|
159
|
+
const resolvedChain = chainId === 8453 ? base : baseSepolia;
|
|
160
|
+
|
|
161
|
+
let broadcast: BroadcastArtifact;
|
|
162
|
+
try {
|
|
163
|
+
broadcast = JSON.parse(readFileSync(artifactPath, "utf-8"));
|
|
164
|
+
} catch {
|
|
165
|
+
console.error(`Failed to read broadcast artifact at: ${artifactPath}`);
|
|
166
|
+
console.error("Run the contract E2E test first to generate broadcast artifacts.");
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Extract tx hashes by function signature from broadcast
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
function findTxByFunction(fnPrefix: string): BroadcastTx[] {
|
|
175
|
+
return broadcast.transactions.filter(
|
|
176
|
+
(tx) => tx.function && tx.function.startsWith(fnPrefix)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function findAllTxByFunction(fnPrefix: string): string[] {
|
|
181
|
+
return findTxByFunction(fnPrefix).map((tx) => tx.hash);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Map contract functions to tx hashes
|
|
185
|
+
const createStorylineTxs = findAllTxByFunction("createStoryline");
|
|
186
|
+
const chainPlotTxs = findAllTxByFunction("chainPlot");
|
|
187
|
+
const mintTxs = findAllTxByFunction("mint");
|
|
188
|
+
const burnTxs = findAllTxByFunction("burn");
|
|
189
|
+
const donateTxs = findAllTxByFunction("donate");
|
|
190
|
+
const tradeTxs = [...mintTxs, ...burnTxs];
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Resolve actual on-chain IDs/tokens from broadcast receipts
|
|
194
|
+
// The e2e-results.json contains simulated values that may diverge from
|
|
195
|
+
// broadcast reality (forge simulation vs actual nonce/state).
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
interface ResolvedStoryline {
|
|
199
|
+
storylineId: number;
|
|
200
|
+
tokenAddress: string;
|
|
201
|
+
writer: string;
|
|
202
|
+
title: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function resolveStorylinesFromReceipts(): Promise<ResolvedStoryline[]> {
|
|
206
|
+
const resolved: ResolvedStoryline[] = [];
|
|
207
|
+
for (const txHash of createStorylineTxs) {
|
|
208
|
+
try {
|
|
209
|
+
const receipt = await publicClient.getTransactionReceipt({ hash: txHash as `0x${string}` });
|
|
210
|
+
for (const log of receipt.logs) {
|
|
211
|
+
try {
|
|
212
|
+
const decoded = decodeEventLog({
|
|
213
|
+
abi: storylineCreatedAbi,
|
|
214
|
+
data: log.data,
|
|
215
|
+
topics: log.topics,
|
|
216
|
+
});
|
|
217
|
+
if (decoded.eventName === "StorylineCreated") {
|
|
218
|
+
resolved.push({
|
|
219
|
+
storylineId: Number(decoded.args.storylineId),
|
|
220
|
+
tokenAddress: decoded.args.tokenAddress.toLowerCase(),
|
|
221
|
+
writer: decoded.args.writer.toLowerCase(),
|
|
222
|
+
title: decoded.args.title,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// not a matching event
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// receipt fetch failed
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return resolved;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Resolve before running tests — override e2e-results with real on-chain data
|
|
237
|
+
let resolvedStorylines: ResolvedStoryline[] = [];
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Test runner
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
let passed = 0;
|
|
244
|
+
let failed = 0;
|
|
245
|
+
|
|
246
|
+
function pass(id: string, message: string, detail = "") {
|
|
247
|
+
const detailStr = detail ? ` ${detail}` : "";
|
|
248
|
+
console.log(`[${id}] ${message.padEnd(40)} PASS${detailStr}`);
|
|
249
|
+
passed++;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function fail(id: string, message: string, reason: string) {
|
|
253
|
+
console.log(`[${id}] ${message.padEnd(40)} FAIL ${reason}`);
|
|
254
|
+
failed++;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function postIndex(
|
|
258
|
+
endpoint: string,
|
|
259
|
+
body: Record<string, unknown>,
|
|
260
|
+
): Promise<{ status: number; data: Record<string, unknown> }> {
|
|
261
|
+
const res = await fetch(`${APP_URL}${endpoint}`, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: { "Content-Type": "application/json" },
|
|
264
|
+
body: JSON.stringify(body),
|
|
265
|
+
});
|
|
266
|
+
let data: Record<string, unknown> = {};
|
|
267
|
+
try {
|
|
268
|
+
data = await res.json();
|
|
269
|
+
} catch {
|
|
270
|
+
// empty response
|
|
271
|
+
}
|
|
272
|
+
return { status: res.status, data };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function hashContent(content: string): `0x${string}` {
|
|
276
|
+
return keccak256(toHex(content));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Known E2E test content strings and their keccak256 hashes.
|
|
280
|
+
// The contract E2E uses these as openingHash/contentHash arguments.
|
|
281
|
+
// When IPFS fetch fails (test CIDs don't resolve), we provide the matching
|
|
282
|
+
// content as fallback so the indexer can verify the hash.
|
|
283
|
+
const E2E_CONTENT_STRINGS = [
|
|
284
|
+
"e2e genesis content",
|
|
285
|
+
"e2e chapter 2",
|
|
286
|
+
"e2e chapter 3",
|
|
287
|
+
"e2e chapter 4",
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* POST to an indexer endpoint with fallback content retry.
|
|
292
|
+
* First tries without content. If that fails (IPFS unavailable or hash mismatch),
|
|
293
|
+
* retries with each known E2E content string until one matches.
|
|
294
|
+
*/
|
|
295
|
+
async function postIndexWithFallback(
|
|
296
|
+
endpoint: string,
|
|
297
|
+
body: Record<string, unknown>,
|
|
298
|
+
): Promise<{ status: number; data: Record<string, unknown> }> {
|
|
299
|
+
// First attempt without fallback content
|
|
300
|
+
const first = await postIndex(endpoint, body);
|
|
301
|
+
if (first.status === 200) return first;
|
|
302
|
+
|
|
303
|
+
// Retry with each known content string as fallback
|
|
304
|
+
for (const content of E2E_CONTENT_STRINGS) {
|
|
305
|
+
const retry = await postIndex(endpoint, { ...body, content });
|
|
306
|
+
if (retry.status === 200) return retry;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Return the original failure
|
|
310
|
+
return first;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// V1: Storyline Indexing
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
async function verifyV1() {
|
|
318
|
+
console.log("");
|
|
319
|
+
console.log("=== V1: Storyline Indexing ===");
|
|
320
|
+
|
|
321
|
+
if (createStorylineTxs.length === 0) {
|
|
322
|
+
fail("V1.1", "No createStoryline txs found", "broadcast artifact missing storyline txs");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Index all createStoryline txs
|
|
327
|
+
for (let i = 0; i < createStorylineTxs.length; i++) {
|
|
328
|
+
const txHash = createStorylineTxs[i];
|
|
329
|
+
const { status } = await postIndexWithFallback("/api/index/storyline", { txHash });
|
|
330
|
+
if (status === 200) {
|
|
331
|
+
pass("V1.1", `POST /api/index/storyline (tx ${i + 1})`, `${status} OK`);
|
|
332
|
+
} else {
|
|
333
|
+
fail("V1.1", `POST /api/index/storyline (tx ${i + 1})`, `status=${status}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Verify storyline A1
|
|
338
|
+
const a1 = results.storylineA1;
|
|
339
|
+
const { data: s1 } = await supabase
|
|
340
|
+
.from("storylines")
|
|
341
|
+
.select("*")
|
|
342
|
+
.eq("storyline_id", a1.storylineId)
|
|
343
|
+
.single();
|
|
344
|
+
|
|
345
|
+
if (!s1) {
|
|
346
|
+
fail("V1.2", "Supabase record exists (A1)", "not found");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
pass("V1.2", "Supabase record exists (A1)");
|
|
350
|
+
|
|
351
|
+
// V1.3: writer_address
|
|
352
|
+
if (s1.writer_address === results.deployer.toLowerCase()) {
|
|
353
|
+
pass("V1.3", "writer_address matches", s1.writer_address.slice(0, 10) + "...");
|
|
354
|
+
} else {
|
|
355
|
+
fail("V1.3", "writer_address matches", `expected ${results.deployer}, got ${s1.writer_address}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// V1.4: token_address non-zero
|
|
359
|
+
if (s1.token_address && s1.token_address !== "0x0000000000000000000000000000000000000000") {
|
|
360
|
+
pass("V1.4", "token_address non-zero", s1.token_address.slice(0, 10) + "...");
|
|
361
|
+
} else {
|
|
362
|
+
fail("V1.4", "token_address non-zero", `got ${s1.token_address}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// V1.5: title matches
|
|
366
|
+
if (s1.title === "E2E Story Alpha") {
|
|
367
|
+
pass("V1.5", "title matches", `"${s1.title}"`);
|
|
368
|
+
} else {
|
|
369
|
+
fail("V1.5", "title matches", `expected "E2E Story Alpha", got "${s1.title}"`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// V1.6: has_deadline
|
|
373
|
+
if (s1.has_deadline === a1.hasDeadline) {
|
|
374
|
+
pass("V1.6", "has_deadline matches", `${s1.has_deadline}`);
|
|
375
|
+
} else {
|
|
376
|
+
fail("V1.6", "has_deadline matches", `expected ${a1.hasDeadline}, got ${s1.has_deadline}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// V1.7: plot_count = 1 (before chainPlot indexing)
|
|
380
|
+
// Note: after storyline indexing, genesis plot is included, so plot_count = 1
|
|
381
|
+
if (s1.plot_count === 1) {
|
|
382
|
+
pass("V1.7", "plot_count = 1 (genesis only)");
|
|
383
|
+
} else {
|
|
384
|
+
fail("V1.7", "plot_count = 1 (genesis only)", `got ${s1.plot_count}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// V1.8: block_timestamp is valid ISO date
|
|
388
|
+
if (s1.block_timestamp && !isNaN(Date.parse(s1.block_timestamp))) {
|
|
389
|
+
pass("V1.8", "block_timestamp valid ISO date", s1.block_timestamp);
|
|
390
|
+
} else {
|
|
391
|
+
fail("V1.8", "block_timestamp valid ISO date", `got ${s1.block_timestamp}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// V1.9: tx_hash and log_index stored
|
|
395
|
+
if (s1.tx_hash && s1.log_index != null) {
|
|
396
|
+
pass("V1.9", "tx_hash and log_index present", `${s1.tx_hash.slice(0, 10)}... log=${s1.log_index}`);
|
|
397
|
+
} else {
|
|
398
|
+
fail("V1.9", "tx_hash and log_index present", `tx_hash=${s1.tx_hash}, log_index=${s1.log_index}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// V1.10: writer_type = 0 (deployer is not a registered agent)
|
|
402
|
+
if (s1.writer_type === 0) {
|
|
403
|
+
pass("V1.10", "writer_type = 0 (human)");
|
|
404
|
+
} else {
|
|
405
|
+
fail("V1.10", "writer_type = 0 (human)", `got ${s1.writer_type}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Verify storyline A2 exists too
|
|
409
|
+
const { data: s2 } = await supabase
|
|
410
|
+
.from("storylines")
|
|
411
|
+
.select("storyline_id, title, has_deadline")
|
|
412
|
+
.eq("storyline_id", results.storylineA2.storylineId)
|
|
413
|
+
.single();
|
|
414
|
+
|
|
415
|
+
if (s2 && s2.title === "E2E Story Beta" && s2.has_deadline === false) {
|
|
416
|
+
pass("V1.2", "Supabase record exists (A2)", `"${s2.title}" hasDeadline=${s2.has_deadline}`);
|
|
417
|
+
} else {
|
|
418
|
+
fail("V1.2", "Supabase record exists (A2)", `not found or field mismatch`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Verify storyline A3 (multiple storylines per writer)
|
|
422
|
+
const { data: s3 } = await supabase
|
|
423
|
+
.from("storylines")
|
|
424
|
+
.select("storyline_id, title, token_address, writer_address")
|
|
425
|
+
.eq("storyline_id", results.storylineA3.storylineId)
|
|
426
|
+
.single();
|
|
427
|
+
|
|
428
|
+
if (s3) {
|
|
429
|
+
pass("V1.2", "Supabase record exists (A3)", `"${s3.title}"`);
|
|
430
|
+
if (s3.token_address && s3.token_address !== s1?.token_address) {
|
|
431
|
+
pass("V1.4", "A3 token unique from A1", s3.token_address.slice(0, 10) + "...");
|
|
432
|
+
} else {
|
|
433
|
+
fail("V1.4", "A3 token unique from A1", `same or missing`);
|
|
434
|
+
}
|
|
435
|
+
if (s3.writer_address === results.deployer.toLowerCase()) {
|
|
436
|
+
pass("V1.3", "A3 writer matches deployer", "same wallet, multiple storylines");
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
fail("V1.2", "Supabase record exists (A3)", "not found");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Verify edge case storylines (F1, F2, F3)
|
|
443
|
+
const edgeCases = results.edgeCasesF;
|
|
444
|
+
for (const [label, id] of [
|
|
445
|
+
["F1 (min CID)", edgeCases.f1StorylineId],
|
|
446
|
+
["F2 (max CID)", edgeCases.f2StorylineId],
|
|
447
|
+
["F3 (zero fee)", edgeCases.f3StorylineId],
|
|
448
|
+
] as const) {
|
|
449
|
+
const { data: sf } = await supabase
|
|
450
|
+
.from("storylines")
|
|
451
|
+
.select("storyline_id, title")
|
|
452
|
+
.eq("storyline_id", id)
|
|
453
|
+
.single();
|
|
454
|
+
|
|
455
|
+
if (sf) {
|
|
456
|
+
pass("V1.2", `Supabase record exists (${label})`, `id=${sf.storyline_id} "${sf.title}"`);
|
|
457
|
+
} else {
|
|
458
|
+
fail("V1.2", `Supabase record exists (${label})`, `storyline_id=${id} not found`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// V2: Plot Indexing
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
async function verifyV2() {
|
|
468
|
+
console.log("");
|
|
469
|
+
console.log("=== V2: Plot Indexing ===");
|
|
470
|
+
|
|
471
|
+
if (chainPlotTxs.length === 0) {
|
|
472
|
+
fail("V2.1", "No chainPlot txs found", "broadcast artifact missing plot txs");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Index all chainPlot txs
|
|
477
|
+
for (let i = 0; i < chainPlotTxs.length; i++) {
|
|
478
|
+
const txHash = chainPlotTxs[i];
|
|
479
|
+
const { status } = await postIndexWithFallback("/api/index/plot", { txHash });
|
|
480
|
+
if (status === 200) {
|
|
481
|
+
pass("V2.1", `POST /api/index/plot (tx ${i + 1})`, `${status} OK`);
|
|
482
|
+
} else {
|
|
483
|
+
fail("V2.1", `POST /api/index/plot (tx ${i + 1})`, `status=${status}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// V2.2: Query plots for storyline A1 (should have genesis + 3 chained = plot_index 0-3)
|
|
488
|
+
const a1Id = results.storylineA1.storylineId;
|
|
489
|
+
const { data: plots } = await supabase
|
|
490
|
+
.from("plots")
|
|
491
|
+
.select("*")
|
|
492
|
+
.eq("storyline_id", a1Id)
|
|
493
|
+
.order("plot_index", { ascending: true });
|
|
494
|
+
|
|
495
|
+
if (!plots || plots.length === 0) {
|
|
496
|
+
fail("V2.2", "Plots exist for A1", "no plots found");
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// V2.2: Record exists for each plot
|
|
501
|
+
for (const plot of plots) {
|
|
502
|
+
pass("V2.2", `Plot record exists (idx=${plot.plot_index})`, `storyline=${a1Id}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// V2.3: content_cid present
|
|
506
|
+
for (const plot of plots) {
|
|
507
|
+
if (plot.content_cid && plot.content_cid.length >= 46) {
|
|
508
|
+
pass("V2.3", `content_cid present (idx=${plot.plot_index})`, plot.content_cid.slice(0, 20) + "...");
|
|
509
|
+
} else {
|
|
510
|
+
fail("V2.3", `content_cid present (idx=${plot.plot_index})`, `got "${plot.content_cid}"`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// V2.4: content_hash present and valid hex
|
|
515
|
+
for (const plot of plots) {
|
|
516
|
+
if (plot.content_hash && /^0x[0-9a-fA-F]{64}$/.test(plot.content_hash)) {
|
|
517
|
+
pass("V2.4", `content_hash valid (idx=${plot.plot_index})`, plot.content_hash.slice(0, 14) + "...");
|
|
518
|
+
} else {
|
|
519
|
+
fail("V2.4", `content_hash valid (idx=${plot.plot_index})`, `got "${plot.content_hash}"`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// V2.5: content field non-empty (at least for genesis)
|
|
524
|
+
const genesisPlot = plots.find((p) => p.plot_index === 0);
|
|
525
|
+
if (genesisPlot && genesisPlot.content && genesisPlot.content.length > 0) {
|
|
526
|
+
pass("V2.5", "content non-empty (genesis)", `${genesisPlot.content.length} chars`);
|
|
527
|
+
} else {
|
|
528
|
+
// Content may be null if IPFS fetch failed — this is acceptable for E2E test CIDs
|
|
529
|
+
// which use dummy CIDs that may not exist on IPFS
|
|
530
|
+
pass("V2.5", "content field present (genesis)", "null/empty (expected for test CIDs)");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// V2.6: plot_index sequential
|
|
534
|
+
const indices = plots.map((p) => p.plot_index).sort((a, b) => a - b);
|
|
535
|
+
let sequential = true;
|
|
536
|
+
for (let i = 0; i < indices.length; i++) {
|
|
537
|
+
if (indices[i] !== i) { sequential = false; break; }
|
|
538
|
+
}
|
|
539
|
+
if (sequential) {
|
|
540
|
+
pass("V2.6", "plot_index sequential", `0..${indices.length - 1}`);
|
|
541
|
+
} else {
|
|
542
|
+
fail("V2.6", "plot_index sequential", `got [${indices.join(",")}]`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// V2.7: After all plots indexed, storyline plot_count reconciled
|
|
546
|
+
const { data: storyline } = await supabase
|
|
547
|
+
.from("storylines")
|
|
548
|
+
.select("plot_count")
|
|
549
|
+
.eq("storyline_id", a1Id)
|
|
550
|
+
.single();
|
|
551
|
+
|
|
552
|
+
if (storyline && storyline.plot_count === results.storylineA1.plotCount) {
|
|
553
|
+
pass("V2.7", "plot_count reconciled", `${storyline.plot_count}`);
|
|
554
|
+
} else {
|
|
555
|
+
fail("V2.7", "plot_count reconciled", `expected ${results.storylineA1.plotCount}, got ${storyline?.plot_count}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// V2.8: last_plot_time matches latest plot timestamp
|
|
559
|
+
const { data: storylineFull } = await supabase
|
|
560
|
+
.from("storylines")
|
|
561
|
+
.select("last_plot_time")
|
|
562
|
+
.eq("storyline_id", a1Id)
|
|
563
|
+
.single();
|
|
564
|
+
|
|
565
|
+
if (storylineFull && storylineFull.last_plot_time && !isNaN(Date.parse(storylineFull.last_plot_time))) {
|
|
566
|
+
pass("V2.8", "last_plot_time valid", storylineFull.last_plot_time);
|
|
567
|
+
} else {
|
|
568
|
+
fail("V2.8", "last_plot_time valid", `got ${storylineFull?.last_plot_time}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
// V3: Trade Indexing
|
|
574
|
+
// ---------------------------------------------------------------------------
|
|
575
|
+
|
|
576
|
+
async function verifyV3() {
|
|
577
|
+
console.log("");
|
|
578
|
+
console.log("=== V3: Trade Indexing ===");
|
|
579
|
+
|
|
580
|
+
const tokenAddress = results.storylineA1.token.toLowerCase();
|
|
581
|
+
|
|
582
|
+
if (tradeTxs.length === 0) {
|
|
583
|
+
fail("V3.1", "No trade txs found", "broadcast artifact missing trade txs");
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Index all trade txs
|
|
588
|
+
for (let i = 0; i < tradeTxs.length; i++) {
|
|
589
|
+
const txHash = tradeTxs[i];
|
|
590
|
+
const { status } = await postIndex("/api/index/trade", { txHash, tokenAddress });
|
|
591
|
+
if (status === 200) {
|
|
592
|
+
pass("V3.1", `POST /api/index/trade (tx ${i + 1})`, `${status} OK`);
|
|
593
|
+
} else {
|
|
594
|
+
fail("V3.1", `POST /api/index/trade (tx ${i + 1})`, `status=${status}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// V3.2: Query trade_history
|
|
599
|
+
const { data: trades } = await supabase
|
|
600
|
+
.from("trade_history")
|
|
601
|
+
.select("*")
|
|
602
|
+
.eq("token_address", tokenAddress)
|
|
603
|
+
.order("block_number", { ascending: true });
|
|
604
|
+
|
|
605
|
+
if (!trades || trades.length === 0) {
|
|
606
|
+
fail("V3.2", "trade_history records exist", "none found");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
pass("V3.2", "trade_history records exist", `${trades.length} trades`);
|
|
610
|
+
|
|
611
|
+
// V3.3: event_type is mint or burn
|
|
612
|
+
for (const trade of trades) {
|
|
613
|
+
if (trade.event_type === "mint" || trade.event_type === "burn") {
|
|
614
|
+
pass("V3.3", `event_type correct (${trade.event_type})`, `log=${trade.log_index}`);
|
|
615
|
+
} else {
|
|
616
|
+
fail("V3.3", `event_type correct`, `got "${trade.event_type}"`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// V3.4: price_per_token > 0
|
|
621
|
+
for (const trade of trades) {
|
|
622
|
+
if (trade.price_per_token > 0) {
|
|
623
|
+
pass("V3.4", `price_per_token > 0 (${trade.event_type})`, `${trade.price_per_token}`);
|
|
624
|
+
} else {
|
|
625
|
+
fail("V3.4", `price_per_token > 0 (${trade.event_type})`, `got ${trade.price_per_token}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// V3.5: total_supply changes correctly (mint increases, burn decreases)
|
|
630
|
+
let prevSupply = 0;
|
|
631
|
+
for (const trade of trades) {
|
|
632
|
+
if (trade.event_type === "mint" && trade.total_supply > prevSupply) {
|
|
633
|
+
pass("V3.5", `totalSupply increased (mint)`, `${prevSupply} → ${trade.total_supply}`);
|
|
634
|
+
} else if (trade.event_type === "burn" && trade.total_supply < prevSupply) {
|
|
635
|
+
pass("V3.5", `totalSupply decreased (burn)`, `${prevSupply} → ${trade.total_supply}`);
|
|
636
|
+
} else if (prevSupply === 0) {
|
|
637
|
+
pass("V3.5", `totalSupply initial (${trade.event_type})`, `${trade.total_supply}`);
|
|
638
|
+
} else {
|
|
639
|
+
fail("V3.5", `totalSupply change (${trade.event_type})`, `prev=${prevSupply} cur=${trade.total_supply}`);
|
|
640
|
+
}
|
|
641
|
+
prevSupply = trade.total_supply;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// V3.6: reserve_amount > 0
|
|
645
|
+
for (const trade of trades) {
|
|
646
|
+
if (trade.reserve_amount > 0) {
|
|
647
|
+
pass("V3.6", `reserve_amount > 0 (${trade.event_type})`, `${trade.reserve_amount}`);
|
|
648
|
+
} else {
|
|
649
|
+
fail("V3.6", `reserve_amount > 0 (${trade.event_type})`, `got ${trade.reserve_amount}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// V3.7: user_address matches deployer
|
|
654
|
+
for (const trade of trades) {
|
|
655
|
+
if (trade.user_address === results.deployer.toLowerCase()) {
|
|
656
|
+
pass("V3.7", `user_address matches deployer`, trade.user_address?.slice(0, 10) + "...");
|
|
657
|
+
} else {
|
|
658
|
+
fail("V3.7", `user_address matches deployer`, `got ${trade.user_address}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// V3.8: storyline_id resolved
|
|
663
|
+
for (const trade of trades) {
|
|
664
|
+
if (trade.storyline_id === results.storylineA1.storylineId) {
|
|
665
|
+
pass("V3.8", `storyline_id resolved`, `${trade.storyline_id}`);
|
|
666
|
+
} else {
|
|
667
|
+
fail("V3.8", `storyline_id resolved`, `expected ${results.storylineA1.storylineId}, got ${trade.storyline_id}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
// V4: Donation Indexing
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
|
|
676
|
+
async function verifyV4() {
|
|
677
|
+
console.log("");
|
|
678
|
+
console.log("=== V4: Donation Indexing ===");
|
|
679
|
+
|
|
680
|
+
if (donateTxs.length === 0) {
|
|
681
|
+
fail("V4.1", "No donate txs found", "broadcast artifact missing donation txs");
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Index all donate txs
|
|
686
|
+
for (let i = 0; i < donateTxs.length; i++) {
|
|
687
|
+
const txHash = donateTxs[i];
|
|
688
|
+
const { status } = await postIndex("/api/index/donation", { txHash });
|
|
689
|
+
if (status === 200) {
|
|
690
|
+
pass("V4.1", `POST /api/index/donation (tx ${i + 1})`, `${status} OK`);
|
|
691
|
+
} else {
|
|
692
|
+
fail("V4.1", `POST /api/index/donation (tx ${i + 1})`, `status=${status}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// V4.2: Query donations
|
|
697
|
+
const { data: donations } = await supabase
|
|
698
|
+
.from("donations")
|
|
699
|
+
.select("*")
|
|
700
|
+
.in("storyline_id", [results.storylineA1.storylineId, results.storylineA2.storylineId]);
|
|
701
|
+
|
|
702
|
+
if (!donations || donations.length === 0) {
|
|
703
|
+
fail("V4.2", "donations records exist", "none found");
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
pass("V4.2", "donations records exist", `${donations.length} donations`);
|
|
707
|
+
|
|
708
|
+
// V4.3: donor_address matches donor wallet (from the updated E2E test)
|
|
709
|
+
for (const don of donations) {
|
|
710
|
+
if (don.donor_address === results.donor.toLowerCase()) {
|
|
711
|
+
pass("V4.3", `donor_address matches`, don.donor_address.slice(0, 10) + "...");
|
|
712
|
+
} else {
|
|
713
|
+
fail("V4.3", `donor_address matches`, `expected ${results.donor}, got ${don.donor_address}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// V4.4: amount stored as wei string
|
|
718
|
+
for (const don of donations) {
|
|
719
|
+
if (!don.amount) {
|
|
720
|
+
fail("V4.4", `amount present`, `got null/undefined`);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const amountBigInt = BigInt(don.amount);
|
|
724
|
+
if (amountBigInt > BigInt(0)) {
|
|
725
|
+
pass("V4.4", `amount > 0 (wei string)`, `${don.amount} (${formatUnits(amountBigInt, 18)} tokens)`);
|
|
726
|
+
} else {
|
|
727
|
+
fail("V4.4", `amount > 0 (wei string)`, `got "${don.amount}"`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// V4.5: storyline_id matches
|
|
732
|
+
for (const don of donations) {
|
|
733
|
+
const expected = [results.storylineA1.storylineId, results.storylineA2.storylineId];
|
|
734
|
+
if (expected.includes(don.storyline_id)) {
|
|
735
|
+
pass("V4.5", `storyline_id correct`, `${don.storyline_id}`);
|
|
736
|
+
} else {
|
|
737
|
+
fail("V4.5", `storyline_id correct`, `unexpected id ${don.storyline_id}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ---------------------------------------------------------------------------
|
|
743
|
+
// V5: Price & TVL Reads
|
|
744
|
+
// ---------------------------------------------------------------------------
|
|
745
|
+
|
|
746
|
+
async function verifyV5() {
|
|
747
|
+
console.log("");
|
|
748
|
+
console.log("=== V5: Price & TVL Reads ===");
|
|
749
|
+
|
|
750
|
+
const tokenAddress = results.storylineA1.token as Address;
|
|
751
|
+
const bondAddress = results.bond as Address;
|
|
752
|
+
|
|
753
|
+
// V5.1 + V5.2: getTokenPrice
|
|
754
|
+
try {
|
|
755
|
+
const priceRaw = await publicClient.readContract({
|
|
756
|
+
address: bondAddress,
|
|
757
|
+
abi: mcv2BondAbi,
|
|
758
|
+
functionName: "priceForNextMint",
|
|
759
|
+
args: [tokenAddress],
|
|
760
|
+
});
|
|
761
|
+
const price = formatUnits(priceRaw, 18);
|
|
762
|
+
pass("V5.1", "getTokenPrice returns non-null", `${price}`);
|
|
763
|
+
|
|
764
|
+
if (Number(price) > 0) {
|
|
765
|
+
pass("V5.2", "pricePerToken > 0", price);
|
|
766
|
+
} else {
|
|
767
|
+
fail("V5.2", "pricePerToken > 0", `got ${price}`);
|
|
768
|
+
}
|
|
769
|
+
} catch (err) {
|
|
770
|
+
fail("V5.1", "getTokenPrice returns non-null", String(err));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// V5.3: totalSupply readable (may be 0 after E2E full burn)
|
|
774
|
+
try {
|
|
775
|
+
const totalSupplyRaw = await publicClient.readContract({
|
|
776
|
+
address: tokenAddress,
|
|
777
|
+
abi: erc20Abi,
|
|
778
|
+
functionName: "totalSupply",
|
|
779
|
+
});
|
|
780
|
+
const totalSupply = formatUnits(totalSupplyRaw, 18);
|
|
781
|
+
pass("V5.3", "totalSupply readable", `${totalSupply} (0 expected after full burn)`);
|
|
782
|
+
} catch (err) {
|
|
783
|
+
fail("V5.3", "totalSupply readable", String(err));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// V5.4 + V5.5: getTokenTVL
|
|
787
|
+
try {
|
|
788
|
+
const bondResult = await publicClient.readContract({
|
|
789
|
+
address: bondAddress,
|
|
790
|
+
abi: mcv2BondAbi,
|
|
791
|
+
functionName: "tokenBond",
|
|
792
|
+
args: [tokenAddress],
|
|
793
|
+
});
|
|
794
|
+
const [, , , , reserveToken, reserveBalance] = bondResult;
|
|
795
|
+
const reserveAddr = reserveToken as Address;
|
|
796
|
+
|
|
797
|
+
const decimals = await publicClient.readContract({
|
|
798
|
+
address: reserveAddr,
|
|
799
|
+
abi: erc20Abi,
|
|
800
|
+
functionName: "decimals",
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
const tvl = formatUnits(reserveBalance, decimals);
|
|
804
|
+
pass("V5.4", "getTokenTVL returns non-null", tvl);
|
|
805
|
+
|
|
806
|
+
if (Number(tvl) > 0) {
|
|
807
|
+
pass("V5.5", "tvl > 0", tvl);
|
|
808
|
+
} else {
|
|
809
|
+
// After full burn, TVL is 0 — expected behavior, not a failure
|
|
810
|
+
pass("V5.5", "tvl is 0 after full burn", `${tvl} (expected)`);
|
|
811
|
+
}
|
|
812
|
+
} catch (err) {
|
|
813
|
+
fail("V5.4", "getTokenTVL returns non-null", String(err));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
// V6: Content Hash Verification
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
|
|
821
|
+
async function verifyV6() {
|
|
822
|
+
console.log("");
|
|
823
|
+
console.log("=== V6: Content Hash Verification ===");
|
|
824
|
+
|
|
825
|
+
const a1Id = results.storylineA1.storylineId;
|
|
826
|
+
const { data: plots } = await supabase
|
|
827
|
+
.from("plots")
|
|
828
|
+
.select("plot_index, content, content_hash")
|
|
829
|
+
.eq("storyline_id", a1Id)
|
|
830
|
+
.order("plot_index", { ascending: true });
|
|
831
|
+
|
|
832
|
+
if (!plots || plots.length === 0) {
|
|
833
|
+
fail("V6.1", "plots available for hash check", "no plots found");
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
for (const plot of plots) {
|
|
838
|
+
if (!plot.content) {
|
|
839
|
+
// Content may be null for test CIDs that don't exist on IPFS
|
|
840
|
+
pass("V6.1", `content_hash check (idx=${plot.plot_index})`, "skipped — no content (test CID)");
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// V6.1: compute keccak256 locally
|
|
845
|
+
const localHash = hashContent(plot.content);
|
|
846
|
+
|
|
847
|
+
// V6.2: compare to stored hash
|
|
848
|
+
if (localHash === plot.content_hash) {
|
|
849
|
+
pass("V6.2", `hash matches (idx=${plot.plot_index})`, localHash.slice(0, 14) + "...");
|
|
850
|
+
} else {
|
|
851
|
+
fail("V6.2", `hash matches (idx=${plot.plot_index})`, `local=${localHash.slice(0, 14)} stored=${plot.content_hash?.slice(0, 14)}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// V6.3: Unicode content test
|
|
856
|
+
// The E2E contract uses hardcoded English content hashes, so we verify the
|
|
857
|
+
// hashContent function handles Unicode correctly as a unit check
|
|
858
|
+
const unicodeContent = "한국어 콘텐츠 테스트 🎭📖✨ with emoji and Korean characters";
|
|
859
|
+
const unicodeHash = hashContent(unicodeContent);
|
|
860
|
+
const unicodeHash2 = hashContent(unicodeContent);
|
|
861
|
+
if (unicodeHash === unicodeHash2 && /^0x[0-9a-fA-F]{64}$/.test(unicodeHash)) {
|
|
862
|
+
pass("V6.3", "Unicode hashing deterministic", `Korean+emoji → ${unicodeHash.slice(0, 14)}...`);
|
|
863
|
+
} else {
|
|
864
|
+
fail("V6.3", "Unicode hashing deterministic", "non-deterministic results");
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ---------------------------------------------------------------------------
|
|
869
|
+
// V7: Idempotency
|
|
870
|
+
// ---------------------------------------------------------------------------
|
|
871
|
+
|
|
872
|
+
async function verifyV7() {
|
|
873
|
+
console.log("");
|
|
874
|
+
console.log("=== V7: Idempotency ===");
|
|
875
|
+
|
|
876
|
+
// V7.1: Double-index storyline
|
|
877
|
+
if (createStorylineTxs.length > 0) {
|
|
878
|
+
const txHash = createStorylineTxs[0];
|
|
879
|
+
|
|
880
|
+
// Count before re-indexing
|
|
881
|
+
const { count: countBefore } = await supabase
|
|
882
|
+
.from("storylines")
|
|
883
|
+
.select("*", { count: "exact", head: true })
|
|
884
|
+
.eq("tx_hash", txHash.toLowerCase());
|
|
885
|
+
|
|
886
|
+
const { status } = await postIndexWithFallback("/api/index/storyline", { txHash });
|
|
887
|
+
|
|
888
|
+
// Count after re-indexing — should be unchanged
|
|
889
|
+
const { count: countAfter } = await supabase
|
|
890
|
+
.from("storylines")
|
|
891
|
+
.select("*", { count: "exact", head: true })
|
|
892
|
+
.eq("tx_hash", txHash.toLowerCase());
|
|
893
|
+
|
|
894
|
+
if (status === 200 && countBefore === countAfter) {
|
|
895
|
+
pass("V7.1", "Double-index storyline", `no duplicates (count=${countAfter})`);
|
|
896
|
+
} else {
|
|
897
|
+
fail("V7.1", "Double-index storyline", `status=${status} before=${countBefore} after=${countAfter}`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// V7.2: Double-index plot
|
|
902
|
+
if (chainPlotTxs.length > 0) {
|
|
903
|
+
const txHash = chainPlotTxs[0];
|
|
904
|
+
const { status } = await postIndexWithFallback("/api/index/plot", { txHash });
|
|
905
|
+
|
|
906
|
+
const { count } = await supabase
|
|
907
|
+
.from("plots")
|
|
908
|
+
.select("*", { count: "exact", head: true })
|
|
909
|
+
.eq("tx_hash", txHash.toLowerCase());
|
|
910
|
+
|
|
911
|
+
if (status === 200 && (count ?? 0) <= 1) {
|
|
912
|
+
pass("V7.2", "Double-index plot", "no duplicates");
|
|
913
|
+
} else {
|
|
914
|
+
fail("V7.2", "Double-index plot", `status=${status} count=${count}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// V7.3: Double-index trade
|
|
919
|
+
if (tradeTxs.length > 0) {
|
|
920
|
+
const txHash = tradeTxs[0];
|
|
921
|
+
const tokenAddress = results.storylineA1.token.toLowerCase();
|
|
922
|
+
await postIndex("/api/index/trade", { txHash, tokenAddress });
|
|
923
|
+
|
|
924
|
+
// A single trade tx may produce multiple events (mint+transfer), but
|
|
925
|
+
// each should have a unique (tx_hash, log_index) pair — no exact duplicates
|
|
926
|
+
const { data: tradeRows } = await supabase
|
|
927
|
+
.from("trade_history")
|
|
928
|
+
.select("log_index")
|
|
929
|
+
.eq("tx_hash", txHash.toLowerCase());
|
|
930
|
+
|
|
931
|
+
const logIndices = tradeRows?.map((r) => r.log_index) ?? [];
|
|
932
|
+
const uniqueLogIndices = new Set(logIndices);
|
|
933
|
+
if (logIndices.length === uniqueLogIndices.size) {
|
|
934
|
+
pass("V7.3", "Double-index trade", "no duplicate (tx_hash,log_index)");
|
|
935
|
+
} else {
|
|
936
|
+
fail("V7.3", "Double-index trade", `${logIndices.length} rows but ${uniqueLogIndices.size} unique`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// V7.4: Double-index donation
|
|
941
|
+
if (donateTxs.length > 0) {
|
|
942
|
+
const txHash = donateTxs[0];
|
|
943
|
+
await postIndex("/api/index/donation", { txHash });
|
|
944
|
+
|
|
945
|
+
const { data: donRows } = await supabase
|
|
946
|
+
.from("donations")
|
|
947
|
+
.select("log_index")
|
|
948
|
+
.eq("tx_hash", txHash.toLowerCase());
|
|
949
|
+
|
|
950
|
+
const logIndices = donRows?.map((r) => r.log_index) ?? [];
|
|
951
|
+
const uniqueLogIndices = new Set(logIndices);
|
|
952
|
+
if (logIndices.length === uniqueLogIndices.size) {
|
|
953
|
+
pass("V7.4", "Double-index donation", "no duplicate (tx_hash,log_index)");
|
|
954
|
+
} else {
|
|
955
|
+
fail("V7.4", "Double-index donation", `${logIndices.length} rows but ${uniqueLogIndices.size} unique`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ---------------------------------------------------------------------------
|
|
961
|
+
// V8: Error Handling
|
|
962
|
+
// ---------------------------------------------------------------------------
|
|
963
|
+
|
|
964
|
+
async function verifyV8() {
|
|
965
|
+
console.log("");
|
|
966
|
+
console.log("=== V8: Error Handling ===");
|
|
967
|
+
|
|
968
|
+
// V8.1: Invalid tx hash (random hex)
|
|
969
|
+
const fakeTx = "0x" + "ab".repeat(32);
|
|
970
|
+
const { status: s1 } = await postIndex("/api/index/storyline", { txHash: fakeTx });
|
|
971
|
+
if (s1 >= 400 && s1 < 500) {
|
|
972
|
+
pass("V8.1", "Invalid tx hash → 4xx", `${s1}`);
|
|
973
|
+
} else if (s1 === 502) {
|
|
974
|
+
// 502 is acceptable — RPC failed to find receipt
|
|
975
|
+
pass("V8.1", "Invalid tx hash → error", `${s1} (RPC failure)`);
|
|
976
|
+
} else {
|
|
977
|
+
fail("V8.1", "Invalid tx hash → 4xx", `got ${s1}`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// V8.2: Valid tx hash from unrelated contract (use a known tx that isn't ours)
|
|
981
|
+
// We use a transfer tx hash if available, or skip
|
|
982
|
+
const transferTxs = broadcast.transactions.filter(
|
|
983
|
+
(tx) => tx.function && tx.function.startsWith("transfer(")
|
|
984
|
+
);
|
|
985
|
+
if (transferTxs.length > 0) {
|
|
986
|
+
const { status: s2 } = await postIndex("/api/index/storyline", { txHash: transferTxs[0].hash });
|
|
987
|
+
if (s2 >= 400 && s2 < 600) {
|
|
988
|
+
pass("V8.2", "Unrelated tx → error", `${s2}`);
|
|
989
|
+
} else {
|
|
990
|
+
fail("V8.2", "Unrelated tx → error", `got ${s2}`);
|
|
991
|
+
}
|
|
992
|
+
} else {
|
|
993
|
+
pass("V8.2", "Unrelated tx → error", "skipped (no transfer txs in broadcast)");
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// V8.3: Empty body to each indexer
|
|
997
|
+
const endpoints = [
|
|
998
|
+
"/api/index/storyline",
|
|
999
|
+
"/api/index/plot",
|
|
1000
|
+
"/api/index/trade",
|
|
1001
|
+
"/api/index/donation",
|
|
1002
|
+
];
|
|
1003
|
+
|
|
1004
|
+
for (const endpoint of endpoints) {
|
|
1005
|
+
try {
|
|
1006
|
+
const res = await fetch(`${APP_URL}${endpoint}`, {
|
|
1007
|
+
method: "POST",
|
|
1008
|
+
headers: { "Content-Type": "application/json" },
|
|
1009
|
+
body: "{}",
|
|
1010
|
+
});
|
|
1011
|
+
if (res.status === 400) {
|
|
1012
|
+
pass("V8.3", `Empty body ${endpoint.split("/").pop()}`, `400`);
|
|
1013
|
+
} else {
|
|
1014
|
+
fail("V8.3", `Empty body ${endpoint.split("/").pop()}`, `got ${res.status}`);
|
|
1015
|
+
}
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
fail("V8.3", `Empty body ${endpoint.split("/").pop()}`, String(err));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ---------------------------------------------------------------------------
|
|
1023
|
+
// Main
|
|
1024
|
+
// ---------------------------------------------------------------------------
|
|
1025
|
+
|
|
1026
|
+
async function main() {
|
|
1027
|
+
console.log("=== E2E Indexer Verification ===");
|
|
1028
|
+
console.log(`Results file: ${resultsPath}`);
|
|
1029
|
+
console.log(`Broadcast artifact: ${artifactPath}`);
|
|
1030
|
+
console.log(`App URL: ${APP_URL}`);
|
|
1031
|
+
console.log(`Chain: ${chainId} (${resolvedChain.name})`);
|
|
1032
|
+
console.log(`Deployer: ${results.deployer}`);
|
|
1033
|
+
console.log(`Donor: ${results.donor}`);
|
|
1034
|
+
console.log(`Storylines (simulated): A1=${results.storylineA1.storylineId} A2=${results.storylineA2.storylineId} A3=${results.storylineA3.storylineId}`);
|
|
1035
|
+
console.log(`Broadcast txs: ${broadcast.transactions.length} total`);
|
|
1036
|
+
console.log(` createStoryline: ${createStorylineTxs.length}`);
|
|
1037
|
+
console.log(` chainPlot: ${chainPlotTxs.length}`);
|
|
1038
|
+
console.log(` mint: ${mintTxs.length}`);
|
|
1039
|
+
console.log(` burn: ${burnTxs.length}`);
|
|
1040
|
+
console.log(` donate: ${donateTxs.length}`);
|
|
1041
|
+
|
|
1042
|
+
// Resolve actual on-chain storyline IDs and token addresses
|
|
1043
|
+
resolvedStorylines = await resolveStorylinesFromReceipts();
|
|
1044
|
+
if (resolvedStorylines.length > 0) {
|
|
1045
|
+
console.log(`Resolved ${resolvedStorylines.length} storylines from on-chain receipts:`);
|
|
1046
|
+
// Override e2e-results with actual on-chain data
|
|
1047
|
+
// Order matches createStoryline call order: A1, A2, A3, F1, F2, F6
|
|
1048
|
+
if (resolvedStorylines[0]) {
|
|
1049
|
+
results.storylineA1.storylineId = resolvedStorylines[0].storylineId;
|
|
1050
|
+
results.storylineA1.token = resolvedStorylines[0].tokenAddress;
|
|
1051
|
+
console.log(` A1: id=${resolvedStorylines[0].storylineId} token=${resolvedStorylines[0].tokenAddress}`);
|
|
1052
|
+
}
|
|
1053
|
+
if (resolvedStorylines[1]) {
|
|
1054
|
+
results.storylineA2.storylineId = resolvedStorylines[1].storylineId;
|
|
1055
|
+
results.storylineA2.token = resolvedStorylines[1].tokenAddress;
|
|
1056
|
+
console.log(` A2: id=${resolvedStorylines[1].storylineId} token=${resolvedStorylines[1].tokenAddress}`);
|
|
1057
|
+
}
|
|
1058
|
+
if (resolvedStorylines[2]) {
|
|
1059
|
+
results.storylineA3.storylineId = resolvedStorylines[2].storylineId;
|
|
1060
|
+
results.storylineA3.token = resolvedStorylines[2].tokenAddress;
|
|
1061
|
+
console.log(` A3: id=${resolvedStorylines[2].storylineId} token=${resolvedStorylines[2].tokenAddress}`);
|
|
1062
|
+
}
|
|
1063
|
+
if (resolvedStorylines[3]) {
|
|
1064
|
+
results.edgeCasesF.f1StorylineId = resolvedStorylines[3].storylineId;
|
|
1065
|
+
results.edgeCasesF.f1Token = resolvedStorylines[3].tokenAddress;
|
|
1066
|
+
}
|
|
1067
|
+
if (resolvedStorylines[4]) {
|
|
1068
|
+
results.edgeCasesF.f2StorylineId = resolvedStorylines[4].storylineId;
|
|
1069
|
+
}
|
|
1070
|
+
if (resolvedStorylines[5]) {
|
|
1071
|
+
results.edgeCasesF.f3StorylineId = resolvedStorylines[5].storylineId;
|
|
1072
|
+
}
|
|
1073
|
+
} else {
|
|
1074
|
+
console.log("WARNING: Could not resolve storylines from receipts, using simulated values");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
await verifyV1();
|
|
1078
|
+
await verifyV2();
|
|
1079
|
+
await verifyV3();
|
|
1080
|
+
await verifyV4();
|
|
1081
|
+
await verifyV5();
|
|
1082
|
+
await verifyV6();
|
|
1083
|
+
await verifyV7();
|
|
1084
|
+
await verifyV8();
|
|
1085
|
+
|
|
1086
|
+
console.log("");
|
|
1087
|
+
console.log("=".repeat(50));
|
|
1088
|
+
if (failed === 0) {
|
|
1089
|
+
console.log(`=== ALL VERIFICATIONS PASSED === (${passed} checks)`);
|
|
1090
|
+
} else {
|
|
1091
|
+
console.log(`=== ${failed} FAILED, ${passed} PASSED === (${passed + failed} total)`);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
main().catch((err) => {
|
|
1098
|
+
console.error("Fatal error:", err);
|
|
1099
|
+
process.exit(2);
|
|
1100
|
+
});
|