Skip to content

feat: improve generation script, add wallet export, add tests#1

Merged
xdustinface merged 1 commit into
mainfrom
feat/strategy
Feb 12, 2026
Merged

feat: improve generation script, add wallet export, add tests#1
xdustinface merged 1 commit into
mainfrom
feat/strategy

Conversation

@xdustinface
Copy link
Copy Markdown
Contributor

@xdustinface xdustinface commented Feb 12, 2026

  • Refactor generate.py into a base Generator class with one strategy: WalletSyncGenerator (SPV
    wallet sync edge cases).
  • Extract shared wallet stats collection into generator/wallet_export.py.
  • Add export_wallets.py for exporting stats from existing blockchain data.
  • Improve dashd_manager extra_args support and rpc_client error handling.
  • Add test suite with unit and integration tests to make sure the script is working.

Summary by CodeRabbit

  • New Features

    • Strategy-based generation modes with wallet-sync default and multi-phase flows
    • Wallet statistics export and a CLI utility to save per-wallet JSON summaries
    • Dash daemon setup helper to download/install a test daemon; expanded CLI options (strategy, dashd path, rpc port, extra args)
  • Tests

    • Comprehensive unit and integration tests covering generation, wallet-sync, CLI, and exported wallet outputs
  • Chores

    • Added Python-related ignore patterns to .gitignore

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Refactors generator into a strategy-driven class system (wallet-sync, random), adds wallet export utilities and a standalone export script, enhances dashd lifecycle and RPC client behavior, changes error types, adds a dashd setup helper, updates .gitignore for Python, and introduces unit and integration tests.

Changes

