Skip to content

Commit b7c4a41

Browse files
committed
feat: fetch /addresses/{addr}/utxos from the ledger state
Resolves #295
1 parent 5cd9757 commit b7c4a41

File tree

3 files changed

+112
-6
lines changed

3 files changed

+112
-6
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ tokio-tungstenite = "0.26"
1919
tracing = "0.1.41"
2020
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] }
2121
serde = { version = "1.0.218", features = ["derive"] }
22+
minicbor = "0.26.1"
2223
tower-http = { version = "0.6.1", features = ["normalize-path"] }
2324
tower-layer = "0.3.2"
2425
tower = "0.5.1"
Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,128 @@
11
use crate::{
2-
BlockfrostError,
2+
BlockfrostError, NodePool,
33
addresses::{AddressInfo, AddressesPath},
44
api::ApiResult,
55
config::Config,
66
pagination::{Pagination, PaginationQuery},
77
};
88
use axum::{
9-
Extension,
9+
Extension, Json,
1010
extract::{Path, Query},
1111
};
12-
use blockfrost_openapi::models::address_utxo_content_inner::AddressUtxoContentInner;
12+
use blockfrost_openapi::models::{
13+
address_utxo_content_inner::AddressUtxoContentInner,
14+
tx_content_output_amount_inner::TxContentOutputAmountInner,
15+
};
16+
use pallas_network::miniprotocols::localstate::{self, queries_v16};
17+
use std::sync::Arc;
18+
19+
const UNIT_LOVELACE: &str = "lovelace";
1320

21+
/// See <https://docs.blockfrost.io/#tag/cardano--addresses/GET/addresses/{address}/utxos>, analogous to:
22+
///
23+
/// ```text
24+
/// ❯ cardano-cli query utxo --testnet-magic 2 --address addr_test1qzv6t… --output-json
25+
/// ```
1426
pub async fn route(
1527
Path(address_path): Path<AddressesPath>,
16-
Extension(config): Extension<Config>,
28+
Extension(node): Extension<NodePool>,
29+
Extension(config): Extension<Arc<Config>>,
1730
Query(pagination_query): Query<PaginationQuery>,
1831
) -> ApiResult<Vec<AddressUtxoContentInner>> {
1932
let AddressesPath { address, asset: _ } = address_path;
2033
let _ = Pagination::from_query(pagination_query).await?;
21-
let _ = AddressInfo::from_address(&address, config.network)?;
34+
let _ = AddressInfo::from_address(&address, config.network.clone())?;
35+
36+
// XXX: Axum must not abort Ouroboros protocols in the middle, hence a separate Tokio task:
37+
let utxos = tokio::spawn(async move {
38+
let mut node = node.get().await?;
39+
40+
let addr = pallas_addresses::Address::from_bech32(&address).map_err(|err| {
41+
BlockfrostError::custom_400(format!("invalid bech32 address: {}: {}", address, err))
42+
})?;
43+
44+
node.with_statequery(|client: &mut localstate::GenericClient| {
45+
Box::pin(async move {
46+
let era: u16 = queries_v16::get_current_era(client).await?;
47+
let addrs: queries_v16::Addrs = Vec::from([addr.to_vec().into()]);
48+
let result = queries_v16::get_utxo_by_address(client, era, addrs)
49+
.await?
50+
.to_vec();
51+
Ok(result)
52+
})
53+
})
54+
.await
55+
})
56+
.await
57+
.expect("addresses_utxos panic!")?;
58+
59+
let mut response: Vec<AddressUtxoContentInner> = Vec::with_capacity(utxos.len());
60+
61+
for (utxo, tx_output) in utxos.into_iter() {
62+
let (address, amount, data_hash, inline_datum, reference_script) = match tx_output {
63+
queries_v16::TransactionOutput::Current(o) => {
64+
let (datum_hash, inline_datum) = match o.inline_datum {
65+
Some(queries_v16::DatumOption::Hash(hash)) => (Some(hash), None),
66+
Some(queries_v16::DatumOption::Data(data)) => (None, Some(data)),
67+
None => (None, None),
68+
};
69+
(o.address, o.amount, datum_hash, inline_datum, o.script_ref)
70+
},
71+
queries_v16::TransactionOutput::Legacy(o) => {
72+
(o.address, o.amount, o.datum_hash, None, None)
73+
},
74+
};
75+
76+
let address = pallas_addresses::Address::from_bytes(address.as_slice()).map_err(|err| {
77+
BlockfrostError::custom_400(format!("invalid bech32 addr: {}: {}", address, err))
78+
})?;
79+
80+
let amount: Vec<TxContentOutputAmountInner> = match amount {
81+
queries_v16::Value::Coin(coin) => vec![TxContentOutputAmountInner {
82+
unit: UNIT_LOVELACE.to_string(),
83+
quantity: Into::<u64>::into(coin).to_string(),
84+
}],
85+
queries_v16::Value::Multiasset(coin, multiasset) => {
86+
let mut rv = vec![TxContentOutputAmountInner {
87+
unit: UNIT_LOVELACE.to_string(),
88+
quantity: Into::<u64>::into(coin).to_string(),
89+
}];
90+
for (policy_id, assets) in multiasset.into_iter() {
91+
for (asset_name, coin) in assets.into_iter() {
92+
rv.push(TxContentOutputAmountInner {
93+
// FIXME: concatenated how? with a comma?
94+
unit: format!("{},{}", policy_id, hex::encode(asset_name.as_slice())),
95+
quantity: Into::<u64>::into(coin).to_string(),
96+
});
97+
}
98+
}
99+
rv
100+
},
101+
};
102+
103+
let inline_datum = inline_datum.map(|id| hex::encode(minicbor::to_vec(id).unwrap())); // safe, infallible
104+
105+
// FIXME: I think this is wrong, because it’s not the hash of the reference script, but the CBOR of the reference script itself?
106+
let reference_script_hash =
107+
reference_script.map(|id| hex::encode(minicbor::to_vec(id).unwrap())); // safe, infallible
108+
109+
// TODO: The block hash may be impossible without a DB Sync (or similar)?
110+
let block = "impossible for now?".to_string();
111+
112+
let output_index: u64 = utxo.index.into();
113+
114+
response.push(AddressUtxoContentInner {
115+
address: address.to_string(),
116+
tx_hash: utxo.transaction_id.to_string(),
117+
tx_index: output_index as i32,
118+
output_index: output_index as i32,
119+
amount,
120+
block,
121+
data_hash: data_hash.map(|a| a.to_string()),
122+
inline_datum,
123+
reference_script_hash,
124+
});
125+
}
22126

23-
Err(BlockfrostError::not_found())
127+
Ok(Json(response))
24128
}

0 commit comments

Comments
 (0)