partner_chains_cardano_offchain/
plutus_script.rs

1use crate::csl::*;
2use anyhow::{Context, Error, anyhow};
3use cardano_serialization_lib::{
4	Address, JsError, Language, LanguageKind, NetworkIdKind, PlutusData, ScriptHash,
5};
6use plutus::ToDatum;
7use sidechain_domain::{AssetId, AssetName, PolicyId};
8use std::marker::PhantomData;
9use uplc::ast::{DeBruijn, Program};
10
11/// Wraps a Plutus script CBOR
12#[derive(Clone, PartialEq, Eq)]
13pub struct PlutusScript {
14	/// CBOR bytes of the encoded Plutus script
15	pub bytes: Vec<u8>,
16	/// The language of the encoded Plutus script
17	pub language: Language,
18}
19
20impl std::fmt::Debug for PlutusScript {
21	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22		f.debug_struct("PlutusScript")
23			.field("bytes", &hex::encode(&self.bytes))
24			.field("language", &self.language.kind())
25			.finish()
26	}
27}
28
29impl PlutusScript {
30	/// Constructs a [PlutusScript].
31	pub fn from_cbor(cbor: &[u8], language: Language) -> Self {
32		Self { bytes: cbor.into(), language }
33	}
34
35	/// Constructs a V2 [PlutusScript].
36	pub fn v2_from_cbor(plutus_script_bytes: &[u8]) -> anyhow::Result<Self> {
37		Ok(Self::from_cbor(&plutus_script_bytes, Language::new_plutus_v2()))
38	}
39
40	/// Applies the [PlutusScript] to the [uplc::PlutusData], binding it to its first argument.
41	/// For example, if the [PlutusScript] has signature:
42	///   `script :: A -> B -> C`
43	/// After application it will be:
44	///   `script' :: B -> C`
45	pub fn apply_data_uplc(self, data: uplc::PlutusData) -> Result<Self, anyhow::Error> {
46		let mut buffer = Vec::new();
47		let mut program = Program::<DeBruijn>::from_cbor(&self.bytes, &mut buffer)
48			.map_err(|e| anyhow!(e.to_string()))?;
49		program = program.apply_data(data);
50		let bytes = program
51			.to_cbor()
52			.map_err(|_| anyhow!("Couldn't encode resulting script as CBOR."))?;
53		Ok(Self { bytes, ..self })
54	}
55
56	/// Extracts the last applied argument from a [PlutusScript].
57	/// For example, if a [PlutusScript] `script` has been applied to [uplc::PlutusData] `data`:
58	/// `script' = script data`, then [Self::unapply_data_uplc] called on `script'` will return `data`.
59	pub fn unapply_data_uplc(&self) -> anyhow::Result<uplc::PlutusData> {
60		let mut buffer = Vec::new();
61		let program = Program::<DeBruijn>::from_cbor(&self.bytes, &mut buffer).unwrap();
62		match program.term {
63			uplc::ast::Term::Apply { function: _, argument } => {
64				let res: Result<uplc::PlutusData, String> = (*argument).clone().try_into();
65				res.map_err(|e| anyhow!(e))
66			},
67			_ => Err(anyhow!("Given Plutus Script is not an applied term")),
68		}
69	}
70
71	/// Extracts the last applied argument from a [PlutusScript], and returns it as CSL [PlutusData].
72	/// For more details see [Self::unapply_data_uplc].
73	pub fn unapply_data_csl(&self) -> Result<PlutusData, anyhow::Error> {
74		let uplc_pd = self.unapply_data_uplc()?;
75		let cbor_bytes = minicbor::to_vec(uplc_pd).expect("to_vec has Infallible error type");
76		Ok(PlutusData::from_bytes(cbor_bytes).expect("UPLC encoded PlutusData is valid"))
77	}
78
79	/// Builds an CSL [Address] for plutus script from the data obtained from smart contracts.
80	pub fn address(&self, network: NetworkIdKind) -> Address {
81		script_address(&self.bytes, network, self.language)
82	}
83
84	/// Returns [uplc::PlutusData] representation of the given script. It is done in the same way as on-chain code expects.
85	/// First, the [Address] is created, then it is converted to [uplc::PlutusData].
86	pub fn address_data(&self, network: NetworkIdKind) -> anyhow::Result<uplc::PlutusData> {
87		let mut se = cbor_event::se::Serializer::new_vec();
88		cbor_event::se::Serialize::serialize(
89			&PlutusData::from_address(&self.address(network))?,
90			&mut se,
91		)
92		.map_err(|e| anyhow!(e))?;
93		let bytes = se.finalize();
94		minicbor::decode(&bytes).map_err(|e| anyhow!(e.to_string()))
95	}
96
97	/// Returns bech32 address of the given PlutusV2 script
98	pub fn address_bech32(&self, network: NetworkIdKind) -> anyhow::Result<String> {
99		self.address(network)
100			.to_bech32(None)
101			.context("Converting script address to bech32")
102	}
103
104	/// Returns script hash of [PlutusScript] as array of bytes.
105	pub fn script_hash(&self) -> [u8; 28] {
106		plutus_script_hash(&self.bytes, self.language)
107	}
108
109	/// Returns [ScriptHash] of [PlutusScript].
110	pub fn csl_script_hash(&self) -> ScriptHash {
111		ScriptHash::from(self.script_hash())
112	}
113
114	/// Returns [PolicyId] of [PlutusScript].
115	pub fn policy_id(&self) -> PolicyId {
116		PolicyId(self.script_hash())
117	}
118
119	/// Returns [AssetId] of [PlutusScript].
120	pub fn empty_name_asset(&self) -> AssetId {
121		AssetId { policy_id: self.policy_id(), asset_name: AssetName::empty() }
122	}
123
124	/// Constructs [AssetId] with given `asset_name` and taking the [PlutusScript] as a minting policy.
125	pub fn asset(
126		&self,
127		asset_name: cardano_serialization_lib::AssetName,
128	) -> Result<AssetId, JsError> {
129		Ok(AssetId { policy_id: self.policy_id(), asset_name: AssetName::from_csl(asset_name)? })
130	}
131
132	/// Converts [PlutusScript] to CSL [cardano_serialization_lib::PlutusScript].
133	pub fn to_csl(&self) -> cardano_serialization_lib::PlutusScript {
134		match self.language.kind() {
135			LanguageKind::PlutusV1 => {
136				cardano_serialization_lib::PlutusScript::new(self.bytes.clone())
137			},
138			LanguageKind::PlutusV2 => {
139				cardano_serialization_lib::PlutusScript::new_v2(self.bytes.clone())
140			},
141			LanguageKind::PlutusV3 => {
142				cardano_serialization_lib::PlutusScript::new_v3(self.bytes.clone())
143			},
144		}
145	}
146}
147
148impl TryFrom<ogmios_client::types::OgmiosScript> for PlutusScript {
149	type Error = Error;
150
151	fn try_from(script: ogmios_client::types::OgmiosScript) -> Result<Self, Self::Error> {
152		let language = match script.language.as_str() {
153			"plutus:v1" => Language::new_plutus_v1(),
154			"plutus:v2" => Language::new_plutus_v2(),
155			"plutus:v3" => Language::new_plutus_v3(),
156			_ => return Err(anyhow!("Unsupported Plutus language version: {}", script.language)),
157		};
158		Ok(Self::from_cbor(&script.cbor, language))
159	}
160}
161
162impl From<PlutusScript> for ogmios_client::types::OgmiosScript {
163	fn from(val: PlutusScript) -> Self {
164		ogmios_client::types::OgmiosScript {
165			language: match val.language.kind() {
166				LanguageKind::PlutusV1 => "plutus:v1",
167				LanguageKind::PlutusV2 => "plutus:v2",
168				LanguageKind::PlutusV3 => "plutus:v3",
169			}
170			.to_string(),
171			cbor: val.bytes,
172			json: None,
173		}
174	}
175}
176
177impl From<raw_scripts::RawScript> for PlutusScript {
178	fn from(value: raw_scripts::RawScript) -> Self {
179		PlutusScript::v2_from_cbor(value.0).expect("raw_scripts provides valid scripts")
180	}
181}
182
183/// Applies arguments to a Plutus script.
184/// The first argument is the script, the rest of the arguments are the datums that will be applied.
185/// * The script can be any type that implements [`Into<PlutusScript>`] for example [raw_scripts::RawScript].
186/// * The arguments can be any type that implements [`Into<PlutusDataWrapper>`]. Implementations are provided for
187///   [uplc::PlutusData] and [plutus::Datum].
188/// Returns [anyhow::Result<uplc::PlutusData>].
189///
190/// Example:
191/// ```rust,ignore
192/// plutus_script![SOME_SCRIPT, genesis_utxo, plutus::Datum::ListDatum(Vec::new())]
193/// ```
194#[macro_export]
195macro_rules! plutus_script {
196    ($ps:expr $(,$args:expr)*) => (
197		{
198			let script = $crate::plutus_script::PlutusScript::from($ps);
199			plutus_script!(@inner, script $(,$args)*)
200		}
201	);
202	(@inner, $ps:expr) => (Ok::<crate::plutus_script::PlutusScript, anyhow::Error>($ps));
203    (@inner, $ps:expr, $arg:expr $(,$args:expr)*) => (
204		$ps.apply_data_uplc($crate::plutus_script::PlutusDataWrapper::from($arg).0)
205	    	.and_then(|ps| plutus_script!(@inner, ps $(,$args)*))
206    )
207}
208
209/// Wrapper type for [uplc::PlutusData].
210///
211/// Note: The type argument is needed to make the compiler accept the implementation for
212/// `impl<T: ToDatum> From<T> for PlutusDataWrapper<T>`.
213pub struct PlutusDataWrapper<T>(pub uplc::PlutusData, PhantomData<T>);
214
215impl<T> PlutusDataWrapper<T> {
216	/// Constructs [PlutusDataWrapper].
217	pub fn new(d: uplc::PlutusData) -> Self {
218		Self(d, PhantomData)
219	}
220}
221
222impl From<uplc::PlutusData> for PlutusDataWrapper<()> {
223	fn from(value: uplc::PlutusData) -> Self {
224		PlutusDataWrapper::new(value)
225	}
226}
227
228impl<T: ToDatum> From<T> for PlutusDataWrapper<T> {
229	fn from(value: T) -> Self {
230		PlutusDataWrapper::new(to_plutus_data(value.to_datum()))
231	}
232}
233
234impl From<raw_scripts::ScriptId> for PlutusDataWrapper<()> {
235	fn from(value: raw_scripts::ScriptId) -> Self {
236		PlutusDataWrapper::new(to_plutus_data((value as u32).to_datum()))
237	}
238}
239
240fn to_plutus_data(datum: plutus::Datum) -> uplc::PlutusData {
241	uplc::plutus_data(&minicbor::to_vec(datum).expect("to_vec is Infallible"))
242		.expect("transformation from PC Datum to pallas PlutusData can't fail")
243}
244
245#[cfg(test)]
246pub(crate) mod tests {
247	use super::*;
248	use hex_literal::hex;
249	use raw_scripts::RawScript;
250	use sidechain_domain::{McTxHash, UtxoId, UtxoIndex};
251
252	pub(crate) const TEST_GENESIS_UTXO: UtxoId =
253		UtxoId { tx_hash: McTxHash([0u8; 32]), index: UtxoIndex(0) };
254
255	// Taken from smart-contracts repository
256	pub(crate) const CANDIDATES_SCRIPT_RAW: RawScript = RawScript(&hex!(
257		"59013b590138010000323322323322323232322222533553353232323233012225335001100f2215333573466e3c014dd7001080909802000980798051bac330033530040022200148040dd7198011a980180311000a4010660026a600400644002900019112999ab9a33710002900009805a4810350543600133003001002300f22253350011300b49103505437002215333573466e1d20000041002133005337020089001000919199109198008018011aab9d001300735573c0026ea80044028402440204c01d2401035054350030092233335573e0024016466a0146ae84008c00cd5d100124c6010446666aae7c00480288cd4024d5d080118019aba20024988c98cd5ce00080109000891001091000980191299a800880211099a80280118020008910010910911980080200191918008009119801980100100081"
258	));
259
260	/// We know it is correct, because we are able to get the same hash as using code from smart-contract repository
261	pub(crate) const CANDIDATES_SCRIPT_WITH_APPLIED_PARAMS: &[u8] = &hex!(
262		"583559013830104c012bd8799fd8799f58200000000000000000000000000000000000000000000000000000000000000000ff00ff0001"
263	);
264
265	#[test]
266	fn apply_parameters_to_deregister() {
267		let applied = plutus_script![CANDIDATES_SCRIPT_RAW, TEST_GENESIS_UTXO].unwrap();
268		assert_eq!(hex::encode(applied.bytes), hex::encode(CANDIDATES_SCRIPT_WITH_APPLIED_PARAMS));
269	}
270
271	#[test]
272	fn unapply_term_csl() {
273		let applied = plutus_script![CANDIDATES_SCRIPT_RAW, TEST_GENESIS_UTXO].unwrap();
274		let data: PlutusData = applied.unapply_data_csl().unwrap();
275		assert_eq!(
276			data,
277			PlutusData::from_bytes(minicbor::to_vec(TEST_GENESIS_UTXO.to_datum()).unwrap())
278				.unwrap()
279		)
280	}
281
282	#[test]
283	fn unapply_term_uplc() {
284		let applied = plutus_script![CANDIDATES_SCRIPT_RAW, TEST_GENESIS_UTXO].unwrap();
285		let data: uplc::PlutusData = applied.unapply_data_uplc().unwrap();
286		assert_eq!(
287			data,
288			uplc::plutus_data(&minicbor::to_vec(TEST_GENESIS_UTXO.to_datum()).unwrap()).unwrap()
289		)
290	}
291}