Skip to content

Commit af546cd

Browse files
committed
feat: account totals REST handler
Signed-off-by: William Hankins <[email protected]>
1 parent b555a59 commit af546cd

File tree

6 files changed

+154
-34
lines changed

6 files changed

+154
-34
lines changed

common/src/queries/addresses.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use crate::{
2-
Address, AddressTotals, NativeAssets, ShelleyAddress, TxIdentifier, UTxOIdentifier, ValueDelta,
3-
};
1+
use crate::{Address, AddressTotals, ShelleyAddress, TxIdentifier, UTxOIdentifier};
42

53
pub const DEFAULT_ADDRESS_QUERY_TOPIC: (&str, &str) =
64
("address-state-query-topic", "cardano.query.address");
@@ -12,7 +10,6 @@ pub enum AddressStateQuery {
1210
GetAddressTransactions { address: Address },
1311

1412
// Accounts related queries
15-
GetAddressesAssets { addresses: Vec<ShelleyAddress> },
1613
GetAddressesTotals { addresses: Vec<ShelleyAddress> },
1714
GetAddressesUTxOs { addresses: Vec<ShelleyAddress> },
1815
}
@@ -24,8 +21,7 @@ pub enum AddressStateQueryResponse {
2421
AddressTransactions(Vec<TxIdentifier>),
2522

2623
// Accounts related queries
27-
AddressesAssets(NativeAssets),
28-
AddressesTotals(ValueDelta),
24+
AddressesTotals(AddressTotals),
2925
AddressesUTxOs(Vec<UTxOIdentifier>),
3026
NotFound,
3127
Error(String),

common/src/types.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,19 @@ pub struct ValueMap {
388388
pub assets: NativeAssetsMap,
389389
}
390390

391+
impl AddAssign for ValueMap {
392+
fn add_assign(&mut self, other: Self) {
393+
self.lovelace += other.lovelace;
394+
395+
for (policy, assets) in other.assets {
396+
let entry = self.assets.entry(policy).or_default();
397+
for (asset_name, amount) in assets {
398+
*entry.entry(asset_name).or_default() += amount;
399+
}
400+
}
401+
}
402+
}
403+
391404
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
392405
pub struct ValueDelta {
393406
pub lovelace: i64,
@@ -2063,6 +2076,14 @@ pub struct AddressTotals {
20632076
pub tx_count: u64,
20642077
}
20652078

2079+
impl AddAssign for AddressTotals {
2080+
fn add_assign(&mut self, other: Self) {
2081+
self.sent += other.sent;
2082+
self.received += other.received;
2083+
self.tx_count += other.tx_count;
2084+
}
2085+
}
2086+
20662087
impl AddressTotals {
20672088
pub fn apply_delta(&mut self, delta: &ValueDelta) {
20682089
if delta.lovelace > 0 {

modules/address_state/src/address_state.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,12 +228,6 @@ impl AddressState {
228228
Err(e) => AddressStateQueryResponse::Error(e.to_string()),
229229
}
230230
}
231-
AddressStateQuery::GetAddressesAssets { addresses } => {
232-
match state.get_addresses_assets(addresses).await {
233-
Ok(assets) => AddressStateQueryResponse::AddressesAssets(assets),
234-
Err(e) => AddressStateQueryResponse::Error(e.to_string()),
235-
}
236-
}
237231
AddressStateQuery::GetAddressesTotals { addresses } => {
238232
match state.get_addresses_totals(addresses).await {
239233
Ok(totals) => AddressStateQueryResponse::AddressesTotals(totals),

modules/address_state/src/state.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use std::{
55
};
66

77
use acropolis_common::{
8-
Address, AddressDelta, AddressTotals, BlockInfo, NativeAssets, ShelleyAddress, TxIdentifier,
9-
UTxOIdentifier, ValueDelta,
8+
Address, AddressDelta, AddressTotals, BlockInfo, ShelleyAddress, TxIdentifier, UTxOIdentifier,
9+
ValueDelta,
1010
};
1111
use anyhow::Result;
1212

@@ -201,15 +201,15 @@ impl State {
201201
Ok(())
202202
}
203203

204-
pub async fn get_addresses_assets(
204+
pub async fn get_addresses_totals(
205205
&self,
206-
_addresses: &[ShelleyAddress],
207-
) -> Result<NativeAssets> {
208-
Ok(NativeAssets::default())
209-
}
210-
211-
pub async fn get_addresses_totals(&self, _addresses: &[ShelleyAddress]) -> Result<ValueDelta> {
212-
Ok(ValueDelta::default())
206+
addresses: &[ShelleyAddress],
207+
) -> Result<AddressTotals> {
208+
let mut totals = AddressTotals::default();
209+
for addr in addresses {
210+
totals += self.get_address_totals(&Address::Shelley(addr.clone())).await?;
211+
}
212+
Ok(totals)
213213
}
214214

215215
pub async fn get_addresses_utxos(

modules/rest_blockfrost/src/handlers/accounts.rs

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use caryatid_sdk::Context;
1717

1818
use crate::handlers_config::HandlersConfig;
1919
use crate::types::{
20-
AccountAddressREST, AccountRewardREST, AccountUTxOREST, AccountWithdrawalREST, AmountList,
21-
DelegationUpdateREST, RegistrationUpdateREST,
20+
AccountAddressREST, AccountRewardREST, AccountTotalsREST, AccountUTxOREST,
21+
AccountWithdrawalREST, AmountList, DelegationUpdateREST, RegistrationUpdateREST,
2222
};
2323

2424
#[derive(serde::Serialize)]
@@ -710,7 +710,7 @@ pub async fn handle_account_assets_blockfrost(
710710
)));
711711
let utxos_balance = query_state(
712712
&context,
713-
&handlers_config.addresses_query_topic,
713+
&handlers_config.utxos_query_topic,
714714
msg,
715715
|message| match message {
716716
Message::StateQueryResponse(StateQueryResponse::UTxOs(
@@ -719,10 +719,10 @@ pub async fn handle_account_assets_blockfrost(
719719
Message::StateQueryResponse(StateQueryResponse::UTxOs(
720720
UTxOStateQueryResponse::Error(e),
721721
)) => Err(anyhow::anyhow!(
722-
"Internal server error while retrieving account UTxOs: {e}"
722+
"Internal server error while retrieving UTxO sum: {e}"
723723
)),
724724
_ => Err(anyhow::anyhow!(
725-
"Unexpected message type while retrieving account UTxOs"
725+
"Unexpected message type while retrieving UTxO sum"
726726
)),
727727
},
728728
)
@@ -737,18 +737,93 @@ pub async fn handle_account_assets_blockfrost(
737737
Ok(json) => Ok(RESTResponse::with_json(200, &json)),
738738
Err(e) => Ok(RESTResponse::with_text(
739739
500,
740-
&format!("Internal server error while serializing addresses: {e}"),
740+
&format!("Internal server error while serializing assets: {e}"),
741741
)),
742742
}
743743
}
744744

745745
/// Handle `/accounts/{stake_address}/addresses/total` Blockfrost-compatible endpoint
746746
pub async fn handle_account_totals_blockfrost(
747-
_context: Arc<Context<Message>>,
748-
_params: Vec<String>,
749-
_handlers_config: Arc<HandlersConfig>,
747+
context: Arc<Context<Message>>,
748+
params: Vec<String>,
749+
handlers_config: Arc<HandlersConfig>,
750750
) -> Result<RESTResponse> {
751-
Ok(RESTResponse::with_text(501, "Not implemented"))
751+
let account = match parse_stake_address(&params) {
752+
Ok(addr) => addr,
753+
Err(resp) => return Ok(resp),
754+
};
755+
756+
// Get addresses from historical accounts state
757+
let msg = Arc::new(Message::StateQuery(StateQuery::Accounts(
758+
AccountsStateQuery::GetAccountAssociatedAddresses {
759+
account: account.clone(),
760+
},
761+
)));
762+
let addresses = query_state(
763+
&context,
764+
&handlers_config.historical_accounts_query_topic,
765+
msg,
766+
|message| match message {
767+
Message::StateQueryResponse(StateQueryResponse::Accounts(
768+
AccountsStateQueryResponse::AccountAssociatedAddresses(addresses),
769+
)) => Ok(Some(addresses)),
770+
Message::StateQueryResponse(StateQueryResponse::Accounts(
771+
AccountsStateQueryResponse::NotFound,
772+
)) => Ok(None),
773+
Message::StateQueryResponse(StateQueryResponse::Accounts(
774+
AccountsStateQueryResponse::Error(e),
775+
)) => Err(anyhow::anyhow!(
776+
"Internal server error while retrieving account addresses: {e}"
777+
)),
778+
_ => Err(anyhow::anyhow!(
779+
"Unexpected message type while retrieving account addresses"
780+
)),
781+
},
782+
)
783+
.await?;
784+
785+
let Some(addresses) = addresses else {
786+
return Ok(RESTResponse::with_text(404, "Account not found"));
787+
};
788+
789+
// Get totals from address state
790+
let msg = Arc::new(Message::StateQuery(StateQuery::Addresses(
791+
AddressStateQuery::GetAddressesTotals { addresses },
792+
)));
793+
let totals = query_state(
794+
&context,
795+
&handlers_config.addresses_query_topic,
796+
msg,
797+
|message| match message {
798+
Message::StateQueryResponse(StateQueryResponse::Addresses(
799+
AddressStateQueryResponse::AddressesTotals(utxos),
800+
)) => Ok(utxos),
801+
Message::StateQueryResponse(StateQueryResponse::Addresses(
802+
AddressStateQueryResponse::Error(e),
803+
)) => Err(anyhow::anyhow!(
804+
"Internal server error while retrieving address totals: {e}"
805+
)),
806+
_ => Err(anyhow::anyhow!(
807+
"Unexpected message type while retrieving address totals"
808+
)),
809+
},
810+
)
811+
.await?;
812+
813+
let rest_response = AccountTotalsREST {
814+
stake_address: account.to_string()?,
815+
received_sum: totals.received.into(),
816+
sent_sum: totals.sent.into(),
817+
tx_count: totals.tx_count,
818+
};
819+
820+
match serde_json::to_string_pretty(&rest_response) {
821+
Ok(json) => Ok(RESTResponse::with_json(200, &json)),
822+
Err(e) => Ok(RESTResponse::with_text(
823+
500,
824+
&format!("Internal server error while serializing totals: {e}"),
825+
)),
826+
}
752827
}
753828

754829
/// Handle `/accounts/{stake_address}/utxos` Blockfrost-compatible endpoint
@@ -912,7 +987,7 @@ pub async fn handle_account_utxos_blockfrost(
912987
Ok(json) => Ok(RESTResponse::with_json(200, &json)),
913988
Err(e) => Ok(RESTResponse::with_text(
914989
500,
915-
&format!("Internal server error while serializing addresses: {e}"),
990+
&format!("Internal server error while serializing utxos: {e}"),
916991
)),
917992
}
918993
}

modules/rest_blockfrost/src/types.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use acropolis_common::{
66
rest_helper::ToCheckedF64,
77
serialization::{Bech32WithHrp, DisplayFromBech32, PoolPrefix},
88
AssetAddressEntry, AssetMetadataStandard, AssetMintRecord, KeyHash, PolicyAsset,
9-
PoolEpochState, PoolId, PoolUpdateAction, Relay, TxHash, Vote, VrfKeyHash,
9+
PoolEpochState, PoolId, PoolUpdateAction, Relay, TxHash, ValueMap, Vote, VrfKeyHash,
1010
};
1111
use anyhow::Result;
1212
use num_traits::ToPrimitive;
@@ -854,6 +854,32 @@ impl From<acropolis_common::Value> for AmountList {
854854
}
855855
}
856856

857+
impl From<ValueMap> for AmountList {
858+
fn from(value: ValueMap) -> Self {
859+
let mut out = Vec::new();
860+
861+
out.push(AmountEntry {
862+
unit: "lovelace".to_string(),
863+
quantity: value.lovelace.to_string(),
864+
});
865+
866+
for (policy_id, assets) in value.assets {
867+
for (asset_name, amount) in assets {
868+
out.push(AmountEntry {
869+
unit: format!(
870+
"{}{}",
871+
hex::encode(policy_id),
872+
hex::encode(asset_name.as_slice())
873+
),
874+
quantity: amount.to_string(),
875+
});
876+
}
877+
}
878+
879+
Self(out)
880+
}
881+
}
882+
857883
#[derive(Serialize)]
858884
pub struct RegistrationUpdateREST {
859885
pub tx_hash: String,
@@ -911,3 +937,11 @@ pub struct AccountUTxOREST {
911937
pub inline_datum: Option<String>,
912938
pub reference_script_hash: Option<String>,
913939
}
940+
941+
#[derive(serde::Serialize)]
942+
pub struct AccountTotalsREST {
943+
pub stake_address: String,
944+
pub received_sum: AmountList,
945+
pub sent_sum: AmountList,
946+
pub tx_count: u64,
947+
}

0 commit comments

Comments
 (0)