arc-devkit 0.4.5__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.5 → arc_devkit-0.4.6}/PKG-INFO +1 -1
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/agents/async_monitor.py +13 -11
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/agents/monitor_agent.py +11 -5
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/agents/payment_agent.py +22 -14
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/analytics/portfolio.py +5 -7
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/cli/flat.py +8 -10
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/copilot/agent.py +5 -2
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/debugger/tx_analyzer.py +11 -6
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/deploy/deployer.py +1 -2
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/events/listener.py +1 -3
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit.egg-info/PKG-INFO +1 -1
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/pyproject.toml +1 -1
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_agents.py +1 -3
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_analytics.py +10 -6
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_api.py +4 -4
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_async_agents.py +17 -10
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_cli.py +28 -7
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_contracts.py +5 -7
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_debugger.py +1 -3
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_deploy.py +3 -1
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_events.py +26 -5
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/README.md +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/agents/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/agents/async_base.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/agents/base_agent.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/analytics/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/api/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/api/main.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/api/routes/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/api/routes/agents.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/api/routes/copilot.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/api/routes/debugger.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/cli/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/cli/commands/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/cli/commands/agent.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/cli/commands/copilot.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/cli/commands/debug.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/cli/main.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/config.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/contracts/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/contracts/loader.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/copilot/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/core/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/core/connection.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/core/gas.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/core/wallet.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/debugger/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/deploy/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/events/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/usdc/__init__.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit/usdc/token.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit.egg-info/SOURCES.txt +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit.egg-info/dependency_links.txt +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit.egg-info/entry_points.txt +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit.egg-info/requires.txt +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/arc_devkit.egg-info/top_level.txt +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/setup.cfg +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_connection.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_copilot.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_integration.py +0 -0
- {arc_devkit-0.4.5 → arc_devkit-0.4.6}/tests/test_usdc.py +0 -0
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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(
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -234,9 +234,7 @@ class TestPaymentAgent:
|
|
|
234
234
|
from arc_devkit.agents.payment_agent import PaymentAgent
|
|
235
235
|
|
|
236
236
|
agent = PaymentAgent(private_key="0x" + "a" * 64)
|
|
237
|
-
with patch("time.sleep") as mock_sleep, patch(
|
|
238
|
-
"time.time", side_effect=[0.0, 1.0, 2.0]
|
|
239
|
-
):
|
|
237
|
+
with patch("time.sleep") as mock_sleep, patch("time.time", side_effect=[0.0, 1.0, 2.0]):
|
|
240
238
|
result = agent._wait_for_receipt(b"\xde\xad", timeout=10)
|
|
241
239
|
assert result == receipt
|
|
242
240
|
mock_sleep.assert_called_once()
|
|
@@ -439,11 +439,11 @@ class TestBalanceHistory:
|
|
|
439
439
|
|
|
440
440
|
from arc_devkit.cli.flat import app
|
|
441
441
|
|
|
442
|
-
monkeypatch.setattr(
|
|
443
|
-
"arc_devkit.analytics.portfolio._HISTORY_DIR", tmp_path / "history"
|
|
444
|
-
)
|
|
442
|
+
monkeypatch.setattr("arc_devkit.analytics.portfolio._HISTORY_DIR", tmp_path / "history")
|
|
445
443
|
runner = CliRunner()
|
|
446
|
-
with patch(
|
|
444
|
+
with patch(
|
|
445
|
+
"arc_devkit.analytics.portfolio.PortfolioAnalyzer.load_history", return_value=[]
|
|
446
|
+
):
|
|
447
447
|
result = runner.invoke(app, ["portfolio", "history", "0x" + "a" * 40])
|
|
448
448
|
assert result.exit_code == 0
|
|
449
449
|
assert "No history" in result.output or "history" in result.output.lower()
|
|
@@ -454,9 +454,13 @@ class TestBalanceHistory:
|
|
|
454
454
|
|
|
455
455
|
from arc_devkit.cli.flat import app
|
|
456
456
|
|
|
457
|
-
records = [
|
|
457
|
+
records = [
|
|
458
|
+
{"native_balance": "1.5", "timestamp": "2026-01-01T00:00:00+00:00", "tx_count": 2}
|
|
459
|
+
]
|
|
458
460
|
runner = CliRunner()
|
|
459
|
-
with patch(
|
|
461
|
+
with patch(
|
|
462
|
+
"arc_devkit.analytics.portfolio.PortfolioAnalyzer.load_history", return_value=records
|
|
463
|
+
):
|
|
460
464
|
result = runner.invoke(app, ["portfolio", "history", "0x" + "a" * 40, "--json"])
|
|
461
465
|
assert result.exit_code == 0
|
|
462
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={
|
|
@@ -144,8 +144,8 @@ async def test_async_monitor_callback_fired_on_change():
|
|
|
144
144
|
w3.from_wei.return_value = "1.0"
|
|
145
145
|
# First call seeds the initial balance, second detects a change
|
|
146
146
|
w3.eth.get_balance.side_effect = [
|
|
147
|
-
_MOCK_BALANCE,
|
|
148
|
-
_MOCK_BALANCE * 2,
|
|
147
|
+
_MOCK_BALANCE, # initial seed in execute()
|
|
148
|
+
_MOCK_BALANCE * 2, # change detected on first iteration
|
|
149
149
|
]
|
|
150
150
|
monitor._w3 = w3
|
|
151
151
|
|
|
@@ -237,8 +237,12 @@ def test_websocket_monitor_accepts_connection():
|
|
|
237
237
|
|
|
238
238
|
from arc_devkit.api.main import app
|
|
239
239
|
|
|
240
|
-
with
|
|
241
|
-
|
|
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
|
+
):
|
|
242
246
|
mock_exec.return_value = {"status": "done", "iterations": 0}
|
|
243
247
|
|
|
244
248
|
client = TestClient(app)
|
|
@@ -257,10 +261,14 @@ def test_async_monitor_state_file_loaded(tmp_path):
|
|
|
257
261
|
import json
|
|
258
262
|
|
|
259
263
|
state = tmp_path / "state.json"
|
|
260
|
-
state.write_text(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
+
state.write_text(
|
|
265
|
+
json.dumps(
|
|
266
|
+
{
|
|
267
|
+
"balances": {_ADDRESS: "5000000000000000000"},
|
|
268
|
+
"last_erc20_block": 42,
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
)
|
|
264
272
|
|
|
265
273
|
with patch("arc_devkit.core.connection.get_web3", return_value=_mock_w3()):
|
|
266
274
|
from arc_devkit.agents.async_monitor import AsyncMonitorAgent
|
|
@@ -330,8 +338,7 @@ async def test_async_monitor_webhook_called(tmp_path, respx_mock=None):
|
|
|
330
338
|
resp.raise_for_status = MagicMock()
|
|
331
339
|
return resp
|
|
332
340
|
|
|
333
|
-
with patch("httpx.AsyncClient.post", fake_post),
|
|
334
|
-
patch("asyncio.sleep", new_callable=AsyncMock):
|
|
341
|
+
with patch("httpx.AsyncClient.post", fake_post), patch("asyncio.sleep", new_callable=AsyncMock):
|
|
335
342
|
await monitor.execute(max_iterations=1)
|
|
336
343
|
|
|
337
344
|
assert len(events_sent) == 1
|
|
@@ -220,7 +220,9 @@ class TestHistory:
|
|
|
220
220
|
def test_history_json_output(self, tmp_path, monkeypatch):
|
|
221
221
|
history_file = tmp_path / "history.json"
|
|
222
222
|
history_file.write_text(
|
|
223
|
-
json.dumps(
|
|
223
|
+
json.dumps(
|
|
224
|
+
[{"type": "debug", "tx_hash": "0x" + "a" * 64, "timestamp": "2026-06-21T10:00:00"}]
|
|
225
|
+
)
|
|
224
226
|
)
|
|
225
227
|
monkeypatch.setattr("arc_devkit.cli.flat._HISTORY_FILE", history_file)
|
|
226
228
|
result = runner.invoke(app, ["history", "--json"])
|
|
@@ -232,7 +234,15 @@ class TestHistory:
|
|
|
232
234
|
def test_history_table_output(self, tmp_path, monkeypatch):
|
|
233
235
|
history_file = tmp_path / "history.json"
|
|
234
236
|
history_file.write_text(
|
|
235
|
-
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
|
+
)
|
|
236
246
|
)
|
|
237
247
|
monkeypatch.setattr("arc_devkit.cli.flat._HISTORY_FILE", history_file)
|
|
238
248
|
result = runner.invoke(app, ["history"])
|
|
@@ -273,7 +283,10 @@ class TestDebugCommand:
|
|
|
273
283
|
"summary": "OK",
|
|
274
284
|
"custo_usdc": "0.0001",
|
|
275
285
|
"revert_reason": None,
|
|
276
|
-
"decoded_input": {
|
|
286
|
+
"decoded_input": {
|
|
287
|
+
"function": "transfer",
|
|
288
|
+
"args": {"to": "0xf...", "amount": "100"},
|
|
289
|
+
},
|
|
277
290
|
"error": None,
|
|
278
291
|
"suggestion": "",
|
|
279
292
|
"raw_data": {},
|
|
@@ -349,7 +362,9 @@ class TestSendCommand:
|
|
|
349
362
|
"raw_transaction": "0xdeadbeef",
|
|
350
363
|
"nota": "Transaction signed.",
|
|
351
364
|
}
|
|
352
|
-
with patch(
|
|
365
|
+
with patch(
|
|
366
|
+
"arc_devkit.agents.payment_agent.PaymentAgent.execute", return_value=signed_result
|
|
367
|
+
):
|
|
353
368
|
result = runner.invoke(
|
|
354
369
|
app,
|
|
355
370
|
["send", "0x" + "b" * 40, "1.0", "--token", "native", "--json"],
|
|
@@ -373,7 +388,9 @@ class TestSendCommand:
|
|
|
373
388
|
"raw_transaction": "0xcafebabe",
|
|
374
389
|
"nota": "USDC transfer signed.",
|
|
375
390
|
}
|
|
376
|
-
with patch(
|
|
391
|
+
with patch(
|
|
392
|
+
"arc_devkit.agents.payment_agent.PaymentAgent.execute", return_value=signed_result
|
|
393
|
+
):
|
|
377
394
|
result = runner.invoke(
|
|
378
395
|
app,
|
|
379
396
|
["send", "0x" + "b" * 40, "5.0", "--token", "usdc", "--json"],
|
|
@@ -398,7 +415,9 @@ class TestSendCommand:
|
|
|
398
415
|
"raw_transaction": "0xdeadbeef01020304050607080910111213",
|
|
399
416
|
"nota": "Transaction signed.",
|
|
400
417
|
}
|
|
401
|
-
with patch(
|
|
418
|
+
with patch(
|
|
419
|
+
"arc_devkit.agents.payment_agent.PaymentAgent.execute", return_value=signed_result
|
|
420
|
+
):
|
|
402
421
|
result = runner.invoke(app, ["send", "0x" + "b" * 40, "2.0"])
|
|
403
422
|
|
|
404
423
|
assert result.exit_code == 0
|
|
@@ -417,7 +436,9 @@ class TestSendCommand:
|
|
|
417
436
|
"tx_hash": "0x" + "de" * 32,
|
|
418
437
|
"aviso": "Timeout waiting for receipt.",
|
|
419
438
|
}
|
|
420
|
-
with patch(
|
|
439
|
+
with patch(
|
|
440
|
+
"arc_devkit.agents.payment_agent.PaymentAgent.execute", return_value=sent_result
|
|
441
|
+
):
|
|
421
442
|
result = runner.invoke(app, ["send", "0x" + "b" * 40, "3.0", "--token", "usdc"])
|
|
422
443
|
|
|
423
444
|
assert result.exit_code == 0
|
|
@@ -14,9 +14,7 @@ import pytest
|
|
|
14
14
|
|
|
15
15
|
class TestLoadAbi:
|
|
16
16
|
def _write_json(self, data: object, suffix: str = ".json") -> Path:
|
|
17
|
-
f = tempfile.NamedTemporaryFile(
|
|
18
|
-
mode="w", suffix=suffix, delete=False, encoding="utf-8"
|
|
19
|
-
)
|
|
17
|
+
f = tempfile.NamedTemporaryFile(mode="w", suffix=suffix, delete=False, encoding="utf-8")
|
|
20
18
|
json.dump(data, f)
|
|
21
19
|
f.flush()
|
|
22
20
|
return Path(f.name)
|
|
@@ -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 == []
|
|
@@ -320,9 +320,7 @@ class TestDebugCLI:
|
|
|
320
320
|
from arc_devkit.cli.flat import app
|
|
321
321
|
|
|
322
322
|
runner = CliRunner()
|
|
323
|
-
result = runner.invoke(
|
|
324
|
-
app, ["debug", "0x" + "a" * 64, "--abi", "nonexistent.json"]
|
|
325
|
-
)
|
|
323
|
+
result = runner.invoke(app, ["debug", "0x" + "a" * 64, "--abi", "nonexistent.json"])
|
|
326
324
|
assert result.exit_code != 0
|
|
327
325
|
|
|
328
326
|
def test_debug_batch_from_txt_file(self, mock_anthropic):
|
|
@@ -161,7 +161,9 @@ class TestContractDeployer:
|
|
|
161
161
|
|
|
162
162
|
def test_deploy_from_source_raises_import_error_without_solcx(self, tmp_path):
|
|
163
163
|
sol_file = tmp_path / "Token.sol"
|
|
164
|
-
sol_file.write_text(
|
|
164
|
+
sol_file.write_text(
|
|
165
|
+
"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\ncontract Token {}"
|
|
166
|
+
)
|
|
165
167
|
|
|
166
168
|
d = self._deployer()
|
|
167
169
|
with patch.dict("sys.modules", {"solcx": None}):
|
|
@@ -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,10 +189,18 @@ class TestEventListenerPoll:
|
|
|
182
189
|
|
|
183
190
|
received: list = []
|
|
184
191
|
bad_cb = MagicMock(side_effect=RuntimeError("boom"))
|
|
192
|
+
|
|
185
193
|
def good_cb(e):
|
|
186
194
|
received.append(e)
|
|
187
195
|
|
|
188
|
-
fake_event = {
|
|
196
|
+
fake_event = {
|
|
197
|
+
"event": "Transfer",
|
|
198
|
+
"address": "0x",
|
|
199
|
+
"args": {},
|
|
200
|
+
"block_number": 1,
|
|
201
|
+
"tx_hash": None,
|
|
202
|
+
"log_index": 0,
|
|
203
|
+
}
|
|
189
204
|
with patch.object(EventListener, "_decode_log", return_value=fake_event):
|
|
190
205
|
el.on("Transfer", bad_cb)
|
|
191
206
|
el.on("Transfer", good_cb)
|
|
@@ -228,7 +243,9 @@ class TestDecodeLog:
|
|
|
228
243
|
"transactionHash": bytes.fromhex("ab" * 32),
|
|
229
244
|
"logIndex": 0,
|
|
230
245
|
}.get(k, default)
|
|
231
|
-
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
|
+
)
|
|
232
249
|
w3.eth.contract.return_value = contract_mock
|
|
233
250
|
|
|
234
251
|
el = EventListener(contract_address=addr, abi=abi, w3=w3)
|
|
@@ -246,7 +263,9 @@ class TestDecodeLog:
|
|
|
246
263
|
|
|
247
264
|
w3 = _make_w3()
|
|
248
265
|
contract_mock = MagicMock()
|
|
249
|
-
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
|
+
)
|
|
250
269
|
w3.eth.contract.return_value = contract_mock
|
|
251
270
|
|
|
252
271
|
el = EventListener(contract_address=addr, abi=abi, w3=w3)
|
|
@@ -276,7 +295,9 @@ class TestDecodeLog:
|
|
|
276
295
|
w3 = _make_w3()
|
|
277
296
|
contract_mock = MagicMock()
|
|
278
297
|
# Make event decoding fail so we fall back to raw
|
|
279
|
-
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
|
+
)
|
|
280
301
|
w3.eth.contract.return_value = contract_mock
|
|
281
302
|
|
|
282
303
|
el = EventListener(contract_address=addr, abi=abi, w3=w3)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|