Cohort / File(s) Summary
Repo ignore
/.gitignore
Add Python ignores: __pycache__/, .venv/, .pytest_cache/.
Core generator & CLI
generate.py
Major refactor to strategy-based Generator API; adds WalletSyncGenerator and RandomGenerator, new lifecycle/export hooks, new Config fields (extra_dashd_args, nullable dashd_datadir, nullable rpc_port), and CLI --strategy flow plus multi-phase generation logic.
Dashd management
generator/dashd_manager.py
Add dashd_preexec_fn to limit file descriptors, accept extra_args, improve executable resolution, append extra args to command, adjust typing and start() return, remove get_port/get_temp_dir helpers, and refine startup/error handling.
RPC client
generator/rpc_client.py
Increase rpc_timeout (30→120s), modernize type hints, change call/_execute signatures to accept `wallet: str
Wallet export utilities
generator/wallet_export.py
New collect_wallet_stats() and save_wallet_file() to aggregate transactions, UTXOs, balance, mnemonic and write per-wallet JSON exports with non-fatal RPC warning handling.
Standalone export script
export_wallets.py
New CLI that discovers free ports, launches dashd with discovered ports, discovers/loads wallets (RPC or filesystem fallback), collects stats per wallet, writes per-wallet JSON files under datadir/wallets, and ensures cleanup on exit.
Generator errors
generator/errors.py
Remove ValidationError and ExportError; simplify several exception class bodies (GeneratorError, InsufficientFundsError, DashdConnectionError, ConfigError).
Dashd setup helper
contrib/setup-dashd.py
New script to download/extract prebuilt dashd for CI; platform-aware asset selection, cache dir handling, extraction, validation, and outputs DASHD_PATH.
Tests — unit
tests/test_generate.py
Add unit tests for WalletSyncGenerator behavior, batch/boundary calculations, Config defaults/overrides, and presence of generator phase methods.
Tests — integration
tests/test_integration.py
Add integration tests for CLI argument validation and end-to-end wallet-sync generation asserting output layout, wallet JSON contents, mnemonics, transaction/UTXO counts, and basic data integrity.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,200,255,0.5)
    actor User
    end
    participant CLI as CLI
    participant Gen as Generator\n(strategy)
    participant Dashd as DashdManager
    participant RPC as DashRPCClient
    participant FS as Filesystem

    User->>CLI: invoke generate.py / export_wallets.py
    CLI->>Gen: instantiate chosen strategy
    Gen->>Dashd: start(dashd_executable, extra_args, rpc_port)
    Dashd->>Dashd: verify executable, set preexec limits
    Dashd->>RPC: create RPC client (rpc_port, datadir)
    RPC->>Dashd: poll getblockcount until ready
    Dashd-->>Gen: return rpc client & temp dir

    Gen->>Gen: run strategy phases / generate txs
    Gen->>RPC: create/broadcast txs, mine blocks
    RPC->>FS: write chainstate / wallet files
    Gen->>Gen: _collect_wallet_statistics()
    Gen->>FS: _save_wallet_files() -> write per-wallet JSON
    Gen->>Dashd: shutdown
    Dashd-->>CLI: cleanup complete
    CLI-->>User: finished
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through code with nimble paws,

Crafted strategies, tightened jaws.
Ports I sniffed, dashd hummed and sprung,
Wallets counted, JSONs sung.
A carrot export — tidy and fun!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: improve generation script, add wallet export, add tests' accurately captures the main objectives of the changeset: refactoring the generate.py script, extracting wallet export functionality, and adding comprehensive tests.
Docstring Coverage ✅ Passed Docstring coverage is 82.76% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/strategy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
generator/rpc_client.py (1)

36-50: ⚠️ Potential issue | 🟡 Minor

call() implicitly returns None if max_retries ≤ 0.

If max_retries is set to 0, range(0) produces an empty loop and the method silently returns None instead of making any RPC call. This is unlikely with the default of 3, but a defensive guard would prevent subtle bugs from misconfiguration.

Proposed fix
     def call(self, method: str, *params, wallet: str | None = None) -> Any:
         """
         Call RPC method with retry logic and comprehensive error handling.
         """
+        if self.max_retries < 1:
+            return self._execute(method, params, wallet)
         for attempt in range(self.max_retries):
🤖 Fix all issues with AI agents
In `@export_wallets.py`:
- Around line 91-98: preexec_fn currently tries to setrlimit to a hardcoded
(10000,10000) which may exceed the system hard limit and silently fails; update
preexec_fn to read the current hard limit via
resource.getrlimit(resource.RLIMIT_NOFILE), cap the desired value at that hard
limit (like dashd_manager.py does), and then call resource.setrlimit with the
min-capped values; also change the subprocess.Popen call for proc to not discard
dashd errors—use stderr=subprocess.PIPE (or otherwise preserve stderr) instead
of subprocess.DEVNULL so startup errors are visible; reference
functions/symbols: preexec_fn, resource.getrlimit/resource.setrlimit, and
subprocess.Popen/stderr.
- Around line 100-104: The cleanup function should accept an optional exit_code
(default 0), call proc.terminate() and proc.wait(timeout=10) inside a try/except
catching subprocess.TimeoutExpired and in that except call proc.kill() then
proc.wait() to ensure the process is stopped; finally call sys.exit(exit_code).
Update all failure call sites that currently call cleanup() to pass a non-zero
code (e.g., cleanup(1)) while success paths keep cleanup() or cleanup(0). Refer
to the cleanup function and callers that invoke cleanup on success/failure and
to the proc variable used inside cleanup.
- Around line 4-5: The import of the Unix-only module resource in
export_wallets.py will crash on Windows; change the top-level import to a
guarded import (try/except ImportError) so resource is optional (set to None on
import failure) and then update the preexec_fn that references resource to check
that resource is not None before calling any resource.* functions (or skip the
resource-specific calls when None); ensure the unique symbol preexec_fn in this
file is adjusted to conditionally apply resource.setrlimit only when the
resource module was successfully imported.

In `@generate.py`:
- Around line 940-961: The stats["blocks_generated"] field is inconsistent
across strategies; make it represent only blocks generated by the generator loop
(not absolute chain height). In _initialize_utxo_pool, remove or replace the
assignment self.stats["blocks_generated"] = self.rpc.call("getblockcount") with
a value that counts only generation-loop blocks (e.g., 0 or the number of
bootstrap blocks explicitly tracked as "bootstrap_blocks"), and likewise update
WalletSyncGenerator where it sets stats["blocks_generated"] to the chain height
(lines referencing WalletSyncGenerator) so those places either stop assigning
absolute heights or store bootstrap height under a different key; ensure
RandomGenerator._generate_blocks continues to increment
stats["blocks_generated"] so the summary at the end reports the same semantic
meaning across strategies.

In `@generator/dashd_manager.py`:
- Around line 126-136: preexec_fn currently attempts
resource.setrlimit(RLIMIT_NOFILE, (target, hard)) and silently swallows OSError
when soft>hard; change the logic to compute new_soft = min(target, hard) and
new_hard = min(hard, target) (or handle RLIM_INFINITY appropriately) so you
never call setrlimit with soft>hard, then call
resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, new_hard)) inside the
try/except in preexec_fn (still catching OSError) to ensure the soft limit is
raised up to the available hard limit when the hard limit is below the target.
🧹 Nitpick comments (8)
generator/errors.py (1)

17-19: Type annotation inconsistency: code: int = None should use int | None = None.

The rest of the PR consistently uses PEP 604 union types (e.g., str | None in rpc_client.py, dashd_manager.py). This parameter should match for consistency.

Proposed fix
-    def __init__(self, message: str, code: int = None):
+    def __init__(self, message: str, code: int | None = None):
generator/wallet_export.py (1)

82-83: Specify explicit encoding="utf-8" for cross-platform consistency.

Without an explicit encoding, open() uses the platform default (which may not be UTF-8 on Windows).

Proposed fix
-    with open(output_path, "w") as f:
+    with open(output_path, "w", encoding="utf-8") as f:
tests/test_generate.py (2)

259-278: Test validates the concept but not the actual implementation.

This test re-implements the weighted selection logic inline (random.choices(tier_wallets, weights=weights, k=1)) rather than exercising the actual _create_transaction method or any generator method that performs wallet selection. If the generator's weighting logic diverges from this test, the test would still pass.

Consider testing the actual generator method (e.g., calling _create_transaction with a mock RPC) or at minimum adding a comment noting that this tests the algorithm in isolation, not the integration.


420-423: Redundant import pytest — already imported at Line 7.

Proposed fix
 if __name__ == "__main__":
-    import pytest
-
     pytest.main([__file__, "-v"])
generate.py (4)

196-206: Consider using abc.ABC and @abstractmethod instead of manual NotImplementedError.

The base Generator uses raise NotImplementedError to enforce the subclass contract. Python's abc module would enforce this at instantiation time rather than at call time, catching missing implementations earlier. The test at Line 374-386 in test_generate.py directly instantiates Generator(config), which would need adjustment if ABC were used, but it would provide a stronger contract.

This isn't urgent since the current pattern works and is tested.


245-295: Shutdown sequence in _export_data directly manipulates dashd_manager internals.

Lines 271-278 manually terminate the process and set self.dashd_manager.process = None, bypassing the DashdManager.stop() method. This duplicates logic and could diverge as stop() evolves (e.g., it already has a kill fallback that this code lacks). Consider extracting a stop_process_only() method on DashdManager or calling stop() with a flag to skip temp dir cleanup.


1198-1263: Local current_height can drift from actual chain height.

The bulk generation loop tracks current_height locally and increments it manually. Extra blocks from boundary transactions (Line 1260-1261), periodic sends (Line 1271-1272), and faucet self-sends (Line 1283-1284) are accounted for, but any RPC failure between _mine_blocks and the increment could cause drift. The final check at Lines 1299-1305 catches undershoots but may overshoot without correction.

Consider periodically syncing with the actual height (e.g., every N iterations):

Proposed addition inside the while loop
+            # Periodically sync with actual chain height to prevent drift
+            if current_height % 5000 == 0:
+                actual = self.rpc.call("getblockcount")
+                if actual != current_height:
+                    current_height = actual
+
             # Progress logging

117-130: KeyboardInterrupt is caught and re-raised as GeneratorError, then also caught at Line 1449.

Line 119 wraps KeyboardInterrupt into GeneratorError("User interrupted"). This will be caught by except GeneratorError at Line 1446, printing "ERROR: User interrupted" and exiting with code 4. The separate except KeyboardInterrupt at Line 1449 only fires if the interrupt happens outside generate(). This works but the double handling is slightly confusing — the user sees "ERROR: User interrupted" rather than the expected "\nInterrupted by user".

Comment thread export_wallets.py Outdated
Comment thread export_wallets.py Outdated
Comment thread export_wallets.py Outdated
Comment thread generate.py Outdated
Comment thread generator/dashd_manager.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@export_wallets.py`:
- Around line 76-101: The subprocess.Popen call that starts dashd (using
variables cmd and dashd_preexec_fn) can raise FileNotFoundError if the dashd
executable is missing; wrap the Popen invocation in a try/except that catches
FileNotFoundError (and optionally OSError), log or print a user-friendly error
message indicating the dashd executable was not found and include the attempted
executable name (dashd_executable), then exit with a non-zero code just like
DashdManager.start() does; ensure cleanup() is not registered/executed if
startup fails and keep the existing proc-related teardown logic for the
successful startup path.
- Around line 46-57: The check for network_subdir treats mainnet (where
network_subdir_name == "") the same as other networks causing a confusing
message like "No / subdirectory found"; update the logic around datadir /
network_subdir to special-case mainnet: if args.network == "mainnet" skip the
redundant existence check (since datadir was already validated) or change the
error message to omit the trailing slash and network name when
network_subdir_name is empty; modify the code that builds/validates
network_subdir (variables network_subdirs, network_subdir_name, network_subdir)
to either bypass the second exists() check for mainnet or produce a clear
message mentioning "mainnet" instead of an empty subdirectory.

