1use crate::{cbor, references};
17use anyhow::{Context, Result, anyhow, bail};
18use pallas_addresses::Address;
19use pallas_codec::minicbor;
20use pallas_crypto::hash::Hash;
21use pallas_primitives::conway::{CostModels, Redeemer, RedeemerTag};
22use pallas_traverse::MultiEraTx;
23use seedelf_koios::koios::UtxoResponse;
24use serde_json::{Value, json};
25use std::collections::BTreeMap;
26
27const MEASURE_LIMIT: (u64, u64) = (14_000_000_000, 20_000_000);
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Resolved {
36 pub tx_hash: [u8; 32],
37 pub index: u64,
38 pub output: Vec<u8>,
39}
40
41impl Resolved {
42 fn input_cbor(&self) -> Result<Vec<u8>> {
43 let mut e = minicbor::Encoder::new(Vec::new());
44 e.array(2)?.bytes(&self.tx_hash)?.u64(self.index)?;
45 Ok(e.into_writer())
46 }
47}
48
49pub fn refusal(row: &UtxoResponse) -> Option<String> {
57 if row.reference_script.is_some() {
58 return Some(format!(
59 "UTxO {}#{} holds a reference script, which the wallet can't evaluate with yet",
60 row.tx_hash, row.tx_index
61 ));
62 }
63 let too_deep = row
64 .inline_datum
65 .as_ref()
66 .and_then(|d| hex::decode(&d.bytes).ok())
67 .is_some_and(|datum| !cbor::within_depth(&datum, cbor::MAX_DEPTH));
68 too_deep.then(|| {
69 format!(
70 "UTxO {}#{} holds a datum the wallet can't read",
71 row.tx_hash, row.tx_index
72 )
73 })
74}
75
76pub fn resolve_row(row: &UtxoResponse) -> Result<Resolved> {
79 let tx_hash: [u8; 32] = hex::decode(&row.tx_hash)?
80 .try_into()
81 .map_err(|_| anyhow!("UTxO {}#{} has a malformed hash", row.tx_hash, row.tx_index))?;
82 if let Some(why) = refusal(row) {
83 bail!(why);
84 }
85 let address = Address::from_bech32(&row.address)
86 .map_err(|e| {
87 anyhow!(
88 "UTxO {}#{} has an unreadable address: {e}",
89 row.tx_hash,
90 row.tx_index
91 )
92 })?
93 .to_vec();
94 let lovelace: u64 = row.value.parse().map_err(|_| {
95 anyhow!(
96 "UTxO {}#{} has an unreadable value",
97 row.tx_hash,
98 row.tx_index
99 )
100 })?;
101 let mut tokens: BTreeMap<Vec<u8>, BTreeMap<Vec<u8>, u64>> = BTreeMap::new();
103 for asset in row.asset_list.iter().flatten() {
104 let quantity: u64 = asset.quantity.parse().map_err(|_| {
105 anyhow!(
106 "UTxO {}#{} has an unreadable token amount",
107 row.tx_hash,
108 row.tx_index
109 )
110 })?;
111 *tokens
112 .entry(hex::decode(&asset.policy_id)?)
113 .or_default()
114 .entry(hex::decode(&asset.asset_name)?)
115 .or_default() += quantity;
116 }
117 let inline = row
118 .inline_datum
119 .as_ref()
120 .map(|d| hex::decode(&d.bytes))
121 .transpose()?;
122 let datum_hash = row.datum_hash.as_ref().map(hex::decode).transpose()?;
123
124 let mut e = minicbor::Encoder::new(Vec::new());
125 let fields = 2 + u64::from(inline.is_some() || datum_hash.is_some());
126 e.map(fields)?.u8(0)?.bytes(&address)?.u8(1)?;
127 if tokens.is_empty() {
128 e.u64(lovelace)?;
129 } else {
130 e.array(2)?.u64(lovelace)?.map(tokens.len() as u64)?;
131 for (policy, names) in &tokens {
132 e.bytes(policy)?.map(names.len() as u64)?;
133 for (name, quantity) in names {
134 e.bytes(name)?.u64(*quantity)?;
135 }
136 }
137 }
138 match (inline, datum_hash) {
139 (Some(datum), _) => {
141 e.u8(2)?
142 .array(2)?
143 .u8(1)?
144 .tag(minicbor::data::Tag::new(24))?
145 .bytes(&datum)?;
146 }
147 (None, Some(hash)) => {
148 e.u8(2)?.array(2)?.u8(0)?.bytes(&hash)?;
149 }
150 (None, None) => {}
151 }
152 Ok(Resolved {
153 tx_hash,
154 index: row.tx_index,
155 output: e.into_writer(),
156 })
157}
158
159pub fn resolve_outputs(tx_cbor: &[u8]) -> Result<Vec<Resolved>> {
162 let tx =
163 MultiEraTx::decode(tx_cbor).map_err(|e| anyhow!("The transaction can't be read: {e}"))?;
164 let tx_hash: [u8; 32] = *tx.hash();
165 Ok(tx
166 .outputs()
167 .iter()
168 .enumerate()
169 .map(|(index, output)| Resolved {
170 tx_hash,
171 index: index as u64,
172 output: output.encode(),
173 })
174 .collect())
175}
176
177pub fn seedelf_references(network_flag: bool) -> Result<Vec<Resolved>> {
180 let config = crate::constants::get_config(crate::constants::VARIANT, network_flag)?;
181 let (wallet, seedelf) = if network_flag {
182 (references::PREPROD_WALLET, references::PREPROD_SEEDELF)
183 } else {
184 (references::MAINNET_WALLET, references::MAINNET_SEEDELF)
185 };
186 Ok(vec![
187 Resolved {
188 tx_hash: config.reference.wallet_reference_utxo,
189 index: 1,
190 output: hex::decode(wallet)?,
191 },
192 Resolved {
193 tx_hash: config.reference.seedelf_reference_utxo,
194 index: 1,
195 output: hex::decode(seedelf)?,
196 },
197 ])
198}
199
200fn slot_config(network_flag: bool) -> (u64, u64, u32) {
202 if network_flag {
203 (1_655_769_600_000, 86_400, 1_000)
204 } else {
205 (1_596_059_091_000, 4_492_800, 1_000)
206 }
207}
208
209pub fn slot_at(network_flag: bool, unix_ms: u64) -> u64 {
213 let (zero_time, zero_slot, slot_length) = slot_config(network_flag);
214 zero_slot + unix_ms.saturating_sub(zero_time) / u64::from(slot_length)
215}
216
217fn purpose(tag: &RedeemerTag) -> &'static str {
218 match tag {
219 RedeemerTag::Spend => "spend",
220 RedeemerTag::Mint => "mint",
221 RedeemerTag::Cert => "publish",
222 RedeemerTag::Reward => "withdraw",
223 RedeemerTag::Vote => "vote",
224 RedeemerTag::Propose => "propose",
225 }
226}
227
228pub fn evaluate(
234 tx_cbor: &[u8],
235 known: &[Resolved],
236 cost_model_v3: &[i64],
237 network_flag: bool,
238) -> Result<Value> {
239 let tx =
240 MultiEraTx::decode(tx_cbor).map_err(|e| anyhow!("The transaction can't be read: {e}"))?;
241 let mut pairs = Vec::new();
242 for input in tx.inputs().iter().chain(tx.reference_inputs().iter()) {
243 let hash: Hash<32> = *input.hash();
244 let index = input.index();
245 let resolved = known
246 .iter()
247 .find(|r| r.tx_hash == *hash && r.index == index)
248 .with_context(|| {
249 format!("The wallet doesn't have UTxO {hash}#{index}, which this transaction uses")
250 })?;
251 pairs.push((resolved.input_cbor()?, resolved.output.clone()));
252 }
253 let cost_models = CostModels {
254 plutus_v1: None,
255 plutus_v2: None,
256 plutus_v3: Some(cost_model_v3.to_vec()),
257 };
258 let cost_models = minicbor::to_vec(&cost_models)
259 .map_err(|e| anyhow!("The cost model can't be encoded: {e}"))?;
260
261 match uplc::tx::eval_phase_two_raw(
262 tx_cbor,
263 &pairs,
264 Some(&cost_models),
265 MEASURE_LIMIT,
266 slot_config(network_flag),
267 true,
268 |_| (),
269 ) {
270 Ok(results) => {
271 let mut rows = Vec::with_capacity(results.len());
272 for (redeemer, result) in results {
273 let redeemer: Redeemer = minicbor::decode(&redeemer)
274 .map_err(|e| anyhow!("The evaluator returned an unreadable redeemer: {e}"))?;
275 let cost = result.cost();
276 rows.push(json!({
277 "validator": { "purpose": purpose(&redeemer.tag), "index": redeemer.index },
278 "budget": { "memory": cost.mem, "cpu": cost.cpu },
279 }));
280 }
281 Ok(json!({ "result": rows }))
282 }
283 Err(uplc::tx::error::Error::RedeemerError { tag, index, err }) => {
284 let reason = err.to_string();
285 let reason = reason.lines().next().unwrap_or("failed").to_string();
286 Ok(json!({
287 "error": {
288 "code": 3010,
289 "message": "Some scripts of the transaction terminated with error(s).",
290 "data": [{
291 "validator": { "purpose": ogmios_purpose(&tag), "index": index },
292 "error": { "message": reason, "data": { "validationError": reason, "traces": [] } },
293 }],
294 }
295 }))
296 }
297 Err(e) => bail!("The wallet couldn't evaluate the transaction: {e}"),
298 }
299}
300
301fn ogmios_purpose(tag: &str) -> &'static str {
303 match tag.to_ascii_lowercase().as_str() {
304 "spend" => "spend",
305 "mint" => "mint",
306 "cert" => "publish",
307 "reward" | "withdraw" => "withdraw",
308 "vote" => "vote",
309 _ => "propose",
310 }
311}
312
313pub fn ogmios_utxos(tx_cbor: &[u8]) -> Result<Value> {
319 let tx =
320 MultiEraTx::decode(tx_cbor).map_err(|e| anyhow!("The transaction can't be read: {e}"))?;
321 let id = hex::encode(tx.hash());
322 let mut rows = Vec::new();
323 for (index, output) in tx.outputs().iter().enumerate() {
324 let address = output
325 .address()
326 .map_err(|e| anyhow!("An output's address can't be read: {e}"))?
327 .to_bech32()
328 .map_err(|e| anyhow!("An output's address can't be written: {e}"))?;
329 let value = output.value();
330 let mut amounts = serde_json::Map::new();
331 amounts.insert("ada".into(), json!({ "lovelace": value.coin() }));
332 for policy in value.assets() {
333 let mut names = serde_json::Map::new();
334 for asset in policy.assets() {
335 names.insert(
336 hex::encode(asset.name()),
337 json!(asset.output_coin().unwrap_or(0)),
338 );
339 }
340 amounts.insert(hex::encode(policy.policy()), Value::Object(names));
341 }
342 let mut row = json!({
343 "transaction": { "id": id },
344 "index": index,
345 "address": address,
346 "value": amounts,
347 });
348 match output.datum() {
349 Some(pallas_primitives::conway::PseudoDatumOption::Data(data)) => {
350 row["datum"] = json!(hex::encode(data.raw_cbor()));
351 }
352 Some(pallas_primitives::conway::PseudoDatumOption::Hash(hash)) => {
353 row["datumHash"] = json!(hex::encode(hash));
354 }
355 None => {}
356 }
357 rows.push(row);
358 }
359 Ok(Value::Array(rows))
360}
361
362pub fn declared_covers(tx_cbor: &[u8], answer: &Value) -> Result<std::result::Result<(), String>> {
366 let measured = match crate::build::Budgets::from_ogmios(answer) {
367 Ok(budgets) => budgets,
368 Err(e) => return Ok(Err(e.to_string())),
369 };
370 let tx =
371 MultiEraTx::decode(tx_cbor).map_err(|e| anyhow!("The transaction can't be read: {e}"))?;
372 let rows: Vec<Value> = tx
373 .redeemers()
374 .iter()
375 .map(|r| {
376 let units = r.ex_units();
377 json!({
378 "validator": { "purpose": purpose(&r.tag()), "index": r.index() },
379 "budget": { "memory": units.mem, "cpu": units.steps },
380 })
381 })
382 .collect();
383 let declared = crate::build::Budgets::from_ogmios(&json!({ "result": rows }))?;
384 Ok(if declared.covers(&measured) {
385 Ok(())
386 } else {
387 Err("the network measures its scripts above what the wallet declared".to_string())
388 })
389}