init
This commit is contained in:
commit
3ce9bc6019
|
@ -0,0 +1,16 @@
|
|||
db/
|
||||
data/
|
||||
tmp/
|
||||
|
||||
result
|
||||
|
||||
secrets/
|
||||
.direnv/
|
||||
target/
|
||||
|
||||
.env
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "konduit-cli"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
rust-version = "1.86.0"
|
||||
# license = "MIT" -- set to apache
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
bech32 = "0.11.0"
|
||||
blockfrost = "1.1.0"
|
||||
blockfrost-openapi = "0.1.75"
|
||||
clap = { version = "4.5.18", features = ["cargo", "derive"] }
|
||||
cryptoxide = "0.5.1"
|
||||
hex = "0.4.3"
|
||||
minicbor = { version = "0.25.1", features = ["alloc", "derive"] }
|
||||
pallas-addresses = "0.33.0"
|
||||
pallas-crypto = "0.33.0"
|
||||
pallas-primitives = "0.33.0"
|
||||
rand = "0.9.2"
|
||||
reqwest = { version = "0.12.23", features = ["json"] }
|
||||
serde = { version = "1.0.213", features = ["derive"] }
|
||||
serde_json = "1.0.138"
|
||||
tokio = { version = "1.47.1", features = ["full"] }
|
||||
uplc = "1.1.19"
|
|
@ -0,0 +1,49 @@
|
|||
# Konduit cli
|
||||
|
||||
> A first stab at the konduit cli
|
||||
|
||||
## TODOs
|
||||
|
||||
- [x] serde for relevant data
|
||||
- [ ] env
|
||||
- [ ] wallet keys
|
||||
- [ ] cardano connection
|
||||
- [ ] txs
|
||||
- [ ] dev
|
||||
- [ ] send
|
||||
- [ ] publish
|
||||
- [ ] unpublish
|
||||
- [ ] open
|
||||
- [ ] cli params TODO
|
||||
- [ ] fn params:
|
||||
- fuel (available utxos)
|
||||
- change address
|
||||
- konduit address :
|
||||
- `konduit_hash`
|
||||
- maybe stake key
|
||||
- amount (currency is always ada)
|
||||
- datum:
|
||||
- constants`(tag, add_vkey, sub_vkey, respond_period)`
|
||||
- [ ] add
|
||||
- [ ] sub
|
||||
- [ ] cli params TODO
|
||||
- [ ] fn args:
|
||||
- generic script args: fuel, change address, script ref ...
|
||||
- instance input (resolved)
|
||||
- redeemer: receipt = (squash, [cheques])
|
||||
- [ ] close
|
||||
- [ ] respond
|
||||
- [ ] unlock
|
||||
- [ ] expire
|
||||
- [ ] end
|
||||
- [ ] elapse
|
||||
- [ ] batch
|
||||
- [ ] mutual
|
||||
- [ ] cardano connection
|
||||
- [ ] api
|
||||
- [ ] utxos at (with optional stake key)
|
||||
- implementations:
|
||||
- [ ] blockfrost
|
||||
- [ ] kupmios
|
||||
- [ ] env handling
|
||||
- [ ] cmd
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1757068644,
|
||||
"narHash": "sha256-NOrUtIhTkIIumj1E/Rsv1J37Yi3xGStISEo8tZm3KW4=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8eb28adfa3dc4de28e792e3bf49fcf9007ca8ac9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1757298987,
|
||||
"narHash": "sha256-yuFSw6fpfjPtVMmym51ozHYpJQ7SzVOTkk7tUv2JA0U=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "cfd63776bde44438ff2936f0c9194c79dd407a5f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
rust-overlay,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [rust-overlay.overlays.default];
|
||||
};
|
||||
|
||||
osxDependencies = with pkgs;
|
||||
lib.optionals stdenv.isDarwin
|
||||
[
|
||||
darwin.apple_sdk.frameworks.Security
|
||||
darwin.apple_sdk.frameworks.CoreServices
|
||||
darwin.apple_sdk.frameworks.SystemConfiguration
|
||||
];
|
||||
|
||||
cargoTomlContents = builtins.readFile ./Cargo.toml;
|
||||
|
||||
version = (builtins.fromTOML cargoTomlContents).package.version;
|
||||
rustVersion = (builtins.fromTOML cargoTomlContents).package."rust-version";
|
||||
|
||||
rustToolchain = pkgs.rust-bin.stable.${rustVersion}.default;
|
||||
|
||||
rustPlatform = pkgs.makeRustPlatform {
|
||||
cargo = rustToolchain;
|
||||
rustc = rustToolchain;
|
||||
};
|
||||
|
||||
my-crate = rustPlatform.buildRustPackage {
|
||||
inherit version;
|
||||
|
||||
name = "my-crate";
|
||||
|
||||
buildInputs = with pkgs; [openssl] ++ osxDependencies;
|
||||
nativeBuildInputs = with pkgs; [pkg-config openssl.dev];
|
||||
|
||||
src = pkgs.lib.cleanSourceWith {src = self;};
|
||||
doCheck = false; # don’t run cargo test
|
||||
CARGO_BUILD_TESTS = "false"; # don’t even compile test binaries
|
||||
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
|
||||
GIT_COMMIT_HASH_SHORT = self.shortRev or "unknown";
|
||||
};
|
||||
|
||||
packages = {
|
||||
my-crate = my-crate;
|
||||
default = packages.my-crate;
|
||||
};
|
||||
|
||||
overlays.default = final: prev: {my-crate = packages.my-crate;};
|
||||
|
||||
gitRev =
|
||||
if (builtins.hasAttr "rev" self)
|
||||
then self.rev
|
||||
else "dirty";
|
||||
in {
|
||||
inherit packages overlays;
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs;
|
||||
[
|
||||
pkg-config
|
||||
openssl
|
||||
cargo-insta
|
||||
(rustToolchain.override {
|
||||
extensions = ["rust-src" "clippy" "rustfmt" "rust-analyzer"];
|
||||
})
|
||||
]
|
||||
++ osxDependencies;
|
||||
|
||||
shellHook = ''
|
||||
export GIT_REVISION=${gitRev}
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use cardano::Cardano;
|
||||
|
||||
pub mod blockfrost;
|
||||
pub mod cardano;
|
||||
|
||||
const PREFIX: &str = "cardano_";
|
||||
|
||||
pub fn from_env(env: &HashMap<String, String>) -> impl Cardano {
|
||||
let cardano_env: HashMap<String, String> = env
|
||||
.iter()
|
||||
.filter_map(|(k, v)| k.strip_prefix(PREFIX).map(|k| (k.to_string(), v.clone())))
|
||||
.collect();
|
||||
match env.get("cardano") {
|
||||
None => panic!("Expect cardano connection details in env"),
|
||||
Some(s) if s == "blockfrost" => {
|
||||
let config = blockfrost::Config::from_env(&cardano_env);
|
||||
blockfrost::Blockfrost::new(config.project_id)
|
||||
}
|
||||
Some(_s) => panic!("Unkown cardano connection"),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,458 @@
|
|||
// 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::{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::{CborWrap, NonEmptyKeyValuePairs},
|
||||
};
|
||||
use pallas_primitives::{
|
||||
conway::{
|
||||
AssetName, DatumOption, PolicyId, PostAlonzoTransactionOutput, PseudoDatumOption,
|
||||
ScriptRef, TransactionInput, TransactionOutput, Tx, Value,
|
||||
},
|
||||
PlutusScript,
|
||||
};
|
||||
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_option(
|
||||
&self,
|
||||
datum_hash: &Option<String>,
|
||||
inline_datum: &Option<String>,
|
||||
) -> Result<Option<DatumOption>> {
|
||||
if let Some(inline_datum) = inline_datum {
|
||||
Ok(Some(PseudoDatumOption::Data(CborWrap(
|
||||
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?,
|
||||
))))
|
||||
} 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<ResolvedInput> {
|
||||
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<ResolvedInput> {
|
||||
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_option = self
|
||||
.resolve_datum_option(&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 {
|
||||
input: input.clone(),
|
||||
output: TransactionOutput::PostAlonzo(PostAlonzoTransactionOutput {
|
||||
address: from_bech32(&output.address).into(),
|
||||
value: from_tx_content_output_amounts(&output.amount[..]),
|
||||
datum_option,
|
||||
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<_>>(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
use anyhow::Result;
|
||||
use pallas_addresses::{Network, ShelleyPaymentPart};
|
||||
use pallas_primitives::conway::Tx;
|
||||
use uplc::{tx::ResolvedInput, TransactionInput};
|
||||
|
||||
use crate::tx::plutus::BuildParams;
|
||||
|
||||
pub trait Cardano {
|
||||
fn network_id(&self) -> Network;
|
||||
fn build_parameters(&self) -> impl std::future::Future<Output = BuildParams> + Send;
|
||||
fn resolve_many(
|
||||
&self,
|
||||
inputs: Vec<TransactionInput>,
|
||||
) -> impl std::future::Future<Output = Vec<ResolvedInput>> + Send;
|
||||
fn resolve(
|
||||
&self,
|
||||
input: TransactionInput,
|
||||
) -> impl std::future::Future<Output = Result<ResolvedInput>> + Send;
|
||||
fn transaction_by_hash(
|
||||
&self,
|
||||
tx_hash: &str,
|
||||
) -> impl std::future::Future<Output = Result<Tx>> + Send;
|
||||
/// This should return all utxos available to be spent,
|
||||
/// ie regardelss of the staking part.
|
||||
fn utxos_at(
|
||||
&self,
|
||||
payment_credential: &ShelleyPaymentPart,
|
||||
) -> impl std::future::Future<Output = Result<Vec<ResolvedInput>>> + Send;
|
||||
fn health(&self) -> impl std::future::Future<Output = Result<String, String>> + Send;
|
||||
fn submit(&self, tx: Vec<u8>) -> impl std::future::Future<Output = Result<String>> + Send;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
mod cardano;
|
||||
mod data;
|
||||
mod tx;
|
||||
mod wallet;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Cmd {
|
||||
/// Txs
|
||||
#[command(subcommand)]
|
||||
Tx(tx::Cmd),
|
||||
#[command(subcommand)]
|
||||
Data(data::Cmd),
|
||||
#[command(subcommand)]
|
||||
Cardano(cardano::Cmd),
|
||||
#[command(subcommand)]
|
||||
Wallet(wallet::Cmd),
|
||||
}
|
||||
|
||||
pub fn handle(cmd: Cmd) {
|
||||
match cmd {
|
||||
Cmd::Tx(inner) => tx::handle(inner),
|
||||
Cmd::Data(inner) => data::handle(inner),
|
||||
Cmd::Cardano(inner) => cardano::handle(inner),
|
||||
Cmd::Wallet(inner) => wallet::handle(inner),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
use clap::Subcommand;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::cardano::{cardano::Cardano, from_env};
|
||||
use crate::env::get_env;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
/// Cardano api
|
||||
pub enum Cmd {
|
||||
/// Health
|
||||
Health,
|
||||
}
|
||||
|
||||
pub fn handle(cmd: Cmd) {
|
||||
match cmd {
|
||||
Cmd::Health => {
|
||||
let env = get_env();
|
||||
let conn = from_env(&env);
|
||||
let rt = Runtime::new().expect("Failed to create Tokio runtime");
|
||||
rt.block_on(async {
|
||||
println!("{:?}", conn.health().await);
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
use crate::data::{
|
||||
base::{Amount, Hash28, Tag, TimeDelta, VKey},
|
||||
constants::Constants,
|
||||
datum::Datum,
|
||||
plutus::to_cbor,
|
||||
stage::Stage,
|
||||
};
|
||||
|
||||
#[derive(Subcommand)]
|
||||
/// Data
|
||||
pub enum Cmd {
|
||||
/// Make an example (serialised) datum
|
||||
Datum,
|
||||
/// other
|
||||
Other,
|
||||
}
|
||||
|
||||
pub fn handle(cmd: Cmd) {
|
||||
match cmd {
|
||||
Cmd::Datum => handle_datum(),
|
||||
_ => println!("Not yet implemented"),
|
||||
};
|
||||
}
|
||||
|
||||
fn handle_datum() {
|
||||
let own_hash = Hash28([0; 28]);
|
||||
let tag = Tag([1; 8].to_vec());
|
||||
let add_vkey = VKey([2; 32]);
|
||||
let sub_vkey = VKey([3; 32]);
|
||||
let close_period = TimeDelta(86_400_000);
|
||||
let constants = Constants {
|
||||
tag,
|
||||
add_vkey,
|
||||
sub_vkey,
|
||||
close_period,
|
||||
};
|
||||
let subbed = Amount(0x4444444444);
|
||||
let stage = Stage::Opened(subbed);
|
||||
let datum = Datum {
|
||||
own_hash,
|
||||
constants,
|
||||
stage,
|
||||
};
|
||||
println!("{}", hex::encode(to_cbor(&datum).unwrap()))
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
mod open;
|
||||
mod send;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
/// Txs
|
||||
pub enum Cmd {
|
||||
/// Send
|
||||
Send(send::Params),
|
||||
/// Open
|
||||
Open(open::Params),
|
||||
/// Add
|
||||
Add,
|
||||
/// Sub
|
||||
Sub,
|
||||
/// Close
|
||||
Close,
|
||||
}
|
||||
|
||||
pub fn handle(cmd: Cmd) {
|
||||
match cmd {
|
||||
Cmd::Open(params) => open::handle(params),
|
||||
Cmd::Send(params) => send::handle(params),
|
||||
Cmd::Close => println!("Do Close"),
|
||||
_ => todo!(),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
use clap::Args;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct Params {
|
||||
/// The channel tag
|
||||
#[arg(long, default_value = "hello")]
|
||||
tag: String,
|
||||
/// The adaptors vkey
|
||||
#[arg(long, default_value = "")]
|
||||
adaptor: String,
|
||||
/// Amount (in ada) to put in the channel
|
||||
#[arg(long, default_value = "10")]
|
||||
amount: usize,
|
||||
/// Minimum time (in milliseconds) from `close` to `elapse`
|
||||
#[arg(long, default_value = "86_400_000")]
|
||||
close_period: usize,
|
||||
}
|
||||
|
||||
pub fn handle(params: Params) {
|
||||
println!("{:?}", params);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
use clap::Args;
|
||||
use pallas_addresses::Address;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::{
|
||||
cardano::cardano::Cardano,
|
||||
env::get_env,
|
||||
tx::{
|
||||
context::TxContext,
|
||||
from_env,
|
||||
send::{self, send},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct Params {
|
||||
/// `<who>:<amount>` It can be used multiple times
|
||||
#[arg(long)]
|
||||
to: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn handle(params: Params) {
|
||||
let outputs = params
|
||||
.to
|
||||
.iter()
|
||||
.map(|x| parse_to(x))
|
||||
.collect::<Vec<(Address, u64)>>();
|
||||
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 });
|
||||
println!("{:?}", tx);
|
||||
}
|
||||
|
||||
pub fn parse_to(to: &str) -> (Address, u64) {
|
||||
let (a, b) = to.split_once(":").expect("Expect `<who>:<amount>`");
|
||||
let addr = Address::from_bech32(a).expect("Cannot parse address");
|
||||
let amount = b.parse::<u64>().expect("Cannot parse amount");
|
||||
(addr, amount)
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
use clap::Subcommand;
|
||||
use pallas_addresses::{
|
||||
Address, Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart,
|
||||
};
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::cardano;
|
||||
use crate::cardano::cardano::Cardano;
|
||||
use crate::env::get_env;
|
||||
use crate::wallet::{from_env, generate, Wallet};
|
||||
|
||||
#[derive(Subcommand)]
|
||||
/// Wallet Api
|
||||
pub enum Cmd {
|
||||
/// Gen new skey
|
||||
Gen,
|
||||
/// Show
|
||||
Show,
|
||||
/// Utxos at address. Requires `Cardano` connection.
|
||||
Utxos,
|
||||
}
|
||||
|
||||
pub fn handle(cmd: Cmd) {
|
||||
match cmd {
|
||||
Cmd::Gen => {
|
||||
println!("KONDUIT_WALLET_KEY={}", hex::encode(generate()));
|
||||
}
|
||||
Cmd::Show => {
|
||||
let env = get_env();
|
||||
let w = from_env(&env);
|
||||
let payment_cred = ShelleyPaymentPart::Key(w.key_hash());
|
||||
let stake_cred = ShelleyDelegationPart::Null;
|
||||
let addr_main =
|
||||
ShelleyAddress::new(Network::Mainnet, payment_cred.clone(), stake_cred.clone());
|
||||
let addr_test =
|
||||
ShelleyAddress::new(Network::Testnet, payment_cred.clone(), stake_cred.clone());
|
||||
println!("VKEY={}", hex::encode(w.vkey()));
|
||||
println!("PAYMENT_CRED={:?}", payment_cred.to_bech32());
|
||||
println!("ADDRESS_MAINNET={:?}", addr_main.to_bech32());
|
||||
println!("ADDRESS_TESTNET={:?}", addr_test.to_bech32());
|
||||
}
|
||||
Cmd::Utxos => {
|
||||
let env = get_env();
|
||||
let w = from_env(&env);
|
||||
let payment_cred = w.payment_credential();
|
||||
let stake_cred = ShelleyDelegationPart::Null;
|
||||
let conn = cardano::from_env(&env);
|
||||
let rt = Runtime::new().expect("Failed to create Tokio runtime");
|
||||
rt.block_on(async {
|
||||
let addr = ShelleyAddress::new(
|
||||
conn.network_id(),
|
||||
payment_cred.clone(),
|
||||
stake_cred.clone(),
|
||||
);
|
||||
println!("{:?}", conn.utxos_at(&w.payment_credential()).await);
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
pub mod base;
|
||||
pub mod cheque_body;
|
||||
pub mod constants;
|
||||
pub mod datum;
|
||||
pub mod mix;
|
||||
pub mod pend_cheque;
|
||||
pub mod plutus;
|
||||
pub mod redeemer;
|
||||
pub mod squash;
|
||||
pub mod stage;
|
||||
pub mod step;
|
||||
pub mod unlocked;
|
|
@ -0,0 +1,16 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
/// Used for variables that are roughly constant per "instance"
|
||||
/// These include signing keys and cardano connection
|
||||
|
||||
const PREFIX: &str = "KONDUIT_";
|
||||
|
||||
pub fn get_env() -> HashMap<String, String> {
|
||||
let mut env = HashMap::new();
|
||||
for (key, value) in std::env::vars() {
|
||||
if let Some(key) = key.strip_prefix(PREFIX) {
|
||||
env.insert(key.to_lowercase(), value);
|
||||
};
|
||||
}
|
||||
env
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
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;
|
|
@ -0,0 +1,18 @@
|
|||
use clap::Parser;
|
||||
|
||||
use kon_gen::cmd;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(arg_required_else_help(true), version, about)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
cmd: Option<cmd::Cmd>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
match args.cmd {
|
||||
Some(cmd) => cmd::handle(cmd),
|
||||
None => println!("See help"),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,397 @@
|
|||
// 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,18 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::cardano;
|
||||
use crate::cardano::cardano::Cardano;
|
||||
use crate::wallet;
|
||||
|
||||
pub mod context;
|
||||
mod open;
|
||||
pub mod plutus;
|
||||
pub mod send;
|
||||
mod torso;
|
||||
|
||||
pub fn from_env(env: &HashMap<String, String>) -> context::TxContext<impl Cardano> {
|
||||
context::TxContext {
|
||||
cardano: cardano::from_env(env),
|
||||
wallet: wallet::from_env(env),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
use crate::{cardano::cardano::Cardano, wallet::Wallet};
|
||||
use anyhow::Result;
|
||||
use pallas_addresses::{ShelleyAddress, ShelleyDelegationPart};
|
||||
use uplc::tx::ResolvedInput;
|
||||
|
||||
use super::plutus::BuildParams;
|
||||
|
||||
pub struct TxContext<CardanoT> {
|
||||
pub cardano: CardanoT,
|
||||
pub wallet: Wallet,
|
||||
}
|
||||
|
||||
impl<T: Cardano> TxContext<T> {
|
||||
pub async fn build_parameters(&self) -> BuildParams {
|
||||
self.cardano.build_parameters().await
|
||||
}
|
||||
|
||||
pub async fn available_utxos(&self) -> Result<Vec<ResolvedInput>> {
|
||||
self.cardano
|
||||
.utxos_at(&self.wallet.payment_credential())
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn wallet_address(&self) -> ShelleyAddress {
|
||||
ShelleyAddress::new(
|
||||
self.cardano.network_id(),
|
||||
self.wallet.payment_credential(),
|
||||
ShelleyDelegationPart::Null,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
// pub struct Instance {
|
||||
// cardano: Cardano,
|
||||
// contract_reference : OutputReference,
|
||||
// wallet_key: SigningKey,
|
||||
// }
|
||||
|
||||
// pub async fn open(
|
||||
// instance
|
||||
// network: Cardano,
|
||||
// delegates: Vec<Hash<28>>,
|
||||
// choice: Vote,
|
||||
// anchor: Option<Anchor>,
|
||||
// proposal_id: GovActionId,
|
||||
// OutputReference(contract): OutputReference,
|
||||
// OutputReference(collateral): OutputReference,
|
||||
// ) -> Tx {
|
||||
// let (validator, validator_hash, _) =
|
||||
// recover_validator(&network, &contract.transaction_id).await;
|
||||
//
|
||||
// let params = network.protocol_parameters().await;
|
||||
//
|
||||
// let resolved_inputs = network.resolve_many(&[&collateral, &contract]).await;
|
||||
// let fuel_output = expect_post_alonzo(&resolved_inputs[0].output);
|
||||
// let contract_output = expect_post_alonzo(&resolved_inputs[1].output);
|
||||
//
|
||||
// let (rules, _) = recover_rules(&network, &validator_hash, &contract_output.value).await;
|
||||
//
|
||||
// build_transaction(
|
||||
// &BuildParams::from(¶ms),
|
||||
// &resolved_inputs[..],
|
||||
// |fee, ex_units| {
|
||||
// let mut redeemers = vec![];
|
||||
//
|
||||
// let inputs = vec![collateral.clone()];
|
||||
//
|
||||
// let reference_inputs = vec![contract.clone()];
|
||||
//
|
||||
// let outputs = vec![
|
||||
// // Change
|
||||
// PostAlonzoTransactionOutput {
|
||||
// address: fuel_output.address.clone(),
|
||||
// value: value_subtract_lovelace(fuel_output.value.clone(), fee)
|
||||
// .expect("not enough fuel"),
|
||||
// datum_option: None,
|
||||
// script_ref: None,
|
||||
// },
|
||||
// ];
|
||||
//
|
||||
// let total_collateral = (fee as f64 * params.collateral_percent).ceil() as u64;
|
||||
//
|
||||
// let collateral_return = PostAlonzoTransactionOutput {
|
||||
// address: fuel_output.address.clone(),
|
||||
// value: value_subtract_lovelace(fuel_output.value.clone(), total_collateral)
|
||||
// .expect("not enough fuel"),
|
||||
// datum_option: None,
|
||||
// script_ref: None,
|
||||
// };
|
||||
//
|
||||
// let votes = vec![(
|
||||
// Voter::DRepScript(validator_hash),
|
||||
// NonEmptyKeyValuePairs::Def(vec![(
|
||||
// proposal_id.clone(),
|
||||
// VotingProcedure {
|
||||
// vote: choice.clone(),
|
||||
// anchor: anchor.clone().map(Nullable::Some).unwrap_or(Nullable::Null),
|
||||
// },
|
||||
// )]),
|
||||
// )];
|
||||
// redeemers.push(Redeemer::vote(0, rules.clone(), ex_units[0]));
|
||||
//
|
||||
// // ----- Put it all together
|
||||
// let redeemers = non_empty_pairs(redeemers).unwrap();
|
||||
// Tx {
|
||||
// transaction_body: TransactionBody {
|
||||
// inputs: Set::from(inputs),
|
||||
// reference_inputs: non_empty_set(reference_inputs),
|
||||
// network_id: Some(from_network(network.network_id())),
|
||||
// outputs: into_outputs(outputs),
|
||||
// voting_procedures: non_empty_pairs(votes),
|
||||
// fee,
|
||||
// collateral: non_empty_set(vec![fuel.clone()]),
|
||||
// collateral_return: Some(PseudoTransactionOutput::PostAlonzo(collateral_return)),
|
||||
// total_collateral: Some(total_collateral),
|
||||
// required_signers: non_empty_set(delegates.clone()),
|
||||
// script_data_hash: Some(
|
||||
// script_integrity_hash(
|
||||
// Some(&redeemers),
|
||||
// None,
|
||||
// &[(Language::PlutusV3, ¶ms.cost_model_v3[..])],
|
||||
// )
|
||||
// .unwrap(),
|
||||
// ),
|
||||
// ..default_transaction_body()
|
||||
// },
|
||||
// transaction_witness_set: WitnessSet {
|
||||
// redeemer: Some(redeemers.into()),
|
||||
// plutus_v3_script: non_empty_set(vec![PlutusScript::<3>(validator.clone())]),
|
||||
// ..default_witness_set()
|
||||
// },
|
||||
// success: true,
|
||||
// auxiliary_data: Nullable::Null,
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// }
|
|
@ -0,0 +1,399 @@
|
|||
// 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<_>>()
|
||||
};
|
||||
|
||||
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 // cbor overhead
|
||||
//+ 16 * ex_units.len() // Redeemers. probably not needed.
|
||||
+ 102 * num_signatories // Reference?!
|
||||
+ serialized_tx.len()) as u64
|
||||
+ total_execution_cost(params, &ex_units)
|
||||
};
|
||||
|
||||
calculated_ex_units.extend(empty_ex_units());
|
||||
attempts += 1;
|
||||
|
||||
// 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,80 @@
|
|||
use anyhow::Result;
|
||||
use minicbor::encode;
|
||||
use pallas_addresses::{Address, ShelleyAddress, ShelleyDelegationPart};
|
||||
use pallas_primitives::{
|
||||
conway::{
|
||||
PostAlonzoTransactionOutput, TransactionBody, TransactionInput, TransactionOutput, Tx,
|
||||
Value, WitnessSet,
|
||||
},
|
||||
Coin, Nullable, Set,
|
||||
};
|
||||
|
||||
use crate::{cardano::cardano::Cardano, wallet::Wallet};
|
||||
|
||||
use super::{
|
||||
context::TxContext,
|
||||
plutus::{
|
||||
build_transaction, default_transaction_body, default_witness_set, expect_post_alonzo,
|
||||
from_network, into_outputs, value_subtract_lovelace, BuildParams,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn send<T>(ctx: TxContext<T>, outputs: Vec<(Address, u64)>) -> Result<String>
|
||||
where
|
||||
T: Cardano,
|
||||
{
|
||||
let available_utxos = ctx.available_utxos().await.unwrap();
|
||||
let wallet_address = ctx.wallet_address();
|
||||
|
||||
let mut tx = build_transaction(
|
||||
&ctx.build_parameters().await,
|
||||
&available_utxos,
|
||||
|fee, ex_units| {
|
||||
let inputs = available_utxos
|
||||
.iter()
|
||||
.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>>();
|
||||
outputs.push(
|
||||
// Change
|
||||
PostAlonzoTransactionOutput {
|
||||
address: wallet_address.to_vec().into(),
|
||||
value: value_subtract_lovelace(x.value.clone(), fee).expect("not enough fuel"),
|
||||
datum_option: None,
|
||||
script_ref: None,
|
||||
},
|
||||
);
|
||||
|
||||
// ----- Put it all together
|
||||
Tx {
|
||||
transaction_body: TransactionBody {
|
||||
inputs: Set::from(inputs),
|
||||
network_id: Some(from_network(ctx.cardano.network_id())),
|
||||
outputs: into_outputs(outputs),
|
||||
fee,
|
||||
..default_transaction_body()
|
||||
},
|
||||
transaction_witness_set: WitnessSet {
|
||||
..default_witness_set()
|
||||
},
|
||||
success: true,
|
||||
auxiliary_data: Nullable::Null,
|
||||
}
|
||||
},
|
||||
);
|
||||
ctx.wallet.sign(&mut tx);
|
||||
let mut serialized_tx = Vec::new();
|
||||
println!("{:#?}", tx);
|
||||
encode(&tx, &mut serialized_tx).unwrap();
|
||||
let tx_hash = ctx.cardano.submit(serialized_tx).await?;
|
||||
Ok(tx_hash)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// /// Torso: the main part of the body relevant to tx building.
|
||||
|
||||
// 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, PolicyId};
|
||||
// use std::{cmp::Ordering, collections::HashMap, str::FromStr};
|
||||
// use uplc::tx::{eval_phase_two, ResolvedInput, SlotConfig};
|
||||
|
||||
// struct TransactionBody {
|
||||
// inputs: Set<TransactionInput>,
|
||||
// outputs: Vec<TransactionOutput>,
|
||||
// fee: Coin,
|
||||
// ttl: Option<u64>,
|
||||
// certificates: Option<NonEmptySet<Certificate>>,
|
||||
// withdrawals: Option<NonEmptyKeyValuePairs<RewardAccount, Coin>>,
|
||||
// auxiliary_data_hash: Option<Bytes>,
|
||||
// validity_interval_start: Option<u64>,
|
||||
// mint: Option<Multiasset<NonZeroInt>>,
|
||||
// script_data_hash: Option<Hash<32>>,
|
||||
// collateral: Option<NonEmptySet<TransactionInput>>,
|
||||
// required_signers: Option<RequiredSigners>,
|
||||
// network_id: Option<NetworkId>,
|
||||
// collateral_return: Option<T1>,
|
||||
// total_collateral: Option<Coin>,
|
||||
// reference_inputs: Option<NonEmptySet<TransactionInput>>,
|
||||
// voting_procedures: Option<VotingProcedures>,
|
||||
// proposal_procedures: Option<NonEmptySet<ProposalProcedure>>,
|
||||
// treasury_value: Option<Coin>,
|
||||
// donation: Option<PositiveCoin>,
|
||||
// }
|
||||
|
||||
// pub struct Torso {
|
||||
// inputs: Vec<ResolvedInput>,
|
||||
// reference_inputs: Vec<ResolvedInput>,
|
||||
// outputs: Vec<TransactionOutput>,
|
||||
// mint: HashMap<PolicyId, HashMap<AssetName, i64>>,
|
||||
// voting_procedures: Vec<Votes>
|
||||
// collateral: Vec<ResolvedInput>,
|
||||
// required_signers: Vec<VerificationKeyHash>,
|
||||
// }
|
||||
//
|
||||
// pub struct WitnessPart {
|
||||
// }
|
|
@ -0,0 +1,22 @@
|
|||
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()))
|
||||
}
|
||||
|
||||
pub fn concat<T: Clone>(l: &[T], r: &[T]) -> Vec<T> {
|
||||
let mut n = l.to_vec();
|
||||
n.extend(r.iter().cloned());
|
||||
return n;
|
||||
}
|
||||
|
||||
pub fn unzip<A, B>(zipped: Vec<(A, B)>) -> (Vec<A>, Vec<B>) {
|
||||
let mut va: Vec<A> = Vec::with_capacity(zipped.len());
|
||||
let mut vb: Vec<B> = Vec::with_capacity(zipped.len());
|
||||
for (a, b) in zipped.into_iter() {
|
||||
va.push(a);
|
||||
vb.push(b);
|
||||
}
|
||||
(va, vb)
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use cryptoxide::hashing::blake2b_256;
|
||||
use minicbor::encode;
|
||||
use pallas_addresses::ShelleyPaymentPart;
|
||||
use pallas_crypto::hash::{Hash, Hasher};
|
||||
use pallas_crypto::key::ed25519::Signature;
|
||||
use pallas_crypto::{self, key::ed25519::SecretKey};
|
||||
use pallas_primitives::conway::{Tx, VKeyWitness};
|
||||
|
||||
use crate::tx::plutus::non_empty_set;
|
||||
use crate::utils::v2a;
|
||||
|
||||
use rand::{rngs::OsRng, TryRngCore};
|
||||
|
||||
const PREFIX: &str = "wallet_";
|
||||
|
||||
pub struct Wallet {
|
||||
pub skey: [u8; 32],
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub fn vkey(&self) -> [u8; 32] {
|
||||
SecretKey::from(self.skey).public_key().into()
|
||||
}
|
||||
|
||||
pub fn key_hash(&self) -> Hash<28> {
|
||||
Hasher::<224>::hash(SecretKey::from(self.skey).public_key().as_ref())
|
||||
}
|
||||
|
||||
pub fn payment_credential(&self) -> ShelleyPaymentPart {
|
||||
ShelleyPaymentPart::Key(self.key_hash())
|
||||
}
|
||||
|
||||
pub fn sign_hash(&self, h: &[u8; 32]) -> Signature {
|
||||
SecretKey::from(self.skey).sign(h)
|
||||
}
|
||||
|
||||
pub fn sign(&self, tx: &mut Tx) {
|
||||
let mut msg = Vec::new();
|
||||
encode(&tx.transaction_body, &mut msg).unwrap();
|
||||
let tx_hash = blake2b_256(&msg);
|
||||
let sig = self.sign_hash(&tx_hash);
|
||||
tx.transaction_witness_set.vkeywitness = non_empty_set(vec![VKeyWitness {
|
||||
vkey: self.vkey().to_vec().into(),
|
||||
signature: sig.as_ref().to_vec().into(),
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env(env: &HashMap<String, String>) -> Wallet {
|
||||
let wallet_env: HashMap<String, String> = env
|
||||
.iter()
|
||||
.filter_map(|(k, v)| k.strip_prefix(PREFIX).map(|k| (k.to_string(), v.clone())))
|
||||
.collect();
|
||||
let raw = wallet_env.get("key").expect("wallet key not found");
|
||||
let skey = parse_raw_skey(raw);
|
||||
Wallet { skey }
|
||||
}
|
||||
|
||||
pub fn generate() -> [u8; 32] {
|
||||
let mut key = [0u8; 32];
|
||||
OsRng.try_fill_bytes(&mut key).unwrap();
|
||||
key
|
||||
}
|
||||
|
||||
fn parse_raw_skey(raw: &str) -> [u8; 32] {
|
||||
// FIXME :: Not tested
|
||||
if raw.len() == 64 {
|
||||
// Assume hex
|
||||
v2a(hex::decode(raw).expect("expected hex")).expect("wrong length")
|
||||
} else if raw.len() == 70 {
|
||||
// Assume Bech
|
||||
v2a(bech32::decode(raw).unwrap().1).expect("wrong length")
|
||||
} else {
|
||||
panic!("Not supported")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue