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, CandidateKeys, GrandpaPublicKey, PermissionedCandidateData,
302 SidechainPublicKey,
303 };
304
305 #[test]
306 fn mint_permissioned_candiates_token_tx_regression_test() {
307 let tx = mint_permissioned_candidates_token_tx(
308 &test_validator(),
309 &test_policy(),
310 &input_candidates(),
311 &test_governance_data(),
312 test_costs_mint(),
313 &test_tx_context(),
314 )
315 .unwrap();
316
317 let body = tx.body();
318 let inputs = body.inputs();
319 assert_eq!(
321 inputs.get(0).to_string(),
322 "0404040404040404040404040404040404040404040404040404040404040404#1"
323 );
324 assert_eq!(
325 inputs.get(1).to_string(),
326 "0707070707070707070707070707070707070707070707070707070707070707#0"
327 );
328 assert_eq!(
330 body.collateral().unwrap().get(0).to_string(),
331 "0404040404040404040404040404040404040404040404040404040404040404#1"
332 );
333 let outputs = body.outputs();
334 let change_output = outputs.into_iter().find(|o| o.address() == payment_addr()).unwrap();
336 let script_output = outputs.into_iter().find(|o| o.address() == validator_addr()).unwrap();
338 let coins_sum = change_output
339 .amount()
340 .coin()
341 .checked_add(&script_output.amount().coin())
342 .unwrap()
343 .checked_add(&body.fee())
344 .unwrap();
345 assert_eq!(
346 coins_sum,
347 (greater_payment_utxo().value.lovelace + lesser_payment_utxo().value.lovelace).into()
348 );
349 assert_eq!(
350 script_output
351 .amount()
352 .multiasset()
353 .unwrap()
354 .get_asset(&token_policy_id().into(), &empty_asset_name(),),
355 1u64.into()
356 );
357 assert_eq!(script_output.plutus_data().unwrap(), expected_plutus_data());
358 let mint = body.mint().unwrap();
360 assert_eq!(
361 mint.get(&token_policy_id().into())
362 .unwrap()
363 .get(0)
364 .unwrap()
365 .get(&empty_asset_name())
366 .unwrap(),
367 Int::new_i32(1)
368 );
369
370 let collateral_return = body.collateral_return().unwrap();
372 assert_eq!(collateral_return.address(), payment_addr());
373 let total_collateral = body.total_collateral().unwrap();
374 assert_eq!(
375 collateral_return.amount().coin().checked_add(&total_collateral).unwrap(),
376 greater_payment_utxo().value.lovelace.into()
377 );
378 }
379
380 #[test]
381 fn update_permissioned_candidates_tx_regression_test() {
382 let script_utxo_lovelace = 1952430;
383 let script_utxo = OgmiosUtxo {
384 transaction: OgmiosTx { id: [15; 32] },
385 index: 0,
386 value: OgmiosValue {
387 lovelace: script_utxo_lovelace,
388 native_tokens: vec![(
389 token_policy_id(),
390 vec![OgmiosAsset { name: vec![], amount: 1 }],
391 )]
392 .into_iter()
393 .collect(),
394 },
395 address: validator_addr().to_bech32(None).unwrap(),
396 ..Default::default()
397 };
398
399 let tx = update_permissioned_candidates_tx(
400 &test_validator(),
401 &test_policy(),
402 &input_candidates(),
403 &script_utxo,
404 &test_governance_data(),
405 test_costs_update(),
406 &test_tx_context(),
407 )
408 .unwrap();
409 let body = tx.body();
410 let inputs = body.inputs();
411 assert_eq!(
413 inputs.get(0).to_string(),
414 "0404040404040404040404040404040404040404040404040404040404040404#1"
415 );
416 assert_eq!(
418 inputs.get(1).to_string(),
419 "0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f#0"
420 );
421 assert_eq!(
423 body.collateral().unwrap().get(0).to_string(),
424 "0404040404040404040404040404040404040404040404040404040404040404#1"
425 );
426 let outputs = body.outputs();
427 let change_output = outputs.into_iter().find(|o| o.address() == payment_addr()).unwrap();
429 let script_output = outputs.into_iter().find(|o| o.address() == validator_addr()).unwrap();
431 let coins_sum = change_output
432 .amount()
433 .coin()
434 .checked_add(&script_output.amount().coin())
435 .unwrap()
436 .checked_add(&body.fee())
437 .unwrap();
438 assert_eq!(
439 coins_sum,
440 (greater_payment_utxo().value.lovelace + script_utxo_lovelace).into()
441 );
442 assert_eq!(
443 script_output
444 .amount()
445 .multiasset()
446 .unwrap()
447 .get_asset(&token_policy_id().into(), &empty_asset_name(),),
448 1u64.into()
449 );
450 assert_eq!(script_output.plutus_data().unwrap(), expected_plutus_data());
451
452 let collateral_return = body.collateral_return().unwrap();
454 assert_eq!(collateral_return.address(), payment_addr());
455 let total_collateral = body.total_collateral().unwrap();
456 assert_eq!(
457 collateral_return.amount().coin().checked_add(&total_collateral).unwrap(),
458 greater_payment_utxo().value.lovelace.into()
459 );
460 }
461
462 fn permissioned_candidates_ex_units() -> ExUnits {
463 ExUnits::new(&10000u32.into(), &200u32.into())
464 }
465 fn governance_ex_units() -> ExUnits {
466 ExUnits::new(&99999u32.into(), &999u32.into())
467 }
468
469 fn test_costs_mint() -> Costs {
470 Costs::new(
471 vec![
472 (test_policy().csl_script_hash(), permissioned_candidates_ex_units()),
473 (test_governance_policy().script().script_hash().into(), governance_ex_units()),
474 ]
475 .into_iter()
476 .collect(),
477 vec![(0, permissioned_candidates_ex_units())].into_iter().collect(),
478 )
479 }
480
481 fn test_costs_update() -> Costs {
482 Costs::new(
483 vec![(test_governance_policy().script().script_hash().into(), governance_ex_units())]
484 .into_iter()
485 .collect(),
486 vec![(0, permissioned_candidates_ex_units())].into_iter().collect(),
487 )
488 }
489
490 fn test_goveranance_utxo() -> OgmiosUtxo {
491 OgmiosUtxo { transaction: OgmiosTx { id: [123; 32] }, index: 17, ..Default::default() }
492 }
493
494 fn test_governance_data() -> GovernanceData {
495 GovernanceData { policy: test_governance_policy(), utxo: test_goveranance_utxo() }
496 }
497
498 fn test_tx_context() -> TransactionContext {
499 TransactionContext {
500 payment_key: payment_key(),
501 payment_key_utxos: vec![
502 lesser_payment_utxo(),
503 greater_payment_utxo(),
504 make_utxo(14u8, 0, 400000, &payment_addr()),
505 ],
506 network: NetworkIdKind::Testnet,
507 protocol_parameters: protocol_parameters(),
508 change_address: payment_addr(),
509 }
510 }
511
512 fn lesser_payment_utxo() -> OgmiosUtxo {
513 make_utxo(7u8, 0, 1700000, &payment_addr())
514 }
515
516 fn greater_payment_utxo() -> OgmiosUtxo {
517 make_utxo(4u8, 1, 1800000, &payment_addr())
518 }
519
520 fn validator_addr() -> Address {
521 Address::from_bech32("addr_test1wpha4546lvfcau5jsrwpht9h6350m3au86fev6nwmuqz9gqer2ung")
522 .unwrap()
523 }
524
525 fn token_policy_id() -> [u8; 28] {
526 hex!("f14241393964259a53ca546af364e7f5688ca5aaa35f1e0da0f951b2")
527 }
528
529 fn input_candidates() -> Vec<PermissionedCandidateData> {
530 vec![
531 PermissionedCandidateData {
532 sidechain_public_key: SidechainPublicKey(
533 hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
534 .into(),
535 ),
536 keys: CandidateKeys(vec![
537 AuraPublicKey(
538 hex!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
539 .into(),
540 )
541 .into(),
542 GrandpaPublicKey(
543 hex!("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
544 .into(),
545 )
546 .into(),
547 ]),
548 },
549 PermissionedCandidateData {
550 sidechain_public_key: SidechainPublicKey(
551 hex!("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd")
552 .into(),
553 ),
554 keys: CandidateKeys(vec![
555 AuraPublicKey(
556 hex!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")
557 .into(),
558 )
559 .into(),
560 GrandpaPublicKey(
561 hex!("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
562 .into(),
563 )
564 .into(),
565 ]),
566 },
567 ]
568 }
569
570 fn expected_plutus_data() -> PlutusData {
571 permissioned_candidates_to_plutus_data(&input_candidates())
572 }
573}