Skip to content

Commit 4173286

Browse files
authored
Add TransactionBuilder (#26)
1 parent a07e731 commit 4173286

File tree

10 files changed

+435
-41
lines changed

10 files changed

+435
-41
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Possible header types:
99
- `Bug Fixes` for any bug fixes.
1010
- `Breaking Changes` for any backwards-incompatible changes.
1111

12+
## v0.1.0-beta.3 (pending)
13+
14+
### Features
15+
- Added `TransactionBuilder` to simplify transaction preparation.
16+
1217
## v0.1.0-beta.2 (2023-12-23)
1318

1419
### Bug Fixes

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ reqwest = { version = "0.11", features = ["json"], optional = true }
3535
serde = { version = "^1.0", features=["derive"], optional = true }
3636
serde_json = { version = "^1.0", optional = true }
3737
serde_with = { version = "^3.4", features = ["hex"], optional = true }
38+
rand = { version = "^0.8", features = ["std", "std_rng"], optional = true }
3839

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

4748
[features]
48-
# default = ['http']
49+
# default = ['builder']
4950
serde = ["dep:serde", "dep:serde_json", "dep:serde_with"]
5051
http = ["dep:reqwest", "serde"]
52+
builder = ["http", "dep:rand"]

examples/transaction_broadcast.rs

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,24 @@
11
//! Network communication requires `http` create feature.
2+
//! Transaction builder requires additionally `builder` feature.
23
34
use std::{thread, time::Duration};
45
use thor_devkit::hdnode::{HDNode, Language, Mnemonic};
5-
use thor_devkit::network::{AResult, BlockReference, ThorNode};
6-
use thor_devkit::transactions::{Clause, Transaction};
6+
use thor_devkit::network::{AResult, ThorNode};
7+
use thor_devkit::transactions::Transaction;
78
use thor_devkit::Address;
89

910
async fn create_and_broadcast_transaction() -> AResult<()> {
1011
let node = ThorNode::testnet();
11-
let block_ref = node
12-
.fetch_block(BlockReference::Best)
13-
.await?
14-
.expect("Must exist")
15-
.0
16-
.id
17-
.0[3];
1812
let recipient: Address = std::env::var("TEST_TO_ADDRESS")
1913
.expect("Address must be provided")
2014
.parse()
2115
.unwrap();
22-
let transaction = Transaction {
23-
chain_tag: node.chain_tag,
24-
block_ref,
25-
expiration: 128,
26-
clauses: vec![Clause {
27-
to: Some(recipient),
28-
value: 1000.into(),
29-
data: b"".to_vec().into(),
30-
}],
31-
gas_price_coef: 128,
32-
gas: 21000,
33-
depends_on: None,
34-
nonce: 0xbc614e,
35-
reserved: None,
36-
signature: None,
37-
};
16+
let amount = 10;
17+
let transaction = Transaction::build(node.clone())
18+
.gas_price_coef(128)
19+
.add_transfer(recipient, amount)
20+
.build()
21+
.await?;
3822
let mnemonic = Mnemonic::from_phrase(
3923
&std::env::var("TEST_MNEMONIC").expect("Mnemonic must be provided"),
4024
Language::English,
@@ -46,10 +30,11 @@ async fn create_and_broadcast_transaction() -> AResult<()> {
4630
sender.to_checksum_address(),
4731
recipient.to_checksum_address()
4832
);
33+
let sender_before = node.fetch_account(sender).await?.balance;
34+
let recipient_before = node.fetch_account(recipient).await?.balance;
4935
println!(
5036
"Balances before: {:?}, {:?}",
51-
node.fetch_account(sender).await?.balance,
52-
node.fetch_account(recipient).await?.balance
37+
sender_before, recipient_before
5338
);
5439
let signed = transaction.sign(&wallet.private_key()?.private_key());
5540
let id = node.broadcast_transaction(&signed).await?;
@@ -66,11 +51,11 @@ async fn create_and_broadcast_transaction() -> AResult<()> {
6651
thread::sleep(Duration::from_secs(2));
6752
}
6853
}
69-
println!(
70-
"Balances after: {:?}, {:?}",
71-
node.fetch_account(sender).await?.balance,
72-
node.fetch_account(recipient).await?.balance
73-
);
54+
let sender_after = node.fetch_account(sender).await?.balance;
55+
let recipient_after = node.fetch_account(recipient).await?.balance;
56+
println!("Balances after: {:?}, {:?}", sender_after, recipient_after);
57+
assert_eq!(sender_before - sender_after, amount.into());
58+
assert_eq!(recipient_after - recipient_before, amount.into());
7459
Ok(())
7560
}
7661

src/hdnode.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ impl<'a> HDNodeBuilder<'a> {
195195
self.path = Some(path);
196196
self
197197
}
198-
pub fn seed(mut self, seed: [u8; 64]) -> Self {
198+
pub const fn seed(mut self, seed: [u8; 64]) -> Self {
199199
//! Set a seed to use.
200200
self.seed = Some(seed);
201201
self

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ pub mod hdnode;
8888
#[cfg(feature = "http")]
8989
pub mod network;
9090
pub mod rlp;
91+
#[cfg(feature = "http")]
92+
mod transaction_builder;
9193
pub mod transactions;
9294
mod utils;
9395
pub use ethereum_types::U256;

src/network.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ pub type AResult<T> = std::result::Result<T, Box<dyn std::error::Error + Send +
1313

1414
/// Validation errors (not related to HTTP failures)
1515
#[derive(Clone, Debug, Eq, PartialEq)]
16+
#[non_exhaustive]
1617
pub enum ValidationError {
1718
/// Account storage keys start from one, there's no key 0.
1819
ZeroStorageKey,
1920
/// Transaction broadcast failed
2021
BroadcastFailed(String),
22+
/// Unexpected failure
23+
Unknown(String),
2124
}
2225

2326
impl std::error::Error for ValidationError {}
@@ -29,11 +32,16 @@ impl std::fmt::Display for ValidationError {
2932
f.write_str("Failed to broadcast: ")?;
3033
f.write_str(text.strip_suffix('\n').unwrap_or(text))
3134
}
35+
Self::Unknown(text) => {
36+
f.write_str("Unknown error: ")?;
37+
f.write_str(text.strip_suffix('\n').unwrap_or(text))
38+
}
3239
}
3340
}
3441
}
3542

