commit 8665bf97a5f673e9201c5fdd44850838fb60f090
parent abf37bc226b7ccc07b2619599bb9eb85804af033
Author: Florian Dold <dold@taler.net>
Date: Mon, 10 Aug 2026 00:42:24 +0200
wallet-cli: compact balance output
Diffstat:
2 files changed, 80 insertions(+), 50 deletions(-)
diff --git a/packages/taler-wallet-cli/src/balance-pretty.test.ts b/packages/taler-wallet-cli/src/balance-pretty.test.ts
@@ -30,7 +30,7 @@ function makeBalance(extra: Record<string, unknown> = {}): WalletBalance {
} as WalletBalance;
}
-test("pretty balance output focuses on funds that need attention", () => {
+test("pretty balance output is compact", () => {
const lines = formatPrettyBalance(
makeBalance({
pendingIncoming: "KUDOS:2",
@@ -38,14 +38,10 @@ test("pretty balance output focuses on funds that need attention", () => {
}),
);
- assert.deepStrictEqual(lines, [
- "KUDOS KUDOS:12.50 [available]",
- " Pending incoming: KUDOS:2",
- " Pending outgoing: KUDOS:1",
- ]);
+ assert.deepStrictEqual(lines, ["KUDOS:12.50 in=KUDOS:2 out=KUDOS:1"]);
});
-test("pretty balance output identifies non-global scopes and restrictions", () => {
+test("pretty balance output abbreviates non-global scopes", () => {
const lines = formatPrettyBalance(
makeBalance({
scopeInfo: {
@@ -57,20 +53,44 @@ test("pretty balance output identifies non-global scopes and restrictions", () =
}),
);
- assert.ok(lines.includes(" Exchange: https://exchange.example/"));
- assert.ok(lines.includes(" Restrictions: outgoing-kyc"));
+ assert.deepStrictEqual(lines, [
+ "KUDOS:12.50 in=KUDOS:0 out=KUDOS:0 @e:exchange.example",
+ ]);
});
-test("the one-line view remains compact", () => {
+test("different scope kinds use distinct abbreviations", () => {
+ const auditor = formatPrettyBalance(
+ makeBalance({
+ scopeInfo: {
+ type: ScopeType.Auditor,
+ currency: "KUDOS",
+ url: "https://auditor.example/",
+ },
+ }),
+ );
+ const legacyExchange = formatPrettyBalance(
+ makeBalance({
+ scopeInfo: {
+ type: ScopeType.ExchangeLegacyKeys,
+ currency: "KUDOS",
+ url: "https://exchange.example/",
+ masterPub: "MASTER_PUB",
+ },
+ }),
+ );
+
+ assert.ok(auditor[0].endsWith("@a:auditor.example"));
+ assert.ok(legacyExchange[0].endsWith("@e-legacy:exchange.example"));
+});
+
+test("the one-line view matches the compact default", () => {
const lines = formatPrettyBalance(
makeBalance({ pendingIncoming: "KUDOS:2" }),
false,
true,
);
- assert.deepStrictEqual(lines, [
- "KUDOS KUDOS:12.50 [available] — Pending incoming: KUDOS:2",
- ]);
+ assert.deepStrictEqual(lines, ["KUDOS:12.50 in=KUDOS:2 out=KUDOS:0"]);
});
test("verbose output includes optional diagnostic details", () => {
@@ -83,6 +103,11 @@ test("verbose output includes optional diagnostic details", () => {
true,
);
+ assert.ok(
+ !formatPrettyBalance(
+ makeBalance({ flags: [BalanceFlag.OutgoingKyc] }),
+ ).some((x) => x.includes("Restrictions:")),
+ );
assert.ok(lines.includes(" Shopping URLs: https://shop.example/"));
assert.ok(lines.includes(" Peer payments: disabled"));
assert.ok(lines.includes(" Direct deposits: disabled"));
diff --git a/packages/taler-wallet-cli/src/balance-pretty.ts b/packages/taler-wallet-cli/src/balance-pretty.ts
@@ -19,31 +19,43 @@
* Compact, useful rendering of wallet balances.
*/
-import {
- Amounts,
- ScopeInfo,
- ScopeType,
- WalletBalance,
-} from "@gnu-taler/taler-util";
+import { ScopeInfo, ScopeType, WalletBalance } from "@gnu-taler/taler-util";
-function scopeDescription(scopeInfo: ScopeInfo): string | undefined {
+function scopeHost(scopeInfo: ScopeInfo): string {
+ if (scopeInfo.type === ScopeType.Global) {
+ return "";
+ }
+ try {
+ return new URL(scopeInfo.url).hostname;
+ } catch {
+ return scopeInfo.url;
+ }
+}
+
+function abbreviatedScope(scopeInfo: ScopeInfo): string | undefined {
switch (scopeInfo.type) {
case ScopeType.Global:
return undefined;
case ScopeType.Exchange:
- return `Exchange: ${scopeInfo.url}`;
+ return `@e:${scopeHost(scopeInfo)}`;
case ScopeType.Auditor:
- return `Auditor: ${scopeInfo.url}`;
+ return `@a:${scopeHost(scopeInfo)}`;
case ScopeType.ExchangeLegacyKeys:
- return `Exchange (old keys): ${scopeInfo.url}`;
+ return `@e-legacy:${scopeHost(scopeInfo)}`;
}
}
function scopeDetails(scopeInfo: ScopeInfo): string | undefined {
- if (scopeInfo.type === ScopeType.ExchangeLegacyKeys) {
- return `Exchange master public key: ${scopeInfo.masterPub}`;
+ switch (scopeInfo.type) {
+ case ScopeType.Global:
+ return undefined;
+ case ScopeType.Exchange:
+ return `Scope: exchange ${scopeInfo.url}`;
+ case ScopeType.Auditor:
+ return `Scope: auditor ${scopeInfo.url}`;
+ case ScopeType.ExchangeLegacyKeys:
+ return `Scope: exchange (old keys) ${scopeInfo.url}`;
}
- return undefined;
}
/** Render one balance for a person reading the CLI output. */
@@ -52,38 +64,31 @@ export function formatPrettyBalance(
verbose = false,
oneline = false,
): string[] {
- const lines = [
- `${balance.scopeInfo.currency} ${balance.available} [available]`,
- ];
- const scope = scopeDescription(balance.scopeInfo);
- const pendingIncoming = Amounts.isNonZero(balance.pendingIncoming)
- ? `Pending incoming: ${balance.pendingIncoming}`
- : undefined;
- const pendingOutgoing = Amounts.isNonZero(balance.pendingOutgoing)
- ? `Pending outgoing: ${balance.pendingOutgoing}`
- : undefined;
+ const scope = abbreviatedScope(balance.scopeInfo);
+ const headline = [
+ balance.available,
+ `in=${balance.pendingIncoming}`,
+ `out=${balance.pendingOutgoing}`,
+ scope,
+ ]
+ .filter((x): x is string => x != null)
+ .join(" ");
+ const lines = [headline];
if (oneline) {
- const parts = [...lines];
- if (pendingIncoming != null) parts.push(pendingIncoming);
- if (pendingOutgoing != null) parts.push(pendingOutgoing);
- if (scope != null) parts.push(scope);
- if (balance.flags.length > 0)
- parts.push(`Restrictions: ${balance.flags.join(", ")}`);
- return [parts.join(" — ")];
- }
-
- if (pendingIncoming != null) lines.push(` ${pendingIncoming}`);
- if (pendingOutgoing != null) lines.push(` ${pendingOutgoing}`);
- if (scope != null) lines.push(` ${scope}`);
- if (balance.flags.length > 0) {
- lines.push(` Restrictions: ${balance.flags.join(", ")}`);
+ return lines;
}
if (!verbose) return lines;
const detail = scopeDetails(balance.scopeInfo);
if (detail != null) lines.push(` ${detail}`);
+ if (balance.scopeInfo.type === ScopeType.ExchangeLegacyKeys) {
+ lines.push(` Exchange master public key: ${balance.scopeInfo.masterPub}`);
+ }
+ if (balance.flags.length > 0) {
+ lines.push(` Restrictions: ${balance.flags.join(", ")}`);
+ }
if (balance.shoppingUrls != null && balance.shoppingUrls.length > 0) {
lines.push(` Shopping URLs: ${balance.shoppingUrls.join(", ")}`);
}