1use crate::await_tx::AwaitTx;
10use crate::csl::{
11 CostStore, Costs, InputsBuilderExt, TransactionBuilderExt, TransactionContext, TransactionExt,
12 empty_asset_name, get_builder_config,
13};
14use crate::governance::GovernanceData;
15use crate::multisig::{MultiSigSmartContractResult, submit_or_create_tx_to_sign};
16use crate::plutus_script::PlutusScript;
17use crate::{cardano_keys::CardanoPaymentSigningKey, scripts_data};
18use anyhow::anyhow;
19use cardano_serialization_lib::{
20 BigInt, NetworkIdKind, PlutusData, Transaction, TransactionBuilder, TxInputsBuilder,
21};
22use ogmios_client::query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId};
23use ogmios_client::query_network::QueryNetwork;
24use ogmios_client::transactions::Transactions;
25use ogmios_client::types::OgmiosUtxo;
26use partner_chains_plutus_data::permissioned_candidates::{
27 PermissionedCandidateDatums, permissioned_candidates_to_plutus_data,
28};
29use sidechain_domain::{PermissionedCandidateData, UtxoId};
30
31pub async fn upsert_permissioned_candidates<
38 C: QueryLedgerState + QueryNetwork + Transactions + QueryUtxoByUtxoId,
39 A: AwaitTx,
40>(
41 genesis_utxo: UtxoId,
42 candidates: &[PermissionedCandidateData],
43 payment_signing_key: &CardanoPaymentSigningKey,
44 client: &C,
45 await_tx: &A,
46) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
47 let ctx = TransactionContext::for_payment_key(payment_signing_key, client).await?;
48 let scripts = scripts_data::permissioned_candidates_scripts(genesis_utxo, ctx.network)?;
49 let governance_data = GovernanceData::get(genesis_utxo, client).await?;
50 let validator_utxos = client.query_utxos(&[scripts.validator_address]).await?;
51 let mut candidates = candidates.to_owned();
52 candidates.sort();
53
54 let result_opt = match get_current_permissioned_candidates(validator_utxos)? {
55 Some((_, current_permissioned_candidates))
56 if current_permissioned_candidates == *candidates =>
57 {
58 log::info!("Current permissioned candidates are equal to the one to be set.");
59 None
60 },
61 Some((current_utxo, _)) => {
62 log::info!(
63 "Current permissioned candidates are different to the one to be set. Preparing transaction to update."
64 );
65 Some(
66 update_permissioned_candidates(
67 &scripts.validator,
68 &scripts.policy,
69 &candidates,
70 ¤t_utxo,
71 &governance_data,
72 ctx,
73 client,
74 await_tx,
75 )
76 .await?,
77 )
78 },
79 None => {
80 log::info!(
81 "There aren't any permissioned candidates. Preparing transaction to insert."
82 );
83 Some(
84 insert_permissioned_candidates(
85 &scripts.validator,
86 &scripts.policy,
87 &candidates,
88 &governance_data,
89 ctx,
90 client,
91 await_tx,
92 )
93 .await?,
94 )
95 },
96 };
97 Ok(result_opt)
98}
99
100fn get_current_permissioned_candidates(
101 validator_utxos: Vec<OgmiosUtxo>,
102) -> Result<Option<(OgmiosUtxo, Vec<PermissionedCandidateData>)>, anyhow::Error> {
103 if let Some(utxo) = validator_utxos.first() {
104 let datum = utxo.datum.clone().ok_or_else(|| {
105 anyhow!("Invalid state: an UTXO at the validator script address does not have a datum")
106 })?;
107 let datum_plutus_data = PlutusData::from_bytes(datum.bytes).map_err(|e| {
108 anyhow!(
109 "Internal error: could not decode datum of permissioned candidates validator script: {}",
110 e
111 )
112 })?;
113 let mut permissioned_candidates: Vec<PermissionedCandidateData> =
114 PermissionedCandidateDatums::try_from(datum_plutus_data)
115 .map_err(|e| {
116 anyhow!(
117 "Internal error: could not decode datum of permissioned candidates validator script: {}",
118 e
119 )
120 })?
121 .into();
122 permissioned_candidates.sort();
123 Ok(Some((utxo.clone(), permissioned_candidates)))
124 } else {
125 Ok(None)
126 }
127}
128
129async fn insert_permissioned_candidates<C, A>(
130 validator: &PlutusScript,
131 policy: &PlutusScript,
132 candidates: &[PermissionedCandidateData],
133 governance_data: &GovernanceData,
134 payment_ctx: TransactionContext,
135 client: &C,
136 await_tx: &A,
137) -> anyhow::Result<MultiSigSmartContractResult>
138where
139 C: Transactions + QueryLedgerState + QueryNetwork + QueryUtxoByUtxoId,
140 A: AwaitTx,
141{
142 submit_or_create_tx_to_sign(
143 governance_data,
144 payment_ctx,
145 |costs, ctx| {
146 mint_permissioned_candidates_token_tx(
147 validator,
148 policy,
149 candidates,
150 governance_data,
151 costs,
152 ctx,
153 )
154 },
155 "Insert Permissioned Candidates",
156 client,
157 await_tx,
158 )
159 .await
160}
161
162async fn update_permissioned_candidates<C, A>(
163 validator: &PlutusScript,
164 policy: &PlutusScript,
165 candidates: &[PermissionedCandidateData],
166 current_utxo: &OgmiosUtxo,
167 governance_data: &GovernanceData,
168 payment_ctx: TransactionContext,
169 client: &C,
170 await_tx: &A,
171) -> anyhow::Result<MultiSigSmartContractResult>
172where
173 C: Transactions + QueryNetwork + QueryLedgerState + QueryUtxoByUtxoId,
174 A: AwaitTx,
175{
176 submit_or_create_tx_to_sign(
177 governance_data,
178 payment_ctx,
179 |costs, ctx| {
180 update_permissioned_candidates_tx(
181 validator,
182 policy,
183 candidates,
184 current_utxo,
185 governance_data,
186 costs,
187 ctx,
188 )
189 },
190 "Update Permissioned Candidates",
191 client,
192 await_tx,
193 )
194 .await
195}
196
197fn mint_permissioned_candidates_token_tx(
199 validator: &PlutusScript,
200 policy: &PlutusScript,
201 permissioned_candidates: &[PermissionedCandidateData],
202 governance_data: &GovernanceData,
203 costs: Costs,
204 ctx: &TransactionContext,
205) -> anyhow::Result<Transaction> {
206 let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
207 tx_builder.add_mint_one_script_token(
209 policy,
210 &empty_asset_name(),
211 &permissioned_candidates_policy_redeemer_data(),
212 &costs.get_mint(&policy.clone()),
213 )?;
214 tx_builder.add_output_with_one_script_token(
215 validator,
216 policy,
217 &permissioned_candidates_to_plutus_data(permissioned_candidates),
218 ctx,
219 )?;
220
221 let gov_tx_input = governance_data.utxo_id_as_tx_input();
222 tx_builder.add_mint_one_script_token_using_reference_script(
223 &governance_data.policy.script(),
224 &gov_tx_input,
225 &costs,
226 )?;
227
228 Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
229}
230
231fn update_permissioned_candidates_tx(
232 validator: &PlutusScript,
233 policy: &PlutusScript,
234 permissioned_candidates: &[PermissionedCandidateData],
235 script_utxo: &OgmiosUtxo,
236 governance_data: &GovernanceData,
237 costs: Costs,
238 ctx: &TransactionContext,
239) -> anyhow::Result<Transaction> {
240 let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
241
242 {
243 let mut inputs = TxInputsBuilder::new();
244 inputs.add_script_utxo_input(
245 script_utxo,
246 validator,
247 &permissioned_candidates_policy_redeemer_data(),
248 &costs.get_one_spend(),
249 )?;
250 tx_builder.set_inputs(&inputs);
251 }
252
253 tx_builder.add_output_with_one_script_token(
254 validator,
255 policy,
256 &permissioned_candidates_to_plutus_data(permissioned_candidates),
257 ctx,
258 )?;
259
260 let gov_tx_input = governance_data.utxo_id_as_tx_input();
261 tx_builder.add_mint_one_script_token_using_reference_script(
262 &governance_data.policy.script(),
263 &gov_tx_input,
264 &costs,
265 )?;
266
267 Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
268}
269
270fn permissioned_candidates_policy_redeemer_data() -> PlutusData {
271 PlutusData::new_integer(&BigInt::zero())
272}
273
274pub async fn get_permissioned_candidates<C>(
276 genesis_utxo: UtxoId,
277 network: NetworkIdKind,
278 client: &C,
279) -> anyhow::Result<Option<Vec<PermissionedCandidateData>>>
280where
281 C: QueryNetwork + QueryLedgerState,
282{
283 let scripts = scripts_data::permissioned_candidates_scripts(genesis_utxo, network)?;
284 let validator_utxos = client.query_utxos(&[scripts.validator_address]).await?;
285 Ok(get_current_permissioned_candidates(validator_utxos)?.map(|(_, candidates)| candidates))
286}
287
288#[cfg(test)]
289mod tests {
290 use super::{mint_permissioned_candidates_token_tx, update_permissioned_candidates_tx};
291 use crate::{
292 csl::{Costs, TransactionContext, empty_asset_name},
293 governance::GovernanceData,
294 test_values::*,
295 };
296 use cardano_serialization_lib::{Address, ExUnits, Int, NetworkIdKind, PlutusData};
297 use hex_literal::hex;
298 use ogmios_client::types::{Asset as OgmiosAsset, OgmiosTx, OgmiosUtxo, OgmiosValue};
299 use partner_chains_plutus_data::permissioned_candidates::permissioned_candidates_to_plutus_data;
300 use sidechain_domain::{
301 AuraPublicKey, GrandpaPublicKey, PermissionedCandidateData, SidechainPublicKey,
302 };
303
304 #[test]
305 fn mint_permissioned_candiates_token_tx_regression_test() {
306 let tx = mint_permissioned_candidates_token_tx(
307 &test_validator(),
308 &test_policy(),
309 &input_candidates(),
310 &test_governance_data(),
311 test_costs_mint(),
312 &test_tx_context(),
313 )
314 .unwrap();
315
316 let body = tx.body();
317 let inputs = body.inputs();
318 assert_eq!(
320 inputs.get(0).to_string(),
321 "0404040404040404040404040404040404040404040404040404040404040404#1"
322 );
323 assert_eq!(
324 inputs.get(1).to_string(),
325 "0707070707070707070707070707070707070707070707070707070707070707#0"
326 );
327 assert_eq!(
329 body.collateral().unwrap().get(0).to_string(),
330 "0404040404040404040404040404040404040404040404040404040404040404#1"
331 );
332 let outputs = body.outputs();
333 let change_output = outputs.into_iter().find(|o| o.address() == payment_addr()).unwrap();
335 let script_output = outputs.into_iter().find(|o| o.address() == validator_addr()).unwrap();
337 let coins_sum = change_output
338 .amount()
339 .coin()
340 .checked_add(&script_output.amount().coin())
341 .unwrap()
342 .checked_add(&body.fee())
343 .unwrap();
344 assert_eq!(
345 coins_sum,
346 (greater_payment_utxo().value.lovelace + lesser_payment_utxo().value.lovelace).into()
347 );
348 assert_eq!(
349 script_output
350 .amount()
351 .multiasset()
352 .unwrap()
353 .get_asset(&token_policy_id().into(), &empty_asset_name(),),
354 1u64.into()
355 );
356 assert_eq!(script_output.plutus_data().unwrap(), expected_plutus_data());
357 let mint = body.mint().unwrap();
359 assert_eq!(
360 mint.get(&token_policy_id().into())
361 .unwrap()
362 .get(0)
363 .unwrap()
364 .get(&empty_asset_name())
365 .unwrap(),
366 Int::new_i32(1)
367 );
368
369 let collateral_return = body.collateral_return().unwrap();
371 assert_eq!(collateral_return.address(), payment_addr());
372 let total_collateral = body.total_collateral().unwrap();
373 assert_eq!(
374 collateral_return.amount().coin().checked_add(&total_collateral).unwrap(),
375 greater_payment_utxo().value.lovelace.into()
376 );
377 }
378
379 #[test]
380 fn update_permissioned_candidates_tx_regression_test() {
381 let script_utxo_lovelace = 1952430;
382 let script_utxo = OgmiosUtxo {
383 transaction: OgmiosTx { id: [15; 32] },
384 index: 0,
385 value: OgmiosValue {
386 lovelace: script_utxo_lovelace,
387 native_tokens: vec![(
388 token_policy_id(),
389 vec![OgmiosAsset { name: vec![], amount: 1 }],
390 )]
391 .into_iter()
392 .collect(),
393 },
394 address: validator_addr().to_bech32(None).unwrap(),
395 ..Default::default()
396 };
397
398 let tx = update_permissioned_candidates_tx(
399 &test_validator(),
400 &test_policy(),
401 &input_candidates(),
402 &script_utxo,
403 &test_governance_data(),
404 test_costs_update(),
405 &test_tx_context(),
406 )
407 .unwrap();
408 let body = tx.body();
409 let inputs = body.inputs();
410 assert_eq!(
412 inputs.get(0).to_string(),
413 "0404040404040404040404040404040404040404040404040404040404040404#1"
414 );
415 assert_eq!(
417 inputs.get(1).to_string(),
418 "0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f#0"
419 );
420 assert_eq!(
422 body.collateral().unwrap().get(0).to_string(),
423 "0404040404040404040404040404040404040404040404040404040404040404#1"
424 );
425 let outputs = body.outputs();
426 let change_output = outputs.into_iter().find(|o| o.address() == payment_addr()).unwrap();
428 let script_output = outputs.into_iter().find(|o| o.address() == validator_addr()).unwrap();
430 let coins_sum = change_output
431 .amount()
432 .coin()
433 .checked_add(&script_output.amount().coin())
434 .unwrap()
435 .checked_add(&body.fee())
436 .unwrap();
437 assert_eq!(
438 coins_sum,
439 (greater_payment_utxo().value.lovelace + script_utxo_lovelace).into()
440 );
441 assert_eq!(
442 script_output
443 .amount()
444 .multiasset()
445 .unwrap()
446 .get_asset(&token_policy_id().into(), &empty_asset_name(),),
447 1u64.into()
448 );
449 assert_eq!(script_output.plutus_data().unwrap(), expected_plutus_data());
450
451 let collateral_return = body.collateral_return().unwrap();
453 assert_eq!(collateral_return.address(), payment_addr());
454 let total_collateral = body.total_collateral().unwrap();
455 assert_eq!(
456 collateral_return.amount().coin().checked_add(&total_collateral).unwrap(),
457 greater_payment_utxo().value.lovelace.into()
458 );
459 }
460
461 fn permissioned_candidates_ex_units() -> ExUnits {
462 ExUnits::new(&10000u32.into(), &200u32.into())
463 }
464 fn governance_ex_units() -> ExUnits {
465 ExUnits::new(&99999u32.into(), &999u32.into())
466 }
467
468 fn test_costs_mint() -> Costs {
469 Costs::new(
470 vec![
471 (test_policy().csl_script_hash(), permissioned_candidates_ex_units()),
472 (test_governance_policy().script().script_hash().into(), governance_ex_units()),
473 ]
474 .into_iter()
475 .collect(),
476 vec![(0, permissioned_candidates_ex_units())].into_iter().collect(),
477 )
478 }
479
480 fn test_costs_update() -> Costs {
481 Costs::new(
482 vec![(test_governance_policy().script().script_hash().into(), governance_ex_units())]
483 .into_iter()
484 .collect(),
485 vec![(0, permissioned_candidates_ex_units())].into_iter().collect(),
486 )
487 }
488
489 fn test_goveranance_utxo() -> OgmiosUtxo {
490 OgmiosUtxo { transaction: OgmiosTx { id: [123; 32] }, index: 17, ..Default::default() }
491 }
492
493 fn test_governance_data() -> GovernanceData {
494 GovernanceData { policy: test_governance_policy(), utxo: test_goveranance_utxo() }
495 }
496
497 fn test_tx_context() -> TransactionContext {
498 TransactionContext {
499 payment_key: payment_key(),
500 payment_key_utxos: vec![
501 lesser_payment_utxo(),
502 greater_payment_utxo(),
503 make_utxo(14u8, 0, 400000, &payment_addr()),
504 ],
505 network: NetworkIdKind::Testnet,
506 protocol_parameters: protocol_parameters(),
507 change_address: payment_addr(),
508 }
509 }
510
511 fn lesser_payment_utxo() -> OgmiosUtxo {
512 make_utxo(7u8, 0, 1700000, &payment_addr())
513 }
514
515 fn greater_payment_utxo() -> OgmiosUtxo {
516 make_utxo(4u8, 1, 1800000, &payment_addr())
517 }
518
519 fn validator_addr() -> Address {
520 Address::from_bech32("addr_test1wpha4546lvfcau5jsrwpht9h6350m3au86fev6nwmuqz9gqer2ung")
521 .unwrap()
522 }
523
524 fn token_policy_id() -> [u8; 28] {
525 hex!("f14241393964259a53ca546af364e7f5688ca5aaa35f1e0da0f951b2")
526 }
527
528 fn input_candidates() -> Vec<PermissionedCandidateData> {
529 vec![
530 PermissionedCandidateData {
531 sidechain_public_key: SidechainPublicKey(
532 hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
533 .into(),
534 ),
535 aura_public_key: AuraPublicKey(
536 hex!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").into(),
537 ),
538 grandpa_public_key: GrandpaPublicKey(
539 hex!("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc").into(),
540 ),
541 },
542 PermissionedCandidateData {
543 sidechain_public_key: SidechainPublicKey(
544 hex!("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd")
545 .into(),
546 ),
547 aura_public_key: AuraPublicKey(
548 hex!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee").into(),
549 ),
550 grandpa_public_key: GrandpaPublicKey(
551 hex!("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").into(),
552 ),
553 },
554 ]
555 }
556
557 fn expected_plutus_data() -> PlutusData {
558 permissioned_candidates_to_plutus_data(&input_candidates())
559 }
560}