kon-cli/crates/cardano-connect/cardano/blockfrost.rs

452 lines
16 KiB
Rust

// 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<String, String>) -> 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<i64>,
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<Tx> {
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::<Vec<_>>();
let mut txs: Vec<Tx> = 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<PlutusData> {
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<String>,
inline_datum: &Option<String>,
) -> Result<Option<DatumOrHash>> {
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<Vec<u8>> {
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::<ResponseCbor>().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<PlutusVersion>{
// 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::<ResponseScript>().await.unwrap();
// }
// _ => Err(anyhow!("No script found")),
// }
// }
}
pub fn plutus_data_from_inline(inline_datum: &str) -> Result<PlutusData> {
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<TransactionInput>) -> Vec<Utxo> {
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<Utxo> {
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<Tx> {
// 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::<ResponseCbor>().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<String, String> {
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<Vec<ResolvedInput>> {
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<u8>) -> Result<String> {
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<u8> {
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<AssetName, u64>| {
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::<Vec<_>>(),
),
)
})
.collect::<Vec<_>>(),
),
)
}
}