From 0480c5078f431285ef6199b7ae9c160d748a4782 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 13 Apr 2023 21:03:34 -0400 Subject: [PATCH 1/3] feat: starting a tx builder Co-authored-by: Lucas Rosa --- Cargo.toml | 1 + pallas-txbuilder/Cargo.toml | 10 +++++ pallas-txbuilder/src/lib.rs | 76 +++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 pallas-txbuilder/Cargo.toml create mode 100644 pallas-txbuilder/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 3f93671e..8a653c53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "pallas-crypto", "pallas-primitives", "pallas-traverse", + "pallas-txbuilder", "pallas-upstream", "pallas", "examples/block-download", diff --git a/pallas-txbuilder/Cargo.toml b/pallas-txbuilder/Cargo.toml new file mode 100644 index 00000000..1bba6b68 --- /dev/null +++ b/pallas-txbuilder/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "pallas-txbuilder" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +pallas-codec = { path = "../pallas-codec", version = "0.18.0" } +pallas-primitives = { path = "../pallas-primitives", version = "0.18.0" } diff --git a/pallas-txbuilder/src/lib.rs b/pallas-txbuilder/src/lib.rs new file mode 100644 index 00000000..f3f5ae45 --- /dev/null +++ b/pallas-txbuilder/src/lib.rs @@ -0,0 +1,76 @@ +use pallas_codec::minicbor::{self, Decode, Encode}; +use pallas_primitives::babbage::{AuxiliaryData, TransactionBody, WitnessSet}; + +#[derive(Encode, Decode)] +pub struct Transaction { + #[n(0)] + body: TransactionBody, + #[n(1)] + witness_set: WitnessSet, + #[n(2)] + is_valid: bool, + #[n(3)] + auxiliary_data: Option, +} + +impl Transaction { + pub fn builder() -> TxBuilder { + TxBuilder::new() + } +} + +#[derive(Default)] +pub struct TxBuilder {} + +impl TxBuilder { + /// Create a new builder + pub fn new() -> Self { + TxBuilder {} + } + + /// Add an input + pub fn input(self) -> Self { + todo!() + } + + /// Add an output + pub fn output(&self) -> Self { + todo!() + } + + /// Require a signer + pub fn signer(self) -> Self { + todo!() + } + + /// Assets to mint or burn + pub fn mint_assets(self) -> Self { + todo!() + } + + pub fn finalize(self) -> Transaction { + todo!() + } +} + +#[cfg(test)] +mod test { + + use pallas_primitives::Fragment; + + use crate::Transaction; + + #[test] + fn build() { + let tx = Transaction::builder() + .input() + .output() + .signer() + .mint_assets() + .finalize(); + + let bytes = tx.encode_fragment().expect("encoding failed"); + + assert_eq!(bytes, vec![0x11]) + } +} From 12d4626f4e683d8cdc6dcfe83dcba609ef976d54 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 13 Apr 2023 22:59:30 -0400 Subject: [PATCH 2/3] feat: first test passing * introduce some fields for TxBuilder * have some methods actually mutate builder * construct a tx in finalize Co-authored-by: Lucas Rosa --- pallas-txbuilder/Cargo.toml | 3 + pallas-txbuilder/src/lib.rs | 115 ++++++++++++++++++++++++++++++------ 2 files changed, 100 insertions(+), 18 deletions(-) diff --git a/pallas-txbuilder/Cargo.toml b/pallas-txbuilder/Cargo.toml index 1bba6b68..2fec27dd 100644 --- a/pallas-txbuilder/Cargo.toml +++ b/pallas-txbuilder/Cargo.toml @@ -8,3 +8,6 @@ edition = "2021" [dependencies] pallas-codec = { path = "../pallas-codec", version = "0.18.0" } pallas-primitives = { path = "../pallas-primitives", version = "0.18.0" } + +[dev-dependencies] +hex = "0.4.3" diff --git a/pallas-txbuilder/src/lib.rs b/pallas-txbuilder/src/lib.rs index f3f5ae45..d5245a2a 100644 --- a/pallas-txbuilder/src/lib.rs +++ b/pallas-txbuilder/src/lib.rs @@ -1,7 +1,10 @@ use pallas_codec::minicbor::{self, Decode, Encode}; -use pallas_primitives::babbage::{AuxiliaryData, TransactionBody, WitnessSet}; +use pallas_primitives::babbage::{ + AddrKeyhash, AuxiliaryData, TransactionBody, TransactionInput, TransactionOutput, Value, + WitnessSet, +}; -#[derive(Encode, Decode)] +#[derive(Encode, Decode, Clone)] pub struct Transaction { #[n(0)] body: TransactionBody, @@ -19,28 +22,49 @@ impl Transaction { } } -#[derive(Default)] -pub struct TxBuilder {} +pub struct TxBuilder { + inputs: Vec<(TransactionInput, TransactionOutput)>, + outputs: Vec, + mint: Option, + required_signers: Vec, +} + +impl Default for TxBuilder { + fn default() -> Self { + Self::new() + } +} impl TxBuilder { /// Create a new builder pub fn new() -> Self { - TxBuilder {} + TxBuilder { + inputs: vec![], + outputs: vec![], + mint: None, + required_signers: vec![], + } } /// Add an input - pub fn input(self) -> Self { - todo!() + pub fn input(mut self, input: TransactionInput, resolved: TransactionOutput) -> Self { + self.inputs.push((input, resolved)); + + self } /// Add an output - pub fn output(&self) -> Self { - todo!() + pub fn output(mut self, output: TransactionOutput) -> Self { + self.outputs.push(output); + + self } /// Require a signer - pub fn signer(self) -> Self { - todo!() + pub fn signer(mut self, new_signer: AddrKeyhash) -> Self { + self.required_signers.push(new_signer); + + self } /// Assets to mint or burn @@ -49,28 +73,83 @@ impl TxBuilder { } pub fn finalize(self) -> Transaction { - todo!() + Transaction { + body: TransactionBody { + inputs: self.inputs.into_iter().map(|i| i.0).collect(), + outputs: self.outputs, + fee: 0, + ttl: None, + certificates: None, + withdrawals: None, + update: None, + auxiliary_data_hash: None, + validity_interval_start: None, + mint: None, + script_data_hash: None, + collateral: None, + required_signers: if self.required_signers.is_empty() { + None + } else { + Some(self.required_signers) + }, + network_id: None, + collateral_return: None, + total_collateral: None, + reference_inputs: None, + }, + witness_set: WitnessSet { + vkeywitness: None, + native_script: None, + bootstrap_witness: None, + plutus_v1_script: None, + plutus_data: None, + redeemer: None, + plutus_v2_script: None, + }, + is_valid: true, + auxiliary_data: None, + } } } #[cfg(test)] mod test { - use pallas_primitives::Fragment; + use pallas_primitives::{ + babbage::{PseudoPostAlonzoTransactionOutput, TransactionInput, TransactionOutput, Value}, + Fragment, + }; use crate::Transaction; #[test] fn build() { + let input = TransactionInput { + transaction_id: [0; 32].into(), + index: 0, + }; + + let resolved = TransactionOutput::PostAlonzo(PseudoPostAlonzoTransactionOutput { + address: vec![].into(), + value: Value::Coin(1000000), + datum_option: None, + script_ref: None, + }); + + let output = TransactionOutput::PostAlonzo(PseudoPostAlonzoTransactionOutput { + address: vec![].into(), + value: Value::Coin(1000000), + datum_option: None, + script_ref: None, + }); + let tx = Transaction::builder() - .input() - .output() - .signer() - .mint_assets() + .input(input, resolved) + .output(output) .finalize(); let bytes = tx.encode_fragment().expect("encoding failed"); - assert_eq!(bytes, vec![0x11]) + assert_eq!(hex::encode(bytes), "83a300818258200000000000000000000000000000000000000000000000000000000000000000000181a20040011a000f42400200a0f5") } } From 2d9c0f304a9bd91d8dc67ea2dbb738841a200edf Mon Sep 17 00:00:00 2001 From: microproofs Date: Sat, 15 Apr 2023 00:16:45 -0400 Subject: [PATCH 3/3] chore: add some more transaction builder functions. valid_until valid_after mint_assets (WIP) --- pallas-txbuilder/src/lib.rs | 141 ++++++++++++++++++++++++++++++++---- 1 file changed, 125 insertions(+), 16 deletions(-) diff --git a/pallas-txbuilder/src/lib.rs b/pallas-txbuilder/src/lib.rs index d5245a2a..574311f7 100644 --- a/pallas-txbuilder/src/lib.rs +++ b/pallas-txbuilder/src/lib.rs @@ -1,9 +1,17 @@ -use pallas_codec::minicbor::{self, Decode, Encode}; +use std::time::Duration; + +use pallas_codec::{ + minicbor::{self, Decode, Encode}, + utils::Bytes, +}; use pallas_primitives::babbage::{ - AddrKeyhash, AuxiliaryData, TransactionBody, TransactionInput, TransactionOutput, Value, - WitnessSet, + AddrKeyhash, AuxiliaryData, PolicyId, TransactionBody, TransactionInput, TransactionOutput, + Value, WitnessSet, }; +// TODO: Replace with some slot conversion lookup +const SLOT_CONVERSION: u64 = 0; + #[derive(Encode, Decode, Clone)] pub struct Transaction { #[n(0)] @@ -22,11 +30,22 @@ impl Transaction { } } +struct TransactionBuilderInput { + input: TransactionInput, + resolved: TransactionOutput, +} + +struct TransactionBuilderOutput { + output: TransactionOutput, +} + pub struct TxBuilder { - inputs: Vec<(TransactionInput, TransactionOutput)>, - outputs: Vec, + inputs: Vec, + outputs: Vec, mint: Option, required_signers: Vec, + valid_after: Option, + valid_until: Option, } impl Default for TxBuilder { @@ -43,19 +62,22 @@ impl TxBuilder { outputs: vec![], mint: None, required_signers: vec![], + valid_after: None, + valid_until: None, } } /// Add an input pub fn input(mut self, input: TransactionInput, resolved: TransactionOutput) -> Self { - self.inputs.push((input, resolved)); + self.inputs + .push(TransactionBuilderInput { input, resolved }); self } /// Add an output pub fn output(mut self, output: TransactionOutput) -> Self { - self.outputs.push(output); + self.outputs.push(TransactionBuilderOutput { output }); self } @@ -68,22 +90,39 @@ impl TxBuilder { } /// Assets to mint or burn - pub fn mint_assets(self) -> Self { - todo!() + pub fn mint_assets(mut self, policy: PolicyId, assets: Vec<(Bytes, u64)>) -> Self { + let mint = vec![(policy, assets.into())]; + + self.mint = Some(Value::Multiasset(0, mint.into())); + + self } - pub fn finalize(self) -> Transaction { + pub fn valid_after(mut self, duration: Duration) -> Self { + self.valid_after = Some(duration); + + self + } + + pub fn valid_until(mut self, duration: Duration) -> Self { + self.valid_until = Some(duration); + + self + } + + pub fn build(self) -> Transaction { Transaction { body: TransactionBody { - inputs: self.inputs.into_iter().map(|i| i.0).collect(), - outputs: self.outputs, + inputs: self.inputs.into_iter().map(|i| i.input).collect(), + outputs: self.outputs.into_iter().map(|i| i.output).collect(), fee: 0, - ttl: None, + ttl: self.valid_until.map(|i| i.as_secs() - SLOT_CONVERSION), + certificates: None, withdrawals: None, update: None, auxiliary_data_hash: None, - validity_interval_start: None, + validity_interval_start: self.valid_after.map(|i| i.as_secs() - SLOT_CONVERSION), mint: None, script_data_hash: None, collateral: None, @@ -115,6 +154,8 @@ impl TxBuilder { #[cfg(test)] mod test { + use std::time::Duration; + use pallas_primitives::{ babbage::{PseudoPostAlonzoTransactionOutput, TransactionInput, TransactionOutput, Value}, Fragment, @@ -123,7 +164,7 @@ mod test { use crate::Transaction; #[test] - fn build() { + fn build_basic() { let input = TransactionInput { transaction_id: [0; 32].into(), index: 0, @@ -146,10 +187,78 @@ mod test { let tx = Transaction::builder() .input(input, resolved) .output(output) - .finalize(); + .build(); let bytes = tx.encode_fragment().expect("encoding failed"); assert_eq!(hex::encode(bytes), "83a300818258200000000000000000000000000000000000000000000000000000000000000000000181a20040011a000f42400200a0f5") } + + #[test] + fn build_ttl_valid_after() { + let input = TransactionInput { + transaction_id: [0; 32].into(), + index: 0, + }; + + let resolved = TransactionOutput::PostAlonzo(PseudoPostAlonzoTransactionOutput { + address: vec![].into(), + value: Value::Coin(1000000), + datum_option: None, + script_ref: None, + }); + + let output = TransactionOutput::PostAlonzo(PseudoPostAlonzoTransactionOutput { + address: vec![].into(), + value: Value::Coin(1000000), + datum_option: None, + script_ref: None, + }); + + let valid_after = 1618400000; // Unix time for April 14, 2021, 12:00:00 AM UTC + let valid_until = 1618430000; + + let tx = Transaction::builder() + .input(input, resolved) + .output(output) + .valid_after(Duration::from_secs(valid_after)) + .valid_until(Duration::from_secs(valid_until)) + .build(); + + let bytes = tx.encode_fragment().expect("encoding failed"); + + assert_eq!(hex::encode(bytes), "83a500818258200000000000000000000000000000000000000000000000000000000000000000000181a20040011a000f42400200031a60774830081a6076d300a0f5") + } + + #[test] + fn build_mint() { + let input = TransactionInput { + transaction_id: [0; 32].into(), + index: 0, + }; + + let resolved = TransactionOutput::PostAlonzo(PseudoPostAlonzoTransactionOutput { + address: vec![].into(), + value: Value::Coin(1000000), + datum_option: None, + script_ref: None, + }); + + let output = TransactionOutput::PostAlonzo(PseudoPostAlonzoTransactionOutput { + address: vec![].into(), + value: Value::Coin(1000000), + datum_option: None, + script_ref: None, + }); + + let tx = Transaction::builder() + .input(input, resolved) + .output(output) + .mint_assets(policy, assets) + .build(); + + let bytes = tx.encode_fragment().expect("encoding failed"); + + assert_eq!(hex::encode(bytes), "83a500818258200000000000000000000000000000000000000000000000000000000000000000000181a20040011a000f42400200031a60774830081a6076d300a0f5") + } }