Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add alchemyTokenMethods #339

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 17 additions & 6 deletions crates/core/src/client/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ use starknet::providers::Provider;
use starknet::signers::LocalWallet;

use super::errors::EthApiError;
use crate::models::allowance::TokenAllowance;
use crate::models::balance::TokenBalances;
use crate::models::metadata::TokenMetadata;
use crate::models::transaction::StarknetTransactions;

#[async_trait]
Expand Down Expand Up @@ -62,12 +64,6 @@ pub trait KakarotEthApi<P: Provider + Send + Sync>: KakarotStarknetApi<P> + Send
block_id: BlockId,
) -> Result<U256, EthApiError<P::Error>>;

async fn token_balances(
&self,
address: Address,
contract_addresses: Vec<Address>,
) -> Result<TokenBalances, EthApiError<P::Error>>;

async fn send_transaction(&self, bytes: Bytes) -> Result<H256, EthApiError<P::Error>>;

async fn get_transaction_count_by_block(&self, block_id: BlockId) -> Result<U64, EthApiError<P::Error>>;
Expand All @@ -86,6 +82,21 @@ pub trait KakarotEthApi<P: Provider + Send + Sync>: KakarotStarknetApi<P> + Send
async fn estimate_gas(&self, request: CallRequest, block_id: BlockId) -> Result<U256, EthApiError<P::Error>>;

async fn gas_price(&self) -> Result<U256, EthApiError<P::Error>>;

async fn token_allowance(
&self,
contract_address: Address,
account_address: Address,
spender_address: Address,
) -> Result<TokenAllowance, EthApiError<P::Error>>;

async fn token_balances(
&self,
address: Address,
contract_addresses: Vec<Address>,
) -> Result<TokenBalances, EthApiError<P::Error>>;

async fn token_metadata(&self, contract_address: Address) -> Result<TokenMetadata, EthApiError<P::Error>>;
}

#[async_trait]
Expand Down
2 changes: 2 additions & 0 deletions crates/core/src/client/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub enum DataDecodingError {
TransactionDecodingError(#[from] DecodeError),
#[error("{entrypoint} returned invalid array length, expected {expected}, got {actual}")]
InvalidReturnArrayLength { entrypoint: String, expected: usize, actual: usize },
#[error("failed to convert bytes to string {0}")]
InvalidBytesString(String),
}

#[derive(Debug)]
Expand Down
85 changes: 65 additions & 20 deletions crates/core/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use crate::contracts::contract_account::ContractAccount;
use crate::contracts::erc20::ethereum_erc20::EthereumErc20;
use crate::contracts::erc20::starknet_erc20::StarknetErc20;
use crate::contracts::kakarot::KakarotContract;
use crate::models::allowance::TokenAllowance;
use crate::models::balance::{FutureTokenBalance, TokenBalances};
use crate::models::block::{BlockWithTxHashes, BlockWithTxs, EthBlockId};
use crate::models::convertible::{
Expand All @@ -57,6 +58,7 @@ use crate::models::convertible::{
use crate::models::event::StarknetEvent;
use crate::models::event_filter::EthEventFilter;
use crate::models::felt::Felt252Wrapper;
use crate::models::metadata::TokenMetadata;
use crate::models::transaction::{StarknetTransaction, StarknetTransactions};
use crate::models::transaction_receipt::StarknetTransactionReceipt as TransactionReceiptWrapper;
use crate::models::ConversionError;
Expand Down Expand Up @@ -386,26 +388,6 @@ impl<P: Provider + Send + Sync + 'static> KakarotEthApi<P> for KakarotClient<P>
Ok(storage_value)
}

/// Returns token balances for a specific address given a list of contracts addresses.
async fn token_balances(
&self,
address: Address,
token_addresses: Vec<Address>,
) -> Result<TokenBalances, EthApiError<P::Error>> {
let block_id = BlockId::Number(BlockNumberOrTag::Latest);

let handles = token_addresses.into_iter().map(|token_address| {
let token_addr: Felt252Wrapper = token_address.into();
let token = EthereumErc20::new(token_addr.into(), &self.kakarot_contract);

FutureTokenBalance::<P, _>::new(token.balance_of(address.into(), block_id), token_address)
});

let token_balances = join_all(handles).await;

Ok(TokenBalances { address, token_balances })
}

/// Sends raw Ethereum transaction bytes to Kakarot
async fn send_transaction(&self, bytes: Bytes) -> Result<H256, EthApiError<P::Error>> {
let mut data = bytes.as_ref();
Expand Down Expand Up @@ -592,6 +574,69 @@ impl<P: Provider + Send + Sync + 'static> KakarotEthApi<P> for KakarotClient<P>

Ok(U256::from(fee_estimate.gas_price))
}

/// Returns the amount which the sender is allowed to withdraw from the owner.
async fn token_allowance(
&self,
contract_address: Address,
account_address: Address,
spender_address: Address,
) -> Result<TokenAllowance, EthApiError<P::Error>> {
let block_id = BlockId::Number(BlockNumberOrTag::Latest);

let contract_addr: Felt252Wrapper = contract_address.into();
let contract = EthereumErc20::new(contract_addr.into(), &self.kakarot_contract);

let token_allowance = contract.allowance(account_address.into(), spender_address.into(), block_id).await;

let token_allowance = match token_allowance {
Ok(a) => TokenAllowance { result: Some(a), error: None },
Err(err) => TokenAllowance { result: None, error: Some(err.to_string()) },
};

Ok(token_allowance)
}

/// Returns token balances for a specific address given a list of contracts addresses.
async fn token_balances(
&self,
address: Address,
token_addresses: Vec<Address>,
) -> Result<TokenBalances, EthApiError<P::Error>> {
let block_id = BlockId::Number(BlockNumberOrTag::Latest);

let handles = token_addresses.into_iter().map(|token_address| {
let token_addr: Felt252Wrapper = token_address.into();
let token = EthereumErc20::new(token_addr.into(), &self.kakarot_contract);

FutureTokenBalance::<P, _>::new(token.balance_of(address.into(), block_id), token_address)
});

let token_balances = join_all(handles).await;

Ok(TokenBalances { address, token_balances })
}

/// Returns metadata (name, symbol, decimals) for a given token contract address.
async fn token_metadata(&self, contract_address: Address) -> Result<TokenMetadata, EthApiError<P::Error>> {
let block_id = BlockId::Number(BlockNumberOrTag::Latest);

let contract_addr: Felt252Wrapper = contract_address.into();
let contract = EthereumErc20::new(contract_addr.into(), &self.kakarot_contract);

let name = contract.clone().name(block_id).await;
let symbol = contract.clone().symbol(block_id).await;
let decimals = contract.decimals(block_id).await;

let token_metadata = match (name, symbol, decimals) {
(Ok(n), Ok(s), Ok(d)) => TokenMetadata { name: Some(n), symbol: Some(s), decimals: Some(d), error: None },
(Err(err), _, _) | (_, Err(err), _) | (_, _, Err(err)) => {
TokenMetadata { name: None, symbol: None, decimals: None, error: Some(err.to_string()) }
}
};

Ok(token_metadata)
}
}

#[async_trait]
Expand Down
80 changes: 79 additions & 1 deletion crates/core/src/contracts/erc20/ethereum_erc20.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use ethers::abi::AbiEncode;
use ethers::prelude::abigen;
use ethers::types::Address;
use reth_primitives::{BlockId, U256};
use reth_primitives::{BlockId, U256, U8};
use starknet::core::types::BlockId as StarknetBlockId;
use starknet::providers::Provider;
use starknet_crypto::FieldElement;
Expand All @@ -20,6 +20,9 @@
r#"[
function balanceOf(address account) external view returns (uint256)
function allowance(address owner, address spender) external view returns (uint256)
function name() external view returns (string)
function symbol() external view returns (string)
function decimals() external view returns (uint8)
]"#,
);

Expand All @@ -29,10 +32,39 @@
kakarot_contract: &'a KakarotContract<P>,
}

impl<'a, P> Clone for EthereumErc20<'a, P> {
fn clone(&self) -> Self {
EthereumErc20 { address: self.address, kakarot_contract: self.kakarot_contract }
}
}

impl<'a, P: Provider + Send + Sync + 'static> EthereumErc20<'a, P> {
pub fn new(address: FieldElement, kakarot_contract: &'a KakarotContract<P>) -> Self {
Self { address, kakarot_contract }
}
pub async fn allowance(
self,
account_address: Address,
spender_address: Address,
block_id: BlockId,
) -> Result<U256, EthApiError<P::Error>> {
// Prepare the calldata for the bytecode function call
let calldata =
IERC20Calls::Allowance(AllowanceCall { owner: account_address, spender: spender_address }).encode();
let calldata = calldata.into_iter().map(FieldElement::from).collect();

Check failure on line 54 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / check

a value of type `&FieldElement` cannot be built from an iterator over elements of type `FieldElement`

Check failure on line 54 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / clippy

a value of type `&starknet_crypto::FieldElement` cannot be built from an iterator over elements of type `starknet_crypto::FieldElement`

let block_id = EthBlockId::new(block_id);
let block_id: StarknetBlockId = block_id.try_into()?;

let result = self.kakarot_contract.eth_call(&self.address, calldata, &block_id).await?;

Check failure on line 59 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / check

this method takes 4 arguments but 3 arguments were supplied

Check failure on line 59 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / clippy

this method takes 4 arguments but 3 arguments were supplied
let allowance: Vec<u8> = result.0.into();

Ok(U256::try_from_be_slice(allowance.as_slice()).ok_or(DataDecodingError::InvalidReturnArrayLength {
entrypoint: "allowance".into(),
expected: 32,
actual: allowance.len(),
})?)
}

pub async fn balance_of(self, evm_address: Address, block_id: BlockId) -> Result<U256, EthApiError<P::Error>> {
// Prepare the calldata for the bytecode function call
Expand All @@ -53,4 +85,50 @@
actual: balance.len(),
})?)
}

pub async fn name(self, block_id: BlockId) -> Result<String, EthApiError<P::Error>> {
// Prepare the calldata for the bytecode function call
let calldata = IERC20Calls::Symbol(SymbolCall).encode();
let calldata = calldata.into_iter().map(FieldElement::from).collect();

Check failure on line 92 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / check

a value of type `&FieldElement` cannot be built from an iterator over elements of type `FieldElement`

Check failure on line 92 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / clippy

a value of type `&starknet_crypto::FieldElement` cannot be built from an iterator over elements of type `starknet_crypto::FieldElement`

let block_id = EthBlockId::new(block_id);
let block_id: StarknetBlockId = block_id.try_into()?;

let result = self.kakarot_contract.eth_call(&self.address, calldata, &block_id).await?;

Check failure on line 97 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / check

this method takes 4 arguments but 3 arguments were supplied

Check failure on line 97 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / clippy

this method takes 4 arguments but 3 arguments were supplied
let name: Vec<u8> = result.0.into();

Ok(String::from_utf8(name).map_err(|err| DataDecodingError::InvalidBytesString(err.to_string()))?)
}

pub async fn symbol(self, block_id: BlockId) -> Result<String, EthApiError<P::Error>> {
// Prepare the calldata for the bytecode function call
let calldata = IERC20Calls::Symbol(SymbolCall).encode();
let calldata = calldata.into_iter().map(FieldElement::from).collect();

Check failure on line 106 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / check

a value of type `&FieldElement` cannot be built from an iterator over elements of type `FieldElement`

Check failure on line 106 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / clippy

a value of type `&starknet_crypto::FieldElement` cannot be built from an iterator over elements of type `starknet_crypto::FieldElement`

let block_id = EthBlockId::new(block_id);
let block_id: StarknetBlockId = block_id.try_into()?;

let result = self.kakarot_contract.eth_call(&self.address, calldata, &block_id).await?;

Check failure on line 111 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / check

this method takes 4 arguments but 3 arguments were supplied

Check failure on line 111 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / clippy

this method takes 4 arguments but 3 arguments were supplied
let symbol: Vec<u8> = result.0.into();

Ok(String::from_utf8(symbol).map_err(|err| DataDecodingError::InvalidBytesString(err.to_string()))?)
}

