Skip to content

Commit ccf0e49

Browse files
fix(shield-swap): await the async record scan; both Copilot findings
AsyncShieldSwap.get_owned_positions did not await record_provider.find. AsyncRecordsModule.find is `async def`, so `records` was a coroutine and iterating it raised "TypeError: 'coroutine' object is not iterable" — the method could never have worked. Reproduced before fixing. My own parity test missed it because it asserted hasattr, not behaviour: a method that exists and always raises satisfies a presence check. So this adds test_owned_positions_async, which drives the async views against a fake whose find() really is a coroutine — the join, the finalize lag, record filtering, the pool filter, lookup by id, and the empty case. Verified as a real guard by removing the await again: all six fail, and pass once it is restored. Also: test_airdrop_stage_refuses_on_mainnet had its docstring after an assignment, making it a no-op string expression rather than a docstring (ast.get_docstring returned None). Moved to the first statement. Both found by Copilot review; both were real. 235 shield-swap (+6), pyright clean, AGENTS.md current.
1 parent 7a60a52 commit ccf0e49

3 files changed

Lines changed: 125 additions & 2 deletions

File tree

‎shield-swap-sdk/python/aleo_shield_swap/async_client.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ async def get_owned_positions(self, *, pool_key: Optional[str] = None,
371371
``state is None`` semantics while a mint finalizes.
372372
"""
373373
acct = self._account(account)
374-
records = self._aleo.record_provider.find(
374+
records = await self._aleo.record_provider.find(
375375
acct, program=self.program, unspent=True)
376376
out: list[OwnedPosition] = []
377377
slots: dict[str, Any] = {} # one slot read per pool, not per position

‎shield-swap-sdk/tests/test_lifecycle.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ def _refresh_credentials(self):
174174

175175

176176
def test_airdrop_stage_refuses_on_mainnet(tmp_path):
177-
tmp_journal = tmp_path / "j.jsonl"
178177
"""The faucet endpoints are testnet-only; on mainnet say so, don't 404."""
178+
tmp_journal = tmp_path / "j.jsonl"
179179
from aleo_shield_swap.errors import NotFundedError
180180
from aleo_shield_swap.lifecycle import _Ctx, _airdrop_run
181181

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""AsyncShieldSwap owned-position views, exercised rather than introspected.
2+
3+
The parity test asserts these methods *exist*; this one asserts they *run*.
4+
That distinction matters: the first version of the async view failed to await
5+
record_provider.find (which is `async def` on the async facade), so it returned
6+
a coroutine and raised TypeError on iteration — invisible to a hasattr check.
7+
"""
8+
from __future__ import annotations
9+
10+
import pytest
11+
12+
from aleo_shield_swap.async_client import AsyncShieldSwap
13+
14+
from .test_owned_positions import (
15+
POSITION_ENTRY,
16+
POSITION_RECORD,
17+
TOKEN_RECORD,
18+
_tick,
19+
)
20+
from .conftest import POOL_TEXT, SLOT_TEXT
21+
22+
pytestmark = pytest.mark.asyncio
23+
24+
25+
class _AsyncProvider:
26+
"""Mirrors the async facade: find() is a coroutine."""
27+
28+
def __init__(self, records):
29+
self._records = records
30+
self.calls = 0
31+
32+
async def find(self, account=None, *, program=None, unspent=True, **_):
33+
self.calls += 1
34+
return list(self._records)
35+
36+
37+
class _AsyncMapping:
38+
def __init__(self, values):
39+
self._values = values
40+
41+
async def get(self, key):
42+
return self._values.get(str(key))
43+
44+
45+
class _AsyncProgram:
46+
def __init__(self, mappings):
47+
self._mappings = mappings
48+
49+
def mapping(self, name):
50+
return _AsyncMapping(self._mappings.get(name, {}))
51+
52+
53+
class _AsyncPrograms:
54+
def __init__(self, mappings):
55+
self._mappings = mappings
56+
57+
async def get(self, program_id, edition=None):
58+
return _AsyncProgram(self._mappings)
59+
60+
61+
class _AsyncFacade:
62+
network_name = "testnet"
63+
64+
def __init__(self, mappings, records):
65+
self.programs = _AsyncPrograms(mappings)
66+
self.record_provider = _AsyncProvider(records)
67+
self.default_account = object()
68+
69+
70+
def _dex(*, positions=None, records=None):
71+
mappings = {
72+
"pools": {"5field": POOL_TEXT},
73+
"slots": {"5field": SLOT_TEXT},
74+
"positions": positions if positions is not None else {"42field": POSITION_ENTRY},
75+
}
76+
facade = _AsyncFacade(mappings, records if records is not None
77+
else [{"record_plaintext": POSITION_RECORD}])
78+
dex = AsyncShieldSwap(facade)
79+
mappings["ticks"] = {
80+
dex.derive_tick_key("5field", -4080): _tick("5field", -4080),
81+
dex.derive_tick_key("5field", 4080): _tick("5field", 4080),
82+
}
83+
return dex
84+
85+
86+
async def test_async_join_matches_the_sync_shape():
87+
owned = await _dex().get_owned_positions()
88+
assert len(owned) == 1
89+
p = owned[0]
90+
assert p.position_token_id == "42field"
91+
assert (p.pool_key, p.tick_lower, p.tick_upper) == ("5field", -4080, 4080)
92+
assert p.withdrawal == "aleo1payout"
93+
assert p.state is not None and p.state.liquidity == 500
94+
assert (p.state.tokens_owed0, p.state.tokens_owed1) == (11, 22)
95+
96+
97+
async def test_async_state_none_while_finalizing():
98+
owned = await _dex(positions={}).get_owned_positions()
99+
assert owned[0].state is None
100+
101+
102+
async def test_async_skips_non_position_records():
103+
dex = _dex(records=[{"record_plaintext": TOKEN_RECORD},
104+
{"record_plaintext": POSITION_RECORD}])
105+
owned = await dex.get_owned_positions()
106+
assert [p.position_token_id for p in owned] == ["42field"]
107+
108+
109+
async def test_async_pool_filter():
110+
dex = _dex()
111+
assert len(await dex.get_owned_positions(pool_key="5field")) == 1
112+
assert await dex.get_owned_positions(pool_key="9field") == []
113+
114+
115+
async def test_async_get_owned_position_by_id():
116+
dex = _dex()
117+
got = await dex.get_owned_position("42field")
118+
assert got is not None and got.position_token_id == "42field"
119+
assert await dex.get_owned_position("999field") is None
120+
121+
122+
async def test_async_no_records_is_empty():
123+
assert await _dex(records=[]).get_owned_positions() == []

0 commit comments

Comments
 (0)