Compare commits

...

2 Commits

Author SHA1 Message Date
waalge 0befe1a40a another stub: cardano value 2025-10-01 14:52:20 +00:00
waalge 2d3e76a894 wip 2025-10-01 14:01:04 +00:00
12 changed files with 329 additions and 443 deletions

View File

@ -2,6 +2,17 @@
> A first stab at the konduit cli
## Workspace
- [ ] cardano-tx-builder - Pure cardano transaction builder utils
- [ ] cardano-connect - Traits for cardano connector
- [ ] cardano-connect-blockfrost - Impl for blockfrost
- [ ] konduit-core - Pure konduit transaction builders.
Depends only on cardano-tx-builder.
- [ ] test round trip data
- [ ] compiles to wasm
- [ ] konduit-cli - cli wrapping of core
## TODOs
- [x] serde for relevant data

View File

@ -7,7 +7,7 @@
use anyhow::{anyhow, Result};
use minicbor::decode;
use crate::{tx::plutus::BuildParams, utils::v2a};
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,
@ -15,15 +15,12 @@ use blockfrost_openapi::models::{
use pallas_addresses::{Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart};
use pallas_codec::{
minicbor as cbor,
utils::{CborWrap, NonEmptyKeyValuePairs},
};
use pallas_primitives::{
conway::{
AssetName, DatumOption, PolicyId, PostAlonzoTransactionOutput, PseudoDatumOption,
ScriptRef, TransactionInput, TransactionOutput, Tx, Value,
},
PlutusScript,
utils::NonEmptyKeyValuePairs,
};
use pallas_primitives::conway::{
AssetName, PolicyId, PostAlonzoTransactionOutput,
TransactionInput, TransactionOutput, Tx, Value,
};
use std::collections::{BTreeMap, HashMap};
use uplc::{tx::ResolvedInput, PlutusData};
@ -147,20 +144,16 @@ impl Blockfrost {
plutus_data_from_inline(&data)
}
pub async fn resolve_datum_option(
pub async fn resolve_datum(
&self,
datum_hash: &Option<String>,
inline_datum: &Option<String>,
) -> Result<Option<DatumOption>> {
) -> Result<Option<DatumOrHash>> {
if let Some(inline_datum) = inline_datum {
Ok(Some(PseudoDatumOption::Data(CborWrap(
plutus_data_from_inline(inline_datum)?,
))))
Ok(Some(DatumOrHash::Data(plutus_data_from_inline(inline_datum)?)))
} else {
if let Some(datum_hash) = datum_hash {
Ok(Some(PseudoDatumOption::Data(CborWrap(
self.plutus_data_from_hash(&datum_hash).await?,
))))
Ok(Some(DatumOrHash::Data(self.plutus_data_from_hash(&datum_hash).await?,)))
} else {
Ok(None)
}
@ -273,7 +266,7 @@ impl Cardano for Blockfrost {
(&pp).into()
}
async fn resolve_many(&self, inputs: Vec<TransactionInput>) -> Vec<ResolvedInput> {
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 {
@ -283,7 +276,7 @@ impl Cardano for Blockfrost {
resolved
}
async fn resolve(&self, input: TransactionInput) -> Result<ResolvedInput> {
async fn resolve(&self, input: TransactionInput) -> Result<Utxo> {
let utxo = self
.api
.transactions_utxos(hex::encode(input.transaction_id).as_str())
@ -299,23 +292,23 @@ impl Cardano for Blockfrost {
output.output_index, input.index as i32,
"somehow resolved the wrong ouput",
);
let datum_option = self
.resolve_datum_option(&output.data_hash, &output.inline_datum)
let datum = self
.resolve_datum(&output.data_hash, &output.inline_datum)
.await
.expect("Something went wrong");
?;
// let script_ref = self.resolve_script(&output.reference_script_hash).await.expect("Something went wrong");
// FIXME!!!!
let script_ref = None;
Ok(ResolvedInput {
Ok(Utxo {
input: input.clone(),
output: TransactionOutput::PostAlonzo(PostAlonzoTransactionOutput {
address: from_bech32(&output.address).into(),
output: Output {
address: Address::from_bech32(&output.address)?,
value: from_tx_content_output_amounts(&output.amount[..]),
datum_option,
datum,
script_ref,
}),
},
})
} else {
Err(anyhow!("No output found"))

View File

@ -2,6 +2,7 @@ use anyhow::Result;
use pallas_addresses::{Network, ShelleyPaymentPart};
use pallas_primitives::conway::Tx;
use uplc::{tx::ResolvedInput, TransactionInput};
use crate::cardano_types::{Utxo};
use crate::tx::plutus::BuildParams;
@ -11,11 +12,11 @@ pub trait Cardano {
fn resolve_many(
&self,
inputs: Vec<TransactionInput>,
) -> impl std::future::Future<Output = Vec<ResolvedInput>> + Send;
) -> impl std::future::Future<Output = Vec<Utxo>> + Send;
fn resolve(
&self,
input: TransactionInput,
) -> impl std::future::Future<Output = Result<ResolvedInput>> + Send;
) -> impl std::future::Future<Output = Result<Utxo>> + Send;
fn transaction_by_hash(
&self,
tx_hash: &str,

114
src/cardano_types.rs Normal file
View File

@ -0,0 +1,114 @@
use pallas_addresses;
use pallas_codec::utils::CborWrap;
use pallas_crypto::hash::Hash;
use pallas_primitives::{
conway::{DatumOption, NativeScript, PostAlonzoTransactionOutput, PseudoScript, PseudoTransactionOutput, Value},
PlutusScript,
};
use uplc::PlutusData;
pub mod address;
use address::Address;
pub mod value;
/// This is a sad file: yet another set of wrapping.
///
/// The pallas types are too confusing.
/// This is surely a consequence of supporting all eras.
/// At least in part.
/// Another part may also be that this support has simply grown,
/// rather than be rationalised.
///
/// Script.
/// There is no support for native scripts,
/// and we only really care about V3.
pub enum Script {
V1(Vec<u8>),
V2(Vec<u8>),
V3(Vec<u8>),
}
impl Into<PseudoScript<NativeScript>> for Script {
fn into(self) -> PseudoScript<NativeScript> {
match self {
Script::V1(items) => PseudoScript::PlutusV1Script(PlutusScript(items.into())),
Script::V2(items) => PseudoScript::PlutusV2Script(PlutusScript(items.into())),
Script::V3(items) => PseudoScript::PlutusV3Script(PlutusScript(items.into())),
}
}
}
impl Into<CborWrap<PseudoScript<NativeScript>>> for Script {
fn into(self) -> CborWrap<PseudoScript<NativeScript>> {
CborWrap(self.into())
}
}
/// Datum
/// We are not being too clever with this.
/// We make use of rust's `Optional` for the no datum case.
/// We are yet to distinguish cases where we need the data,
/// in place of the hash.
/// It should be documented.
pub enum DatumOrHash {
Hash(Hash<32>),// Check this
Data(PlutusData),
}
impl Into<DatumOption> for DatumOrHash {
fn into(self) -> DatumOption {
match self {
DatumOrHash::Hash(hash) => DatumOption::Hash(hash),
DatumOrHash::Data(plutus_data) => DatumOption::Data(CborWrap(plutus_data)),
}
}
}
/// Output
pub struct Output {
pub address: Address,
pub value: Value,
pub datum: Option<DatumOrHash>,
pub script_ref: Option<Script>,
}
impl Into<PostAlonzoTransactionOutput> for Output {
fn into(self) -> PostAlonzoTransactionOutput {
let address : pallas_addresses::ShelleyAddress = self.address.into();
PostAlonzoTransactionOutput {
address: address.to_vec().into(),
value: self.value,
datum_option: self.datum.map(|x| x.into()),
script_ref: self.script_ref.map(|x| x.into()),
}
}
}
impl Into<pallas_primitives::conway::PseudoTransactionOutput<PostAlonzoTransactionOutput>> for Output {
fn into(self) -> PseudoTransactionOutput<PostAlonzoTransactionOutput> {
PseudoTransactionOutput::PostAlonzo(self.into())
}
}
/// Input
/// No change from pallas primitives here.
pub type Input = pallas_primitives::TransactionInput;
/// Utxo
/// Aka resolved input.
pub struct Utxo {
pub input: Input,
pub output: Output,
}
impl Into<uplc::tx::ResolvedInput> for Utxo {
fn into(self) -> uplc::tx::ResolvedInput {
uplc::tx::ResolvedInput {
input: self.input,
output: self.output.into(),
}
}
}

View File

@ -0,0 +1,129 @@
/// Seems like quite a lot of code just to pullout `from_bech32` on address
/// This is mostly extracted from private function of pallas_addresses
use anyhow::{anyhow, Result};
use pallas_addresses::{self, Network};
use pallas_crypto::hash::Hash;
use pallas_primitives::NetworkId;
use crate::utils::v2a;
/// Network
pub fn network_from_id(id: NetworkId) -> Network {
match id {
NetworkId::Testnet => Network::Testnet,
NetworkId::Mainnet => Network::Mainnet,
}
}
fn parse_network(header: u8) -> Network {
let masked = header & 0b0000_1111;
match masked {
0b_0000_0000 => Network::Testnet,
0b_0000_0001 => Network::Mainnet,
_ => Network::Other(masked),
}
}
/// Credential
#[derive(Debug, PartialEq)]
pub enum Credential {
Key(Hash<28>),
Script(Hash<28>),
}
impl Into<pallas_addresses::ShelleyPaymentPart> for Credential {
fn into(self) -> pallas_addresses::ShelleyPaymentPart {
match self {
Credential::Key(hash) => pallas_addresses::ShelleyPaymentPart::Key(hash.into()),
Credential::Script(hash) => pallas_addresses::ShelleyPaymentPart::Script(hash.into()),
}
}
}
impl Into<pallas_addresses::ShelleyDelegationPart> for Credential {
fn into(self) -> pallas_addresses::ShelleyDelegationPart {
match self {
Credential::Key(hash) => pallas_addresses::ShelleyDelegationPart::Key(hash.into()),
Credential::Script(hash) => {
pallas_addresses::ShelleyDelegationPart::Script(hash.into())
}
}
}
}
/// Address
#[derive(Debug)]
pub struct Address {
pub network: Network,
pub payment: Credential,
pub delegation: Option<Credential>,
}
impl Address {
pub fn new(network: Network, payment: Credential, delegation: Option<Credential>) -> Self {
Self { network, payment, delegation }
}
pub fn from_bech32(s : &str) -> Result<Self> {
let (_, bytes) = bech32::decode(s)?;
let x = Address::try_from(bytes)?;
Ok(x)
}
}
impl TryFrom<Vec<u8>> for Address {
fn try_from(bytes : Vec<u8>) -> std::result::Result<Self, Self::Error> {
let header = *bytes.first().expect("Missing bytes");
let network = parse_network(header);
let payment : Hash<28> = v2a(bytes[1..=28].to_vec())?.into();
let delegation = v2a(bytes[28..].to_vec());
match header & 0b1111_0000 {
0b0000_0000 => Ok(Address::new(network, Credential::Key(payment), Some(Credential::Key(delegation?.into())))),
0b0001_0000 => Ok(Address::new(network, Credential::Script(payment),Some(Credential::Key(delegation?.into())))),
0b0010_0000 => Ok(Address::new(network, Credential::Key(payment), Some(Credential::Script(delegation?.into())))),
0b0011_0000 => Ok(Address::new(network, Credential::Script(payment), Some(Credential::Script(delegation?.into())))),
0b0110_0000 => Ok(Address::new(network, Credential::Key(payment), None)),
0b0111_0000 => Ok(Address::new(network, Credential::Script(payment), None)),
_ => Err(anyhow!("Header not supported")),
}
}
type Error = anyhow::Error;
}
impl Into<pallas_addresses::ShelleyAddress> for Address {
fn into(self) -> pallas_addresses::ShelleyAddress {
let network = self.network;
pallas_addresses::ShelleyAddress::new(
network,
self.payment.into(),
self.delegation
.map_or(pallas_addresses::ShelleyDelegationPart::Null, |x| x.into()),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_bech32() {
let testnet = "addr_test1vqpymjsk49r6p9f2f6d3gm04ther0ka7ts8jsv8dukg4nvgcdjxy9";
let addr0 = Address::from_bech32(testnet).unwrap();
let mainnet = "addr1vypymjsk49r6p9f2f6d3gm04ther0ka7ts8jsv8dukg4nvgr9x6tq";
let addr1 = Address::from_bech32(mainnet).unwrap();
assert_eq!(addr0.delegation,addr1.delegation, "oops");
let sa0 : pallas_addresses::ShelleyAddress = addr0.into();
assert_eq!(sa0.to_bech32().unwrap(), testnet, "oops");
let sa1 : pallas_addresses::ShelleyAddress = addr1.into();
assert_eq!(sa1.to_bech32().unwrap(), mainnet, "oops");
}
}

View File

@ -0,0 +1,17 @@
// TODO: Write some sane value handling fuctions:
// - add
// - sub
// - prune
// - split (negative, positive)
// - is_positive
use std::collections::HashMap;
use pallas_crypto::hash::Hash;
/// Naive version of value
pub type MultiAsset = HashMap<Hash<28>, HashMap<Vec<u8>, u64>>;
#[derive(Debug)]
pub struct Value(u64, MultiAsset);

View File

@ -1,5 +1,5 @@
use clap::Args;
use pallas_addresses::Address;
use pallas_addresses::{Address, ShelleyAddress};
use tokio::runtime::Runtime;
use crate::{
@ -20,7 +20,7 @@ pub struct Params {
}
pub fn handle(params: Params) {
let outputs = params
let to = params
.to
.iter()
.map(|x| parse_to(x))
@ -28,7 +28,7 @@ pub fn handle(params: Params) {
let env = get_env();
let tx_ctx = from_env(&env);
let rt = Runtime::new().expect("Failed to create Tokio runtime");
let tx = rt.block_on(async { send(tx_ctx, vec![]).await });
let tx = rt.block_on(async { send(tx_ctx, to).await });
println!("{:?}", tx);
}

View File

@ -2,7 +2,7 @@ pub mod cardano;
pub mod cmd;
pub mod data;
pub mod env;
pub mod pallas_extra;
pub mod tx;
pub mod utils;
pub mod wallet;
pub mod cardano_types;

View File

@ -1,397 +0,0 @@
// 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/.
// SOURCE : github.com/cardanoSolutions/zhuli
use pallas_addresses::{Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart};
use pallas_codec::{
minicbor as cbor,
utils::{NonEmptyKeyValuePairs, NonEmptySet, Set},
};
use pallas_crypto::hash::{Hash, Hasher};
use pallas_primitives::{
conway::{
AssetName, Constr, ExUnits, Language, Multiasset, NetworkId, PlutusData,
PostAlonzoTransactionOutput, PseudoTransactionOutput, RedeemerTag, RedeemersKey,
RedeemersValue, TransactionBody, TransactionInput, TransactionOutput, Tx, Value,
WitnessSet,
},
MaybeIndefArray,
};
use std::{cmp::Ordering, str::FromStr};
use uplc::tx::{eval_phase_two, ResolvedInput, SlotConfig};
#[derive(Debug)]
pub struct BuildParams {
pub fee_constant: u64,
pub fee_coefficient: u64,
pub price_mem: f64,
pub price_steps: f64,
}
pub struct OutputReference(pub TransactionInput);
impl FromStr for OutputReference {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match &s.split('#').collect::<Vec<_>>()[..] {
[tx_id_str, ix_str] => {
let transaction_id: Hash<32> = tx_id_str
.parse()
.map_err(|e| format!("failed to decode transaction id from hex: {e:?}"))?;
let index: u64 = ix_str
.parse()
.map_err(|e| format!("failed to decode output index: {e:?}"))?;
Ok(OutputReference(TransactionInput {
transaction_id,
index,
}))
}
_ => Err("malformed output reference: expected a hex-encode string and an index separated by '#'".to_string()),
}
}
}
pub struct Redeemer {}
impl Redeemer {
pub fn mint(index: u32, data: PlutusData, ex_units: ExUnits) -> (RedeemersKey, RedeemersValue) {
(
RedeemersKey {
tag: RedeemerTag::Mint,
index,
},
RedeemersValue { data, ex_units },
)
}
pub fn spend(
(inputs, target): (&[TransactionInput], &TransactionInput),
data: PlutusData,
ex_units: ExUnits,
) -> (RedeemersKey, RedeemersValue) {
(
RedeemersKey {
tag: RedeemerTag::Spend,
index: inputs
.iter()
.enumerate()
.find(|(_, i)| *i == target)
.unwrap()
.0 as u32,
},
RedeemersValue { data, ex_units },
)
}
pub fn publish(
index: u32,
data: PlutusData,
ex_units: ExUnits,
) -> (RedeemersKey, RedeemersValue) {
(
RedeemersKey {
tag: RedeemerTag::Cert,
index,
},
RedeemersValue { data, ex_units },
)
}
pub fn vote(index: u32, data: PlutusData, ex_units: ExUnits) -> (RedeemersKey, RedeemersValue) {
(
RedeemersKey {
tag: RedeemerTag::Vote,
index,
},
RedeemersValue { data, ex_units },
)
}
}
pub fn void() -> PlutusData {
PlutusData::Constr(Constr {
tag: 121,
any_constructor: None,
fields: MaybeIndefArray::Indef(vec![]),
})
}
pub fn from_network(network: Network) -> NetworkId {
match network {
Network::Mainnet => NetworkId::Mainnet,
_ => NetworkId::Testnet,
}
}
pub fn non_empty_set<T>(set: Vec<T>) -> Option<NonEmptySet<T>>
where
T: std::fmt::Debug,
{
if set.is_empty() {
None
} else {
Some(NonEmptySet::try_from(set).unwrap())
}
}
pub fn non_empty_pairs<K, V>(pairs: Vec<(K, V)>) -> Option<NonEmptyKeyValuePairs<K, V>>
where
V: Clone,
K: Clone,
{
if pairs.is_empty() {
None
} else {
Some(NonEmptyKeyValuePairs::Def(pairs))
}
}
pub fn into_outputs(outputs: Vec<PostAlonzoTransactionOutput>) -> Vec<TransactionOutput> {
outputs
.into_iter()
.map(PseudoTransactionOutput::PostAlonzo)
.collect()
}
pub fn singleton_assets<T: Clone>(
validator_hash: Hash<28>,
assets: &[(AssetName, T)],
) -> Multiasset<T> {
NonEmptyKeyValuePairs::Def(vec![(
validator_hash,
NonEmptyKeyValuePairs::Def(assets.to_vec()),
)])
}
pub fn from_validator(validator: &[u8], network_id: Network) -> (Hash<28>, ShelleyAddress) {
let validator_hash = Hasher::<224>::hash_tagged(validator, 3);
let validator_address = ShelleyAddress::new(
network_id,
ShelleyPaymentPart::script_hash(validator_hash),
ShelleyDelegationPart::script_hash(validator_hash),
);
(validator_hash, validator_address)
}
pub fn value_subtract_lovelace(value: Value, lovelace: u64) -> Option<Value> {
match value {
Value::Coin(total) if total > lovelace => Some(Value::Coin(total - lovelace)),
Value::Multiasset(total, assets) if total > lovelace => {
Some(Value::Multiasset(total - lovelace, assets))
}
_ => None,
}
}
pub fn value_add_lovelace(value: Value, lovelace: u64) -> Value {
match value {
Value::Coin(total) => Value::Coin(total + lovelace),
Value::Multiasset(total, assets) => Value::Multiasset(total + lovelace, assets),
}
}
pub fn lovelace_of(value: &Value) -> u64 {
match value {
Value::Coin(lovelace) | Value::Multiasset(lovelace, _) => *lovelace,
}
}
pub fn new_min_value_output<F>(per_byte: u64, build: F) -> PostAlonzoTransactionOutput
where
F: Fn(u64) -> PostAlonzoTransactionOutput,
{
let value = build(1);
let mut buffer: Vec<u8> = Vec::new();
cbor::encode(&value, &mut buffer).unwrap();
// NOTE: 160 overhead as per the spec + 4 bytes for actual final lovelace value.
// Technically, the final value could need 8 more additional bytes if the resulting
// value was larger than 4_294_967_295 lovelaces, which would realistically never be
// the case.
build((buffer.len() as u64 + 164) * per_byte)
}
pub fn total_execution_cost(params: &BuildParams, redeemers: &[ExUnits]) -> u64 {
redeemers.iter().fold(0, |acc, ex_units| {
acc + ((params.price_mem * ex_units.mem as f64).ceil() as u64)
+ ((params.price_steps * ex_units.steps as f64).ceil() as u64)
})
}
pub fn script_integrity_hash(
redeemers: Option<&NonEmptyKeyValuePairs<RedeemersKey, RedeemersValue>>,
datums: Option<&NonEmptyKeyValuePairs<Hash<32>, PlutusData>>,
language_views: &[(Language, &[i64])],
) -> Option<Hash<32>> {
if redeemers.is_none() && language_views.is_empty() && datums.is_none() {
return None;
}
let mut preimage: Vec<u8> = Vec::new();
if let Some(redeemers) = redeemers {
cbor::encode(redeemers, &mut preimage).unwrap();
}
if let Some(datums) = datums {
cbor::encode(datums, &mut preimage).unwrap();
}
// NOTE: This doesn't work for PlutusV1, but I don't care.
if !language_views.is_empty() {
let mut views = language_views.to_vec();
// TODO: Derive an Ord instance in Pallas.
views.sort_by(|(a, _), (b, _)| match (a, b) {
(Language::PlutusV3, Language::PlutusV3) => Ordering::Equal,
(Language::PlutusV3, _) => Ordering::Greater,
(_, Language::PlutusV3) => Ordering::Less,
(Language::PlutusV2, Language::PlutusV2) => Ordering::Equal,
(Language::PlutusV2, _) => Ordering::Greater,
(_, Language::PlutusV2) => Ordering::Less,
(Language::PlutusV1, Language::PlutusV1) => Ordering::Equal,
});
cbor::encode(NonEmptyKeyValuePairs::Def(views), &mut preimage).unwrap()
}
Some(Hasher::<256>::hash(&preimage))
}
pub fn default_transaction_body() -> TransactionBody {
TransactionBody {
auxiliary_data_hash: None,
certificates: None,
collateral: None,
collateral_return: None,
donation: None,
fee: 0,
inputs: Set::from(vec![]),
mint: None,
network_id: None,
outputs: vec![],
proposal_procedures: None,
reference_inputs: None,
required_signers: None,
script_data_hash: None,
total_collateral: None,
treasury_value: None,
ttl: None,
validity_interval_start: None,
voting_procedures: None,
withdrawals: None,
}
}
pub fn default_witness_set() -> WitnessSet {
WitnessSet {
bootstrap_witness: None,
native_script: None,
plutus_data: None,
plutus_v1_script: None,
plutus_v2_script: None,
plutus_v3_script: None,
redeemer: None,
vkeywitness: None,
}
}
// Build a transaction by repeatedly executing some building logic with different fee and execution
// units settings. Stops when a fixed point is reached. The final transaction has corresponding
// fees and execution units.
pub fn build_transaction<F>(params: &BuildParams, resolved_inputs: &[ResolvedInput], with: F) -> Tx
where
F: Fn(u64, &[ExUnits]) -> Tx,
{
let empty_ex_units = || {
vec![
ExUnits { mem: 0, steps: 0 },
ExUnits { mem: 0, steps: 0 },
ExUnits { mem: 0, steps: 0 },
ExUnits { mem: 0, steps: 0 },
]
};
let mut fee = 0;
let mut ex_units = empty_ex_units();
let mut tx;
let mut attempts = 0;
loop {
tx = with(fee, &ex_units[..]);
// Convert to minted_tx...
let mut serialized_tx = Vec::new();
cbor::encode(&tx, &mut serialized_tx).unwrap();
let mut calculated_ex_units = if resolved_inputs.is_empty() {
empty_ex_units()
} else {
// Compute execution units
let minted_tx = cbor::decode(&serialized_tx).unwrap();
eval_phase_two(
&minted_tx,
resolved_inputs,
None,
None,
&SlotConfig::default(),
false,
|_| (),
)
.expect("script evaluation failed")
.into_iter()
.map(|r| r.0.ex_units)
.collect::<Vec<_>>()
};
calculated_ex_units.extend(empty_ex_units());
attempts += 1;
let estimated_fee = {
// NOTE: This is a best effort to estimate the number of signatories since signatures
// will add an overhead to the fee. Yet, if inputs are locked by native scripts each
// requiring multiple signatories, this will unfortunately fall short.
//
// For similar reasons, it will also over-estimate fees by a small margin for every
// script-locked inputs that do not require signatories.
//
// This is however *acceptable* in our context.
let num_signatories = tx.transaction_body.inputs.len()
+ tx.transaction_body
.required_signers
.as_ref()
.map(|xs| xs.len())
.unwrap_or(0);
params.fee_constant
+ params.fee_coefficient
* (5 + ex_units.len() * 16 + num_signatories * 102 + serialized_tx.len()) as u64
+ total_execution_cost(params, &ex_units)
};
// Check if we've reached a fixed point, or start over.
if fee >= estimated_fee
&& calculated_ex_units
.iter()
.zip(ex_units)
.all(|(l, r)| l.eq(&r))
{
break;
} else if attempts >= 3 {
panic!("failed to build transaction: did not converge after three attempts.");
} else {
ex_units = calculated_ex_units;
fee = estimated_fee;
}
}
tx
}
pub fn expect_post_alonzo(output: &TransactionOutput) -> &PostAlonzoTransactionOutput {
if let TransactionOutput::PostAlonzo(ref o) = output {
o
} else {
panic!("expected PostAlonzo output but got a legacy one.")
}
}

19
src/tx/output.rs Normal file
View File

@ -0,0 +1,19 @@
use pallas_codec::utils::CborWrap;
use pallas_primitives::conway::{DatumOption, NativeScript, PostAlonzoTransactionOutput, PseudoScript, Value};
/// Address needs to be a Vec<u8>
///
pub fn output(
address: Vec<u8>,
value: Value,
datum_option: Option<DatumOption>,
script_ref: PseudoScript<NativeScript>,
) -> PostAlonzoTransactionOutput
{
PostAlonzoTransactionOutput {
address: address.into(),
value,
datum_option,
script_ref : Some(CborWrap(script_ref.into())),
}
}

View File

@ -19,7 +19,7 @@ use super::{
},
};
pub async fn send<T>(ctx: TxContext<T>, outputs: Vec<(Address, u64)>) -> Result<String>
pub async fn send<T>(ctx: TxContext<T>, target: Vec<(Address, u64)>) -> Result<String>
where
T: Cardano,
{
@ -35,15 +35,15 @@ where
.map(|u| u.input.clone())
.collect::<Vec<TransactionInput>>();
let x = expect_post_alonzo(&available_utxos[0].output);
let mut outputs = vec![];
// let mut outputs = outputs.iter().map(|(addr, amount)| {
// TransactionOutput::PostAlonzoTransactionOutput {
// address: addr,
// value: Value::Coin(amount * 1_000_000) ,
// datum_option: None,
// script_ref: None,
// }
// }).collect::<Vec<TransactionOutput>>();
let mut outputs = target
.iter()
.map(|tup| PostAlonzoTransactionOutput {
address: tup.0.to_vec().into(),
value: Value::Coin(tup.1 * 1_000_000),
datum_option: None,
script_ref: None,
})
.collect::<Vec<PostAlonzoTransactionOutput>>();
outputs.push(
// Change
PostAlonzoTransactionOutput {
@ -73,7 +73,7 @@ where
);
ctx.wallet.sign(&mut tx);
let mut serialized_tx = Vec::new();
println!("{:#?}", tx);
println!("{:?}", tx);
encode(&tx, &mut serialized_tx).unwrap();
let tx_hash = ctx.cardano.submit(serialized_tx).await?;
Ok(tx_hash)

View File

@ -1,8 +1,7 @@
use anyhow::{anyhow, Result};
pub fn v2a<T, const N: usize>(v: Vec<T>) -> Result<[T; N]> {
v.try_into()
.map_err(|v: Vec<T>| anyhow!("Expected a Vec of length {}, but got {}", N, v.len()))
<[T;N]>::try_from(v).map_err(|v: Vec<T>| anyhow!("Expected a Vec of length {}, but got {}", N, v.len()))
}
pub fn concat<T: Clone>(l: &[T], r: &[T]) -> Vec<T> {