degenbot.runner.build_paths =========================== .. py:module:: degenbot.runner.build_paths .. autoapi-nested-parse:: Path discovery + registration for the settlement-arbitrage ``BotRunner``. Extracted from ``examples/eth_backrun_v2_v3_v4_rust.py``. Owns ``build_paths`` and its registration machinery: :class:`ConstructionContext` (registration-owned construction resources kept out of the main-loop trim), :class:`PathRegistrationPipeline` (the reusable, pump-concurrent per-path registration / verify / dedup), and the bounded producer/consumer helper that drives it. The driver is Python-companion orchestration (``stays-python``): it registers paths with the Rust-owned engine (``EngineRegistry``) but owns no pool state. Module Contents --------------- .. py:data:: REG_INTAKE_WINDOW :value: 32 .. py:class:: RegistrationUnitOutcome The per-path unit outcome, reported back to the driver. The units run on fleet seats (plain threads, possibly concurrent), so they NEVER touch the pipeline counters — they return one of these and the single-loop driver folds it into the summary counters exactly as the retired inline ``_consume`` did (counter parity is the PRG-3/5 bar). .. py:attribute:: kind :type: str .. py:attribute:: tag :type: str | None :value: None .. py:attribute:: created :type: bool :value: False .. py:attribute:: v4_hops :type: int :value: 0 .. py:attribute:: counts_as_skip :type: bool :value: True .. py:attribute:: detail :type: str | None :value: None .. py:function:: resolve_directions(pools: list[degenbot.UniswapV2Pool | degenbot.UniswapV3Pool | degenbot.UniswapV4Pool], input_token_address: str) -> list[bool] Determine zero_for_one for each hop so the cycle closes. The cycle: input_token → hop_0 → intermediate → hop_1 → ... → input_token. Returns a list of zfo values (one per hop). The mechanic lives in the core (`degenbot_pathfinding::directions::resolve_directions`); this adapter flattens the constructed pools into the typed seam's hop tuples and re-raises the core's refusal as :class:`DirectionResolutionError`. Resolution is kind-blind. V4 pools use NATIVE_CURRENCY_ADDRESS (address(0)) for ETH, which the core treats as equivalent to WETH — the profit token is always WETH. The core's refusal surfaces as :class:`DirectionResolutionError` — a hop carries neither tracked token, or the cycle does not close: an invariant violation (wrong pool built, stale subgraph, or a builder bug), never a skip. :returns: One zero-for-one value per hop, in hop order. .. py:class:: ConstructionContext Registration-owned construction resources, kept out of run()'s trim. Bundles everything ``build_paths`` needs to construct and register pools, so the registration task owns them as a single self-contained context for its lifetime. ``BotRunner.run()`` trims *main-loop* state (``release_python_state()`` + ``self.bot = None``); the context is a *separate* identity that a background registration task holds and that the trim never severs — the decoupling seam for Sub-B (background registration on the pump runtime). The context holds RESOLVED POLICY VALUES only: the construction route (CONTEXT.md, Construction route — the ordered factory rungs + the generic builder rung) and the WETH token, built once here. The core route entry (``pool_builder::route``) owns the walk — route order, the DB two-step identity, get-or-register into session state — and classifies every failure on the build-refusal taxonomy, so no tracker or snapshot object lives here (the retired three-tracker fallback chain was the bare except-and-continue bug this replaces). .. py:attribute:: bot :type: degenbot.Bot .. py:attribute:: chain_id :type: int .. py:attribute:: database_path :type: pathlib.Path .. py:attribute:: construction_route :type: degenbot.builders.request.ConstructionRoute .. py:attribute:: weth :type: Any .. py:method:: for_bot(bot: degenbot.Bot) -> ConstructionContext :classmethod: Build the construction context for a bot. Resolves the route policy (the mainnet V3 fork factories in policy order, generic builder rung armed) and builds WETH once. The core route entry owns everything else about construction. :returns: The construction context. .. py:class:: PathRegistrationPipeline(*, context: ConstructionContext, engine_registry: degenbot.arbitrage.engine_registry.EngineRegistry, retry_policy: degenbot.arbitrage.RetryPolicy | None = None, max_paths: int, discovery_batch_size: int, progress_interval_secs: float | None = None) Reusable, pump-concurrent registration pipeline (D1c). Owns the per-path registration work that ``build_paths`` previously ran inline: construction (through the retained ``ConstructionContext`` — the Rust ``PoolBuilder``), engine registration + verification, direction resolution, registered-path dedup, per-path release, and the summary counters. It is LONG-LIVED by design: it keeps the ``ConstructionContext`` AND the ``engine_registry`` for the session's lifetime, so an operator can add a specific path (``enqueue_path``) or trigger a bounded on-demand discovery (``trigger_discovery``) at ANY time — including after ``run()`` trims the main-loop bot. The context survives the trim (Sub-A seam), so these methods never need the dropped Python ``bot``. The pipeline never awaits the pump, so adds/discovery cannot block update/solve/dispatch. ``max_paths`` is the registered-path cap (``0`` = uncapped) and is REQUIRED, because it is a configuration value: the caller that resolved it (:attr:`~degenbot.runner.config.ArbitrageConfig.max_registered_paths` in production) states it, and a default here would be a second authority that no config layer can reach. Registration stops accepting new paths once the engine path registry reaches the cap; the engine then reaches steady state with a bounded path universe, so solve performance is observable without ongoing registration load. The pipeline announces the cap it was given at startup, since the code default and the running environment may differ. The fail-fast tripwire is preserved: a fatal ``VerificationMismatchError`` / ``VerificationRpcError`` is NOT swallowed here — it propagates out of the worker and must abort the pipeline loudly. .. py:attribute:: constr_ctx .. py:attribute:: constr_bot .. py:attribute:: constr_chain_id .. py:attribute:: constr_database_path .. py:attribute:: construction_route .. py:attribute:: weth .. py:attribute:: engine_registry .. py:attribute:: retry_policy_obj .. py:attribute:: discovery_batch_size .. py:attribute:: pool_types :type: list[degenbot.pathfinding.PoolKind] :value: [] .. py:attribute:: pool_type_per_depth :type: list[set[degenbot.pathfinding.PoolKind] | None] | None :value: None .. py:attribute:: path_count :value: 0 .. py:attribute:: cap_skip_count :value: 0 .. py:attribute:: skip_count :value: 0 .. py:attribute:: token_filter_count :value: 0 .. py:attribute:: engine_reject_count :value: 0 .. py:attribute:: dup_count :value: 0 .. py:attribute:: register_fail_count :value: 0 .. py:attribute:: v4_pool_count :value: 0 .. py:attribute:: v4_hook_rejected :value: 0 .. py:attribute:: v4_dynamic_fee_rejected :value: 0 .. py:attribute:: other_exc_count :value: 0 .. py:attribute:: capped :value: False .. py:method:: emit_registration_progress(*, force: bool = False) -> None Log the registration counters + top skip-reason breakdown. The legacy ``[build_paths] Progress`` line only fires when ``path_count`` reaches a multiple of 1000. During a discovery-heavy crawl that registers few paths it never fires, so the skip/dup/reject counts (and their reasons) stay invisible. This is the same summary emitted on ``force`` (a wall-clock cadence) so the cause is always observable mid-crawl. .. py:method:: run_registration(*, producer: collections.abc.AsyncIterable[object]) -> None :async: Run the crawl: submit each discovered path as ONE fleet unit. PRG-5: the bounded producer/consumer queue retired with the crawl shell — discovery iterates directly and every path leaves as a single ``PoolStateUpdater`` intake unit (build + verify lifecycles + path registration inside the Rust core). The concurrency is the fleet's (duty-counted seats + its own bounded queue), and the driver-side backpressure is the submission window: at most :data:`REG_INTAKE_WINDOW` receipts are outstanding, so discovery can never outrun registration by more than the window. Units are resolved in FIFO submission order (the retired workers' ordering guarantee), so the Progress summary's counter drift and the 1000-boundary log lines keep their retired shapes exactly. Returns with EVERY submitted receipt resolved (the completion clause that replaced the retired executor-drain: all cloned ``Arc`` handles acquired inside units are dropped before ``build_paths`` returns, keeping the close_snapshot_tx() Arc::try_unwrap canary quiet). The unit's fatal exception (VerificationMismatchError / VerificationRpcError / DirectionResolutionError) propagates through the receipt and aborts the crawl loudly — the "shut down" contract, unchanged. On a fatal the crawl stops submitting immediately (outstanding units still finish — fleet units are never cancelled, the Deferrable cordon class). .. py:method:: enqueue_path(path_steps: Any, directions: list[bool] | None = None) -> None :async: Add ONE specific path at any time (D1c operator surface). .. py:method:: trigger_discovery(*, bound: int | None = None) -> int :async: Trigger a bounded one-shot discovery sweep (D1c). A sweep that runs to NATURAL completion (not bound-truncated, not capped) latches the structural graph edition; a later trigger over the same edition stops immediately and returns 0 — the unchanged structure can only re-yield paths the pipeline already processed. The latch re-arms itself when the edition changes (pool added or removed), when the probe is unavailable, and after any truncated sweep. :returns: The number of paths processed by the sweep. .. py:method:: discovery_sweep(*, find_paths_async: collections.abc.Callable[..., collections.abc.AsyncGenerator[object, None]] = find_paths_async) -> collections.abc.AsyncGenerator[object, None] Run a single discovery sweep over the DB subgraph (V2/V3/V4 DFS). ``find_paths_async`` is the discovery producer seam (tests inject a recording producer to observe the forwarded batch size); the default is the production adapter. :returns: The async generator of discovered candidate paths. .. py:class:: BuildPathsOptions The knobs :func:`build_paths` takes, one options object. Bundling the construction/registration inputs keeps the ``build_paths`` call site to the two required resources plus one options object; each field mirrors the former keyword parameter. ``max_registered_paths`` and ``discovery_batch_size`` are the non-defaulted fields, and they are required for the same reason the pipeline's ``max_paths`` and ``discovery_batch_size`` are: both are configuration values, so the caller states what it resolved and no code path invents one. A caller that supplies ``pipeline`` already carries them on the pipeline it passes. The snapshot fields are retired: per-pool tick data resolves core-side at construction (the DB arm of the Rust builder, or the Chain arm), so a driver-side snapshot object has no consumer in the construction route. .. py:attribute:: max_registered_paths :type: int .. py:attribute:: discovery_batch_size :type: int .. py:attribute:: retry_policy :type: degenbot.arbitrage.RetryPolicy | None :value: None .. py:attribute:: context :type: ConstructionContext | None :value: None .. py:attribute:: pipeline :type: PathRegistrationPipeline | None :value: None .. py:attribute:: permutation_filter :type: frozenset[str] | None :value: None .. py:function:: build_paths(*, bot: degenbot.Bot, engine_registry: degenbot.arbitrage.engine_registry.EngineRegistry, options: BuildPathsOptions) -> None :async: Discover V2/V3/V4 arb paths, build Python pools, register with Rust engine. V4 pools are discovered via find_paths_async and built through ``bot.build_managed_pool()``. V4 pool admission (amount-modifying hooks / dynamic fees) is enforced by the Rust core at registration time, surfacing as typed HookedPoolRejectedError / DynamicFeePoolRejectedError. Each per-pool verify lifecycle runs through the core-owned bounded retry dance with the resolved policy injected (transient ``VerificationRpcError`` is retried; ``VerificationMismatchError`` is never retried and crashes loudly). Discovery is a single pass over the DB subgraph driven through a reusable :class:`PathRegistrationPipeline`; after it completes the orphan sweep releases Tracked pools whose path was skipped before ``register_vN_pool``. ``options`` is required because it carries the registered-path cap: the caller that resolved the cap states it, and this function never invents one for a pipeline it builds itself.