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