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