Skip to content

Commit

Permalink
feat(balance): adding force balance update by the contract address
Browse files Browse the repository at this point in the history
  • Loading branch information
geekbrother committed May 14, 2024
1 parent ef68cc6 commit 965d110
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 6 deletions.
29 changes: 29 additions & 0 deletions integration/balance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,33 @@ describe('Account balance', () => {
expect(typeof resp.data.balances).toBe('object')
expect(resp.data.balances).toHaveLength(0)
})

it('force update balance for the token', async () => {
// USDC token contract address on Base
const token_contract_address = 'eip155:8453:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'
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')
}
})
})
61 changes: 57 additions & 4 deletions src/handlers/balance.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use {
super::HANDLER_TASK_METRICS,
crate::{analytics::BalanceLookupInfo, error::RpcError, state::AppState, utils::network},
crate::{
analytics::BalanceLookupInfo,
error::RpcError,
state::AppState,
utils::{crypto, network},
},
axum::{
extract::{ConnectInfo, Path, Query, State},
response::{IntoResponse, Response},
Expand All @@ -16,7 +21,7 @@ use {
time::{Duration, SystemTime},
},
tap::TapFallible,
tracing::log::error,
tracing::log::{debug, error},
wc::future::FutureExt,
};

Expand Down Expand Up @@ -60,6 +65,8 @@ pub struct BalanceQueryParams {
pub project_id: String,
pub currency: BalanceCurrencies,
pub chain_id: Option<String>,
/// Comma separated list of CAIP-10 contract addresses to force update the balance
pub force_update: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
Expand Down Expand Up @@ -112,7 +119,7 @@ async fn handler_internal(
Path(address): Path<String>,
) -> Result<Response, RpcError> {
let project_id = query.project_id.clone();
address
let parsed_address = address
.parse::<Address>()
.map_err(|_| RpcError::InvalidAddress)?;

Expand All @@ -126,7 +133,7 @@ async fn handler_internal(
}

let start = SystemTime::now();
let response = state
let mut response = state
.providers
.balance_provider
.get_balance(address.clone(), query.clone().0, state.http_client.clone())
Expand Down Expand Up @@ -167,5 +174,51 @@ async fn handler_internal(
}
}

// Check for the cache invalidation for the certain token contract addresses and
// update/override balance results for the token from the RPC call
if let Some(force_update) = &query.force_update {
let rpc_project_id = state
.config
.server
.testing_project_id
.as_ref()
.ok_or_else(|| {
RpcError::InvalidConfiguration(
"Missing testing project id in the configuration for the balance RPC lookups"
.to_string(),
)
})?;
let force_update: Vec<&str> = force_update.split(',').collect();
for caip_contract_address in force_update {
debug!(
"Forcing balance update for the contract address: {}",
caip_contract_address
);
let (namespace, chain_id, contract_address) =
crypto::disassemble_caip10(caip_contract_address)
.map_err(|_| RpcError::InvalidAddress)?;
let contract_address = contract_address
.parse::<Address>()
.map_err(|_| RpcError::InvalidAddress)?;
let rpc_balance = crypto::get_erc20_balance(
format!("{}:{}", namespace, chain_id).as_str(),
contract_address,
parsed_address,
rpc_project_id,
)
.await?;
if let Some(balance) = response
.balances
.iter_mut()
.find(|b| b.address == Some(caip_contract_address.to_string()))
{
balance.quantity.numeric = crypto::format_token_amount(
rpc_balance,
balance.quantity.decimals.parse::<u32>().unwrap_or(0),
);
}
}
}

Ok(Json(response).into_response())
}
58 changes: 56 additions & 2 deletions src/utils/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use {
alloy_primitives::Address,
ethers::types::H256,
ethers::{
prelude::abigen,
providers::{Http, Provider},
types::{H160, H256, U256},
},
once_cell::sync::Lazy,
regex::Regex,
relay_rpc::auth::cacao::{signature::eip6492::verify_eip6492, CacaoError},
std::str::FromStr,
std::{str::FromStr, sync::Arc},
strum::IntoEnumIterator,
strum_macros::{Display, EnumIter, EnumString},
tracing::warn,
Expand Down Expand Up @@ -104,6 +108,39 @@ pub async fn verify_eip6492_message_signature(
}
}

/// Get the balance of the ERC20 token
#[tracing::instrument]
pub async fn get_erc20_balance(
chain_id: &str,
contract: H160,
wallet: H160,
rpc_project_id: &str,
) -> Result<U256, CryptoUitlsError> {
abigen!(
ERC20Contract,
r#"[
function balanceOf(address account) external view returns (uint256)
]"#,
);

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 contract = ERC20Contract::new(contract, provider);
let balance = contract.balance_of(wallet).call().await.map_err(|e| {
CryptoUitlsError::ContractCallError(format!(
"Failed to call ERC20 contract for the balance: {}",
e
))
})?;

Ok(balance)
}

/// Convert EVM chain ID to coin type ENSIP-11
#[tracing::instrument]
pub fn convert_evm_chain_id_to_coin_type(chain_id: u32) -> u32 {
Expand Down Expand Up @@ -289,6 +326,23 @@ pub fn constant_time_eq(a: impl AsRef<[u8]>, b: impl AsRef<[u8]>) -> bool {
result == 0
}

/// Format token amount to human readable format according to the token decimals
pub fn format_token_amount(amount: U256, decimals: u32) -> String {
let amount_str = amount.to_string();
let decimals_usize = decimals as usize;

// 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 zeros = "0".repeat(required_zeros);
return format!("0.{}{}", zeros, amount_str);
}

// Insert the decimal point at the correct position
let (integer_part, decimal_part) = amount_str.split_at(amount_str.len() - decimals_usize);
format!("{}.{}", integer_part, decimal_part)
}

#[cfg(test)]
mod tests {
use {super::*, std::collections::HashMap};
Expand Down

0 comments on commit 965d110

Please sign in to comment.