Compare commits
2 Commits
712ae21615
...
0befe1a40a
Author | SHA1 | Date |
---|---|---|
![]() |
0befe1a40a | |
![]() |
2d3e76a894 |
11
README.md
11
README.md
|
@ -2,6 +2,17 @@
|
||||||
|
|
||||||
> A first stab at the konduit cli
|
> 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
|
## TODOs
|
||||||
|
|
||||||
- [x] serde for relevant data
|
- [x] serde for relevant data
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use minicbor::decode;
|
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::{BlockfrostAPI, Pagination};
|
||||||
use blockfrost_openapi::models::{
|
use blockfrost_openapi::models::{
|
||||||
asset_history_inner::Action, tx_content_output_amount_inner::TxContentOutputAmountInner,
|
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_addresses::{Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart};
|
||||||
use pallas_codec::{
|
use pallas_codec::{
|
||||||
minicbor as cbor,
|
minicbor as cbor,
|
||||||
utils::{CborWrap, NonEmptyKeyValuePairs},
|
utils::NonEmptyKeyValuePairs,
|
||||||
};
|
|
||||||
use pallas_primitives::{
|
|
||||||
conway::{
|
|
||||||
AssetName, DatumOption, PolicyId, PostAlonzoTransactionOutput, PseudoDatumOption,
|
|
||||||
ScriptRef, TransactionInput, TransactionOutput, Tx, Value,
|
|
||||||
},
|
|
||||||
PlutusScript,
|
|
||||||
};
|
};
|
||||||
|
use pallas_primitives::conway::{
|
||||||
|
AssetName, PolicyId, PostAlonzoTransactionOutput,
|
||||||
|
TransactionInput, TransactionOutput, Tx, Value,
|
||||||
|
};
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use uplc::{tx::ResolvedInput, PlutusData};
|
use uplc::{tx::ResolvedInput, PlutusData};
|
||||||
|
|
||||||
|
@ -147,20 +144,16 @@ impl Blockfrost {
|
||||||
plutus_data_from_inline(&data)
|
plutus_data_from_inline(&data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_datum_option(
|
pub async fn resolve_datum(
|
||||||
&self,
|
&self,
|
||||||
datum_hash: &Option<String>,
|
datum_hash: &Option<String>,
|
||||||
inline_datum: &Option<String>,
|
inline_datum: &Option<String>,
|
||||||
) -> Result<Option<DatumOption>> {
|
) -> Result<Option<DatumOrHash>> {
|
||||||
if let Some(inline_datum) = inline_datum {
|
if let Some(inline_datum) = inline_datum {
|
||||||
Ok(Some(PseudoDatumOption::Data(CborWrap(
|
Ok(Some(DatumOrHash::Data(plutus_data_from_inline(inline_datum)?)))
|
||||||
plutus_data_from_inline(inline_datum)?,
|
|
||||||
))))
|
|
||||||
} else {
|
} else {
|
||||||
if let Some(datum_hash) = datum_hash {
|
if let Some(datum_hash) = datum_hash {
|
||||||
Ok(Some(PseudoDatumOption::Data(CborWrap(
|
Ok(Some(DatumOrHash::Data(self.plutus_data_from_hash(&datum_hash).await?,)))
|
||||||
self.plutus_data_from_hash(&datum_hash).await?,
|
|
||||||
))))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
@ -273,7 +266,7 @@ impl Cardano for Blockfrost {
|
||||||
(&pp).into()
|
(&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![];
|
let mut resolved = vec![];
|
||||||
for i in inputs {
|
for i in inputs {
|
||||||
if let Ok(r) = self.resolve(i).await {
|
if let Ok(r) = self.resolve(i).await {
|
||||||
|
@ -283,7 +276,7 @@ impl Cardano for Blockfrost {
|
||||||
resolved
|
resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn resolve(&self, input: TransactionInput) -> Result<ResolvedInput> {
|
async fn resolve(&self, input: TransactionInput) -> Result<Utxo> {
|
||||||
let utxo = self
|
let utxo = self
|
||||||
.api
|
.api
|
||||||
.transactions_utxos(hex::encode(input.transaction_id).as_str())
|
.transactions_utxos(hex::encode(input.transaction_id).as_str())
|
||||||
|
@ -299,23 +292,23 @@ impl Cardano for Blockfrost {
|
||||||
output.output_index, input.index as i32,
|
output.output_index, input.index as i32,
|
||||||
"somehow resolved the wrong ouput",
|
"somehow resolved the wrong ouput",
|
||||||
);
|
);
|
||||||
let datum_option = self
|
let datum = self
|
||||||
.resolve_datum_option(&output.data_hash, &output.inline_datum)
|
.resolve_datum(&output.data_hash, &output.inline_datum)
|
||||||
.await
|
.await
|
||||||
.expect("Something went wrong");
|
?;
|
||||||
// let script_ref = self.resolve_script(&output.reference_script_hash).await.expect("Something went wrong");
|
// let script_ref = self.resolve_script(&output.reference_script_hash).await.expect("Something went wrong");
|
||||||
|
|
||||||
// FIXME!!!!
|
// FIXME!!!!
|
||||||
let script_ref = None;
|
let script_ref = None;
|
||||||
|
|
||||||
Ok(ResolvedInput {
|
Ok(Utxo {
|
||||||
input: input.clone(),
|
input: input.clone(),
|
||||||
output: TransactionOutput::PostAlonzo(PostAlonzoTransactionOutput {
|
output: Output {
|
||||||
address: from_bech32(&output.address).into(),
|
address: Address::from_bech32(&output.address)?,
|
||||||
value: from_tx_content_output_amounts(&output.amount[..]),
|
value: from_tx_content_output_amounts(&output.amount[..]),
|
||||||
datum_option,
|
datum,
|
||||||
script_ref,
|
script_ref,
|
||||||
}),
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("No output found"))
|
Err(anyhow!("No output found"))
|
||||||
|
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
||||||
use pallas_addresses::{Network, ShelleyPaymentPart};
|
use pallas_addresses::{Network, ShelleyPaymentPart};
|
||||||
use pallas_primitives::conway::Tx;
|
use pallas_primitives::conway::Tx;
|
||||||
use uplc::{tx::ResolvedInput, TransactionInput};
|
use uplc::{tx::ResolvedInput, TransactionInput};
|
||||||
|
use crate::cardano_types::{Utxo};
|
||||||
|
|
||||||
use crate::tx::plutus::BuildParams;
|
use crate::tx::plutus::BuildParams;
|
||||||
|
|
||||||
|
@ -11,11 +12,11 @@ pub trait Cardano {
|
||||||
fn resolve_many(
|
fn resolve_many(
|
||||||
&self,
|
&self,
|
||||||
inputs: Vec<TransactionInput>,
|
inputs: Vec<TransactionInput>,
|
||||||
) -> impl std::future::Future<Output = Vec<ResolvedInput>> + Send;
|
) -> impl std::future::Future<Output = Vec<Utxo>> + Send;
|
||||||
fn resolve(
|
fn resolve(
|
||||||
&self,
|
&self,
|
||||||
input: TransactionInput,
|
input: TransactionInput,
|
||||||
) -> impl std::future::Future<Output = Result<ResolvedInput>> + Send;
|
) -> impl std::future::Future<Output = Result<Utxo>> + Send;
|
||||||
fn transaction_by_hash(
|
fn transaction_by_hash(
|
||||||
&self,
|
&self,
|
||||||
tx_hash: &str,
|
tx_hash: &str,
|
||||||
|
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use pallas_addresses::Address;
|
use pallas_addresses::{Address, ShelleyAddress};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -20,7 +20,7 @@ pub struct Params {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(params: Params) {
|
pub fn handle(params: Params) {
|
||||||
let outputs = params
|
let to = params
|
||||||
.to
|
.to
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| parse_to(x))
|
.map(|x| parse_to(x))
|
||||||
|
@ -28,7 +28,7 @@ pub fn handle(params: Params) {
|
||||||
let env = get_env();
|
let env = get_env();
|
||||||
let tx_ctx = from_env(&env);
|
let tx_ctx = from_env(&env);
|
||||||
let rt = Runtime::new().expect("Failed to create Tokio runtime");
|
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);
|
println!("{:?}", tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ pub mod cardano;
|
||||||
pub mod cmd;
|
pub mod cmd;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod pallas_extra;
|
|
||||||
pub mod tx;
|
pub mod tx;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
|
pub mod cardano_types;
|
||||||
|
|
|
@ -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.")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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())),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
where
|
||||||
T: Cardano,
|
T: Cardano,
|
||||||
{
|
{
|
||||||
|
@ -35,15 +35,15 @@ where
|
||||||
.map(|u| u.input.clone())
|
.map(|u| u.input.clone())
|
||||||
.collect::<Vec<TransactionInput>>();
|
.collect::<Vec<TransactionInput>>();
|
||||||
let x = expect_post_alonzo(&available_utxos[0].output);
|
let x = expect_post_alonzo(&available_utxos[0].output);
|
||||||
let mut outputs = vec![];
|
let mut outputs = target
|
||||||
// let mut outputs = outputs.iter().map(|(addr, amount)| {
|
.iter()
|
||||||
// TransactionOutput::PostAlonzoTransactionOutput {
|
.map(|tup| PostAlonzoTransactionOutput {
|
||||||
// address: addr,
|
address: tup.0.to_vec().into(),
|
||||||
// value: Value::Coin(amount * 1_000_000) ,
|
value: Value::Coin(tup.1 * 1_000_000),
|
||||||
// datum_option: None,
|
datum_option: None,
|
||||||
// script_ref: None,
|
script_ref: None,
|
||||||
// }
|
})
|
||||||
// }).collect::<Vec<TransactionOutput>>();
|
.collect::<Vec<PostAlonzoTransactionOutput>>();
|
||||||
outputs.push(
|
outputs.push(
|
||||||
// Change
|
// Change
|
||||||
PostAlonzoTransactionOutput {
|
PostAlonzoTransactionOutput {
|
||||||
|
@ -73,7 +73,7 @@ where
|
||||||
);
|
);
|
||||||
ctx.wallet.sign(&mut tx);
|
ctx.wallet.sign(&mut tx);
|
||||||
let mut serialized_tx = Vec::new();
|
let mut serialized_tx = Vec::new();
|
||||||
println!("{:#?}", tx);
|
println!("{:?}", tx);
|
||||||
encode(&tx, &mut serialized_tx).unwrap();
|
encode(&tx, &mut serialized_tx).unwrap();
|
||||||
let tx_hash = ctx.cardano.submit(serialized_tx).await?;
|
let tx_hash = ctx.cardano.submit(serialized_tx).await?;
|
||||||
Ok(tx_hash)
|
Ok(tx_hash)
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
pub fn v2a<T, const N: usize>(v: Vec<T>) -> Result<[T; N]> {
|
pub fn v2a<T, const N: usize>(v: Vec<T>) -> Result<[T; N]> {
|
||||||
v.try_into()
|
<[T;N]>::try_from(v).map_err(|v: Vec<T>| anyhow!("Expected a Vec of length {}, but got {}", N, v.len()))
|
||||||
.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> {
|
pub fn concat<T: Clone>(l: &[T], r: &[T]) -> Vec<T> {
|
||||||
|
|
Loading…
Reference in New Issue