diff --git a/.github/scripts/tempo-mpp.sh b/.github/scripts/tempo-mpp.sh index 1a98b1ed9c494..0d34c4a3b5fa5 100755 --- a/.github/scripts/tempo-mpp.sh +++ b/.github/scripts/tempo-mpp.sh @@ -110,7 +110,7 @@ BLOCK2=$("$CAST" block-number --rpc-url "$RPC_MPP") AFTER2=$("$CAST" erc20 balance "$TOKEN" "$WALLET" --rpc-url "$RPC" | awk '{print $1}') SPENT2=$((BEFORE2 - AFTER2)) echo "Block: $BLOCK2" -echo "Spent: $SPENT2 units (should be 0 — channel reused from ~/.tempo/foundry/channels.json)" +echo "Spent: $SPENT2 units (should be 0 — channel reused from ~/.tempo/channels.db)" # 6. forge script via MPP echo "" @@ -129,9 +129,9 @@ contract MppCheck is Script { } } SOL -VCNT_BEFORE=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_BEFORE=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") "$FORGE" script "$TMPDIR/script/Mpp.s.sol" --rpc-url "$RPC_MPP" --root "$TMPDIR" -VCNT_AFTER=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_AFTER=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") echo "Vouchers paid: +$((VCNT_AFTER - VCNT_BEFORE)) ($((( VCNT_AFTER - VCNT_BEFORE ) / 1000)) RPC calls via MPP)" # 7. forge test with vm.createSelectFork via MPP @@ -149,15 +149,15 @@ contract MppForkTest is Test { } } SOL -VCNT_BEFORE=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_BEFORE=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") "$FORGE" test --match-test test_fork_via_mpp --root "$TMPDIR" -vvv -VCNT_AFTER=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_AFTER=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") echo "Vouchers paid: +$((VCNT_AFTER - VCNT_BEFORE)) ($((( VCNT_AFTER - VCNT_BEFORE ) / 1000)) RPC calls via MPP)" # 8. anvil fork via MPP echo "" echo "=== 8. anvil --fork-url (via MPP) ===" -VCNT_BEFORE=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_BEFORE=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") "$ANVIL" --fork-url "$RPC_MPP" --port 8555 --silent & ANVIL_PID=$! for _ in $(seq 1 30); do @@ -167,15 +167,15 @@ done echo "chain-id: $("$CAST" chain-id --rpc-url http://localhost:8555)" kill $ANVIL_PID 2>/dev/null wait $ANVIL_PID 2>/dev/null -VCNT_AFTER=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_AFTER=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") echo "Vouchers paid: +$((VCNT_AFTER - VCNT_BEFORE)) ($((( VCNT_AFTER - VCNT_BEFORE ) / 1000)) RPC calls via MPP)" # 9. chisel fork via MPP echo "" echo "=== 9. chisel --fork-url (via MPP) ===" -VCNT_BEFORE=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_BEFORE=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") echo 'block.number' | "$CHISEL" --fork-url "$RPC_MPP" 2>&1 | grep -E "Decimal|Type" -VCNT_AFTER=$(grep cumulative_amount ~/.tempo/foundry/channels.json | awk -F'"' '{print $4}') +VCNT_AFTER=$(sqlite3 ~/.tempo/channels.db "SELECT cumulative_amount FROM channels LIMIT 1") echo "Vouchers paid: +$((VCNT_AFTER - VCNT_BEFORE)) ($((( VCNT_AFTER - VCNT_BEFORE ) / 1000)) RPC calls via MPP)" echo "" diff --git a/Cargo.lock b/Cargo.lock index 3308883127ad1..bfc0874cf1fd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1194,7 +1194,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1218,7 +1218,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2558,7 +2558,7 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" dependencies = [ - "darling 0.20.11", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -3021,7 +3021,7 @@ dependencies = [ "terminfo", "thiserror 2.0.18", "which", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3174,7 +3174,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3958,7 +3958,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4263,7 +4263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4376,6 +4376,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fast-float2" version = "0.2.3" @@ -5009,6 +5021,7 @@ dependencies = [ "foundry-common-fmt", "foundry-compilers", "foundry-config", + "foundry-wallets", "itertools 0.14.0", "jiff", "mpp", @@ -5544,7 +5557,7 @@ dependencies = [ [[package]] name = "foundry-wallets" version = "0.1.0" -source = "git+https://github.com/foundry-rs/foundry-core?rev=7f401c1397af90a0a94ef7424a48bbf3dc0248cc#7f401c1397af90a0a94ef7424a48bbf3dc0248cc" +source = "git+https://github.com/foundry-rs/foundry-core?rev=e8c8be1#e8c8be112909abee760a49801dab0ba68f6516f5" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -5568,6 +5581,7 @@ dependencies = [ "eth-keystore", "eyre", "rpassword", + "rusqlite", "serde", "serde_json", "tempo-primitives", @@ -5970,6 +5984,15 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -6565,7 +6588,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6920,6 +6943,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libusb1-sys" version = "0.7.0" @@ -7480,7 +7514,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -8464,7 +8498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -8633,7 +8667,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -9930,6 +9964,20 @@ dependencies = [ "libusb1-sys", ] +[[package]] +name = "rusqlite" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b" +dependencies = [ + "bitflags 2.11.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -10000,7 +10048,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -10059,7 +10107,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -10762,7 +10810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -10830,7 +10878,7 @@ dependencies = [ "derive_more", "dunce", "inturn", - "itertools 0.12.1", + "itertools 0.14.0", "itoa", "normalize-path", "once_map", @@ -10865,7 +10913,7 @@ dependencies = [ "alloy-primitives", "bitflags 2.11.1", "bumpalo", - "itertools 0.12.1", + "itertools 0.14.0", "memchr", "num-bigint", "num-rational", @@ -11289,7 +11337,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -11494,7 +11542,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -11504,7 +11552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -12721,7 +12769,7 @@ dependencies = [ "watchexec-events", "watchexec-signals", "watchexec-supervisor", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -12871,7 +12919,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 294af9323cfe8..33ba7f64fab10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -355,7 +355,7 @@ svm = { package = "svm-rs", version = "0.5", default-features = false, features ] } ## foundry-core -foundry-wallets = { git = "https://github.com/foundry-rs/foundry-core", rev = "7f401c1397af90a0a94ef7424a48bbf3dc0248cc", default-features = false } +foundry-wallets = { git = "https://github.com/foundry-rs/foundry-core", rev = "e8c8be1", default-features = false } ## alloy alloy-consensus = { version = "2.0.0", default-features = false } diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index ac7a8c12eae71..04c2a06a3fa90 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -82,6 +82,7 @@ flate2.workspace = true tempo-alloy.workspace = true tempo-primitives.workspace = true mpp.workspace = true +foundry-wallets.workspace = true [build-dependencies] chrono.workspace = true diff --git a/crates/common/src/provider/mpp/persist.rs b/crates/common/src/provider/mpp/persist.rs index 6d6781ab4c77b..803c7270e041e 100644 --- a/crates/common/src/provider/mpp/persist.rs +++ b/crates/common/src/provider/mpp/persist.rs @@ -1,243 +1,241 @@ //! Persistent channel storage for MPP sessions. //! -//! Stores open payment channel state in a JSON file at -//! `$TEMPO_HOME/foundry/channels.json` (default: `~/.tempo/foundry/channels.json`). +//! Stores open payment channel state in a SQLite database at +//! `$TEMPO_HOME/channels.db` (default: `~/.tempo/channels.db`). //! This allows channel reuse across process invocations, avoiding the cost of //! opening a new on-chain channel for every `cast` / `forge` command. use alloy_primitives::{Address, B256}; +use foundry_wallets::{Channel, ChannelDb}; use mpp::client::channel_ops::ChannelEntry; -use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, - path::PathBuf, + sync::OnceLock, time::{SystemTime, UNIX_EPOCH}, }; use tracing::{debug, warn}; use crate::tempo::tempo_home; -/// Relative path from Tempo home to the Foundry channels file. -const CHANNELS_PATH: &str = "foundry/channels.json"; +/// Process-wide database handle. +fn global_db() -> Option<&'static ChannelDb> { + static DB: OnceLock> = OnceLock::new(); + DB.get_or_init(|| { + let path = tempo_home()?.join("channels.db"); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + if let Some(old) = + tempo_home().map(|h| h.join("foundry/channels.json")).filter(|p| p.exists()) + { + warn!( + ?old, + "found old channels.json — this file is no longer used; channels will be re-opened" + ); + } -/// Current schema version. -const SCHEMA_VERSION: u64 = 2; + match ChannelDb::open(&path) { + Ok(db) => { + debug!(?path, "opened channel database"); + Some(db) + } + Err(e) => { + warn!(?path, %e, "failed to open channel database"); + None + } + } + }) + .as_ref() +} -/// On-disk representation of the channel store. -#[derive(Debug, Serialize, Deserialize)] -struct ChannelStore { - version: u64, - #[serde(default)] - channels: HashMap, +/// Reconstruct the composite HashMap key from a persisted `Channel`. +/// +/// Mirrors `SessionProvider::channel_key()` in session.rs. +fn channel_key_from_persisted(ch: &Channel) -> String { + let origin_hash = &alloy_primitives::keccak256(ch.origin.as_bytes()).to_string()[..18]; + format!( + "{}:{}:{}:{}:{}:{}:{}", + origin_hash, + ch.chain_id, + ch.payer, + ch.authorized_signer, + ch.payee, + ch.token, + ch.escrow_contract + ) + .to_lowercase() } -impl Default for ChannelStore { - fn default() -> Self { - Self { version: SCHEMA_VERSION, channels: HashMap::new() } +/// Whether a channel can still be used (active and not fully spent). +fn is_usable(ch: &Channel) -> bool { + if ch.state != "active" { + return false; } + let cumulative: u128 = ch.cumulative_amount.parse().unwrap_or(u128::MAX); + let deposit: u128 = ch.deposit.parse().unwrap_or(0); + cumulative < deposit } -/// A persisted channel entry. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PersistedChannel { - pub channel_id: String, - pub salt: String, - pub escrow_contract: String, - pub chain_id: u64, - pub cumulative_amount: String, - pub deposit: String, - pub status: String, - pub origin: String, - pub created_at: u64, - pub last_used_at: u64, +/// Convert a persisted `Channel` to a `ChannelEntry`. +pub fn to_channel_entry(ch: &Channel) -> Option { + let channel_id: B256 = ch.channel_id.parse().ok()?; + let salt: B256 = ch.salt.parse().ok()?; + let escrow_contract: Address = ch.escrow_contract.parse().ok()?; + let cumulative_amount: u128 = ch.cumulative_amount.parse().ok()?; + + Some(ChannelEntry { + channel_id, + salt, + cumulative_amount, + escrow_contract, + chain_id: ch.chain_id as u64, + opened: ch.state == "active", + }) } -impl PersistedChannel { - /// Convert to an mpp `ChannelEntry` for use in the session provider. - pub fn to_channel_entry(&self) -> Option { - let channel_id: B256 = self.channel_id.parse().ok()?; - let salt: B256 = self.salt.parse().ok()?; - let escrow_contract: Address = self.escrow_contract.parse().ok()?; - let cumulative_amount: u128 = self.cumulative_amount.parse().ok()?; - - Some(ChannelEntry { - channel_id, - salt, - cumulative_amount, - escrow_contract, - chain_id: self.chain_id, - opened: self.status == "active", - }) - } - - /// Create from a `ChannelEntry` with metadata. - pub fn from_channel_entry(entry: &ChannelEntry, deposit: u128, origin: &str) -> Self { - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); - - Self { - channel_id: entry.channel_id.to_string(), - salt: entry.salt.to_string(), - escrow_contract: entry.escrow_contract.to_string(), - chain_id: entry.chain_id, - cumulative_amount: entry.cumulative_amount.to_string(), - deposit: deposit.to_string(), - status: if entry.opened { "active" } else { "closed" }.to_string(), - origin: origin.to_string(), - created_at: now, - last_used_at: now, - } - } - - /// Whether this channel can still be used (active and not fully spent). - fn is_usable(&self) -> bool { - if self.status != "active" { - return false; - } - let cumulative: u128 = self.cumulative_amount.parse().unwrap_or(u128::MAX); - let deposit: u128 = self.deposit.parse().unwrap_or(0); - cumulative < deposit +/// Create a `Channel` from a `ChannelEntry` with metadata. +pub fn from_channel_entry( + entry: &ChannelEntry, + deposit: u128, + origin: &str, + payer: &Address, + payee: &Address, + token: &Address, + authorized_signer: &Address, +) -> Channel { + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() as i64; + + Channel { + channel_id: entry.channel_id.to_string(), + version: 1, + origin: origin.to_string(), + request_url: String::new(), + chain_id: entry.chain_id as i64, + escrow_contract: entry.escrow_contract.to_string(), + token: token.to_string(), + payee: payee.to_string(), + payer: payer.to_string(), + authorized_signer: authorized_signer.to_string(), + salt: entry.salt.to_string(), + deposit: deposit.to_string(), + cumulative_amount: entry.cumulative_amount.to_string(), + challenge_echo: String::new(), + state: if entry.opened { "active" } else { "closed" }.to_string(), + close_requested_at: 0, + grace_ready_at: 0, + created_at: now, + last_used_at: now, } } -/// Returns the path to the channels file. -fn channels_path() -> Option { - tempo_home().map(|home| home.join(CHANNELS_PATH)) -} - -/// Load channels from disk, evicting spent/inactive entries. -pub fn load_channels() -> HashMap { - let Some(path) = channels_path().filter(|p| p.exists()) else { +/// Load channels from database, evicting spent/inactive entries. +pub fn load_channels() -> HashMap { + let Some(db) = global_db() else { return HashMap::new(); }; - let Ok(contents) = std::fs::read_to_string(&path).inspect_err(|e| { - warn!(?path, %e, "failed to read channels file"); - }) else { - return HashMap::new(); - }; - - let Ok(store) = serde_json::from_str::(&contents).inspect_err(|e| { - warn!(?path, %e, "failed to parse channels file, starting fresh"); - }) else { - return HashMap::new(); + let channels = match db.load() { + Ok(channels) => channels, + Err(e) => { + warn!(%e, "failed to load channels from database"); + return HashMap::new(); + } }; - if store.version != SCHEMA_VERSION { - warn!( - version = store.version, - expected = SCHEMA_VERSION, - "channels file version mismatch, starting fresh" - ); - return HashMap::new(); - } - - // Evict spent/inactive entries - let usable: HashMap = - store.channels.into_iter().filter(|(_, ch)| ch.is_usable()).collect(); + let usable: HashMap = channels + .into_iter() + .filter(is_usable) + .map(|ch| { + let key = channel_key_from_persisted(&ch); + (key, ch) + }) + .collect(); debug!(count = usable.len(), "loaded persisted MPP channels"); usable } -/// Save channels to disk. -pub fn save_channels(channels: &HashMap) { - let Some(path) = channels_path() else { +/// Save channels to database. +pub fn save_channels(channels: &HashMap) { + let Some(db) = global_db() else { return; }; - if let Some(parent) = path.parent() - && let Err(e) = std::fs::create_dir_all(parent) - { - warn!(?path, %e, "failed to create channels directory"); - return; + for ch in channels.values() { + if let Err(e) = db.upsert(ch) { + warn!(%e, channel_id = %ch.channel_id, "failed to save channel"); + } } + debug!(count = channels.len(), "saved MPP channels"); +} - let store = ChannelStore { version: SCHEMA_VERSION, channels: channels.clone() }; - - match serde_json::to_string_pretty(&store) { - Ok(json) => { - if let Err(e) = std::fs::write(&path, json) { - warn!(?path, %e, "failed to write channels file"); - } else { - debug!(?path, count = channels.len(), "saved MPP channels"); - } - } - Err(e) => warn!(%e, "failed to serialize channels"), +/// Delete a channel from the database by its channel ID. +pub fn delete_channel_from_db(channel_id: &str) { + let Some(db) = global_db() else { + return; + }; + if let Err(e) = db.delete(channel_id) { + warn!(%e, channel_id, "failed to delete channel from database"); } } /// Look up a usable persisted channel by key. -pub fn find_channel( - channels: &HashMap, - key: &str, -) -> Option { - channels.get(key).filter(|ch| ch.is_usable()).and_then(|ch| ch.to_channel_entry()) +pub fn find_channel(channels: &HashMap, key: &str) -> Option { + channels.get(key).filter(|ch| is_usable(ch)).and_then(to_channel_entry) } -/// Insert or update a channel entry in memory only (no disk write). -/// -/// Use [`upsert_channel`] when you want to persist immediately, or call -/// [`save_channels`] separately after this. +/// Insert or update a channel entry in memory only (no DB write). pub fn upsert_channel_in_memory( - channels: &mut HashMap, + channels: &mut HashMap, key: &str, entry: &ChannelEntry, - deposit: u128, - origin: &str, ) { - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() as i64; if let Some(existing) = channels.get_mut(key) { existing.cumulative_amount = entry.cumulative_amount.to_string(); existing.last_used_at = now; - existing.status = if entry.opened { "active" } else { "closed" }.to_string(); + existing.state = if entry.opened { "active" } else { "closed" }.to_string(); } else { - channels - .insert(key.to_string(), PersistedChannel::from_channel_entry(entry, deposit, origin)); + warn!(key, "upsert_channel_in_memory called for unknown channel"); } } -/// Insert or update a channel entry and save to disk. -/// -/// When updating an existing entry, `deposit` is ignored (preserved from the -/// original open). When inserting a new entry, `deposit` is recorded. -pub fn upsert_channel( - channels: &mut HashMap, - key: &str, - entry: &ChannelEntry, - deposit: u128, - origin: &str, -) { - upsert_channel_in_memory(channels, key, entry, deposit, origin); - save_channels(channels); -} - #[cfg(test)] mod tests { use super::*; - fn test_channel(status: &str, cumulative: &str, deposit: &str) -> PersistedChannel { - PersistedChannel { + fn test_channel(state: &str, cumulative: &str, deposit: &str) -> Channel { + Channel { channel_id: format!("0x{}", "ab".repeat(32)), - salt: format!("0x{}", "cd".repeat(32)), - escrow_contract: "0xe1c4d3dce17bc111181ddf716f75bae49e61a336".to_string(), + version: 1, + origin: "https://rpc.mpp.moderato.tempo.xyz".to_string(), + request_url: String::new(), chain_id: 42431, - cumulative_amount: cumulative.to_string(), + escrow_contract: "0xe1c4d3dce17bc111181ddf716f75bae49e61a336".to_string(), + token: "0x20c0000000000000000000000000000000000000".to_string(), + payee: "0x3333333333333333333333333333333333333333".to_string(), + payer: "0x1111111111111111111111111111111111111111".to_string(), + authorized_signer: "0x1111111111111111111111111111111111111111".to_string(), + salt: format!("0x{}", "cd".repeat(32)), deposit: deposit.to_string(), - status: status.to_string(), - origin: "https://rpc.mpp.moderato.tempo.xyz".to_string(), + cumulative_amount: cumulative.to_string(), + challenge_echo: String::new(), + state: state.to_string(), + close_requested_at: 0, + grace_ready_at: 0, created_at: 1000, last_used_at: 1000, } } #[test] - fn is_usable() { - assert!(test_channel("active", "5000", "100000").is_usable()); - assert!(!test_channel("active", "100000", "100000").is_usable()); - assert!(!test_channel("active", "200000", "100000").is_usable()); - assert!(!test_channel("closed", "0", "100000").is_usable()); - assert!(!test_channel("closing", "0", "100000").is_usable()); + fn usable() { + assert!(is_usable(&test_channel("active", "5000", "100000"))); + assert!(!is_usable(&test_channel("active", "100000", "100000"))); + assert!(!is_usable(&test_channel("active", "200000", "100000"))); + assert!(!is_usable(&test_channel("closed", "0", "100000"))); + assert!(!is_usable(&test_channel("closing", "0", "100000"))); } #[test] @@ -251,8 +249,12 @@ mod tests { opened: true, }; - let persisted = PersistedChannel::from_channel_entry(&entry, 100_000, "https://rpc.test"); - let restored = persisted.to_channel_entry().expect("should parse back"); + let payer = Address::random(); + let payee = Address::random(); + let token = Address::random(); + let persisted = + from_channel_entry(&entry, 100_000, "https://rpc.test", &payer, &payee, &token, &payer); + let restored = to_channel_entry(&persisted).expect("should parse back"); assert_eq!(restored.channel_id, entry.channel_id); assert_eq!(restored.salt, entry.salt); @@ -262,46 +264,6 @@ mod tests { assert!(restored.opened); } - #[test] - fn load_evicts_and_handles_edge_cases() { - let dir = tempfile::tempdir().unwrap(); - let foundry_dir = dir.path().join("foundry"); - std::fs::create_dir_all(&foundry_dir).unwrap(); - - let store = ChannelStore { - version: SCHEMA_VERSION, - channels: HashMap::from([ - ("active".into(), test_channel("active", "1000", "100000")), - ("spent".into(), test_channel("active", "100000", "100000")), - ("closed".into(), test_channel("closed", "0", "100000")), - ]), - }; - let json = serde_json::to_string(&store).unwrap(); - std::fs::write(foundry_dir.join("channels.json"), &json).unwrap(); - - unsafe { std::env::set_var("TEMPO_HOME", dir.path()) }; - let loaded = load_channels(); - unsafe { std::env::remove_var("TEMPO_HOME") }; - - assert_eq!(loaded.len(), 1); - assert!(loaded.contains_key("active")); - } - - #[test] - fn load_missing_and_wrong_version() { - let dir = tempfile::tempdir().unwrap(); - unsafe { std::env::set_var("TEMPO_HOME", dir.path()) }; - assert!(load_channels().is_empty()); - - let foundry_dir = dir.path().join("foundry"); - std::fs::create_dir_all(&foundry_dir).unwrap(); - std::fs::write(foundry_dir.join("channels.json"), r#"{"version": 999, "channels": {}}"#) - .unwrap(); - assert!(load_channels().is_empty()); - - unsafe { std::env::remove_var("TEMPO_HOME") }; - } - #[test] fn find_channel_filters_unusable() { let mut channels = HashMap::new(); @@ -312,34 +274,4 @@ mod tests { assert!(find_channel(&channels, "spent").is_none()); assert!(find_channel(&channels, "missing").is_none()); } - - #[test] - fn upsert_inserts_and_updates() { - let dir = tempfile::tempdir().unwrap(); - unsafe { std::env::set_var("TEMPO_HOME", dir.path()) }; - - let mut channels = HashMap::new(); - let entry = ChannelEntry { - channel_id: B256::random(), - salt: B256::random(), - cumulative_amount: 1000, - escrow_contract: Address::random(), - chain_id: 42431, - opened: true, - }; - - upsert_channel(&mut channels, "key1", &entry, 100_000, "https://rpc.test"); - assert_eq!(channels["key1"].cumulative_amount, "1000"); - assert_eq!(channels["key1"].deposit, "100000"); - let created_at = channels["key1"].created_at; - - let mut updated = entry.clone(); - updated.cumulative_amount = 5000; - upsert_channel(&mut channels, "key1", &updated, 0, "https://rpc.test"); - assert_eq!(channels["key1"].cumulative_amount, "5000"); - assert_eq!(channels["key1"].deposit, "100000"); - assert_eq!(channels["key1"].created_at, created_at); - - unsafe { std::env::remove_var("TEMPO_HOME") }; - } } diff --git a/crates/common/src/provider/mpp/session.rs b/crates/common/src/provider/mpp/session.rs index 5996c933218d2..334166b844613 100644 --- a/crates/common/src/provider/mpp/session.rs +++ b/crates/common/src/provider/mpp/session.rs @@ -6,8 +6,9 @@ //! `eth_getTransactionCount`. This avoids the chicken-and-egg problem when //! the RPC endpoint is itself 402-gated. -use super::persist::{self, PersistedChannel}; +use super::persist; use alloy_primitives::{Address, B256, Bytes, TxKind, U256}; +use foundry_wallets::Channel; use mpp::{ client::{ PaymentProvider, @@ -42,7 +43,7 @@ static GLOBAL_CHANNELS: OnceLock>> = O /// /// Using a single map ensures saves from different origins don't clobber /// each other's state. -static GLOBAL_PERSISTED: OnceLock>>> = OnceLock::new(); +static GLOBAL_PERSISTED: OnceLock>>> = OnceLock::new(); /// Tracks uncommitted channel state from the most recent payment. /// @@ -86,7 +87,7 @@ pub struct SessionProvider { default_deposit: Option, channels: Arc>>, key_provisioned: Arc>, - persisted: Arc>>, + persisted: Arc>>, /// Tracks uncommitted open/top-up state for deferred persistence. pending: Arc>>, /// Chain ID from the key entry in `keys.toml` that was used to initialize @@ -128,10 +129,10 @@ impl SessionProvider { map.entry(origin.clone()) .or_insert_with(|| { // Hydrate only channels belonging to this origin. - let mut channels = HashMap::new(); + let mut channels: HashMap = HashMap::new(); for (key, ch) in persisted.lock().unwrap().iter() { if ch.origin == origin - && let Some(entry) = ch.to_channel_entry() + && let Some(entry) = persist::to_channel_entry(ch) { channels.insert(key.clone(), entry); } @@ -209,16 +210,16 @@ impl SessionProvider { // Lock order: channels → persisted (consistent with pay_session) let mut channels = self.channels.lock().unwrap(); let mut persisted = self.persisted.lock().unwrap(); - let keys_to_remove: Vec = persisted + let keys_to_remove: Vec<(String, String)> = persisted .iter() .filter(|(_, ch)| ch.origin == *origin) - .map(|(k, _)| k.clone()) + .map(|(k, ch): (&String, &Channel)| (k.clone(), ch.channel_id.clone())) .collect(); - for key in &keys_to_remove { + for (key, channel_id) in &keys_to_remove { channels.remove(key); persisted.remove(key); + persist::delete_channel_from_db(channel_id); } - persist::save_channels(&persisted); } /// Mark whether the access key has been provisioned on-chain. @@ -685,13 +686,7 @@ impl SessionProvider { // confirms acceptance. let updated_entry = ChannelEntry { cumulative_amount: new_cumulative, ..entry }; let mut persisted = self.persisted.lock().unwrap(); - persist::upsert_channel_in_memory( - &mut persisted, - &key, - &updated_entry, - 0, - &self.origin, - ); + persist::upsert_channel_in_memory(&mut persisted, &key, &updated_entry); drop(persisted); // Track the voucher so we can roll back cumulative_amount @@ -727,9 +722,18 @@ impl SessionProvider { // Update in-memory state but defer disk persistence until server confirms. self.channels.lock().unwrap().insert(key.clone(), entry.clone()); + let authorized_signer = self.authorized_signer.unwrap_or(payer); self.persisted.lock().unwrap().insert( key.clone(), - persist::PersistedChannel::from_channel_entry(&entry, deposit, &self.origin), + persist::from_channel_entry( + &entry, + deposit, + &self.origin, + &payer, + &payee, + ¤cy, + &authorized_signer, + ), ); *self.pending.lock().unwrap() = Some(PendingAction::Open { key }); Ok(build_credential(challenge, payload, chain_id, payer))