pub async fn decimals(self, block_id: BlockId) -> Result<U8, EthApiError<P::Error>> {
// Prepare the calldata for the bytecode function call
let calldata = IERC20Calls::Decimals(DecimalsCall).encode();
let calldata = calldata.into_iter().map(FieldElement::from).collect();

Check failure on line 120 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / check

a value of type `&FieldElement` cannot be built from an iterator over elements of type `FieldElement`

Check failure on line 120 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / clippy

a value of type `&starknet_crypto::FieldElement` cannot be built from an iterator over elements of type `starknet_crypto::FieldElement`

let block_id = EthBlockId::new(block_id);
let block_id: StarknetBlockId = block_id.try_into()?;

let result = self.kakarot_contract.eth_call(&self.address, calldata, &block_id).await?;

Check failure on line 125 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / check

this method takes 4 arguments but 3 arguments were supplied

Check failure on line 125 in crates/core/src/contracts/erc20/ethereum_erc20.rs

View workflow job for this annotation

GitHub Actions / Linters / clippy

this method takes 4 arguments but 3 arguments were supplied
let decimals: Vec<u8> = result.0.into();

Ok(U8::try_from_be_slice(decimals.as_slice()).ok_or(DataDecodingError::InvalidReturnArrayLength {
Eikix marked this conversation as resolved.
Show resolved Hide resolved
entrypoint: "decimals".into(),
expected: 32,
actual: decimals.len(),
})?)
}
}
8 changes: 8 additions & 0 deletions crates/core/src/models/allowance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use reth_primitives::U256;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenAllowance {
pub result: Option<U256>,
pub error: Option<String>,
}
10 changes: 10 additions & 0 deletions crates/core/src/models/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use reth_primitives::U8;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenMetadata {
pub name: Option<String>,
erik-rt marked this conversation as resolved.
Show resolved Hide resolved
pub symbol: Option<String>,
pub decimals: Option<U8>,
pub error: Option<String>,
}
2 changes: 2 additions & 0 deletions crates/core/src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
pub mod allowance;
pub mod balance;
pub mod block;
pub mod call;
pub mod convertible;
pub mod event;
pub mod event_filter;
pub mod felt;
pub mod metadata;
pub mod signature;
#[cfg(test)]
pub mod tests;
Expand Down
1 change: 1 addition & 0 deletions crates/eth-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jsonrpsee = { workspace = true }
# async
async-trait = { workspace = true }
tokio = { workspace = true }
futures = "0.3.26"

# misc
anyhow = "1.0.68"
Expand Down
13 changes: 13 additions & 0 deletions crates/eth-rpc/src/api/alchemy_api.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
use jsonrpsee::core::RpcResult as Result;
use jsonrpsee::proc_macros::rpc;
use kakarot_rpc_core::models::allowance::TokenAllowance;
use kakarot_rpc_core::models::balance::TokenBalances;
use kakarot_rpc_core::models::metadata::TokenMetadata;
use reth_primitives::Address;

// TODO: Define and implement of methods of Alchemy API
#[rpc(server, namespace = "alchemy")]
#[async_trait]
pub trait AlchemyApi {
#[method(name = "getTokenAllowance")]
async fn token_allowance(
&self,
contract_address: Address,
account_address: Address,
spender_address: Address,
) -> Result<TokenAllowance>;

#[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>;
}
18 changes: 18 additions & 0 deletions crates/eth-rpc/src/servers/alchemy_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use std::sync::Arc;

use jsonrpsee::core::{async_trait, RpcResult as Result};
use kakarot_rpc_core::client::api::KakarotEthApi;
use kakarot_rpc_core::models::allowance::TokenAllowance;
use kakarot_rpc_core::models::balance::TokenBalances;
use kakarot_rpc_core::models::metadata::TokenMetadata;
use reth_primitives::Address;
use starknet::providers::Provider;

Expand All @@ -21,8 +23,24 @@ impl<P: Provider + Send + Sync> AlchemyRpc<P> {

#[async_trait]
impl<P: Provider + Send + Sync + 'static> AlchemyApiServer for AlchemyRpc<P> {
async fn token_allowance(
&self,
contract_address: Address,
account_address: Address,
spender_address: Address,
) -> Result<TokenAllowance> {
let token_allowance =
self.kakarot_client.token_allowance(contract_address, account_address, spender_address).await?;
Ok(token_allowance)
}

async fn token_balances(&self, address: Address, contract_addresses: Vec<Address>) -> Result<TokenBalances> {
let token_balances = self.kakarot_client.token_balances(address, contract_addresses).await?;
Ok(token_balances)
}

async fn token_metadata(&self, contract_address: Address) -> Result<TokenMetadata> {
let token_metadata = self.kakarot_client.token_metadata(contract_address).await?;
Ok(token_metadata)
}
}
Loading