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