plotlink-ows 1.2.94 → 1.2.96
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/app/lib/active-wallet.ts +260 -0
- package/app/lib/cartoon-coach.ts +1 -1
- package/app/lib/cartoon-readiness.ts +12 -10
- package/app/lib/cuts.ts +135 -18
- package/app/lib/lettering-status.ts +64 -6
- package/app/lib/story-progress.ts +2 -3
- package/app/routes/dashboard.ts +6 -4
- package/app/routes/publish.ts +56 -23
- package/app/routes/settings.ts +92 -37
- package/app/routes/wallet.ts +58 -30
- package/app/web/components/CartoonNextAction.tsx +145 -0
- package/app/web/components/CartoonPublishPage.tsx +1 -1
- package/app/web/components/CutListPanel.tsx +1198 -488
- package/app/web/components/Dashboard.tsx +15 -6
- package/app/web/components/FinishEpisodePanel.tsx +57 -46
- package/app/web/components/LetteringEditor.tsx +867 -366
- package/app/web/components/PreviewPanel.tsx +1459 -844
- package/app/web/components/StoriesPage.tsx +985 -475
- package/app/web/components/StoryProgressPanel.tsx +32 -102
- package/app/web/components/WalletCard.tsx +110 -8
- package/app/web/components/WorkflowCoach.tsx +63 -35
- package/app/web/dist/assets/{export-cut-nKQ_n2-J.js → export-cut-BqZI0-Rv.js} +1 -1
- package/app/web/dist/assets/index-C43toXVm.js +141 -0
- package/app/web/dist/assets/index-CcfChGEK.css +32 -0
- package/app/web/dist/index.html +2 -2
- package/package.json +1 -1
- package/app/web/dist/assets/index-BAZGwVwj.js +0 -143
- package/app/web/dist/assets/index-DoXH2OlP.css +0 -32
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
// tested and shared between the editor checklist and the insert-from-script
|
|
4
4
|
// panel. None of this changes the export model or publish readiness rules.
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
comfortableOverlaySize,
|
|
8
|
+
createOverlay,
|
|
9
|
+
type Overlay,
|
|
10
|
+
type OverlayType,
|
|
11
|
+
} from "./overlays";
|
|
7
12
|
|
|
8
13
|
/** The cut fields the lettering guidance reads (a structural subset of Cut). */
|
|
9
14
|
export interface LetteringCut {
|
|
@@ -16,6 +21,7 @@ export interface LetteringCut {
|
|
|
16
21
|
sfx?: string;
|
|
17
22
|
dialogue?: { speaker: string; text: string }[];
|
|
18
23
|
overlays?: Overlay[];
|
|
24
|
+
kind?: "image" | "text";
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export interface LetteringChecklist {
|
|
@@ -45,12 +51,16 @@ export function cutLetteringChecklist(
|
|
|
45
51
|
cut: LetteringCut,
|
|
46
52
|
opts: { staleExport?: boolean } = {},
|
|
47
53
|
): LetteringChecklist {
|
|
48
|
-
const exported =
|
|
49
|
-
|
|
54
|
+
const exported =
|
|
55
|
+
!opts.staleExport && (!!cut.finalImagePath || !!cut.exportedAt);
|
|
56
|
+
const uploaded =
|
|
57
|
+
!opts.staleExport && (!!cut.uploadedUrl || !!cut.uploadedCid);
|
|
50
58
|
return {
|
|
51
59
|
hasCleanImage: !!cut.cleanImagePath,
|
|
52
60
|
hasScriptText:
|
|
53
|
-
(cut.dialogue?.length ?? 0) > 0 ||
|
|
61
|
+
(cut.dialogue?.length ?? 0) > 0 ||
|
|
62
|
+
!!cut.narration?.trim() ||
|
|
63
|
+
!!cut.sfx?.trim(),
|
|
54
64
|
bubblesPlaced: cut.overlays?.length ?? 0,
|
|
55
65
|
exported,
|
|
56
66
|
uploaded,
|
|
@@ -120,14 +130,62 @@ export function cutScriptLines(cut: LetteringCut): ScriptLine[] {
|
|
|
120
130
|
const lines: ScriptLine[] = [];
|
|
121
131
|
(cut.dialogue ?? []).forEach((d, i) => {
|
|
122
132
|
if (d?.text?.trim()) {
|
|
123
|
-
lines.push({
|
|
133
|
+
lines.push({
|
|
134
|
+
type: "speech",
|
|
135
|
+
speaker: d.speaker,
|
|
136
|
+
text: d.text.trim(),
|
|
137
|
+
key: `speech-${i}`,
|
|
138
|
+
});
|
|
124
139
|
}
|
|
125
140
|
});
|
|
126
141
|
if (cut.narration?.trim()) {
|
|
127
|
-
lines.push({
|
|
142
|
+
lines.push({
|
|
143
|
+
type: "narration",
|
|
144
|
+
text: cut.narration.trim(),
|
|
145
|
+
key: "narration",
|
|
146
|
+
});
|
|
128
147
|
}
|
|
129
148
|
if (cut.sfx?.trim()) {
|
|
130
149
|
lines.push({ type: "sfx", text: cut.sfx.trim(), key: "sfx" });
|
|
131
150
|
}
|
|
132
151
|
return lines;
|
|
133
152
|
}
|
|
153
|
+
|
|
154
|
+
function draftAnchorFor(
|
|
155
|
+
type: OverlayType,
|
|
156
|
+
index: number,
|
|
157
|
+
total: number,
|
|
158
|
+
): { x: number; y: number } {
|
|
159
|
+
if (type === "narration")
|
|
160
|
+
return { x: 0.08, y: 0.05 + Math.min(index, 2) * 0.18 };
|
|
161
|
+
if (type === "sfx") {
|
|
162
|
+
const col = index % 2;
|
|
163
|
+
const row = Math.floor(index / 2);
|
|
164
|
+
return { x: col === 0 ? 0.1 : 0.62, y: 0.68 + row * 0.12 };
|
|
165
|
+
}
|
|
166
|
+
const row = Math.floor(index / 2);
|
|
167
|
+
const left = index % 2 === 0;
|
|
168
|
+
const y = 0.08 + row * Math.max(0.15, Math.min(0.22, total > 4 ? 0.16 : 0.2));
|
|
169
|
+
return { x: left ? 0.05 : 0.45, y };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a first-pass editable overlay set from the cut script (#494). Pure,
|
|
174
|
+
* deterministic, and intentionally approximate: these are draft bubble/caption
|
|
175
|
+
* positions for the writer to review in the focused editor, not export-ready
|
|
176
|
+
* layout. Empty script pieces produce no overlays.
|
|
177
|
+
*/
|
|
178
|
+
export function buildDraftOverlays(cut: LetteringCut): Overlay[] {
|
|
179
|
+
const lines = cutScriptLines(cut);
|
|
180
|
+
return lines.map((line, index) => {
|
|
181
|
+
const { x, y } = draftAnchorFor(line.type, index, lines.length);
|
|
182
|
+
const overlay = createOverlay(line.type, x, y);
|
|
183
|
+
const comfortable = comfortableOverlaySize(line.type, x, y);
|
|
184
|
+
return {
|
|
185
|
+
...overlay,
|
|
186
|
+
...comfortable,
|
|
187
|
+
text: line.text,
|
|
188
|
+
...(line.type === "speech" ? { speaker: line.speaker ?? "" } : {}),
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -38,9 +38,8 @@ export interface EpisodeProgress {
|
|
|
38
38
|
summary: string;
|
|
39
39
|
published: boolean;
|
|
40
40
|
/**
|
|
41
|
-
* Cartoon cut progress; null for fiction. `needClean`/`
|
|
42
|
-
* cuts only
|
|
43
|
-
* the clean-image stage from the lettering stage.
|
|
41
|
+
* Cartoon cut progress; null for fiction. `needClean`/`withClean` count image
|
|
42
|
+
* cuts only; `withText`, export, and upload count every cut including text panels.
|
|
44
43
|
*/
|
|
45
44
|
cuts: { total: number; needClean: number; withClean: number; withText: number; exported: number; uploaded: number } | null;
|
|
46
45
|
/**
|
package/app/routes/dashboard.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { base } from "viem/chains";
|
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { getEthBalance } from "../lib/publish";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveActiveWallet } from "../lib/active-wallet";
|
|
8
8
|
import { mcv2BondAbi } from "../../packages/cli/src/sdk/abi";
|
|
9
9
|
import { STORIES_DIR, readPublishStatus } from "./stories";
|
|
10
10
|
|
|
@@ -82,10 +82,10 @@ dashboard.get("/", async (c) => {
|
|
|
82
82
|
// Get wallet info
|
|
83
83
|
let walletInfo = null;
|
|
84
84
|
try {
|
|
85
|
-
const
|
|
86
|
-
const wallet =
|
|
85
|
+
const resolvedWallet = await resolveActiveWallet();
|
|
86
|
+
const wallet = resolvedWallet.activeWallet;
|
|
87
87
|
if (wallet) {
|
|
88
|
-
const address =
|
|
88
|
+
const address = wallet.address;
|
|
89
89
|
if (address) {
|
|
90
90
|
const ethBalance = await getEthBalance(address);
|
|
91
91
|
|
|
@@ -107,6 +107,8 @@ dashboard.get("/", async (c) => {
|
|
|
107
107
|
} catch { /* best effort */ }
|
|
108
108
|
|
|
109
109
|
walletInfo = {
|
|
110
|
+
walletId: wallet.walletId,
|
|
111
|
+
name: wallet.name,
|
|
110
112
|
address,
|
|
111
113
|
ethBalance: ethBalance.toString(),
|
|
112
114
|
ethFormatted: (Number(ethBalance) / 1e18).toFixed(6),
|
package/app/routes/publish.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { streamSSE } from "hono/streaming";
|
|
3
3
|
import { publishStoryline, publishPlot, getEthBalance, getCreationFee, estimatePublishCost, uploadCoverImage, uploadPlotImage, updateStoryline } from "../lib/publish";
|
|
4
4
|
import { keccak256, toBytes } from "viem";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveActiveWallet } from "../lib/active-wallet";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { STORIES_DIR } from "../lib/paths";
|
|
8
8
|
import { readCutsFile } from "../lib/cuts";
|
|
@@ -51,13 +51,18 @@ const publish = new Hono();
|
|
|
51
51
|
publish.get("/preflight", async (c) => {
|
|
52
52
|
try {
|
|
53
53
|
// Check wallet
|
|
54
|
-
const
|
|
55
|
-
const wallet =
|
|
54
|
+
const resolvedWallet = await resolveActiveWallet();
|
|
55
|
+
const wallet = resolvedWallet.activeWallet;
|
|
56
56
|
if (!wallet) {
|
|
57
|
-
return c.json({
|
|
57
|
+
return c.json({
|
|
58
|
+
ready: false,
|
|
59
|
+
error: resolvedWallet.error || "No OWS wallet found",
|
|
60
|
+
selectionRequired: resolvedWallet.selectionRequired,
|
|
61
|
+
wallets: resolvedWallet.wallets,
|
|
62
|
+
});
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
const address =
|
|
65
|
+
const address = wallet.address;
|
|
61
66
|
if (!address) {
|
|
62
67
|
return c.json({ ready: false, error: "No EVM address on wallet" });
|
|
63
68
|
}
|
|
@@ -76,6 +81,8 @@ publish.get("/preflight", async (c) => {
|
|
|
76
81
|
return c.json({
|
|
77
82
|
ready: false,
|
|
78
83
|
address,
|
|
84
|
+
walletName: wallet.name,
|
|
85
|
+
walletId: wallet.walletId,
|
|
79
86
|
ethBalance: balance.toString(),
|
|
80
87
|
error: "Could not read creation fee — check RPC and contract config",
|
|
81
88
|
});
|
|
@@ -108,6 +115,8 @@ publish.get("/preflight", async (c) => {
|
|
|
108
115
|
return c.json({
|
|
109
116
|
ready: hasEnoughEth,
|
|
110
117
|
address,
|
|
118
|
+
walletName: wallet.name,
|
|
119
|
+
walletId: wallet.walletId,
|
|
111
120
|
ethBalance: balance.toString(),
|
|
112
121
|
creationFee: creationFee.toString(),
|
|
113
122
|
requiredBalance: requiredBalance.toString(),
|
|
@@ -259,16 +268,22 @@ publish.post("/file", async (c) => {
|
|
|
259
268
|
}
|
|
260
269
|
}
|
|
261
270
|
|
|
262
|
-
// Get wallet
|
|
263
|
-
let
|
|
271
|
+
// Get active wallet
|
|
272
|
+
let wallet;
|
|
264
273
|
try {
|
|
265
|
-
|
|
274
|
+
const resolvedWallet = await resolveActiveWallet();
|
|
275
|
+
wallet = resolvedWallet.activeWallet;
|
|
276
|
+
if (!wallet) {
|
|
277
|
+
return c.json({
|
|
278
|
+
error: resolvedWallet.error || "No OWS wallet",
|
|
279
|
+
selectionRequired: resolvedWallet.selectionRequired,
|
|
280
|
+
wallets: resolvedWallet.wallets,
|
|
281
|
+
}, 400);
|
|
282
|
+
}
|
|
266
283
|
} catch (err) {
|
|
267
|
-
console.error("[publish/file]
|
|
284
|
+
console.error("[publish/file] resolveActiveWallet error:", err);
|
|
268
285
|
return c.json({ error: `OWS wallet error: ${err instanceof Error ? err.message : String(err)}` }, 500);
|
|
269
286
|
}
|
|
270
|
-
const wallet = wallets.find((w) => w.name.startsWith("plotlink-writer"));
|
|
271
|
-
if (!wallet) return c.json({ error: "No OWS wallet" }, 400);
|
|
272
287
|
|
|
273
288
|
console.log("[publish/file] Starting publish for", body.storyName, body.fileName, "wallet:", wallet.name);
|
|
274
289
|
|
|
@@ -369,11 +384,17 @@ publish.post("/retry-index", async (c) => {
|
|
|
369
384
|
/** POST /api/publish/upload-cover — upload cover image with wallet signature */
|
|
370
385
|
publish.post("/upload-cover", async (c) => {
|
|
371
386
|
try {
|
|
372
|
-
const
|
|
373
|
-
const wallet =
|
|
374
|
-
if (!wallet)
|
|
387
|
+
const resolvedWallet = await resolveActiveWallet();
|
|
388
|
+
const wallet = resolvedWallet.activeWallet;
|
|
389
|
+
if (!wallet) {
|
|
390
|
+
return c.json({
|
|
391
|
+
error: resolvedWallet.error || "No OWS wallet",
|
|
392
|
+
selectionRequired: resolvedWallet.selectionRequired,
|
|
393
|
+
wallets: resolvedWallet.wallets,
|
|
394
|
+
}, 400);
|
|
395
|
+
}
|
|
375
396
|
|
|
376
|
-
const address =
|
|
397
|
+
const address = wallet.address;
|
|
377
398
|
if (!address) return c.json({ error: "No EVM address on wallet" }, 400);
|
|
378
399
|
|
|
379
400
|
const formData = await c.req.formData();
|
|
@@ -407,11 +428,17 @@ publish.post("/upload-cover", async (c) => {
|
|
|
407
428
|
/** POST /api/publish/upload-plot-image — upload plot illustration with wallet signature */
|
|
408
429
|
publish.post("/upload-plot-image", async (c) => {
|
|
409
430
|
try {
|
|
410
|
-
const
|
|
411
|
-
const wallet =
|
|
412
|
-
if (!wallet)
|
|
431
|
+
const resolvedWallet = await resolveActiveWallet();
|
|
432
|
+
const wallet = resolvedWallet.activeWallet;
|
|
433
|
+
if (!wallet) {
|
|
434
|
+
return c.json({
|
|
435
|
+
error: resolvedWallet.error || "No OWS wallet",
|
|
436
|
+
selectionRequired: resolvedWallet.selectionRequired,
|
|
437
|
+
wallets: resolvedWallet.wallets,
|
|
438
|
+
}, 400);
|
|
439
|
+
}
|
|
413
440
|
|
|
414
|
-
const address =
|
|
441
|
+
const address = wallet.address;
|
|
415
442
|
if (!address) return c.json({ error: "No EVM address on wallet" }, 400);
|
|
416
443
|
|
|
417
444
|
const formData = await c.req.formData();
|
|
@@ -442,11 +469,17 @@ publish.post("/upload-plot-image", async (c) => {
|
|
|
442
469
|
/** POST /api/publish/update-storyline — update storyline metadata with wallet signature */
|
|
443
470
|
publish.post("/update-storyline", async (c) => {
|
|
444
471
|
try {
|
|
445
|
-
const
|
|
446
|
-
const wallet =
|
|
447
|
-
if (!wallet)
|
|
472
|
+
const resolvedWallet = await resolveActiveWallet();
|
|
473
|
+
const wallet = resolvedWallet.activeWallet;
|
|
474
|
+
if (!wallet) {
|
|
475
|
+
return c.json({
|
|
476
|
+
error: resolvedWallet.error || "No OWS wallet",
|
|
477
|
+
selectionRequired: resolvedWallet.selectionRequired,
|
|
478
|
+
wallets: resolvedWallet.wallets,
|
|
479
|
+
}, 400);
|
|
480
|
+
}
|
|
448
481
|
|
|
449
|
-
const address =
|
|
482
|
+
const address = wallet.address;
|
|
450
483
|
if (!address) return c.json({ error: "No EVM address on wallet" }, 400);
|
|
451
484
|
|
|
452
485
|
const body = await c.req.json<{
|
package/app/routes/settings.ts
CHANGED
|
@@ -2,28 +2,66 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { createPublicClient, createWalletClient, http, decodeEventLog } from "viem";
|
|
3
3
|
import { base } from "viem/chains";
|
|
4
4
|
import { erc8004Abi } from "../../packages/cli/src/sdk/abi";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveActiveWallet } from "../lib/active-wallet";
|
|
6
6
|
import { createOwsAccount } from "../lib/publish";
|
|
7
|
-
import { db } from "../db";
|
|
8
|
-
import {
|
|
9
|
-
signMessage as owsSignMsg,
|
|
10
|
-
} from "@open-wallet-standard/core";
|
|
11
7
|
import { CONFIG_DIR } from "../lib/paths";
|
|
12
8
|
import fs from "fs";
|
|
13
9
|
import path from "path";
|
|
14
10
|
|
|
15
11
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
12
|
+
type Config = Record<string, unknown>;
|
|
16
13
|
|
|
17
|
-
function readConfig():
|
|
14
|
+
function readConfig(): Config {
|
|
18
15
|
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")); } catch { return {}; }
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
function writeConfig(updates:
|
|
18
|
+
function writeConfig(updates: Config) {
|
|
22
19
|
const config = readConfig();
|
|
23
20
|
Object.assign(config, updates);
|
|
24
21
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
25
22
|
}
|
|
26
23
|
|
|
24
|
+
function normalizeAddress(address: unknown): string | null {
|
|
25
|
+
return typeof address === "string" && /^0x[a-fA-F0-9]{40}$/.test(address)
|
|
26
|
+
? address.toLowerCase()
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getWalletAgentConfig(
|
|
31
|
+
config: Config,
|
|
32
|
+
wallet: { walletId?: string; name: string; address: string },
|
|
33
|
+
selectableWalletCount: number,
|
|
34
|
+
): Config | null {
|
|
35
|
+
if (!config.agentId) return null;
|
|
36
|
+
|
|
37
|
+
const cachedAddress = normalizeAddress(config.agentWalletAddress);
|
|
38
|
+
const activeAddress = normalizeAddress(wallet.address);
|
|
39
|
+
if (cachedAddress && activeAddress) {
|
|
40
|
+
return cachedAddress === activeAddress ? config : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof config.agentWalletId === "string" && wallet.walletId) {
|
|
44
|
+
return config.agentWalletId === wallet.walletId ? config : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof config.agentWalletName === "string") {
|
|
48
|
+
return config.agentWalletName === wallet.name ? config : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Backwards compatibility for pre-#196 installs: an unscoped cache can only
|
|
52
|
+
// be trusted when there is no wallet-switching ambiguity.
|
|
53
|
+
return selectableWalletCount <= 1 ? config : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function walletAgentConfig(wallet: { walletId?: string; name: string; address: string }, updates: Config): Config {
|
|
57
|
+
return {
|
|
58
|
+
...updates,
|
|
59
|
+
agentWalletAddress: wallet.address.toLowerCase(),
|
|
60
|
+
agentWalletName: wallet.name,
|
|
61
|
+
...(wallet.walletId ? { agentWalletId: wallet.walletId } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
27
65
|
const ERC_8004 = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432" as const;
|
|
28
66
|
const rpcUrl = process.env.NEXT_PUBLIC_RPC_URL || "https://mainnet.base.org";
|
|
29
67
|
|
|
@@ -43,33 +81,37 @@ settings.post("/generate-binding", async (c) => {
|
|
|
43
81
|
}
|
|
44
82
|
|
|
45
83
|
try {
|
|
46
|
-
const
|
|
47
|
-
const wallet =
|
|
48
|
-
if (!wallet)
|
|
84
|
+
const resolvedWallet = await resolveActiveWallet();
|
|
85
|
+
const wallet = resolvedWallet.activeWallet;
|
|
86
|
+
if (!wallet) {
|
|
87
|
+
return c.json({
|
|
88
|
+
error: resolvedWallet.error || "No OWS wallet found. Create one in Wallet settings first.",
|
|
89
|
+
selectionRequired: resolvedWallet.selectionRequired,
|
|
90
|
+
wallets: resolvedWallet.wallets,
|
|
91
|
+
}, 400);
|
|
92
|
+
}
|
|
49
93
|
|
|
50
|
-
const owsWallet =
|
|
94
|
+
const owsWallet = wallet.address;
|
|
51
95
|
if (!owsWallet) return c.json({ error: "No EVM address on wallet" }, 400);
|
|
52
96
|
|
|
53
97
|
const message = `I authorize ${body.humanWallet} as my PlotLink owner. Wallet: ${owsWallet}`;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const result = owsSignMsg(wallet.name, "eip155:8453", message, passphrase);
|
|
57
|
-
const signature = result.signature.startsWith("0x") ? result.signature : `0x${result.signature}`;
|
|
98
|
+
const account = createOwsAccount(wallet.name, owsWallet as `0x${string}`);
|
|
99
|
+
const signature = await account.signMessage({ message });
|
|
58
100
|
|
|
59
101
|
// Include agent data from config.json if available
|
|
60
|
-
const config = readConfig();
|
|
102
|
+
const config = getWalletAgentConfig(readConfig(), wallet, resolvedWallet.wallets.filter((w) => w.address).length);
|
|
61
103
|
|
|
62
104
|
return c.json({
|
|
63
105
|
message,
|
|
64
106
|
signature,
|
|
65
107
|
owsWallet,
|
|
66
|
-
agentId: config
|
|
67
|
-
agentName: (config
|
|
68
|
-
agentDescription: (config
|
|
69
|
-
agentGenre: (config
|
|
70
|
-
agentLlmModel: (config
|
|
71
|
-
agentRegisteredBy: (config
|
|
72
|
-
agentRegisteredAt: (config
|
|
108
|
+
agentId: config?.agentId ? Number(config.agentId) : undefined,
|
|
109
|
+
agentName: (config?.agentName as string) || undefined,
|
|
110
|
+
agentDescription: (config?.agentDescription as string) || undefined,
|
|
111
|
+
agentGenre: (config?.agentGenre as string) || undefined,
|
|
112
|
+
agentLlmModel: (config?.agentLlmModel as string) || undefined,
|
|
113
|
+
agentRegisteredBy: (config?.agentRegisteredBy as string) || undefined,
|
|
114
|
+
agentRegisteredAt: (config?.agentRegisteredAt as string) || undefined,
|
|
73
115
|
});
|
|
74
116
|
} catch (err: unknown) {
|
|
75
117
|
const msg = err instanceof Error ? err.message : "Failed to generate binding proof";
|
|
@@ -89,11 +131,17 @@ settings.post("/register-agent", async (c) => {
|
|
|
89
131
|
}
|
|
90
132
|
|
|
91
133
|
try {
|
|
92
|
-
const
|
|
93
|
-
const wallet =
|
|
94
|
-
if (!wallet)
|
|
134
|
+
const resolvedWallet = await resolveActiveWallet();
|
|
135
|
+
const wallet = resolvedWallet.activeWallet;
|
|
136
|
+
if (!wallet) {
|
|
137
|
+
return c.json({
|
|
138
|
+
error: resolvedWallet.error || "No OWS wallet found. Create one in Wallet settings first.",
|
|
139
|
+
selectionRequired: resolvedWallet.selectionRequired,
|
|
140
|
+
wallets: resolvedWallet.wallets,
|
|
141
|
+
}, 400);
|
|
142
|
+
}
|
|
95
143
|
|
|
96
|
-
const owsAddress =
|
|
144
|
+
const owsAddress = wallet.address;
|
|
97
145
|
if (!owsAddress) return c.json({ error: "No EVM address on wallet" }, 400);
|
|
98
146
|
|
|
99
147
|
// Check if already registered
|
|
@@ -160,7 +208,7 @@ settings.post("/register-agent", async (c) => {
|
|
|
160
208
|
}
|
|
161
209
|
|
|
162
210
|
// Cache full tokenURI data in config.json (survives npx reinstalls, no Prisma dependency)
|
|
163
|
-
writeConfig({
|
|
211
|
+
writeConfig(walletAgentConfig(wallet, {
|
|
164
212
|
agentId,
|
|
165
213
|
agentName: body.name.trim(),
|
|
166
214
|
agentDescription: body.description.trim(),
|
|
@@ -168,7 +216,7 @@ settings.post("/register-agent", async (c) => {
|
|
|
168
216
|
agentLlmModel: "Claude",
|
|
169
217
|
agentRegisteredBy: "plotlink-ows",
|
|
170
218
|
agentRegisteredAt: registeredAt,
|
|
171
|
-
});
|
|
219
|
+
}));
|
|
172
220
|
|
|
173
221
|
return c.json({
|
|
174
222
|
agentId,
|
|
@@ -184,16 +232,23 @@ settings.post("/register-agent", async (c) => {
|
|
|
184
232
|
/** GET /api/settings/link-status — check if OWS wallet is registered on ERC-8004 */
|
|
185
233
|
settings.get("/link-status", async (c) => {
|
|
186
234
|
try {
|
|
187
|
-
const
|
|
188
|
-
const wallet =
|
|
189
|
-
if (!wallet)
|
|
235
|
+
const resolvedWallet = await resolveActiveWallet();
|
|
236
|
+
const wallet = resolvedWallet.activeWallet;
|
|
237
|
+
if (!wallet) {
|
|
238
|
+
return c.json({
|
|
239
|
+
linked: false,
|
|
240
|
+
error: resolvedWallet.error || "No wallet",
|
|
241
|
+
selectionRequired: resolvedWallet.selectionRequired,
|
|
242
|
+
wallets: resolvedWallet.wallets,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
190
245
|
|
|
191
|
-
const address =
|
|
246
|
+
const address = wallet.address;
|
|
192
247
|
if (!address) return c.json({ linked: false, error: "No EVM address" });
|
|
193
248
|
|
|
194
249
|
// Check config.json cache first (survives npx reinstalls + RPC rate limits)
|
|
195
|
-
const config = readConfig();
|
|
196
|
-
if (config
|
|
250
|
+
const config = getWalletAgentConfig(readConfig(), wallet, resolvedWallet.wallets.filter((w) => w.address).length);
|
|
251
|
+
if (config?.agentId) {
|
|
197
252
|
return c.json({ linked: true, agentId: Number(config.agentId), owsWallet: address });
|
|
198
253
|
}
|
|
199
254
|
|
|
@@ -207,7 +262,7 @@ settings.get("/link-status", async (c) => {
|
|
|
207
262
|
}) as bigint;
|
|
208
263
|
|
|
209
264
|
if (agentId > 0n) {
|
|
210
|
-
writeConfig({ agentId: Number(agentId) });
|
|
265
|
+
writeConfig(walletAgentConfig(wallet, { agentId: Number(agentId) }));
|
|
211
266
|
return c.json({ linked: true, agentId: Number(agentId), owsWallet: address });
|
|
212
267
|
}
|
|
213
268
|
} catch { /* agentIdByWallet may revert if not bound */ }
|
|
@@ -235,7 +290,7 @@ settings.get("/link-status", async (c) => {
|
|
|
235
290
|
} catch { /* ERC-721 Enumerable not supported */ }
|
|
236
291
|
|
|
237
292
|
if (agentId !== undefined) {
|
|
238
|
-
writeConfig({ agentId });
|
|
293
|
+
writeConfig(walletAgentConfig(wallet, { agentId }));
|
|
239
294
|
}
|
|
240
295
|
return c.json({ linked: true, agentId, owsWallet: address });
|
|
241
296
|
}
|
package/app/routes/wallet.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import { ENV_FILE } from "../lib/paths";
|
|
4
|
+
import { nextPlotlinkWalletName, resolveActiveWallet, selectActiveWallet, toPublicActiveWallet } from "../lib/active-wallet";
|
|
4
5
|
|
|
5
6
|
const envPath = ENV_FILE;
|
|
6
7
|
|
|
@@ -21,18 +22,8 @@ function readEnvPassphrase(): string | null {
|
|
|
21
22
|
/** GET /api/wallet — get wallet info */
|
|
22
23
|
wallet.get("/", async (c) => {
|
|
23
24
|
try {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
// Try to find existing wallet
|
|
27
|
-
const { listAgentWallets } = await import("../../lib/ows/wallet");
|
|
28
|
-
const wallets = listAgentWallets();
|
|
29
|
-
const plotlinkWallet = wallets.find((w) => w.name.startsWith("plotlink-writer"));
|
|
30
|
-
|
|
31
|
-
if (!plotlinkWallet) {
|
|
32
|
-
return c.json({ exists: false });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const address = getBaseAddress(plotlinkWallet);
|
|
25
|
+
const resolved = await resolveActiveWallet();
|
|
26
|
+
const activeWallet = resolved.activeWallet;
|
|
36
27
|
|
|
37
28
|
// Fetch balances on Base via RPC
|
|
38
29
|
let ethBalance = "0";
|
|
@@ -40,8 +31,8 @@ wallet.get("/", async (c) => {
|
|
|
40
31
|
let plotBalance = "0";
|
|
41
32
|
const rpcUrl = process.env.NEXT_PUBLIC_RPC_URL || "https://mainnet.base.org";
|
|
42
33
|
|
|
43
|
-
if (address) {
|
|
44
|
-
const addrPadded = "000000000000000000000000" + address.slice(2).toLowerCase();
|
|
34
|
+
if (activeWallet?.address) {
|
|
35
|
+
const addrPadded = "000000000000000000000000" + activeWallet.address.slice(2).toLowerCase();
|
|
45
36
|
const balanceOfSig = "0x70a08231" + addrPadded;
|
|
46
37
|
|
|
47
38
|
try {
|
|
@@ -49,7 +40,7 @@ wallet.get("/", async (c) => {
|
|
|
49
40
|
const ethRes = await fetch(rpcUrl, {
|
|
50
41
|
method: "POST",
|
|
51
42
|
headers: { "Content-Type": "application/json" },
|
|
52
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_getBalance", params: [address, "latest"] }),
|
|
43
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_getBalance", params: [activeWallet.address, "latest"] }),
|
|
53
44
|
});
|
|
54
45
|
const ethData = await ethRes.json() as { result?: string };
|
|
55
46
|
if (ethData.result && ethData.result !== "0x" && ethData.result !== "0x0") {
|
|
@@ -82,15 +73,27 @@ wallet.get("/", async (c) => {
|
|
|
82
73
|
} catch { /* balance fetch best-effort */ }
|
|
83
74
|
}
|
|
84
75
|
|
|
76
|
+
if (!activeWallet) {
|
|
77
|
+
return c.json({
|
|
78
|
+
exists: resolved.wallets.length > 0,
|
|
79
|
+
selectionRequired: resolved.selectionRequired,
|
|
80
|
+
error: resolved.error,
|
|
81
|
+
wallets: resolved.wallets,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
85
|
return c.json({
|
|
86
86
|
exists: true,
|
|
87
|
-
walletId:
|
|
88
|
-
name:
|
|
89
|
-
address,
|
|
87
|
+
walletId: activeWallet.walletId,
|
|
88
|
+
name: activeWallet.name,
|
|
89
|
+
address: activeWallet.address,
|
|
90
|
+
activeWallet: toPublicActiveWallet(activeWallet),
|
|
91
|
+
selectionRequired: false,
|
|
92
|
+
wallets: resolved.wallets,
|
|
90
93
|
ethBalance,
|
|
91
94
|
usdcBalance,
|
|
92
95
|
plotBalance,
|
|
93
|
-
accounts:
|
|
96
|
+
accounts: activeWallet.wallet.accounts,
|
|
94
97
|
});
|
|
95
98
|
} catch (err: unknown) {
|
|
96
99
|
const message = err instanceof Error ? err.message : "Failed to get wallet";
|
|
@@ -98,6 +101,30 @@ wallet.get("/", async (c) => {
|
|
|
98
101
|
}
|
|
99
102
|
});
|
|
100
103
|
|
|
104
|
+
/** POST /api/wallet/active — select active OWS wallet */
|
|
105
|
+
wallet.post("/active", async (c) => {
|
|
106
|
+
const body = await c.req.json<{ walletId?: string; name?: string; address?: string }>();
|
|
107
|
+
if (!body.walletId && !body.name && !body.address) {
|
|
108
|
+
return c.json({ error: "walletId, name, or address required" }, 400);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const resolved = await selectActiveWallet(body);
|
|
112
|
+
if (!resolved.activeWallet) {
|
|
113
|
+
return c.json({
|
|
114
|
+
error: resolved.error || "Could not select wallet",
|
|
115
|
+
selectionRequired: resolved.selectionRequired,
|
|
116
|
+
wallets: resolved.wallets,
|
|
117
|
+
}, 400);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return c.json({
|
|
121
|
+
ok: true,
|
|
122
|
+
activeWallet: toPublicActiveWallet(resolved.activeWallet),
|
|
123
|
+
wallets: resolved.wallets,
|
|
124
|
+
selectionRequired: false,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
101
128
|
/** POST /api/wallet/create — create OWS wallet */
|
|
102
129
|
wallet.post("/create", async (c) => {
|
|
103
130
|
try {
|
|
@@ -106,20 +133,21 @@ wallet.post("/create", async (c) => {
|
|
|
106
133
|
return c.json({ error: "Passphrase not configured" }, 400);
|
|
107
134
|
}
|
|
108
135
|
|
|
109
|
-
const { createAgentWallet,
|
|
136
|
+
const { createAgentWallet, listAgentWallets } = await import("../../lib/ows/wallet");
|
|
110
137
|
|
|
111
|
-
// Check if wallet already exists
|
|
112
138
|
const wallets = listAgentWallets();
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return c.json({ walletId: existing.id, address, alreadyExisted: true });
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const wallet = createAgentWallet("plotlink-writer", passphrase);
|
|
120
|
-
const address = getBaseAddress(wallet);
|
|
139
|
+
const name = nextPlotlinkWalletName(wallets);
|
|
140
|
+
const createdWallet = createAgentWallet(name, passphrase);
|
|
141
|
+
const resolved = await selectActiveWallet({ walletId: createdWallet.id, name: createdWallet.name });
|
|
121
142
|
|
|
122
|
-
return c.json({
|
|
143
|
+
return c.json({
|
|
144
|
+
walletId: resolved.activeWallet?.walletId ?? createdWallet.id,
|
|
145
|
+
name: createdWallet.name,
|
|
146
|
+
address: resolved.activeWallet?.address,
|
|
147
|
+
activeWallet: resolved.activeWallet ? toPublicActiveWallet(resolved.activeWallet) : null,
|
|
148
|
+
wallets: resolved.wallets,
|
|
149
|
+
alreadyExisted: false,
|
|
150
|
+
});
|
|
123
151
|
} catch (err: unknown) {
|
|
124
152
|
const message = err instanceof Error ? err.message : "Wallet creation failed";
|
|
125
153
|
return c.json({ error: message }, 500);
|