diff --git a/web3_proxy/src/errors.rs b/web3_proxy/src/errors.rs index 43e16b4a..463fde9c 100644 --- a/web3_proxy/src/errors.rs +++ b/web3_proxy/src/errors.rs @@ -38,6 +38,7 @@ impl From for Web3ProxyResult<()> { // TODO: replace all String with `Cow<'static, str>` #[derive(Debug, Display, Error, From)] pub enum Web3ProxyError { + Abi(ethers::abi::Error), AccessDenied, #[error(ignore)] Anyhow(anyhow::Error), @@ -155,6 +156,17 @@ impl Web3ProxyError { pub fn into_response_parts(self) -> (StatusCode, JsonRpcResponseEnum) { // TODO: include a unique request id in the data let (code, err): (StatusCode, JsonRpcErrorData) = match self { + Self::Abi(err) => { + warn!("abi error={:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + JsonRpcErrorData { + message: Cow::Owned(err.to_string()), + code: StatusCode::INTERNAL_SERVER_ERROR.as_u16().into(), + data: None, + }, + ) + } Self::AccessDenied => { // TODO: attach something to this trace. probably don't include much in the message though. don't want to leak creds by accident trace!("access denied"); diff --git a/web3_proxy/src/frontend/users/payment.rs b/web3_proxy/src/frontend/users/payment.rs index 3933e869..5b6857f3 100644 --- a/web3_proxy/src/frontend/users/payment.rs +++ b/web3_proxy/src/frontend/users/payment.rs @@ -11,7 +11,7 @@ use axum_macros::debug_handler; use entities::{balance, increase_on_chain_balance_receipt, user}; use ethbloom::Input as BloomInput; use ethers::abi::{AbiEncode, ParamType}; -use ethers::types::{Address, TransactionReceipt, H256, U256}; +use ethers::types::{Address, TransactionReceipt, ValueOrArray, H256, U256}; use hashbrown::HashMap; use http::StatusCode; use log::{debug, info, trace}; @@ -22,7 +22,7 @@ use migration::sea_orm::{ }; use num_traits::Pow; use payment_contracts::ierc20::IERC20; -use payment_contracts::payment_factory::PaymentFactory; +use payment_contracts::payment_factory::{self, PaymentFactory}; use serde_json::json; use std::str::FromStr; use std::sync::Arc; @@ -135,6 +135,8 @@ pub async fn user_balance_post( .await? .is_some() { + // TODO: double check that the transaction is still seen as "confirmed" if it is NOT, we need to remove credits! + // this will be status code 200, not 204 let response = Json(json!({ "result": "success", @@ -162,183 +164,145 @@ pub async fn user_balance_post( .deposit_factory_contract .context("A deposit_contract must be provided in the config to parse payments")?; - let payment_factory = + let payment_factory_contract = PaymentFactory::new(payment_factory_address, app.internal_provider().clone()); - // TODO: this should be in the abigen stuff somewhere - // let payment_factory_deposit_topic = payment_factory.something?; - let payment_factory_deposit_topic = app - .config - .deposit_topic - .context("A deposit_topic must be provided in the config to parse payments")?; + // check bloom filter to be sure this transaction contains any relevant logs + if let Some(ValueOrArray::Value(Some(x))) = payment_factory_contract + .payment_received_filter() + .filter + .topics[0] + { + let bloom_input = BloomInput::Hash(x.as_fixed_bytes()); - let bloom_input = BloomInput::Raw(payment_factory_deposit_topic.as_bytes()); - - // do a quick check that this transaction contains the required log - if !transaction_receipt.logs_bloom.contains_input(bloom_input) { - return Err(Web3ProxyError::BadRequest("no matching logs found".into())); + // do a quick check that this transaction contains the required log + if !transaction_receipt.logs_bloom.contains_input(bloom_input) { + return Err(Web3ProxyError::BadRequest("no matching logs found".into())); + } } + // the transaction might contain multiple relevant logs. collect them all let mut response_data = vec![]; + // all or nothing let txn = db_conn.begin().await?; // parse the logs from the transaction receipt - // there might be multiple logs with the event if the transaction is doing things in bulk - // TODO: change the indexes to be unique on (chain, txhash, log_index) for log in transaction_receipt.logs { - // TODO: use abigen to make this simpler? - if log.address != payment_factory_address { - trace!( - "Out: Address is not relevant: {:?} {:?}", - log.address, - payment_factory_address, - ); - continue; + if let Some(true) = log.removed { + todo!("delete this transaction from the database"); } - // TODO: use abigen to make this simpler? - let topic = log.topics.get(0).unwrap(); - if *topic != payment_factory_deposit_topic { - trace!( - "Out: Topic is not relevant: {:?} {:?}", - topic, - payment_factory_deposit_topic, - ); - continue; - } - - // TODO: use abigen to make this simpler - let (recipient_account, payment_token_address, payment_token_wei): ( - Address, - Address, - U256, - ) = match ethers::abi::decode( - &[ParamType::Address, ParamType::Address, ParamType::Uint(256)], - &log.data, - ) { - Ok(tpl) => ( - tpl.get(0) - .unwrap() - .clone() - .into_address() - .context("Could not decode recipient")?, - tpl.get(1) - .unwrap() - .clone() - .into_address() - .context("Could not decode token")?, - tpl.get(2) - .unwrap() - .clone() - .into_uint() - .context("Could not decode amount")?, - ), - Err(err) => { - trace!("Out: Could not decode! {:?}", err); - continue; - } - }; - - // there is no need to check that payment_token_address is an allowed token - // the smart contract already reverts if the token isn't accepted - - // we used to skip here if amount is 0, but that means the txid wouldn't ever show up in the database which could be confusing - // also, the contract already reverts for 0 value - - let log_index = log - .log_index - .context("no log_index. transaction must not be confirmed")?; - - // the internal provider will handle caching - let payment_token = IERC20::new(payment_token_address, app.internal_provider().clone()); - - // get the decimals for the token - let payment_token_decimals = payment_token.decimals().call().await?; - - // TODO: how should we do U256 to Decimal? - let decimal_shift = Decimal::from(10).pow(payment_token_decimals.as_u64()); - - let mut payment_token_amount = - Decimal::from_str(&format!("{}", payment_token_wei)).unwrap(); - payment_token_amount.set_scale(payment_token_decimals.as_u32())?; - payment_token_amount /= decimal_shift; - - info!( - "Found deposit transaction for: {:?} {:?} {:?}", - recipient_account, payment_token_address, payment_token_amount - ); - - // Encoding is inefficient, revisit later - let recipient = match user::Entity::find() - .filter(user::Column::Address.eq(recipient_account.encode_hex())) - .one(&db_conn) - .await? + if let Ok(event) = payment_factory_contract + .decode_event::( + "PaymentReceived", + log.topics, + log.data, + ) { - Some(x) => x, - None => todo!("make their account"), - }; + let recipient_account = event.account; + let payment_token_address = event.token; + let payment_token_wei = event.amount; - // For now we only accept stablecoins - // And we hardcode the peg (later we would have to depeg this, for example - // 1$ = Decimal(1) for any stablecoin - // TODO: Let's assume that people don't buy too much at _once_, we do support >$1M which should be fine for now - debug!( - "Arithmetic is: {:?} / 10 ^ {:?} = {:?}", - payment_token_wei, payment_token_decimals, payment_token_amount - ); + // there is no need to check that payment_token_address is an allowed token + // the smart contract already reverts if the token isn't accepted - // Check if the item is in the database. If it is not, then add it into the database - // TODO: select ... for update - let user_balance = balance::Entity::find() - .filter(balance::Column::UserId.eq(recipient.id)) - .one(&txn) - .await?; + // we used to skip here if amount is 0, but that means the txid wouldn't ever show up in the database which could be confusing + // its irrelevant though because the contract already reverts for 0 value - match user_balance { - Some(user_balance) => { - // Update the entry, adding the balance - let balance_plus_amount = user_balance.available_balance + payment_token_amount; + let log_index = log + .log_index + .context("no log_index. transaction must not be confirmed")?; - let mut active_user_balance = user_balance.into_active_model(); - active_user_balance.available_balance = sea_orm::Set(balance_plus_amount); + // the internal provider will handle caching of requests + let payment_token = IERC20::new(payment_token_address, app.internal_provider().clone()); - debug!("New user balance: {:?}", active_user_balance); - active_user_balance.save(&txn).await?; + // get the decimals for the token + // hopefully u32 is always enough, because the Decimal crate doesn't accept a larger scale + // uses uint8, but i've seen pretty much every int in practice + let payment_token_decimals = payment_token.decimals().call().await?.as_u32(); + + let decimal_shift = Decimal::from(10).pow(payment_token_decimals as u64); + + let mut payment_token_amount = Decimal::from_str_exact(&payment_token_wei.to_string())?; + payment_token_amount.set_scale(payment_token_decimals)?; + payment_token_amount /= decimal_shift; + + info!( + "Found deposit transaction for: {:?} {:?} {:?}", + recipient_account, payment_token_address, payment_token_amount + ); + + let recipient = match user::Entity::find() + .filter(user::Column::Address.eq(recipient_account.encode_hex())) + .one(&db_conn) + .await? + { + Some(x) => x, + None => todo!("make their account"), + }; + + // For now we only accept stablecoins + // And we hardcode the peg (later we would have to depeg this, for example + // 1$ = Decimal(1) for any stablecoin + // TODO: Let's assume that people don't buy too much at _once_, we do support >$1M which should be fine for now + debug!( + "Arithmetic is: {:?} / 10 ^ {:?} = {:?}", + payment_token_wei, payment_token_decimals, payment_token_amount + ); + + // Check if the item is in the database. If it is not, then add it into the database + // TODO: `insert ... on duplicate update` to avoid a race + let user_balance = balance::Entity::find() + .filter(balance::Column::UserId.eq(recipient.id)) + .one(&txn) + .await?; + + match user_balance { + Some(user_balance) => { + // Update the entry, adding the balance + let balance_plus_amount = user_balance.available_balance + payment_token_amount; + + let mut active_user_balance = user_balance.into_active_model(); + active_user_balance.available_balance = sea_orm::Set(balance_plus_amount); + + debug!("New user balance: {:?}", active_user_balance); + active_user_balance.save(&txn).await?; + } + None => { + // Create the entry with the respective balance + let active_user_balance = balance::ActiveModel { + available_balance: sea_orm::ActiveValue::Set(payment_token_amount), + user_id: sea_orm::ActiveValue::Set(recipient.id), + ..Default::default() + }; + + debug!("New user balance: {:?}", active_user_balance); + active_user_balance.save(&txn).await?; + } } - None => { - // Create the entry with the respective balance - let active_user_balance = balance::ActiveModel { - available_balance: sea_orm::ActiveValue::Set(payment_token_amount), - user_id: sea_orm::ActiveValue::Set(recipient.id), - ..Default::default() - }; - debug!("New user balance: {:?}", active_user_balance); - active_user_balance.save(&txn).await?; - } - }; + debug!("Setting tx_hash: {:?}", tx_hash); + let receipt = increase_on_chain_balance_receipt::ActiveModel { + tx_hash: sea_orm::ActiveValue::Set(tx_hash.encode_hex()), + chain_id: sea_orm::ActiveValue::Set(app.config.chain_id), + // TODO: need a migration that adds log_index + amount: sea_orm::ActiveValue::Set(payment_token_amount), + deposit_to_user_id: sea_orm::ActiveValue::Set(recipient.id), + ..Default::default() + }; - debug!("Setting tx_hash: {:?}", tx_hash); - let receipt = increase_on_chain_balance_receipt::ActiveModel { - tx_hash: sea_orm::ActiveValue::Set(tx_hash.encode_hex()), - chain_id: sea_orm::ActiveValue::Set(app.config.chain_id), - // TODO: log_index - amount: sea_orm::ActiveValue::Set(payment_token_amount), - deposit_to_user_id: sea_orm::ActiveValue::Set(recipient.id), - ..Default::default() - }; + receipt.save(&txn).await?; - receipt.save(&txn).await?; + let x = json!({ + "tx_hash": tx_hash, + "log_index": log_index, + "token": payment_token_address, + "amount": payment_token_amount, + }); - let x = json!({ - "tx_hash": tx_hash, - "log_index": log_index, - "token": payment_token_address, - "amount": payment_token_amount, - }); - - response_data.push(x); + response_data.push(x); + } } txn.commit().await?;