Skip to content

Commit 4d7db7f

Browse files
0xl3onzeroXbrock
andauthored
feat: move default data dir (#499)
* feat: move default data dir * docs: update changelogs * docs: update path references --------- Co-authored-by: brock 🤖⚡ <2791467+zeroXbrock@users.noreply.github.com>
1 parent 7dbdc5c commit 4d7db7f

7 files changed

Lines changed: 119 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
55

66
> Note: this file did not exist until after `v0.5.6`.
77
8+
## [Unreleased]
9+
10+
- move default data dir to `$XDG_STATE_HOME/contender` (`~/.local/state/contender`), with automatic migration from legacy `~/.contender` ([#460](https://github.com/flashbots/contender/issues/460))
11+
812
## [0.9.0](https://github.com/flashbots/contender/releases/tag/v0.9.0) - 2026-03-17
913

1014
- added `--send-raw-tx-sync` flag to `spam` and `campaign` for `eth_sendRawTransactionSync` support ([#459](https://github.com/flashbots/contender/pull/459))

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ export RPC="http://host.docker.internal:8545"
4545
Run contender in a container with persistent state:
4646

4747
```bash
48-
docker run -it -v /tmp/.contender:/root/.contender \
48+
docker run -it -v /tmp/.contender:/root/.local/state/contender \
4949
contender spam --tps 20 -r $RPC transfers
5050
```
5151

52-
> `-v` maps `/tmp/.contender` on the host machine to `/root/.contender` in the container, which contains the DB; used for generating reports and saving contract deployments.
52+
> `-v` maps `/tmp/.contender` on the host machine to `/root/.local/state/contender` in the container, which contains the DB; used for generating reports and saving contract deployments.
5353
5454
## ⚙️ Prerequisites
5555

crates/cli/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
- move default data dir from `~/.contender` to `$XDG_STATE_HOME/contender` (defaults to `~/.local/state/contender`), with automatic migration of existing data ([#460](https://github.com/flashbots/contender/issues/460))
11+
812
## [0.9.0](https://github.com/flashbots/contender/releases/tag/v0.9.0) - 2026-03-17
913

1014
- added `--send-raw-tx-sync` flag to `spam` and `campaign` commands ([#459](https://github.com/flashbots/contender/pull/459))

crates/cli/src/commands/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pub enum ReportFormat {
4040
about = "A flexible JSON-RPC spammer for EVM chains."
4141
)]
4242
pub struct ContenderCli {
43-
/// Override the default data directory (~/.contender).
43+
/// Override the default data directory (~/.local/state/contender).
4444
/// This directory stores the database and reports.
4545
#[arg(long, global = true, env = "CONTENDER_DATA_DIR", value_name = "PATH")]
4646
pub data_dir: Option<PathBuf>,

crates/cli/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async fn run() -> Result<(), CliError> {
4747
let db_path = db_file_in(&data_dir);
4848
init_reports_dir(&data_dir);
4949

50-
debug!("data directory: {data_dir:?}");
50+
info!("data directory: {}", data_dir.display());
5151
debug!("opening DB at {db_path:?}");
5252

5353
let db = SqliteDb::from_file(&db_path)?;

crates/cli/src/util/utils.rs

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -380,15 +380,43 @@ pub async fn find_insufficient_balances(
380380

381381
/// Returns the path to the default data directory.
382382
/// The directory is created if it does not exist.
383+
///
384+
/// Uses `$XDG_STATE_HOME/contender` (defaulting to `~/.local/state/contender`).
385+
/// If the legacy `~/.contender` directory exists, it is migrated to the new location.
383386
pub fn default_data_dir() -> Result<PathBuf, UtilError> {
384-
let home_dir = if cfg!(windows) {
385-
std::env::var("USERPROFILE")?
386-
} else {
387-
std::env::var("HOME")?
387+
let home_dir = PathBuf::from(std::env::var("HOME")?);
388+
let xdg_state_home = std::env::var("XDG_STATE_HOME").ok();
389+
resolve_default_data_dir(&home_dir, xdg_state_home.as_deref())
390+
}
391+
392+
/// Core logic for resolving the default data directory. Extracted for testability.
393+
fn resolve_default_data_dir(
394+
home_dir: &Path,
395+
xdg_state_home: Option<&str>,
396+
) -> Result<PathBuf, UtilError> {
397+
let state_home = match xdg_state_home {
398+
Some(val) if !val.is_empty() => PathBuf::from(val),
399+
_ => home_dir.join(".local").join("state"),
388400
};
389-
let home_dir = PathBuf::from(&home_dir);
390401

391-
let dir = home_dir.join(".contender");
402+
let dir = state_home.join("contender");
403+
404+
// migrate legacy ~/.contender to the new location
405+
let legacy_dir = home_dir.join(".contender");
406+
if legacy_dir.exists() && !dir.exists() {
407+
if let Some(parent) = dir.parent() {
408+
std::fs::create_dir_all(parent)?;
409+
}
410+
if std::fs::rename(&legacy_dir, &dir).is_err() {
411+
// rename can fail across filesystem boundaries; warn and continue
412+
eprintln!(
413+
"warning: failed to migrate legacy data dir '{}' to '{}'. \
414+
You can move it manually or set CONTENDER_DATA_DIR.",
415+
legacy_dir.display(),
416+
dir.display()
417+
);
418+
}
419+
}
392420

393421
// ensure directory exists
394422
std::fs::create_dir_all(&dir)?;
@@ -398,7 +426,7 @@ pub fn default_data_dir() -> Result<PathBuf, UtilError> {
398426
/// Resolves the data directory with the following priority:
399427
/// 1. CLI argument (if provided)
400428
/// 2. CONTENDER_DATA_DIR environment variable (if set)
401-
/// 3. Default: $HOME/.contender
429+
/// 3. Default: $XDG_STATE_HOME/contender (or ~/.local/state/contender)
402430
///
403431
/// Creates the directory if it does not exist.
404432
pub fn resolve_data_dir(cli_arg: Option<std::path::PathBuf>) -> Result<PathBuf, UtilError> {
@@ -852,6 +880,77 @@ mod test {
852880
);
853881
}
854882

883+
#[test]
884+
fn default_data_dir_uses_xdg_state_home() {
885+
let tmp = tempfile::tempdir().unwrap();
886+
let home = tmp.path().join("home");
887+
let xdg = tmp.path().join("custom_state");
888+
std::fs::create_dir_all(&home).unwrap();
889+
890+
let result = super::resolve_default_data_dir(&home, Some(xdg.to_str().unwrap())).unwrap();
891+
assert_eq!(result, xdg.join("contender"));
892+
assert!(result.exists());
893+
}
894+
895+
#[test]
896+
fn default_data_dir_falls_back_to_local_state() {
897+
let tmp = tempfile::tempdir().unwrap();
898+
let home = tmp.path().join("home");
899+
std::fs::create_dir_all(&home).unwrap();
900+
901+
let result = super::resolve_default_data_dir(&home, None).unwrap();
902+
assert_eq!(result, home.join(".local").join("state").join("contender"));
903+
assert!(result.exists());
904+
}
905+
906+
#[test]
907+
fn default_data_dir_ignores_empty_xdg() {
908+
let tmp = tempfile::tempdir().unwrap();
909+
let home = tmp.path().join("home");
910+
std::fs::create_dir_all(&home).unwrap();
911+
912+
let result = super::resolve_default_data_dir(&home, Some("")).unwrap();
913+
assert_eq!(result, home.join(".local").join("state").join("contender"));
914+
}
915+
916+
#[test]
917+
fn default_data_dir_migrates_legacy_dir() {
918+
let tmp = tempfile::tempdir().unwrap();
919+
let home = tmp.path().join("home");
920+
let legacy = home.join(".contender");
921+
std::fs::create_dir_all(&legacy).unwrap();
922+
std::fs::write(legacy.join("contender.db"), b"test data").unwrap();
923+
924+
let result = super::resolve_default_data_dir(&home, None).unwrap();
925+
assert_eq!(result, home.join(".local").join("state").join("contender"));
926+
// legacy dir should be gone, data should be at new location
927+
assert!(!legacy.exists());
928+
assert_eq!(
929+
std::fs::read_to_string(result.join("contender.db")).unwrap(),
930+
"test data"
931+
);
932+
}
933+
934+
#[test]
935+
fn default_data_dir_skips_migration_if_new_dir_exists() {
936+
let tmp = tempfile::tempdir().unwrap();
937+
let home = tmp.path().join("home");
938+
let legacy = home.join(".contender");
939+
let new_dir = home.join(".local").join("state").join("contender");
940+
std::fs::create_dir_all(&legacy).unwrap();
941+
std::fs::create_dir_all(&new_dir).unwrap();
942+
std::fs::write(legacy.join("old.db"), b"old").unwrap();
943+
std::fs::write(new_dir.join("new.db"), b"new").unwrap();
944+
945+
let result = super::resolve_default_data_dir(&home, None).unwrap();
946+
// both dirs should still exist, no overwrite
947+
assert!(legacy.exists());
948+
assert_eq!(
949+
std::fs::read_to_string(result.join("new.db")).unwrap(),
950+
"new"
951+
);
952+
}
953+
855954
#[test]
856955
fn human_readable_gas_works() {
857956
assert_eq!(human_readable_gas(500), "500 gas");

docs/campaigns.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Flags mirror `spam` where they make sense:
9797
- Campaign summary: `contender report --campaign [<campaign_id>]` (alias: `--campaign-id`)
9898
- If `<campaign_id>` is omitted, the latest campaign is used.
9999
- Generates per-run HTML for all runs in the campaign.
100-
- Writes `campaign-<campaign_id>.html` and `campaign-<campaign_id>.json` under `~/.contender/reports/` with links, aggregate metrics, and per-stage/per-scenario rollups.
100+
- Writes `campaign-<campaign_id>.html` and `campaign-<campaign_id>.json` under `~/.local/state/contender/reports/` with links, aggregate metrics, and per-stage/per-scenario rollups.
101101
- If you pass `--report` to `contender campaign ...`, contender will also generate a report for the run-id range at the end of the campaign.
102102
- If transaction logs are incomplete for any run (e.g., tracing/storage gaps), the campaign report will use stored run metadata for totals/durations and will display a notice; error counts may be under-reported in that case.
103103
- When a stage has multiple `[[spam.stage.mix]]` entries, do not combine it with `--override-senders`; using a single sender across mixes is rejected because it would cause nonce conflicts.

0 commit comments

Comments
 (0)