452 lines
16 KiB
Rust
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<_>>(),
|
|
),
|
|
)
|
|
}
|
|
}
|