In `@generate.py`:
- Around line 1219-1248: The code currently increments
self.stats["transactions_created"] when calling self._mine_blocks for coinbase
rewards in the mature/immature coinbase branches (using variables
mature_coinbase_start, immature_coinbase_start and coinbase_wallet_addr), which
incorrectly counts mined coinbase rewards as user transactions; change this by
incrementing a new counter like self.stats["coinbase_rewards"] (or similarly
named) instead of self.stats["transactions_created"] in both places where
wallet_blocks is added, and ensure the rest of the code/reporting uses
transactions_created only for actual sendtoaddress/sendmany calls.

In `@generator/wallet_export.py`:
- Around line 36-45: The list comprehension building utxos can raise KeyError
for missing keys; update the construction that iterates over wallet_utxos to use
safe .get() calls: replace u["address"] with u.get("address") (or
u.get("address", None) if you prefer explicit default) and replace
u["confirmations"] with u.get("confirmations", 0) so utxos creation in the utxos
variable is defensive against UTXOs missing those fields.
🧹 Nitpick comments (11)
generator/rpc_client.py (2)

36-50: Inconsistent exception chaining: from None vs from e.

Line 45 uses from None (suppresses original traceback) while Line 49 uses from e (preserves it). For timeouts, the original TimeoutExpired exception contains useful diagnostic info (command, timeout duration). Consider using from e consistently for both paths.