3643
/// A simple HTTP REST client for a VeChain node.
44+
#[derive(Clone, Debug)]
3745
pub struct ThorNode {
3846
/// API base url
3947
pub base_url: Url,
@@ -286,6 +294,13 @@ pub struct BlockInfo {
286294
pub signer: Address,
287295
}
288296

297+
impl BlockInfo {
298+
pub const fn block_ref(&self) -> u64 {
299+
//! Extract blockRef for transaction.
300+
self.id.0[3]
301+
}
302+
}
303+
289304
/// Transaction data included in the block extended details.
290305
///
291306
/// Combines [`ExtendedTransaction`] and [`Receipt`].
@@ -563,6 +578,12 @@ impl ThorNode {
563578
}
564579
}
565580

581+
pub async fn fetch_best_block(&self) -> AResult<(BlockInfo, Vec<U256>)> {
582+
//! Retrieve a best block from node.
583+
let info = self.fetch_block(BlockReference::Best).await?;
584+
Ok(info.ok_or(ValidationError::Unknown("Best block not found".to_string()))?)
585+
}
586+
566587
pub async fn fetch_block_expanded(
567588
&self,
568589
block_ref: BlockReference,

src/transaction_builder.rs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use rand::Rng;
2+
3+
use crate::address::Address;
4+
use crate::network::ThorNode;
5+
use crate::rlp::Bytes;
6+
use crate::transactions::{Clause, Reserved, Transaction};
7+
use crate::U256;
8+
9+
#[derive(Clone, Debug, Eq, PartialEq, Default)]
10+
struct TransactionTemplate {
11+
block_ref: Option<u64>,
12+
expiration: Option<u32>,
13+
clauses: Vec<Clause>,
14+
gas_price_coef: Option<u8>,
15+
gas: Option<u64>,
16+
depends_on: Option<U256>,
17+
nonce: Option<u64>,
18+
delegated: bool,
19+
}
20+
21+
/// Transaction builder allows to create and prepare transactions
22+
/// with minimal developers efforts.
23+
#[derive(Clone, Debug)]
24+
pub struct TransactionBuilder {
25+
node: ThorNode,
26+
template: TransactionTemplate,
27+
}
28+
29+
impl TransactionBuilder {
30+
pub fn new(node: ThorNode) -> Self {
31+
//! Create a new builder.
32+
Self {
33+
node,
34+
template: TransactionTemplate::default(),
35+
}
36+
}
37+
pub const fn delegated(mut self) -> Self {
38+
//! Make a transaction delegated.
39+
self.template.delegated = true;
40+
self
41+
}
42+
pub const fn nonce(mut self, nonce: u64) -> Self {
43+
//! Set a nonce for transaction.
44+
self.template.nonce = Some(nonce);
45+
self
46+
}
47+
pub const fn depends_on(mut self, depends_on: U256) -> Self {
48+
//! Mark a transaction as dependent on another one.
49+
self.template.depends_on = Some(depends_on);
50+
self
51+
}
52+
pub const fn gas(mut self, gas: u64) -> Self {
53+
//! Set maximal gas amount for transaction.
54+
self.template.gas = Some(gas);
55+
self
56+
}
57+
pub const fn gas_price_coef(mut self, gas_price_coef: u8) -> Self {
58+
//! Set gas price coefficient for transaction.
59+
self.template.gas_price_coef = Some(gas_price_coef);
60+
self
61+
}
62+
pub const fn expiration(mut self, expiration: u32) -> Self {
63+
//! Set expiration for transaction in blocks, starting from `block_ref`.
64+
self.template.expiration = Some(expiration);
65+
self
66+
}
67+
pub const fn block_ref(mut self, block_ref: u64) -> Self {
68+
//! Set block_ref for transaction to count `expiration` from.
69+
self.template.block_ref = Some(block_ref);
70+
self
71+
}
72+
pub fn add_transfer<T: Into<U256>>(self, recipient: Address, value: T) -> Self {
73+
//! Add a simple transfer to clauses.
74+
self.add_clause(Clause {
75+
to: Some(recipient),
76+
value: value.into(),
77+
data: Bytes::new(),
78+
})
79+
}
80+
pub fn add_contract_create(self, contract_bytes: Bytes) -> Self {
81+
//! Add a contract creation clause.
82+
self.add_clause(Clause {
83+
to: None,
84+
value: U256::zero(),
85+
data: contract_bytes,
86+
})
87+
}
88+
pub fn add_contract_call(self, contract_address: Address, call_bytes: Bytes) -> Self {
89+
//! Add a contract method call clause.
90+
self.add_clause(Clause {
91+
to: Some(contract_address),
92+
value: U256::zero(),
93+
data: call_bytes,
94+
})
95+
}
96+
pub fn add_clause(mut self, clause: Clause) -> Self {
97+
//! Add an arbitrary, user-provided clause.
98+
self.template.clauses.push(clause);
99+
self
100+
}
101+
102+
pub async fn build(&self) -> Result<Transaction, TransactionBuilderError> {
103+
//! Prepare a `Transaction`. This may perform a network request
104+
//! to identify appropriate parameters.
105+
if self.template.clauses.is_empty() {
106+
return Err(TransactionBuilderError::EmptyTransaction);
107+
}
108+
let block_ref = match self.template.block_ref {
109+
Some(r) => r,
110+
None => self
111+
.node
112+
.fetch_best_block()
113+
.await
114+
.map_err(|_| TransactionBuilderError::NetworkError)?
115+
.0
116+
.block_ref(),
117+
};
118+
let mut tx = Transaction {
119+
chain_tag: self.node.chain_tag,
120+
block_ref,
121+
expiration: self.template.expiration.unwrap_or(128),
122+
clauses: self.template.clauses.clone(),
123+
gas_price_coef: self.template.gas_price_coef.unwrap_or(0),
124+
gas: self.template.gas.unwrap_or(0),
125+
depends_on: self.template.depends_on,
126+
nonce: self.template.nonce.unwrap_or_else(|| {
127+
let mut rng = rand::thread_rng();
128+
rng.gen::<u64>()
129+
}),
130+
reserved: if self.template.delegated {
131+
Some(Reserved::new_delegated())
132+
} else {
133+
None
134+
},
135+
signature: None,
136+
};
137+
if self.template.gas.is_some() {
138+
Ok(tx)
139+
} else if tx.clauses.iter().all(|clause| clause.data.is_empty()) {
140+
tx.gas = tx.intrinsic_gas();
141+
Ok(tx)
142+
} else {
143+
Err(TransactionBuilderError::CannotEstimateGas)
144+
}
145+
}
146+
}
147+
148+
/// Transaction creation errors
149+
#[derive(Clone, Debug, Eq, PartialEq)]
150+
pub enum TransactionBuilderError {
151+
/// Network error (failed to fetch data from node)
152+
NetworkError,
153+
/// No clauses provided
154+
EmptyTransaction,
155+
/// Transaction clauses involve contract interaction, and gas was not provided.
156+
CannotEstimateGas,
157+
}
158+
159+
impl std::error::Error for TransactionBuilderError {}
160+
impl std::fmt::Display for TransactionBuilderError {
161+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162+
match self {
163+
Self::NetworkError => f.write_str("Failed to retrieve data from network"),
164+
Self::EmptyTransaction => f.write_str("Cannot build an empty transaction - make sure to add at least one clause first."),
165+
Self::CannotEstimateGas => f.write_str("Transaction clauses involve contract interaction, please provide gas amount explicitly."),
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)