11use std:: sync:: Arc ;
22
3- use crate :: types:: AddressTotalsREST ;
3+ use crate :: types:: { AddressInfoExtended , AddressTotalsREST } ;
44use crate :: { handlers_config:: HandlersConfig , types:: AddressInfoREST } ;
5+ use acropolis_common:: queries:: assets:: { AssetsStateQuery , AssetsStateQueryResponse } ;
56use acropolis_common:: queries:: errors:: QueryError ;
67use acropolis_common:: rest_error:: RESTError ;
8+ use acropolis_common:: AssetMetadata ;
79use acropolis_common:: {
810 messages:: { Message , RESTResponse , StateQuery , StateQueryResponse } ,
911 queries:: {
@@ -14,6 +16,8 @@ use acropolis_common::{
1416 Address , Value ,
1517} ;
1618use caryatid_sdk:: Context ;
19+ use serde:: Serialize ;
20+ use serde_cbor:: Value as CborValue ;
1721
1822/// Handle `/addresses/{address}` Blockfrost-compatible endpoint
1923pub 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
116120pub 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