use abigen types for decoding the logs
This commit is contained in:
parent
7dcc2b4d93
commit
62997774b1
@ -38,6 +38,7 @@ impl From<Web3ProxyError> for Web3ProxyResult<()> {
|
|||||||
// TODO: replace all String with `Cow<'static, str>`
|
// TODO: replace all String with `Cow<'static, str>`
|
||||||
#[derive(Debug, Display, Error, From)]
|
#[derive(Debug, Display, Error, From)]
|
||||||
pub enum Web3ProxyError {
|
pub enum Web3ProxyError {
|
||||||
|
Abi(ethers::abi::Error),
|
||||||
AccessDenied,
|
AccessDenied,
|
||||||
#[error(ignore)]
|
#[error(ignore)]
|
||||||
Anyhow(anyhow::Error),
|
Anyhow(anyhow::Error),
|
||||||
@ -155,6 +156,17 @@ impl Web3ProxyError {
|
|||||||
pub fn into_response_parts<R: Serialize>(self) -> (StatusCode, JsonRpcResponseEnum<R>) {
|
pub fn into_response_parts<R: Serialize>(self) -> (StatusCode, JsonRpcResponseEnum<R>) {
|
||||||
// TODO: include a unique request id in the data
|
// TODO: include a unique request id in the data
|
||||||
let (code, err): (StatusCode, JsonRpcErrorData) = match self {
|
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 => {
|
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
|
// 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");
|
trace!("access denied");
|
||||||
|
@ -11,7 +11,7 @@ use axum_macros::debug_handler;
|
|||||||
use entities::{balance, increase_on_chain_balance_receipt, user};
|
use entities::{balance, increase_on_chain_balance_receipt, user};
|
||||||
use ethbloom::Input as BloomInput;
|
use ethbloom::Input as BloomInput;
|
||||||
use ethers::abi::{AbiEncode, ParamType};
|
use ethers::abi::{AbiEncode, ParamType};
|
||||||
use ethers::types::{Address, TransactionReceipt, H256, U256};
|
use ethers::types::{Address, TransactionReceipt, ValueOrArray, H256, U256};
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use log::{debug, info, trace};
|
use log::{debug, info, trace};
|
||||||
@ -22,7 +22,7 @@ use migration::sea_orm::{
|
|||||||
};
|
};
|
||||||
use num_traits::Pow;
|
use num_traits::Pow;
|
||||||
use payment_contracts::ierc20::IERC20;
|
use payment_contracts::ierc20::IERC20;
|
||||||
use payment_contracts::payment_factory::PaymentFactory;
|
use payment_contracts::payment_factory::{self, PaymentFactory};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -135,6 +135,8 @@ pub async fn user_balance_post(
|
|||||||
.await?
|
.await?
|
||||||
.is_some()
|
.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
|
// this will be status code 200, not 204
|
||||||
let response = Json(json!({
|
let response = Json(json!({
|
||||||
"result": "success",
|
"result": "success",
|
||||||
@ -162,183 +164,145 @@ pub async fn user_balance_post(
|
|||||||
.deposit_factory_contract
|
.deposit_factory_contract
|
||||||
.context("A deposit_contract must be provided in the config to parse payments")?;
|
.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());
|
PaymentFactory::new(payment_factory_address, app.internal_provider().clone());
|
||||||
|
|
||||||
// TODO: this should be in the abigen stuff somewhere
|
// check bloom filter to be sure this transaction contains any relevant logs
|
||||||
// let payment_factory_deposit_topic = payment_factory.something?;
|
if let Some(ValueOrArray::Value(Some(x))) = payment_factory_contract
|
||||||
let payment_factory_deposit_topic = app
|
.payment_received_filter()
|
||||||
.config
|
.filter
|
||||||
.deposit_topic
|
.topics[0]
|
||||||
.context("A deposit_topic must be provided in the config to parse payments")?;
|
{
|
||||||
|
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) {
|
||||||
// do a quick check that this transaction contains the required log
|
return Err(Web3ProxyError::BadRequest("no matching logs found".into()));
|
||||||
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![];
|
let mut response_data = vec![];
|
||||||
|
|
||||||
|
// all or nothing
|
||||||
let txn = db_conn.begin().await?;
|
let txn = db_conn.begin().await?;
|
||||||
|
|
||||||
// parse the logs from the transaction receipt
|
// 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 {
|
for log in transaction_receipt.logs {
|
||||||
// TODO: use abigen to make this simpler?
|
if let Some(true) = log.removed {
|
||||||
if log.address != payment_factory_address {
|
todo!("delete this transaction from the database");
|
||||||
trace!(
|
|
||||||
"Out: Address is not relevant: {:?} {:?}",
|
|
||||||
log.address,
|
|
||||||
payment_factory_address,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use abigen to make this simpler?
|
if let Ok(event) = payment_factory_contract
|
||||||
let topic = log.topics.get(0).unwrap();
|
.decode_event::<payment_factory::PaymentReceivedFilter>(
|
||||||
if *topic != payment_factory_deposit_topic {
|
"PaymentReceived",
|
||||||
trace!(
|
log.topics,
|
||||||
"Out: Topic is not relevant: {:?} {:?}",
|
log.data,
|
||||||
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?
|
|
||||||
{
|
{
|
||||||
Some(x) => x,
|
let recipient_account = event.account;
|
||||||
None => todo!("make their account"),
|
let payment_token_address = event.token;
|
||||||
};
|
let payment_token_wei = event.amount;
|
||||||
|
|
||||||
// For now we only accept stablecoins
|
// there is no need to check that payment_token_address is an allowed token
|
||||||
// And we hardcode the peg (later we would have to depeg this, for example
|
// the smart contract already reverts if the token isn't accepted
|
||||||
// 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
|
// 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
|
||||||
// TODO: select ... for update
|
// its irrelevant though because the contract already reverts for 0 value
|
||||||
let user_balance = balance::Entity::find()
|
|
||||||
.filter(balance::Column::UserId.eq(recipient.id))
|
|
||||||
.one(&txn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match user_balance {
|
let log_index = log
|
||||||
Some(user_balance) => {
|
.log_index
|
||||||
// Update the entry, adding the balance
|
.context("no log_index. transaction must not be confirmed")?;
|
||||||
let balance_plus_amount = user_balance.available_balance + payment_token_amount;
|
|
||||||
|
|
||||||
let mut active_user_balance = user_balance.into_active_model();
|
// the internal provider will handle caching of requests
|
||||||
active_user_balance.available_balance = sea_orm::Set(balance_plus_amount);
|
let payment_token = IERC20::new(payment_token_address, app.internal_provider().clone());
|
||||||
|
|
||||||
debug!("New user balance: {:?}", active_user_balance);
|
// get the decimals for the token
|
||||||
active_user_balance.save(&txn).await?;
|
// 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);
|
debug!("Setting tx_hash: {:?}", tx_hash);
|
||||||
active_user_balance.save(&txn).await?;
|
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);
|
receipt.save(&txn).await?;
|
||||||
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?;
|
let x = json!({
|
||||||
|
"tx_hash": tx_hash,
|
||||||
|
"log_index": log_index,
|
||||||
|
"token": payment_token_address,
|
||||||
|
"amount": payment_token_amount,
|
||||||
|
});
|
||||||
|
|
||||||
let x = json!({
|
response_data.push(x);
|
||||||
"tx_hash": tx_hash,
|
}
|
||||||
"log_index": log_index,
|
|
||||||
"token": payment_token_address,
|
|
||||||
"amount": payment_token_amount,
|
|
||||||
});
|
|
||||||
|
|
||||||
response_data.push(x);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
txn.commit().await?;
|
txn.commit().await?;
|
||||||
|
Loading…
Reference in New Issue
Block a user