335 lines
12 KiB
Rust
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()))
|
|
}
|