partner_chains_dolos_data_sources/
governed_map.rs

1use crate::{
2	Result,
3	client::{MiniBFClient, api::MiniBFApi},
4};
5use async_trait::async_trait;
6use cardano_serialization_lib::PlutusData;
7use partner_chains_plutus_data::governed_map::GovernedMapDatum;
8use sidechain_domain::byte_string::ByteString;
9use sidechain_domain::*;
10use sp_governed_map::{GovernedMapDataSource, MainChainScriptsV1};
11use std::collections::BTreeMap;
12
13pub struct GovernedMapDataSourceImpl {
14	client: MiniBFClient,
15}
16
17impl GovernedMapDataSourceImpl {
18	pub fn new(client: MiniBFClient) -> Self {
19		Self { client }
20	}
21}
22
23#[async_trait]
24impl GovernedMapDataSource for GovernedMapDataSourceImpl {
25	async fn get_state_at_block(
26		&self,
27		mc_block: McBlockHash,
28		main_chain_scripts: MainChainScriptsV1,
29	) -> Result<BTreeMap<String, ByteString>> {
30		// Get the block to ensure it exists and get its number
31		let block = self.client.blocks_by_id(mc_block.clone()).await?;
32		let block_number =
33			McBlockNumber(block.height.unwrap_or_default().try_into().unwrap_or(0u32));
34
35		// Get all UTXOs at the governed map validator address
36		let utxos = self
37			.client
38			.addresses_utxos(main_chain_scripts.validator_address.clone())
39			.await
40			.unwrap_or_default();
41
42		// Filter UTXOs that:
43		// 1. Contain the governed map asset
44		// 2. Were created before or at the target block
45		let asset_unit = format_asset_unit(&main_chain_scripts.asset_policy_id);
46		let mut mappings = BTreeMap::new();
47
48		for utxo in utxos {
49			// Check if this UTXO was created before or at target block
50			let tx_hash = match McTxHash::decode_hex(&utxo.tx_hash) {
51				Ok(hash) => hash,
52				Err(e) => {
53					log::warn!("Failed to decode tx_hash '{}': {}", utxo.tx_hash, e);
54					continue;
55				},
56			};
57			let tx = self.client.transaction_by_hash(tx_hash).await?;
58			let utxo_block_height = tx.block_height as u32;
59
60			if utxo_block_height > block_number.0 {
61				continue;
62			}
63
64			// Check if UTXO contains the governed map asset
65			let has_asset = utxo.amount.iter().any(|a| a.unit == asset_unit);
66			if !has_asset {
67				continue;
68			}
69
70			// Parse the datum
71			if let Some(datum_hex) = &utxo.inline_datum {
72				if let Some((key, value)) = parse_governed_map_datum(datum_hex) {
73					mappings.insert(key, value);
74				}
75			}
76		}
77
78		Ok(mappings)
79	}
80
81	async fn get_mapping_changes(
82		&self,
83		since_mc_block: Option<McBlockHash>,
84		up_to_mc_block: McBlockHash,
85		scripts: MainChainScriptsV1,
86	) -> Result<Vec<(String, Option<ByteString>)>> {
87		// Get current state at up_to_mc_block
88		let current_mappings = self.get_state_at_block(up_to_mc_block, scripts.clone()).await?;
89
90		// If no since_mc_block, return all current mappings as additions
91		let Some(since_mc_block) = since_mc_block else {
92			let changes =
93				current_mappings.into_iter().map(|(key, value)| (key, Some(value))).collect();
94			return Ok(changes);
95		};
96
97		// Get previous state at since_mc_block
98		let previous_mappings = self.get_state_at_block(since_mc_block, scripts).await?;
99
100		// Calculate changes
101		let mut changes = Vec::new();
102
103		// Find additions and modifications
104		for (key, value) in current_mappings.iter() {
105			if previous_mappings.get(key) != Some(value) {
106				changes.push((key.clone(), Some(value.clone())));
107			}
108		}
109
110		// Find deletions
111		for key in previous_mappings.keys() {
112			if !current_mappings.contains_key(key) {
113				changes.push((key.clone(), None));
114			}
115		}
116
117		Ok(changes)
118	}
119}
120
121fn format_asset_unit(policy_id: &PolicyId) -> String {
122	// Asset unit format in blockfrost is policy_id + asset_name (hex)
123	// For empty asset names, it's just the policy_id without "0x" prefix
124	policy_id.to_hex_string()[2..].to_string()
125}
126
127/// Helper function to parse GovernedMapDatum from hex-encoded PlutusData
128fn parse_governed_map_datum(datum_hex: &str) -> Option<(String, ByteString)> {
129	match PlutusData::from_hex(datum_hex) {
130		Ok(plutus_data) => match GovernedMapDatum::try_from(plutus_data) {
131			Ok(GovernedMapDatum { key, value }) => Some((key, value)),
132			Err(err) => {
133				log::warn!("Failed to parse GovernedMapDatum: {}", err);
134				None
135			},
136		},
137		Err(err) => {
138			log::warn!("Failed to parse PlutusData from hex: {}", err);
139			None
140		},
141	}
142}