partner_chains_cardano_offchain/governed_map/
mod.rs

1use crate::csl::{
2	CostStore, Costs, InputsBuilderExt, NetworkTypeExt, TransactionBuilderExt, TransactionExt,
3	empty_asset_name, get_builder_config, unit_plutus_data,
4};
5use crate::governance::GovernanceData;
6use crate::multisig::MultiSigSmartContractResult::TransactionSubmitted;
7use crate::multisig::submit_or_create_tx_to_sign;
8use crate::plutus_script::PlutusScript;
9use crate::{
10	await_tx::AwaitTx, cardano_keys::CardanoPaymentSigningKey, csl::TransactionContext,
11	multisig::MultiSigSmartContractResult,
12};
13use anyhow::anyhow;
14use cardano_serialization_lib::{
15	Int, PlutusData, Transaction, TransactionBuilder, TxInputsBuilder,
16};
17use ogmios_client::{
18	query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId},
19	query_network::QueryNetwork,
20	transactions::Transactions,
21	types::OgmiosUtxo,
22};
23use partner_chains_plutus_data::governed_map::{
24	GovernedMapDatum, governed_map_datum_to_plutus_data,
25};
26use sidechain_domain::byte_string::ByteString;
27use sidechain_domain::{PolicyId, UtxoId};
28use std::ops::Neg;
29
30#[cfg(test)]
31mod tests;
32
33/// Inserts an entry into the governed map.
34/// If the `key` is already set to the provided `value` a transaction is not submitted and the operation succeeds.
35/// Else if the `key` is already set, the operation fails.
36pub async fn run_insert<
37	C: QueryLedgerState + QueryNetwork + Transactions + QueryUtxoByUtxoId,
38	A: AwaitTx,
39>(
40	genesis_utxo: UtxoId,
41	key: String,
42	value: ByteString,
43	payment_signing_key: &CardanoPaymentSigningKey,
44	ogmios_client: &C,
45	await_tx: &A,
46) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
47	let ctx = TransactionContext::for_payment_key(payment_signing_key, ogmios_client).await?;
48	let scripts = crate::scripts_data::governed_map_scripts(genesis_utxo, ctx.network)?;
49	let validator_utxos = ogmios_client.query_utxos(&[scripts.validator_address.clone()]).await?;
50
51	let tx_hash_opt = match get_current_value(validator_utxos, key.clone(), scripts.policy_id()) {
52		Some(current_value) if current_value != value => {
53			return Err(anyhow!("There is already a value stored for key '{key}'."));
54		},
55		Some(_current_value) => {
56			log::info!("Value for key '{key}' is already set to the same value. Skipping insert.");
57			None
58		},
59		None => {
60			log::info!("There is no value stored for key '{key}'. Inserting new one.");
61			Some(
62				insert(
63					&scripts.validator,
64					&scripts.policy,
65					key,
66					value,
67					ctx,
68					genesis_utxo,
69					ogmios_client,
70					await_tx,
71				)
72				.await?,
73			)
74		},
75	};
76	if let Some(TransactionSubmitted(tx_hash)) = tx_hash_opt {
77		await_tx.await_tx_output(ogmios_client, UtxoId::new(tx_hash.0, 0)).await?;
78	}
79	Ok(tx_hash_opt)
80}
81
82async fn insert<
83	C: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
84	A: AwaitTx,
85>(
86	validator: &PlutusScript,
87	policy: &PlutusScript,
88	key: String,
89	value: ByteString,
90	ctx: TransactionContext,
91	genesis_utxo: UtxoId,
92	client: &C,
93	await_tx: &A,
94) -> anyhow::Result<MultiSigSmartContractResult> {
95	let governance_data = GovernanceData::get(genesis_utxo, client).await?;
96
97	submit_or_create_tx_to_sign(
98		&governance_data,
99		ctx,
100		|costs, ctx| {
101			insert_key_value_tx(
102				validator,
103				policy,
104				key.clone(),
105				value.clone(),
106				&governance_data,
107				costs,
108				&ctx,
109			)
110		},
111		"Insert Key-Value pair",
112		client,
113		await_tx,
114	)
115	.await
116}
117
118fn insert_key_value_tx(
119	validator: &PlutusScript,
120	policy: &PlutusScript,
121	key: String,
122	value: ByteString,
123	governance_data: &GovernanceData,
124	costs: Costs,
125	ctx: &TransactionContext,
126) -> anyhow::Result<Transaction> {
127	let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
128	tx_builder.add_mint_one_script_token(
129		policy,
130		&empty_asset_name(),
131		&unit_plutus_data(),
132		&costs.get_mint(&policy.clone()),
133	)?;
134	tx_builder.add_output_with_one_script_token(
135		validator,
136		policy,
137		&governed_map_datum_to_plutus_data(&GovernedMapDatum::new(key, value)),
138		ctx,
139	)?;
140
141	let gov_tx_input = governance_data.utxo_id_as_tx_input();
142	tx_builder.add_mint_one_script_token_using_reference_script(
143		&governance_data.policy.script(),
144		&gov_tx_input,
145		&costs,
146	)?;
147
148	Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
149}
150
151/// Updates an entry in the governed map.
152/// If `expected_current_value` is provided, the current `value` for the `key` must match it, otherwise the operation fails.
153/// If the `key` is not set, the operation fails.
154/// If the `key` is already set to the provided `value` a transaction is not submitted and the operation succeeds.
155pub async fn run_update<
156	C: QueryLedgerState + QueryNetwork + Transactions + QueryUtxoByUtxoId,
157	A: AwaitTx,
158>(
159	genesis_utxo: UtxoId,
160	key: String,
161	value: ByteString,
162	expected_current_value: Option<ByteString>,
163	payment_signing_key: &CardanoPaymentSigningKey,
164	ogmios_client: &C,
165	await_tx: &A,
166) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
167	let ctx = TransactionContext::for_payment_key(payment_signing_key, ogmios_client).await?;
168	let scripts = crate::scripts_data::governed_map_scripts(genesis_utxo, ctx.network)?;
169	let validator_utxos = ogmios_client.query_utxos(&[scripts.validator_address.clone()]).await?;
170	let utxos_for_key =
171		get_utxos_for_key(validator_utxos.clone(), key.clone(), scripts.policy_id());
172
173	let Some(actual_current_value) =
174		get_current_value(validator_utxos.clone(), key.clone(), scripts.policy_id())
175	else {
176		return Err(anyhow!("Cannot update nonexistent key :'{key}'."));
177	};
178
179	if matches!(expected_current_value,
180		Some(ref expected_current_value) if *expected_current_value != actual_current_value)
181	{
182		return Err(anyhow!("Value for key '{key}' is set to a different value than expected."));
183	}
184
185	let tx_hash_opt = {
186		if actual_current_value != value {
187			Some(
188				update(
189					&scripts.validator,
190					&scripts.policy,
191					key,
192					value,
193					&utxos_for_key,
194					ctx,
195					genesis_utxo,
196					ogmios_client,
197					await_tx,
198				)
199				.await?,
200			)
201		} else {
202			log::info!("Value for key '{key}' is already set to the same value. Skipping update.");
203			None
204		}
205	};
206
207	if let Some(TransactionSubmitted(tx_hash)) = tx_hash_opt {
208		await_tx.await_tx_output(ogmios_client, UtxoId::new(tx_hash.0, 0)).await?;
209	}
210	Ok(tx_hash_opt)
211}
212
213async fn update<
214	C: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
215	A: AwaitTx,
216>(
217	validator: &PlutusScript,
218	policy: &PlutusScript,
219	key: String,
220	value: ByteString,
221	utxos_for_key: &[OgmiosUtxo],
222	ctx: TransactionContext,
223	genesis_utxo: UtxoId,
224	client: &C,
225	await_tx: &A,
226) -> anyhow::Result<MultiSigSmartContractResult> {
227	let governance_data = GovernanceData::get(genesis_utxo, client).await?;
228
229	submit_or_create_tx_to_sign(
230		&governance_data,
231		ctx,
232		|costs, ctx| {
233			update_key_value_tx(
234				validator,
235				policy,
236				key.clone(),
237				value.clone(),
238				utxos_for_key,
239				&governance_data,
240				costs,
241				&ctx,
242			)
243		},
244		"Update Key-Value pair",
245		client,
246		await_tx,
247	)
248	.await
249}
250
251fn update_key_value_tx(
252	validator: &PlutusScript,
253	policy: &PlutusScript,
254	key: String,
255	value: ByteString,
256	utxos_for_key: &[OgmiosUtxo],
257	governance_data: &GovernanceData,
258	costs: Costs,
259	ctx: &TransactionContext,
260) -> anyhow::Result<Transaction> {
261	let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
262
263	let gov_tx_input = governance_data.utxo_id_as_tx_input();
264	tx_builder.add_mint_one_script_token_using_reference_script(
265		&governance_data.policy.script(),
266		&gov_tx_input,
267		&costs,
268	)?;
269
270	let spend_indicies = costs.get_spend_indices();
271
272	let mut inputs = TxInputsBuilder::new();
273	for (ix, utxo) in utxos_for_key.iter().enumerate() {
274		inputs.add_script_utxo_input(
275			utxo,
276			validator,
277			&PlutusData::new_bytes(vec![]),
278			&costs.get_spend(*spend_indicies.get(ix).unwrap_or(&0)),
279		)?;
280	}
281	tx_builder.set_inputs(&inputs);
282
283	if utxos_for_key.len() > 1 {
284		let burn_amount = (utxos_for_key.len() as i32 - 1).neg();
285		tx_builder.add_mint_script_tokens(
286			policy,
287			&empty_asset_name(),
288			&unit_plutus_data(),
289			&costs.get_mint(&policy.clone()),
290			&Int::new_i32(burn_amount),
291		)?;
292	}
293
294	tx_builder.add_output_with_one_script_token(
295		validator,
296		policy,
297		&governed_map_datum_to_plutus_data(&GovernedMapDatum::new(key, value)),
298		ctx,
299	)?;
300
301	Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
302}
303
304/// Removes an entry from the governed map.
305/// If the `key` doesn't exist in the map a transaction is not submitted and the operation succeeds.
306pub async fn run_remove<
307	C: QueryLedgerState + QueryNetwork + Transactions + QueryUtxoByUtxoId,
308	A: AwaitTx,
309>(
310	genesis_utxo: UtxoId,
311	key: String,
312	payment_signing_key: &CardanoPaymentSigningKey,
313	ogmios_client: &C,
314	await_tx: &A,
315) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
316	let ctx = TransactionContext::for_payment_key(payment_signing_key, ogmios_client).await?;
317	let scripts = crate::scripts_data::governed_map_scripts(genesis_utxo, ctx.network)?;
318	let validator_utxos = ogmios_client.query_utxos(&[scripts.validator_address.clone()]).await?;
319
320	let utxos_for_key = get_utxos_for_key(validator_utxos, key.clone(), scripts.policy_id());
321
322	let tx_hash_opt = match utxos_for_key.len() {
323		0 => {
324			log::info!("There is no value stored for key '{key}'. Skipping remove.");
325			None
326		},
327		_ => Some(
328			remove(
329				&scripts.validator,
330				&scripts.policy,
331				&utxos_for_key,
332				ctx,
333				genesis_utxo,
334				ogmios_client,
335				await_tx,
336			)
337			.await?,
338		),
339	};
340	if let Some(TransactionSubmitted(tx_hash)) = tx_hash_opt {
341		await_tx.await_tx_output(ogmios_client, UtxoId::new(tx_hash.0, 0)).await?;
342	}
343	Ok(tx_hash_opt)
344}
345
346async fn remove<
347	C: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
348	A: AwaitTx,
349>(
350	validator: &PlutusScript,
351	policy: &PlutusScript,
352	utxos_for_key: &Vec<OgmiosUtxo>,
353	ctx: TransactionContext,
354	genesis_utxo: UtxoId,
355	client: &C,
356	await_tx: &A,
357) -> anyhow::Result<MultiSigSmartContractResult> {
358	let governance_data = GovernanceData::get(genesis_utxo, client).await?;
359
360	submit_or_create_tx_to_sign(
361		&governance_data,
362		ctx,
363		|costs, ctx| {
364			remove_key_value_tx(validator, policy, utxos_for_key, &governance_data, costs, &ctx)
365		},
366		"Remove Key-Value pair",
367		client,
368		await_tx,
369	)
370	.await
371}
372
373fn remove_key_value_tx(
374	validator: &PlutusScript,
375	policy: &PlutusScript,
376	utxos_for_key: &Vec<OgmiosUtxo>,
377	governance_data: &GovernanceData,
378	costs: Costs,
379	ctx: &TransactionContext,
380) -> anyhow::Result<Transaction> {
381	let mut tx_builder = TransactionBuilder::new(&get_builder_config(ctx)?);
382
383	let gov_tx_input = governance_data.utxo_id_as_tx_input();
384	tx_builder.add_mint_one_script_token_using_reference_script(
385		&governance_data.policy.script(),
386		&gov_tx_input,
387		&costs,
388	)?;
389
390	let spend_indices = costs.get_spend_indices();
391
392	let mut inputs = TxInputsBuilder::new();
393	for (ix, utxo) in utxos_for_key.iter().enumerate() {
394		inputs.add_script_utxo_input(
395			utxo,
396			validator,
397			&unit_plutus_data(),
398			&costs.get_spend(*spend_indices.get(ix).unwrap_or(&0)),
399		)?;
400	}
401	tx_builder.set_inputs(&inputs);
402
403	let burn_amount = (utxos_for_key.len() as i32).neg();
404	tx_builder.add_mint_script_tokens(
405		policy,
406		&empty_asset_name(),
407		&unit_plutus_data(),
408		&costs.get_mint(&policy.clone()),
409		&Int::new_i32(burn_amount),
410	)?;
411
412	Ok(tx_builder.balance_update_and_build(ctx)?.remove_native_script_witnesses())
413}
414
415/// Queries all entries stored in the governed map.
416pub async fn run_list<C: QueryLedgerState + QueryNetwork + Transactions + QueryUtxoByUtxoId>(
417	genesis_utxo: UtxoId,
418	ogmios_client: &C,
419) -> anyhow::Result<impl Iterator<Item = GovernedMapDatum>> {
420	let network = ogmios_client.shelley_genesis_configuration().await?.network.to_csl();
421	let scripts = crate::scripts_data::governed_map_scripts(genesis_utxo, network)?;
422	let validator_utxos = ogmios_client.query_utxos(&[scripts.validator_address.clone()]).await?;
423	Ok(ogmios_utxos_to_governed_map_utxos(validator_utxos.into_iter(), scripts.policy_id())
424		.map(|(_, datum)| datum))
425}
426
427/// Queries the provided `key` from the governed map.
428pub async fn run_get<C: QueryLedgerState + QueryNetwork + Transactions + QueryUtxoByUtxoId>(
429	genesis_utxo: UtxoId,
430	key: String,
431	ogmios_client: &C,
432) -> anyhow::Result<Option<ByteString>> {
433	let network = ogmios_client.shelley_genesis_configuration().await?.network.to_csl();
434	let scripts = crate::scripts_data::governed_map_scripts(genesis_utxo, network)?;
435	let validator_utxos = ogmios_client.query_utxos(&[scripts.validator_address.clone()]).await?;
436	Ok(get_current_value(validator_utxos, key, scripts.policy_id()))
437}
438
439fn ogmios_utxos_to_governed_map_utxos(
440	utxos: impl Iterator<Item = OgmiosUtxo>,
441	token: PolicyId,
442) -> impl Iterator<Item = (OgmiosUtxo, GovernedMapDatum)> {
443	utxos.flat_map(move |utxo| {
444		let _ = utxo.clone().value.native_tokens.get(&token.0)?;
445		let datum = utxo.clone().datum?;
446		let datum_plutus_data = PlutusData::from_bytes(datum.bytes).ok()?;
447
448		GovernedMapDatum::try_from(datum_plutus_data).ok().and_then(|d| Some((utxo, d)))
449	})
450}
451
452fn get_current_value(
453	validator_utxos: Vec<OgmiosUtxo>,
454	key: String,
455	token: PolicyId,
456) -> Option<ByteString> {
457	ogmios_utxos_to_governed_map_utxos(validator_utxos.into_iter(), token)
458		.find(|(_, datum)| datum.key == key)
459		.map(|(_, datum)| datum.value)
460}
461
462fn get_utxos_for_key(
463	validator_utxos: Vec<OgmiosUtxo>,
464	key: String,
465	token: PolicyId,
466) -> Vec<OgmiosUtxo> {
467	ogmios_utxos_to_governed_map_utxos(validator_utxos.into_iter(), token)
468		.filter(|(_, datum)| datum.key == key)
469		.map(|(utxo, _)| utxo)
470		.collect()
471}
472
473/// Necessary to test rare case, where two inserts for the same key are executed
474#[allow(dead_code)]
475pub async fn run_insert_with_force<
476	C: QueryLedgerState + QueryNetwork + Transactions + QueryUtxoByUtxoId,
477	A: AwaitTx,
478>(
479	genesis_utxo: UtxoId,
480	key: String,
481	value: ByteString,
482	payment_signing_key: &CardanoPaymentSigningKey,
483	ogmios_client: &C,
484	await_tx: &A,
485) -> anyhow::Result<Option<MultiSigSmartContractResult>> {
486	let ctx = TransactionContext::for_payment_key(payment_signing_key, ogmios_client).await?;
487	let scripts = crate::scripts_data::governed_map_scripts(genesis_utxo, ctx.network)?;
488
489	let tx_hash_opt = Some(
490		insert(
491			&scripts.validator,
492			&scripts.policy,
493			key,
494			value,
495			ctx,
496			genesis_utxo,
497			ogmios_client,
498			await_tx,
499		)
500		.await?,
501	);
502
503	if let Some(TransactionSubmitted(tx_hash)) = tx_hash_opt {
504		await_tx.await_tx_output(ogmios_client, UtxoId::new(tx_hash.0, 0)).await?;
505	}
506	Ok(tx_hash_opt)
507}