Proposed fix
-                    raise RPCError(f"RPC timeout after {self.rpc_timeout}s: {method}", code=-1) from None
+                    raise RPCError(f"RPC timeout after {self.rpc_timeout}s: {method}", code=-1) from e

52-73: if self.rpc_port: is falsy for port 0.

if self.rpc_port: will skip appending -rpcport=0 if port is 0. While port 0 is unlikely for RPC, the idiomatic check should be if self.rpc_port is not None: to match the int | None type annotation. Same applies to if self.datadir: on Line 56 (empty string vs None), and if wallet: on Line 62.

Proposed fix
-        if self.datadir:
+        if self.datadir is not None:
             cmd.append(f"-datadir={self.datadir}")
 
-        if self.rpc_port:
+        if self.rpc_port is not None:
             cmd.append(f"-rpcport={self.rpc_port}")
 
-        if wallet:
+        if wallet is not None:
             cmd.append(f"-rpcwallet={wallet}")
generator/wallet_export.py (1)

82-83: Specify encoding="utf-8" for portable JSON output.

open(output_path, "w") uses the platform default encoding, which may not be UTF-8 on all systems (e.g., Windows). Since JSON should be UTF-8, explicitly specify the encoding.

Proposed fix
-    with open(output_path, "w") as f:
+    with open(output_path, "w", encoding="utf-8") as f:
         json.dump(export_data, f, indent=2)
generator/dashd_manager.py (1)

155-159: atexit.register(self.stop) can accumulate handlers.

If start() were called more than once (e.g., after a stop()), each call registers another atexit handler, leading to redundant cleanup attempts. Not likely in current usage but worth guarding against.

Proposed fix
+        # Unregister any previous cleanup handler before registering a new one
+        atexit.unregister(self.stop)
         atexit.register(self.stop)
export_wallets.py (1)

20-30: find_free_port duplicates logic from DashdManager without SO_REUSEADDR.

