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.
@@ -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 type { Overlay } from "./overlays";
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 = !opts.staleExport && (!!cut.finalImagePath || !!cut.exportedAt);
49
- const uploaded = !opts.staleExport && (!!cut.uploadedUrl || !!cut.uploadedCid);
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 || !!cut.narration?.trim() || !!cut.sfx?.trim(),
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({ type: "speech", speaker: d.speaker, text: d.text.trim(), key: `speech-${i}` });
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({ type: "narration", text: cut.narration.trim(), key: "narration" });
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`/`withText` count IMAGE
42
- * cuts only (text panels are excluded), so the workflow coach (#429) can tell
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
  /**
@@ -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 { listAgentWallets, getBaseAddress } from "../../lib/ows/wallet";
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 wallets = listAgentWallets();
86
- const wallet = wallets.find((w) => w.name.startsWith("plotlink-writer"));
85
+ const resolvedWallet = await resolveActiveWallet();
86
+ const wallet = resolvedWallet.activeWallet;
87
87
  if (wallet) {
88
- const address = getBaseAddress(wallet);
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),
@@ -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 { listAgentWallets, getBaseAddress } from "../../lib/ows/wallet";
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 wallets = listAgentWallets();
55
- const wallet = wallets.find((w) => w.name.startsWith("plotlink-writer"));
54
+ const resolvedWallet = await resolveActiveWallet();
55
+ const wallet = resolvedWallet.activeWallet;
56
56
  if (!wallet) {
57
- return c.json({ ready: false, error: "No OWS wallet found" });
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 = getBaseAddress(wallet);
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 wallets;
271
+ // Get active wallet
272
+ let wallet;
264
273
  try {
265
- wallets = listAgentWallets();
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] listAgentWallets error:", err);
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 wallets = listAgentWallets();
373
- const wallet = wallets.find((w) => w.name.startsWith("plotlink-writer"));
374
- if (!wallet) return c.json({ error: "No OWS wallet" }, 400);
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 = getBaseAddress(wallet);
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 wallets = listAgentWallets();
411
- const wallet = wallets.find((w) => w.name.startsWith("plotlink-writer"));
412
- if (!wallet) return c.json({ error: "No OWS wallet" }, 400);
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 = getBaseAddress(wallet);
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 wallets = listAgentWallets();
446
- const wallet = wallets.find((w) => w.name.startsWith("plotlink-writer"));
447
- if (!wallet) return c.json({ error: "No OWS wallet" }, 400);
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 = getBaseAddress(wallet);
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<{
@@ -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 { listAgentWallets, getBaseAddress } from "../../lib/ows/wallet";
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(): Record<string, unknown> {
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: Record<string, unknown>) {
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 wallets = listAgentWallets();
47
- const wallet = wallets.find((w) => w.name.startsWith("plotlink-writer"));
48
- if (!wallet) return c.json({ error: "No OWS wallet found. Create one in Wallet settings first." }, 400);
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 = getBaseAddress(wallet);
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 passphrase = process.env.OWS_PASSPHRASE;
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.agentId ? Number(config.agentId) : undefined,
67
- agentName: (config.agentName as string) || undefined,
68
- agentDescription: (config.agentDescription as string) || undefined,
69
- agentGenre: (config.agentGenre as string) || undefined,
70
- agentLlmModel: (config.agentLlmModel as string) || undefined,
71
- agentRegisteredBy: (config.agentRegisteredBy as string) || undefined,
72
- agentRegisteredAt: (config.agentRegisteredAt as string) || undefined,
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 wallets = listAgentWallets();
93
- const wallet = wallets.find((w) => w.name.startsWith("plotlink-writer"));
94
- if (!wallet) return c.json({ error: "No OWS wallet found. Create one in Wallet settings first." }, 400);
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 = getBaseAddress(wallet);
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 wallets = listAgentWallets();
188
- const wallet = wallets.find((w) => w.name.startsWith("plotlink-writer"));
189
- if (!wallet) return c.json({ linked: false, error: "No 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 = getBaseAddress(wallet);
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.agentId) {
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
  }
@@ -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 { getAgentWallet, getBaseAddress } = await import("../../lib/ows/wallet");
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: plotlinkWallet.id,
88
- name: plotlinkWallet.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: plotlinkWallet.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, getBaseAddress, listAgentWallets } = await import("../../lib/ows/wallet");
136
+ const { createAgentWallet, listAgentWallets } = await import("../../lib/ows/wallet");
110
137
 
111
- // Check if wallet already exists
112
138
  const wallets = listAgentWallets();
113
- const existing = wallets.find((w) => w.name.startsWith("plotlink-writer"));
114
- if (existing) {
115
- const address = getBaseAddress(existing);
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({ walletId: wallet.id, address, alreadyExisted: false });
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);