diff --git a/integration/balance.test.ts b/integration/balance.test.ts
index 94c1554cac..dd1b63b1ea 100644
--- a/integration/balance.test.ts
+++ b/integration/balance.test.ts
@@ -61,7 +61,7 @@ describe('Account balance', () => {
expect(resp.data.balances).toHaveLength(0)
})
- it('force update balance for the token', async () => {
+ it('force update balance for the ERC20 token', async () => {
// USDC token contract address on Base
const token_contract_address = 'eip155:8453:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'
const endpoint = `/v1/account/${fulfilled_address}/balance`;
@@ -89,4 +89,34 @@ describe('Account balance', () => {
expect(typeof item.iconUrl).toBe('string')
}
})
+
+ it('force update balance for the native token', async () => {
+ // ETH token
+ // We are using `0xe...` as a contract address for native tokens
+ const token_contract_address = 'eip155:1:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
+ const endpoint = `/v1/account/${fulfilled_address}/balance`;
+ const queryParams = `?projectId=${projectId}¤cy=${currency}&forceUpdate=${token_contract_address}`;
+ const url = `${baseUrl}${endpoint}${queryParams}`;
+ const headers = {
+ 'x-sdk-version': sdk_version,
+ };
+ let resp = await httpClient.get(url, { headers });
+ expect(resp.status).toBe(200)
+ expect(typeof resp.data.balances).toBe('object')
+ expect(resp.data.balances.length).toBeGreaterThan(1)
+
+ for (const item of resp.data.balances) {
+ expect(typeof item.name).toBe('string')
+ expect(typeof item.symbol).toBe('string')
+ expect(item.chainId).toEqual(expect.stringMatching(/^(eip155:)?\d+$/))
+ if (item.address !== undefined) {
+ expect(item.address).toEqual(expect.stringMatching(/^(eip155:\d+:0x[0-9a-fA-F]{40})$/))
+ } else {
+ expect(item.address).toBeUndefined()
+ }
+ expect(typeof item.price).toBe('number')
+ expect(typeof item.quantity).toBe('object')
+ expect(typeof item.iconUrl).toBe('string')
+ }
+ })
})
diff --git a/src/handlers/balance.rs b/src/handlers/balance.rs
index c89751a597..961b61ba68 100644
--- a/src/handlers/balance.rs
+++ b/src/handlers/balance.rs
@@ -11,7 +11,7 @@ use {
response::{IntoResponse, Response},
Json,
},
- ethers::abi::Address,
+ ethers::{abi::Address, types::H160},
hyper::HeaderMap,
serde::{Deserialize, Serialize},
std::{
@@ -200,8 +200,9 @@ async fn handler_internal(
let contract_address = contract_address
.parse::
()
.map_err(|_| RpcError::InvalidAddress)?;
+ let caip2_chain_id = format!("{}:{}", namespace, chain_id);
let rpc_balance = crypto::get_erc20_balance(
- format!("{}:{}", namespace, chain_id).as_str(),
+ &caip2_chain_id,
contract_address,
parsed_address,
rpc_project_id,
@@ -216,6 +217,30 @@ async fn handler_internal(
rpc_balance,
balance.quantity.decimals.parse::().unwrap_or(0),
);
+ // Recalculating the value with the latest balance
+ balance.value = Some(crypto::convert_token_amount_to_value(
+ rpc_balance,
+ balance.price,
+ balance.quantity.decimals.parse::().unwrap_or(0),
+ ));
+ }
+ if contract_address == H160::repeat_byte(0xee) {
+ if let Some(balance) = response
+ .balances
+ .iter_mut()
+ .find(|b| b.address.is_none() && b.chain_id == Some(caip2_chain_id.clone()))
+ {
+ balance.quantity.numeric = crypto::format_token_amount(
+ rpc_balance,
+ balance.quantity.decimals.parse::().unwrap_or(0),
+ );
+ // Recalculate the value with the latest balance
+ balance.value = Some(crypto::convert_token_amount_to_value(
+ rpc_balance,
+ balance.price,
+ balance.quantity.decimals.parse::().unwrap_or(0),
+ ));
+ }
}
}
}
diff --git a/src/utils/crypto.rs b/src/utils/crypto.rs
index e0e3555b14..686bb5e14e 100644
--- a/src/utils/crypto.rs
+++ b/src/utils/crypto.rs
@@ -2,7 +2,7 @@ use {
alloy_primitives::Address,
ethers::{
prelude::abigen,
- providers::{Http, Provider},
+ providers::{Http, Middleware, Provider},
types::{H160, H256, U256},
},
once_cell::sync::Lazy,
@@ -35,6 +35,8 @@ pub enum CryptoUitlsError {
WrongCaip2Format(String),
#[error("Wrong CAIP-10 format: {0}")]
WrongCaip10Format(String),
+ #[error("Provider call error: {0}")]
+ ProviderError(String),
#[error("Contract call error: {0}")]
ContractCallError(String),
#[error("Wrong address format: {0}")]
@@ -115,6 +117,25 @@ pub async fn get_erc20_balance(
contract: H160,
wallet: H160,
rpc_project_id: &str,
+) -> Result {
+ // Use JSON-RPC call for the balance of the native ERC20 tokens
+ // or call the contract for the custom ERC20 tokens
+ let balance = if contract == H160::repeat_byte(0xee) {
+ get_erc20_jsonrpc_balance(chain_id, wallet, rpc_project_id).await?
+ } else {
+ get_erc20_contract_balance(chain_id, contract, wallet, rpc_project_id).await?
+ };
+
+ Ok(balance)
+}
+
+/// Get the balance of ERC20 token by calling the contract address
+#[tracing::instrument]
+async fn get_erc20_contract_balance(
+ chain_id: &str,
+ contract: H160,
+ wallet: H160,
+ rpc_project_id: &str,
) -> Result {
abigen!(
ERC20Contract,
@@ -137,7 +158,27 @@ pub async fn get_erc20_balance(
e
))
})?;
+ Ok(balance)
+}
+/// Get the balance of ERC20 token using JSON-RPC call
+#[tracing::instrument]
+async fn get_erc20_jsonrpc_balance(
+ chain_id: &str,
+ wallet: H160,
+ rpc_project_id: &str,
+) -> Result {
+ let provider = Provider::::try_from(format!(
+ "https://rpc.walletconnect.com/v1?chainId={}&projectId={}",
+ chain_id, rpc_project_id
+ ))
+ .map_err(|e| CryptoUitlsError::RpcUrlParseError(format!("Failed to parse RPC url: {}", e)))?;
+ let provider = Arc::new(provider);
+
+ let balance = provider
+ .get_balance(wallet, None)
+ .await
+ .map_err(|e| CryptoUitlsError::ProviderError(format!("{}", e)))?;
Ok(balance)
}
@@ -333,7 +374,7 @@ pub fn format_token_amount(amount: U256, decimals: u32) -> String {
// Handle cases where the total digits are less than or equal to the decimals
if amount_str.len() <= decimals_usize {
- let required_zeros = decimals_usize - amount_str.len() + 1;
+ let required_zeros = decimals_usize - amount_str.len();
let zeros = "0".repeat(required_zeros);
return format!("0.{}{}", zeros, amount_str);
}
@@ -343,6 +384,14 @@ pub fn format_token_amount(amount: U256, decimals: u32) -> String {
format!("{}.{}", integer_part, decimal_part)
}
+/// Convert token amount to value depending on the token price and decimals
+pub fn convert_token_amount_to_value(balance: U256, price: f64, decimals: u32) -> f64 {
+ let decimals_usize = decimals as usize;
+ let scaling_factor = 10_u64.pow(decimals_usize as u32) as f64;
+ let balance_f64 = balance.as_u64() as f64 / scaling_factor;
+ balance_f64 * price
+}
+
#[cfg(test)]
mod tests {
use {super::*, std::collections::HashMap};
@@ -448,4 +497,31 @@ mod tests {
let error_result = disassemble_caip10(malformed_caip10);
assert!(error_result.is_err());
}
+
+ #[test]
+ fn test_format_token_amount() {
+ // Test case for ethereum 18 decimals
+ let amount_18 = U256::from_dec_str("959694527317077690").unwrap();
+ let decimals_18 = 18;
+ assert_eq!(
+ format_token_amount(amount_18, decimals_18),
+ "0.959694527317077690"
+ );
+
+ // Test case for polygon usdc 6 decimals
+ let amount_6 = U256::from_dec_str("125320550").unwrap();
+ let decimals_6 = 6;
+ assert_eq!(format_token_amount(amount_6, decimals_6), "125.320550");
+ }
+
+ #[test]
+ fn test_convert_token_amount_to_value() {
+ let balance = U256::from_dec_str("959694527317077690").unwrap();
+ let price = 10000.05;
+ let decimals = 18;
+ assert_eq!(
+ convert_token_amount_to_value(balance, price, decimals),
+ 0.959_694_527_317_077_7 * price
+ );
+ }
}