1use crate::{
2 await_tx::AwaitTx,
3 cardano_keys::CardanoPaymentSigningKey,
4 csl::{
5 CostStore, Costs, InputsBuilderExt, MultiAssetExt, OgmiosUtxoExt, TransactionBuilderExt,
6 TransactionContext, TransactionExt, TransactionOutputAmountBuilderExt, get_builder_config,
7 },
8 governance::GovernanceData,
9 multisig::{MultiSigSmartContractResult, submit_or_create_tx_to_sign},
10 plutus_script::PlutusScript,
11 scripts_data::{self, PlutusScriptData},
12};
13use anyhow::anyhow;
14use cardano_serialization_lib::{
15 AssetName, BigInt, MultiAsset, PlutusData, ScriptRef, Transaction, TransactionBuilder,
16 TransactionOutputBuilder, TxInputsBuilder,
17};
18use ogmios_client::{
19 query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId},
20 query_network::QueryNetwork,
21 transactions::Transactions,
22 types::OgmiosUtxo,
23};
24use partner_chains_plutus_data::version_oracle::{VersionOracleDatum, VersionOraclePolicyRedeemer};
25use raw_scripts::ScriptId;
26use sidechain_domain::UtxoId;
27
28pub(crate) struct ScriptData {
29 name: String,
30 plutus_script: PlutusScript,
31 id: ScriptId,
32}
33
34impl ScriptData {
35 pub(crate) fn new(name: &str, raw_bytes: Vec<u8>, id: ScriptId) -> Self {
36 let plutus_script =
37 PlutusScript::v2_from_cbor(&raw_bytes).expect("Plutus script should be valid");
38 Self { name: name.to_string(), plutus_script, id }
39 }
40}
41
42pub(crate) async fn initialize_script<
43 T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
44 A: AwaitTx,
45>(
46 script: ScriptData,
47 genesis_utxo: UtxoId,
48 payment_key: &CardanoPaymentSigningKey,
49 client: &T,
50 await_tx: &A,
51) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
52 let payment_ctx = TransactionContext::for_payment_key(payment_key, client).await?;
53 let governance = GovernanceData::get(genesis_utxo, client).await?;
54 let version_oracle = scripts_data::version_oracle(genesis_utxo, payment_ctx.network)?;
55
56 if script_is_initialized(&script, &version_oracle, &payment_ctx, client).await? {
57 log::info!("Script '{}' is already initialized", script.name);
58 return Ok(None);
59 }
60 Ok(Some(
61 submit_or_create_tx_to_sign(
62 &governance,
63 payment_ctx,
64 |costs, ctx| init_script_tx(&script, &governance, &version_oracle, costs, &ctx),
65 &format!("Init {}", script.name),
66 client,
67 await_tx,
68 )
69 .await?,
70 ))
71}
72
73pub(crate) async fn update_script<
74 T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
75 A: AwaitTx,
76>(
77 script: ScriptData,
78 genesis_utxo: UtxoId,
79 old_versioned_utxo: OgmiosUtxo,
80 payment_key: &CardanoPaymentSigningKey,
81 client: &T,
82 await_tx: &A,
83) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
84 let payment_ctx = TransactionContext::for_payment_key(payment_key, client).await?;
85 let governance = GovernanceData::get(genesis_utxo, client).await?;
86 let version_oracle = scripts_data::version_oracle(genesis_utxo, payment_ctx.network)?;
87
88 if !script_is_initialized(&script, &version_oracle, &payment_ctx, client).await? {
89 log::info!("Script '{}' isn't initialized yet", script.name);
90 return Ok(None);
91 }
92 Ok(Some(
93 submit_or_create_tx_to_sign(
94 &governance,
95 payment_ctx,
96 |costs, ctx| {
97 update_script_tx(
98 &script,
99 &governance,
100 &version_oracle,
101 &old_versioned_utxo,
102 costs,
103 &ctx,
104 )
105 },
106 &format!("Update {}", script.name),
107 client,
108 await_tx,
109 )
110 .await?,
111 ))
112}
113
114pub async fn upsert_script<
116 T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
117 A: AwaitTx,
118>(
119 plutus_script: PlutusScript,
120 script_id: u32,
121 genesis_utxo: UtxoId,
122 payment_key: &CardanoPaymentSigningKey,
123 client: &T,
124 await_tx: &A,
125) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
126 let payment_ctx = TransactionContext::for_payment_key(payment_key, client).await?;
127 let version_oracle = scripts_data::version_oracle(genesis_utxo, payment_ctx.network)?;
128
129 let script_id = script_id.try_into().map_err(|_| anyhow!("Invalid script id"))?;
130 let versioned_utxo = find_script_utxo(script_id, &version_oracle, &payment_ctx, client).await?;
131 let script_data = ScriptData { name: format!("{:?}", script_id), plutus_script, id: script_id };
132
133 match versioned_utxo {
134 Some(utxo) => {
135 update_script(script_data, genesis_utxo, utxo, payment_key, client, await_tx).await
136 },
137
138 None => initialize_script(script_data, genesis_utxo, payment_key, client, await_tx).await,
139 }
140}
141
142fn init_script_tx(
143 script: &ScriptData,
144 governance: &GovernanceData,
145 version_oracle: &PlutusScriptData,
146 costs: Costs,
147 ctx: &TransactionContext,
148) -> anyhow::Result<Transaction> {
149 let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
150 {
151 tx_builder.add_mint_one_script_token(
152 &version_oracle.policy,
153 &version_oracle_asset_name(),
154 &VersionOraclePolicyRedeemer::MintVersionOracle(
155 script.id.into(),
156 script.plutus_script.script_hash().into(),
157 )
158 .into(),
159 &costs.get_mint(&version_oracle.policy.clone()),
160 )?;
161 }
162 {
163 let script_ref = ScriptRef::new_plutus_script(&script.plutus_script.to_csl());
164 let amount_builder = TransactionOutputBuilder::new()
165 .with_address(&version_oracle.validator.address(ctx.network))
166 .with_plutus_data(
167 &VersionOracleDatum {
168 version_oracle: script.id.into(),
169 currency_symbol: version_oracle.policy.script_hash().into(),
170 }
171 .into(),
172 )
173 .with_script_ref(&script_ref)
174 .next()?;
175 let ma = MultiAsset::new()
176 .with_asset_amount(&version_oracle.policy.asset(version_oracle_asset_name())?, 1u64)?;
177 let output = amount_builder.with_minimum_ada_and_asset(&ma, ctx)?.build()?;
178 tx_builder.add_output(&output)?;
179 }
180 let gov_tx_input = governance.utxo_id_as_tx_input();
182 tx_builder.add_mint_one_script_token_using_reference_script(
183 &governance.policy.script(),
184 &gov_tx_input,
185 &costs,
186 )?;
187 Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
188}
189
190fn update_script_tx(
191 script: &ScriptData,
192 governance: &GovernanceData,
193 version_oracle: &PlutusScriptData,
194 old_versioned_utxo: &OgmiosUtxo,
195 costs: Costs,
196 ctx: &TransactionContext,
197) -> anyhow::Result<Transaction> {
198 let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
199 {
200 tx_builder.set_inputs(&{
201 let mut inputs = TxInputsBuilder::new();
202 inputs.add_script_utxo_input(
203 &old_versioned_utxo,
204 &version_oracle.validator,
205 &PlutusData::new_integer(&BigInt::from(script.id as u32)),
206 &costs.get_one_spend(),
207 )?;
208
209 inputs
210 });
211 }
212 {
213 let script_ref = ScriptRef::new_plutus_script(&script.plutus_script.to_csl());
214 let amount_builder = TransactionOutputBuilder::new()
215 .with_address(&version_oracle.validator.address(ctx.network))
216 .with_plutus_data(
217 &VersionOracleDatum {
218 version_oracle: script.id.into(),
219 currency_symbol: version_oracle.policy.script_hash().into(),
220 }
221 .into(),
222 )
223 .with_script_ref(&script_ref)
224 .next()?;
225 let ma = MultiAsset::new()
226 .with_asset_amount(&version_oracle.policy.asset(version_oracle_asset_name())?, 1u64)?;
227 let output = amount_builder.with_minimum_ada_and_asset(&ma, ctx)?.build()?;
228 tx_builder.add_output(&output)?;
229 }
230 let gov_tx_input = governance.utxo_id_as_tx_input();
232 tx_builder.add_mint_one_script_token_using_reference_script(
233 &governance.policy.script(),
234 &gov_tx_input,
235 &costs,
236 )?;
237 Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
238}
239
240fn version_oracle_asset_name() -> AssetName {
241 AssetName::new(b"Version oracle".to_vec()).unwrap()
242}
243
244async fn script_is_initialized<T: QueryLedgerState>(
248 script: &ScriptData,
249 version_oracle: &PlutusScriptData,
250 ctx: &TransactionContext,
251 client: &T,
252) -> Result<bool, anyhow::Error> {
253 Ok(find_script_utxo(script.id, version_oracle, ctx, client).await?.is_some())
254}
255
256pub(crate) async fn find_script_utxo<T: QueryLedgerState>(
260 script_id: ScriptId,
261 version_oracle: &PlutusScriptData,
262 ctx: &TransactionContext,
263 client: &T,
264) -> Result<Option<OgmiosUtxo>, anyhow::Error> {
265 let validator_address = version_oracle.validator.address(ctx.network).to_bech32(None)?;
266 let validator_utxos = client.query_utxos(&[validator_address]).await?;
267 Ok(validator_utxos.into_iter().find(|utxo| {
269 utxo.get_plutus_data()
270 .and_then(|data| TryInto::<VersionOracleDatum>::try_into(data).ok())
271 .is_some_and(|datum| {
272 datum.version_oracle.script_id == script_id as u32
273 && datum.currency_symbol == version_oracle.policy.script_hash().into()
274 })
275 }))
276}
277
278pub async fn get_script_utxo<T: QueryLedgerState + QueryNetwork>(
280 script_id: ScriptId,
281 version_oracle: &PlutusScriptData,
282 payment_key: &CardanoPaymentSigningKey,
283 client: &T,
284) -> Result<Option<OgmiosUtxo>, anyhow::Error> {
285 let ctx = TransactionContext::for_payment_key(payment_key, client).await?;
286 find_script_utxo(script_id, version_oracle, &ctx, client).await
287}
288
289#[cfg(test)]
290mod tests {
291 use super::{ScriptData, init_script_tx};
292 use crate::{
293 csl::{Costs, OgmiosUtxoExt, TransactionContext, unit_plutus_data},
294 governance::GovernanceData,
295 plutus_script,
296 scripts_data::{self, PlutusScriptData},
297 test_values::{
298 make_utxo, payment_addr, payment_key, protocol_parameters, test_governance_policy,
299 },
300 };
301 use cardano_serialization_lib::{
302 AssetName, BigNum, ExUnits, Int, NetworkIdKind, PlutusData, RedeemerTag, ScriptHash,
303 Transaction,
304 };
305 use ogmios_client::types::{OgmiosTx, OgmiosUtxo};
306 use partner_chains_plutus_data::version_oracle::VersionOraclePolicyRedeemer;
307 use raw_scripts::ScriptId;
308 use sidechain_domain::UtxoId;
309
310 #[test]
311 fn init_script_tx_version_oracle_output_test() {
312 let outputs = make_init_script_tx().body().outputs();
313 let voo = outputs
314 .into_iter()
315 .find(|o| o.address() == version_oracle_validator_address())
316 .expect("Init Script Transaction should have output to Version Oracle Validator");
317 let voo_script_ref = voo
318 .script_ref()
319 .expect("Version Oracle Validator output should have script reference");
320 let voo_plutus_script = voo_script_ref
321 .plutus_script()
322 .expect("Script reference should be Plutus script");
323 assert_eq!(voo_plutus_script, test_initialized_script().plutus_script.to_csl());
324 let amount = voo
325 .amount()
326 .multiasset()
327 .and_then(|ma| ma.get(&version_oracle_policy_csl_script_hash()))
328 .and_then(|vo_ma| vo_ma.get(&AssetName::new(b"Version oracle".to_vec()).unwrap()))
329 .expect("Version Oracle Validator output has a token of Version Oracle Policy");
330
331 assert_eq!(amount, 1u64.into());
332
333 let voo_plutus_data = voo
334 .plutus_data()
335 .and_then(|pd| pd.as_list())
336 .expect("Version Oracle Validator output should have Plutus Data of List type");
337 assert_eq!(
338 voo_plutus_data.get(0),
339 PlutusData::new_integer(&(test_initialized_script().id as u32).into())
340 );
341 assert_eq!(
342 voo_plutus_data.get(1),
343 PlutusData::new_bytes(version_oracle_data().policy.script_hash().to_vec())
344 );
345 }
346
347 #[test]
348 fn init_script_tx_change_output_test() {
349 let outputs = make_init_script_tx().body().outputs();
350 let change_output = outputs
351 .into_iter()
352 .find(|o| o.address() == payment_addr())
353 .expect("Change output has to be present to keep governance token")
354 .clone();
355 let gov_token_amount = change_output
356 .amount()
357 .multiasset()
358 .and_then(|ma| ma.get(&test_governance_data().policy.script().script_hash().into()))
359 .and_then(|gov_ma| gov_ma.get(&AssetName::new(vec![]).unwrap()))
360 .unwrap();
361 assert_eq!(gov_token_amount, BigNum::one(), "Change contains one governance token");
362 }
363
364 #[test]
365 fn init_script_tx_reference_input() {
366 let ref_input = make_init_script_tx()
368 .body()
369 .reference_inputs()
370 .expect("Init transaction should have reference input")
371 .get(0);
372 assert_eq!(ref_input, test_governance_input().to_csl_tx_input());
373 }
374
375 #[test]
376 fn init_script_tx_mints() {
377 let tx = make_init_script_tx();
378 let vo_mint = tx
379 .body()
380 .mint()
381 .and_then(|mint| mint.get(&version_oracle_policy_csl_script_hash()))
382 .and_then(|assets| assets.get(0))
383 .and_then(|assets| assets.get(&AssetName::new(b"Version oracle".to_vec()).unwrap()))
384 .expect("Transaction should have a mint of Version Oracle Policy token");
385 assert_eq!(vo_mint, Int::new_i32(1));
386
387 let gov_mint = tx
388 .body()
389 .mint()
390 .and_then(|mint| mint.get(&test_governance_data().policy.script().script_hash().into()))
391 .and_then(|assets| assets.get(0))
392 .and_then(|assets| assets.get(&AssetName::new(vec![]).unwrap()))
393 .expect("Transaction should have a mint of Governance Policy token");
394 assert_eq!(gov_mint, Int::new_i32(1));
395 }
396
397 #[test]
398 fn init_script_tx_redeemers() {
399 let ws = make_init_script_tx()
401 .witness_set()
402 .redeemers()
403 .expect("Transaction has two redeemers");
404 let redeemers = vec![ws.get(0), ws.get(1)];
405
406 let expected_vo_redeemer_data = {
407 let script_hash = test_initialized_script().plutus_script.script_hash();
408 VersionOraclePolicyRedeemer::MintVersionOracle(
409 test_initialized_script().id.into(),
410 script_hash.into(),
411 )
412 .into()
413 };
414
415 let _ = redeemers
416 .iter()
417 .find(|r| {
418 r.tag() == RedeemerTag::new_mint()
419 && r.data() == expected_vo_redeemer_data
420 && r.ex_units() == versioning_script_cost()
421 })
422 .expect("Transaction should have a mint redeemer for Version Oracle Policy");
423 let _ = redeemers
424 .iter()
425 .find(|r| {
426 r.tag() == RedeemerTag::new_mint()
427 && r.data() == unit_plutus_data()
428 && r.ex_units() == governance_script_cost()
429 })
430 .expect("Transaction should have a mint redeemer for Governance Policy");
431 }
432
433 fn make_init_script_tx() -> Transaction {
434 init_script_tx(
435 &test_initialized_script(),
436 &test_governance_data(),
437 &version_oracle_data(),
438 test_costs(),
439 &test_transaction_context(),
440 )
441 .unwrap()
442 }
443
444 fn test_initialized_script() -> ScriptData {
445 ScriptData::new(
446 "Test Script",
447 plutus_script![
448 raw_scripts::RESERVE_VALIDATOR,
449 version_oracle_data().policy_id_as_plutus_data()
450 ]
451 .unwrap()
452 .bytes
453 .to_vec(),
454 ScriptId::ReserveValidator,
455 )
456 }
457
458 fn test_governance_input() -> OgmiosUtxo {
459 OgmiosUtxo { transaction: OgmiosTx { id: [16; 32] }, index: 0, ..Default::default() }
460 }
461
462 fn test_governance_data() -> GovernanceData {
463 GovernanceData { policy: test_governance_policy(), utxo: test_governance_input() }
464 }
465
466 fn version_oracle_data() -> PlutusScriptData {
467 scripts_data::version_oracle(UtxoId::new([255u8; 32], 0), NetworkIdKind::Testnet).unwrap()
468 }
469
470 fn version_oracle_validator_address() -> cardano_serialization_lib::Address {
471 version_oracle_data().validator.address(NetworkIdKind::Testnet)
472 }
473
474 fn version_oracle_policy_csl_script_hash() -> ScriptHash {
475 version_oracle_data().policy.csl_script_hash()
476 }
477
478 fn test_transaction_context() -> TransactionContext {
479 TransactionContext {
480 payment_key: payment_key(),
481 payment_key_utxos: vec![make_utxo(121u8, 3, 996272387, &payment_addr())],
482 network: NetworkIdKind::Testnet,
483 protocol_parameters: protocol_parameters(),
484 change_address: payment_addr(),
485 }
486 }
487
488 fn governance_script_cost() -> ExUnits {
489 ExUnits::new(&100u64.into(), &200u64.into())
490 }
491
492 fn versioning_script_cost() -> ExUnits {
493 ExUnits::new(&300u64.into(), &400u64.into())
494 }
495
496 fn test_costs() -> Costs {
497 Costs::new(
498 vec![
499 (test_governance_policy().script().script_hash().into(), governance_script_cost()),
500 (version_oracle_policy_csl_script_hash(), versioning_script_cost()),
501 ]
502 .into_iter()
503 .collect(),
504 std::collections::HashMap::new(),
505 )
506 }
507}