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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.11.1] - 2025-08-06

### 🐛 Bug Fixes
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ This will prompt you for a name, a kind (only UTxORPC supported), whether it is

> If you have a [Demeter](https://demeter.run) port you would have to set the URL as `https://{host}` and on put `dmtr-api-key:YOUR_API_KEY` on the headers.

To inspect the current wallet's UTxOs in JSON format, run:

```sh
cargo run -- wallet utxos
```

Use `--output-format` to override the default JSON response if you prefer a table view instead.

# Examples

In the `examples` folder you can find scripts demonstrating advanced capabilities.
Expand Down
10 changes: 10 additions & 0 deletions docs/wallet.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,13 @@ And the `--help` flag can be used anywhere in Cshell. For example, after choosin
```bash
cshell wallet create --help
```

### Inspect wallet UTxOs

Retrieve the live UTxO set for the currently selected wallet. The response defaults to JSON, mirroring the exact schema returned by the configured UTxoRPC provider.

```bash
cshell wallet utxos
```

Use `--output-format table` if you prefer a tabular summary in the terminal.
10 changes: 6 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,21 +96,23 @@ pub struct Context {
pub store: store::Store,
pub output_format: output::OutputFormat,
pub log_level: LogLevel,
pub output_format_overridden: bool,
}

impl Context {
fn from_cli(cli: &Cli) -> anyhow::Result<Self> {
let store = store::Store::open(cli.store_path.clone())?;
let output_format = cli
.output_format
.clone()
.unwrap_or(output::OutputFormat::Table);
let (output_format, output_format_overridden) = match cli.output_format.clone() {
Some(value) => (value, true),
None => (output::OutputFormat::Table, false),
};
let log_level = cli.log_level.clone().unwrap_or(LogLevel::Info);

Ok(Context {
store,
output_format,
log_level,
output_format_overridden,
})
}

Expand Down
41 changes: 41 additions & 0 deletions src/provider/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,47 @@ impl Provider {
})
}

pub async fn get_wallet_utxos(
&self,
address: &Address,
) -> Result<Vec<utxorpc::spec::query::AnyUtxoData>> {
let mut client: CardanoQueryClient = self.client().await?;

let predicate = utxorpc::spec::query::UtxoPredicate {
r#match: Some(utxorpc::spec::query::AnyUtxoPattern {
utxo_pattern: Some(UtxoPattern::Cardano(
utxorpc::spec::cardano::TxOutputPattern {
address: Some(utxorpc::spec::cardano::AddressPattern {
exact_address: address.to_vec().into(),
..Default::default()
}),
..Default::default()
},
)),
}),
..Default::default()
};

let response = client
.search_utxos(predicate, None, u32::MAX)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unbounded UTxO query could cause performance issues.

Using u32::MAX as the limit allows fetching an unbounded number of UTxOs. Wallets with many UTxOs (e.g., exchange wallets, large DeFi contracts) could trigger slow queries, high memory usage, or timeouts.

Consider either:

  1. Adding a configurable limit with a reasonable default (e.g., 1000)
  2. Implementing pagination support
  3. Documenting this limitation if it's intentional
-        let response = client
-            .search_utxos(predicate, None, u32::MAX)
-            .await
-            .context("failed to query utxos")?;
+        // Consider adding a reasonable limit or pagination
+        const MAX_UTXOS: u32 = 10000;
+        let response = client
+            .search_utxos(predicate, None, MAX_UTXOS)
+            .await
+            .context("failed to query utxos")?;

Committable suggestion skipped: line range outside the PR's diff.

.await
.context("failed to query utxos")?;

let utxos = response
.items
.into_iter()
.map(|utxo| utxorpc::spec::query::AnyUtxoData {
native_bytes: utxo.native,
txo_ref: utxo.txo_ref,
parsed_state: utxo
.parsed
.map(utxorpc::spec::query::any_utxo_data::ParsedState::Cardano),
})
.collect();

Ok(utxos)
}

