@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.
@@ -0,0 +1,356 @@
1
+ import type { AkitaDaoSDK } from "@akta/sdk";
2
+ import { getNetworkAppIds, type AkitaNetwork } from "@akta/sdk";
3
+ import algosdk from "algosdk";
4
+ import { printJson, printColumns, header, printKV, printPluginCard } from "../output";
5
+ import {
6
+ truncateAddress,
7
+ formatBigInt,
8
+ formatTimestamp,
9
+ isZeroAddress,
10
+ camelToLabel,
11
+ resolveAppName,
12
+ getAppName,
13
+ delegationTypeLabel,
14
+ formatDuration,
15
+ colorBool,
16
+ } from "../formatting";
17
+
18
+ // Fields to hide from wallet info display
19
+ const HIDDEN_WALLET_FIELDS = new Set([
20
+ "spendingAddress",
21
+ "currentPlugin",
22
+ "rekeyIndex",
23
+ ]);
24
+
25
+ async function getWallet(dao: AkitaDaoSDK) {
26
+ return await dao.getWallet();
27
+ }
28
+
29
+ export async function walletInfo(dao: AkitaDaoSDK, network: AkitaNetwork, json: boolean): Promise<void> {
30
+ const wallet = await getWallet(dao);
31
+ const state = await wallet.getGlobalState();
32
+
33
+ if (json) return printJson(state);
34
+
35
+ header("Wallet Info");
36
+ const pairs: [string, unknown][] = [];
37
+
38
+ for (const [k, v] of Object.entries(state)) {
39
+ if (HIDDEN_WALLET_FIELDS.has(k)) continue;
40
+
41
+ // Hide referrer if zero address
42
+ if (k === "referrer" && typeof v === "string" && isZeroAddress(v)) continue;
43
+
44
+ const label = camelToLabel(k);
45
+
46
+ if (v === undefined || v === null) {
47
+ pairs.push([label, "not set"]);
48
+ } else if (v instanceof Uint8Array) {
49
+ const hex = Buffer.from(v).toString("hex");
50
+ pairs.push([label, hex || "not set"]);
51
+ } else if (typeof v === "bigint") {
52
+ // Show app names for known app ID fields
53
+ if (k === "akitaDao" || k === "escrowFactory" || k === "factoryApp" || k === "revocation") {
54
+ pairs.push([label, v > 0n ? resolveAppName(v, network) : "not set"]);
55
+ } else if (k === "lastUserInteraction" || k === "lastChange") {
56
+ pairs.push([label, formatTimestamp(v)]);
57
+ } else {
58
+ pairs.push([label, v.toString()]);
59
+ }
60
+ } else if (typeof v === "string") {
61
+ pairs.push([label, v || "not set"]);
62
+ } else if (typeof v === "object" && v !== null) {
63
+ // PluginKey or other structs - skip complex objects
64
+ continue;
65
+ } else {
66
+ pairs.push([label, String(v)]);
67
+ }
68
+ }
69
+
70
+ printKV(pairs);
71
+ }
72
+
73
+ export async function walletPlugins(dao: AkitaDaoSDK, network: AkitaNetwork, json: boolean): Promise<void> {
74
+ const wallet = await getWallet(dao);
75
+ const plugins = await wallet.getPlugins();
76
+
77
+ const entries = Array.from(plugins.entries());
78
+
79
+ if (json) {
80
+ const arr = entries.map(([key, info]) => ({ key, ...info }));
81
+ return printJson(arr);
82
+ }
83
+
84
+ header("Installed Plugins");
85
+
86
+ if (entries.length === 0) {
87
+ console.log(" No plugins installed.");
88
+ return;
89
+ }
90
+
91
+ for (const [key, info] of entries) {
92
+ const { pluginId, caller, escrow } = parsePluginKey(key);
93
+
94
+ const pluginName = pluginId ? getAppName(pluginId, network) : undefined;
95
+ const pluginLabel = pluginName
96
+ ? `${pluginName} (${pluginId})`
97
+ : pluginId?.toString() ?? key;
98
+
99
+ const pairs: [string, string][] = [
100
+ ["Caller", caller === "" ? "(global)" : truncateAddress(caller)],
101
+ ];
102
+ if (escrow) pairs.push(["Escrow", escrow]);
103
+ pairs.push(
104
+ ["Admin", colorBool(info.admin)],
105
+ ["Delegation", delegationTypeLabel(info.delegationType)],
106
+ ["Cover Fees", colorBool(info.coverFees)],
107
+ ["Can Reclaim", colorBool(info.canReclaim)],
108
+ ["Use Exec Key", colorBool(info.useExecutionKey)],
109
+ ["Use Rounds", colorBool(info.useRounds)],
110
+ );
111
+ if (info.cooldown > 0n) {
112
+ pairs.push(["Cooldown", formatDuration(info.cooldown)]);
113
+ }
114
+ if (info.lastCalled > 0n) {
115
+ pairs.push(["Last Called", formatTimestamp(info.lastCalled)]);
116
+ }
117
+ if (info.start > 0n) {
118
+ pairs.push(["Start", formatTimestamp(info.start)]);
119
+ }
120
+ if (info.lastValid < BigInt("18446744073709551615")) {
121
+ pairs.push(["Last Valid", info.useRounds ? info.lastValid.toString() : formatTimestamp(info.lastValid)]);
122
+ }
123
+
124
+ // Method restrictions as hex selectors
125
+ const methods = info.methods.length > 0
126
+ ? info.methods.map((m) => Buffer.from(m.name).toString("hex"))
127
+ : undefined;
128
+
129
+ printPluginCard({ name: pluginLabel, pairs, methods });
130
+ }
131
+ }
132
+
133
+ /** Parse the concatenated plugin key string back into its components */
134
+ function parsePluginKey(key: string): { pluginId: bigint | null; caller: string; escrow: string } {
135
+ let digitEnd = 0;
136
+ while (digitEnd < key.length && key[digitEnd] >= "0" && key[digitEnd] <= "9") {
137
+ digitEnd++;
138
+ }
139
+
140
+ if (digitEnd === 0) {
141
+ return { pluginId: null, caller: "", escrow: key };
142
+ }
143
+
144
+ const pluginId = BigInt(key.slice(0, digitEnd));
145
+ const rest = key.slice(digitEnd);
146
+
147
+ if (rest.length >= 58) {
148
+ return {
149
+ pluginId,
150
+ caller: rest.slice(0, 58),
151
+ escrow: rest.slice(58),
152
+ };
153
+ }
154
+
155
+ return { pluginId, caller: rest, escrow: "" };
156
+ }
157
+
158
+ export async function walletNamedPlugins(dao: AkitaDaoSDK, json: boolean): Promise<void> {
159
+ const wallet = await getWallet(dao);
160
+ const named = await wallet.getNamedPlugins();
161
+
162
+ if (json) {
163
+ const obj = Object.fromEntries(Array.from(named.entries()).map(([name, key]) => [name, key]));
164
+ return printJson(obj);
165
+ }
166
+
167
+ header("Named Plugins");
168
+
169
+ const entries = Array.from(named.entries());
170
+ if (entries.length === 0) {
171
+ console.log(" No named plugins.");
172
+ return;
173
+ }
174
+
175
+ const rows = entries.map(([name, key]) => [
176
+ name,
177
+ key.plugin.toString(),
178
+ truncateAddress(key.caller),
179
+ key.escrow || "(default)",
180
+ ]);
181
+ printColumns(["Name", "Plugin App", "Caller", "Escrow"], rows);
182
+ }
183
+
184
+ export async function walletEscrows(dao: AkitaDaoSDK, json: boolean): Promise<void> {
185
+ const wallet = await getWallet(dao);
186
+ const escrows = await wallet.getEscrows();
187
+
188
+ if (json) {
189
+ const obj = Object.fromEntries(escrows.entries());
190
+ return printJson(obj);
191
+ }
192
+
193
+ header("Escrows");
194
+
195
+ const entries = Array.from(escrows.entries());
196
+ if (entries.length === 0) {
197
+ console.log(" No escrows.");
198
+ return;
199
+ }
200
+
201
+ const rows = entries.map(([name, info]) => [
202
+ name,
203
+ info.id.toString(),
204
+ String(info.locked),
205
+ ]);
206
+ printColumns(["Name", "App ID", "Locked"], rows);
207
+ }
208
+
209
+ export async function walletAllowances(dao: AkitaDaoSDK, json: boolean): Promise<void> {
210
+ const wallet = await getWallet(dao);
211
+ const allowances = await wallet.getAllowances();
212
+
213
+ const entries = Array.from(allowances.entries());
214
+
215
+ if (json) {
216
+ const arr = entries.map(([key, info]) => ({ key, ...info }));
217
+ return printJson(arr);
218
+ }
219
+
220
+ header("Allowances");
221
+
222
+ if (entries.length === 0) {
223
+ console.log(" No allowances.");
224
+ return;
225
+ }
226
+
227
+ const rows = entries.map(([key, info]) => {
228
+ let details: string;
229
+ if (info.type === "flat") {
230
+ details = `amount: ${formatBigInt(info.amount)}, spent: ${formatBigInt(info.spent)}`;
231
+ } else if (info.type === "window") {
232
+ details = `amount: ${formatBigInt(info.amount)}, spent: ${formatBigInt(info.spent)}, interval: ${formatDuration(info.interval)}`;
233
+ } else {
234
+ details = `rate: ${formatBigInt(info.rate)}, max: ${formatBigInt(info.max)}, interval: ${formatDuration(info.interval)}`;
235
+ }
236
+ return [key, info.type, details];
237
+ });
238
+ printColumns(["Key", "Type", "Details"], rows);
239
+ }
240
+
241
+ export async function walletExecutions(dao: AkitaDaoSDK, json: boolean): Promise<void> {
242
+ const wallet = await getWallet(dao);
243
+ const executions = await wallet.getExecutions();
244
+
245
+ if (json) {
246
+ const arr = Array.from(executions.entries()).map(([key, info]) => ({
247
+ key: Buffer.from(key).toString("hex"),
248
+ info,
249
+ }));
250
+ return printJson(arr);
251
+ }
252
+
253
+ header("Execution Keys");
254
+
255
+ const entries = Array.from(executions.entries());
256
+ if (entries.length === 0) {
257
+ console.log(" No execution keys.");
258
+ return;
259
+ }
260
+
261
+ const rows = entries.map(([key, info]) => [
262
+ Buffer.from(key).toString("hex").slice(0, 16) + "...",
263
+ info.firstValid.toString(),
264
+ info.lastValid.toString(),
265
+ ]);
266
+ printColumns(["Lease (hex)", "First Valid", "Last Valid"], rows);
267
+ }
268
+
269
+ export async function walletBalance(
270
+ dao: AkitaDaoSDK,
271
+ network: AkitaNetwork,
272
+ json: boolean,
273
+ assetArgs: string[],
274
+ escrowName?: string
275
+ ): Promise<void> {
276
+ const wallet = await getWallet(dao);
277
+ const ids = getNetworkAppIds(network);
278
+
279
+ // Determine which address to check
280
+ let address: string;
281
+ let label: string;
282
+
283
+ if (escrowName) {
284
+ const escrows = await wallet.getEscrows();
285
+ const escrow = escrows.get(escrowName);
286
+ if (!escrow) {
287
+ console.error(`Escrow "${escrowName}" not found. Use 'akita wallet escrows' to see available escrows.`);
288
+ return;
289
+ }
290
+ address = algosdk.getApplicationAddress(escrow.id).toString();
291
+ label = `Escrow: ${escrowName}`;
292
+ } else {
293
+ address = algosdk.getApplicationAddress(wallet.appId).toString();
294
+ label = "Wallet";
295
+ }
296
+
297
+ let assets: bigint[];
298
+ if (assetArgs.length > 0) {
299
+ assets = assetArgs.map((a) => BigInt(a));
300
+ } else {
301
+ // Default: ALGO (0), AKTA, BONES
302
+ assets = [0n, ids.akta, ids.bones];
303
+ }
304
+
305
+ const assetLabels = assets.map((a) => {
306
+ if (a === 0n) return "ALGO";
307
+ if (a === ids.akta) return "AKTA";
308
+ if (a === ids.bones) return "BONES";
309
+ if (a === ids.usdc) return "USDC";
310
+ return a.toString();
311
+ });
312
+
313
+ // Fetch balances via algod account info
314
+ let balances: bigint[];
315
+ try {
316
+ const accountInfo = await dao.algorand.account.getInformation(address);
317
+ balances = assets.map((assetId) => {
318
+ if (assetId === 0n) {
319
+ return BigInt(accountInfo.amount);
320
+ }
321
+ const holding = accountInfo.assets?.find(
322
+ (a: { assetId: bigint }) => a.assetId === assetId
323
+ );
324
+ return holding ? holding.amount : 0n;
325
+ });
326
+ } catch (e: any) {
327
+ console.error(`Failed to fetch balances: ${e.message ?? e}`);
328
+ return;
329
+ }
330
+
331
+ if (json) {
332
+ const data = assets.map((asset, i) => ({
333
+ asset,
334
+ label: assetLabels[i],
335
+ balance: balances[i],
336
+ }));
337
+ return printJson(data);
338
+ }
339
+
340
+ header(`${label} Balances`);
341
+ const rows = assets.map((asset, i) => {
342
+ let formatted: string;
343
+ if (asset === 0n) {
344
+ // ALGO: 6 decimals
345
+ const whole = balances[i] / 1_000_000n;
346
+ const frac = balances[i] % 1_000_000n;
347
+ formatted = frac === 0n
348
+ ? whole.toLocaleString()
349
+ : `${whole.toLocaleString()}.${frac.toString().padStart(6, "0").replace(/0+$/, "")}`;
350
+ } else {
351
+ formatted = formatBigInt(balances[i]);
352
+ }
353
+ return [assetLabels[i], asset.toString(), formatted];
354
+ });
355
+ printColumns(["Asset", "ID", "Balance"], rows);
356
+ }