Skip to content

Commit af308a3

Browse files
committed
feat: address extended REST handler
Signed-off-by: William Hankins <[email protected]>
1 parent 3ebd8a9 commit af308a3

File tree

4 files changed

+257
-39
lines changed

4 files changed

+257
-39
lines changed

common/src/queries/assets.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::queries::errors::QueryError;
22
use crate::{
3-
AssetAddressEntry, AssetInfoRecord, AssetMintRecord, AssetName, PolicyAsset, PolicyId,
4-
TxIdentifier,
3+
AssetAddressEntry, AssetInfoRecord, AssetMetadata, AssetMintRecord, AssetName, NativeAssets,
4+
PolicyAsset, PolicyId, TxIdentifier,
55
};
66

77
pub const DEFAULT_ASSETS_QUERY_TOPIC: (&str, &str) =
@@ -27,6 +27,7 @@ pub enum AssetsStateQuery {
2727
GetPolicyIdAssets { policy: PolicyId },
2828
GetAssetAddresses { policy: PolicyId, name: AssetName },
2929
GetAssetTransactions { policy: PolicyId, name: AssetName },
30+
GetAssetsMetadata { assets: NativeAssets },
3031
}
3132

3233
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -37,5 +38,6 @@ pub enum AssetsStateQueryResponse {
3738
AssetAddresses(AssetAddresses),
3839
AssetTransactions(AssetTransactions),
3940
PolicyIdAssets(PolicyAssets),
41+
AssetsMetadata(Vec<AssetMetadata>),
4042
Error(QueryError),
4143
}

modules/rest_blockfrost/src/handlers/addresses.rs

Lines changed: 214 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::sync::Arc;
22

