Repository navigation
design: evaluate sync/async maintenance strategy during D2 #492
Description
Background
PR #491 adds async clients (asyncuniswap.py, asyncuniswap4.py) alongside the existing sync clients. Handwritten parallel implementations can require fixes and protocol changes in two places. Its review raised nonce-handling and ETH-route concerns; those correctness/testing questions are distinct from whether generation is the best maintenance approach.
@ErikBjare raised the maintenance concern during grant planning and requested this issue to evaluate an async source of truth with generated synchronous clients. @liquid-8 points to web3.py's separate Web3/AsyncWeb3 classes and considers the current approach reasonable, with discussion welcome during D2.
Goal
Evaluate whether single-source generation would improve maintenance without adding disproportionate complexity or breaking either public API. Keep sync and async clients distinct; this is not a proposal to combine them into one runtime class.
Retaining separate implementations is a valid outcome. No framework or rewrite has been selected.
Options to evaluate during D2
- Current separate clients, shared pure helpers, and matched behavioral tests — retain provider-specific I/O and reduce duplication where it is straightforward.
unasyncor comparable async-to-sync generation — generate a separate synchronous implementation from canonical async code. Evaluate the required provider/type/decorator/locking mappings and reproducibility before adopting it.- Thin sync wrapper around async code — evaluate event-loop ownership, provider/session lifetimes, and compatibility with callers already running an event loop. Per-call
asyncio.run()is not a drop-in solution.
Using AnyIO or Trio alone does not generate a synchronous API; an explicit bridge or generation strategy would still be required.
Decision / acceptance criteria
- Discuss during D2 and record whether to retain the current design or pursue a bounded prototype, with rationale.
- If a prototype is agreed, verify compatibility and run the existing sync and async tests; do not infer correctness from signature parity alone.
- Define appropriate parity coverage for the chosen design: signatures where applicable, plus matched behavioral tests for equivalent routes/calldata/value, approval, and nonce behavior, with intentional differences documented.
- Document the chosen maintenance approach and, if generation is adopted, the reproducibility check and contributing workflow.
A per-instance nonce lock does not coordinate different client instances using the same wallet; that limitation and the documented native multihop testing gap remain separate correctness/testing concerns.
Relationship to #491 / UF grant
PR #491 is merged and ships the handwritten sync and async implementations for D1. This issue tracks a D2 architecture discussion, not a D1 merge blocker. The architectural refactor is not an agreed production-release prerequisite; any resulting implementation work requires an explicit scope decision.
Activity
liquid-8 commentedon Oct 14, 2026
Let's take a look at
web3.py. Openhttps://github.com/ApeWorX/web3.py/blob/main/web3/main.py
What's there? Two classes:
Web3andAsyncWeb3. Historically, devs tried to keep it together, but dismissed that idea before 0.7, if I recall correctly. The reason is simple: it is easier to write ~ the same code twice than to combine both async and sync functionality in one class. Besides simplicity, there is also a quite logical reason: you shouldn't mix sync and async code without necessity (unless you know what you're doing very well).The aforementioned issues, like nonce handling and ETH route logic, are non-blocking. The former has always been a potential problem, just a non-obvious one. All you have to do to see that is to send transactions from the same wallet using different wrapper instances; here we go. Providing custom nonces lets users manage that. Regarding the ETH route logic, v2/v3 native multihops have been added in this delivery for a reason, but haven't been tested, which is stated clearly in the release notes.
TL; DR: I think we should not be very concerned about this. However, I don't mind discussing this matter during D2.
TimeToBuildBob commentedon Oct 14, 2026
Agreed on keeping this a D2 discussion rather than blocking D1. I checked the linked
web3/main.py: it has separateWeb3/AsyncWeb3classes and provider-specific I/O, with common utilities inBaseWeb3. That is a useful precedent for keeping the public sync and async APIs separate. My concern is duplicated swap/transaction logic, not the existence of two classes; generation would still produce two separate classes, without running async code inside the sync client.The issue is too prescriptive as written. Erik asked us to evaluate single-source maintenance, not to commit to a rewrite, and my “before v4 is production-ready” sentence overstates that request. Keeping the current implementations is a valid outcome if generation adds more complexity than it removes. I would not choose per-call
asyncio.run()wrappers as the default: event-loop ownership and provider/session lifetimes need explicit handling.For D2, the useful comparison is the current separate clients with shared pure helpers and matched behavioral tests versus a bounded generation prototype. Signatures alone cannot establish parity; equivalent route inputs should produce equivalent calldata/value, and tests should cover approval and nonce behavior while documenting intentional differences. I also agree that a per-instance nonce lock does not coordinate multiple clients using the same wallet, and that the documented native multihop testing gap is separate from this architecture decision. No refactor started here; the next step is agreement during D2 on whether a prototype is warranted and which compatibility requirements it must preserve.
[-]refactor: single async source of truth with generated/wrapped sync client (eliminate dual-implementation drift)[/-][+]design: evaluate sync/async maintenance strategy during D2[/+]