DashdManager.is_port_available (dashd_manager.py Line 58) sets SO_REUSEADDR before binding, which avoids false negatives on recently-closed sockets. This local version doesn't. Consider either reusing DashdManager's method or adding the socket option.

Proposed fix
 def find_free_port(start=19998):
     import socket
 
     for port in range(start, start + 20):
         try:
             with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+                s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                 s.bind(("127.0.0.1", port))
                 return port
         except OSError:
             continue
     raise RuntimeError("No free port found")
generate.py (4)

43-63: Config default probabilities don't match any named density preset.

The dataclass defaults (tx_probability_none=0.50, tx_probability_low=0.70) don't correspond to any of the named density configs in main() (minimal, light, normal, heavy). While main() always overrides these, direct Config() construction (e.g., in tests) silently gets a density profile that doesn't match any labeled preset. Consider aligning defaults with "normal" (0.30/0.70/0.90) for consistency.

Proposed fix
     # Transaction generation parameters (used by RandomGenerator)
-    tx_probability_none: float = 0.50  # Probability of 0 transactions per block
+    tx_probability_none: float = 0.30  # Probability of 0 transactions per block
     tx_probability_low: float = 0.70  # Cumulative probability for 1-3 transactions

196-206: Consider using abc.ABC and @abstractmethod for the base Generator.

The base Generator uses raise NotImplementedError to enforce subclassing. Using abc.ABC and @abstractmethod would provide a clearer contract and catch missing implementations at instantiation time rather than at call time. strategy_name() (Line 132) also returns "base" rather than being abstract.


267-293: _export_data bypasses DashdManager.stop() to manually manage the process.

Lines 271-278 directly call self.dashd_manager.process.terminate() and set process = None, duplicating stop logic and bypassing the atexit-registered handler. If DashdManager.stop() gains additional cleanup (e.g., flushing, logging), this code path would miss it. Consider adding a stop_process_only() method to DashdManager that stops the process without cleaning up the temp dir, keeping lifecycle management in one place.


1152-1296: _phase_bulk_generation is complex but well-structured.

This ~150-line method handles multiple interleaved concerns (boundary transactions, coinbase mining ranges, periodic sends, progress logging). While it's long, the inline comments are clear and the logic is sequential. If this grows further, consider extracting the mature/immature coinbase ranges into helper methods.

tests/test_generate.py (2)

259-278: Test duplicates the weighting logic rather than exercising the actual method.

test_heavy_receives_more_transactions re-implements the random.choices weighting logic from RandomGenerator._create_transaction (Lines 787-794 in generate.py) instead of calling the method itself. This validates the concept but won't catch regressions if the weight values change in the production code.

Since _create_transaction requires RPC calls, this is a reasonable pragmatic choice for a unit test. Consider adding a comment to that effect.


420-423: Redundant import pytest.

pytest is already imported at Line 7.

Proposed fix
 if __name__ == "__main__":
-    import pytest
-
     pytest.main([__file__, "-v"])

