@akta/dao-cli 0.1.0
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/README.md +137 -0
- package/images/dao_view_styled.png +0 -0
- package/images/fees_view_styled.png +0 -0
- package/images/proposals_view_styled.png +0 -0
- package/images/wallet_view_styled.png +0 -0
- package/install.sh +19 -0
- package/package.json +33 -0
- package/src/commands/info.ts +33 -0
- package/src/commands/proposals.ts +133 -0
- package/src/commands/state.ts +167 -0
- package/src/commands/wallet.ts +356 -0
- package/src/formatting.ts +659 -0
- package/src/index.ts +188 -0
- package/src/output.ts +232 -0
- package/src/sdk.ts +37 -0
- package/src/theme.ts +73 -0
- package/src/tui/app.ts +366 -0
- package/src/tui/input.ts +85 -0
- package/src/tui/panels.ts +133 -0
- package/src/tui/renderer.ts +126 -0
- package/src/tui/terminal.ts +66 -0
- package/src/tui/types.ts +74 -0
- package/src/tui/views/dao.ts +338 -0
- package/src/tui/views/fees.ts +164 -0
- package/src/tui/views/proposal-detail.ts +331 -0
- package/src/tui/views/proposals-list.ts +213 -0
- package/src/tui/views/wallet.ts +560 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import type { AkitaNetwork } from "@akta/sdk";
|
|
2
|
+
import { ProposalActionEnum } from "@akta/sdk/dao";
|
|
3
|
+
import type { DecodedProposalAction } from "@akta/sdk/dao";
|
|
4
|
+
import { renderKV } from "../../output";
|
|
5
|
+
import theme from "../../theme";
|
|
6
|
+
import {
|
|
7
|
+
truncateAddress,
|
|
8
|
+
formatTimestamp,
|
|
9
|
+
formatCID,
|
|
10
|
+
proposalStatusLabel,
|
|
11
|
+
proposalActionLabel,
|
|
12
|
+
formatMicroAlgo,
|
|
13
|
+
formatBasisPoints,
|
|
14
|
+
formatDuration,
|
|
15
|
+
formatBigInt,
|
|
16
|
+
colorStatus,
|
|
17
|
+
colorBool,
|
|
18
|
+
delegationTypeLabel,
|
|
19
|
+
isZeroAddress,
|
|
20
|
+
resolveAppName,
|
|
21
|
+
resolveMethodSelector,
|
|
22
|
+
decodeFieldUpdate,
|
|
23
|
+
} from "../../formatting";
|
|
24
|
+
import { renderPanel, renderPanelRow, splitWidth } from "../panels";
|
|
25
|
+
|
|
26
|
+
// ── Allowance type mapping ─────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const ALLOWANCE_TYPES: Record<number, string> = { 1: "Flat", 2: "Window", 3: "Drip" };
|
|
29
|
+
|
|
30
|
+
// ── Proposal detail rendering ──────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export interface ProposalData {
|
|
33
|
+
status: number;
|
|
34
|
+
cid: Uint8Array;
|
|
35
|
+
votes: { approvals: bigint; rejections: bigint; abstains: bigint };
|
|
36
|
+
creator: string;
|
|
37
|
+
votingTs: bigint;
|
|
38
|
+
created: bigint;
|
|
39
|
+
feesPaid: bigint;
|
|
40
|
+
actions: DecodedProposalAction[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render all detail panels for a decoded proposal.
|
|
45
|
+
* Returns an array of lines (stacked panels: meta, votes, actions).
|
|
46
|
+
*/
|
|
47
|
+
export function renderProposalDetail(
|
|
48
|
+
proposal: ProposalData,
|
|
49
|
+
proposalId: bigint,
|
|
50
|
+
network: AkitaNetwork,
|
|
51
|
+
width: number,
|
|
52
|
+
): string[] {
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
|
|
55
|
+
// Meta + Votes side-by-side if wide enough, otherwise stacked
|
|
56
|
+
const metaContent = renderKV([
|
|
57
|
+
["Status", colorStatus(proposalStatusLabel(proposal.status))],
|
|
58
|
+
["Creator", truncateAddress(proposal.creator)],
|
|
59
|
+
["CID", formatCID(proposal.cid)],
|
|
60
|
+
["Created", formatTimestamp(proposal.created)],
|
|
61
|
+
["Voting", formatTimestamp(proposal.votingTs)],
|
|
62
|
+
["Fees Paid", formatMicroAlgo(proposal.feesPaid)],
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const votesContent = renderKV([
|
|
66
|
+
["Approvals", formatBigInt(proposal.votes.approvals)],
|
|
67
|
+
["Rejections", formatBigInt(proposal.votes.rejections)],
|
|
68
|
+
["Abstains", formatBigInt(proposal.votes.abstains)],
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
if (width >= 60) {
|
|
72
|
+
const [leftW, rightW] = splitWidth(width, 2);
|
|
73
|
+
const metaPanel = renderPanel(metaContent, { title: `Proposal #${proposalId}`, width: leftW });
|
|
74
|
+
const votesPanel = renderPanel(votesContent, { title: "Votes", width: rightW });
|
|
75
|
+
lines.push(...renderPanelRow([metaPanel, votesPanel]));
|
|
76
|
+
} else {
|
|
77
|
+
lines.push(...renderPanel(metaContent, { title: `Proposal #${proposalId}`, width }));
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push(...renderPanel(votesContent, { title: "Votes", width }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Actions — one panel each
|
|
83
|
+
for (let i = 0; i < proposal.actions.length; i++) {
|
|
84
|
+
lines.push("");
|
|
85
|
+
lines.push(...renderActionPanel(proposal.actions[i], i, network, width));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Action panel rendering ─────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function colorActionLabel(type: number): string {
|
|
94
|
+
const label = proposalActionLabel(type);
|
|
95
|
+
switch (type) {
|
|
96
|
+
case ProposalActionEnum.AddPlugin:
|
|
97
|
+
case ProposalActionEnum.AddNamedPlugin:
|
|
98
|
+
case ProposalActionEnum.AddAllowances:
|
|
99
|
+
case ProposalActionEnum.NewEscrow:
|
|
100
|
+
return theme.actionAdd(label);
|
|
101
|
+
case ProposalActionEnum.RemovePlugin:
|
|
102
|
+
case ProposalActionEnum.RemoveNamedPlugin:
|
|
103
|
+
case ProposalActionEnum.RemoveAllowances:
|
|
104
|
+
case ProposalActionEnum.RemoveExecutePlugin:
|
|
105
|
+
return theme.actionRemove(label);
|
|
106
|
+
case ProposalActionEnum.UpgradeApp:
|
|
107
|
+
case ProposalActionEnum.ExecutePlugin:
|
|
108
|
+
case ProposalActionEnum.ToggleEscrowLock:
|
|
109
|
+
case ProposalActionEnum.UpdateFields:
|
|
110
|
+
return theme.actionModify(label);
|
|
111
|
+
default:
|
|
112
|
+
return label;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderActionPanel(
|
|
117
|
+
action: DecodedProposalAction,
|
|
118
|
+
idx: number,
|
|
119
|
+
network: AkitaNetwork,
|
|
120
|
+
width: number,
|
|
121
|
+
): string[] {
|
|
122
|
+
const title = `Action ${idx + 1}: ${colorActionLabel(action.type)}`;
|
|
123
|
+
const content = renderKV(getActionPairs(action, network));
|
|
124
|
+
|
|
125
|
+
appendActionExtras(action, content, network);
|
|
126
|
+
|
|
127
|
+
return renderPanel(content, { title, width });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getActionPairs(
|
|
131
|
+
action: DecodedProposalAction,
|
|
132
|
+
network: AkitaNetwork,
|
|
133
|
+
): [string, string][] {
|
|
134
|
+
switch (action.type) {
|
|
135
|
+
case ProposalActionEnum.UpgradeApp:
|
|
136
|
+
return [
|
|
137
|
+
["App", resolveAppName(action.app, network)],
|
|
138
|
+
["Exec Key", formatBytes(action.executionKey)],
|
|
139
|
+
["Groups", `${action.groups.length}`],
|
|
140
|
+
["First Valid", action.firstValid.toString()],
|
|
141
|
+
["Last Valid", action.lastValid.toString()],
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
case ProposalActionEnum.AddPlugin:
|
|
145
|
+
return addPluginPairs(action, network);
|
|
146
|
+
|
|
147
|
+
case ProposalActionEnum.AddNamedPlugin:
|
|
148
|
+
return [["Name", action.name], ...addPluginPairs(action, network)];
|
|
149
|
+
|
|
150
|
+
case ProposalActionEnum.ExecutePlugin:
|
|
151
|
+
return [
|
|
152
|
+
["Plugin", resolveAppName(action.plugin, network)],
|
|
153
|
+
["Escrow", action.escrow || "(default)"],
|
|
154
|
+
["Exec Key", formatBytes(action.executionKey)],
|
|
155
|
+
["Groups", `${action.groups.length}`],
|
|
156
|
+
["First Valid", action.firstValid.toString()],
|
|
157
|
+
["Last Valid", action.lastValid.toString()],
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
case ProposalActionEnum.RemoveExecutePlugin:
|
|
161
|
+
return [
|
|
162
|
+
["Exec Key", formatBytes(action.executionKey)],
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
case ProposalActionEnum.RemovePlugin:
|
|
166
|
+
return [
|
|
167
|
+
["Plugin", resolveAppName(action.plugin, network)],
|
|
168
|
+
["Caller", formatCaller(action.caller)],
|
|
169
|
+
["Escrow", action.escrow || "(default)"],
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
case ProposalActionEnum.RemoveNamedPlugin:
|
|
173
|
+
return [
|
|
174
|
+
["Name", action.name],
|
|
175
|
+
["Plugin", resolveAppName(action.plugin, network)],
|
|
176
|
+
["Caller", formatCaller(action.caller)],
|
|
177
|
+
["Escrow", action.escrow || "(default)"],
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
case ProposalActionEnum.AddAllowances:
|
|
181
|
+
return [
|
|
182
|
+
["Escrow", action.escrow || "(default)"],
|
|
183
|
+
["Count", `${action.allowances.length}`],
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
case ProposalActionEnum.RemoveAllowances:
|
|
187
|
+
return [
|
|
188
|
+
["Escrow", action.escrow || "(default)"],
|
|
189
|
+
["Assets", action.assets.map((a) => a.toString()).join(", ")],
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
case ProposalActionEnum.NewEscrow:
|
|
193
|
+
return [["Escrow", action.escrow]];
|
|
194
|
+
|
|
195
|
+
case ProposalActionEnum.ToggleEscrowLock:
|
|
196
|
+
return [["Escrow", action.escrow]];
|
|
197
|
+
|
|
198
|
+
case ProposalActionEnum.UpdateFields: {
|
|
199
|
+
const decoded = decodeFieldUpdate(action.field, action.value, network);
|
|
200
|
+
return [
|
|
201
|
+
["Field", `${action.field} (${decoded.label})`],
|
|
202
|
+
...decoded.pairs,
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
default:
|
|
207
|
+
return [["Type", String((action as any).type)]];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── AddPlugin helper ───────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
function addPluginPairs(
|
|
214
|
+
action: {
|
|
215
|
+
plugin: bigint;
|
|
216
|
+
caller: string;
|
|
217
|
+
escrow: string;
|
|
218
|
+
delegationType: number;
|
|
219
|
+
lastValid: bigint;
|
|
220
|
+
cooldown: bigint;
|
|
221
|
+
useRounds: boolean;
|
|
222
|
+
useExecutionKey: boolean;
|
|
223
|
+
coverFees: boolean;
|
|
224
|
+
defaultToEscrow: boolean;
|
|
225
|
+
fee: bigint;
|
|
226
|
+
power: bigint;
|
|
227
|
+
duration: bigint;
|
|
228
|
+
participation: bigint;
|
|
229
|
+
approval: bigint;
|
|
230
|
+
sourceLink: string;
|
|
231
|
+
},
|
|
232
|
+
network: AkitaNetwork,
|
|
233
|
+
): [string, string][] {
|
|
234
|
+
const pairs: [string, string][] = [
|
|
235
|
+
["Plugin", resolveAppName(action.plugin, network)],
|
|
236
|
+
["Caller", formatCaller(action.caller)],
|
|
237
|
+
["Escrow", action.escrow || "(default)"],
|
|
238
|
+
["Delegation", delegationTypeLabel(action.delegationType)],
|
|
239
|
+
["Cover Fees", colorBool(action.coverFees)],
|
|
240
|
+
["Default to Escrow", colorBool(action.defaultToEscrow)],
|
|
241
|
+
["Use Exec Key", colorBool(action.useExecutionKey)],
|
|
242
|
+
["Use Rounds", colorBool(action.useRounds)],
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
if (action.cooldown > 0n) {
|
|
246
|
+
pairs.push(["Cooldown", formatDuration(action.cooldown)]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const MAX_UINT64 = BigInt("18446744073709551615");
|
|
250
|
+
if (action.lastValid < MAX_UINT64) {
|
|
251
|
+
pairs.push(["Last Valid", action.useRounds ? action.lastValid.toString() : formatTimestamp(action.lastValid)]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (action.sourceLink) {
|
|
255
|
+
pairs.push(["Source", action.sourceLink]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (action.useExecutionKey) {
|
|
259
|
+
pairs.push(
|
|
260
|
+
["Proposal Fee", formatMicroAlgo(action.fee)],
|
|
261
|
+
["Voting Power", formatBigInt(action.power)],
|
|
262
|
+
["Duration", formatDuration(action.duration)],
|
|
263
|
+
["Participation", formatBasisPoints(action.participation)],
|
|
264
|
+
["Approval", formatBasisPoints(action.approval)],
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return pairs;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Extra content (methods, allowances) ────────────────────────
|
|
272
|
+
|
|
273
|
+
function appendActionExtras(
|
|
274
|
+
action: DecodedProposalAction,
|
|
275
|
+
content: string[],
|
|
276
|
+
_network: AkitaNetwork,
|
|
277
|
+
): void {
|
|
278
|
+
if (
|
|
279
|
+
action.type === ProposalActionEnum.AddPlugin ||
|
|
280
|
+
action.type === ProposalActionEnum.AddNamedPlugin
|
|
281
|
+
) {
|
|
282
|
+
if (action.methods.length > 0) {
|
|
283
|
+
const methods = action.methods.map(([selector, cooldown]) => {
|
|
284
|
+
const name = resolveMethodSelector(selector);
|
|
285
|
+
return cooldown > 0n ? `${name} (${formatDuration(cooldown)})` : name;
|
|
286
|
+
});
|
|
287
|
+
content.push("");
|
|
288
|
+
content.push(" " + theme.label("Methods: ") + methods.join(", "));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (action.allowances.length > 0) {
|
|
292
|
+
content.push("");
|
|
293
|
+
content.push(" " + theme.label("Allowances:"));
|
|
294
|
+
for (const [asset, type, amount, max, interval, useRounds] of action.allowances) {
|
|
295
|
+
const typeName = ALLOWANCE_TYPES[type] ?? `Type(${type})`;
|
|
296
|
+
const parts = [`Asset ${asset}`, typeName];
|
|
297
|
+
if (type === 1) parts.push(`amount: ${formatBigInt(amount)}`);
|
|
298
|
+
else if (type === 2) parts.push(`amount: ${formatBigInt(amount)}`, `interval: ${formatDuration(interval)}`);
|
|
299
|
+
else if (type === 3) parts.push(`rate: ${formatBigInt(amount)}`, `max: ${formatBigInt(max)}`, `interval: ${formatDuration(interval)}`);
|
|
300
|
+
if (useRounds) parts.push("(rounds)");
|
|
301
|
+
content.push(" " + theme.label(parts.join(" · ")));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (action.type === ProposalActionEnum.AddAllowances && action.allowances.length > 0) {
|
|
307
|
+
content.push("");
|
|
308
|
+
for (const [asset, type, amount, max, interval, useRounds] of action.allowances) {
|
|
309
|
+
const typeName = ALLOWANCE_TYPES[type] ?? `Type(${type})`;
|
|
310
|
+
const parts = [`Asset ${asset}`, typeName];
|
|
311
|
+
if (type === 1) parts.push(`amount: ${formatBigInt(amount)}`);
|
|
312
|
+
else if (type === 2) parts.push(`amount: ${formatBigInt(amount)}`, `interval: ${formatDuration(interval)}`);
|
|
313
|
+
else if (type === 3) parts.push(`rate: ${formatBigInt(amount)}`, `max: ${formatBigInt(max)}`, `interval: ${formatDuration(interval)}`);
|
|
314
|
+
if (useRounds) parts.push("(rounds)");
|
|
315
|
+
content.push(" " + theme.label(parts.join(" · ")));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
function formatCaller(caller: string): string {
|
|
323
|
+
if (!caller || isZeroAddress(caller)) return theme.globalCaller("Global");
|
|
324
|
+
return truncateAddress(caller);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function formatBytes(bytes: Uint8Array): string {
|
|
328
|
+
const hex = Buffer.from(bytes).toString("hex");
|
|
329
|
+
if (hex.length <= 16) return hex;
|
|
330
|
+
return hex.slice(0, 16) + "...";
|
|
331
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { renderColumns } from "../../output";
|
|
2
|
+
import theme from "../../theme";
|
|
3
|
+
import {
|
|
4
|
+
truncateAddress,
|
|
5
|
+
formatTimestamp,
|
|
6
|
+
proposalStatusLabel,
|
|
7
|
+
colorStatus,
|
|
8
|
+
} from "../../formatting";
|
|
9
|
+
import { renderPanel } from "../panels";
|
|
10
|
+
import { renderProposalDetail } from "./proposal-detail";
|
|
11
|
+
import type { ProposalData } from "./proposal-detail";
|
|
12
|
+
import type { LoadResult, View, ViewContext } from "../types";
|
|
13
|
+
|
|
14
|
+
// ── Cached data ────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
interface ListEntry {
|
|
17
|
+
id: bigint;
|
|
18
|
+
status: number;
|
|
19
|
+
creator: string;
|
|
20
|
+
votes: { approvals: bigint; rejections: bigint; abstains: bigint };
|
|
21
|
+
actionCount: number;
|
|
22
|
+
created: bigint;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const LIST_CACHE_TTL = 120_000; // 2 minutes
|
|
26
|
+
let _listCacheTs = 0;
|
|
27
|
+
let _cachedEntries: ListEntry[] = [];
|
|
28
|
+
|
|
29
|
+
const DETAIL_CACHE_TTL = 60_000;
|
|
30
|
+
const _detailCache = new Map<string, { ts: number; data: ProposalData }>();
|
|
31
|
+
|
|
32
|
+
export function invalidateProposalsCache(): void {
|
|
33
|
+
_listCacheTs = 0;
|
|
34
|
+
_detailCache.clear();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Module-level cursor state ──────────────────────────────────
|
|
38
|
+
|
|
39
|
+
let _proposalIds: bigint[] = [];
|
|
40
|
+
let _cursor = 0;
|
|
41
|
+
|
|
42
|
+
export function getProposalIdAtCursor(cursor?: number): bigint | undefined {
|
|
43
|
+
return _proposalIds[cursor ?? _cursor];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getProposalCount(): number {
|
|
47
|
+
return _proposalIds.length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function cycleProposalCursor(dir: 1 | -1): void {
|
|
51
|
+
if (_proposalIds.length <= 0) return;
|
|
52
|
+
_cursor = (_cursor + dir + _proposalIds.length) % _proposalIds.length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getProposalCursor(): number {
|
|
56
|
+
return _cursor;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resetProposalCursor(): void {
|
|
60
|
+
_cursor = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── View ───────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export const proposalsListView: View = {
|
|
66
|
+
selectable: true,
|
|
67
|
+
|
|
68
|
+
selectableCount(lines: string[]): number {
|
|
69
|
+
return _proposalIds.length;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async load(ctx: ViewContext): Promise<string[] | LoadResult> {
|
|
73
|
+
const { dao, network, width } = ctx;
|
|
74
|
+
|
|
75
|
+
// Fetch proposals list (cached)
|
|
76
|
+
if (Date.now() - _listCacheTs > LIST_CACHE_TTL) {
|
|
77
|
+
// Read proposals in small batches to avoid 429 rate limits
|
|
78
|
+
// (getMap fires all box reads in parallel, overwhelming the API)
|
|
79
|
+
const globalState = await dao.getGlobalState();
|
|
80
|
+
const count = Number(globalState.proposalId ?? 0n);
|
|
81
|
+
const BATCH = 5;
|
|
82
|
+
const raw: [bigint, any][] = [];
|
|
83
|
+
for (let i = 0; i < count; i += BATCH) {
|
|
84
|
+
const batch = Array.from(
|
|
85
|
+
{ length: Math.min(BATCH, count - i) },
|
|
86
|
+
(_, j) => BigInt(i + j),
|
|
87
|
+
);
|
|
88
|
+
const results = await Promise.all(
|
|
89
|
+
batch.map(async (id) => {
|
|
90
|
+
try {
|
|
91
|
+
const p = await dao.client.state.box.proposals.value(id);
|
|
92
|
+
return p ? ([id, p] as const) : null;
|
|
93
|
+
} catch { return null; }
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
for (const r of results) if (r) raw.push([r[0], r[1]]);
|
|
97
|
+
}
|
|
98
|
+
raw.sort((a, b) => (b[0] > a[0] ? 1 : b[0] < a[0] ? -1 : 0));
|
|
99
|
+
|
|
100
|
+
_cachedEntries = raw.map(([id, p]) => ({
|
|
101
|
+
id,
|
|
102
|
+
status: p.status,
|
|
103
|
+
creator: p.creator,
|
|
104
|
+
votes: p.votes,
|
|
105
|
+
actionCount: p.actions.length,
|
|
106
|
+
created: p.created,
|
|
107
|
+
}));
|
|
108
|
+
_listCacheTs = Date.now();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_proposalIds = _cachedEntries.map((e) => e.id);
|
|
112
|
+
if (_cursor >= _proposalIds.length) _cursor = 0;
|
|
113
|
+
|
|
114
|
+
if (_cachedEntries.length === 0) {
|
|
115
|
+
return [
|
|
116
|
+
"",
|
|
117
|
+
...renderPanel([" No proposals found."], { title: "Proposals", width }),
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fetch selected proposal detail (cached)
|
|
122
|
+
const selectedId = _proposalIds[_cursor];
|
|
123
|
+
let detailData: ProposalData | null = null;
|
|
124
|
+
|
|
125
|
+
if (selectedId !== undefined) {
|
|
126
|
+
const cacheKey = selectedId.toString();
|
|
127
|
+
const cached = _detailCache.get(cacheKey);
|
|
128
|
+
if (cached && Date.now() - cached.ts < DETAIL_CACHE_TTL) {
|
|
129
|
+
detailData = cached.data;
|
|
130
|
+
} else {
|
|
131
|
+
try {
|
|
132
|
+
const data = await dao.getProposal(selectedId);
|
|
133
|
+
detailData = data;
|
|
134
|
+
_detailCache.set(cacheKey, { ts: Date.now(), data });
|
|
135
|
+
} catch {
|
|
136
|
+
// Detail fetch failed — show list only
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Narrow: single column (list, then detail below)
|
|
142
|
+
if (width < 80) {
|
|
143
|
+
return renderSingleColumn(_cachedEntries, detailData, selectedId, network, width);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Wide: two-panel layout (left scrolls, right fixed)
|
|
147
|
+
const listW = Math.floor((width - 2) * 0.35);
|
|
148
|
+
const detailW = width - listW - 2;
|
|
149
|
+
|
|
150
|
+
const listPanel = renderListPanel(_cachedEntries, listW);
|
|
151
|
+
|
|
152
|
+
let detailLines: string[];
|
|
153
|
+
if (detailData && selectedId !== undefined) {
|
|
154
|
+
detailLines = renderProposalDetail(detailData, selectedId, network, detailW);
|
|
155
|
+
} else {
|
|
156
|
+
detailLines = renderPanel([" Select a proposal with [ ] keys."], { title: "Proposal Detail", width: detailW });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
lines: ["", ...listPanel],
|
|
161
|
+
fixedRight: ["", ...detailLines],
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// ── Left panel: compact proposals list ─────────────────────────
|
|
167
|
+
|
|
168
|
+
function renderListPanel(entries: ListEntry[], width: number): string[] {
|
|
169
|
+
const rows = entries.map((e, i) => {
|
|
170
|
+
const marker = i === _cursor ? theme.cursor("▸ ") : " ";
|
|
171
|
+
return [
|
|
172
|
+
marker + e.id.toString(),
|
|
173
|
+
colorStatus(proposalStatusLabel(e.status)),
|
|
174
|
+
e.actionCount.toString(),
|
|
175
|
+
];
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const content = renderColumns([" ID", "Status", "Act"], rows);
|
|
179
|
+
return renderPanel(content, { title: `Proposals (${entries.length})`, width });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Single-column fallback ─────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function renderSingleColumn(
|
|
185
|
+
entries: ListEntry[],
|
|
186
|
+
detailData: ProposalData | null,
|
|
187
|
+
selectedId: bigint | undefined,
|
|
188
|
+
network: string,
|
|
189
|
+
width: number,
|
|
190
|
+
): string[] {
|
|
191
|
+
const lines: string[] = [""];
|
|
192
|
+
|
|
193
|
+
// Compact list
|
|
194
|
+
const rows = entries.map((e, i) => {
|
|
195
|
+
const marker = i === _cursor ? theme.cursor("▸ ") : " ";
|
|
196
|
+
return [
|
|
197
|
+
marker + e.id.toString(),
|
|
198
|
+
colorStatus(proposalStatusLabel(e.status)),
|
|
199
|
+
truncateAddress(e.creator),
|
|
200
|
+
formatTimestamp(e.created),
|
|
201
|
+
];
|
|
202
|
+
});
|
|
203
|
+
const listContent = renderColumns([" ID", "Status", "Creator", "Created"], rows);
|
|
204
|
+
lines.push(...renderPanel(listContent, { title: `Proposals (${entries.length})`, width }));
|
|
205
|
+
|
|
206
|
+
// Selected proposal detail below
|
|
207
|
+
if (detailData && selectedId !== undefined) {
|
|
208
|
+
lines.push("");
|
|
209
|
+
lines.push(...renderProposalDetail(detailData, selectedId, network as any, width));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return lines;
|
|
213
|
+
}
|