partner_chains_dolos_data_sources/
bridge.rs

1use crate::{
2	Result,
3	client::{MiniBFClient, api::MiniBFApi, minibf::format_asset_id},
4};
5use blockfrost_openapi::models::{
6	tx_content::TxContent, tx_content_output_amount_inner::TxContentOutputAmountInner,
7	tx_content_utxo::TxContentUtxo,
8};
9use cardano_serialization_lib::PlutusData;
10use partner_chains_plutus_data::bridge::{TokenTransferDatum, TokenTransferDatumV1};
11use sidechain_domain::*;
12use sp_partner_chains_bridge::{
13	BridgeDataCheckpoint, BridgeTransferV1, MainChainScripts, TokenBridgeDataSource,
14};
15use std::fmt::Debug;
16use std::marker::PhantomData;
17
18pub struct TokenBridgeDataSourceImpl<RecipientAddress> {
19	client: MiniBFClient,
20	_phantom: PhantomData<RecipientAddress>,
21}
22
23impl<RecipientAddress> TokenBridgeDataSourceImpl<RecipientAddress> {
24	pub fn new(client: MiniBFClient) -> Self {
25		Self { client, _phantom: PhantomData::default() }
26	}
27}
28
29#[async_trait::async_trait]
30impl<RecipientAddress: Send + Sync> TokenBridgeDataSource<RecipientAddress>
31	for TokenBridgeDataSourceImpl<RecipientAddress>
32where
33	RecipientAddress: Debug,
34	RecipientAddress: (for<'a> TryFrom<&'a [u8]>),
35{
36	async fn get_transfers(
37		&self,
38		main_chain_scripts: MainChainScripts,
39		data_checkpoint: BridgeDataCheckpoint,
40		max_transfers: u32,
41		current_mc_block_hash: McBlockHash,
42	) -> Result<(Vec<BridgeTransferV1<RecipientAddress>>, BridgeDataCheckpoint)> {
43		let current_mc_block = self.client.blocks_by_id(current_mc_block_hash).await?;
44
45		let data_checkpoint = match data_checkpoint {
46			BridgeDataCheckpoint::Utxo(utxo) => {
47				let TxBlockInfo { block_number, tx_ix } =
48					get_block_info_for_utxo(&self.client, utxo.tx_hash.into()).await?.ok_or(
49						format!(
50							"Could not find block info for data checkpoint: {data_checkpoint:?}"
51						),
52					)?;
53				ResolvedBridgeDataCheckpoint::Utxo {
54					block_number,
55					tx_ix,
56					tx_out_ix: utxo.index.into(),
57				}
58			},
59			BridgeDataCheckpoint::Block(number) => {
60				ResolvedBridgeDataCheckpoint::Block { number: number.into() }
61			},
62		};
63
64		let asset = AssetId {
65			policy_id: main_chain_scripts.token_policy_id.into(),
66			asset_name: main_chain_scripts.token_asset_name.into(),
67		};
68		let current_mc_block_height: McBlockNumber = McBlockNumber(
69			current_mc_block.height.expect("current mc block has valid height") as u32,
70		);
71		let utxos = get_bridge_utxos_tx(
72			&self.client,
73			&main_chain_scripts.illiquid_circulation_supply_validator_address.into(),
74			asset,
75			data_checkpoint,
76			current_mc_block_height,
77			Some(max_transfers),
78		)
79		.await?;
80
81		let new_checkpoint = match utxos.last() {
82			None => BridgeDataCheckpoint::Block(current_mc_block_height),
83			Some(_) if (utxos.len() as u32) < max_transfers => {
84				BridgeDataCheckpoint::Block(current_mc_block_height)
85			},
86			Some(utxo) => BridgeDataCheckpoint::Utxo(utxo.utxo_id()),
87		};
88
89		let transfers = utxos.into_iter().flat_map(utxo_to_transfer).collect();
90
91		Ok((transfers, new_checkpoint))
92	}
93}
94
95fn utxo_to_transfer<RecipientAddress>(
96	utxo: BridgeUtxo,
97) -> Option<BridgeTransferV1<RecipientAddress>>
98where
99	RecipientAddress: for<'a> TryFrom<&'a [u8]>,
100{
101	let token_delta = utxo.tokens_out.0.checked_sub(utxo.tokens_in.0)?;
102
103	if token_delta == 0 {
104		return None;
105	}
106
107	let token_amount = token_delta as u64;
108
109	let Some(datum) = utxo.datum.clone() else {
110		return Some(BridgeTransferV1::InvalidTransfer { token_amount, utxo_id: utxo.utxo_id() });
111	};
112
113	let transfer = match TokenTransferDatum::try_from(datum) {
114		Ok(TokenTransferDatum::V1(TokenTransferDatumV1::UserTransfer { receiver })) => {
115			match RecipientAddress::try_from(receiver.0.as_ref()) {
116				Ok(recipient) => BridgeTransferV1::UserTransfer { token_amount, recipient },
117				Err(_) => {
118					BridgeTransferV1::InvalidTransfer { token_amount, utxo_id: utxo.utxo_id() }
119				},
120			}
121		},
122		Ok(TokenTransferDatum::V1(TokenTransferDatumV1::ReserveTransfer)) => {
123			BridgeTransferV1::ReserveTransfer { token_amount }
124		},
125		Err(_) => BridgeTransferV1::InvalidTransfer { token_amount, utxo_id: utxo.utxo_id() },
126	};
127
128	Some(transfer)
129}
130
131pub(crate) struct BridgeUtxo {
132	pub(crate) block_number: McBlockNumber,
133	pub(crate) tx_ix: McTxIndexInBlock,
134	pub(crate) tx_hash: McTxHash,
135	pub(crate) utxo_ix: UtxoIndex,
136	pub(crate) tokens_out: NativeTokenAmount,
137	pub(crate) tokens_in: NativeTokenAmount,
138	pub(crate) datum: Option<cardano_serialization_lib::PlutusData>,
139}
140
141impl BridgeUtxo {
142	pub(crate) fn utxo_id(&self) -> UtxoId {
143		UtxoId { tx_hash: self.tx_hash.into(), index: self.utxo_ix.into() }
144	}
145
146	pub(crate) fn ordering_key(&self) -> UtxoOrderingKey {
147		(self.block_number, self.tx_ix, self.utxo_ix)
148	}
149}
150
151pub(crate) type UtxoOrderingKey = (McBlockNumber, McTxIndexInBlock, UtxoIndex);
152
153#[derive(Debug, Clone, PartialEq)]
154pub(crate) struct TxBlockInfo {
155	pub(crate) block_number: McBlockNumber,
156	pub(crate) tx_ix: McTxIndexInBlock,
157}
158
159pub(crate) async fn get_block_info_for_utxo(
160	client: &MiniBFClient,
161	tx_hash: McTxHash,
162) -> Result<Option<TxBlockInfo>> {
163	let tx = client.transaction_by_hash(tx_hash).await?;
164	Ok(Some(TxBlockInfo {
165		block_number: McBlockNumber(tx.block_height as u32),
166		tx_ix: McTxIndexInBlock(tx.index as u32),
167	}))
168}
169
170#[derive(Clone)]
171pub(crate) enum ResolvedBridgeDataCheckpoint {
172	Utxo { block_number: McBlockNumber, tx_ix: McTxIndexInBlock, tx_out_ix: UtxoIndex },
173	Block { number: McBlockNumber },
174}
175
176impl ResolvedBridgeDataCheckpoint {
177	fn block_number(&self) -> McBlockNumber {
178		match self {
179			ResolvedBridgeDataCheckpoint::Utxo { block_number, .. } => *block_number,
180			ResolvedBridgeDataCheckpoint::Block { number } => *number,
181		}
182	}
183}
184
185pub(crate) async fn get_bridge_utxos_tx(
186	client: &MiniBFClient,
187	icp_address: &MainchainAddress,
188	native_token: AssetId,
189	checkpoint: ResolvedBridgeDataCheckpoint,
190	to_block: McBlockNumber,
191	max_utxos: Option<u32>,
192) -> Result<Vec<BridgeUtxo>> {
193	let txs = client.assets_transactions(native_token.clone()).await?;
194	let checkpoint_block_no = checkpoint.block_number().0;
195	let futures = txs.into_iter().map(|a| async move {
196		let block_no = a.block_height as u32;
197		if checkpoint_block_no < block_no && block_no <= to_block.0 {
198			let tx_hash = McTxHash::from_hex_unsafe(&a.tx_hash);
199			let utxos = client.transactions_utxos(tx_hash).await?;
200			let tx = client.transaction_by_hash(tx_hash).await?;
201			Result::Ok(Some((utxos, tx)))
202		} else {
203			Result::Ok(None)
204		}
205	});
206	let mut bridge_utxos = futures::future::try_join_all(futures)
207		.await?
208		.iter()
209		.flatten()
210		.flat_map(|(utxos, tx): &(TxContentUtxo, TxContent)| {
211			let inputs = utxos.inputs.iter().filter(|i| i.address == icp_address.to_string());
212			let outputs = utxos.outputs.iter().filter(|o| o.address == icp_address.to_string());
213			let native_token = native_token.clone();
214			let checkpoint_clone = checkpoint.clone();
215			outputs.filter_map(move |output| {
216				let native_token = native_token.clone();
217				let output_tokens = get_all_tokens(&output.amount, &native_token.clone());
218				let input_tokens = inputs
219					.clone()
220					.map(move |input| get_all_tokens(&input.amount, &native_token.clone()))
221					.sum();
222
223				match checkpoint_clone {
224					ResolvedBridgeDataCheckpoint::Utxo { tx_ix, tx_out_ix, .. }
225						if tx.block_height <= tx_ix.0 as i32
226							&& output.output_index <= tx_out_ix.0.into() =>
227					{
228						None
229					},
230					_ => Some(BridgeUtxo {
231						block_number: McBlockNumber(tx.block_height as u32),
232						tokens_out: NativeTokenAmount(output_tokens),
233						tokens_in: NativeTokenAmount(input_tokens),
234						datum: output
235							.inline_datum
236							.clone()
237							.map(|d| PlutusData::from_hex(&d).expect("valid datum")),
238						tx_ix: McTxIndexInBlock(tx.index as u32),
239						tx_hash: McTxHash::from_hex_unsafe(&tx.hash),
240						utxo_ix: UtxoIndex(output.output_index as u16),
241					}),
242				}
243			})
244		})
245		.collect::<Vec<_>>();
246	bridge_utxos.sort_by_key(|b| b.ordering_key());
247
248	if let Some(max_utxos) = max_utxos {
249		bridge_utxos.truncate(max_utxos as usize);
250	}
251
252	Ok(bridge_utxos)
253}
254
255fn get_all_tokens(amount: &Vec<TxContentOutputAmountInner>, asset_id: &AssetId) -> u128 {
256	amount
257		.iter()
258		.map(|v| {
259			if v.unit == format_asset_id(asset_id) {
260				v.quantity.parse::<u128>().expect("valid quantity is u128")
261			} else {
262				0u128
263			}
264		})
265		.sum()
266}