Skip to content

Commit

Permalink
feat: adding handling of native tokens and updating tests
Browse files Browse the repository at this point in the history
  • Loading branch information
geekbrother committed May 20, 2024
1 parent d5c4f47 commit 45bf830
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 5 deletions.
32 changes: 31 additions & 1 deletion integration/balance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -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}&currency=${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')
}
})
})
29 changes: 27 additions & 2 deletions src/handlers/balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use {
response::{IntoResponse, Response},
Json,
},
ethers::abi::Address,
ethers::{abi::Address, types::H160},
hyper::HeaderMap,
serde::{Deserialize, Serialize},
std::{
Expand Down Expand Up @@ -200,8 +200,9 @@ async fn handler_internal(
let contract_address = contract_address
.parse::<Address>()
.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,
Expand All @@ -216,6 +217,30 @@ async fn handler_internal(
rpc_balance,
balance.quantity.decimals.parse::<u32>().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::<u32>().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::<u32>().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::<u32>().unwrap_or(0),
));
}
}
}
}
Expand Down
80 changes: 78 additions & 2 deletions src/utils/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}")]
Expand Down Expand Up @@ -115,6 +117,25 @@ pub async fn get_erc20_balance(
contract: H160,
wallet: H160,
rpc_project_id: &str,
) -> Result<U256, CryptoUitlsError> {
// 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<U256, CryptoUitlsError> {
abigen!(
ERC20Contract,
Expand All @@ -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<U256, CryptoUitlsError> {
let provider = Provider::<Http>::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)
}

Expand Down Expand Up @@ -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);
}
Expand All @@ -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};
Expand Down Expand Up @@ -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
);
}
}

0 comments on commit 45bf830

Please sign in to comment.