pub async fn get_detailed_balance(&self, address: &Address) -> Result<DetailedBalance> {
let mut client: CardanoQueryClient = self.client().await?;

Expand Down
4 changes: 4 additions & 0 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod info;
mod list;
mod restore;
pub mod types;
mod utxos;

#[derive(Parser)]
pub struct Args {
Expand All @@ -36,6 +37,8 @@ enum Commands {
Delete(delete::Args),
/// show wallet balance
Balance(balance::Args),
/// List wallet UTxOs
Utxos(utxos::Args),
}

#[instrument("wallet", skip_all)]
Expand All @@ -49,5 +52,6 @@ pub async fn run(args: Args, ctx: &mut crate::Context) -> anyhow::Result<()> {
Commands::List => list::run(ctx).await,
Commands::Delete(args) => delete::run(args, ctx).await,
Commands::Balance(args) => balance::run(args, ctx).await,
Commands::Utxos(args) => utxos::run(args, ctx).await,
}
}
124 changes: 124 additions & 0 deletions src/wallet/utxos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use anyhow::bail;
use clap::Parser;
use comfy_table::Table;
use serde_json::json;
use utxorpc::spec::query::{any_utxo_data::ParsedState, AnyUtxoData};

use crate::output::{OutputFormat, OutputFormatter};

#[derive(Parser)]
pub struct Args {
/// Name of the wallet to show the UTxOs of. If undefined, the default wallet is used.
name: Option<String>,

/// Name of the provider to use. If undefined, the default provider is used.
provider: Option<String>,
}

pub async fn run(args: Args, ctx: &crate::Context) -> anyhow::Result<()> {
let wallet = match args.name {
Some(name) => ctx.store.find_wallet(&name),
None => ctx.store.default_wallet(),
};

let provider = match args.provider {
Some(name) => ctx.store.find_provider(&name),
None => ctx.store.default_provider(),
};

match (wallet, provider) {
(Some(wallet), Some(provider)) => {
let address = wallet.address(provider.is_testnet());
let utxos = provider.get_wallet_utxos(&address).await?;
let output = WalletUtxoOutput::new(utxos);

let format = if ctx.output_format_overridden {
ctx.output_format.clone()
} else {
OutputFormat::Json
};

output.output(&format);

Ok(())
}
(None, Some(_)) => bail!("Wallet not found."),
(Some(_), None) => bail!("Provider not found."),
(None, None) => bail!("Wallet and provider not found."),
}
}

struct WalletUtxoOutput {
utxos: Vec<AnyUtxoData>,
}

impl WalletUtxoOutput {
fn new(utxos: Vec<AnyUtxoData>) -> Self {
Self { utxos }
}
}

impl OutputFormatter for WalletUtxoOutput {
fn to_table(&self) {
let mut table = Table::new();

table.set_header(vec!["Tx Hash", "Index", "Lovelace", "Assets", "Datum Hash"]);

for utxo in &self.utxos {
let (tx_hash, index) = utxo
.txo_ref
.as_ref()
.map(|reference| (hex::encode(&reference.hash), reference.index.to_string()))
.unwrap_or_else(|| ("-".to_string(), "-".to_string()));

let (coin, asset_count, datum_hash) = match &utxo.parsed_state {
Some(ParsedState::Cardano(output)) => {
let asset_count: usize = output
.assets
.iter()
.map(|multiasset| multiasset.assets.len())
.sum();

let datum_hash = output
.datum
.as_ref()
.map(|datum| hex::encode(&datum.hash))
.unwrap_or_else(|| "-".to_string());

(output.coin.to_string(), asset_count.to_string(), datum_hash)
}
None => ("-".to_string(), "0".to_string(), "-".to_string()),
};

table.add_row(vec![tx_hash, index, coin, asset_count, datum_hash]);
}

println!("{table}");
}

fn to_json(&self) {
let payload = json!({ "utxos": self.utxos });
println!("{}", serde_json::to_string_pretty(&payload).unwrap());
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parses_without_arguments() {
let args = Args::parse_from(["wallet-utxos"]);

assert!(args.name.is_none());
assert!(args.provider.is_none());
}

#[test]
fn parses_with_wallet_and_provider() {
let args = Args::parse_from(["wallet-utxos", "alice", "mainnet"]);

assert_eq!(args.name.as_deref(), Some("alice"));
assert_eq!(args.provider.as_deref(), Some("mainnet"));
}
}
Loading