Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ User-visible changes to `voltage`, newest first. Each pull request that changes
- Help headers and usage are Voltage orange where the terminal draws 256 or 24-bit color, and bold elsewhere. `FORCE_COLOR` is honored: `0` or `false` turns color off, other values turn it on when redirected; `--color` and `NO_COLOR` still win.
- A successful `voltage login` on a terminal shows the `voltage` wordmark, a lightning bolt beside the name in Voltage orange, above `✓ Logged in as`.
- `voltage context` and `voltage auth status` draw their fields in a titled card on a terminal, with sources dimmed. Redirected output is unchanged.
- `voltage wallets get` on a terminal leads with the wallet's name, its balances (with a USD estimate on mainnet), and a 14-day sparkline of completed payments.

## 0.2.0 - 2026-10-22

Expand Down
2 changes: 2 additions & 0 deletions docs/guides/wallets.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ voltage wallets ledger list WALLET_ID --profile prod --all
voltage wallets payments summary WALLET_ID --profile prod
```

On a terminal, `wallets get` first shows the wallet's name, each available balance, labelled `Available` (with a USD estimate for a BTC balance on mainnet, from the price service), and a sparkline of its completed payment volume over the last 14 days. That takes two extra reads, the BTC price on mainnet and the wallet's 100 newest completed payments (payment checks excluded), summed from each payment's amount in the wallet's currency. Each read has 5 seconds at most, and one that fails or is slow leaves its line out. The wallet's fields follow as usual, and redirected or `--json` output is unchanged.

The ledger lists every credit, hold, capture, and release on the wallet. `--all` follows every page; with `--output ndjson` each page is written as it arrives.

## Change a wallet
Expand Down
66 changes: 65 additions & 1 deletion src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
Error, Result,
auth::{self, ApiCredential, Resolver},
backoff::{Backoff, POLL_CEILING},
balance,
cli::{ApiInvocation, GlobalFlags, Origin, Requested},
config::{
API_URL, EXECUTE_VARIABLE, Scope, Settings, Source, new_private, private_dir, read_private,
Expand All @@ -19,8 +20,10 @@ use crate::{
input::{PaginationMode, Request},
output::{Envelope, Outcome, Output},
payment::{Currency, PaymentDirection, PaymentView, ReceiveKind, WaitProgress, WaitTarget},
price::{PRICE_URL, PriceService, UsdPerBtc},
registry::{AuthScheme, Method, Operation, OperationId, operation},
secret::Secret,
style::Depth,
terminal::Terminal,
timeline::Timeline,
};
Expand All @@ -32,7 +35,7 @@ use std::{
collections::BTreeSet,
io::Write,
sync::{Mutex, OnceLock, PoisonError},
time::Duration,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tokio::time::Instant;
use uuid::Uuid;
Expand Down Expand Up @@ -630,6 +633,7 @@ pub async fn execute(
}
let call = Call {
api,
global,
invocation,
scope,
deadline: Instant::now() + global.timeout,
Expand Down Expand Up @@ -862,6 +866,7 @@ impl Mutation {
/// method, so the request and output are not handed from step to step.
struct Call<'a> {
api: Api,
global: &'a GlobalFlags,
invocation: &'a ApiInvocation,
scope: &'a Scope,
deadline: Instant,
Expand Down Expand Up @@ -906,11 +911,70 @@ impl Call<'_> {
self.write_pages(response, Outcome::Retrieved, false).await
}
ReadThen::Pages { follow } => {
if operation.id == OperationId::GetWallet && self.out.on_terminal() {
self.lead_with_balance(&response.body).await?;
}
self.write_pages(response, Outcome::Retrieved, follow).await
}
}
}

/// On a terminal, put the wallet's balance and recent activity above its fields. The
/// price and payments reads are best effort: a failure leaves its line out.
async fn lead_with_balance(&self, wallet: &Value) -> Result<()> {
let mut headline = balance::Headline::from_wallet(wallet);
if headline.priceable.is_some() {
let base = self.global.price_url.as_deref().unwrap_or(PRICE_URL);
if let Ok(service) =
PriceService::new(base, balance::READ_LIMIT.min(self.global.timeout))
&& let Ok(quote) = self
.api
.terminal
.during("Reading the BTC price...", service.quote(None))
.await
&& let Ok(rate) = UsdPerBtc::parse(&quote.price)
{
headline.price(rate);
}
}
// The wallet's completed payments, newest first, as `payments list` would ask.
let uuid = |name: &str| wallet.get(name)?.as_str()?.parse::<Uuid>().ok();
let scope = Scope {
org: uuid("organization_id"),
envs: uuid("environment_id").into_iter().collect(),
wallet: uuid("id"),
..Scope::default()
};
if scope.wallet.is_some()
&& let Ok(mut request) = Request::scoped(operation(OperationId::GetPayments), &scope)
{
request.query.extend(
[
("statuses[]", "completed"),
// Payment checks are not payments, so they add no volume.
("check_only", "false"),
("sort_order", "DESC"),
("limit", balance::ACTIVITY_LIMIT),
]
.map(|(name, value)| (name.to_owned(), value.to_owned())),
);
let read = self
.api
.send(Method::Get, &request.path, &request.query, None);
if let Ok(Ok(page)) = tokio::time::timeout(balance::READ_LIMIT, read).await {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |since| {
i64::try_from(since.as_secs()).unwrap_or(i64::MAX)
});
headline.activity = Some(balance::activity(&page.body, now, &headline.currency));
}
}
let glyphs = self.api.terminal.glyphs();
let depth = Depth::detect(|name| std::env::var(name).ok());
self.out.preface(&headline.lines(glyphs, depth))
}

/// Verify and journal, then send the mutation exactly once. A transport
/// failure is reported as an uncertain submission unless reconciliation proves acceptance.
async fn mutate(
Expand Down
Loading
Loading