arc-devkit 0.4.4__tar.gz → 0.4.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/PKG-INFO +1 -1
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/__init__.py +2 -1
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/agents/async_monitor.py +15 -13
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/agents/monitor_agent.py +11 -5
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/agents/payment_agent.py +23 -15
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/analytics/portfolio.py +7 -9
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/api/routes/agents.py +1 -1
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/cli/flat.py +8 -10
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/copilot/agent.py +5 -2
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/debugger/tx_analyzer.py +11 -6
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/deploy/deployer.py +2 -3
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/events/listener.py +1 -3
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit.egg-info/PKG-INFO +1 -1
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/pyproject.toml +1 -1
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_agents.py +1 -4
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_analytics.py +12 -11
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_api.py +4 -4
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_async_agents.py +21 -15
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_cli.py +29 -12
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_contracts.py +8 -10
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_debugger.py +3 -8
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_deploy.py +3 -2
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_events.py +29 -7
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_usdc.py +4 -3
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/README.md +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/agents/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/agents/async_base.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/agents/base_agent.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/analytics/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/api/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/api/main.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/api/routes/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/api/routes/copilot.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/api/routes/debugger.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/cli/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/cli/commands/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/cli/commands/agent.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/cli/commands/copilot.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/cli/commands/debug.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/cli/main.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/config.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/contracts/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/contracts/loader.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/copilot/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/core/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/core/connection.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/core/gas.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/core/wallet.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/debugger/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/deploy/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/events/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/usdc/__init__.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit/usdc/token.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit.egg-info/SOURCES.txt +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit.egg-info/dependency_links.txt +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit.egg-info/entry_points.txt +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit.egg-info/requires.txt +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/arc_devkit.egg-info/top_level.txt +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/setup.cfg +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_connection.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_copilot.py +0 -0
- {arc_devkit-0.4.4 → arc_devkit-0.4.6}/tests/test_integration.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Arc DevKit — Developer tools for the Arc blockchain by Circle."""
|
|
2
2
|
|
|
3
|
-
from importlib.metadata import PackageNotFoundError
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _version
|
|
4
5
|
|
|
5
6
|
try:
|
|
6
7
|
__version__ = _version("arc-devkit")
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
|
-
from collections.abc import AsyncIterator, Callable
|
|
6
|
+
from collections.abc import AsyncIterator, Callable
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, cast
|
|
9
9
|
|
|
@@ -99,10 +99,12 @@ class AsyncMonitorAgent(AsyncBaseAgent):
|
|
|
99
99
|
if self._state_file:
|
|
100
100
|
try:
|
|
101
101
|
self._state_file.write_text(
|
|
102
|
-
json.dumps(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
json.dumps(
|
|
103
|
+
{
|
|
104
|
+
"balances": {k: str(v) for k, v in self._last_balances.items()},
|
|
105
|
+
"last_erc20_block": self._last_erc20_block,
|
|
106
|
+
}
|
|
107
|
+
)
|
|
106
108
|
)
|
|
107
109
|
except Exception as exc:
|
|
108
110
|
logger.warning("Failed to save state: %s", exc)
|
|
@@ -159,7 +161,11 @@ class AsyncMonitorAgent(AsyncBaseAgent):
|
|
|
159
161
|
|
|
160
162
|
direction = "credit" if recipient in self._watched_lower else "debit"
|
|
161
163
|
address = next(
|
|
162
|
-
(
|
|
164
|
+
(
|
|
165
|
+
a
|
|
166
|
+
for a in self._watched
|
|
167
|
+
if a.lower() == (recipient if direction == "credit" else sender)
|
|
168
|
+
),
|
|
163
169
|
"",
|
|
164
170
|
)
|
|
165
171
|
|
|
@@ -182,9 +188,7 @@ class AsyncMonitorAgent(AsyncBaseAgent):
|
|
|
182
188
|
async def get_balance(self) -> dict: # type: ignore[override]
|
|
183
189
|
resultado = {}
|
|
184
190
|
for addr in self._watched:
|
|
185
|
-
wei = await self._acall_rpc(
|
|
186
|
-
self._w3.eth.get_balance, cast(ChecksumAddress, addr)
|
|
187
|
-
)
|
|
191
|
+
wei = await self._acall_rpc(self._w3.eth.get_balance, cast(ChecksumAddress, addr))
|
|
188
192
|
resultado[addr] = {
|
|
189
193
|
"address": addr,
|
|
190
194
|
"balance_wei": str(wei),
|
|
@@ -215,9 +219,7 @@ class AsyncMonitorAgent(AsyncBaseAgent):
|
|
|
215
219
|
|
|
216
220
|
if self._usdc_contract and self._last_erc20_block == 0:
|
|
217
221
|
try:
|
|
218
|
-
self._last_erc20_block = await self._acall_rpc(
|
|
219
|
-
lambda: self._w3.eth.block_number
|
|
220
|
-
)
|
|
222
|
+
self._last_erc20_block = await self._acall_rpc(lambda: self._w3.eth.block_number)
|
|
221
223
|
except Exception:
|
|
222
224
|
pass
|
|
223
225
|
|
|
@@ -295,7 +297,7 @@ class AsyncMonitorAgent(AsyncBaseAgent):
|
|
|
295
297
|
break
|
|
296
298
|
try:
|
|
297
299
|
event = await asyncio.wait_for(queue.get(), timeout=1.0)
|
|
298
|
-
except
|
|
300
|
+
except TimeoutError:
|
|
299
301
|
continue # re-check task.done() on next iteration
|
|
300
302
|
yield event
|
|
301
303
|
count += 1
|
|
@@ -111,10 +111,12 @@ class MonitorAgent(BaseAgent):
|
|
|
111
111
|
if self._state_file:
|
|
112
112
|
try:
|
|
113
113
|
self._state_file.write_text(
|
|
114
|
-
json.dumps(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
json.dumps(
|
|
115
|
+
{
|
|
116
|
+
"balances": {k: str(v) for k, v in self._last_balances.items()},
|
|
117
|
+
"last_erc20_block": self._last_erc20_block,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
118
120
|
)
|
|
119
121
|
except Exception as exc:
|
|
120
122
|
logger.warning("Failed to save state: %s", exc)
|
|
@@ -170,7 +172,11 @@ class MonitorAgent(BaseAgent):
|
|
|
170
172
|
|
|
171
173
|
direction = "credit" if recipient in self._watched_lower else "debit"
|
|
172
174
|
address = next(
|
|
173
|
-
(
|
|
175
|
+
(
|
|
176
|
+
a
|
|
177
|
+
for a in self._watched
|
|
178
|
+
if a.lower() == (recipient if direction == "credit" else sender)
|
|
179
|
+
),
|
|
174
180
|
"",
|
|
175
181
|
)
|
|
176
182
|
|
|
@@ -77,7 +77,7 @@ class PaymentAgent(BaseAgent):
|
|
|
77
77
|
|
|
78
78
|
def _build_usdc_signed_tx(self, to: str, amount: Decimal) -> tuple:
|
|
79
79
|
"""Build and sign a USDC ERC-20 transfer tx; return (signed, gas_limit)."""
|
|
80
|
-
from arc_devkit.usdc.token import _ERC20_ABI,
|
|
80
|
+
from arc_devkit.usdc.token import _ERC20_ABI, USDC_ARC_TESTNET_ADDRESS, USDC_MULTIPLIER
|
|
81
81
|
|
|
82
82
|
usdc_address = Web3.to_checksum_address(USDC_ARC_TESTNET_ADDRESS)
|
|
83
83
|
contract = self._w3.eth.contract(address=usdc_address, abi=_ERC20_ABI)
|
|
@@ -85,12 +85,15 @@ class PaymentAgent(BaseAgent):
|
|
|
85
85
|
nonce = self._w3.eth.get_transaction_count(cast(ChecksumAddress, self._address))
|
|
86
86
|
|
|
87
87
|
tx = contract.functions.transfer(to, atomic).build_transaction(
|
|
88
|
-
cast(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
cast(
|
|
89
|
+
TxParams,
|
|
90
|
+
{
|
|
91
|
+
"from": self._address,
|
|
92
|
+
"nonce": nonce,
|
|
93
|
+
"gasPrice": self._w3.eth.gas_price,
|
|
94
|
+
"chainId": self._w3.eth.chain_id,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
94
97
|
)
|
|
95
98
|
gas_limit = self._estimate_gas(cast(TxParams, tx))
|
|
96
99
|
tx["gas"] = gas_limit
|
|
@@ -137,15 +140,20 @@ class PaymentAgent(BaseAgent):
|
|
|
137
140
|
else:
|
|
138
141
|
value_wei = self._w3.to_wei(amount_usdc, "ether")
|
|
139
142
|
nonce = self._w3.eth.get_transaction_count(cast(ChecksumAddress, self._address))
|
|
140
|
-
tx_base = cast(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
tx_base = cast(
|
|
144
|
+
TxParams,
|
|
145
|
+
{
|
|
146
|
+
"from": self._address,
|
|
147
|
+
"to": destinatario,
|
|
148
|
+
"value": value_wei,
|
|
149
|
+
"nonce": nonce,
|
|
150
|
+
"chainId": self._w3.eth.chain_id,
|
|
151
|
+
},
|
|
152
|
+
)
|
|
147
153
|
gas_limit = self._estimate_gas(tx_base)
|
|
148
|
-
tx = cast(
|
|
154
|
+
tx = cast(
|
|
155
|
+
TxParams, {**tx_base, "gas": gas_limit, "gasPrice": self._w3.eth.gas_price}
|
|
156
|
+
)
|
|
149
157
|
signed = self._w3.eth.account.sign_transaction(tx, self._private_key)
|
|
150
158
|
self.log("Transaction signed successfully.")
|
|
151
159
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from datetime import
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
7
|
from decimal import Decimal
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any, Literal
|
|
@@ -34,11 +34,11 @@ class PortfolioSnapshot:
|
|
|
34
34
|
"""Point-in-time view of a wallet's portfolio on Arc."""
|
|
35
35
|
|
|
36
36
|
address: str
|
|
37
|
-
native_balance: Decimal
|
|
38
|
-
usdc_balance: Decimal | None
|
|
39
|
-
nonce: int
|
|
37
|
+
native_balance: Decimal # ARC (18 decimals, converted to ether)
|
|
38
|
+
usdc_balance: Decimal | None # USDC (6 decimals); None if contract unavailable
|
|
39
|
+
nonce: int # total txs ever sent from this address
|
|
40
40
|
recent_txs: list[TransactionSummary]
|
|
41
|
-
blocks_scanned: int
|
|
41
|
+
blocks_scanned: int # actual number of blocks inspected
|
|
42
42
|
blocks_from: int
|
|
43
43
|
blocks_to: int
|
|
44
44
|
activity_score: ActivityLevel
|
|
@@ -151,7 +151,7 @@ class PortfolioAnalyzer:
|
|
|
151
151
|
history_file = store / f"{safe_addr}.jsonl"
|
|
152
152
|
|
|
153
153
|
record = self.to_dict(snapshot)
|
|
154
|
-
record["timestamp"] = datetime.now(tz=
|
|
154
|
+
record["timestamp"] = datetime.now(tz=UTC).isoformat()
|
|
155
155
|
|
|
156
156
|
with history_file.open("a", encoding="utf-8") as f:
|
|
157
157
|
f.write(json.dumps(record) + "\n")
|
|
@@ -289,9 +289,7 @@ class PortfolioAnalyzer:
|
|
|
289
289
|
except Exception:
|
|
290
290
|
pass
|
|
291
291
|
|
|
292
|
-
value_arc = Decimal(
|
|
293
|
-
str(self._w3.from_wei(tx.get("value", 0), "ether"))
|
|
294
|
-
)
|
|
292
|
+
value_arc = Decimal(str(self._w3.from_wei(tx.get("value", 0), "ether")))
|
|
295
293
|
|
|
296
294
|
results.append(
|
|
297
295
|
TransactionSummary(
|
|
@@ -180,7 +180,7 @@ async def monitor_ws(
|
|
|
180
180
|
try:
|
|
181
181
|
event = await asyncio.wait_for(queue.get(), timeout=1.0)
|
|
182
182
|
await websocket.send_json(event)
|
|
183
|
-
except
|
|
183
|
+
except TimeoutError:
|
|
184
184
|
# Send a heartbeat ping to detect dead connections
|
|
185
185
|
try:
|
|
186
186
|
await websocket.send_json({"event_type": "ping"})
|
|
@@ -193,6 +193,7 @@ def balance(
|
|
|
193
193
|
checksum = _validate_address(address)
|
|
194
194
|
w3 = get_web3()
|
|
195
195
|
from eth_typing import ChecksumAddress as _CA
|
|
196
|
+
|
|
196
197
|
saldo = Decimal(str(w3.from_wei(w3.eth.get_balance(cast(_CA, checksum)), "ether")))
|
|
197
198
|
nonce = w3.eth.get_transaction_count(cast(_CA, checksum))
|
|
198
199
|
|
|
@@ -397,9 +398,7 @@ def debug_batch(
|
|
|
397
398
|
console.print(summary_table)
|
|
398
399
|
ok = sum(1 for r in results if r.get("status") == "success")
|
|
399
400
|
fail = len(results) - ok
|
|
400
|
-
console.print(
|
|
401
|
-
f"\n[green]✓ {ok} succeeded[/green] [red]✗ {fail} failed[/red]"
|
|
402
|
-
)
|
|
401
|
+
console.print(f"\n[green]✓ {ok} succeeded[/green] [red]✗ {fail} failed[/red]")
|
|
403
402
|
|
|
404
403
|
|
|
405
404
|
@app.command()
|
|
@@ -426,9 +425,12 @@ def codegen(
|
|
|
426
425
|
)
|
|
427
426
|
|
|
428
427
|
with console.status("DevCopilot generating code...", spinner="dots"):
|
|
429
|
-
resposta = DevCopilot().ask(prompt)
|
|
428
|
+
resposta = DevCopilot(max_tokens=8000).ask(prompt)
|
|
430
429
|
|
|
431
430
|
match = re.search(r"```python\s*(.*?)```", resposta, re.DOTALL)
|
|
431
|
+
if not match:
|
|
432
|
+
# Fallback: grab everything after ```python even if closing fence is missing
|
|
433
|
+
match = re.search(r"```python\s*(.*)", resposta, re.DOTALL)
|
|
432
434
|
codigo = match.group(1).strip() if match else None
|
|
433
435
|
explicacao = re.sub(r"```python.*?```", "", resposta, flags=re.DOTALL).strip()
|
|
434
436
|
|
|
@@ -970,9 +972,7 @@ def portfolio_report(
|
|
|
970
972
|
except SystemExit:
|
|
971
973
|
continue
|
|
972
974
|
|
|
973
|
-
with console.status(
|
|
974
|
-
f"Analyzing [cyan]{checksum[:10]}...[/cyan]", spinner="dots"
|
|
975
|
-
):
|
|
975
|
+
with console.status(f"Analyzing [cyan]{checksum[:10]}...[/cyan]", spinner="dots"):
|
|
976
976
|
snapshot = analyzer.analyze(checksum, scan_blocks=blocks)
|
|
977
977
|
|
|
978
978
|
data = analyzer.to_dict(snapshot)
|
|
@@ -981,9 +981,7 @@ def portfolio_report(
|
|
|
981
981
|
|
|
982
982
|
score_color = _SCORE_COLOR[snapshot.activity_score]
|
|
983
983
|
score_icon = _SCORE_ICON[snapshot.activity_score]
|
|
984
|
-
usdc_str =
|
|
985
|
-
f"{snapshot.usdc_balance:.4f}" if snapshot.usdc_balance is not None else "N/A"
|
|
986
|
-
)
|
|
984
|
+
usdc_str = f"{snapshot.usdc_balance:.4f}" if snapshot.usdc_balance is not None else "N/A"
|
|
987
985
|
|
|
988
986
|
report_table.add_row(
|
|
989
987
|
label or "—",
|
|
@@ -71,6 +71,7 @@ class DevCopilot:
|
|
|
71
71
|
extra_context: str | None = None,
|
|
72
72
|
model: str | None = None,
|
|
73
73
|
offline: bool = False,
|
|
74
|
+
max_tokens: int | None = None,
|
|
74
75
|
) -> None:
|
|
75
76
|
"""
|
|
76
77
|
Args:
|
|
@@ -79,10 +80,12 @@ class DevCopilot:
|
|
|
79
80
|
model: Model override (default: ANTHROPIC_MODEL from .env).
|
|
80
81
|
offline: When True, return a mock response without calling the API.
|
|
81
82
|
Useful for local tests and CI environments without an API key.
|
|
83
|
+
max_tokens: Override the default MAX_TOKENS limit for this instance.
|
|
82
84
|
"""
|
|
83
85
|
self._offline = offline
|
|
84
86
|
self._client = anthropic.Anthropic(api_key=settings.anthropic_api_key)
|
|
85
87
|
self.model = model or settings.anthropic_model
|
|
88
|
+
self._max_tokens = max_tokens if max_tokens is not None else self.MAX_TOKENS
|
|
86
89
|
self._history: list[dict] = []
|
|
87
90
|
self._cache: dict[str, tuple[str, float]] = {} # key → (response, timestamp)
|
|
88
91
|
|
|
@@ -147,7 +150,7 @@ class DevCopilot:
|
|
|
147
150
|
|
|
148
151
|
message = self._client.messages.create(
|
|
149
152
|
model=self.model,
|
|
150
|
-
max_tokens=self.
|
|
153
|
+
max_tokens=self._max_tokens,
|
|
151
154
|
system=self._system,
|
|
152
155
|
messages=cast(list[MessageParam], list(self._history)),
|
|
153
156
|
)
|
|
@@ -197,7 +200,7 @@ class DevCopilot:
|
|
|
197
200
|
|
|
198
201
|
with self._client.messages.stream(
|
|
199
202
|
model=self.model,
|
|
200
|
-
max_tokens=self.
|
|
203
|
+
max_tokens=self._max_tokens,
|
|
201
204
|
system=self._system,
|
|
202
205
|
messages=cast(list[MessageParam], self._history),
|
|
203
206
|
) as stream:
|
|
@@ -13,8 +13,8 @@ from arc_devkit.core.connection import get_web3
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
# Solidity revert selectors
|
|
16
|
-
_REVERT_SELECTOR = bytes.fromhex("08c379a0")
|
|
17
|
-
_PANIC_SELECTOR = bytes.fromhex("4e487b71")
|
|
16
|
+
_REVERT_SELECTOR = bytes.fromhex("08c379a0") # Error(string)
|
|
17
|
+
_PANIC_SELECTOR = bytes.fromhex("4e487b71") # Panic(uint256)
|
|
18
18
|
|
|
19
19
|
_PANIC_CODES: dict[int, str] = {
|
|
20
20
|
0x00: "Generic compiler-inserted panic",
|
|
@@ -164,6 +164,7 @@ class TxAnalyzer:
|
|
|
164
164
|
self._w3 = w3
|
|
165
165
|
elif rpc_url:
|
|
166
166
|
from web3.middleware import ExtraDataToPOAMiddleware
|
|
167
|
+
|
|
167
168
|
_w3 = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"timeout": 10}))
|
|
168
169
|
_w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
|
169
170
|
self._w3 = _w3
|
|
@@ -252,9 +253,7 @@ class TxAnalyzer:
|
|
|
252
253
|
summary = copilot.ask(prompt)
|
|
253
254
|
except Exception as exc:
|
|
254
255
|
logger.warning("AI analysis unavailable: %s", exc)
|
|
255
|
-
summary =
|
|
256
|
-
f"Status: {status_str} | Gas used: {gas_used} | Cost: {cost_decimal} USDC"
|
|
257
|
-
)
|
|
256
|
+
summary = f"Status: {status_str} | Gas used: {gas_used} | Cost: {cost_decimal} USDC"
|
|
258
257
|
if revert_reason:
|
|
259
258
|
summary += f" | Revert: {revert_reason}"
|
|
260
259
|
|
|
@@ -364,7 +363,13 @@ class TxAnalyzer:
|
|
|
364
363
|
normalized[k] = str(v)
|
|
365
364
|
elif isinstance(v, (list, tuple)):
|
|
366
365
|
normalized[k] = [
|
|
367
|
-
(
|
|
366
|
+
(
|
|
367
|
+
"0x" + x.hex()
|
|
368
|
+
if isinstance(x, bytes)
|
|
369
|
+
else str(x)
|
|
370
|
+
if isinstance(x, int)
|
|
371
|
+
else x
|
|
372
|
+
)
|
|
368
373
|
for x in v
|
|
369
374
|
]
|
|
370
375
|
else:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Contract compilation and deployment for Arc DevKit."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass
|
|
5
5
|
from decimal import Decimal
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
@@ -214,8 +214,7 @@ class ContractDeployer:
|
|
|
214
214
|
if key is None:
|
|
215
215
|
available = [k.split(":")[-1] for k in compiled]
|
|
216
216
|
raise ValueError(
|
|
217
|
-
f"Contract '{contract_name}' not found. "
|
|
218
|
-
f"Available: {', '.join(available)}"
|
|
217
|
+
f"Contract '{contract_name}' not found. Available: {', '.join(available)}"
|
|
219
218
|
)
|
|
220
219
|
|
|
221
220
|
abi = compiled[key]["abi"]
|
|
@@ -50,9 +50,7 @@ class EventListener:
|
|
|
50
50
|
|
|
51
51
|
self._contract = None
|
|
52
52
|
if self._contract_address and self._abi:
|
|
53
|
-
self._contract = self._w3.eth.contract(
|
|
54
|
-
address=self._contract_address, abi=self._abi
|
|
55
|
-
)
|
|
53
|
+
self._contract = self._w3.eth.contract(address=self._contract_address, abi=self._abi)
|
|
56
54
|
|
|
57
55
|
# ------------------------------------------------------------------
|
|
58
56
|
# Registration
|
|
@@ -94,7 +94,6 @@ class TestPaymentAgent:
|
|
|
94
94
|
assert result == 21_000
|
|
95
95
|
|
|
96
96
|
def test_wait_for_receipt_returns_receipt(self, mock_web3):
|
|
97
|
-
import time
|
|
98
97
|
|
|
99
98
|
receipt = {"status": 1, "gasUsed": 21_000}
|
|
100
99
|
mock_web3.eth.get_transaction_receipt.return_value = receipt
|
|
@@ -235,9 +234,7 @@ class TestPaymentAgent:
|
|
|
235
234
|
from arc_devkit.agents.payment_agent import PaymentAgent
|
|
236
235
|
|
|
237
236
|
agent = PaymentAgent(private_key="0x" + "a" * 64)
|
|
238
|
-
with patch("time.sleep") as mock_sleep, patch(
|
|
239
|
-
"time.time", side_effect=[0.0, 1.0, 2.0]
|
|
240
|
-
):
|
|
237
|
+
with patch("time.sleep") as mock_sleep, patch("time.time", side_effect=[0.0, 1.0, 2.0]):
|
|
241
238
|
result = agent._wait_for_receipt(b"\xde\xad", timeout=10)
|
|
242
239
|
assert result == receipt
|
|
243
240
|
mock_sleep.assert_called_once()
|
|
@@ -4,9 +4,6 @@ import json
|
|
|
4
4
|
from decimal import Decimal
|
|
5
5
|
from unittest.mock import MagicMock, patch
|
|
6
6
|
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
|
|
10
7
|
# ---------------------------------------------------------------------------
|
|
11
8
|
# Helpers
|
|
12
9
|
# ---------------------------------------------------------------------------
|
|
@@ -358,7 +355,7 @@ class TestPortfolioCLI:
|
|
|
358
355
|
|
|
359
356
|
class TestBalanceHistory:
|
|
360
357
|
def _make_snapshot(self, address="0x" + "a" * 40, tx_count=3):
|
|
361
|
-
from arc_devkit.analytics.portfolio import
|
|
358
|
+
from arc_devkit.analytics.portfolio import PortfolioSnapshot
|
|
362
359
|
|
|
363
360
|
return PortfolioSnapshot(
|
|
364
361
|
address=address,
|
|
@@ -390,7 +387,7 @@ class TestBalanceHistory:
|
|
|
390
387
|
analyzer.save_snapshot(snap, history_dir=tmp_path)
|
|
391
388
|
analyzer.save_snapshot(snap, history_dir=tmp_path)
|
|
392
389
|
path = tmp_path / f"{snap.address.lower()}.jsonl"
|
|
393
|
-
lines = [
|
|
390
|
+
lines = [line for line in path.read_text().splitlines() if line.strip()]
|
|
394
391
|
assert len(lines) == 2
|
|
395
392
|
|
|
396
393
|
def test_save_snapshot_includes_timestamp(self, tmp_path):
|
|
@@ -442,11 +439,11 @@ class TestBalanceHistory:
|
|
|
442
439
|
|
|
443
440
|
from arc_devkit.cli.flat import app
|
|
444
441
|
|
|
445
|
-
monkeypatch.setattr(
|
|
446
|
-
"arc_devkit.analytics.portfolio._HISTORY_DIR", tmp_path / "history"
|
|
447
|
-
)
|
|
442
|
+
monkeypatch.setattr("arc_devkit.analytics.portfolio._HISTORY_DIR", tmp_path / "history")
|
|
448
443
|
runner = CliRunner()
|
|
449
|
-
with patch(
|
|
444
|
+
with patch(
|
|
445
|
+
"arc_devkit.analytics.portfolio.PortfolioAnalyzer.load_history", return_value=[]
|
|
446
|
+
):
|
|
450
447
|
result = runner.invoke(app, ["portfolio", "history", "0x" + "a" * 40])
|
|
451
448
|
assert result.exit_code == 0
|
|
452
449
|
assert "No history" in result.output or "history" in result.output.lower()
|
|
@@ -457,9 +454,13 @@ class TestBalanceHistory:
|
|
|
457
454
|
|
|
458
455
|
from arc_devkit.cli.flat import app
|
|
459
456
|
|
|
460
|
-
records = [
|
|
457
|
+
records = [
|
|
458
|
+
{"native_balance": "1.5", "timestamp": "2026-01-01T00:00:00+00:00", "tx_count": 2}
|
|
459
|
+
]
|
|
461
460
|
runner = CliRunner()
|
|
462
|
-
with patch(
|
|
461
|
+
with patch(
|
|
462
|
+
"arc_devkit.analytics.portfolio.PortfolioAnalyzer.load_history", return_value=records
|
|
463
|
+
):
|
|
463
464
|
result = runner.invoke(app, ["portfolio", "history", "0x" + "a" * 40, "--json"])
|
|
464
465
|
assert result.exit_code == 0
|
|
465
466
|
data = json.loads(result.output)
|
|
@@ -130,9 +130,7 @@ def test_estimate_gas_sem_to_retorna_422(client):
|
|
|
130
130
|
|
|
131
131
|
def test_estimate_gas_bad_address_returns_400(client):
|
|
132
132
|
with patch("arc_devkit.core.gas.estimate_transfer", side_effect=Exception("bad addr")):
|
|
133
|
-
resp = client.get(
|
|
134
|
-
"/debug/estimate", params={"to": "bad", "amount": 1.0}
|
|
135
|
-
)
|
|
133
|
+
resp = client.get("/debug/estimate", params={"to": "bad", "amount": 1.0})
|
|
136
134
|
assert resp.status_code == 400
|
|
137
135
|
|
|
138
136
|
|
|
@@ -244,7 +242,9 @@ def test_payment_execute_error_returns_400(client):
|
|
|
244
242
|
|
|
245
243
|
|
|
246
244
|
def test_payment_500_on_unexpected_exception(client, mock_web3):
|
|
247
|
-
with patch(
|
|
245
|
+
with patch(
|
|
246
|
+
"arc_devkit.agents.payment_agent.PaymentAgent.execute", side_effect=Exception("boom")
|
|
247
|
+
):
|
|
248
248
|
resp = client.post(
|
|
249
249
|
"/agents/payment",
|
|
250
250
|
json={
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"""Tests for AsyncBaseAgent, AsyncMonitorAgent, and WebSocket endpoint."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
5
4
|
|
|
6
5
|
import pytest
|
|
7
6
|
|
|
8
|
-
|
|
9
7
|
# ---------------------------------------------------------------------------
|
|
10
8
|
# Helpers
|
|
11
9
|
# ---------------------------------------------------------------------------
|
|
@@ -146,8 +144,8 @@ async def test_async_monitor_callback_fired_on_change():
|
|
|
146
144
|
w3.from_wei.return_value = "1.0"
|
|
147
145
|
# First call seeds the initial balance, second detects a change
|
|
148
146
|
w3.eth.get_balance.side_effect = [
|
|
149
|
-
_MOCK_BALANCE,
|
|
150
|
-
_MOCK_BALANCE * 2,
|
|
147
|
+
_MOCK_BALANCE, # initial seed in execute()
|
|
148
|
+
_MOCK_BALANCE * 2, # change detected on first iteration
|
|
151
149
|
]
|
|
152
150
|
monitor._w3 = w3
|
|
153
151
|
|
|
@@ -235,11 +233,16 @@ async def test_async_monitor_event_stream_yields():
|
|
|
235
233
|
|
|
236
234
|
def test_websocket_monitor_accepts_connection():
|
|
237
235
|
"""WebSocket /agents/monitor/{address} must accept and send heartbeat pings."""
|
|
238
|
-
from arc_devkit.api.main import app
|
|
239
236
|
from fastapi.testclient import TestClient
|
|
240
237
|
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
from arc_devkit.api.main import app
|
|
239
|
+
|
|
240
|
+
with (
|
|
241
|
+
patch("arc_devkit.core.connection.get_web3", return_value=_mock_w3()),
|
|
242
|
+
patch(
|
|
243
|
+
"arc_devkit.agents.async_monitor.AsyncMonitorAgent.execute", new_callable=AsyncMock
|
|
244
|
+
) as mock_exec,
|
|
245
|
+
):
|
|
243
246
|
mock_exec.return_value = {"status": "done", "iterations": 0}
|
|
244
247
|
|
|
245
248
|
client = TestClient(app)
|
|
@@ -258,10 +261,14 @@ def test_async_monitor_state_file_loaded(tmp_path):
|
|
|
258
261
|
import json
|
|
259
262
|
|
|
260
263
|
state = tmp_path / "state.json"
|
|
261
|
-
state.write_text(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
state.write_text(
|
|
265
|
+
json.dumps(
|
|
266
|
+
{
|
|
267
|
+
"balances": {_ADDRESS: "5000000000000000000"},
|
|
268
|
+
"last_erc20_block": 42,
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
)
|
|
265
272
|
|
|
266
273
|
with patch("arc_devkit.core.connection.get_web3", return_value=_mock_w3()):
|
|
267
274
|
from arc_devkit.agents.async_monitor import AsyncMonitorAgent
|
|
@@ -331,8 +338,7 @@ async def test_async_monitor_webhook_called(tmp_path, respx_mock=None):
|
|
|
331
338
|
resp.raise_for_status = MagicMock()
|
|
332
339
|
return resp
|
|
333
340
|
|
|
334
|
-
with patch("httpx.AsyncClient.post", fake_post),
|
|
335
|
-
patch("asyncio.sleep", new_callable=AsyncMock):
|
|
341
|
+
with patch("httpx.AsyncClient.post", fake_post), patch("asyncio.sleep", new_callable=AsyncMock):
|
|
336
342
|
await monitor.execute(max_iterations=1)
|
|
337
343
|
|
|
338
344
|
assert len(events_sent) == 1
|
|
@@ -345,7 +351,7 @@ async def test_async_monitor_webhook_called(tmp_path, respx_mock=None):
|
|
|
345
351
|
|
|
346
352
|
|
|
347
353
|
def test_copilot_offline_ask_returns_mock(mock_anthropic):
|
|
348
|
-
from arc_devkit.copilot.agent import
|
|
354
|
+
from arc_devkit.copilot.agent import _OFFLINE_RESPONSE, DevCopilot
|
|
349
355
|
|
|
350
356
|
copilot = DevCopilot(offline=True)
|
|
351
357
|
assert copilot.ask("What is Arc?") == _OFFLINE_RESPONSE
|
|
@@ -353,7 +359,7 @@ def test_copilot_offline_ask_returns_mock(mock_anthropic):
|
|
|
353
359
|
|
|
354
360
|
|
|
355
361
|
def test_copilot_offline_stream_returns_mock(mock_anthropic):
|
|
356
|
-
from arc_devkit.copilot.agent import
|
|
362
|
+
from arc_devkit.copilot.agent import _OFFLINE_RESPONSE, DevCopilot
|
|
357
363
|
|
|
358
364
|
copilot = DevCopilot(offline=True)
|
|
359
365
|
result = "".join(copilot.ask_stream("What is Arc?"))
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"""Unit tests for arc_devkit.cli.flat — arc CLI commands."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
import tempfile
|
|
5
4
|
from decimal import Decimal
|
|
6
|
-
from
|
|
7
|
-
from unittest.mock import MagicMock, patch
|
|
5
|
+
from unittest.mock import patch
|
|
8
6
|
|
|
9
|
-
import pytest
|
|
10
7
|
from typer.testing import CliRunner
|
|
11
8
|
|
|
12
9
|
from arc_devkit.cli.flat import app
|
|
@@ -172,7 +169,6 @@ class TestWallet:
|
|
|
172
169
|
|
|
173
170
|
class TestConfig:
|
|
174
171
|
def test_config_set_and_get(self, tmp_path, monkeypatch):
|
|
175
|
-
env_file = tmp_path / ".env"
|
|
176
172
|
monkeypatch.chdir(tmp_path)
|
|
177
173
|
|
|
178
174
|
# set
|
|
@@ -224,7 +220,9 @@ class TestHistory:
|
|
|
224
220
|
def test_history_json_output(self, tmp_path, monkeypatch):
|
|
225
221
|
history_file = tmp_path / "history.json"
|
|
226
222
|
history_file.write_text(
|
|
227
|
-
json.dumps(
|
|
223
|
+
json.dumps(
|
|
224
|
+
[{"type": "debug", "tx_hash": "0x" + "a" * 64, "timestamp": "2026-06-21T10:00:00"}]
|
|
225
|
+
)
|
|
228
226
|
)
|
|
229
227
|
monkeypatch.setattr("arc_devkit.cli.flat._HISTORY_FILE", history_file)
|
|
230
228
|
result = runner.invoke(app, ["history", "--json"])
|
|
@@ -236,7 +234,15 @@ class TestHistory:
|
|
|
236
234
|
def test_history_table_output(self, tmp_path, monkeypatch):
|
|
237
235
|
history_file = tmp_path / "history.json"
|
|
238
236
|
history_file.write_text(
|
|
239
|
-
json.dumps(
|
|
237
|
+
json.dumps(
|
|
238
|
+
[
|
|
239
|
+
{
|
|
240
|
+
"type": "portfolio",
|
|
241
|
+
"address": "0x" + "a" * 40,
|
|
242
|
+
"timestamp": "2026-06-21T10:00:00",
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
)
|
|
240
246
|
)
|
|
241
247
|
monkeypatch.setattr("arc_devkit.cli.flat._HISTORY_FILE", history_file)
|
|
242
248
|
result = runner.invoke(app, ["history"])
|
|
@@ -277,7 +283,10 @@ class TestDebugCommand:
|
|
|
277
283
|
"summary": "OK",
|
|
278
284
|
"custo_usdc": "0.0001",
|
|
279
285
|
"revert_reason": None,
|
|
280
|
-
"decoded_input": {
|
|
286
|
+
"decoded_input": {
|
|
287
|
+
"function": "transfer",
|
|
288
|
+
"args": {"to": "0xf...", "amount": "100"},
|
|
289
|
+
},
|
|
281
290
|
"error": None,
|
|
282
291
|
"suggestion": "",
|
|
283
292
|
"raw_data": {},
|
|
@@ -353,7 +362,9 @@ class TestSendCommand:
|
|
|
353
362
|
"raw_transaction": "0xdeadbeef",
|
|
354
363
|
"nota": "Transaction signed.",
|
|
355
364
|
}
|
|
356
|
-
with patch(
|
|
365
|
+
with patch(
|
|
366
|
+
"arc_devkit.agents.payment_agent.PaymentAgent.execute", return_value=signed_result
|
|
367
|
+
):
|
|
357
368
|
result = runner.invoke(
|
|
358
369
|
app,
|
|
359
370
|
["send", "0x" + "b" * 40, "1.0", "--token", "native", "--json"],
|
|
@@ -377,7 +388,9 @@ class TestSendCommand:
|
|
|
377
388
|
"raw_transaction": "0xcafebabe",
|
|
378
389
|
"nota": "USDC transfer signed.",
|
|
379
390
|
}
|
|
380
|
-
with patch(
|
|
391
|
+
with patch(
|
|
392
|
+
"arc_devkit.agents.payment_agent.PaymentAgent.execute", return_value=signed_result
|
|
393
|
+
):
|
|
381
394
|
result = runner.invoke(
|
|
382
395
|
app,
|
|
383
396
|
["send", "0x" + "b" * 40, "5.0", "--token", "usdc", "--json"],
|
|
@@ -402,7 +415,9 @@ class TestSendCommand:
|
|
|
402
415
|
"raw_transaction": "0xdeadbeef01020304050607080910111213",
|
|
403
416
|
"nota": "Transaction signed.",
|
|
404
417
|
}
|
|
405
|
-
with patch(
|
|
418
|
+
with patch(
|
|
419
|
+
"arc_devkit.agents.payment_agent.PaymentAgent.execute", return_value=signed_result
|
|
420
|
+
):
|
|
406
421
|
result = runner.invoke(app, ["send", "0x" + "b" * 40, "2.0"])
|
|
407
422
|
|
|
408
423
|
assert result.exit_code == 0
|
|
@@ -421,7 +436,9 @@ class TestSendCommand:
|
|
|
421
436
|
"tx_hash": "0x" + "de" * 32,
|
|
422
437
|
"aviso": "Timeout waiting for receipt.",
|
|
423
438
|
}
|
|
424
|
-
with patch(
|
|
439
|
+
with patch(
|
|
440
|
+
"arc_devkit.agents.payment_agent.PaymentAgent.execute", return_value=sent_result
|
|
441
|
+
):
|
|
425
442
|
result = runner.invoke(app, ["send", "0x" + "b" * 40, "3.0", "--token", "usdc"])
|
|
426
443
|
|
|
427
444
|
assert result.exit_code == 0
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import tempfile
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from unittest.mock import MagicMock
|
|
6
|
+
from unittest.mock import MagicMock
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
# ---------------------------------------------------------------------------
|
|
12
11
|
# load_abi
|
|
13
12
|
# ---------------------------------------------------------------------------
|
|
@@ -15,9 +14,7 @@ import pytest
|
|
|
15
14
|
|
|
16
15
|
class TestLoadAbi:
|
|
17
16
|
def _write_json(self, data: object, suffix: str = ".json") -> Path:
|
|
18
|
-
f = tempfile.NamedTemporaryFile(
|
|
19
|
-
mode="w", suffix=suffix, delete=False, encoding="utf-8"
|
|
20
|
-
)
|
|
17
|
+
f = tempfile.NamedTemporaryFile(mode="w", suffix=suffix, delete=False, encoding="utf-8")
|
|
21
18
|
json.dump(data, f)
|
|
22
19
|
f.flush()
|
|
23
20
|
return Path(f.name)
|
|
@@ -79,9 +76,10 @@ class TestCallView:
|
|
|
79
76
|
assert result == "USDC"
|
|
80
77
|
|
|
81
78
|
def test_checksum_applied_to_address(self):
|
|
82
|
-
from arc_devkit.contracts.loader import call_view
|
|
83
79
|
from web3 import Web3
|
|
84
80
|
|
|
81
|
+
from arc_devkit.contracts.loader import call_view
|
|
82
|
+
|
|
85
83
|
w3 = self._w3_with_result(0)
|
|
86
84
|
abi: list = []
|
|
87
85
|
call_view(abi, "0x" + "a" * 40, "balanceOf", w3=w3)
|
|
@@ -153,7 +151,9 @@ class TestDecodeEvents:
|
|
|
153
151
|
|
|
154
152
|
w3 = MagicMock()
|
|
155
153
|
decoded_log = MagicMock()
|
|
156
|
-
decoded_log.__getitem__ = lambda self, k: {
|
|
154
|
+
decoded_log.__getitem__ = lambda self, k: {
|
|
155
|
+
"args": {"from": "0xa", "to": "0xb", "value": 100}
|
|
156
|
+
}[k]
|
|
157
157
|
w3.eth.contract.return_value.events.__getitem__.return_value.return_value.process_receipt.return_value = [
|
|
158
158
|
{"args": {"from": "0xa", "to": "0xb", "value": 100}}
|
|
159
159
|
]
|
|
@@ -180,8 +180,6 @@ class TestDecodeEvents:
|
|
|
180
180
|
from arc_devkit.contracts.loader import decode_events
|
|
181
181
|
|
|
182
182
|
w3 = MagicMock()
|
|
183
|
-
w3.eth.contract.return_value.events.__getitem__.return_value.return_value.process_receipt.return_value =
|
|
184
|
-
[]
|
|
185
|
-
)
|
|
183
|
+
w3.eth.contract.return_value.events.__getitem__.return_value.return_value.process_receipt.return_value = []
|
|
186
184
|
result = decode_events({}, [], "Transfer", "0x" + "a" * 40, w3=w3)
|
|
187
185
|
assert result == []
|
|
@@ -2,12 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import tempfile
|
|
5
|
-
from decimal import Decimal
|
|
6
5
|
from unittest.mock import MagicMock, patch
|
|
7
6
|
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
|
|
11
7
|
# ---------------------------------------------------------------------------
|
|
12
8
|
# Helpers
|
|
13
9
|
# ---------------------------------------------------------------------------
|
|
@@ -115,9 +111,10 @@ class TestDecodeRevertBytes:
|
|
|
115
111
|
assert "42" in result
|
|
116
112
|
|
|
117
113
|
def test_custom_error_no_args(self):
|
|
118
|
-
from arc_devkit.debugger.tx_analyzer import decode_revert_bytes
|
|
119
114
|
from web3 import Web3
|
|
120
115
|
|
|
116
|
+
from arc_devkit.debugger.tx_analyzer import decode_revert_bytes
|
|
117
|
+
|
|
121
118
|
abi = [{"type": "error", "name": "Unauthorized", "inputs": []}]
|
|
122
119
|
selector = Web3.keccak(text="Unauthorized()")[:4]
|
|
123
120
|
result = decode_revert_bytes(selector, abi=abi)
|
|
@@ -323,9 +320,7 @@ class TestDebugCLI:
|
|
|
323
320
|
from arc_devkit.cli.flat import app
|
|
324
321
|
|
|
325
322
|
runner = CliRunner()
|
|
326
|
-
result = runner.invoke(
|
|
327
|
-
app, ["debug", "0x" + "a" * 64, "--abi", "nonexistent.json"]
|
|
328
|
-
)
|
|
323
|
+
result = runner.invoke(app, ["debug", "0x" + "a" * 64, "--abi", "nonexistent.json"])
|
|
329
324
|
assert result.exit_code != 0
|
|
330
325
|
|
|
331
326
|
def test_debug_batch_from_txt_file(self, mock_anthropic):
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Unit tests for arc_devkit.deploy.deployer.ContractDeployer."""
|
|
2
2
|
|
|
3
|
-
from decimal import Decimal
|
|
4
3
|
from unittest.mock import MagicMock, patch
|
|
5
4
|
|
|
6
5
|
import pytest
|
|
@@ -162,7 +161,9 @@ class TestContractDeployer:
|
|
|
162
161
|
|
|
163
162
|
def test_deploy_from_source_raises_import_error_without_solcx(self, tmp_path):
|
|
164
163
|
sol_file = tmp_path / "Token.sol"
|
|
165
|
-
sol_file.write_text(
|
|
164
|
+
sol_file.write_text(
|
|
165
|
+
"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\ncontract Token {}"
|
|
166
|
+
)
|
|
166
167
|
|
|
167
168
|
d = self._deployer()
|
|
168
169
|
with patch.dict("sys.modules", {"solcx": None}):
|
|
@@ -100,7 +100,7 @@ class TestEventListenerPoll:
|
|
|
100
100
|
log = _make_log(block_number=101)
|
|
101
101
|
w3 = _make_w3(current_block=101, logs=[log])
|
|
102
102
|
el = EventListener(w3=w3, from_block=100)
|
|
103
|
-
|
|
103
|
+
el.poll()
|
|
104
104
|
w3.eth.get_logs.assert_called_once()
|
|
105
105
|
call_params = w3.eth.get_logs.call_args[0][0]
|
|
106
106
|
assert call_params["fromBlock"] == 100
|
|
@@ -127,7 +127,14 @@ class TestEventListenerPoll:
|
|
|
127
127
|
received: list = []
|
|
128
128
|
|
|
129
129
|
def fake_decode(self_log):
|
|
130
|
-
return {
|
|
130
|
+
return {
|
|
131
|
+
"event": "Transfer",
|
|
132
|
+
"address": "0x...",
|
|
133
|
+
"args": {},
|
|
134
|
+
"block_number": 1,
|
|
135
|
+
"tx_hash": None,
|
|
136
|
+
"log_index": 0,
|
|
137
|
+
}
|
|
131
138
|
|
|
132
139
|
with patch.object(EventListener, "_decode_log", side_effect=fake_decode):
|
|
133
140
|
el.on("Transfer", lambda e: received.append(e))
|
|
@@ -182,9 +189,18 @@ class TestEventListenerPoll:
|
|
|
182
189
|
|
|
183
190
|
received: list = []
|
|
184
191
|
bad_cb = MagicMock(side_effect=RuntimeError("boom"))
|
|
185
|
-
good_cb = lambda e: received.append(e)
|
|
186
192
|
|
|
187
|
-
|
|
193
|
+
def good_cb(e):
|
|
194
|
+
received.append(e)
|
|
195
|
+
|
|
196
|
+
fake_event = {
|
|
197
|
+
"event": "Transfer",
|
|
198
|
+
"address": "0x",
|
|
199
|
+
"args": {},
|
|
200
|
+
"block_number": 1,
|
|
201
|
+
"tx_hash": None,
|
|
202
|
+
"log_index": 0,
|
|
203
|
+
}
|
|
188
204
|
with patch.object(EventListener, "_decode_log", return_value=fake_event):
|
|
189
205
|
el.on("Transfer", bad_cb)
|
|
190
206
|
el.on("Transfer", good_cb)
|
|
@@ -227,7 +243,9 @@ class TestDecodeLog:
|
|
|
227
243
|
"transactionHash": bytes.fromhex("ab" * 32),
|
|
228
244
|
"logIndex": 0,
|
|
229
245
|
}.get(k, default)
|
|
230
|
-
contract_mock.events.__getitem__.return_value.return_value.process_log.return_value =
|
|
246
|
+
contract_mock.events.__getitem__.return_value.return_value.process_log.return_value = (
|
|
247
|
+
decoded
|
|
248
|
+
)
|
|
231
249
|
w3.eth.contract.return_value = contract_mock
|
|
232
250
|
|
|
233
251
|
el = EventListener(contract_address=addr, abi=abi, w3=w3)
|
|
@@ -245,7 +263,9 @@ class TestDecodeLog:
|
|
|
245
263
|
|
|
246
264
|
w3 = _make_w3()
|
|
247
265
|
contract_mock = MagicMock()
|
|
248
|
-
contract_mock.events.__getitem__.return_value.return_value.process_log.side_effect =
|
|
266
|
+
contract_mock.events.__getitem__.return_value.return_value.process_log.side_effect = (
|
|
267
|
+
Exception("bad log")
|
|
268
|
+
)
|
|
249
269
|
w3.eth.contract.return_value = contract_mock
|
|
250
270
|
|
|
251
271
|
el = EventListener(contract_address=addr, abi=abi, w3=w3)
|
|
@@ -275,7 +295,9 @@ class TestDecodeLog:
|
|
|
275
295
|
w3 = _make_w3()
|
|
276
296
|
contract_mock = MagicMock()
|
|
277
297
|
# Make event decoding fail so we fall back to raw
|
|
278
|
-
contract_mock.events.__getitem__.return_value.return_value.process_log.side_effect =
|
|
298
|
+
contract_mock.events.__getitem__.return_value.return_value.process_log.side_effect = (
|
|
299
|
+
Exception("fail")
|
|
300
|
+
)
|
|
279
301
|
w3.eth.contract.return_value = contract_mock
|
|
280
302
|
|
|
281
303
|
el = EventListener(contract_address=addr, abi=abi, w3=w3)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Unit tests for arc_devkit.usdc.token.USDCToken."""
|
|
2
2
|
|
|
3
3
|
from decimal import Decimal
|
|
4
|
-
from unittest.mock import MagicMock
|
|
4
|
+
from unittest.mock import MagicMock
|
|
5
5
|
|
|
6
6
|
# Standard Hardhat test private key — valid secp256k1 key safe for tests
|
|
7
7
|
_PRIVKEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
@@ -34,7 +34,7 @@ def _make_contract_w3(balance_atomic: int = 0) -> MagicMock:
|
|
|
34
34
|
|
|
35
35
|
class TestUSDCToken:
|
|
36
36
|
def _token(self, w3=None, balance_atomic: int = 0, contract_address: str | None = None):
|
|
37
|
-
from arc_devkit.usdc.token import
|
|
37
|
+
from arc_devkit.usdc.token import USDCToken
|
|
38
38
|
|
|
39
39
|
addr = contract_address or "0x" + "c" * 40
|
|
40
40
|
w3 = w3 or _make_contract_w3(balance_atomic)
|
|
@@ -128,9 +128,10 @@ class TestUSDCToken:
|
|
|
128
128
|
# --- contract_address property ---
|
|
129
129
|
|
|
130
130
|
def test_contract_address_property(self):
|
|
131
|
-
from arc_devkit.usdc.token import USDCToken
|
|
132
131
|
from web3 import Web3
|
|
133
132
|
|
|
133
|
+
from arc_devkit.usdc.token import USDCToken
|
|
134
|
+
|
|
134
135
|
addr = "0x" + "d" * 40
|
|
135
136
|
token = USDCToken(contract_address=addr, w3=_make_contract_w3())
|
|
136
137
|
assert token.contract_address == Web3.to_checksum_address(addr)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|