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