@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,560 @@
|
|
|
1
|
+
import { getNetworkAppIds } from "@akta/sdk";
|
|
2
|
+
import type { AkitaNetwork } from "@akta/sdk";
|
|
3
|
+
import algosdk from "algosdk";
|
|
4
|
+
import { renderKV, renderColumns, padEndVisible } from "../../output";
|
|
5
|
+
import theme from "../../theme";
|
|
6
|
+
import {
|
|
7
|
+
truncateAddress,
|
|
8
|
+
getAppName,
|
|
9
|
+
resolveAppName,
|
|
10
|
+
camelToLabel,
|
|
11
|
+
isZeroAddress,
|
|
12
|
+
delegationTypeLabel,
|
|
13
|
+
formatDuration,
|
|
14
|
+
formatTimestamp,
|
|
15
|
+
formatBigInt,
|
|
16
|
+
formatCompact,
|
|
17
|
+
colorBool,
|
|
18
|
+
parsePluginKey,
|
|
19
|
+
resolveMethodSelector,
|
|
20
|
+
} from "../../formatting";
|
|
21
|
+
import { renderPanel, splitWidth } from "../panels";
|
|
22
|
+
import type { LoadResult, View, ViewContext } from "../types";
|
|
23
|
+
|
|
24
|
+
// ── Module-level account selection state ────────────────────────
|
|
25
|
+
|
|
26
|
+
let _accountIdx = 0;
|
|
27
|
+
let _accountCount = 1;
|
|
28
|
+
let _selectedLine = 0; // line of selected account in left panel content
|
|
29
|
+
|
|
30
|
+
export function cycleWalletAccount(dir: 1 | -1): void {
|
|
31
|
+
if (_accountCount <= 1) return;
|
|
32
|
+
_accountIdx = (_accountIdx + dir + _accountCount) % _accountCount;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resetWalletAccount(): void {
|
|
36
|
+
_accountIdx = 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getWalletAccountCount(): number {
|
|
40
|
+
return _accountCount;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getSelectedAccountLine(): number {
|
|
44
|
+
return _selectedLine;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Cached wallet data ──────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
interface WalletCache {
|
|
50
|
+
ts: number;
|
|
51
|
+
globalState: Record<string, unknown>;
|
|
52
|
+
plugins: [string, PluginData][];
|
|
53
|
+
namedPlugins: [string, { plugin: bigint; caller: string; escrow: string }][];
|
|
54
|
+
escrows: [string, { id: bigint; locked: boolean }][];
|
|
55
|
+
allowances: [string, AllowanceData][];
|
|
56
|
+
executions: [string, { firstValid: bigint; lastValid: bigint }][];
|
|
57
|
+
balances: Map<string, { algo: bigint; akta: bigint; bones: bigint; usdc: bigint }>;
|
|
58
|
+
decimals: { akta: number; bones: number; usdc: number };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface PluginData {
|
|
62
|
+
admin: boolean;
|
|
63
|
+
delegationType: number;
|
|
64
|
+
coverFees: boolean;
|
|
65
|
+
canReclaim: boolean;
|
|
66
|
+
useExecutionKey: boolean;
|
|
67
|
+
useRounds: boolean;
|
|
68
|
+
cooldown: bigint;
|
|
69
|
+
lastCalled: bigint;
|
|
70
|
+
start: bigint;
|
|
71
|
+
lastValid: bigint;
|
|
72
|
+
methods: { name: Uint8Array; cooldown: bigint; lastCalled: bigint }[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface AllowanceData {
|
|
76
|
+
type: string;
|
|
77
|
+
amount?: bigint;
|
|
78
|
+
spent?: bigint;
|
|
79
|
+
rate?: bigint;
|
|
80
|
+
max?: bigint;
|
|
81
|
+
interval?: bigint;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const CACHE_TTL = 30_000;
|
|
85
|
+
let _cache: WalletCache | null = null;
|
|
86
|
+
|
|
87
|
+
function invalidateCache(): void {
|
|
88
|
+
_cache = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { invalidateCache as invalidateWalletCache };
|
|
92
|
+
|
|
93
|
+
// ── Hidden fields for wallet info ───────────────────────────────
|
|
94
|
+
|
|
95
|
+
const HIDDEN_WALLET_FIELDS = new Set([
|
|
96
|
+
"spendingAddress",
|
|
97
|
+
"currentPlugin",
|
|
98
|
+
"rekeyIndex",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
// ── Main wallet view ────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export const walletView: View = {
|
|
104
|
+
async load(ctx: ViewContext): Promise<string[] | LoadResult> {
|
|
105
|
+
const { dao, network, width } = ctx;
|
|
106
|
+
const wallet = await dao.getWallet();
|
|
107
|
+
const ids = getNetworkAppIds(network);
|
|
108
|
+
|
|
109
|
+
// Fetch and cache data
|
|
110
|
+
if (!_cache || Date.now() - _cache.ts > CACHE_TTL) {
|
|
111
|
+
const [globalState, plugins, namedPlugins, escrows, allowances, executions] = await Promise.all([
|
|
112
|
+
wallet.getGlobalState(),
|
|
113
|
+
wallet.getPlugins(),
|
|
114
|
+
wallet.getNamedPlugins(),
|
|
115
|
+
wallet.getEscrows(),
|
|
116
|
+
wallet.getAllowances(),
|
|
117
|
+
wallet.getExecutions(),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
const escrowEntries = Array.from(escrows.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
121
|
+
|
|
122
|
+
// Fetch balances + asset decimals in parallel
|
|
123
|
+
const mainAddr = algosdk.getApplicationAddress(wallet.appId).toString();
|
|
124
|
+
const addresses = [mainAddr, ...escrowEntries.map(([, info]) =>
|
|
125
|
+
algosdk.getApplicationAddress(info.id).toString()
|
|
126
|
+
)];
|
|
127
|
+
const balanceMap = new Map<string, { algo: bigint; akta: bigint; bones: bigint; usdc: bigint }>();
|
|
128
|
+
|
|
129
|
+
const [balanceResults, assetDecimals] = await Promise.all([
|
|
130
|
+
Promise.allSettled(
|
|
131
|
+
addresses.map(async (addr) => {
|
|
132
|
+
const accountInfo = await dao.algorand.account.getInformation(addr);
|
|
133
|
+
const algo = BigInt((accountInfo as any).amount);
|
|
134
|
+
const findAsset = (id: bigint) =>
|
|
135
|
+
accountInfo.assets?.find((a: { assetId: bigint }) => a.assetId === id);
|
|
136
|
+
return {
|
|
137
|
+
addr,
|
|
138
|
+
algo,
|
|
139
|
+
akta: findAsset(ids.akta)?.amount ?? 0n,
|
|
140
|
+
bones: findAsset(ids.bones)?.amount ?? 0n,
|
|
141
|
+
usdc: findAsset(ids.usdc)?.amount ?? 0n,
|
|
142
|
+
};
|
|
143
|
+
})
|
|
144
|
+
),
|
|
145
|
+
Promise.all([
|
|
146
|
+
dao.algorand.asset.getById(ids.akta).then(a => a.decimals).catch(() => 0),
|
|
147
|
+
dao.algorand.asset.getById(ids.bones).then(a => a.decimals).catch(() => 0),
|
|
148
|
+
dao.algorand.asset.getById(ids.usdc).then(a => a.decimals).catch(() => 6),
|
|
149
|
+
]),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
for (const result of balanceResults) {
|
|
153
|
+
if (result.status === "fulfilled") {
|
|
154
|
+
const { addr, algo, akta, bones, usdc } = result.value;
|
|
155
|
+
balanceMap.set(addr, { algo, akta, bones, usdc });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_cache = {
|
|
160
|
+
ts: Date.now(),
|
|
161
|
+
globalState: globalState as unknown as Record<string, unknown>,
|
|
162
|
+
plugins: Array.from(plugins.entries()) as [string, PluginData][],
|
|
163
|
+
namedPlugins: Array.from(namedPlugins.entries()) as [string, { plugin: bigint; caller: string; escrow: string }][],
|
|
164
|
+
escrows: escrowEntries as [string, { id: bigint; locked: boolean }][],
|
|
165
|
+
allowances: Array.from(allowances.entries()) as [string, AllowanceData][],
|
|
166
|
+
executions: Array.from(executions.entries()).map(([key, info]) => [
|
|
167
|
+
Buffer.from(key as Uint8Array).toString("hex"),
|
|
168
|
+
info as { firstValid: bigint; lastValid: bigint },
|
|
169
|
+
]),
|
|
170
|
+
balances: balanceMap,
|
|
171
|
+
decimals: { akta: assetDecimals[0], bones: assetDecimals[1], usdc: assetDecimals[2] },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const cache = _cache!;
|
|
176
|
+
|
|
177
|
+
// Build account list: main wallet + escrows
|
|
178
|
+
const mainAddr = algosdk.getApplicationAddress(wallet.appId).toString();
|
|
179
|
+
const accounts: { name: string; address: string; escrowName: string }[] = [
|
|
180
|
+
{ name: "Main Wallet", address: mainAddr, escrowName: "" },
|
|
181
|
+
];
|
|
182
|
+
for (const [escrowName, info] of cache.escrows) {
|
|
183
|
+
accounts.push({
|
|
184
|
+
name: escrowName,
|
|
185
|
+
address: algosdk.getApplicationAddress(info.id).toString(),
|
|
186
|
+
escrowName,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_accountCount = accounts.length;
|
|
191
|
+
if (_accountIdx >= _accountCount) _accountIdx = 0;
|
|
192
|
+
|
|
193
|
+
const selectedAccount = accounts[_accountIdx];
|
|
194
|
+
|
|
195
|
+
if (width < 80) {
|
|
196
|
+
return renderSingleColumn(cache, network, accounts, selectedAccount, wallet.appId);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Two-panel layout
|
|
200
|
+
const [leftW, rightW] = splitWidth(width, 2);
|
|
201
|
+
|
|
202
|
+
const selectedAppId = selectedAccount.escrowName === ""
|
|
203
|
+
? wallet.appId
|
|
204
|
+
: cache.escrows.find(([n]) => n === selectedAccount.escrowName)?.[1].id ?? 0n;
|
|
205
|
+
|
|
206
|
+
const leftLines = renderLeftPanel(cache, network, accounts, leftW, wallet.appId);
|
|
207
|
+
const rightLines = renderRightPanel(cache, network, selectedAccount, selectedAppId, rightW);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
lines: ["", ...leftLines],
|
|
211
|
+
fixedRight: ["", ...rightLines],
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// ── Left panel: Wallet Info + Account List ─────────────────────
|
|
217
|
+
|
|
218
|
+
function renderLeftPanel(
|
|
219
|
+
cache: WalletCache,
|
|
220
|
+
network: AkitaNetwork,
|
|
221
|
+
accounts: { name: string; address: string; escrowName: string }[],
|
|
222
|
+
width: number,
|
|
223
|
+
walletAppId: bigint,
|
|
224
|
+
): string[] {
|
|
225
|
+
const lines: string[] = [];
|
|
226
|
+
|
|
227
|
+
// Wallet Info section
|
|
228
|
+
const infoPairs: [string, string][] = [];
|
|
229
|
+
const gs = cache.globalState;
|
|
230
|
+
|
|
231
|
+
const fields: [string, string][] = [
|
|
232
|
+
["Version", gs.version as string ?? "-"],
|
|
233
|
+
["Admin", typeof gs.admin === "string" ? truncateAddress(gs.admin) : "-"],
|
|
234
|
+
["Domain", typeof gs.domain === "string" && gs.domain ? gs.domain : "-"],
|
|
235
|
+
["Nickname", typeof gs.nickname === "string" && gs.nickname ? gs.nickname : "-"],
|
|
236
|
+
["DAO", typeof gs.akitaDao === "bigint" ? resolveAppName(gs.akitaDao as bigint, network) : "-"],
|
|
237
|
+
["Factory", typeof gs.factoryApp === "bigint" && (gs.factoryApp as bigint) > 0n
|
|
238
|
+
? resolveAppName(gs.factoryApp as bigint, network)
|
|
239
|
+
: "-"],
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
if (typeof gs.referrer === "string" && !isZeroAddress(gs.referrer)) {
|
|
243
|
+
fields.push(["Referrer", truncateAddress(gs.referrer)]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const [label, value] of fields) {
|
|
247
|
+
infoPairs.push([label, value]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const infoContent = renderKV(infoPairs);
|
|
251
|
+
lines.push(...renderPanel(infoContent, { title: "Wallet Info", width }));
|
|
252
|
+
|
|
253
|
+
// Account list section
|
|
254
|
+
lines.push("");
|
|
255
|
+
const accountsStart = lines.length + 1; // +1 for panel top border
|
|
256
|
+
const accountContent: string[] = [];
|
|
257
|
+
|
|
258
|
+
// Main wallet (full width)
|
|
259
|
+
{
|
|
260
|
+
const acct = accounts[0];
|
|
261
|
+
const selected = _accountIdx === 0;
|
|
262
|
+
if (selected) _selectedLine = accountsStart + accountContent.length;
|
|
263
|
+
const marker = selected ? theme.cursor("▸ ") : " ";
|
|
264
|
+
const bal = cache.balances.get(acct.address);
|
|
265
|
+
const addr = truncateAddress(acct.address, 4);
|
|
266
|
+
const pluginCount = cache.plugins.filter(([key]) => parsePluginKey(key).escrow === "").length;
|
|
267
|
+
|
|
268
|
+
accountContent.push(`${marker}${selected ? theme.selected(acct.name) : acct.name} ${theme.label(addr)}`);
|
|
269
|
+
const details = [
|
|
270
|
+
bal ? `${formatAlgoCompact(bal.algo)} · ${formatCompact(bal.akta, cache.decimals.akta)} AKTA` : null,
|
|
271
|
+
`${pluginCount} plugin${pluginCount !== 1 ? "s" : ""}`,
|
|
272
|
+
].filter(Boolean).join(" · ");
|
|
273
|
+
accountContent.push(` ${theme.label(details)}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Grouped escrows in 2 columns
|
|
277
|
+
if (accounts.length > 1) {
|
|
278
|
+
const groups = buildEscrowGroups(accounts, cache);
|
|
279
|
+
const colWidth = Math.floor((width - 4) / 2);
|
|
280
|
+
const leftGroups = groups.filter((_, i) => i % 2 === 0);
|
|
281
|
+
const rightGroups = groups.filter((_, i) => i % 2 === 1);
|
|
282
|
+
const { lines: leftCol, selectedLine: leftSel } = renderGroupColumn(leftGroups, cache.decimals);
|
|
283
|
+
const { lines: rightCol, selectedLine: rightSel } = renderGroupColumn(rightGroups, cache.decimals);
|
|
284
|
+
const maxLen = Math.max(leftCol.length, rightCol.length);
|
|
285
|
+
|
|
286
|
+
accountContent.push("");
|
|
287
|
+
const mergeStart = accountContent.length;
|
|
288
|
+
for (let i = 0; i < maxLen; i++) {
|
|
289
|
+
const left = leftCol[i] ?? "";
|
|
290
|
+
const right = rightCol[i] ?? "";
|
|
291
|
+
accountContent.push(padEndVisible(left, colWidth) + right);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Track selected line from whichever column has it
|
|
295
|
+
const selInCol = leftSel >= 0 ? leftSel : rightSel;
|
|
296
|
+
if (selInCol >= 0) {
|
|
297
|
+
_selectedLine = accountsStart + mergeStart + selInCol;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
lines.push(...renderPanel(accountContent, { title: `Accounts (${accounts.length})`, width }));
|
|
302
|
+
|
|
303
|
+
return lines;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── Right panel: Plugins, Named Plugins, Allowances, Executions ─
|
|
307
|
+
|
|
308
|
+
function renderRightPanel(
|
|
309
|
+
cache: WalletCache,
|
|
310
|
+
network: AkitaNetwork,
|
|
311
|
+
selectedAccount: { name: string; address: string; escrowName: string },
|
|
312
|
+
appId: bigint,
|
|
313
|
+
width: number,
|
|
314
|
+
): string[] {
|
|
315
|
+
const lines: string[] = [];
|
|
316
|
+
const escrowFilter = selectedAccount.escrowName;
|
|
317
|
+
|
|
318
|
+
// Account info header
|
|
319
|
+
const accountContent = renderKV([
|
|
320
|
+
["App ID", appId.toString()],
|
|
321
|
+
["Address", selectedAccount.address],
|
|
322
|
+
]);
|
|
323
|
+
lines.push(...renderPanel(accountContent, { title: selectedAccount.name, width }));
|
|
324
|
+
lines.push("");
|
|
325
|
+
|
|
326
|
+
// Filter plugins by selected account
|
|
327
|
+
const filteredPlugins = cache.plugins.filter(([key]) => {
|
|
328
|
+
const parsed = parsePluginKey(key);
|
|
329
|
+
return escrowFilter === "" ? parsed.escrow === "" : parsed.escrow === escrowFilter;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Plugins section
|
|
333
|
+
if (filteredPlugins.length > 0) {
|
|
334
|
+
for (let i = 0; i < filteredPlugins.length; i++) {
|
|
335
|
+
const [key, info] = filteredPlugins[i];
|
|
336
|
+
const { pluginId, caller } = parsePluginKey(key);
|
|
337
|
+
const pluginName = pluginId ? getAppName(pluginId, network) : undefined;
|
|
338
|
+
const pluginLabel = pluginName
|
|
339
|
+
? `${pluginName} (${pluginId})`
|
|
340
|
+
: pluginId?.toString() ?? key;
|
|
341
|
+
|
|
342
|
+
const callerLabel = caller === "" || isZeroAddress(caller) ? theme.globalCaller("Global") : truncateAddress(caller);
|
|
343
|
+
const pairs: [string, string][] = [
|
|
344
|
+
["Caller", callerLabel],
|
|
345
|
+
["Admin", colorBool(info.admin)],
|
|
346
|
+
["Delegation", delegationTypeLabel(info.delegationType)],
|
|
347
|
+
["Cover Fees", colorBool(info.coverFees)],
|
|
348
|
+
["Can Reclaim", colorBool(info.canReclaim)],
|
|
349
|
+
["Use Exec Key", colorBool(info.useExecutionKey)],
|
|
350
|
+
["Use Rounds", colorBool(info.useRounds)],
|
|
351
|
+
];
|
|
352
|
+
if (info.cooldown > 0n) pairs.push(["Cooldown", formatDuration(info.cooldown)]);
|
|
353
|
+
if (info.lastCalled > 0n) pairs.push(["Last Called", formatTimestamp(info.lastCalled)]);
|
|
354
|
+
if (info.start > 0n) pairs.push(["Start", formatTimestamp(info.start)]);
|
|
355
|
+
if (info.lastValid < BigInt("18446744073709551615")) {
|
|
356
|
+
pairs.push(["Last Valid", info.useRounds ? info.lastValid.toString() : formatTimestamp(info.lastValid)]);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const content = renderKV(pairs);
|
|
360
|
+
|
|
361
|
+
if (info.methods.length > 0) {
|
|
362
|
+
const methods = info.methods.map((m) => resolveMethodSelector(m.name));
|
|
363
|
+
content.push(" " + theme.label("Methods: ") + methods.join(", "));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (i > 0) lines.push("");
|
|
367
|
+
lines.push(...renderPanel(content, { title: pluginLabel, width }));
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
lines.push(...renderPanel([" No plugins for this account."], { title: "Plugins", width }));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Named Plugins section (not filtered by escrow - show all)
|
|
374
|
+
if (cache.namedPlugins.length > 0) {
|
|
375
|
+
const namedRows = cache.namedPlugins.map(([name, key]) => [
|
|
376
|
+
name,
|
|
377
|
+
key.plugin.toString(),
|
|
378
|
+
truncateAddress(key.caller),
|
|
379
|
+
key.escrow || "(default)",
|
|
380
|
+
]);
|
|
381
|
+
lines.push("");
|
|
382
|
+
const namedContent = renderColumns(["Name", "Plugin App", "Caller", "Escrow"], namedRows);
|
|
383
|
+
lines.push(...renderPanel(namedContent, { title: "Named Plugins", width }));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Allowances filtered by escrow
|
|
387
|
+
const filteredAllowances = cache.allowances.filter(([key]) => {
|
|
388
|
+
// Allowance keys are stringified AllowanceKey {escrow, asset}
|
|
389
|
+
// The key format from ValueMap is the stringified version
|
|
390
|
+
// For main wallet, escrow is "" or the escrow name
|
|
391
|
+
if (escrowFilter === "") return true; // Show all for main wallet
|
|
392
|
+
return key.includes(escrowFilter);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (filteredAllowances.length > 0) {
|
|
396
|
+
const rows = filteredAllowances.map(([key, info]) => {
|
|
397
|
+
let details: string;
|
|
398
|
+
if (info.type === "flat") {
|
|
399
|
+
details = `amount: ${formatBigInt(info.amount!)}, spent: ${formatBigInt(info.spent!)}`;
|
|
400
|
+
} else if (info.type === "window") {
|
|
401
|
+
details = `amount: ${formatBigInt(info.amount!)}, spent: ${formatBigInt(info.spent!)}, interval: ${formatDuration(info.interval!)}`;
|
|
402
|
+
} else {
|
|
403
|
+
details = `rate: ${formatBigInt(info.rate!)}, max: ${formatBigInt(info.max!)}, interval: ${formatDuration(info.interval!)}`;
|
|
404
|
+
}
|
|
405
|
+
return [key, info.type, details];
|
|
406
|
+
});
|
|
407
|
+
lines.push("");
|
|
408
|
+
const allowContent = renderColumns(["Key", "Type", "Details"], rows);
|
|
409
|
+
lines.push(...renderPanel(allowContent, { title: "Allowances", width }));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Execution keys (show all)
|
|
413
|
+
if (cache.executions.length > 0) {
|
|
414
|
+
const rows = cache.executions.map(([key, info]) => [
|
|
415
|
+
key.slice(0, 16) + "...",
|
|
416
|
+
info.firstValid.toString(),
|
|
417
|
+
info.lastValid.toString(),
|
|
418
|
+
]);
|
|
419
|
+
lines.push("");
|
|
420
|
+
const execContent = renderColumns(["Lease (hex)", "First Valid", "Last Valid"], rows);
|
|
421
|
+
lines.push(...renderPanel(execContent, { title: "Execution Keys", width }));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return lines;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── Single-column fallback ──────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
function renderSingleColumn(
|
|
430
|
+
cache: WalletCache,
|
|
431
|
+
network: AkitaNetwork,
|
|
432
|
+
accounts: { name: string; address: string; escrowName: string }[],
|
|
433
|
+
selectedAccount: { name: string; address: string; escrowName: string },
|
|
434
|
+
walletAppId: bigint,
|
|
435
|
+
): string[] {
|
|
436
|
+
const width = 78; // Narrow fallback width
|
|
437
|
+
const allLines: string[] = [""];
|
|
438
|
+
|
|
439
|
+
// Wallet Info
|
|
440
|
+
const gs = cache.globalState;
|
|
441
|
+
const infoPairs: [string, string][] = [
|
|
442
|
+
["Version", gs.version as string ?? "-"],
|
|
443
|
+
["Admin", typeof gs.admin === "string" ? truncateAddress(gs.admin) : "-"],
|
|
444
|
+
["DAO", typeof gs.akitaDao === "bigint" ? resolveAppName(gs.akitaDao as bigint, network) : "-"],
|
|
445
|
+
];
|
|
446
|
+
allLines.push(...renderPanel(renderKV(infoPairs), { title: "Wallet Info", width }));
|
|
447
|
+
|
|
448
|
+
// Account list with selection (grouped)
|
|
449
|
+
allLines.push("");
|
|
450
|
+
const accountLines: string[] = [];
|
|
451
|
+
{
|
|
452
|
+
const selected = _accountIdx === 0;
|
|
453
|
+
const marker = selected ? theme.cursor("▸ ") : " ";
|
|
454
|
+
accountLines.push(`${marker}${selected ? theme.selected(accounts[0].name) : accounts[0].name}`);
|
|
455
|
+
}
|
|
456
|
+
if (accounts.length > 1) {
|
|
457
|
+
const groups = buildEscrowGroups(accounts, cache);
|
|
458
|
+
accountLines.push("");
|
|
459
|
+
accountLines.push(...renderGroupColumn(groups, cache.decimals).lines);
|
|
460
|
+
}
|
|
461
|
+
allLines.push(...renderPanel(accountLines, { title: `Accounts (${accounts.length})`, width }));
|
|
462
|
+
|
|
463
|
+
// Right-side content for selected account
|
|
464
|
+
const selectedAppId = selectedAccount.escrowName === ""
|
|
465
|
+
? walletAppId
|
|
466
|
+
: cache.escrows.find(([n]) => n === selectedAccount.escrowName)?.[1].id ?? 0n;
|
|
467
|
+
const right = renderRightPanel(cache, network, selectedAccount, selectedAppId, width);
|
|
468
|
+
allLines.push("");
|
|
469
|
+
allLines.push(...right);
|
|
470
|
+
|
|
471
|
+
return allLines;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── Helpers ─────────────────────────────────────────────────────
|
|
475
|
+
|
|
476
|
+
function formatAlgoCompact(microAlgos: bigint): string {
|
|
477
|
+
const whole = microAlgos / 1_000_000n;
|
|
478
|
+
return `${formatCompact(whole)} ALGO`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── Escrow grouping ─────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
interface EscrowGroupItem {
|
|
484
|
+
idx: number;
|
|
485
|
+
acct: { name: string; address: string; escrowName: string };
|
|
486
|
+
locked: boolean;
|
|
487
|
+
pluginCount: number;
|
|
488
|
+
balances?: { algo: bigint; akta: bigint; bones: bigint; usdc: bigint };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
interface EscrowGroup {
|
|
492
|
+
label: string;
|
|
493
|
+
items: EscrowGroupItem[];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function escrowPrefix(name: string): string {
|
|
497
|
+
const underscoreIdx = name.indexOf("_");
|
|
498
|
+
if (underscoreIdx > 0) return name.slice(0, underscoreIdx);
|
|
499
|
+
const match = name.match(/^[a-z]+/);
|
|
500
|
+
return match ? match[0] : name;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function buildEscrowGroups(
|
|
504
|
+
accounts: { name: string; address: string; escrowName: string }[],
|
|
505
|
+
cache: WalletCache,
|
|
506
|
+
): EscrowGroup[] {
|
|
507
|
+
const groupMap = new Map<string, EscrowGroup>();
|
|
508
|
+
const groupOrder: string[] = [];
|
|
509
|
+
|
|
510
|
+
for (let i = 1; i < accounts.length; i++) {
|
|
511
|
+
const acct = accounts[i];
|
|
512
|
+
const prefix = escrowPrefix(acct.escrowName);
|
|
513
|
+
if (!groupMap.has(prefix)) {
|
|
514
|
+
const label = prefix.charAt(0).toUpperCase() + prefix.slice(1);
|
|
515
|
+
groupMap.set(prefix, { label, items: [] });
|
|
516
|
+
groupOrder.push(prefix);
|
|
517
|
+
}
|
|
518
|
+
const escrowInfo = cache.escrows.find(([n]) => n === acct.escrowName)?.[1];
|
|
519
|
+
const pluginCount = cache.plugins.filter(([key]) => {
|
|
520
|
+
const parsed = parsePluginKey(key);
|
|
521
|
+
return parsed.escrow === acct.escrowName;
|
|
522
|
+
}).length;
|
|
523
|
+
groupMap.get(prefix)!.items.push({
|
|
524
|
+
idx: i,
|
|
525
|
+
acct,
|
|
526
|
+
locked: escrowInfo?.locked ?? false,
|
|
527
|
+
pluginCount,
|
|
528
|
+
balances: cache.balances.get(acct.address),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return groupOrder.map((p) => groupMap.get(p)!);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function renderGroupColumn(groups: EscrowGroup[], decimals: { akta: number; bones: number; usdc: number }): { lines: string[]; selectedLine: number } {
|
|
536
|
+
const lines: string[] = [];
|
|
537
|
+
let selectedLine = -1;
|
|
538
|
+
for (const g of groups) {
|
|
539
|
+
if (lines.length > 0) lines.push("");
|
|
540
|
+
lines.push(theme.label(g.label));
|
|
541
|
+
for (const item of g.items) {
|
|
542
|
+
const selected = item.idx === _accountIdx;
|
|
543
|
+
if (selected) selectedLine = lines.length;
|
|
544
|
+
const marker = selected ? theme.cursor("▸ ") : " ";
|
|
545
|
+
const name = selected ? theme.selected(item.acct.name) : item.acct.name;
|
|
546
|
+
const lockStr = item.locked ? theme.locked(" locked") : theme.unlocked(" unlocked");
|
|
547
|
+
lines.push(`${marker}${name}${lockStr}`);
|
|
548
|
+
if (item.balances) {
|
|
549
|
+
const parts: string[] = [];
|
|
550
|
+
if (item.balances.algo > 0n) parts.push(`${formatAlgoCompact(item.balances.algo)}`);
|
|
551
|
+
if (item.balances.usdc > 0n) parts.push(`${formatCompact(item.balances.usdc, decimals.usdc)} USDC`);
|
|
552
|
+
if (item.balances.akta > 0n) parts.push(`${formatCompact(item.balances.akta, decimals.akta)} AKTA`);
|
|
553
|
+
if (item.balances.bones > 0n) parts.push(`${formatCompact(item.balances.bones, decimals.bones)} BONES`);
|
|
554
|
+
if (parts.length > 0) lines.push(` ${theme.label(parts.join(" · "))}`);
|
|
555
|
+
}
|
|
556
|
+
lines.push(` ${theme.label(`${item.pluginCount} plugin${item.pluginCount !== 1 ? "s" : ""}`)}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return { lines, selectedLine };
|
|
560
|
+
}
|