Comment thread export_wallets.py
Comment thread export_wallets.py
Comment thread generate.py
Comment thread generator/wallet_export.py
@xdustinface xdustinface changed the title feat: add pluggable strategy pattern, wallet export, and tests feat: improve generation script, add wallet export, add tests Feb 12, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@contrib/setup-dashd.py`:
- Line 9: Update the module docstring to reflect the correct default DASHVERSION
(change "22.0.0" to "23.0.2") so it matches the actual default defined later;
edit the top-level docstring text referencing DASHVERSION in
contrib/setup-dashd.py to show the correct version string used by the default on
line where DASHVERSION is set.
- Around line 37-38: The Linux branch always returns the x86_64 asset; update
the logic that builds the filename to inspect platform.machine() (same approach
used for Darwin) and return "dashcore-{DASHVERSION}-aarch64-linux-gnu.tar.gz"
when machine() reports "aarch64" or "arm64", otherwise return the existing
x86_64 name; modify the conditional around system == "Linux" in
contrib/setup-dashd.py to handle ARM64 vs x86_64 using platform.machine() and
the DASHVERSION symbol.

In `@generator/rpc_client.py`:
- Around line 47-50: Remove the dead except ConnectionRefusedError branch in the
retry loop that calls subprocess.run (the block catching ConnectionRefusedError
and sleeping/raising DashdConnectionError), because subprocess.run invoking
dash-cli will not raise ConnectionRefusedError; instead errors are handled by
_handle_error which raises DashdConnectionError. Either delete that except
clause entirely from the function containing the subprocess.run call, or (if you
want retry-on-dashd-failure) change the retry logic to catch
DashdConnectionError raised by _handle_error and implement the backoff there;
locate references to subprocess.run, the ConnectionRefusedError except block,
_handle_error, and DashdConnectionError to make the change.
🧹 Nitpick comments (9)
generate.py (2)

677-786: Bulk generation loop manually tracks current_height — subtle drift risk.

current_height is incremented manually after each _mine_blocks call rather than re-queried via getblockcount. While generatetoaddress in regtest is deterministic, any unexpected behavior (e.g., _send_to_wallet_mine_blocks(1) at Line 739-740 adding an extra block) could cause the local counter to drift from the actual chain height.

The overshoot detection at Lines 778-784 mitigates this for the final height, but mid-loop drift could cause boundary transactions to land at slightly wrong heights.

This is acceptable for test data generation but worth being aware of.


108-121: KeyboardInterrupt re-raised as GeneratorError — caught again by the outer handler.

In generate(), KeyboardInterrupt is caught and re-raised as GeneratorError("User interrupted"). In main() (Line 880), the GeneratorError handler catches this and exits with code 4. However, Line 883-884 also has a separate KeyboardInterrupt handler. If the interrupt happens outside generate() (e.g., during config setup), it'll be caught by the outer handler. This is fine, but the from None on Line 110 suppresses the original traceback which may hinder debugging.

tests/test_generate.py (1)

166-169: Duplicate import pytest at module-level entry point.

pytest is already imported at Line 6. The re-import on Line 167 is harmless but redundant.

Proposed fix
 if __name__ == "__main__":
-    import pytest
-
     pytest.main([__file__, "-v"])
tests/test_integration.py (1)

99-107: Module-scoped fixture is efficient but consider a longer timeout for CI.

The generated_data fixture runs --blocks 200 with the default 120s timeout. On slower CI runners this could be tight. Consider bumping the timeout in run_generate for this call.

Proposed fix
-    result = run_generate("--blocks", "200", "--output-dir", str(output_dir))
+    result = run_generate("--blocks", "200", "--output-dir", str(output_dir), timeout=300)
export_wallets.py (2)

20-30: find_free_port duplicates DashdManager.find_free_port without SO_REUSEADDR.

This standalone version omits the SO_REUSEADDR socket option that DashdManager.is_port_available (dashd_manager.py Line 58) sets. This could cause false negatives on recently-freed ports. Consider extracting the port-finding logic into a shared utility or reusing DashdManager.


76-89: Empty-string sentinel for mainnet network flag is fragile.

Building the command with an empty string and then filtering it out works, but is easy to break if someone adds arguments between. A cleaner approach:

Proposed fix
     cmd = [
         dashd_executable,
-        f"-{args.network}" if args.network != "mainnet" else "",
         f"-datadir={datadir}",
         f"-port={p2p_port}",
         f"-rpcport={rpc_port}",
         "-server=1",
         "-daemon=0",
         "-rpcbind=127.0.0.1",
         "-rpcallowip=127.0.0.1",
         "-listen=0",
     ]
-    # Remove empty strings from cmd (mainnet needs no network flag)
-    cmd = [c for c in cmd if c]
+    if args.network != "mainnet":
+        cmd.insert(1, f"-{args.network}")
contrib/setup-dashd.py (1)

57-63: Ruff S202 flags only tarfile.extractall(), not zipfile.extractall(); both still need path validation for defense-in-depth.

The code extracts archives from GitHub releases (trusted source, low risk), but to mitigate path-traversal vulnerabilities: use tarfile.extractall(dest_dir, filter="data") on Python 3.12+ for tar archives, and validate zipfile.ZipFile.namelist() members before extraction for zip archives.

generator/rpc_client.py (2)

36-50: call() silently returns None when max_retries <= 0.

If max_retries is set to 0 (or negative), range(self.max_retries) produces an empty sequence, the loop body never executes, and call() returns None implicitly. A guard at the top would make the contract explicit.

Suggested guard
     def call(self, method: str, *params, wallet: str | None = None) -> Any:
         """
         Call RPC method with retry logic and comprehensive error handling.
         """
+        if self.max_retries <= 0:
+            raise ValueError("max_retries must be >= 1")
         for attempt in range(self.max_retries):

59-60: if self.rpc_port: skips port 0 — use is not None for correctness.

Falsy check means rpc_port=0 would be silently ignored. While port 0 is unlikely for RPC, the intent is clearly "was a port explicitly provided," which is not None expresses accurately.

-        if self.rpc_port:
+        if self.rpc_port is not None:

Comment thread contrib/setup-dashd.py Outdated
Comment thread contrib/setup-dashd.py Outdated
Comment thread generator/rpc_client.py Outdated
- Refactor `generate.py` into a base `Generator` class with one strategy: `WalletSyncGenerator` (SPV
wallet sync edge cases).
- Extract shared wallet stats collection into `generator/wallet_export.py`.
- Add `export_wallets.py` for exporting stats from existing blockchain data.
- Improve dashd_manager extra_args support and rpc_client error handling.
- Add test suite with unit and integration tests to make sure the script is working.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@contrib/setup-dashd.py`:
- Around line 58-64: The extract function currently calls
tarfile.open(...).extractall(...) which is vulnerable to path traversal;
implement a safe extraction path check and use the
tarfile.extractall(filter=...) parameter when supported. Inside extract, create
a predicate (e.g., is_safe_member(member, dest_dir)) that normalizes and joins
member.name with dest_dir and rejects absolute paths or any path that escapes
dest_dir (contains '..' after normpath or startswith outside dest_dir); then
call tf.extractall(dest_dir, filter=is_safe_member) inside a try block and fall
back in except TypeError by iterating tf.getmembers(), applying the same
is_safe_member check, and extracting only those members with tf.extract(member,
dest_dir). Reference the extract function, tarfile.open, tf.extractall, and
tf.extract/getmembers in your changes.
🧹 Nitpick comments (5)
export_wallets.py (1)

