Skip to content
This repository has been archived by the owner on Jan 8, 2025. It is now read-only.

Commit

Permalink
feat(alchemy): add alchemy token methods (#1266)
Browse files Browse the repository at this point in the history
* feat(alchemy): add alchemy token methods

* cleanup

* fix dep

* fix comments

* Update Makefile

* fix
  • Loading branch information
tcoratger authored Jul 5, 2024
1 parent 4003bd0 commit 07b7d8b
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 95 deletions.
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ http-body-util = { version = "0.1", default-features = false }
pin-project-lite = { version = "0.2", default-features = false }

# Testing crates
alloy-dyn-abi = { version = "0.7.6", default-features = false, optional = true }
alloy-dyn-abi = { version = "0.7.6", default-features = false }
alloy-json-abi = { version = "0.7.6", default-features = false, optional = true }
alloy-primitives = { version = "0.7.2", default-features = false, optional = true }
alloy-signer-wallet = { version = "0.1.0", default-features = false, optional = true }
Expand Down Expand Up @@ -148,7 +148,6 @@ toml = { version = "0.8", default-features = false }

[features]
testing = [
"alloy-dyn-abi",
"alloy-json-abi",
"alloy-primitives",
"alloy-signer-wallet",
Expand Down
98 changes: 82 additions & 16 deletions src/eth_provider/contracts/erc20.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#![allow(clippy::pub_underscore_fields)]

use alloy_dyn_abi::DynSolType;
use alloy_sol_types::{sol, SolCall};
use reth_primitives::Address;
use reth_primitives::{BlockId, TxKind, U256};
use reth_primitives::{BlockId, Bytes, TxKind, U256};
use reth_rpc_types::request::TransactionInput;
use reth_rpc_types::TransactionRequest;

Expand All @@ -15,39 +16,104 @@ sol! {
contract ERC20Contract {
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
function decimals() external view returns (uint8);
function name() external view returns (string);
function symbol() external view returns (string);
}
}

/// Abstraction for a Kakarot ERC20 contract.
#[derive(Debug)]
pub struct EthereumErc20<P: EthereumProvider> {
/// The address of the ERC20 contract.
pub address: Address,
/// The provider for interacting with the Ethereum network.
pub provider: P,
}

impl<P: EthereumProvider> EthereumErc20<P> {
/// Creates a new instance of [`EthereumErc20`].
pub const fn new(address: Address, provider: P) -> Self {
Self { address, provider }
}

pub async fn balance_of(self, evm_address: Address, block_id: BlockId) -> EthProviderResult<U256> {
// Get the calldata for the function call.
/// Gets the balance of the specified address.
pub async fn balance_of(&self, evm_address: Address, block_id: BlockId) -> EthProviderResult<U256> {
// Encode the calldata for the balanceOf function call
let calldata = ERC20Contract::balanceOfCall { account: evm_address }.abi_encode();

let request = TransactionRequest {
from: Some(Address::default()),
to: Some(TxKind::Call(self.address)),
gas_price: Some(0),
gas: Some(1_000_000),
value: Some(U256::ZERO),
input: TransactionInput { input: Some(calldata.into()), data: None },
..Default::default()
};

let ret = self.provider.call(request, Some(block_id)).await?;
// Call the contract with the encoded calldata
let ret = self.call_contract(calldata, block_id).await?;
// Deserialize the returned bytes into a U256 balance
let balance = U256::try_from_be_slice(&ret)
.ok_or_else(|| ExecutionError::Other("failed to deserialize balance".to_string()))?;

Ok(balance)
}

/// Gets the number of decimals the token uses.
pub async fn decimals(&self, block_id: BlockId) -> EthProviderResult<U256> {
// Encode the calldata for the decimals function call
let calldata = ERC20Contract::decimalsCall {}.abi_encode();
// Call the contract with the encoded calldata
let ret = self.call_contract(calldata, block_id).await?;
// Deserialize the returned bytes into a U256 representing decimals
let decimals = U256::try_from_be_slice(&ret)
.ok_or_else(|| ExecutionError::Other("failed to deserialize decimals".to_string()))?;
Ok(decimals)
}

/// Gets the name of the token.
pub async fn name(&self, block_id: BlockId) -> EthProviderResult<String> {
// Encode the calldata for the name function call
let calldata = ERC20Contract::nameCall {}.abi_encode();
// Call the contract with the encoded calldata
let ret = self.call_contract(calldata, block_id).await?;
// Deserialize the returned bytes into a String representing the name
let name = DynSolType::String
.abi_decode(&ret)
.map_err(|_| ExecutionError::Other("failed to deserialize name".to_string()))?;
Ok(name.as_str().unwrap_or_default().to_string())
}

/// Gets the symbol of the token.
pub async fn symbol(&self, block_id: BlockId) -> EthProviderResult<String> {
// Encode the calldata for the symbol function call
let calldata = ERC20Contract::symbolCall {}.abi_encode();
// Call the contract with the encoded calldata
let ret = self.call_contract(calldata, block_id).await?;
// Deserialize the returned bytes into a String representing the symbol
let symbol = DynSolType::String
.abi_decode(&ret)
.map_err(|_| ExecutionError::Other("failed to deserialize symbol".to_string()))?;
Ok(symbol.as_str().unwrap_or_default().to_string())
}

/// Gets the allowance the owner has granted to the spender.
pub async fn allowance(&self, owner: Address, spender: Address, block_id: BlockId) -> EthProviderResult<U256> {
// Encode the calldata for the allowance function call
let calldata = ERC20Contract::allowanceCall { owner, spender }.abi_encode();
// Call the contract with the encoded calldata
let ret = self.call_contract(calldata, block_id).await?;
// Deserialize the returned bytes into a U256 representing the allowance
let allowance = U256::try_from_be_slice(&ret)
.ok_or_else(|| ExecutionError::Other("failed to deserialize allowance".to_string()))?;
Ok(allowance)
}

/// Calls the contract with the given calldata.
async fn call_contract(&self, calldata: Vec<u8>, block_id: BlockId) -> EthProviderResult<Bytes> {
self.provider
.call(
TransactionRequest {
from: Some(Address::default()),
to: Some(TxKind::Call(self.address)),
gas_price: Some(0),
gas: Some(1_000_000),
value: Some(U256::ZERO),
input: TransactionInput { input: Some(calldata.into()), data: None },
..Default::default()
},
Some(block_id),
)
.await
}
}
10 changes: 8 additions & 2 deletions src/eth_rpc/api/alchemy_api.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
use crate::models::balance::TokenBalances;
use crate::models::token::{TokenBalances, TokenMetadata};
use jsonrpsee::core::RpcResult as Result;
use jsonrpsee::proc_macros::rpc;
use reth_primitives::Address;
use reth_primitives::{Address, U256};

#[rpc(server, namespace = "alchemy")]
#[async_trait]
pub trait AlchemyApi {
#[method(name = "getTokenBalances")]
async fn token_balances(&self, address: Address, contract_addresses: Vec<Address>) -> Result<TokenBalances>;

#[method(name = "getTokenMetadata")]
async fn token_metadata(&self, contract_address: Address) -> Result<TokenMetadata>;

#[method(name = "getTokenAllowance")]
async fn token_allowance(&self, contract_address: Address, owner: Address, spender: Address) -> Result<U256>;
}
66 changes: 55 additions & 11 deletions src/eth_rpc/servers/alchemy_rpc.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,85 @@
#![allow(clippy::blocks_in_conditions)]

use futures::future::join_all;
use jsonrpsee::core::{async_trait, RpcResult};
use reth_primitives::{Address, BlockId, BlockNumberOrTag};
use reth_primitives::{Address, BlockId, BlockNumberOrTag, U256};

use crate::eth_provider::contracts::erc20::EthereumErc20;
use crate::eth_provider::error::EthApiError;
use crate::eth_provider::provider::EthereumProvider;
use crate::eth_rpc::api::alchemy_api::AlchemyApiServer;
use crate::models::balance::TokenBalanceFuture;
use crate::{eth_provider::provider::EthereumProvider, models::balance::TokenBalances};
use crate::models::token::{TokenBalance, TokenBalances, TokenMetadata};

/// The RPC module for the Ethereum protocol required by Kakarot.
#[derive(Debug)]
pub struct AlchemyRpc<P: EthereumProvider> {
/// The provider for interacting with the Ethereum network.
eth_provider: P,
}

impl<P: EthereumProvider> AlchemyRpc<P> {
/// Creates a new instance of [`AlchemyRpc`].
pub const fn new(eth_provider: P) -> Self {
Self { eth_provider }
}
}

#[async_trait]
impl<P: EthereumProvider + Send + Sync + 'static> AlchemyApiServer for AlchemyRpc<P> {
#[tracing::instrument(skip_all, ret, fields(address = %address, token_addresses = ?token_addresses))]
/// Retrieves the token balances for a given address.
#[tracing::instrument(skip_all, ret, fields(address = %address), err)]
async fn token_balances(&self, address: Address, token_addresses: Vec<Address>) -> RpcResult<TokenBalances> {
tracing::info!("Serving alchemy_getTokenBalances");

// Set the block ID to the latest block
let block_id = BlockId::Number(BlockNumberOrTag::Latest);

Ok(TokenBalances {
address,
token_balances: join_all(token_addresses.into_iter().map(|token_address| async move {
// Create a new instance of `EthereumErc20` for each token address
let token = EthereumErc20::new(token_address, &self.eth_provider);
// Retrieve the balance for the given address
let token_balance = token.balance_of(address, block_id).await?;
Ok(TokenBalance { token_address, token_balance })
}))
.await
.into_iter()
.collect::<Result<Vec<_>, EthApiError>>()?,
})
}

/// Retrieves the metadata for a given token.
#[tracing::instrument(skip(self), ret, err)]
async fn token_metadata(&self, token_address: Address) -> RpcResult<TokenMetadata> {
tracing::info!("Serving alchemy_getTokenMetadata");

// Set the block ID to the latest block
let block_id = BlockId::Number(BlockNumberOrTag::Latest);
let handles = token_addresses.into_iter().map(|token_addr| {
let token = EthereumErc20::new(token_addr, &self.eth_provider);
let balance = token.balance_of(address, block_id);
// Create a new instance of `EthereumErc20`
let token = EthereumErc20::new(token_address, &self.eth_provider);

// Await all futures concurrently to retrieve decimals, name, and symbol
let (decimals, name, symbol) =
futures::try_join!(token.decimals(block_id), token.name(block_id), token.symbol(block_id))?;

TokenBalanceFuture::new(Box::pin(balance), token_addr)
});
// Return the metadata
Ok(TokenMetadata { decimals, name, symbol })
}

let token_balances = join_all(handles).await.into_iter().collect::<Result<Vec<_>, EthApiError>>()?;
/// Retrieves the allowance of a given owner for a spender.
#[tracing::instrument(skip(self), ret, err)]
async fn token_allowance(&self, token_address: Address, owner: Address, spender: Address) -> RpcResult<U256> {
tracing::info!("Serving alchemy_getTokenAllowance");

// Set the block ID to the latest block
let block_id = BlockId::Number(BlockNumberOrTag::Latest);
// Create a new instance of `EthereumErc20`
let token = EthereumErc20::new(token_address, &self.eth_provider);
// Retrieve the allowance for the given owner and spender
let allowance = token.allowance(owner, spender, block_id).await?;

Ok(TokenBalances { address, token_balances })
// Return the allowance
Ok(allowance)
}
}
61 changes: 0 additions & 61 deletions src/models/balance.rs

This file was deleted.

2 changes: 1 addition & 1 deletion src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub mod balance;
pub mod block;
pub mod felt;
pub mod token;
pub mod transaction;
31 changes: 31 additions & 0 deletions src/models/token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use reth_primitives::{Address, U256};
use serde::{Deserialize, Serialize};

/// Represents the balance of a specific ERC20 token.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenBalance {
/// The address of the ERC20 token.
pub token_address: Address,
/// The balance of the ERC20 token.
pub token_balance: U256,
}

/// Represents the balances of multiple ERC20 tokens for a specific address.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenBalances {
/// The address for which the token balances are queried.
pub address: Address,
/// A list of token balances associated with the address.
pub token_balances: Vec<TokenBalance>,
}

/// Represents the metadata (decimals, name, symbol) of an ERC20 token.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenMetadata {
/// The number of decimals the token uses.
pub decimals: U256,
/// The name of the token.
pub name: String,
/// The symbol of the token.
pub symbol: String,
}
Loading

0 comments on commit 07b7d8b

Please sign in to comment.