use abigen types for decoding the logs

This commit is contained in:
Bryan Stitt 2023-05-31 12:44:02 -07:00
parent 7dcc2b4d93
commit 62997774b1
2 changed files with 131 additions and 155 deletions

@ -38,6 +38,7 @@ impl From<Web3ProxyError> 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<R: Serialize>(self) -> (StatusCode, JsonRpcResponseEnum<R>) {
// 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");

@ -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::<payment_factory::PaymentReceivedFilter>(
"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
// <https://eips.ethereum.org/EIPS/eip-20> 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?;