taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-wallet-cli/src/balance-pretty.test.ts | 51++++++++++++++++++++++++++++++++++++++-------------
Mpackages/taler-wallet-cli/src/balance-pretty.ts | 79++++++++++++++++++++++++++++++++++++++++++-------------------------------------
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(", ")}`); }