1#![allow(rustdoc::private_intra_doc_links)]
2use super::{ReserveData, add_reserve_utxo_input_with_validator_script_reference};
25use crate::{
26 await_tx::AwaitTx,
27 bridge::{ICSData, add_ics_utxo_input_with_validator_script_reference, select_utxo_to_spend},
28 cardano_keys::CardanoPaymentSigningKey,
29 csl::*,
30 plutus_script::PlutusScript,
31 reserve::ReserveUtxo,
32};
33use anyhow::anyhow;
34use cardano_serialization_lib::{
35 Assets, Int, MultiAsset, PlutusData, Transaction, TransactionBuilder, TransactionOutputBuilder,
36 TxInputsBuilder,
37};
38use ogmios_client::{
39 query_ledger_state::*, query_network::QueryNetwork, transactions::Transactions,
40 types::OgmiosUtxo,
41};
42use partner_chains_plutus_data::{bridge::TokenTransferDatumV1, reserve::ReserveRedeemer};
43use sidechain_domain::{McTxHash, UtxoId};
44use std::num::NonZero;
45
46pub async fn release_reserve_funds<
48 T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
49 A: AwaitTx,
50>(
51 amount: NonZero<u64>,
52 genesis_utxo: UtxoId,
53 reference_utxo: UtxoId,
54 payment_key: &CardanoPaymentSigningKey,
55 client: &T,
56 await_tx: &A,
57) -> anyhow::Result<McTxHash> {
58 let ctx = TransactionContext::for_payment_key(payment_key, client).await?;
59 let tip = client.get_tip().await?;
60 let reserve_data = ReserveData::get(genesis_utxo, &ctx, client).await?;
61 let Some(reference_utxo) = client.query_utxo_by_id(reference_utxo).await? else {
62 return Err(anyhow!("Reference utxo {reference_utxo:?} not found on chain"));
63 };
64 let ics_data = ICSData::get(genesis_utxo, &ctx, client).await?;
65
66 let reserve_utxo = reserve_data.get_reserve_utxo(&ctx, client).await?;
67 let ics_utxos = ics_data.get_validator_utxos_with_auth_token(&ctx, client).await?;
68 let ics_utxo = select_utxo_to_spend(&ics_utxos, &ctx).ok_or(anyhow::anyhow!(
69 "Cannot find UTXOs with an 'auth token' at ICS Validator! Is the Bridge initialized?"
70 ))?;
71
72 let tx = Costs::calculate_costs(
73 |costs| {
74 reserve_release_tx(
75 &ctx,
76 &reserve_data,
77 &ics_data,
78 &reserve_utxo,
79 &ics_utxo,
80 &reference_utxo,
81 amount.get(),
82 tip.slot,
83 costs,
84 )
85 },
86 client,
87 )
88 .await?;
89
90 let signed_tx = ctx.sign(&tx).to_bytes();
91
92 let res = client.submit_transaction(&signed_tx).await.map_err(|e| {
93 anyhow::anyhow!(
94 "Reserve release transaction request failed: {}, tx bytes: {}",
95 e,
96 hex::encode(signed_tx)
97 )
98 })?;
99 let tx_id = res.transaction.id;
100 log::info!("Reserve release transaction submitted: {}", hex::encode(tx_id));
101 await_tx.await_tx_output(client, McTxHash(tx_id)).await?;
102
103 Ok(McTxHash(tx_id))
104}
105
106fn reserve_release_tx(
107 ctx: &TransactionContext,
108 reserve_data: &ReserveData,
109 ics_data: &ICSData,
110 previous_reserve: &ReserveUtxo,
111 ics_utxo: &OgmiosUtxo,
112 reference_utxo: &OgmiosUtxo,
113 amount_to_transfer: u64,
114 latest_slot: u64,
115 costs: Costs,
116) -> anyhow::Result<Transaction> {
117 let token = &previous_reserve.datum.immutable_settings.token;
118 let stats = &previous_reserve.datum.stats;
119
120 let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
121
122 let reserve_balance = previous_reserve.utxo.get_asset_amount(token);
123 let ics_balance = ics_utxo.get_asset_amount(token);
124 let token_total_amount_transferred = stats.token_total_amount_transferred;
125 let cumulative_total_transfer: u64 = token_total_amount_transferred
126 .checked_add(amount_to_transfer)
127 .expect("cumulative_total_transfer can't overflow u64");
128
129 let left_in_reserve = reserve_balance.checked_sub(amount_to_transfer)
130 .ok_or_else(||anyhow!("Not enough funds in the reserve to transfer {amount_to_transfer} tokens (reserve balance: {reserve_balance})"))?;
131
132 tx_builder.add_script_reference_input(
134 &reserve_data.auth_policy_version_utxo.to_csl_tx_input(),
135 reserve_data.scripts.auth_policy.bytes.len(),
136 );
137 tx_builder.add_script_reference_input(
138 &ics_data.validator_version_utxo.to_csl_tx_input(),
139 ics_data.scripts.validator.bytes.len(),
140 );
141 tx_builder.add_script_reference_input(
142 &ics_data.auth_policy_version_utxo.to_csl_tx_input(),
143 reserve_data.scripts.auth_policy.bytes.len(),
144 );
145
146 let v_function = v_function_from_utxo(reference_utxo)?;
149 tx_builder.add_mint_script_token_using_reference_script(
150 &Script::Plutus(v_function),
151 &reference_utxo.to_csl_tx_input(),
152 &Int::new(&cumulative_total_transfer.into()),
153 &costs,
154 )?;
155
156 let mut tx_inputs = TxInputsBuilder::new();
157
158 let spend_indices = costs.get_spend_indices();
159
160 let mut spend_costs = vec![
161 costs.get_spend(*spend_indices.get(1).unwrap_or(&0)),
162 costs.get_spend(*spend_indices.get(0).unwrap_or(&0)),
163 ];
164
165 spend_costs.sort();
166
167 add_reserve_utxo_input_with_validator_script_reference(
169 &mut tx_inputs,
170 &previous_reserve.utxo,
171 reserve_data,
172 ReserveRedeemer::ReleaseFromReserve,
173 spend_costs.get(1).unwrap(),
174 )?;
175
176 add_ics_utxo_input_with_validator_script_reference(
177 &mut tx_inputs,
178 ics_utxo,
179 ics_data,
180 spend_costs.get(0).unwrap(),
181 )?;
182
183 tx_builder.set_inputs(&tx_inputs);
184
185 let mut ics_tokens = ics_utxo
186 .value
187 .to_csl()?
188 .multiasset()
189 .expect("ICS UTXO should have native tokens");
190
191 let mut assets = Assets::new();
192
193 assets.insert(
194 &token.asset_name.to_csl()?,
195 &amount_to_transfer.saturating_add(ics_balance).into(),
196 );
197
198 ics_tokens.insert(&token.policy_id.0.into(), &assets);
199
200 tx_builder.add_output(&{
202 TransactionOutputBuilder::new()
203 .with_address(&ics_data.scripts.validator.address(ctx.network))
204 .with_plutus_data(&TokenTransferDatumV1::ReserveTransfer.into())
205 .next()?
206 .with_minimum_ada_and_asset(&ics_tokens, ctx)?
207 .build()?
208 })?;
209
210 tx_builder.add_output(&{
212 TransactionOutputBuilder::new()
213 .with_address(&reserve_data.scripts.validator.address(ctx.network))
214 .with_plutus_data(&PlutusData::from(
215 previous_reserve.datum.clone().after_withdrawal(amount_to_transfer),
216 ))
217 .next()?
218 .with_minimum_ada_and_asset(
219 &MultiAsset::from_ogmios_utxo(&previous_reserve.utxo)?
220 .with_asset_amount(token, left_in_reserve)?,
221 ctx,
222 )?
223 .build()?
224 })?;
225
226 tx_builder.set_validity_start_interval_bignum(latest_slot.into());
227
228 Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
229}
230
231fn v_function_from_utxo(utxo: &OgmiosUtxo) -> anyhow::Result<PlutusScript> {
232 let Some(v_function_script) = utxo.script.clone() else {
233 return Err(anyhow!("V-Function reference script missing from the reference UTXO",));
234 };
235 PlutusScript::try_from(v_function_script)
236 .map_err(|val| anyhow!("{val:?} is not a valid Plutus Script"))
237}
238
239#[cfg(test)]
240mod tests {
241 use super::{AssetNameExt, Costs, TransactionContext, empty_asset_name, reserve_release_tx};
242 use crate::plutus_script;
243 use crate::{
244 bridge::ICSData,
245 cardano_keys::CardanoPaymentSigningKey,
246 plutus_script::PlutusScript,
247 reserve::{ReserveData, ReserveUtxo, release::OgmiosUtxoExt},
248 scripts_data::{ICSScripts, ReserveScripts},
249 test_values::{payment_addr, protocol_parameters},
250 };
251 use cardano_serialization_lib::{Int, NetworkIdKind, PolicyID, Transaction};
252 use hex_literal::hex;
253 use ogmios_client::types::{Asset, OgmiosTx, OgmiosUtxo, OgmiosValue};
254 use partner_chains_plutus_data::reserve::{
255 ReserveDatum, ReserveImmutableSettings, ReserveMutableSettings, ReserveStats,
256 };
257 use pretty_assertions::assert_eq;
258 use raw_scripts::{
259 EXAMPLE_V_FUNCTION_POLICY, ILLIQUID_CIRCULATION_SUPPLY_AUTHORITY_TOKEN_POLICY,
260 ILLIQUID_CIRCULATION_SUPPLY_VALIDATOR, RESERVE_AUTH_POLICY, RESERVE_VALIDATOR,
261 };
262 use sidechain_domain::{AssetName, PolicyId};
263
264 fn payment_key() -> CardanoPaymentSigningKey {
265 CardanoPaymentSigningKey::from_normal_bytes(hex!(
266 "94f7531c9639654b77fa7e10650702b6937e05cd868f419f54bcb8368e413f04"
267 ))
268 .unwrap()
269 }
270
271 fn test_address_bech32() -> String {
272 "addr_test1vpmd59ajuvm34d723r8q2qzyz9ylq0x9pygqn7vun8qgpkgs7y5hw".into()
273 }
274
275 fn payment_utxo() -> OgmiosUtxo {
276 OgmiosUtxo {
277 transaction: OgmiosTx {
278 id: hex!("f5e751f474e909419c714bb5666a8f810e7ed61fadad236c29f67dafc1ff398b"),
279 },
280 index: 1,
281 value: OgmiosValue {
282 lovelace: 994916563,
283 native_tokens: [(
284 hex!("08b95138e16a062fa8d623a2b1beebd59c06210f3d33690580733e73"),
286 vec![Asset { name: vec![], amount: 1 }],
287 )]
288 .into(),
289 },
290 address: test_address_bech32(),
291
292 ..OgmiosUtxo::default()
293 }
294 }
295
296 fn tx_context() -> TransactionContext {
297 TransactionContext {
298 payment_key: payment_key(),
299 payment_key_utxos: vec![payment_utxo()],
300 network: NetworkIdKind::Testnet,
301 protocol_parameters: protocol_parameters(),
302 change_address: payment_addr(),
303 }
304 }
305
306 fn reserve_validator_script() -> PlutusScript {
307 RESERVE_VALIDATOR.into()
308 }
309
310 fn auth_policy_script() -> PlutusScript {
311 RESERVE_AUTH_POLICY.into()
312 }
313
314 fn illiquid_supply_validator_script() -> PlutusScript {
315 ILLIQUID_CIRCULATION_SUPPLY_VALIDATOR.into()
316 }
317
318 fn illiquid_supply_auth_token_policy_script() -> PlutusScript {
319 ILLIQUID_CIRCULATION_SUPPLY_AUTHORITY_TOKEN_POLICY.into()
320 }
321
322 const UNIX_T0: u64 = 1736504093000u64;
323
324 fn applied_v_function() -> PlutusScript {
325 plutus_script![EXAMPLE_V_FUNCTION_POLICY, UNIX_T0].unwrap()
326 }
327
328 fn version_oracle_address() -> String {
329 "addr_test1wqskkgpmsyf0yr2renk0spgsvea75rkq4yvalrpwwudwr5ga3relp".to_string()
330 }
331
332 fn ics_validator_address() -> String {
333 "addr_test1wz7s8uzkldpv7rvyu6v8wg4vk3ulxy23kzqyngk2yc76xucestz8f".to_string()
334 }
335
336 fn reserve_data() -> ReserveData {
337 ReserveData {
338 scripts: ReserveScripts {
339 validator: reserve_validator_script(),
340 auth_policy: auth_policy_script(),
341 },
342 auth_policy_version_utxo: OgmiosUtxo {
343 transaction: OgmiosTx {
344 id: hex!("d1030b0ce5cf33d97a6e8aafa1cfe150e7a8b3a5584bd7a345743938e78ec44b"),
345 },
346 index: 0,
347 address: version_oracle_address(),
348 script: Some(auth_policy_script().into()),
349 ..Default::default()
350 },
351 validator_version_utxo: OgmiosUtxo {
352 transaction: OgmiosTx {
353 id: hex!("fcb5d7877e6ce7cfaef579c4f4b5fdbbdb807e4fe613752671742f1a5191c850"),
354 },
355 index: 0,
356 address: version_oracle_address(),
357 script: Some(reserve_validator_script().into()),
358 ..Default::default()
359 },
360 }
361 }
362
363 fn ics_data() -> ICSData {
364 ICSData {
365 scripts: ICSScripts {
366 validator: illiquid_supply_validator_script(),
367 auth_policy: illiquid_supply_auth_token_policy_script(),
368 },
369 auth_policy_version_utxo: OgmiosUtxo {
370 transaction: OgmiosTx {
371 id: hex!("f5890475177fcc7cf40679974751f66331c7b25fcf2f1a148c53cf7e0e147114"),
372 },
373 index: 1,
374 address: version_oracle_address(),
375 script: Some(illiquid_supply_auth_token_policy_script().into()),
376 ..Default::default()
377 },
378 validator_version_utxo: OgmiosUtxo {
379 transaction: OgmiosTx {
380 id: hex!("f5890475177fcc7cf40679974751f66331c7b25fcf2f1a148c53cf7e0e147114"),
381 },
382 index: 0,
383 address: version_oracle_address(),
384 script: Some(illiquid_supply_validator_script().into()),
385 ..Default::default()
386 },
387 }
388 }
389
390 fn reference_utxo() -> OgmiosUtxo {
391 OgmiosUtxo {
392 transaction: OgmiosTx {
393 id: hex!("45882cfd2de9381f34ae68ad073452e2a57a7ad11095dae49f365266637e9d04"),
394 },
395 index: 0,
396 script: Some(applied_v_function().into()),
397 ..Default::default()
398 }
399 }
400
401 fn previous_reserve_ogmios_utxo() -> OgmiosUtxo {
402 OgmiosUtxo {
403 transaction: OgmiosTx {
404 id: hex!("23de508bbfeb6af651da305a2de022463f71e47d58365eba36d98fa6c4aed731"),
405 },
406 value: OgmiosValue {
407 lovelace: 1672280,
408 native_tokens: [
409 (
410 token_policy().0,
412 vec![Asset { name: token_name().0.to_vec(), amount: 990 }],
413 ),
414 (
415 hex!("75b8875ff8958c66fecbd93740ac5ffd7370d299e729a46bb5632066"),
417 vec![Asset { name: vec![], amount: 1 }],
418 ),
419 ]
420 .into(),
421 },
422
423 index: 1,
424 ..Default::default()
425 }
426 }
427
428 fn previous_reserve_utxo() -> ReserveUtxo {
429 ReserveUtxo { utxo: previous_reserve_ogmios_utxo(), datum: previous_reserve_datum() }
430 }
431
432 fn ics_utxo() -> OgmiosUtxo {
433 OgmiosUtxo {
434 transaction: OgmiosTx {
435 id: hex!("23de508bbfeb6af651da305a2de022463f71e47d58365eba36d98fa6c4aed731"),
436 },
437 value: OgmiosValue {
438 lovelace: 1672280,
439 native_tokens: [(
440 ics_auth_token_policy().0,
442 vec![Asset { name: vec![], amount: 1 }],
443 )]
444 .into(),
445 },
446
447 index: 2,
448 address: ics_validator_address(),
449 ..Default::default()
450 }
451 }
452
453 fn token_policy() -> PolicyId {
454 PolicyId(hex!("1fab25f376bc49a181d03a869ee8eaa3157a3a3d242a619ca7995b2b"))
455 }
456
457 fn ics_auth_token_policy() -> PolicyId {
458 PolicyId(hex!("d4a1d93484fa63847f8c4c271dc0c46a55b6e5916f46e14ee849a381"))
459 }
460
461 fn token_name() -> AssetName {
462 AssetName::from_hex_unsafe("52657761726420746f6b656e")
463 }
464
465 fn token_id() -> sidechain_domain::AssetId {
466 sidechain_domain::AssetId { policy_id: token_policy(), asset_name: token_name() }
467 }
468
469 fn previous_reserve_datum() -> ReserveDatum {
470 ReserveDatum {
471 immutable_settings: ReserveImmutableSettings { token: token_id() },
472 mutable_settings: ReserveMutableSettings {
473 total_accrued_function_asset_name: applied_v_function().policy_id(),
474 initial_incentive: 0,
475 },
476 stats: ReserveStats { token_total_amount_transferred: 10 },
477 }
478 }
479
480 fn reserve_release_test_tx() -> Transaction {
481 reserve_release_tx(
482 &tx_context(),
483 &reserve_data(),
484 &ics_data(),
485 &previous_reserve_utxo(),
486 &ics_utxo(),
487 &reference_utxo(),
488 5,
489 0,
490 Costs::ZeroCosts,
491 )
492 .unwrap()
493 }
494
495 #[test]
496 fn should_have_correct_reference_utxos() {
497 let ref_inputs: Vec<_> = reserve_release_test_tx()
498 .body()
499 .reference_inputs()
500 .expect("Should have reference inputs")
501 .into_iter()
502 .cloned()
503 .collect();
504
505 assert!(ref_inputs.contains(&reference_utxo().to_csl_tx_input()));
506 assert!(ref_inputs.contains(&reserve_data().auth_policy_version_utxo.to_csl_tx_input()));
507 assert!(ref_inputs.contains(&reserve_data().validator_version_utxo.to_csl_tx_input()));
508 assert!(ref_inputs.contains(&ics_data().auth_policy_version_utxo.to_csl_tx_input()));
509 assert!(ref_inputs.contains(&ics_data().validator_version_utxo.to_csl_tx_input()));
510 assert_eq!(ref_inputs.len(), 5)
511 }
512
513 #[test]
514 fn should_mint_v_function_scripts() {
515 let v_function_token_mint_amount = reserve_release_test_tx()
516 .body()
517 .mint()
518 .expect("Should mint a token")
519 .get(&applied_v_function().csl_script_hash())
520 .and_then(|policy| policy.get(0))
521 .expect("Should mint a v-function policy token")
522 .get(&empty_asset_name())
523 .expect("The minted token should have an empty asset name");
524
525 assert_eq!(v_function_token_mint_amount, Int::new_i32(15))
526 }
527
528 #[test]
529 fn should_burn_previoius_reserve_utxo() {
530 let inputs: Vec<_> =
531 reserve_release_test_tx().body().inputs().into_iter().cloned().collect();
532
533 assert!(inputs.contains(&previous_reserve_utxo().utxo.to_csl_tx_input()))
534 }
535
536 #[test]
537 fn should_add_correct_number_of_tokens_to_illiquid_supply() {
538 let illiquid_supply_output = (reserve_release_test_tx().body().outputs().into_iter())
539 .find(|output| {
540 output.address()
541 == illiquid_supply_validator_script().address(NetworkIdKind::Testnet)
542 })
543 .expect("Should output a UTXO to illiquid supply validator")
544 .amount()
545 .multiasset()
546 .expect("Should output native tokens to illiquid supply")
547 .get(&PolicyID::from(token_policy().0))
548 .expect("Should transfer reserve token policy token to illiquid supply")
549 .get(&token_name().to_csl().unwrap())
550 .expect("Should transfer reserve token to illiquid supply");
551
552 assert_eq!(illiquid_supply_output, 5u64.into())
553 }
554
555 #[test]
556 fn should_leave_unreleased_tokens_at_reserve_validator() {
557 let validator_output = (reserve_release_test_tx().body().outputs().into_iter())
558 .find(|output| {
559 output.address() == reserve_validator_script().address(NetworkIdKind::Testnet)
560 })
561 .expect("Should output a UTXO to illiquid supply validator")
562 .amount()
563 .multiasset()
564 .expect("Should output native tokens to illiquid supply")
565 .get(&PolicyID::from(token_policy().0))
566 .expect("Should transfer reserve token policy token to illiquid supply")
567 .get(&token_name().to_csl().unwrap())
568 .expect("Should transfer reserve token to illiquid supply");
569
570 assert_eq!(validator_output, 985u64.into())
571 }
572}