// ORIGINAL SOURCE : https://github.com/CardanoSolutions/zhuli/blob/main/cli/src/cardano.rs // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. use anyhow::{anyhow, Result}; use minicbor::decode; use crate::{cardano_types::{address::Address, DatumOrHash, Output, Utxo}, tx::plutus::BuildParams, utils::v2a}; use blockfrost::{BlockfrostAPI, Pagination}; use blockfrost_openapi::models::{ asset_history_inner::Action, tx_content_output_amount_inner::TxContentOutputAmountInner, }; use pallas_addresses::{Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart}; use pallas_codec::{ minicbor as cbor, utils::NonEmptyKeyValuePairs, }; use pallas_primitives::conway::{ AssetName, PolicyId, PostAlonzoTransactionOutput, TransactionInput, TransactionOutput, Tx, Value, }; use std::collections::{BTreeMap, HashMap}; use uplc::{tx::ResolvedInput, PlutusData}; use super::cardano::Cardano; pub struct Config { pub project_id: String, } impl Config { pub fn from_env(env: &HashMap) -> Self { let project_id = env .get("project_id") .expect("Blockfrost requires `project_id`") .clone(); Self { project_id } } } pub struct Blockfrost { api: BlockfrostAPI, base_url: String, client: reqwest::Client, network: Network, project_id: String, } const UNIT_LOVELACE: &str = "lovelace"; const MAINNET_PREFIX: &str = "mainnet"; const PREPROD_PREFIX: &str = "preprod"; const PREVIEW_PREFIX: &str = "preview"; #[derive(Debug)] pub struct ProtocolParameters { pub collateral_percent: f64, pub cost_model_v3: Vec, pub drep_deposit: u64, pub fee_constant: u64, pub fee_coefficient: u64, pub min_utxo_deposit_coefficient: u64, pub price_mem: f64, pub price_steps: f64, } impl From<&ProtocolParameters> for BuildParams { fn from(params: &ProtocolParameters) -> BuildParams { BuildParams { fee_constant: params.fee_constant, fee_coefficient: params.fee_coefficient, price_mem: params.price_mem, price_steps: params.price_steps, } } } impl Blockfrost { pub fn new(project_id: String) -> Self { let network_prefix = if project_id.starts_with(MAINNET_PREFIX) { MAINNET_PREFIX.to_string() } else if project_id.starts_with(PREPROD_PREFIX) { PREPROD_PREFIX.to_string() } else if project_id.starts_with(PREVIEW_PREFIX) { PREVIEW_PREFIX.to_string() } else { panic!("unexpected project id prefix") }; let base_url = format!("https://cardano-{}.blockfrost.io/api/v0", network_prefix,); let api = BlockfrostAPI::new(project_id.as_str(), Default::default()); Self { api, base_url, client: reqwest::Client::new(), network: if project_id.starts_with(MAINNET_PREFIX) { Network::Mainnet } else { Network::Testnet }, project_id, } } pub async fn minting(&self, policy_id: &PolicyId, asset_name: &AssetName) -> Vec { let history = self .api .assets_history( &format!("{}{}", hex::encode(policy_id), hex::encode(&asset_name[..])), Pagination::all(), ) .await .ok() .unwrap_or(vec![]) .into_iter() .filter_map(|inner| { if matches!(inner.action, Action::Minted) { Some(inner.tx_hash) } else { None } }) .collect::>(); let mut txs: Vec = vec![]; for tx_hash in history { if let Ok(tx) = self.transaction_by_hash(&tx_hash).await { txs.push(tx) } } txs } pub async fn plutus_data_from_hash(&self, datum_hash: &str) -> Result { let x = self.api.scripts_datum_hash_cbor(datum_hash).await?; let data = x .as_object() .expect("Expect an object") .get("cbor") .expect("Expect key `cbor`") .as_str() .expect("Expect value to be string"); plutus_data_from_inline(&data) } pub async fn resolve_datum( &self, datum_hash: &Option, inline_datum: &Option, ) -> Result> { if let Some(inline_datum) = inline_datum { Ok(Some(DatumOrHash::Data(plutus_data_from_inline(inline_datum)?))) } else { if let Some(datum_hash) = datum_hash { Ok(Some(DatumOrHash::Data(self.plutus_data_from_hash(&datum_hash).await?,))) } else { Ok(None) } } } /// Blockfrost client has the wrong type. pub async fn scripts_hash_cbor(&self, script_hash: &str) -> Result> { let response = self .client .get(&format!("{}/scripts/{}", self.base_url, script_hash)) .header("Accept", "application/json") .header("project_id", self.project_id.as_str()) .send() .await .unwrap(); match response.status() { reqwest::StatusCode::OK => { let ResponseCbor { cbor } = response.json::().await.unwrap(); let bytes = hex::decode(cbor)?; Ok(bytes) } _ => Err(anyhow!("No script found")), } } // /// Blockfrost client has incomplete type // pub async fn script_type(&self, script_hash : &str) -> Result{ // let response = self // .client // .get(&format!( "{}/scripts/{}/cbor", self.base_url, script_hash)) // .header("Accept", "application/json") // .header("project_id", self.project_id.as_str()) // .send() // .await // .unwrap(); // match response.status() { // reqwest::StatusCode::OK => { // let ResponseScript { plutus_type,.. } = response.json::().await.unwrap(); // } // _ => Err(anyhow!("No script found")), // } // } } pub fn plutus_data_from_inline(inline_datum: &str) -> Result { Ok(decode(&hex::decode(inline_datum)?)?) } impl Cardano for Blockfrost { fn network_id(&self) -> Network { self.network } async fn build_parameters(&self) -> BuildParams { let params = self .api .epochs_latest_parameters() .await .expect("failed to fetch protocol parameters"); let pp = ProtocolParameters { collateral_percent: (params .collateral_percent .expect("protocol parameters are missing collateral percent") as f64) / 1e2, // NOTE: Blockfrost returns cost models out of order. They must be ordered by their // "ParamName" according to how Plutus defines it, but they are usually found ordered // by ascending keys, unfortunately. Given that they are unlikely to change anytime // soon, I am going to bundle them as-is. cost_model_v3: vec![ 100788, 420, 1, 1, 1000, 173, 0, 1, 1000, 59957, 4, 1, 11183, 32, 201305, 8356, 4, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 16000, 100, 100, 100, 16000, 100, 94375, 32, 132994, 32, 61462, 4, 72010, 178, 0, 1, 22151, 32, 91189, 769, 4, 2, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 1, 1000, 42921, 4, 2, 24548, 29498, 38, 1, 898148, 27279, 1, 51775, 558, 1, 39184, 1000, 60594, 1, 141895, 32, 83150, 32, 15299, 32, 76049, 1, 13169, 4, 22100, 10, 28999, 74, 1, 28999, 74, 1, 43285, 552, 1, 44749, 541, 1, 33852, 32, 68246, 32, 72362, 32, 7243, 32, 7391, 32, 11546, 32, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 90434, 519, 0, 1, 74433, 32, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 1, 85848, 123203, 7305, -900, 1716, 549, 57, 85848, 0, 1, 955506, 213312, 0, 2, 270652, 22588, 4, 1457325, 64566, 4, 20467, 1, 4, 0, 141992, 32, 100788, 420, 1, 1, 81663, 32, 59498, 32, 20142, 32, 24588, 32, 20744, 32, 25933, 32, 24623, 32, 43053543, 10, 53384111, 14333, 10, 43574283, 26308, 10, 16000, 100, 16000, 100, 962335, 18, 2780678, 6, 442008, 1, 52538055, 3756, 18, 267929, 18, 76433006, 8868, 18, 52948122, 18, 1995836, 36, 3227919, 12, 901022, 1, 166917843, 4307, 36, 284546, 36, 158221314, 26549, 36, 74698472, 36, 333849714, 1, 254006273, 72, 2174038, 72, 2261318, 64571, 4, 207616, 8310, 4, 1293828, 28716, 63, 0, 1, 1006041, 43623, 251, 0, 1, ], drep_deposit: 500_000_000, // NOTE: Missing from Blockfrost fee_constant: params.min_fee_b as u64, fee_coefficient: params.min_fee_a as u64, min_utxo_deposit_coefficient: params .coins_per_utxo_size .expect("protocol parameters are missing min utxo deposit coefficient") .parse() .unwrap(), price_mem: params .price_mem .expect("protocol parameters are missing price mem") as f64, price_steps: params .price_step .expect("protocol parameters are missing price step") as f64, }; (&pp).into() } async fn resolve_many(&self, inputs: Vec) -> Vec { let mut resolved = vec![]; for i in inputs { if let Ok(r) = self.resolve(i).await { resolved.push(r) } } resolved } async fn resolve(&self, input: TransactionInput) -> Result { let utxo = self .api .transactions_utxos(hex::encode(input.transaction_id).as_str()) .await?; if let Some(output) = utxo .outputs .into_iter() .filter(|o| !o.collateral) .nth(input.index as usize) { assert_eq!( output.output_index, input.index as i32, "somehow resolved the wrong ouput", ); let datum = self .resolve_datum(&output.data_hash, &output.inline_datum) .await ?; // let script_ref = self.resolve_script(&output.reference_script_hash).await.expect("Something went wrong"); // FIXME!!!! let script_ref = None; Ok(Utxo { input: input.clone(), output: Output { address: Address::from_bech32(&output.address)?, value: from_tx_content_output_amounts(&output.amount[..]), datum, script_ref, }, }) } else { Err(anyhow!("No output found")) } } async fn transaction_by_hash(&self, tx_hash: &str) -> Result { // NOTE: Not part of the Rust SDK somehow... let response = self .client .get(&format!("{}/txs/{}/cbor", self.base_url, tx_hash)) .header("Accept", "application/json") .header("project_id", self.project_id.as_str()) .send() .await .unwrap(); match response.status() { reqwest::StatusCode::OK => { let ResponseCbor { cbor } = response.json::().await.unwrap(); let bytes = hex::decode(cbor).unwrap(); let tx = cbor::decode(&bytes).unwrap(); Ok(tx) } _ => Err(anyhow!("No tx found")), } } async fn health(&self) -> Result { match self.api.health().await { Ok(x) => Ok(format!("{:?}", x)), Err(y) => Err(y.to_string()), } } async fn utxos_at( &self, payment_credential: &ShelleyPaymentPart, ) -> Result> { let addr = ShelleyAddress::new( self.network_id(), payment_credential.clone(), ShelleyDelegationPart::Null, ); let response = self .api .addresses_utxos(&addr.to_bech32()?, Pagination::all()) .await?; response .iter() .map(|o| { // FIXME: This should pull the datum and script reference let datum_option = None; let script_ref = None; Ok(ResolvedInput { input: TransactionInput { transaction_id: v2a(hex::decode(&o.tx_hash)?)?.into(), index: o.tx_index as u64, }, output: TransactionOutput::PostAlonzo(PostAlonzoTransactionOutput { address: from_bech32(&o.address).into(), value: from_tx_content_output_amounts(&o.amount[..]), datum_option, script_ref, }), }) }) .collect() } async fn submit(&self, tx: Vec) -> Result { let tx_hash = self.api.transactions_submit(tx).await?; Ok(tx_hash) } } #[derive(serde::Serialize, serde::Deserialize, Debug)] struct ResponseCbor { cbor: String, } #[derive(serde::Serialize, serde::Deserialize, Debug)] struct ResponseScript { script_hash: String, #[serde(rename = "type")] plutus_type: String, serialised_size: u64, } fn from_bech32(bech32: &str) -> Vec { bech32::decode(bech32).unwrap().1 } fn from_tx_content_output_amounts(xs: &[TxContentOutputAmountInner]) -> Value { let mut lovelaces = 0; let mut assets = BTreeMap::new(); for asset in xs { let quantity: u64 = asset.quantity.parse().unwrap(); if asset.unit == UNIT_LOVELACE { lovelaces += quantity; } else { let policy_id: PolicyId = asset.unit[0..56].parse().unwrap(); let asset_name: AssetName = hex::decode(&asset.unit[56..]).unwrap().into(); assets .entry(policy_id) .and_modify(|m: &mut BTreeMap| { m.entry(asset_name.clone()) .and_modify(|q| *q += quantity) .or_insert(quantity); }) .or_insert_with(|| BTreeMap::from([(asset_name, quantity)])); } } if assets.is_empty() { Value::Coin(lovelaces) } else { Value::Multiasset( lovelaces, NonEmptyKeyValuePairs::Def( assets .into_iter() .map(|(policy_id, policies)| { ( policy_id, NonEmptyKeyValuePairs::Def( policies .into_iter() .map(|(asset_name, quantity)| { (asset_name, quantity.try_into().unwrap()) }) .collect::>(), ), ) }) .collect::>(), ), ) } }