yevm-gate 0.1.0

Local RPC proxy that intercepts eth_sendRawTransaction, simulates locally with YEVM, and holds the transaction until the owner approves the decoded side effects.
use eyre::Result;
use sqlx::{SqlitePool, sqlite::SqliteConnectOptions};
use std::{collections::HashMap, str::FromStr};
use yevm_base::Acc;

use crate::{PendingTx, SimResult};

pub async fn open(path: &str) -> Result<SqlitePool> {
    let opts = SqliteConnectOptions::from_str(&format!("sqlite:{path}"))?.create_if_missing(true);
    let pool = SqlitePool::connect_with(opts).await?;
    sqlx::query(
        "CREATE TABLE IF NOT EXISTS pending_txs (
            hash     TEXT PRIMARY KEY,
            from_addr TEXT NOT NULL,
            raw      TEXT NOT NULL,
            sim      TEXT NOT NULL
        )",
    )
    .execute(&pool)
    .await?;
    Ok(pool)
}

pub async fn load_all(pool: &SqlitePool) -> Result<HashMap<String, PendingTx>> {
    let rows = sqlx::query_as::<_, (String, String, String, String)>(
        "SELECT hash, from_addr, raw, sim FROM pending_txs",
    )
    .fetch_all(pool)
    .await?;

    let mut map = HashMap::new();
    for (hash, from_addr, raw, sim_json) in rows {
        let from_bytes = hex::decode(from_addr.trim_start_matches("0x")).unwrap_or_default();
        let from = Acc::from(from_bytes.as_slice());
        let sim: SimResult = match serde_json::from_str(&sim_json) {
            Ok(s) => s,
            Err(e) => {
                tracing::warn!("skipping malformed sim for {hash}: {e}");
                continue;
            }
        };
        map.insert(hash, PendingTx { from, raw, sim });
    }
    Ok(map)
}

pub async fn insert(
    pool: &SqlitePool,
    hash: &str,
    from: &Acc,
    raw: &str,
    sim: &SimResult,
) -> Result<()> {
    let from_str = format!("{from}");
    let sim_json = serde_json::to_string(sim)?;
    sqlx::query(
        "INSERT OR IGNORE INTO pending_txs (hash, from_addr, raw, sim) VALUES (?, ?, ?, ?)",
    )
    .bind(hash)
    .bind(&from_str)
    .bind(raw)
    .bind(&sim_json)
    .execute(pool)
    .await?;
    Ok(())
}

pub async fn update_sim(pool: &SqlitePool, hash: &str, sim: &SimResult) -> Result<()> {
    let sim_json = serde_json::to_string(sim)?;
    sqlx::query("UPDATE pending_txs SET sim = ? WHERE hash = ?")
        .bind(&sim_json)
        .bind(hash)
        .execute(pool)
        .await?;
    Ok(())
}

pub async fn delete(pool: &SqlitePool, hash: &str) -> Result<()> {
    sqlx::query("DELETE FROM pending_txs WHERE hash = ?")
        .bind(hash)
        .execute(pool)
        .await?;
    Ok(())
}