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

335 lines
12 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 std::collections::HashMap;
use anyhow::{Result, anyhow};
use futures::stream::{self, StreamExt};
use blockfrost::{BlockfrostAPI, Pagination};
use blockfrost_openapi::models::{address_utxo_content_inner::AddressUtxoContentInner, tx_content_output_amount_inner::TxContentOutputAmountInner};
use cardano_connect::CardanoConnect;
use cardano_tx_builder::{
Address, BuildParameters, Credential, Datum, Input, Network, Output, PlutusData, Script, Utxo,
Value,
};
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 BuildParameters {
fn from(params: &ProtocolParameters) -> BuildParameters {
BuildParameters {
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 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<Datum> {
match (inline_datum, datum_hash) {
(None, None) => Ok(Datum::None),
(Some(inline_datum), _) => Ok(Datum::Data(plutus_data_from_inline(inline_datum)?)),
(_, Some(datum_hash)) => {
Ok(Datum::Data(self.plutus_data_from_hash(&datum_hash).await?))
}
}
}
pub async fn resolve_utxo(
&self,
bf_utxo: AddressUtxoContentInner,
) -> Result<Utxo> {
let datum = self
.resolve_datum(&bf_utxo.data_hash, &bf_utxo.inline_datum)
.await?;
let script_ref = match &bf_utxo.reference_script_hash {
None => None,
Some(hash) => Some(self.resolve_script(&hash).await?),
};
Ok(Utxo {
input: Input {
transaction_id: v2a(hex::decode(&bf_utxo.tx_hash)?)?.into(),
index: bf_utxo.tx_index as u64,
},
output: Output {
address: Address::from_bech32(&bf_utxo.address)?,
value: from_tx_content_output_amounts(&bf_utxo.amount[..])?,
datum,
script_ref,
},
})
}
/// Blockfrost client has the wrong type.
pub async fn resolve_script(&self, script_hash: &str) -> Result<Script> {
let plutus_version = self.plutus_version(script_hash);
let bytes = self.scripts_hash_cbor(script_hash);
match plutus_version.await? {
1 => Ok(Script::V1(bytes.await?)),
2 => Ok(Script::V2(bytes.await?)),
3 => Ok(Script::V3(bytes.await?)),
_ => Err(anyhow!("Unknown script")),
}
}
/// 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 plutus_version(&self, script_hash: &str) -> Result<u8> {
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();
match plutus_type.as_str() {
"plutusV1" => Ok(1),
"plutusV2" => Ok(2),
"plutusV3" => Ok(3),
"plutusV4" => Ok(4),
_ => Err(anyhow!("Unknown plutus version")),
}
}
_ => Err(anyhow!("No script found")),
}
}
}
impl CardanoConnect for Blockfrost {
fn network(&self) -> Network {
self.network
}
async fn health(&self) -> Result<String> {
Ok(format!("{:?}", self.api.health().await?))
}
async fn build_parameters(&self) -> BuildParameters {
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 utxos_at(
&self,
payment: &Credential,
delegation: &Option<Credential>,
) -> Result<Vec<Utxo>> {
let addr = Address::new(self.network(), payment.clone(), delegation.clone());
let response = self
.api
.addresses_utxos(&addr.to_bech32(), Pagination::all())
.await?;
let s = stream::iter(response)
.map(move |bf_utxo| self.resolve_utxo(bf_utxo))
.buffer_unordered(10)
.collect::<Vec<Result<Utxo>>>()
.await;
s.into_iter().collect::<Result<Vec<Utxo>>>()
}
async fn submit(&self, tx: Vec<u8>) -> Result<String> {
Ok(self.api.transactions_submit(tx).await?)
}
}
#[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_tx_content_output_amounts(xs: &[TxContentOutputAmountInner]) -> Result<Value> {
let mut v = Value::zero();
for asset in xs {
let amount: i128 = asset.quantity.parse()?;
if asset.unit == UNIT_LOVELACE {
v.add_lovelace(amount);
} else {
let hash: [u8; 28] = v2a(hex::decode(&asset.unit[0..56])?)?;
let name: Vec<u8> = hex::decode(&asset.unit[56..])?;
v.add_asset(hash, name, amount);
}
}
Ok(v)
}
pub fn plutus_data_from_inline(inline_datum: &str) -> Result<PlutusData> {
Ok(minicbor::decode(&hex::decode(inline_datum)?)?)
}
/// Handles the map error
fn v2a<T, const N: usize>(v: Vec<T>) -> Result<[T; N]> {
<[T; N]>::try_from(v)
.map_err(|v: Vec<T>| anyhow!("Expected a Vec of length {}, but got {}", N, v.len()))
}