114-124: Bare except Exception is intentional here but worth a brief comment.

The polling loop catches all exceptions during startup because the RPC client can raise various error types while dashd is initializing. A brief inline comment would clarify intent for future readers.

generate.py (1)

677-786: Manual current_height tracking may drift from actual chain height.

Throughout _phase_bulk_generation, current_height is maintained manually by adding block counts after each _mine_blocks call. If any code path (e.g., the faucet self-send at Line 758-765 or the periodic sends at Line 744-755) fails to properly account for mined blocks, the local counter will drift from the actual chain height. The final reconciliation at Lines 777-784 mitigates this for the end result, but intermediate boundary/coinbase logic could misfire.

This isn't a blocking issue given the reconciliation, but consider periodically syncing current_height with self.rpc.call("getblockcount") (e.g., every N iterations) for robustness.

tests/test_integration.py (1)

21-27: Prefer pytest.skip() over assert for missing environment prerequisites.

When DASHD_PATH is not set, assert raises AssertionError, which pytest reports as a test failure rather than a skip. Using pytest.skip() gives a clearer signal in CI and local runs that the test was intentionally skipped due to a missing prerequisite.

Suggested change
 def get_dashd_path():
     path = os.environ.get("DASHD_PATH", "")
-    assert path, "DASHD_PATH environment variable is not set (run contrib/setup-dashd.py first)"
+    if not path:
+        pytest.skip("DASHD_PATH environment variable is not set (run contrib/setup-dashd.py first)")
     dashd = Path(path)
-    assert dashd.is_file(), f"dashd binary not found at {path}"
-    assert os.access(path, os.X_OK), f"dashd binary not executable at {path}"
+    if not dashd.is_file():
+        pytest.skip(f"dashd binary not found at {path}")
+    if not os.access(path, os.X_OK):
+        pytest.skip(f"dashd binary not executable at {path}")
     return path
tests/test_generate.py (2)

148-163: Class name is misleading — these tests check initial state and method existence, not address indices.

Consider renaming to something like TestWalletSyncInitialization to better reflect what's being tested.


166-169: Duplicate import pytest — already imported at line 6.

Proposed fix
 if __name__ == "__main__":
-    import pytest
-
     pytest.main([__file__, "-v"])

Comment thread contrib/setup-dashd.py
@xdustinface xdustinface merged commit e1db500 into main Feb 12, 2026
1 check passed
@xdustinface xdustinface deleted the feat/strategy branch February 12, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant