plotlink-ows 1.2.94 → 1.2.95
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/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 +124 -86
- package/app/web/components/Dashboard.tsx +15 -6
- package/app/web/components/LetteringEditor.tsx +55 -14
- package/app/web/components/PreviewPanel.tsx +2 -1
- package/app/web/components/StoriesPage.tsx +35 -0
- 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-che5mMWc.js} +1 -1
- package/app/web/dist/assets/index-CcfChGEK.css +32 -0
- package/app/web/dist/assets/index-Dc2TQ3Ij.js +143 -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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { WalletInfo } from "../../lib/ows/types";
|
|
2
|
+
import { getBaseAddress, listAgentWallets } from "../../lib/ows/wallet";
|
|
3
|
+
import { db } from "../db";
|
|
4
|
+
|
|
5
|
+
const ACTIVE_WALLET_SETTING_KEY = "activeOwsWallet.v1";
|
|
6
|
+
const PLOTLINK_WALLET_PREFIX = "plotlink-writer";
|
|
7
|
+
|
|
8
|
+
export interface StoredWalletSelection {
|
|
9
|
+
walletId?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
address?: string;
|
|
12
|
+
source: "ows";
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WalletChoice {
|
|
17
|
+
walletId?: string;
|
|
18
|
+
name: string;
|
|
19
|
+
address?: string;
|
|
20
|
+
normalizedAddress?: string;
|
|
21
|
+
source: "ows";
|
|
22
|
+
label: string;
|
|
23
|
+
recognized: boolean;
|
|
24
|
+
active: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ActiveWallet {
|
|
28
|
+
wallet: WalletInfo;
|
|
29
|
+
walletId?: string;
|
|
30
|
+
name: string;
|
|
31
|
+
address: string;
|
|
32
|
+
normalizedAddress: string;
|
|
33
|
+
source: "ows";
|
|
34
|
+
label: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PublicActiveWallet {
|
|
38
|
+
walletId?: string;
|
|
39
|
+
name: string;
|
|
40
|
+
address: string;
|
|
41
|
+
normalizedAddress: string;
|
|
42
|
+
source: "ows";
|
|
43
|
+
label: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ActiveWalletResolution {
|
|
47
|
+
activeWallet: ActiveWallet | null;
|
|
48
|
+
wallets: WalletChoice[];
|
|
49
|
+
selectionRequired: boolean;
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeAddress(address: string | undefined): string | undefined {
|
|
54
|
+
const trimmed = address?.trim();
|
|
55
|
+
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getWalletId(wallet: WalletInfo): string | undefined {
|
|
59
|
+
const maybeId = (wallet as WalletInfo & { id?: unknown }).id;
|
|
60
|
+
return typeof maybeId === "string" && maybeId.trim() ? maybeId : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toWalletChoice(wallet: WalletInfo, activeSelection?: StoredWalletSelection): WalletChoice {
|
|
64
|
+
const address = getBaseAddress(wallet);
|
|
65
|
+
const normalizedAddress = normalizeAddress(address);
|
|
66
|
+
const walletId = getWalletId(wallet);
|
|
67
|
+
const recognized = wallet.name.startsWith(PLOTLINK_WALLET_PREFIX);
|
|
68
|
+
return {
|
|
69
|
+
walletId,
|
|
70
|
+
name: wallet.name,
|
|
71
|
+
address: normalizedAddress,
|
|
72
|
+
normalizedAddress,
|
|
73
|
+
source: "ows",
|
|
74
|
+
label: recognized ? "PlotLink writer wallet" : "OWS wallet",
|
|
75
|
+
recognized,
|
|
76
|
+
active: matchesSelection(wallet, address, activeSelection),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function matchesSelection(wallet: WalletInfo, address: string | undefined, selection: StoredWalletSelection | null | undefined): boolean {
|
|
81
|
+
if (!selection) return false;
|
|
82
|
+
const walletId = getWalletId(wallet);
|
|
83
|
+
const normalizedAddress = normalizeAddress(address);
|
|
84
|
+
const selectedAddress = normalizeAddress(selection.address);
|
|
85
|
+
if (selection.walletId && walletId && selection.walletId === walletId) return true;
|
|
86
|
+
if (selectedAddress && normalizedAddress && selectedAddress === normalizedAddress) return true;
|
|
87
|
+
if (selection.name && selection.name === wallet.name) return true;
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function storedSelectionFor(wallet: WalletInfo): StoredWalletSelection {
|
|
92
|
+
const address = normalizeAddress(getBaseAddress(wallet));
|
|
93
|
+
return {
|
|
94
|
+
walletId: getWalletId(wallet),
|
|
95
|
+
name: wallet.name,
|
|
96
|
+
address,
|
|
97
|
+
source: "ows",
|
|
98
|
+
label: wallet.name.startsWith(PLOTLINK_WALLET_PREFIX) ? "PlotLink writer wallet" : "OWS wallet",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function readStoredSelection(): Promise<StoredWalletSelection | null> {
|
|
103
|
+
try {
|
|
104
|
+
const row = await db.setting.findUnique({ where: { key: ACTIVE_WALLET_SETTING_KEY } });
|
|
105
|
+
if (!row?.value) return null;
|
|
106
|
+
const parsed = JSON.parse(row.value) as Partial<StoredWalletSelection>;
|
|
107
|
+
if (parsed.source !== "ows") return null;
|
|
108
|
+
return {
|
|
109
|
+
walletId: typeof parsed.walletId === "string" ? parsed.walletId : undefined,
|
|
110
|
+
name: typeof parsed.name === "string" ? parsed.name : undefined,
|
|
111
|
+
address: normalizeAddress(parsed.address),
|
|
112
|
+
source: "ows",
|
|
113
|
+
label: typeof parsed.label === "string" ? parsed.label : undefined,
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function writeStoredSelection(selection: StoredWalletSelection): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
await db.setting.upsert({
|
|
123
|
+
where: { key: ACTIVE_WALLET_SETTING_KEY },
|
|
124
|
+
create: { key: ACTIVE_WALLET_SETTING_KEY, value: JSON.stringify(selection) },
|
|
125
|
+
update: { value: JSON.stringify(selection) },
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
// The app can still operate in legacy single-wallet mode if persistence is
|
|
129
|
+
// temporarily unavailable; signing never depends on this write succeeding.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findSelectedWallet(wallets: WalletInfo[], selection: StoredWalletSelection | null): WalletInfo | null {
|
|
134
|
+
if (!selection) return null;
|
|
135
|
+
return wallets.find((wallet) => matchesSelection(wallet, getBaseAddress(wallet), selection)) ?? null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toActiveWallet(wallet: WalletInfo): ActiveWallet | null {
|
|
139
|
+
const address = normalizeAddress(getBaseAddress(wallet));
|
|
140
|
+
if (!address) return null;
|
|
141
|
+
return {
|
|
142
|
+
wallet,
|
|
143
|
+
walletId: getWalletId(wallet),
|
|
144
|
+
name: wallet.name,
|
|
145
|
+
address,
|
|
146
|
+
normalizedAddress: address,
|
|
147
|
+
source: "ows",
|
|
148
|
+
label: wallet.name.startsWith(PLOTLINK_WALLET_PREFIX) ? "PlotLink writer wallet" : "OWS wallet",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function listWalletChoices(): Promise<WalletChoice[]> {
|
|
153
|
+
const wallets = listAgentWallets();
|
|
154
|
+
const selection = await readStoredSelection();
|
|
155
|
+
return wallets.map((wallet) => toWalletChoice(wallet, selection));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function resolveActiveWallet(): Promise<ActiveWalletResolution> {
|
|
159
|
+
const wallets = listAgentWallets();
|
|
160
|
+
const selection = await readStoredSelection();
|
|
161
|
+
const storedWallet = findSelectedWallet(wallets, selection);
|
|
162
|
+
const activeFromStored = storedWallet ? toActiveWallet(storedWallet) : null;
|
|
163
|
+
if (activeFromStored) {
|
|
164
|
+
return {
|
|
165
|
+
activeWallet: activeFromStored,
|
|
166
|
+
wallets: wallets.map((wallet) => toWalletChoice(wallet, storedSelectionFor(storedWallet))),
|
|
167
|
+
selectionRequired: false,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const evmWallets = wallets.filter((wallet) => Boolean(getBaseAddress(wallet)));
|
|
172
|
+
const recognizedWallets = evmWallets.filter((wallet) => wallet.name.startsWith(PLOTLINK_WALLET_PREFIX));
|
|
173
|
+
const autoSelected = recognizedWallets.length === 1
|
|
174
|
+
? recognizedWallets[0]
|
|
175
|
+
: recognizedWallets.length === 0 && evmWallets.length === 1
|
|
176
|
+
? evmWallets[0]
|
|
177
|
+
: null;
|
|
178
|
+
|
|
179
|
+
if (autoSelected) {
|
|
180
|
+
const stored = storedSelectionFor(autoSelected);
|
|
181
|
+
await writeStoredSelection(stored);
|
|
182
|
+
return {
|
|
183
|
+
activeWallet: toActiveWallet(autoSelected),
|
|
184
|
+
wallets: wallets.map((wallet) => toWalletChoice(wallet, stored)),
|
|
185
|
+
selectionRequired: false,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const choices = wallets.map((wallet) => toWalletChoice(wallet, null));
|
|
190
|
+
const hasSelectableWallets = evmWallets.length > 0;
|
|
191
|
+
return {
|
|
192
|
+
activeWallet: null,
|
|
193
|
+
wallets: choices,
|
|
194
|
+
selectionRequired: hasSelectableWallets,
|
|
195
|
+
error: hasSelectableWallets
|
|
196
|
+
? "Multiple OWS wallets found. Select an active wallet before publishing or signing."
|
|
197
|
+
: "No OWS wallet found",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function selectActiveWallet(input: { walletId?: string; name?: string; address?: string }): Promise<ActiveWalletResolution> {
|
|
202
|
+
const wallets = listAgentWallets();
|
|
203
|
+
const normalizedInputAddress = normalizeAddress(input.address);
|
|
204
|
+
const selected = wallets.find((wallet) => {
|
|
205
|
+
const walletId = getWalletId(wallet);
|
|
206
|
+
const address = normalizeAddress(getBaseAddress(wallet));
|
|
207
|
+
if (input.walletId && walletId && walletId === input.walletId) return true;
|
|
208
|
+
if (normalizedInputAddress && address && address === normalizedInputAddress) return true;
|
|
209
|
+
if (input.name && wallet.name === input.name) return true;
|
|
210
|
+
return false;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (!selected) {
|
|
214
|
+
return {
|
|
215
|
+
activeWallet: null,
|
|
216
|
+
wallets: wallets.map((wallet) => toWalletChoice(wallet, null)),
|
|
217
|
+
selectionRequired: true,
|
|
218
|
+
error: "Selected OWS wallet was not found",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const activeWallet = toActiveWallet(selected);
|
|
223
|
+
if (!activeWallet) {
|
|
224
|
+
return {
|
|
225
|
+
activeWallet: null,
|
|
226
|
+
wallets: wallets.map((wallet) => toWalletChoice(wallet, null)),
|
|
227
|
+
selectionRequired: true,
|
|
228
|
+
error: "Selected OWS wallet has no EVM address",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const stored = storedSelectionFor(selected);
|
|
233
|
+
await writeStoredSelection(stored);
|
|
234
|
+
return {
|
|
235
|
+
activeWallet,
|
|
236
|
+
wallets: wallets.map((wallet) => toWalletChoice(wallet, stored)),
|
|
237
|
+
selectionRequired: false,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function nextPlotlinkWalletName(wallets: WalletInfo[]): string {
|
|
242
|
+
const names = new Set(wallets.map((wallet) => wallet.name));
|
|
243
|
+
if (!names.has(PLOTLINK_WALLET_PREFIX)) return PLOTLINK_WALLET_PREFIX;
|
|
244
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
245
|
+
const name = `${PLOTLINK_WALLET_PREFIX}-${index}`;
|
|
246
|
+
if (!names.has(name)) return name;
|
|
247
|
+
}
|
|
248
|
+
return `${PLOTLINK_WALLET_PREFIX}-${Date.now()}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function toPublicActiveWallet(wallet: ActiveWallet): PublicActiveWallet {
|
|
252
|
+
return {
|
|
253
|
+
walletId: wallet.walletId,
|
|
254
|
+
name: wallet.name,
|
|
255
|
+
address: wallet.address,
|
|
256
|
+
normalizedAddress: wallet.normalizedAddress,
|
|
257
|
+
source: wallet.source,
|
|
258
|
+
label: wallet.label,
|
|
259
|
+
};
|
|
260
|
+
}
|
package/app/lib/cartoon-coach.ts
CHANGED
|
@@ -168,7 +168,7 @@ function coachForEpisode(ep: EpisodeProgress, undetectedClean: number): CartoonC
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// 2) Lettering — place speech bubbles & captions in the cut workspace.
|
|
171
|
-
if (c.withText < c.
|
|
171
|
+
if (c.withText < c.total) {
|
|
172
172
|
return ui("Clean images ready", "Review cuts and start lettering", "open-lettering", file);
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -492,7 +492,7 @@ export interface CartoonCutProgress {
|
|
|
492
492
|
needClean: number;
|
|
493
493
|
/** Of `needClean`, how many have a clean image recorded. */
|
|
494
494
|
withClean: number;
|
|
495
|
-
/**
|
|
495
|
+
/** Cuts with lettering overlays placed. Image cuts still require clean art first; text panels are first-class lettering targets. */
|
|
496
496
|
withText: number;
|
|
497
497
|
/** Cuts (any kind) with an exported final image. */
|
|
498
498
|
exported: number;
|
|
@@ -517,8 +517,8 @@ export function summarizeCutProgress(cuts: Cut[]): CartoonCutProgress {
|
|
|
517
517
|
let uploaded = 0;
|
|
518
518
|
for (const cut of cuts) {
|
|
519
519
|
// Image cuts need a clean image → lettering; text/interstitial panels (#350)
|
|
520
|
-
// do not (they're text on a styled background).
|
|
521
|
-
//
|
|
520
|
+
// do not (they're text on a styled background). Text panels still require
|
|
521
|
+
// lettering overlays before the shared workflow can advance to export (#488).
|
|
522
522
|
if (!isTextPanel(cut)) {
|
|
523
523
|
needClean++;
|
|
524
524
|
// A PNG clean image is a draft intermediate, not a finished clean asset
|
|
@@ -531,6 +531,8 @@ export function summarizeCutProgress(cuts: Cut[]): CartoonCutProgress {
|
|
|
531
531
|
// every cut-list render now (#414), so a bad persisted cut must not crash it.
|
|
532
532
|
if ((cut.overlays?.length ?? 0) > 0) withText++;
|
|
533
533
|
}
|
|
534
|
+
} else if ((cut.overlays?.length ?? 0) > 0) {
|
|
535
|
+
withText++;
|
|
534
536
|
}
|
|
535
537
|
if (cut.finalImagePath && cut.exportedAt) exported++;
|
|
536
538
|
if (cut.uploadedUrl) uploaded++;
|
|
@@ -583,12 +585,12 @@ export function cartoonChecklist(input: { cuts: Cut[]; published?: boolean }): C
|
|
|
583
585
|
const p = summarizeCutProgress(cuts);
|
|
584
586
|
if (p.total === 0) return { steps: [], nextStep: null };
|
|
585
587
|
|
|
586
|
-
// Clean
|
|
587
|
-
// cut including text panels
|
|
588
|
-
//
|
|
588
|
+
// Clean gates only IMAGE cuts (needClean); lettering/export/upload gate EVERY
|
|
589
|
+
// cut including text panels. Text panels need no clean art, but they are still
|
|
590
|
+
// editable lettering targets before export (#488).
|
|
589
591
|
const planDone = p.total > 0;
|
|
590
592
|
const cleanDone = planDone && p.withClean === p.needClean;
|
|
591
|
-
const letterDone = cleanDone && p.withText === p.
|
|
593
|
+
const letterDone = cleanDone && p.withText === p.total;
|
|
592
594
|
const exportDone = letterDone && p.exported === p.total;
|
|
593
595
|
const uploadDone = exportDone && p.uploaded === p.total;
|
|
594
596
|
const publishDone = uploadDone && published;
|
|
@@ -604,13 +606,13 @@ export function cartoonChecklist(input: { cuts: Cut[]; published?: boolean }): C
|
|
|
604
606
|
const order: CartoonStepKey[] = ["plan", "clean", "letter", "export", "upload", "publish"];
|
|
605
607
|
const currentIdx = order.findIndex((k) => !complete[k]);
|
|
606
608
|
|
|
607
|
-
// Clean
|
|
609
|
+
// Clean counts image cuts (needClean); lettering/export/upload count every cut
|
|
608
610
|
// (total). An all-text-panel episode has needClean === 0 → "no image cuts".
|
|
609
611
|
const imageDetail = (done: number) => (p.needClean > 0 ? fraction(done, p.needClean) : "no image cuts");
|
|
610
612
|
const detail: Record<CartoonStepKey, string | null> = {
|
|
611
613
|
plan: fraction(p.total, p.total),
|
|
612
614
|
clean: imageDetail(p.withClean),
|
|
613
|
-
letter:
|
|
615
|
+
letter: fraction(p.withText, p.total),
|
|
614
616
|
export: fraction(p.exported, p.total),
|
|
615
617
|
upload: fraction(p.uploaded, p.total),
|
|
616
618
|
publish: null,
|
|
@@ -702,7 +704,7 @@ export function previewFooterGuidance(ctx: PreviewFooterContext): string | null
|
|
|
702
704
|
if (p.withClean < p.needClean) {
|
|
703
705
|
return "Genesis has a cut plan — generate the clean images for its cuts next.";
|
|
704
706
|
}
|
|
705
|
-
if (p.withText < p.
|
|
707
|
+
if (p.withText < p.total) {
|
|
706
708
|
return "Genesis clean art is ready — review the cuts and add speech bubbles & captions next.";
|
|
707
709
|
}
|
|
708
710
|
if (p.exported < p.total) {
|
|
@@ -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<{
|