Skip to content

cartesi/tx-manager

Repository files navigation

Transaction Manager

The eth-tx-manager is a Rust library for submitting transactions to the blockchain. It tries to account for the many scenarios that can befall a transaction sent to the transaction pool and adjust accordingly.

Most notably, it:

  • Manages nonces automatically.
  • Resends transactions if necessary.
  • Waits for a given number of block confirmations.
  • Recovers from hard crashes.

Usage example

The code for this example is available at examples/send_transaction.rs. You can run it with the cargo run --example send_transaction command. You need to have a local geth node running on dev mode.

To start sending transactions we must first instantiate a TransactionManager object by calling its constructor.

TransactionManager::new(provider, gas_oracle, database, chain, configuration)

The chain parameter is a Chain object that holds a chain ID and a boolean indicating whether or not the target blockchain implements the EIP-1559. We can instantiate it by calling the Chain::new or Chain::legacy functions.

let chain = Chain::new(1337);

The provider is an object that implements the ethers::providers::Middleware trait. (In our examples, we will send transactions to a local geth node running on develop mode.)

let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();

The provider is responsible for signing the transactions we will be sending, therefore, we wrap it using a signer middleware.

let wallet = LocalWallet::new(&mut thread_rng()).with_chain_id(chain.id);
let provider = SignerMiddleware::new(provider, wallet);

The gas_oracle and database parameters are dependencies injected into the transaction manager to, respectivelly, deal with gas prices and guarantee robustness. The configuration is used for fine tuning internal waiting times. We will discuss these in the next sections but, for now, we will use their default provided implementations.

let gas_oracle = DefaultGasOracle::new();
let database = FileSystemDatabase::new("database.json".to_string());
let configuration = Configuration::default();

We can, then, instantiate the transaction manager:

let (manager, receipt) =
    TransactionManager::new(provider, gas_oracle, database, chain, configuration)
        .await
        .unwrap();

assert!(receipt.is_none());

Note that the new function is asynchronous and returns both a transaction manager and a Option<ethers::types::TransactionReceipt>. We designed the transaction manager to be robust. In case the manager fails or gets interrupted while sending a transaction (a hardware crash, for example), it will try to confirm that transaction during its next instantiation, thus, the possible receipt and the need for async. In essence, this guarantees that we always deal with pending transactions.

With the manager in hands we can send a transaction by calling the aptly named send_transaction method.

pub async fn send_transaction(
    mut self,
    transaction: Transaction,
    confirmations: usize,
    priority: Priority,
) -> Result<(Self, TransactionReceipt), Error<M, GO, DB>> {

The send_transaction method takes a mut self transaction manager, effectivelly taking ownership of the manager instance. When the function is done, it returns that instance alongside the expected transaction receipt. This enforces through the type system that (1) we will need to instantiate a new manager in case the function fails and (2) we can only send transactions sequentially. Property 1 guarantees that any pending transactions will be managed by the constructor and property 2 enforces synchronicity (concurrency leads to a lot of undesired complexity).

The function also takes a confirmations argument. The number of confirmations is the number of blocks that must be mined, after the transaction is placed in a block, so that the function returns successfully. In other words, if the number of confirmations is 0, the function returns immediatelly after the transaction gets mined, otherwise, it will wait for confirmations more blocks to be mined before returning. (This basically accounts for network reorganizations.)

The priority level is a parameter sent to the gas oracle to calculate the appropriate gas fees. Higher priorities cost more, but reduce waiting times, and vice-versa for lower priorities.

let transaction = Transaction {
    from: wallet.address(),
    to: H160::random(),
    value: Value::Number(U256::from(1e9 as u64)),
    call_data: None,
};

let result = manager
    .send_transaction(transaction, 1, Priority::Normal)
    .await;
assert!(result.is_err());

In our contrived example, we are sending funds from a random wallet, so it is pretty clear that that transaction will fail with an "insufficient funds" error.

Gas Oracle

TODO.

Database

TODO.

Configuration

TODO.

Inner workings

TODO.