Skip to content

Commit

Permalink
evm transaction retries, satisfy the TransactionSigner interface
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrostr committed Jan 29, 2025
1 parent c0c8a24 commit 6a30eb4
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 36 deletions.
2 changes: 1 addition & 1 deletion listen-kit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description = "Blockchain actions for AI agents"
documentation = "https://docs.listen-rs.com"

[features]
default = ["solana"]
default = ["full"]
full = ["http", "solana", "evm"]
http = [
"solana",
Expand Down
32 changes: 19 additions & 13 deletions listen-kit/src/evm/trade.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::str::FromStr;

use alloy::network::EthereumWallet;
use alloy::primitives::Address;
use alloy::{
network::TransactionBuilder, providers::Provider,
rpc::types::TransactionRequest, signers::local::PrivateKeySigner,
rpc::types::TransactionRequest,
};
use anyhow::{Context, Result};
use uniswap_sdk_core::{prelude::*, token};
Expand Down Expand Up @@ -34,7 +35,7 @@ pub async fn trade(
input_amount: String,
output_token_address: String,
provider: &EvmProvider,
signer: PrivateKeySigner,
wallet: &EthereumWallet,
) -> Result<String> {
// Convert addresses from string to Address type
let input_addr = Address::from_str(&input_token_address)?;
Expand Down Expand Up @@ -65,7 +66,7 @@ pub async fn trade(

if !check_allowance(
input_addr,
signer.address(),
wallet.default_signer().address(),
router_address,
amount,
provider,
Expand All @@ -85,12 +86,12 @@ pub async fn trade(
};

let request = TransactionRequest::default()
.with_from(signer.address())
.with_from(wallet.default_signer().address())
.with_to(input_addr)
.with_call(&call)
.with_gas_price(gas_price);

send_transaction(request, provider, signer.clone()).await?;
send_transaction(request, provider, wallet).await?;
// should probably wait for the tx here and verify approvals, but retries will handle this
}

Expand All @@ -116,31 +117,31 @@ pub async fn trade(
let params = swap_call_parameters(
&mut [trade],
SwapOptions {
recipient: signer.address(),
recipient: wallet.default_signer().address(),
..Default::default()
},
)
.context("Failed to get swap parameters")?;

let request = TransactionRequest::default()
.with_from(signer.address())
.with_from(wallet.default_signer().address())
.with_to(router_address)
.with_input(params.calldata)
.with_value(params.value)
.with_gas_price(gas_price);

send_transaction(request, provider, signer).await
send_transaction(request, provider, wallet).await
}

#[cfg(test)]
mod tests {
use super::*;
use crate::evm::util::{make_provider, make_signer};
use crate::evm::util::{make_provider, make_wallet};

#[tokio::test]
async fn test_trade_evm() {
let provider = make_provider().unwrap();
let signer = make_signer().unwrap();
let wallet = make_wallet().unwrap();

// WETH on arbitrum
let output_token =
Expand All @@ -151,9 +152,14 @@ mod tests {
// 1 usdc
let input_amount = "1000000".to_string();

let result =
trade(input_token, input_amount, output_token, &provider, signer)
.await;
let result = trade(
input_token,
input_amount,
output_token,
&provider,
&wallet,
)
.await;

assert!(result.is_ok(), "Trade failed: {:?}", result);
}
Expand Down
48 changes: 44 additions & 4 deletions listen-kit/src/evm/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,57 @@
use alloy::network::{EthereumWallet, TransactionBuilder};
use alloy::providers::Provider;
use alloy::rpc::types::TransactionRequest;
use alloy::signers::local::PrivateKeySigner;
use anyhow::{Context, Result};
use std::time::Duration;
use tokio::time::sleep;

use super::util::EvmProvider;

pub async fn send_transaction(
request: TransactionRequest,
provider: &EvmProvider,
signer: PrivateKeySigner,
wallet: &EthereumWallet,
) -> Result<String> {
const MAX_RETRIES: u32 = 3;
const RETRY_DELAY: Duration = Duration::from_secs(1);

for attempt in 0..MAX_RETRIES {
if attempt > 0 {
tracing::info!("Retry attempt {} for transaction", attempt);
sleep(RETRY_DELAY).await;
}

match try_send_transaction(request.clone(), provider, wallet).await {
Ok(hash) => return Ok(hash),
Err(e) => {
if attempt == MAX_RETRIES - 1 {
return Err(e);
}
tracing::warn!("Transaction failed: {:?}. Retrying...", e);
}
}
}

Err(anyhow::anyhow!(
"Failed to send transaction after all retries"
))
}

async fn try_send_transaction(
request: TransactionRequest,
provider: &EvmProvider,
wallet: &EthereumWallet,
) -> Result<String> {
tracing::info!(?request, "Sending transaction");

let address = wallet.default_signer().address();

// Get the latest nonce
let nonce = provider
.get_transaction_count(address)
.await
.context("Failed to get nonce")?;

// Estimate gas
let gas_limit = provider
.estimate_gas(&request)
Expand All @@ -22,8 +62,8 @@ pub async fn send_transaction(
let tx = request
.with_gas_limit(gas_limit)
.with_chain_id(provider.get_chain_id().await?)
.with_nonce(provider.get_transaction_count(signer.address()).await?)
.build(&EthereumWallet::from(signer))
.with_nonce(nonce)
.build(wallet)
.await?;

// Send transaction and wait for receipt
Expand Down
31 changes: 16 additions & 15 deletions listen-kit/src/evm/transfer.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use alloy::network::TransactionBuilder;
use alloy::network::{EthereumWallet, TransactionBuilder};
use alloy::primitives::{Address, U256};
use alloy::providers::Provider;
use alloy::rpc::types::TransactionRequest;
use alloy::signers::local::PrivateKeySigner;
use anyhow::{Context, Result};

use super::abi::IERC20;
Expand All @@ -14,7 +13,7 @@ pub async fn transfer_eth(
to: Address,
amount: U256,
provider: &EvmProvider,
signer: PrivateKeySigner,
wallet: &EthereumWallet,
) -> Result<String> {
// Get the current gas price
let gas_price = provider
Expand All @@ -29,7 +28,7 @@ pub async fn transfer_eth(
.with_value(amount)
.with_gas_price(gas_price);

send_transaction(request, provider, signer).await
send_transaction(request, provider, wallet).await
}

pub async fn transfer_erc20(
Expand All @@ -38,7 +37,7 @@ pub async fn transfer_erc20(
to: Address,
amount: U256,
provider: &EvmProvider,
signer: PrivateKeySigner,
wallet: &EthereumWallet,
) -> Result<String> {
// Create contract instance
let call = IERC20::transferCall { to, amount };
Expand All @@ -55,39 +54,41 @@ pub async fn transfer_erc20(
.with_call(&call)
.with_gas_price(gas_price);

send_transaction(request, provider, signer).await
send_transaction(request, provider, wallet).await
}

#[cfg(test)]
mod tests {
use super::*;
use crate::evm::util::{make_provider, make_signer};
use crate::evm::util::{make_provider, make_wallet};
use alloy::primitives::{address, U256};

#[tokio::test]
async fn test_transfer_eth() {
let provider = make_provider().unwrap();
let signer = make_signer().unwrap();
let from = signer.address();
let to = signer.address();
let wallet = make_wallet().unwrap();
let address = wallet.default_signer().address();
let from = address;
let to = address;
let amount = U256::from(10000000000000u64); // 0.00001 ETH

let result = transfer_eth(from, to, amount, &provider, signer).await;
let result = transfer_eth(from, to, amount, &provider, &wallet).await;
assert!(result.is_ok(), "Transfer failed: {:?}", result);
}

#[tokio::test]
async fn test_transfer_erc20() {
let provider = make_provider().unwrap();
let signer = make_signer().unwrap();
let from = signer.address();
let to = signer.address();
let wallet = make_wallet().unwrap();
let address = wallet.default_signer().address();
let from = address;
let to = address;
// USDC token address on ARB mainnet
let token = address!("0xaf88d065e77c8cc2239327c5edb3a432268e5831");
let amount = U256::from(1000000); // 1 USDC (6 decimals)

let result =
transfer_erc20(from, token, to, amount, &provider, signer).await;
transfer_erc20(from, token, to, amount, &provider, &wallet).await;
assert!(result.is_ok(), "Transfer failed: {:?}", result);
}
}
7 changes: 6 additions & 1 deletion listen-kit/src/evm/util.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::str::FromStr;

use alloy::network::EthereumWallet;
use alloy::providers::{ProviderBuilder, RootProvider};
use alloy::signers::local::PrivateKeySigner;
use alloy::transports::http::{Client, Http};
use anyhow::{anyhow, Result};
use anyhow::Result;

pub type EvmProvider = RootProvider<Http<Client>>;

Expand All @@ -16,6 +17,10 @@ pub fn make_signer() -> Result<PrivateKeySigner> {
Ok(PrivateKeySigner::from_str(&env("ETHEREUM_PRIVATE_KEY"))?)
}

pub fn make_wallet() -> Result<EthereumWallet> {
Ok(EthereumWallet::from(make_signer()?))
}

pub fn env(var: &str) -> String {
std::env::var(var).unwrap_or_else(|_| panic!("{} env var not set", var))
}
7 changes: 5 additions & 2 deletions listen-kit/src/signer/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use anyhow::Result;
use async_trait::async_trait;
use std::str::FromStr;

use crate::evm::transaction::send_transaction;
use crate::evm::util::make_provider;

use super::TransactionSigner;

pub struct LocalEvmSigner {
Expand All @@ -28,8 +31,8 @@ impl TransactionSigner for LocalEvmSigner {

async fn sign_and_send_evm_transaction(
&self,
_tx: alloy::rpc::types::TransactionRequest,
tx: alloy::rpc::types::TransactionRequest,
) -> Result<String> {
todo!()
send_transaction(tx, &make_provider()?, &self.wallet).await
}
}

0 comments on commit 6a30eb4

Please sign in to comment.