Skip to content

Commit

Permalink
Add TransactionBuilder (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
sterliakov authored Mar 6, 2024
1 parent a07e731 commit 4173286
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 41 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ Possible header types:
- `Bug Fixes` for any bug fixes.
- `Breaking Changes` for any backwards-incompatible changes.

## v0.1.0-beta.3 (pending)

### Features
- Added `TransactionBuilder` to simplify transaction preparation.

## v0.1.0-beta.2 (2023-12-23)

### Bug Fixes
Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ reqwest = { version = "0.11", features = ["json"], optional = true }
serde = { version = "^1.0", features=["derive"], optional = true }
serde_json = { version = "^1.0", optional = true }
serde_with = { version = "^3.4", features = ["hex"], optional = true }
rand = { version = "^0.8", features = ["std", "std_rng"], optional = true }

[dev-dependencies]
# version_sync: to ensure versions in `Cargo.toml` and `README.md` are in sync
Expand All @@ -45,6 +46,7 @@ bloomfilter = "^1.0"
ethabi = "^18.0"

[features]
# default = ['http']
# default = ['builder']
serde = ["dep:serde", "dep:serde_json", "dep:serde_with"]
http = ["dep:reqwest", "serde"]
builder = ["http", "dep:rand"]
49 changes: 17 additions & 32 deletions examples/transaction_broadcast.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,24 @@
//! Network communication requires `http` create feature.
//! Transaction builder requires additionally `builder` feature.
use std::{thread, time::Duration};
use thor_devkit::hdnode::{HDNode, Language, Mnemonic};
use thor_devkit::network::{AResult, BlockReference, ThorNode};
use thor_devkit::transactions::{Clause, Transaction};
use thor_devkit::network::{AResult, ThorNode};
use thor_devkit::transactions::Transaction;
use thor_devkit::Address;

async fn create_and_broadcast_transaction() -> AResult<()> {
let node = ThorNode::testnet();
let block_ref = node
.fetch_block(BlockReference::Best)
.await?
.expect("Must exist")
.0
.id
.0[3];
let recipient: Address = std::env::var("TEST_TO_ADDRESS")
.expect("Address must be provided")
.parse()
.unwrap();
let transaction = Transaction {
chain_tag: node.chain_tag,
block_ref,
expiration: 128,
clauses: vec![Clause {
to: Some(recipient),
value: 1000.into(),
data: b"".to_vec().into(),
}],
gas_price_coef: 128,
gas: 21000,
depends_on: None,
nonce: 0xbc614e,
reserved: None,
signature: None,
};
let amount = 10;
let transaction = Transaction::build(node.clone())
.gas_price_coef(128)
.add_transfer(recipient, amount)
.build()
.await?;
let mnemonic = Mnemonic::from_phrase(
&std::env::var("TEST_MNEMONIC").expect("Mnemonic must be provided"),
Language::English,
Expand All @@ -46,10 +30,11 @@ async fn create_and_broadcast_transaction() -> AResult<()> {
sender.to_checksum_address(),
recipient.to_checksum_address()
);
let sender_before = node.fetch_account(sender).await?.balance;
let recipient_before = node.fetch_account(recipient).await?.balance;
println!(
"Balances before: {:?}, {:?}",
node.fetch_account(sender).await?.balance,
node.fetch_account(recipient).await?.balance
sender_before, recipient_before
);
let signed = transaction.sign(&wallet.private_key()?.private_key());
let id = node.broadcast_transaction(&signed).await?;
Expand All @@ -66,11 +51,11 @@ async fn create_and_broadcast_transaction() -> AResult<()> {
thread::sleep(Duration::from_secs(2));
}
}
println!(
"Balances after: {:?}, {:?}",
node.fetch_account(sender).await?.balance,
node.fetch_account(recipient).await?.balance
);
let sender_after = node.fetch_account(sender).await?.balance;
let recipient_after = node.fetch_account(recipient).await?.balance;
println!("Balances after: {:?}, {:?}", sender_after, recipient_after);
assert_eq!(sender_before - sender_after, amount.into());
assert_eq!(recipient_after - recipient_before, amount.into());
Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion src/hdnode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ impl<'a> HDNodeBuilder<'a> {
self.path = Some(path);
self
}
pub fn seed(mut self, seed: [u8; 64]) -> Self {
pub const fn seed(mut self, seed: [u8; 64]) -> Self {
//! Set a seed to use.
self.seed = Some(seed);
self
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ pub mod hdnode;
#[cfg(feature = "http")]
pub mod network;
pub mod rlp;
#[cfg(feature = "http")]
mod transaction_builder;
pub mod transactions;
mod utils;
pub use ethereum_types::U256;
Expand Down
21 changes: 21 additions & 0 deletions src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ pub type AResult<T> = std::result::Result<T, Box<dyn std::error::Error + Send +

/// Validation errors (not related to HTTP failures)
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ValidationError {
/// Account storage keys start from one, there's no key 0.
ZeroStorageKey,
/// Transaction broadcast failed
BroadcastFailed(String),
/// Unexpected failure
Unknown(String),
}

impl std::error::Error for ValidationError {}
Expand All @@ -29,11 +32,16 @@ impl std::fmt::Display for ValidationError {
f.write_str("Failed to broadcast: ")?;
f.write_str(text.strip_suffix('\n').unwrap_or(text))
}
Self::Unknown(text) => {
f.write_str("Unknown error: ")?;
f.write_str(text.strip_suffix('\n').unwrap_or(text))
}
}
}
}

/// A simple HTTP REST client for a VeChain node.
#[derive(Clone, Debug)]
pub struct ThorNode {
/// API base url
pub base_url: Url,
Expand Down Expand Up @@ -286,6 +294,13 @@ pub struct BlockInfo {
pub signer: Address,
}

impl BlockInfo {
pub const fn block_ref(&self) -> u64 {
//! Extract blockRef for transaction.
self.id.0[3]
}
}

/// Transaction data included in the block extended details.
///
/// Combines [`ExtendedTransaction`] and [`Receipt`].
Expand Down Expand Up @@ -563,6 +578,12 @@ impl ThorNode {
}
}

pub async fn fetch_best_block(&self) -> AResult<(BlockInfo, Vec<U256>)> {
//! Retrieve a best block from node.
let info = self.fetch_block(BlockReference::Best).await?;
Ok(info.ok_or(ValidationError::Unknown("Best block not found".to_string()))?)
}

pub async fn fetch_block_expanded(
&self,
block_ref: BlockReference,
Expand Down
168 changes: 168 additions & 0 deletions src/transaction_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use rand::Rng;

use crate::address::Address;
use crate::network::ThorNode;
use crate::rlp::Bytes;
use crate::transactions::{Clause, Reserved, Transaction};
use crate::U256;

#[derive(Clone, Debug, Eq, PartialEq, Default)]
struct TransactionTemplate {
block_ref: Option<u64>,
expiration: Option<u32>,
clauses: Vec<Clause>,
gas_price_coef: Option<u8>,
gas: Option<u64>,
depends_on: Option<U256>,
nonce: Option<u64>,
delegated: bool,
}

/// Transaction builder allows to create and prepare transactions
/// with minimal developers efforts.
#[derive(Clone, Debug)]
pub struct TransactionBuilder {
node: ThorNode,
template: TransactionTemplate,
}

impl TransactionBuilder {
pub fn new(node: ThorNode) -> Self {
//! Create a new builder.
Self {
node,
template: TransactionTemplate::default(),
}
}
pub const fn delegated(mut self) -> Self {
//! Make a transaction delegated.
self.template.delegated = true;
self
}
pub const fn nonce(mut self, nonce: u64) -> Self {
//! Set a nonce for transaction.
self.template.nonce = Some(nonce);
self
}
pub const fn depends_on(mut self, depends_on: U256) -> Self {
//! Mark a transaction as dependent on another one.
self.template.depends_on = Some(depends_on);
self
}
pub const fn gas(mut self, gas: u64) -> Self {
//! Set maximal gas amount for transaction.
self.template.gas = Some(gas);
self
}
pub const fn gas_price_coef(mut self, gas_price_coef: u8) -> Self {
//! Set gas price coefficient for transaction.
self.template.gas_price_coef = Some(gas_price_coef);
self
}
pub const fn expiration(mut self, expiration: u32) -> Self {
//! Set expiration for transaction in blocks, starting from `block_ref`.
self.template.expiration = Some(expiration);
self
}
pub const fn block_ref(mut self, block_ref: u64) -> Self {
//! Set block_ref for transaction to count `expiration` from.
self.template.block_ref = Some(block_ref);
self
}
pub fn add_transfer<T: Into<U256>>(self, recipient: Address, value: T) -> Self {
//! Add a simple transfer to clauses.
self.add_clause(Clause {
to: Some(recipient),
value: value.into(),
data: Bytes::new(),
})
}
pub fn add_contract_create(self, contract_bytes: Bytes) -> Self {
//! Add a contract creation clause.
self.add_clause(Clause {
to: None,
value: U256::zero(),
data: contract_bytes,
})
}
pub fn add_contract_call(self, contract_address: Address, call_bytes: Bytes) -> Self {
//! Add a contract method call clause.
self.add_clause(Clause {
to: Some(contract_address),
value: U256::zero(),
data: call_bytes,
})
}
pub fn add_clause(mut self, clause: Clause) -> Self {
//! Add an arbitrary, user-provided clause.
self.template.clauses.push(clause);
self
}

pub async fn build(&self) -> Result<Transaction, TransactionBuilderError> {
//! Prepare a `Transaction`. This may perform a network request
//! to identify appropriate parameters.
if self.template.clauses.is_empty() {
return Err(TransactionBuilderError::EmptyTransaction);
}
let block_ref = match self.template.block_ref {
Some(r) => r,
None => self
.node
.fetch_best_block()
.await
.map_err(|_| TransactionBuilderError::NetworkError)?
.0
.block_ref(),
};
let mut tx = Transaction {
chain_tag: self.node.chain_tag,
block_ref,
expiration: self.template.expiration.unwrap_or(128),
clauses: self.template.clauses.clone(),
gas_price_coef: self.template.gas_price_coef.unwrap_or(0),
gas: self.template.gas.unwrap_or(0),
depends_on: self.template.depends_on,
nonce: self.template.nonce.unwrap_or_else(|| {
let mut rng = rand::thread_rng();
rng.gen::<u64>()
}),
reserved: if self.template.delegated {
Some(Reserved::new_delegated())
} else {
None
},
signature: None,
};
if self.template.gas.is_some() {
Ok(tx)
} else if tx.clauses.iter().all(|clause| clause.data.is_empty()) {
tx.gas = tx.intrinsic_gas();
Ok(tx)
} else {
Err(TransactionBuilderError::CannotEstimateGas)
}
}
}

/// Transaction creation errors
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TransactionBuilderError {
/// Network error (failed to fetch data from node)
NetworkError,
/// No clauses provided
EmptyTransaction,
/// Transaction clauses involve contract interaction, and gas was not provided.
CannotEstimateGas,
}

impl std::error::Error for TransactionBuilderError {}
impl std::fmt::Display for TransactionBuilderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NetworkError => f.write_str("Failed to retrieve data from network"),
Self::EmptyTransaction => f.write_str("Cannot build an empty transaction - make sure to add at least one clause first."),
Self::CannotEstimateGas => f.write_str("Transaction clauses involve contract interaction, please provide gas amount explicitly."),
}
}
}
Loading

0 comments on commit 4173286

Please sign in to comment.