@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,66 @@
|
|
|
1
|
+
// ── ANSI escape sequences ───────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
const ESC = "\x1b";
|
|
4
|
+
|
|
5
|
+
export const ansi = {
|
|
6
|
+
clearScreen: `${ESC}[2J`,
|
|
7
|
+
clearLine: `${ESC}[2K`,
|
|
8
|
+
moveTo: (row: number, col: number) => `${ESC}[${row};${col}H`,
|
|
9
|
+
hideCursor: `${ESC}[?25l`,
|
|
10
|
+
showCursor: `${ESC}[?25h`,
|
|
11
|
+
enterAlt: `${ESC}[?1049h`,
|
|
12
|
+
leaveAlt: `${ESC}[?1049l`,
|
|
13
|
+
reset: `${ESC}[0m`,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ── Terminal size ───────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export function getTermSize(): { rows: number; cols: number } {
|
|
19
|
+
return {
|
|
20
|
+
rows: process.stdout.rows || 24,
|
|
21
|
+
cols: process.stdout.columns || 120,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Raw mode management ────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
let isRawMode = false;
|
|
28
|
+
|
|
29
|
+
export function enterRawMode(): void {
|
|
30
|
+
if (isRawMode) return;
|
|
31
|
+
process.stdout.write(ansi.enterAlt + ansi.hideCursor);
|
|
32
|
+
if (process.stdin.isTTY) {
|
|
33
|
+
process.stdin.setRawMode(true);
|
|
34
|
+
process.stdin.resume();
|
|
35
|
+
}
|
|
36
|
+
isRawMode = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function exitRawMode(): void {
|
|
40
|
+
if (!isRawMode) return;
|
|
41
|
+
process.stdout.write(ansi.showCursor + ansi.leaveAlt);
|
|
42
|
+
if (process.stdin.isTTY) {
|
|
43
|
+
process.stdin.setRawMode(false);
|
|
44
|
+
process.stdin.pause();
|
|
45
|
+
}
|
|
46
|
+
isRawMode = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Frame writing (flicker-free) ────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export function writeFrame(lines: string[]): void {
|
|
52
|
+
// Build entire frame as a single string, then write once
|
|
53
|
+
let frame = ansi.moveTo(1, 1);
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
frame += ansi.clearLine + lines[i];
|
|
56
|
+
if (i < lines.length - 1) frame += "\n";
|
|
57
|
+
}
|
|
58
|
+
process.stdout.write(frame);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Resize listener ────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export function onResize(cb: () => void): () => void {
|
|
64
|
+
process.stdout.on("resize", cb);
|
|
65
|
+
return () => process.stdout.off("resize", cb);
|
|
66
|
+
}
|
package/src/tui/types.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AkitaDaoSDK, AkitaNetwork } from "@akta/sdk";
|
|
2
|
+
|
|
3
|
+
// ── Tab & View identifiers ──────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type TabId = "dao" | "fees" | "wallet" | "proposals";
|
|
6
|
+
|
|
7
|
+
export const TABS: TabId[] = ["dao", "fees", "wallet", "proposals"];
|
|
8
|
+
|
|
9
|
+
export const TAB_LABELS: Record<TabId, string> = {
|
|
10
|
+
dao: "DAO",
|
|
11
|
+
fees: "Fees",
|
|
12
|
+
wallet: "Wallet",
|
|
13
|
+
proposals: "Proposals",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ── View identification ─────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export type ViewId =
|
|
19
|
+
| { tab: "dao" }
|
|
20
|
+
| { tab: "fees" }
|
|
21
|
+
| { tab: "wallet" }
|
|
22
|
+
| { tab: "proposals" };
|
|
23
|
+
|
|
24
|
+
// ── Key actions ─────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export type KeyAction =
|
|
27
|
+
| { type: "quit" }
|
|
28
|
+
| { type: "tab-next" }
|
|
29
|
+
| { type: "tab-prev" }
|
|
30
|
+
| { type: "sub-next" }
|
|
31
|
+
| { type: "sub-prev" }
|
|
32
|
+
| { type: "up" }
|
|
33
|
+
| { type: "down" }
|
|
34
|
+
| { type: "page-up" }
|
|
35
|
+
| { type: "page-down" }
|
|
36
|
+
| { type: "top" }
|
|
37
|
+
| { type: "bottom" }
|
|
38
|
+
| { type: "enter" }
|
|
39
|
+
| { type: "back" }
|
|
40
|
+
| { type: "refresh" }
|
|
41
|
+
| { type: "json" };
|
|
42
|
+
|
|
43
|
+
// ── Application state ───────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export interface AppState {
|
|
46
|
+
tab: TabId;
|
|
47
|
+
walletAccountIdx: number;
|
|
48
|
+
scrollOffset: number;
|
|
49
|
+
cursor: number;
|
|
50
|
+
viewStack: ViewId[];
|
|
51
|
+
jsonMode: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── View interface ──────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export interface ViewContext {
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
network: AkitaNetwork;
|
|
60
|
+
dao: AkitaDaoSDK;
|
|
61
|
+
navigate: (view: ViewId) => void;
|
|
62
|
+
refresh: () => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LoadResult {
|
|
66
|
+
lines: string[];
|
|
67
|
+
fixedRight?: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface View {
|
|
71
|
+
load(ctx: ViewContext): Promise<string[] | LoadResult>;
|
|
72
|
+
selectable?: boolean;
|
|
73
|
+
selectableCount?: (lines: string[]) => number;
|
|
74
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { getNetworkAppIds } from "@akta/sdk";
|
|
2
|
+
import type { AkitaNetwork } from "@akta/sdk";
|
|
3
|
+
import type { AkitaDaoGlobalState } from "@akta/sdk/dao";
|
|
4
|
+
import { renderKV, renderColumns, visibleLength } from "../../output";
|
|
5
|
+
import theme from "../../theme";
|
|
6
|
+
import {
|
|
7
|
+
formatMicroAlgo,
|
|
8
|
+
formatBasisPoints,
|
|
9
|
+
formatDuration,
|
|
10
|
+
formatBigInt,
|
|
11
|
+
formatCompact,
|
|
12
|
+
daoStateLabel,
|
|
13
|
+
resolveAppName,
|
|
14
|
+
getAppName,
|
|
15
|
+
colorState,
|
|
16
|
+
} from "../../formatting";
|
|
17
|
+
import { renderPanel, renderPanelGrid, splitWidth } from "../panels";
|
|
18
|
+
import type { View, ViewContext } from "../types";
|
|
19
|
+
|
|
20
|
+
export const daoView: View = {
|
|
21
|
+
async load(ctx: ViewContext): Promise<string[]> {
|
|
22
|
+
const { dao, network, width } = ctx;
|
|
23
|
+
const state = await dao.getGlobalState();
|
|
24
|
+
const ids = getNetworkAppIds(network);
|
|
25
|
+
|
|
26
|
+
// Fetch supply data for AKTA & BONES
|
|
27
|
+
let aktaSupply: SupplyInfo | null = null;
|
|
28
|
+
let bonesSupply: SupplyInfo | null = null;
|
|
29
|
+
try {
|
|
30
|
+
const [aktaInfo, bonesInfo] = await Promise.all([
|
|
31
|
+
dao.algorand.asset.getById(ids.akta),
|
|
32
|
+
dao.algorand.asset.getById(ids.bones),
|
|
33
|
+
]);
|
|
34
|
+
const [aktaReserve, bonesReserve] = await Promise.all([
|
|
35
|
+
aktaInfo.reserve
|
|
36
|
+
? dao.algorand.asset.getAccountInformation(aktaInfo.reserve, ids.akta)
|
|
37
|
+
: null,
|
|
38
|
+
bonesInfo.reserve
|
|
39
|
+
? dao.algorand.asset.getAccountInformation(bonesInfo.reserve, ids.bones)
|
|
40
|
+
: null,
|
|
41
|
+
]);
|
|
42
|
+
aktaSupply = {
|
|
43
|
+
total: aktaInfo.total,
|
|
44
|
+
circulating: aktaInfo.total - (aktaReserve?.balance ?? 0n),
|
|
45
|
+
decimals: aktaInfo.decimals,
|
|
46
|
+
};
|
|
47
|
+
bonesSupply = {
|
|
48
|
+
total: bonesInfo.total,
|
|
49
|
+
circulating: bonesInfo.total - (bonesReserve?.balance ?? 0n),
|
|
50
|
+
decimals: bonesInfo.decimals,
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
// Asset info fetch failed — skip supply charts
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (width < 80) {
|
|
57
|
+
return renderSingleColumn(state, network, ids, width, aktaSupply, bonesSupply);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const gridRows: string[][][] = [];
|
|
61
|
+
|
|
62
|
+
// Row 1: DAO info + Assets + Token Supply
|
|
63
|
+
const hasSupply = aktaSupply || bonesSupply;
|
|
64
|
+
const colCount = hasSupply ? 3 : 2;
|
|
65
|
+
const colWidths = splitWidth(width, colCount);
|
|
66
|
+
|
|
67
|
+
const infoContent = renderKV([
|
|
68
|
+
["Network", network],
|
|
69
|
+
["App ID", dao.appId.toString()],
|
|
70
|
+
["Version", state.version ?? "-"],
|
|
71
|
+
["State", state.state !== undefined ? colorState(daoStateLabel(state.state)) : "-"],
|
|
72
|
+
["Wallet", state.wallet ? resolveAppName(state.wallet, network) : "-"],
|
|
73
|
+
]);
|
|
74
|
+
const infoPanel = renderPanel(infoContent, { title: "Akita DAO", width: colWidths[0] });
|
|
75
|
+
|
|
76
|
+
const assetsContent = renderKV([
|
|
77
|
+
["AKTA", state.akitaAssets?.akta?.toString() ?? ids.akta.toString()],
|
|
78
|
+
["BONES", state.akitaAssets?.bones?.toString() ?? ids.bones.toString()],
|
|
79
|
+
["Next Proposal", state.proposalId?.toString() ?? "-"],
|
|
80
|
+
["Action Limit", state.proposalActionLimit?.toString() ?? "-"],
|
|
81
|
+
["Min Rewards", state.minRewardsImpact?.toString() ?? "-"],
|
|
82
|
+
]);
|
|
83
|
+
const assetsPanel = renderPanel(assetsContent, { title: "Assets", width: colWidths[1] });
|
|
84
|
+
|
|
85
|
+
if (hasSupply) {
|
|
86
|
+
const supplyW = colWidths[2];
|
|
87
|
+
const supplyContent = renderSupplyCharts(aktaSupply, bonesSupply, supplyW - 4);
|
|
88
|
+
const supplyPanel = renderPanel(supplyContent, { title: "Token Supply", width: supplyW });
|
|
89
|
+
gridRows.push([infoPanel, assetsPanel, supplyPanel]);
|
|
90
|
+
} else {
|
|
91
|
+
gridRows.push([infoPanel, assetsPanel]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Row 3: App IDs (40%) + Proposal Settings / Revenue Splits stacked (60%)
|
|
95
|
+
const appLines = renderAppTable(state, network);
|
|
96
|
+
const proposalLines = renderProposalSettings(state);
|
|
97
|
+
|
|
98
|
+
if (appLines.length > 0 || proposalLines.length > 0) {
|
|
99
|
+
// ~40/60 split
|
|
100
|
+
const appW = Math.floor((width - 2) * 0.4);
|
|
101
|
+
const rightPanelW = width - appW - 2;
|
|
102
|
+
const revLines = renderRevenueSplits(state, network, rightPanelW - 4);
|
|
103
|
+
|
|
104
|
+
const leftPanel = appLines.length > 0
|
|
105
|
+
? renderPanel(appLines, { title: "App IDs", width: appW })
|
|
106
|
+
: renderPanel([" No app ID data"], { title: "App IDs", width: appW });
|
|
107
|
+
|
|
108
|
+
// Stack Proposal Settings + Revenue Splits into one right column
|
|
109
|
+
const rightPanels: string[] = [];
|
|
110
|
+
if (proposalLines.length > 0) {
|
|
111
|
+
rightPanels.push(...renderPanel(proposalLines, { title: "Proposal Settings", width: rightPanelW }));
|
|
112
|
+
}
|
|
113
|
+
if (revLines.length > 0) {
|
|
114
|
+
if (rightPanels.length > 0) rightPanels.push("");
|
|
115
|
+
rightPanels.push(...renderPanel(revLines, { title: "Revenue Splits", width: rightPanelW }));
|
|
116
|
+
}
|
|
117
|
+
if (rightPanels.length === 0) {
|
|
118
|
+
rightPanels.push(...renderPanel([" No proposal settings"], { title: "Proposal Settings", width: rightPanelW }));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
gridRows.push([leftPanel, rightPanels]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return ["", ...renderPanelGrid(gridRows, { rowGap: 1 })];
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// ── Single-column fallback for narrow terminals ────────────────
|
|
129
|
+
|
|
130
|
+
function renderSingleColumn(
|
|
131
|
+
state: Partial<AkitaDaoGlobalState>,
|
|
132
|
+
network: AkitaNetwork,
|
|
133
|
+
ids: { akta: bigint; bones: bigint },
|
|
134
|
+
width: number,
|
|
135
|
+
aktaSupply: SupplyInfo | null,
|
|
136
|
+
bonesSupply: SupplyInfo | null,
|
|
137
|
+
): string[] {
|
|
138
|
+
const lines: string[] = [""];
|
|
139
|
+
|
|
140
|
+
const infoContent = renderKV([
|
|
141
|
+
["Network", network],
|
|
142
|
+
["State", state.state !== undefined ? colorState(daoStateLabel(state.state)) : "-"],
|
|
143
|
+
["Version", state.version ?? "-"],
|
|
144
|
+
["Wallet", state.wallet ? resolveAppName(state.wallet, network) : "-"],
|
|
145
|
+
["AKTA", state.akitaAssets?.akta?.toString() ?? ids.akta.toString()],
|
|
146
|
+
["BONES", state.akitaAssets?.bones?.toString() ?? ids.bones.toString()],
|
|
147
|
+
["Next Proposal", state.proposalId?.toString() ?? "-"],
|
|
148
|
+
["Action Limit", state.proposalActionLimit?.toString() ?? "-"],
|
|
149
|
+
]);
|
|
150
|
+
lines.push(...renderPanel(infoContent, { title: "Akita DAO", width }));
|
|
151
|
+
|
|
152
|
+
if (aktaSupply || bonesSupply) {
|
|
153
|
+
lines.push("");
|
|
154
|
+
const supplyContent = renderSupplyCharts(aktaSupply, bonesSupply, width - 4);
|
|
155
|
+
lines.push(...renderPanel(supplyContent, { title: "Token Supply", width }));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const appLines = renderAppTable(state, network);
|
|
159
|
+
if (appLines.length > 0) {
|
|
160
|
+
lines.push("");
|
|
161
|
+
lines.push(...renderPanel(appLines, { title: "App IDs", width }));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const proposalLines = renderProposalSettings(state);
|
|
165
|
+
if (proposalLines.length > 0) {
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push(...renderPanel(proposalLines, { title: "Proposal Settings", width }));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const revLines = renderRevenueSplits(state, network, width - 4);
|
|
171
|
+
if (revLines.length > 0) {
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push(...renderPanel(revLines, { title: "Revenue Splits", width }));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
function renderAppTable(state: Partial<AkitaDaoGlobalState>, network: AkitaNetwork): string[] {
|
|
182
|
+
const sections: [string, Record<string, bigint> | undefined][] = [
|
|
183
|
+
["Core", state.akitaAppList as Record<string, bigint> | undefined],
|
|
184
|
+
["Social", state.akitaSocialAppList as Record<string, bigint> | undefined],
|
|
185
|
+
["Plugins", state.pluginAppList as Record<string, bigint> | undefined],
|
|
186
|
+
["Other", state.otherAppList as Record<string, bigint> | undefined],
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const rows: string[][] = [];
|
|
190
|
+
for (const [category, apps] of sections) {
|
|
191
|
+
if (!apps) continue;
|
|
192
|
+
for (const [, id] of Object.entries(apps)) {
|
|
193
|
+
rows.push([category, getAppName(id, network) ?? id.toString(), id.toString()]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (rows.length === 0) return [];
|
|
198
|
+
return renderColumns(["Category", "Name", "App ID"], rows);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderProposalSettings(state: Partial<AkitaDaoGlobalState>): string[] {
|
|
202
|
+
const settings: [string, { fee: bigint; power: bigint; duration: bigint; participation: bigint; approval: bigint } | undefined][] = [
|
|
203
|
+
["Upgrade App", state.upgradeAppProposalSettings],
|
|
204
|
+
["Add Plugin", state.addPluginProposalSettings],
|
|
205
|
+
["Remove Plugin", state.removePluginProposalSettings],
|
|
206
|
+
["Rm Exec Plugin", state.removeExecutePluginProposalSettings],
|
|
207
|
+
["Add Allowances", state.addAllowancesProposalSettings],
|
|
208
|
+
["Rm Allowances", state.removeAllowancesProposalSettings],
|
|
209
|
+
["New Escrow", state.newEscrowProposalSettings],
|
|
210
|
+
["Toggle Lock", state.toggleEscrowLockProposalSettings],
|
|
211
|
+
["Update Fields", state.updateFieldsProposalSettings],
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const hasAny = settings.some(([, v]) => v !== undefined);
|
|
215
|
+
if (!hasAny) return [];
|
|
216
|
+
|
|
217
|
+
const rows = settings.map(([label, ps]) => {
|
|
218
|
+
if (!ps) return [label, "-", "-", "-", "-", "-"];
|
|
219
|
+
return [
|
|
220
|
+
label,
|
|
221
|
+
formatMicroAlgo(ps.fee),
|
|
222
|
+
formatBigInt(ps.power),
|
|
223
|
+
formatDuration(ps.duration),
|
|
224
|
+
inlineBar(ps.participation),
|
|
225
|
+
inlineBar(ps.approval),
|
|
226
|
+
];
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return renderColumns(["Cat", "Fee", "Pwr", "Dur", "Part", "Appr"], rows);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const SPLIT_COLORS = theme.splitColors;
|
|
233
|
+
|
|
234
|
+
function renderRevenueSplits(state: Partial<AkitaDaoGlobalState>, network: AkitaNetwork, barWidth: number): string[] {
|
|
235
|
+
if (!state.revenueSplits || state.revenueSplits.length === 0) return [];
|
|
236
|
+
|
|
237
|
+
// Calculate remainder percentage (100% minus all explicit percentages)
|
|
238
|
+
let pctSum = 0n;
|
|
239
|
+
for (const [, type, value] of state.revenueSplits) {
|
|
240
|
+
if (type === 20) pctSum += value; // Percentage type
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Resolve each split's percentage
|
|
244
|
+
const splits: { name: string; pct: number }[] = state.revenueSplits.map(([[wallet, escrow], type, value]) => {
|
|
245
|
+
let bp: bigint;
|
|
246
|
+
if (type === 20) bp = value;
|
|
247
|
+
else if (type === 30) bp = 100_000n - pctSum;
|
|
248
|
+
else bp = 0n;
|
|
249
|
+
|
|
250
|
+
const name = escrow || (getAppName(wallet, network) ?? wallet.toString());
|
|
251
|
+
return { name, pct: Number(bp) / 1000 };
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const barMax = Math.max(10, barWidth - 4);
|
|
255
|
+
|
|
256
|
+
// Build stacked bar — one segment per split
|
|
257
|
+
let bar = " ";
|
|
258
|
+
let usedChars = 0;
|
|
259
|
+
for (let i = 0; i < splits.length; i++) {
|
|
260
|
+
const color = SPLIT_COLORS[i % SPLIT_COLORS.length];
|
|
261
|
+
const isLast = i === splits.length - 1;
|
|
262
|
+
const chars = isLast ? barMax - usedChars : Math.round((splits[i].pct / 100) * barMax);
|
|
263
|
+
bar += color("█".repeat(Math.max(0, chars)));
|
|
264
|
+
usedChars += chars;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Legend line — colored markers with labels
|
|
268
|
+
const legend = splits.map((s, i) => {
|
|
269
|
+
const color = SPLIT_COLORS[i % SPLIT_COLORS.length];
|
|
270
|
+
return color("■") + ` ${s.name} ${theme.chartDim(`${s.pct}%`)}`;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Wrap legend entries to fit width
|
|
274
|
+
const lines: string[] = [];
|
|
275
|
+
const maxLegendW = barWidth - 2;
|
|
276
|
+
let current = " ";
|
|
277
|
+
for (const entry of legend) {
|
|
278
|
+
const candidate = current === " " ? " " + entry : current + " " + entry;
|
|
279
|
+
if (visibleLength(candidate) > maxLegendW && current !== " ") {
|
|
280
|
+
lines.push(current);
|
|
281
|
+
current = " " + entry;
|
|
282
|
+
} else {
|
|
283
|
+
current = candidate;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (current !== " ") lines.push(current);
|
|
287
|
+
|
|
288
|
+
lines.push("");
|
|
289
|
+
lines.push(bar);
|
|
290
|
+
|
|
291
|
+
return lines;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Inline bar for basis-point percentages ─────────────────────
|
|
295
|
+
|
|
296
|
+
const INLINE_BAR_WIDTH = 8;
|
|
297
|
+
|
|
298
|
+
function inlineBar(bp: bigint): string {
|
|
299
|
+
const pct = Number(bp) / 1000;
|
|
300
|
+
const filled = Math.round((pct / 100) * INLINE_BAR_WIDTH);
|
|
301
|
+
const empty = INLINE_BAR_WIDTH - filled;
|
|
302
|
+
return `${theme.barFilled("█".repeat(filled))}${theme.barEmpty("░".repeat(empty))} ${theme.chartDim(`${pct}%`)}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Supply bar chart ────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
interface SupplyInfo { total: bigint; circulating: bigint; decimals: number }
|
|
308
|
+
|
|
309
|
+
function renderSupplyCharts(
|
|
310
|
+
akta: SupplyInfo | null,
|
|
311
|
+
bones: SupplyInfo | null,
|
|
312
|
+
barWidth: number,
|
|
313
|
+
): string[] {
|
|
314
|
+
const lines: string[] = [];
|
|
315
|
+
if (akta) lines.push(...renderSupplyBar("AKTA", akta, barWidth));
|
|
316
|
+
if (akta && bones) lines.push("");
|
|
317
|
+
if (bones) lines.push(...renderSupplyBar("BONES", bones, barWidth));
|
|
318
|
+
return lines;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function renderSupplyBar(label: string, supply: SupplyInfo, maxWidth: number): string[] {
|
|
322
|
+
const pct = supply.total > 0n
|
|
323
|
+
? Number((supply.circulating * 10000n) / supply.total) / 100
|
|
324
|
+
: 0;
|
|
325
|
+
|
|
326
|
+
// Label line: AKTA 1.5B / 2.3B (65.2%)
|
|
327
|
+
const circStr = formatCompact(supply.circulating, supply.decimals);
|
|
328
|
+
const totalStr = formatCompact(supply.total, supply.decimals);
|
|
329
|
+
const header = ` ${theme.chartLabel(label)} ${circStr} / ${totalStr} ${theme.chartDim(`(${pct.toFixed(1)}%)`)}`;
|
|
330
|
+
|
|
331
|
+
// Bar: ████████████░░░░░░░░
|
|
332
|
+
const barMax = Math.max(10, maxWidth - 4); // 2 padding each side
|
|
333
|
+
const filled = Math.round((pct / 100) * barMax);
|
|
334
|
+
const empty = barMax - filled;
|
|
335
|
+
const bar = ` ${theme.barFilled("█".repeat(filled))}${theme.barEmpty("░".repeat(empty))}`;
|
|
336
|
+
|
|
337
|
+
return [header, bar];
|
|
338
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { AkitaDaoGlobalState } from "@akta/sdk/dao";
|
|
2
|
+
import type { KVGroup } from "../../output";
|
|
3
|
+
import theme from "../../theme";
|
|
4
|
+
import {
|
|
5
|
+
formatMicroAlgo,
|
|
6
|
+
formatBasisPoints,
|
|
7
|
+
formatBigInt,
|
|
8
|
+
} from "../../formatting";
|
|
9
|
+
import { renderPanel, splitWidth } from "../panels";
|
|
10
|
+
import { padEndVisible, visibleLength } from "../../output";
|
|
11
|
+
import type { View, ViewContext } from "../types";
|
|
12
|
+
|
|
13
|
+
const PERCENTAGE_RE = /Percentage|Tax/i;
|
|
14
|
+
const FEE_RE = /Fee$/i;
|
|
15
|
+
|
|
16
|
+
function formatFeeValue(key: string, value: bigint): string {
|
|
17
|
+
if (PERCENTAGE_RE.test(key)) return formatBasisPoints(value);
|
|
18
|
+
if (FEE_RE.test(key)) return formatMicroAlgo(value);
|
|
19
|
+
return formatBigInt(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function feeLabel(key: string): string {
|
|
23
|
+
return key.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const feesView: View = {
|
|
27
|
+
async load(ctx: ViewContext): Promise<string[]> {
|
|
28
|
+
const { dao, width } = ctx;
|
|
29
|
+
const state = await dao.getGlobalState();
|
|
30
|
+
|
|
31
|
+
const groups = collectFeeGroups(state);
|
|
32
|
+
|
|
33
|
+
if (groups.length === 0) {
|
|
34
|
+
return [
|
|
35
|
+
"",
|
|
36
|
+
...renderPanel([" No fee data available."], { title: "Fees", width }),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (width < 80) {
|
|
41
|
+
return renderSingleColumn(groups, width);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cols = 2;
|
|
45
|
+
const colGap = 2;
|
|
46
|
+
const panelGap = 1;
|
|
47
|
+
const [leftW, rightW] = splitWidth(width, cols, colGap);
|
|
48
|
+
|
|
49
|
+
// Compute max key width per grid column
|
|
50
|
+
const colKeyWidths: number[] = Array(cols).fill(0);
|
|
51
|
+
for (let i = 0; i < groups.length; i++) {
|
|
52
|
+
const col = i % cols;
|
|
53
|
+
for (const [key] of groups[i].pairs) {
|
|
54
|
+
colKeyWidths[col] = Math.max(colKeyWidths[col], key.length);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Split groups into independent columns
|
|
59
|
+
const colWidths = [leftW, rightW];
|
|
60
|
+
const columns: string[][] = [[], []];
|
|
61
|
+
for (let i = 0; i < groups.length; i++) {
|
|
62
|
+
const col = i % cols;
|
|
63
|
+
if (columns[col].length > 0) {
|
|
64
|
+
for (let g = 0; g < panelGap; g++) columns[col].push("");
|
|
65
|
+
}
|
|
66
|
+
const content = renderFeeKV(groups[i].pairs, colKeyWidths[col]);
|
|
67
|
+
columns[col].push(...renderPanel(content, { title: groups[i].title, width: colWidths[col] }));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Merge columns side-by-side
|
|
71
|
+
const maxHeight = Math.max(columns[0].length, columns[1].length);
|
|
72
|
+
const gapStr = " ".repeat(colGap);
|
|
73
|
+
const lines: string[] = [""];
|
|
74
|
+
for (let i = 0; i < maxHeight; i++) {
|
|
75
|
+
const left = i < columns[0].length ? padEndVisible(columns[0][i], leftW) : " ".repeat(leftW);
|
|
76
|
+
const right = i < columns[1].length ? columns[1][i] : "";
|
|
77
|
+
lines.push(left + gapStr + right);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return lines;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function renderSingleColumn(groups: KVGroup[], width: number): string[] {
|
|
85
|
+
const lines: string[] = [""];
|
|
86
|
+
for (const group of groups) {
|
|
87
|
+
if (lines.length > 1) lines.push("");
|
|
88
|
+
const content = renderFeeKV(group.pairs);
|
|
89
|
+
lines.push(...renderPanel(content, { title: group.title, width }));
|
|
90
|
+
}
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Render KV pairs aligned to a shared key width */
|
|
95
|
+
function renderFeeKV(pairs: [string, string][], keyWidth?: number): string[] {
|
|
96
|
+
const w = keyWidth ?? Math.max(...pairs.map(([k]) => k.length));
|
|
97
|
+
return pairs.map(([key, value]) => ` ${theme.label(key.padEnd(w))} ${value}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function collectFeeGroups(state: Partial<AkitaDaoGlobalState>): KVGroup[] {
|
|
101
|
+
const feeGroups: [string, Record<string, bigint> | undefined][] = [
|
|
102
|
+
["Wallet Fees", state.walletFees as Record<string, bigint> | undefined],
|
|
103
|
+
["Social Fees", state.socialFees as Record<string, bigint> | undefined],
|
|
104
|
+
["Staking Fees", state.stakingFees as Record<string, bigint> | undefined],
|
|
105
|
+
["Sub Fees", state.subscriptionFees as Record<string, bigint> | undefined],
|
|
106
|
+
["Swap Fees", state.swapFees as Record<string, bigint> | undefined],
|
|
107
|
+
["NFT Fees", state.nftFees as Record<string, bigint> | undefined],
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const groups: KVGroup[] = [];
|
|
111
|
+
for (const [name, fees] of feeGroups) {
|
|
112
|
+
if (!fees) continue;
|
|
113
|
+
groups.push({
|
|
114
|
+
title: name,
|
|
115
|
+
pairs: mergeRangePairs(fees),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return groups;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Merge Min/Max pairs into range display (e.g. "Impact Tax: 1% – 5%")
|
|
123
|
+
* and pass through standalone fields normally.
|
|
124
|
+
*/
|
|
125
|
+
function mergeRangePairs(fees: Record<string, bigint>): [string, string][] {
|
|
126
|
+
const entries = Object.entries(fees);
|
|
127
|
+
const used = new Set<string>();
|
|
128
|
+
const pairs: [string, string][] = [];
|
|
129
|
+
|
|
130
|
+
for (const [key, value] of entries) {
|
|
131
|
+
if (used.has(key)) continue;
|
|
132
|
+
|
|
133
|
+
// Check for Min/Max pair
|
|
134
|
+
const minMatch = key.match(/^(.+)Min$/);
|
|
135
|
+
const maxMatch = key.match(/^(.+)Max$/);
|
|
136
|
+
|
|
137
|
+
if (minMatch) {
|
|
138
|
+
const base = minMatch[1];
|
|
139
|
+
const maxKey = `${base}Max`;
|
|
140
|
+
if (maxKey in fees) {
|
|
141
|
+
used.add(key);
|
|
142
|
+
used.add(maxKey);
|
|
143
|
+
const label = feeLabel(base);
|
|
144
|
+
const minVal = formatFeeValue(key, value);
|
|
145
|
+
const maxVal = formatFeeValue(maxKey, fees[maxKey]);
|
|
146
|
+
pairs.push([label, `${minVal} – ${maxVal}`]);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (maxMatch) {
|
|
151
|
+
const base = maxMatch[1];
|
|
152
|
+
const minKey = `${base}Min`;
|
|
153
|
+
if (minKey in fees) {
|
|
154
|
+
// Already handled by Min pass above
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
used.add(key);
|
|
160
|
+
pairs.push([feeLabel(key), formatFeeValue(key, value)]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return pairs;
|
|
164
|
+
}
|