3-
use crate::types::AddressTotalsREST;
3+
use crate::types::{AddressInfoExtended, AddressTotalsREST};
44
use crate::{handlers_config::HandlersConfig, types::AddressInfoREST};
5+
use acropolis_common::queries::assets::{AssetsStateQuery, AssetsStateQueryResponse};
56
use acropolis_common::queries::errors::QueryError;
67
use acropolis_common::rest_error::RESTError;
8+
use acropolis_common::AssetMetadata;
79
use acropolis_common::{
810
messages::{Message, RESTResponse, StateQuery, StateQueryResponse},
911
queries::{
@@ -14,6 +16,8 @@ use acropolis_common::{
1416
Address, Value,
1517
};
1618
use caryatid_sdk::Context;
19+
use serde::Serialize;
20+
use serde_cbor::Value as CborValue;
1721

1822
/// Handle `/addresses/{address}` Blockfrost-compatible endpoint
1923
pub async fn handle_address_single_blockfrost(
@@ -114,11 +118,125 @@ pub async fn handle_address_single_blockfrost(
114118

115119
/// Handle `/addresses/{address}/extended` Blockfrost-compatible endpoint
116120
pub async fn handle_address_extended_blockfrost(
117-
_context: Arc<Context<Message>>,
118-
_params: Vec<String>,
119-
_handlers_config: Arc<HandlersConfig>,
121+
context: Arc<Context<Message>>,
122+
params: Vec<String>,
123+
handlers_config: Arc<HandlersConfig>,
120124
) -> Result<RESTResponse, RESTError> {
121-
Err(RESTError::not_implemented("Address extended endpoint"))
125+
let address = parse_address(&params)?;
126+
let stake_address = match address {
127+
Address::Shelley(ref addr) => addr.stake_address_string()?,
128+
_ => None,
129+
};
130+
131+
let address_type = address.kind().to_string();
132+
let is_script = address.is_script();
133+
134+
let address_query_msg = Arc::new(Message::StateQuery(StateQuery::Addresses(
135+
AddressStateQuery::GetAddressUTxOs {
136+
address: address.clone(),
137+
},
138+
)));
139+
140+
let utxo_identifiers = query_state(
141+
&context,
142+
&handlers_config.addresses_query_topic,
143+
address_query_msg,
144+
|message| match message {
145+
Message::StateQueryResponse(StateQueryResponse::Addresses(
146+
AddressStateQueryResponse::AddressUTxOs(utxo_identifiers),
147+
)) => Ok(Some(utxo_identifiers)),
148+
Message::StateQueryResponse(StateQueryResponse::Addresses(
149+
AddressStateQueryResponse::Error(QueryError::NotFound { .. }),
150+
)) => Ok(None),
151+
Message::StateQueryResponse(StateQueryResponse::Addresses(
152+
AddressStateQueryResponse::Error(e),
153+
)) => Err(e),
154+
_ => Err(QueryError::internal_error(
155+
"Unexpected message type while retrieving address UTxOs",
156+
)),
157+
},
158+
)
159+
.await?;
160+
161+
let utxo_identifiers = match utxo_identifiers {
162+
Some(identifiers) => identifiers,
163+
None => {
164+
// Empty address - return zero balance (Blockfrost behavior)
165+
let rest_response = AddressInfoREST {
166+
address: address.to_string()?,
167+
amount: Value {
168+
lovelace: 0,
169+
assets: Vec::new(),
170+
}
171+
.into(),
172+
stake_address,
173+
address_type,
174+
script: is_script,
175+
};
176+
177+
let json = serde_json::to_string_pretty(&rest_response)?;
178+
return Ok(RESTResponse::with_json(200, &json));
179+
}
180+
};
181+
182+
let utxos_query_msg = Arc::new(Message::StateQuery(StateQuery::UTxOs(
183+
UTxOStateQuery::GetUTxOsSum { utxo_identifiers },
184+
)));
185+
186+
let address_balance = query_state(
187+
&context,
188+
&handlers_config.utxos_query_topic,
189+
utxos_query_msg,
190+
|message| match message {
191+
Message::StateQueryResponse(StateQueryResponse::UTxOs(
192+
UTxOStateQueryResponse::UTxOsSum(balance),
193+
)) => Ok(balance),
194+
Message::StateQueryResponse(StateQueryResponse::UTxOs(
195+
UTxOStateQueryResponse::Error(e),
196+
)) => Err(e),
197+
_ => Err(QueryError::internal_error(
198+
"Unexpected message type while retrieving UTxO sum",
199+
)),
200+
},
201+
)
202+
.await?;
203+
204+
let assets_query_msg = Arc::new(Message::StateQuery(StateQuery::Assets(
205+
AssetsStateQuery::GetAssetsMetadata {
206+
assets: address_balance.assets.clone(),
207+
},
208+
)));
209+
210+
let assets_metadata = query_state(
211+
&context,
212+
&handlers_config.assets_query_topic,
213+
assets_query_msg,
214+
|message| match message {
215+
Message::StateQueryResponse(StateQueryResponse::Assets(
216+
AssetsStateQueryResponse::AssetsMetadata(balance),
217+
)) => Ok(balance),
218+
Message::StateQueryResponse(StateQueryResponse::Assets(
219+
AssetsStateQueryResponse::Error(e),
220+
)) => Err(e),
221+
_ => Err(QueryError::internal_error(
222+
"Unexpected message type while retrieving assets metadata",
223+
)),
224+
},
225+
)
226+
.await?;
227+
228+
let amount = AmountListExtended::from_value_and_metadata(address_balance, &assets_metadata);
229+
230+
let rest_response = AddressInfoExtended {
231+
address: address.to_string()?,
232+
amount,
233+
stake_address,
234+
type_: address_type,
235+
script: is_script,
236+
};
237+
238+
let json = serde_json::to_string_pretty(&rest_response)?;
239+
Ok(RESTResponse::with_json(200, &json))
122240
}
123241

124242
/// Handle `/addresses/{address}/totals` Blockfrost-compatible endpoint
@@ -198,3 +316,94 @@ fn parse_address(params: &[String]) -> Result<Address, RESTError> {
198316

199317
Ok(Address::from_string(address_str)?)
200318
}
319+
320+
#[derive(Serialize)]
321+
pub struct AmountEntryExtended {
322+
pub unit: String,
323+
pub quantity: String,
324+
pub decimals: Option<u64>,
325+
pub has_nft_onchain_metadata: bool,
326+
}
327+
328+
#[derive(Serialize)]
329+
pub struct AmountListExtended(pub Vec<AmountEntryExtended>);
330+
331+
impl AmountListExtended {
332+
pub fn from_value_and_metadata(
333+
value: acropolis_common::Value,
334+
metadata: &[AssetMetadata],
335+
) -> Self {
336+
let mut out = Vec::new();
337+
338+
out.push(AmountEntryExtended {
339+
unit: "lovelace".to_string(),
340+
quantity: value.coin().to_string(),
341+
decimals: Some(6),
342+
has_nft_onchain_metadata: false,
343+
});
344+
345+
let mut idx = 0;
346+
347+
for (policy_id, assets) in &value.assets {
348+
for asset in assets {
349+
let meta = &metadata[idx];
350+
idx += 1;
351+
352+
let decimals = if let Some(raw) = meta.cip68_metadata.as_ref() {
353+
extract_cip68_decimals(raw)
354+
} else {
355+
None
356+
};
357+
358+
out.push(AmountEntryExtended {
359+
unit: format!(
360+
"{}{}",
361+
hex::encode(policy_id),
362+
hex::encode(asset.name.as_slice())
363+
),
364+
quantity: asset.amount.to_string(),
365+
decimals,
366+
has_nft_onchain_metadata: meta.cip25_metadata.is_some(),
367+
});
368+
}
369+
}
370+
371+
Self(out)
372+
}
373+
}
374+
375+
pub fn extract_cip68_decimals(raw: &[u8]) -> Option<u64> {
376+
let decoded: CborValue = serde_cbor::from_slice(raw).ok()?;
377+
378+
let arr = match decoded {
379+
CborValue::Array(a) => a,
380+
_ => return None,
381+
};
382+
383+
if arr.len() < 2 {
384+
return None;
385+
}
386+
387+
let metadata = &arr[0];
388+
389+
let map = match metadata {
390+
CborValue::Map(m) => m,
391+
_ => return None,
392+
};
393+
394+
for (key, value) in map {
395+
let key_str = match key {
396+
CborValue::Text(s) => s.as_str(),
397+
CborValue::Bytes(b) => std::str::from_utf8(b).ok()?,
398+
_ => continue,
399+
};
400+
401+
if key_str == "decimals" {
402+
if let CborValue::Integer(i) = value {
403+
return Some(*i as u64);
404+
}
405+
}
406+
}
407+
408+
None
409+
}

0 commit comments

Comments
 (0)