use crate::error::{Error, Result};
use crate::replay::Mutation;
use crate::scope::{AccountState, Scope};
use serde_json::{json, Value};
use solana_client::rpc_client::RpcClient;
use solana_client::rpc_request::RpcRequest;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WriteRef {
pub signature: String,
pub slot: u64,
pub failed: bool,
}
pub trait LedgerSource {
fn signatures_for_address(
&self,
address: &str,
before: Option<&str>,
limit: usize,
) -> Result<Vec<WriteRef>>;
fn transaction(&self, signature: &str) -> Result<Value>;
}
pub struct RpcLedger {
client: RpcClient,
}
impl RpcLedger {
pub fn new(url: impl Into<String>) -> RpcLedger {
RpcLedger {
client: RpcClient::new(url.into()),
}
}
}
impl LedgerSource for RpcLedger {
fn signatures_for_address(
&self,
address: &str,
before: Option<&str>,
limit: usize,
) -> Result<Vec<WriteRef>> {
let mut opts = json!({ "limit": limit });
if let Some(b) = before {
opts["before"] = json!(b);
}
let resp: Value = self
.client
.send(RpcRequest::GetSignaturesForAddress, json!([address, opts]))
.map_err(Error::rpc)?;
let arr = resp.as_array().ok_or_else(|| {
Error::MalformedRpcResponse("getSignaturesForAddress: not an array".into())
})?;
Ok(arr
.iter()
.filter_map(|s| {
Some(WriteRef {
signature: s["signature"].as_str()?.to_string(),
slot: s["slot"].as_u64()?,
failed: !s["err"].is_null(),
})
})
.collect())
}
fn transaction(&self, signature: &str) -> Result<Value> {
let resp: Value = self
.client
.send(
RpcRequest::GetTransaction,
json!([
signature,
{ "encoding": "json", "commitment": "confirmed", "maxSupportedTransactionVersion": 0 }
]),
)
.map_err(Error::rpc)?;
if resp.is_null() {
return Err(Error::TransactionNotFound(signature.to_string()));
}
Ok(resp)
}
}
pub fn write_history(
src: &dyn LedgerSource,
address: &str,
before_slot: u64,
max: usize,
max_pages: usize,
) -> Result<Vec<WriteRef>> {
let page = 1000usize.min(max.max(1));
let mut out: Vec<WriteRef> = Vec::new();
let mut before: Option<String> = None;
for _ in 0..max_pages {
let batch = src.signatures_for_address(address, before.as_deref(), page)?;
let Some(last) = batch.last() else {
break; };
before = Some(last.signature.clone());
for w in &batch {
if w.slot < before_slot && !w.failed {
out.push(w.clone());
if out.len() >= max {
break;
}
}
}
if out.len() >= max {
break;
}
}
out.reverse();
Ok(out)
}
#[derive(Debug, Clone)]
pub struct Reconstructed {
pub address: String,
pub before_slot: u64,
pub state: Option<AccountState>,
pub writes_replayed: usize,
pub writes_skipped: usize,
}
pub fn reconstruct_account(
scope: &Scope,
ledger: &dyn LedgerSource,
address: &str,
before_slot: u64,
max_writes: usize,
max_pages: usize,
) -> Result<Reconstructed> {
let history = write_history(ledger, address, before_slot, max_writes, max_pages)?;
let mut state: Option<AccountState> = None;
let mut replayed = 0usize;
let mut skipped = 0usize;
for w in &history {
let replay = match scope.replay(&w.signature) {
Ok(r) => r,
Err(_) => {
skipped += 1;
continue;
}
};
let muts: Vec<Mutation> = match &state {
Some(s) => vec![
Mutation::data(address.to_string(), s.data.clone()),
Mutation::lamports(address.to_string(), s.lamports),
],
None => Vec::new(),
};
match replay.account_after(&muts, address) {
Ok(after) => {
state = after; replayed += 1;
}
Err(_) => skipped += 1,
}
}
Ok(Reconstructed {
address: address.to_string(),
before_slot,
state,
writes_replayed: replayed,
writes_skipped: skipped,
})
}
#[derive(Debug, Clone)]
pub struct Recon {
pub state: Option<AccountState>,
pub exact: bool,
}
fn is_infra(address: &str) -> bool {
address.starts_with("Sysvar")
|| matches!(
address,
"11111111111111111111111111111111" | "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" | "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" | "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" | "ComputeBudget111111111111111111111111111111"
| "NativeLoader1111111111111111111111111111111"
| "BPFLoader2111111111111111111111111111111111"
| "BPFLoaderUpgradeab1e11111111111111111111111"
)
}
fn last_write(
src: &dyn LedgerSource,
address: &str,
slot: u64,
max_pages: usize,
) -> Result<Option<WriteRef>> {
Ok(write_history(src, address, slot, 1, max_pages)?
.into_iter()
.next())
}
pub struct Reconstructor<'a> {
scope: &'a Scope,
ledger: &'a dyn LedgerSource,
memo: HashMap<(String, u64), Recon>,
budget: usize,
max_pages: usize,
replays: usize,
}
impl<'a> Reconstructor<'a> {
pub fn new(scope: &'a Scope, ledger: &'a dyn LedgerSource, budget: usize) -> Self {
Reconstructor {
scope,
ledger,
memo: HashMap::new(),
budget,
max_pages: 20,
replays: 0,
}
}
pub fn replays(&self) -> usize {
self.replays
}
pub fn reconstruct(&mut self, address: &str, before_slot: u64) -> Result<Recon> {
let key = (address.to_string(), before_slot);
if let Some(r) = self.memo.get(&key) {
return Ok(r.clone());
}
let recon = self.compute(address, before_slot)?;
self.memo.insert(key, recon.clone());
Ok(recon)
}
fn compute(&mut self, address: &str, before_slot: u64) -> Result<Recon> {
let Some(last) = last_write(self.ledger, address, before_slot, self.max_pages)? else {
return Ok(Recon {
state: None,
exact: true,
});
};
if self.replays >= self.budget {
let state = self.scope.account_data(address)?;
return Ok(Recon {
state,
exact: false,
});
}
let tx = self.ledger.transaction(&last.signature)?;
let keys = crate::utils::resolve_account_keys(&tx);
let mut muts: Vec<Mutation> = Vec::new();
let mut cone_exact = true;
for b in &keys {
if is_infra(b) {
continue; }
let rec_b = self.reconstruct(b, last.slot)?;
cone_exact &= rec_b.exact;
if let Some(s) = rec_b.state {
muts.push(Mutation::data(b.clone(), s.data));
muts.push(Mutation::lamports(b.clone(), s.lamports));
}
}
self.replays += 1;
let replay = match self.scope.replay(&last.signature) {
Ok(r) => r,
Err(_) => {
let state = self.scope.account_data(address)?;
return Ok(Recon {
state,
exact: false,
});
}
};
let state = replay.account_after(&muts, address)?;
Ok(Recon {
state,
exact: cone_exact,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockLedger {
sigs: Vec<WriteRef>,
}
impl LedgerSource for MockLedger {
fn signatures_for_address(
&self,
_address: &str,
before: Option<&str>,
limit: usize,
) -> Result<Vec<WriteRef>> {
let start = match before {
None => 0,
Some(b) => self
.sigs
.iter()
.position(|w| w.signature == b)
.map(|i| i + 1)
.unwrap_or(self.sigs.len()),
};
Ok(self.sigs[start..].iter().take(limit).cloned().collect())
}
fn transaction(&self, _signature: &str) -> Result<Value> {
Ok(json!({}))
}
}
fn w(sig: &str, slot: u64, failed: bool) -> WriteRef {
WriteRef {
signature: sig.to_string(),
slot,
failed,
}
}
#[test]
fn write_history_keeps_pre_slot_successes_oldest_first() {
let ledger = MockLedger {
sigs: vec![
w("s120", 120, false), w("s090", 90, false),
w("s080", 80, true), w("s070", 70, false),
w("s060", 60, false),
],
};
let hist = write_history(&ledger, "Acc", 100, 100, 10).unwrap();
let sigs: Vec<&str> = hist.iter().map(|w| w.signature.as_str()).collect();
assert_eq!(sigs, vec!["s060", "s070", "s090"]);
}
#[test]
fn infra_accounts_are_recognised_and_skipped() {
assert!(is_infra("11111111111111111111111111111111")); assert!(is_infra("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")); assert!(is_infra("SysvarC1ock11111111111111111111111111111111")); assert!(is_infra("ComputeBudget111111111111111111111111111111"));
assert!(!is_infra("Cd2zEXTrYoV4UcDxZJumwZsz4A1bSZfRuZpEu5RJDDVk"));
}
#[test]
fn last_write_returns_the_most_recent_before_the_slot() {
let ledger = MockLedger {
sigs: vec![
w("s150", 150, false),
w("s099", 99, false), w("s050", 50, false),
],
};
let lw = last_write(&ledger, "Acc", 100, 10).unwrap().unwrap();
assert_eq!(lw.signature, "s099");
assert!(last_write(&ledger, "Acc", 40, 10).unwrap().is_none());
}
#[test]
fn write_history_respects_the_max_cap() {
let ledger = MockLedger {
sigs: (0..50)
.map(|i| w(&format!("s{i:02}"), 50 - i, false))
.collect(),
};
let hist = write_history(&ledger, "Acc", 1000, 5, 10).unwrap();
assert_eq!(hist.len(), 5);
assert!(hist[0].slot < hist[hist.len() - 1].slot);
}
}