primecli 0.10.1__tar.gz → 0.10.2__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.
- {primecli-0.10.1 → primecli-0.10.2}/PKG-INFO +2 -2
- {primecli-0.10.1 → primecli-0.10.2}/README.md +1 -1
- {primecli-0.10.1 → primecli-0.10.2}/primecli/_flowledger.py +60 -0
- {primecli-0.10.1 → primecli-0.10.2}/primecli/arbprime.py +51 -10
- {primecli-0.10.1 → primecli-0.10.2}/primecli/degenprime.py +82 -21
- {primecli-0.10.1 → primecli-0.10.2}/primecli/deltaprime.py +51 -10
- {primecli-0.10.1 → primecli-0.10.2}/primecli/health_monitor.py +2 -2
- {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/PKG-INFO +2 -2
- {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/SOURCES.txt +1 -0
- {primecli-0.10.1 → primecli-0.10.2}/pyproject.toml +1 -1
- primecli-0.10.2/tests/test_flowledger_transferred_amount.py +102 -0
- {primecli-0.10.1 → primecli-0.10.2}/LICENSE +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/primecli/__init__.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/primecli/_wallets.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/primecli/bridge.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/dependency_links.txt +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/entry_points.txt +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/requires.txt +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/top_level.txt +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/setup.cfg +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_aero_range_and_swap_fallback.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_aero_rebalance.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_bridge.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_cross_file_identity.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_gas_limit.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_gas_pricing.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_health_meter.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_health_monitor.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_paraswap_validator.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_redstone_encoding.py +0 -0
- {primecli-0.10.1 → primecli-0.10.2}/tests/test_to_wei_units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.2
|
|
4
4
|
Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
|
|
5
5
|
Author: Mnemosyne-quest contributors
|
|
6
6
|
License: MIT
|
|
@@ -47,7 +47,7 @@ Built for agent use:
|
|
|
47
47
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
48
48
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
49
49
|
|
|
50
|
-
**Current version:** 0.10.
|
|
50
|
+
**Current version:** 0.10.2 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
51
51
|
|
|
52
52
|
> **Breaking change in 0.5.0:** there is no longer a default signing key. Earlier versions silently fell back to a baked-in agent when no key was configured; that fallback has been removed. With no key configured, every command now fails closed with `No signing key found...`. Set a key explicitly (see [Configuration](#configuration)).
|
|
53
53
|
|
|
@@ -16,7 +16,7 @@ Built for agent use:
|
|
|
16
16
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
17
17
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
18
18
|
|
|
19
|
-
**Current version:** 0.10.
|
|
19
|
+
**Current version:** 0.10.2 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
20
20
|
|
|
21
21
|
> **Breaking change in 0.5.0:** there is no longer a default signing key. Earlier versions silently fell back to a baked-in agent when no key was configured; that fallback has been removed. With no key configured, every command now fails closed with `No signing key found...`. Set a key explicitly (see [Configuration](#configuration)).
|
|
22
22
|
|
|
@@ -90,6 +90,66 @@ def append_flow(chain: str, account: str, record: dict, ledger_dir=None) -> bool
|
|
|
90
90
|
return False
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
# ERC20 Transfer(address,address,uint256) event topic (keccak of the signature).
|
|
94
|
+
# Hardcoded so this module stays web3-free (it only does JSONL IO otherwise).
|
|
95
|
+
_ERC20_TRANSFER_TOPIC = (
|
|
96
|
+
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _to_hex(v) -> str:
|
|
101
|
+
"""Normalise a web3 HexBytes / bytes / str to a 0x-prefixed lowercase hex string."""
|
|
102
|
+
h = v.hex() if hasattr(v, "hex") else str(v)
|
|
103
|
+
h = h.lower()
|
|
104
|
+
return h if h.startswith("0x") else "0x" + h
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _norm_addr(v) -> str:
|
|
108
|
+
"""Last-20-bytes address as 0x-prefixed lowercase (handles 32-byte padded topics
|
|
109
|
+
and 20-byte log addresses alike)."""
|
|
110
|
+
return "0x" + _to_hex(v)[2:][-40:]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def transferred_amount(receipt, token_addr: str, from_addr: str, to_addr: str,
|
|
114
|
+
decimals: int):
|
|
115
|
+
"""Real ERC20 amount moved `from_addr` -> `to_addr` in `token_addr` within this
|
|
116
|
+
receipt, in human units (raw / 10**decimals). Sums all matching Transfer logs.
|
|
117
|
+
|
|
118
|
+
Returns None when the receipt carries NO matching Transfer log at all — the caller
|
|
119
|
+
then falls back to the requested amount (can't determine the truth). A matching
|
|
120
|
+
Transfer of value 0 returns 0.0 (the fund pulled nothing), NOT None.
|
|
121
|
+
|
|
122
|
+
This is the truth source for a fund flow: `fund(asset, amount)` can pull LESS than
|
|
123
|
+
`amount` when the wallet's balance/allowance is short — e.g. a leverage-open where
|
|
124
|
+
most of the position is borrowed, so the EOA only holds dust. Logging the requested
|
|
125
|
+
arg then over-counts contribution and corrupts every downstream PnL/ROI/APR. Parsing
|
|
126
|
+
the actual on-chain Transfer fixes that at the source.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
token = _norm_addr(token_addr)
|
|
130
|
+
frm = _norm_addr(from_addr)
|
|
131
|
+
to = _norm_addr(to_addr)
|
|
132
|
+
total_raw = 0
|
|
133
|
+
found = False
|
|
134
|
+
for log in receipt["logs"]:
|
|
135
|
+
if _norm_addr(log["address"]) != token:
|
|
136
|
+
continue
|
|
137
|
+
topics = log["topics"]
|
|
138
|
+
if len(topics) != 3 or _to_hex(topics[0]) != _ERC20_TRANSFER_TOPIC:
|
|
139
|
+
continue
|
|
140
|
+
if _norm_addr(topics[1]) != frm or _norm_addr(topics[2]) != to:
|
|
141
|
+
continue
|
|
142
|
+
total_raw += int(_to_hex(log["data"]), 16)
|
|
143
|
+
found = True
|
|
144
|
+
if not found:
|
|
145
|
+
return None
|
|
146
|
+
return total_raw / (10 ** decimals)
|
|
147
|
+
except Exception as e: # noqa: BLE001 — never break the caller over a parse error
|
|
148
|
+
print(f" WARN flowledger: transfer parse failed ({type(e).__name__}: {e})",
|
|
149
|
+
file=sys.stderr)
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
93
153
|
def make_record(*, ts: int, ftype: str, asset: str, token_amount: float,
|
|
94
154
|
usd_value, tx: str, block: int, source: str) -> dict:
|
|
95
155
|
"""Build a ledger record with exactly the keys pnl_backfill writes (minus the
|
|
@@ -1966,21 +1966,42 @@ def cmd_fund(pool_name: str, amount: float, execute: bool = False):
|
|
|
1966
1966
|
receipt = _sign_and_send(w3, acct, fund_tx, f"Fund {amount} {symbol}", fallback_gas=3000000)
|
|
1967
1967
|
ok = receipt["status"] == 1
|
|
1968
1968
|
if ok:
|
|
1969
|
-
_log_fund_flow(
|
|
1969
|
+
_log_fund_flow(
|
|
1970
|
+
pa, symbol, amount, receipt,
|
|
1971
|
+
token_addr=(None if cfg["native"] else cfg["token"]),
|
|
1972
|
+
from_addr=acct.address, decimals=cfg["decimals"],
|
|
1973
|
+
)
|
|
1970
1974
|
return ok
|
|
1971
1975
|
|
|
1972
1976
|
|
|
1973
|
-
def _log_fund_flow(account_addr: str, symbol: str, amount: float, receipt
|
|
1977
|
+
def _log_fund_flow(account_addr: str, symbol: str, amount: float, receipt,
|
|
1978
|
+
token_addr: str = None, from_addr: str = None,
|
|
1979
|
+
decimals: int = None) -> None:
|
|
1974
1980
|
"""Append a 'deposit' flow record after a successful fund broadcast. asset is the
|
|
1975
1981
|
funded bytes32 symbol (the wrapped-native symbol for native funding, e.g. 'ETH').
|
|
1976
1982
|
usd_value uses the current spot price (≈ flow-time price); None when unpriced.
|
|
1977
|
-
Wrapped so a logging failure can never fail the financial op.
|
|
1983
|
+
Wrapped so a logging failure can never fail the financial op.
|
|
1984
|
+
|
|
1985
|
+
Logs the ACTUAL ERC20 Transfer(EOA -> account) amount from the receipt, not the
|
|
1986
|
+
requested `amount`: an ERC20 fund() can pull less than asked when the wallet is
|
|
1987
|
+
short (leverage-opens fund mostly from borrow), and logging the request over-counts
|
|
1988
|
+
contribution. Native funding moves the exact msg.value, so `amount` stands there
|
|
1989
|
+
(token_addr is None)."""
|
|
1978
1990
|
try:
|
|
1991
|
+
actual = amount
|
|
1992
|
+
if token_addr and from_addr and decimals is not None:
|
|
1993
|
+
moved = _flowledger.transferred_amount(
|
|
1994
|
+
receipt, token_addr, from_addr, account_addr, decimals)
|
|
1995
|
+
if moved is not None:
|
|
1996
|
+
if abs(moved - amount) > max(1e-6, abs(amount) * 1e-4):
|
|
1997
|
+
print(f" NOTE fund pulled {moved} {symbol} of {amount} requested "
|
|
1998
|
+
f"(logging actual)", file=sys.stderr)
|
|
1999
|
+
actual = moved
|
|
1979
2000
|
px = token_price(symbol)
|
|
1980
|
-
usd = round(
|
|
2001
|
+
usd = round(actual * px, 8) if px else None
|
|
1981
2002
|
rec = _flowledger.make_record(
|
|
1982
2003
|
ts=int(time.time()), ftype="deposit", asset=symbol,
|
|
1983
|
-
token_amount=
|
|
2004
|
+
token_amount=actual, usd_value=usd,
|
|
1984
2005
|
tx=receipt["transactionHash"].hex(), block=receipt["blockNumber"],
|
|
1985
2006
|
source="live-fund",
|
|
1986
2007
|
)
|
|
@@ -3500,20 +3521,40 @@ def cmd_execute_withdrawal(pool_name: str, index: int = None, execute: bool = Fa
|
|
|
3500
3521
|
ok = receipt["status"] == 1
|
|
3501
3522
|
if ok:
|
|
3502
3523
|
executed_amount = sum(intents[i][0] for i in ready) / 10 ** cfg["decimals"]
|
|
3503
|
-
_log_withdraw_flow(
|
|
3524
|
+
_log_withdraw_flow(
|
|
3525
|
+
pa, symbol, executed_amount, receipt,
|
|
3526
|
+
token_addr=(None if cfg["native"] else cfg["token"]),
|
|
3527
|
+
to_addr=acct.address, decimals=cfg["decimals"],
|
|
3528
|
+
)
|
|
3504
3529
|
|
|
3505
3530
|
|
|
3506
|
-
def _log_withdraw_flow(account_addr: str, symbol: str, amount: float, receipt
|
|
3531
|
+
def _log_withdraw_flow(account_addr: str, symbol: str, amount: float, receipt,
|
|
3532
|
+
token_addr: str = None, to_addr: str = None,
|
|
3533
|
+
decimals: int = None) -> None:
|
|
3507
3534
|
"""Append a 'withdraw' flow record after a successful executeWithdrawalIntent
|
|
3508
3535
|
broadcast (funds left the account to the EOA). amount is the total executed across
|
|
3509
3536
|
the matured intents. usd_value uses current spot price; None when unpriced. Wrapped
|
|
3510
|
-
so a logging failure can never fail the financial op.
|
|
3537
|
+
so a logging failure can never fail the financial op.
|
|
3538
|
+
|
|
3539
|
+
Prefers the ACTUAL ERC20 Transfer(account -> EOA) amount from the receipt over the
|
|
3540
|
+
committed `amount`, symmetric with the fund path: the executed amount is normally
|
|
3541
|
+
exact, but parsing the receipt keeps the ledger honest if it ever diverges. Native
|
|
3542
|
+
unwraps emit no ERC20 Transfer, so `amount` stands there (token_addr is None)."""
|
|
3511
3543
|
try:
|
|
3544
|
+
actual = amount
|
|
3545
|
+
if token_addr and to_addr and decimals is not None:
|
|
3546
|
+
moved = _flowledger.transferred_amount(
|
|
3547
|
+
receipt, token_addr, account_addr, to_addr, decimals)
|
|
3548
|
+
if moved is not None:
|
|
3549
|
+
if abs(moved - amount) > max(1e-6, abs(amount) * 1e-4):
|
|
3550
|
+
print(f" NOTE withdraw moved {moved} {symbol} of {amount} executed "
|
|
3551
|
+
f"(logging actual)", file=sys.stderr)
|
|
3552
|
+
actual = moved
|
|
3512
3553
|
px = token_price(symbol)
|
|
3513
|
-
usd = round(
|
|
3554
|
+
usd = round(actual * px, 8) if px else None
|
|
3514
3555
|
rec = _flowledger.make_record(
|
|
3515
3556
|
ts=int(time.time()), ftype="withdraw", asset=symbol,
|
|
3516
|
-
token_amount=
|
|
3557
|
+
token_amount=actual, usd_value=usd,
|
|
3517
3558
|
tx=receipt["transactionHash"].hex(), block=receipt["blockNumber"],
|
|
3518
3559
|
source="live-withdraw",
|
|
3519
3560
|
)
|
|
@@ -2002,22 +2002,43 @@ def cmd_fund(pool_name: str, amount: float, execute: bool = False):
|
|
|
2002
2002
|
receipt = _sign_and_send(w3, acct, fund_tx, f"Fund {amount} {symbol}", fallback_gas=3000000)
|
|
2003
2003
|
ok = receipt["status"] == 1
|
|
2004
2004
|
if ok:
|
|
2005
|
-
_log_fund_flow(
|
|
2005
|
+
_log_fund_flow(
|
|
2006
|
+
pa, symbol, amount, receipt,
|
|
2007
|
+
token_addr=(None if cfg["native"] else cfg["token"]),
|
|
2008
|
+
from_addr=acct.address, decimals=cfg["decimals"],
|
|
2009
|
+
)
|
|
2006
2010
|
return ok
|
|
2007
2011
|
|
|
2008
2012
|
|
|
2009
|
-
def _log_fund_flow(account_addr: str, symbol: str, amount: float, receipt
|
|
2013
|
+
def _log_fund_flow(account_addr: str, symbol: str, amount: float, receipt,
|
|
2014
|
+
token_addr: str = None, from_addr: str = None,
|
|
2015
|
+
decimals: int = None) -> None:
|
|
2010
2016
|
"""Append a 'deposit' flow record after a successful fund broadcast. asset is the
|
|
2011
2017
|
funded bytes32 symbol (the wrapped-native symbol for native funding — degenprime's
|
|
2012
2018
|
POOLS already stores the native pool's symbol as the wrapped name, e.g. 'ETH').
|
|
2013
2019
|
usd_value uses the current spot price (≈ flow-time price); None when unpriced.
|
|
2014
|
-
Wrapped so a logging failure can never fail the financial op.
|
|
2020
|
+
Wrapped so a logging failure can never fail the financial op.
|
|
2021
|
+
|
|
2022
|
+
The logged amount is the ACTUAL ERC20 Transfer(EOA -> account) from the receipt,
|
|
2023
|
+
not the requested `amount`: an ERC20 fund() can pull less than asked when the
|
|
2024
|
+
wallet is short (leverage-opens fund mostly from borrow), and logging the request
|
|
2025
|
+
over-counts contribution. Native funding moves the exact msg.value (can't be
|
|
2026
|
+
partial), so `amount` stands there (token_addr is None)."""
|
|
2015
2027
|
try:
|
|
2028
|
+
actual = amount
|
|
2029
|
+
if token_addr and from_addr and decimals is not None:
|
|
2030
|
+
moved = _flowledger.transferred_amount(
|
|
2031
|
+
receipt, token_addr, from_addr, account_addr, decimals)
|
|
2032
|
+
if moved is not None:
|
|
2033
|
+
if abs(moved - amount) > max(1e-6, abs(amount) * 1e-4):
|
|
2034
|
+
print(f" NOTE fund pulled {moved} {symbol} of {amount} requested "
|
|
2035
|
+
f"(logging actual)", file=sys.stderr)
|
|
2036
|
+
actual = moved
|
|
2016
2037
|
px = token_price(symbol)
|
|
2017
|
-
usd = round(
|
|
2038
|
+
usd = round(actual * px, 8) if px else None
|
|
2018
2039
|
rec = _flowledger.make_record(
|
|
2019
2040
|
ts=int(time.time()), ftype="deposit", asset=symbol,
|
|
2020
|
-
token_amount=
|
|
2041
|
+
token_amount=actual, usd_value=usd,
|
|
2021
2042
|
tx=receipt["transactionHash"].hex(), block=receipt["blockNumber"],
|
|
2022
2043
|
source="live-fund",
|
|
2023
2044
|
)
|
|
@@ -3138,7 +3159,17 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
|
|
|
3138
3159
|
"""Swap one in-account asset for another via the Degen Account on ParaSwap v6.
|
|
3139
3160
|
Sells the account's in-account balance of --from for --to. Carries remainsSolvent,
|
|
3140
3161
|
so the --execute path appends a RedStone signed-price payload to the calldata."""
|
|
3141
|
-
|
|
3162
|
+
# Canonicalize to the exact SWAP_ASSETS form (case-insensitive), matching swap-debt.
|
|
3163
|
+
# Without this, a mixed-case pool symbol like cbBTC (passed as 'CBBTC'/'cbbtc') fails
|
|
3164
|
+
# _swap_asset_meta even though it's valid. Unknown assets fall through uppercased so
|
|
3165
|
+
# the "Unknown asset" errors below still fire clearly.
|
|
3166
|
+
def _canon_swap_sym(s):
|
|
3167
|
+
s_up = str(s).upper()
|
|
3168
|
+
for _k in SWAP_ASSETS:
|
|
3169
|
+
if _k.upper() == s_up:
|
|
3170
|
+
return _k
|
|
3171
|
+
return s_up
|
|
3172
|
+
from_sym, to_sym = _canon_swap_sym(from_sym), _canon_swap_sym(to_sym)
|
|
3142
3173
|
if from_sym == to_sym:
|
|
3143
3174
|
print("--from and --to must differ.")
|
|
3144
3175
|
return
|
|
@@ -3603,19 +3634,39 @@ def cmd_execute_withdrawal(pool_name: str, index: int = None, execute: bool = Fa
|
|
|
3603
3634
|
ok = receipt["status"] == 1
|
|
3604
3635
|
if ok:
|
|
3605
3636
|
executed_amount = sum(intents[i][0] for i in ready) / 10 ** cfg["decimals"]
|
|
3606
|
-
_log_withdraw_flow(
|
|
3637
|
+
_log_withdraw_flow(
|
|
3638
|
+
pa, symbol, executed_amount, receipt,
|
|
3639
|
+
token_addr=(None if cfg["native"] else cfg["token"]),
|
|
3640
|
+
to_addr=acct.address, decimals=cfg["decimals"],
|
|
3641
|
+
)
|
|
3607
3642
|
|
|
3608
|
-
def _log_withdraw_flow(account_addr: str, symbol: str, amount: float, receipt
|
|
3643
|
+
def _log_withdraw_flow(account_addr: str, symbol: str, amount: float, receipt,
|
|
3644
|
+
token_addr: str = None, to_addr: str = None,
|
|
3645
|
+
decimals: int = None) -> None:
|
|
3609
3646
|
"""Append a 'withdraw' flow record after a successful executeWithdrawalIntent
|
|
3610
3647
|
broadcast (funds left the account to the EOA). amount is the total executed across
|
|
3611
3648
|
the matured intents. usd_value uses current spot price; None when unpriced. Wrapped
|
|
3612
|
-
so a logging failure can never fail the financial op.
|
|
3649
|
+
so a logging failure can never fail the financial op.
|
|
3650
|
+
|
|
3651
|
+
Prefers the ACTUAL ERC20 Transfer(account -> EOA) amount from the receipt over the
|
|
3652
|
+
committed `amount`, symmetric with the fund path: the executed amount is normally
|
|
3653
|
+
exact, but parsing the receipt keeps the ledger honest if it ever diverges. Native
|
|
3654
|
+
unwraps emit no ERC20 Transfer, so `amount` stands there (token_addr is None)."""
|
|
3613
3655
|
try:
|
|
3656
|
+
actual = amount
|
|
3657
|
+
if token_addr and to_addr and decimals is not None:
|
|
3658
|
+
moved = _flowledger.transferred_amount(
|
|
3659
|
+
receipt, token_addr, account_addr, to_addr, decimals)
|
|
3660
|
+
if moved is not None:
|
|
3661
|
+
if abs(moved - amount) > max(1e-6, abs(amount) * 1e-4):
|
|
3662
|
+
print(f" NOTE withdraw moved {moved} {symbol} of {amount} executed "
|
|
3663
|
+
f"(logging actual)", file=sys.stderr)
|
|
3664
|
+
actual = moved
|
|
3614
3665
|
px = token_price(symbol)
|
|
3615
|
-
usd = round(
|
|
3666
|
+
usd = round(actual * px, 8) if px else None
|
|
3616
3667
|
rec = _flowledger.make_record(
|
|
3617
3668
|
ts=int(time.time()), ftype="withdraw", asset=symbol,
|
|
3618
|
-
token_amount=
|
|
3669
|
+
token_amount=actual, usd_value=usd,
|
|
3619
3670
|
tx=receipt["transactionHash"].hex(), block=receipt["blockNumber"],
|
|
3620
3671
|
source="live-withdraw",
|
|
3621
3672
|
)
|
|
@@ -4247,15 +4298,17 @@ def _aero_use_all_available(
|
|
|
4247
4298
|
print(f" Error reading pool slot0: {e}")
|
|
4248
4299
|
return False
|
|
4249
4300
|
|
|
4250
|
-
# 4.
|
|
4301
|
+
# 4. Build the deploy set. Non-pool assets need a RedStone price >= $5 to be
|
|
4302
|
+
# worth sweeping in. The pool's OWN two tokens always count if held — they go
|
|
4303
|
+
# straight into the LP, priced by the pool tick, not RedStone. Without this, a
|
|
4304
|
+
# pool token with no RedStone feed (e.g. ZORA, priced via BaseOracle TWAP) reads
|
|
4305
|
+
# usd=0 and was silently dropped whenever the OTHER leg (USDC) was priced, so the
|
|
4306
|
+
# held balance never got deployed. (Paraklaudios fix 2026-06-23.)
|
|
4251
4307
|
valuable = {sym: bal for sym, (bal, dec, usd) in inventory.items()
|
|
4252
4308
|
if usd >= MIN_USD_VALUE}
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
if sym in inventory:
|
|
4257
|
-
bal, dec, _ = inventory[sym]
|
|
4258
|
-
valuable[sym] = bal
|
|
4309
|
+
for sym in (sym0, sym1):
|
|
4310
|
+
if sym in inventory and sym not in valuable:
|
|
4311
|
+
valuable[sym] = inventory[sym][0]
|
|
4259
4312
|
if not valuable:
|
|
4260
4313
|
print(" No available assets to deploy.")
|
|
4261
4314
|
return False
|
|
@@ -4395,8 +4448,12 @@ def _aero_use_all_available(
|
|
|
4395
4448
|
ok = _swap_with_usdc_fallback(account, sym, dest_sym, amount_human,
|
|
4396
4449
|
slippage_pct, execute=True)
|
|
4397
4450
|
if not ok:
|
|
4398
|
-
print(f" Swap {sym} -> {dest_sym} failed (direct + USDC 2-hop).
|
|
4399
|
-
|
|
4451
|
+
print(f" Swap {sym} -> {dest_sym} failed (direct + USDC 2-hop). "
|
|
4452
|
+
f"Skipping — minting with current pool-token balances.")
|
|
4453
|
+
# Don't abort just because a small sweep fails. The pool tokens
|
|
4454
|
+
# (ZORA/USDC etc.) are still in the account and the mint can
|
|
4455
|
+
# proceed without converting this non-pool asset. The leftover
|
|
4456
|
+
# will be swept on the next tick.
|
|
4400
4457
|
|
|
4401
4458
|
return (total0_wei, total1_wei, tick_lower, tick_upper, pool_tick)
|
|
4402
4459
|
|
|
@@ -4594,7 +4651,7 @@ def _cmd_aero_add_liquidity_all_available(pool_key, slippage_pct, execute, width
|
|
|
4594
4651
|
)
|
|
4595
4652
|
if not plan:
|
|
4596
4653
|
print(" Swap execution finished but post-swap plan is empty. Aborting mint.")
|
|
4597
|
-
|
|
4654
|
+
sys.exit(2)
|
|
4598
4655
|
total0_wei, total1_wei, tick_lower, tick_upper, pool_tick = plan
|
|
4599
4656
|
|
|
4600
4657
|
# ── Precision balance sweep ──────────────────────────────────
|
|
@@ -4787,6 +4844,10 @@ def _cmd_aero_add_liquidity_all_available(pool_key, slippage_pct, execute, width
|
|
|
4787
4844
|
}
|
|
4788
4845
|
receipt = _sign_and_send(w3, acct, tx, "Add liquidity", timeout=300, fallback_gas=5000000)
|
|
4789
4846
|
ok = receipt["status"] == 1
|
|
4847
|
+
if not ok:
|
|
4848
|
+
print(" ABORT: mint transaction reverted.")
|
|
4849
|
+
sys.exit(2)
|
|
4850
|
+
print(" ✓ Mint confirmed.")
|
|
4790
4851
|
|
|
4791
4852
|
|
|
4792
4853
|
def cmd_aero_rebuild(token_id: int, width_pct: float = 2.0, slippage_pct: float = 1.0,
|
|
@@ -1985,21 +1985,42 @@ def cmd_fund(pool_name: str, amount: float, execute: bool = False):
|
|
|
1985
1985
|
receipt = _sign_and_send(w3, acct, fund_tx, f"Fund {amount} {symbol}", fallback_gas=3000000)
|
|
1986
1986
|
ok = receipt["status"] == 1
|
|
1987
1987
|
if ok:
|
|
1988
|
-
_log_fund_flow(
|
|
1988
|
+
_log_fund_flow(
|
|
1989
|
+
pa, symbol, amount, receipt,
|
|
1990
|
+
token_addr=(None if cfg["native"] else cfg["token"]),
|
|
1991
|
+
from_addr=acct.address, decimals=cfg["decimals"],
|
|
1992
|
+
)
|
|
1989
1993
|
return ok
|
|
1990
1994
|
|
|
1991
1995
|
|
|
1992
|
-
def _log_fund_flow(account_addr: str, symbol: str, amount: float, receipt
|
|
1996
|
+
def _log_fund_flow(account_addr: str, symbol: str, amount: float, receipt,
|
|
1997
|
+
token_addr: str = None, from_addr: str = None,
|
|
1998
|
+
decimals: int = None) -> None:
|
|
1993
1999
|
"""Append a 'deposit' flow record after a successful fund broadcast. asset is the
|
|
1994
2000
|
funded bytes32 symbol (the wrapped-native symbol for native funding, e.g. 'AVAX').
|
|
1995
2001
|
usd_value uses the current spot price (≈ flow-time price); None when unpriced.
|
|
1996
|
-
Wrapped so a logging failure can never fail the financial op.
|
|
2002
|
+
Wrapped so a logging failure can never fail the financial op.
|
|
2003
|
+
|
|
2004
|
+
Logs the ACTUAL ERC20 Transfer(EOA -> account) amount from the receipt, not the
|
|
2005
|
+
requested `amount`: an ERC20 fund() can pull less than asked when the wallet is
|
|
2006
|
+
short (leverage-opens fund mostly from borrow), and logging the request over-counts
|
|
2007
|
+
contribution. Native funding moves the exact msg.value, so `amount` stands there
|
|
2008
|
+
(token_addr is None)."""
|
|
1997
2009
|
try:
|
|
2010
|
+
actual = amount
|
|
2011
|
+
if token_addr and from_addr and decimals is not None:
|
|
2012
|
+
moved = _flowledger.transferred_amount(
|
|
2013
|
+
receipt, token_addr, from_addr, account_addr, decimals)
|
|
2014
|
+
if moved is not None:
|
|
2015
|
+
if abs(moved - amount) > max(1e-6, abs(amount) * 1e-4):
|
|
2016
|
+
print(f" NOTE fund pulled {moved} {symbol} of {amount} requested "
|
|
2017
|
+
f"(logging actual)", file=sys.stderr)
|
|
2018
|
+
actual = moved
|
|
1998
2019
|
px = token_price(symbol)
|
|
1999
|
-
usd = round(
|
|
2020
|
+
usd = round(actual * px, 8) if px else None
|
|
2000
2021
|
rec = _flowledger.make_record(
|
|
2001
2022
|
ts=int(time.time()), ftype="deposit", asset=symbol,
|
|
2002
|
-
token_amount=
|
|
2023
|
+
token_amount=actual, usd_value=usd,
|
|
2003
2024
|
tx=receipt["transactionHash"].hex(), block=receipt["blockNumber"],
|
|
2004
2025
|
source="live-fund",
|
|
2005
2026
|
)
|
|
@@ -3516,20 +3537,40 @@ def cmd_execute_withdrawal(pool_name: str, index: int = None, execute: bool = Fa
|
|
|
3516
3537
|
ok = receipt["status"] == 1
|
|
3517
3538
|
if ok:
|
|
3518
3539
|
executed_amount = sum(intents[i][0] for i in ready) / 10 ** cfg["decimals"]
|
|
3519
|
-
_log_withdraw_flow(
|
|
3540
|
+
_log_withdraw_flow(
|
|
3541
|
+
pa, symbol, executed_amount, receipt,
|
|
3542
|
+
token_addr=(None if cfg["native"] else cfg["token"]),
|
|
3543
|
+
to_addr=acct.address, decimals=cfg["decimals"],
|
|
3544
|
+
)
|
|
3520
3545
|
|
|
3521
3546
|
|
|
3522
|
-
def _log_withdraw_flow(account_addr: str, symbol: str, amount: float, receipt
|
|
3547
|
+
def _log_withdraw_flow(account_addr: str, symbol: str, amount: float, receipt,
|
|
3548
|
+
token_addr: str = None, to_addr: str = None,
|
|
3549
|
+
decimals: int = None) -> None:
|
|
3523
3550
|
"""Append a 'withdraw' flow record after a successful executeWithdrawalIntent
|
|
3524
3551
|
broadcast (funds left the account to the EOA). amount is the total executed across
|
|
3525
3552
|
the matured intents. usd_value uses current spot price; None when unpriced. Wrapped
|
|
3526
|
-
so a logging failure can never fail the financial op.
|
|
3553
|
+
so a logging failure can never fail the financial op.
|
|
3554
|
+
|
|
3555
|
+
Prefers the ACTUAL ERC20 Transfer(account -> EOA) amount from the receipt over the
|
|
3556
|
+
committed `amount`, symmetric with the fund path: the executed amount is normally
|
|
3557
|
+
exact, but parsing the receipt keeps the ledger honest if it ever diverges. Native
|
|
3558
|
+
unwraps emit no ERC20 Transfer, so `amount` stands there (token_addr is None)."""
|
|
3527
3559
|
try:
|
|
3560
|
+
actual = amount
|
|
3561
|
+
if token_addr and to_addr and decimals is not None:
|
|
3562
|
+
moved = _flowledger.transferred_amount(
|
|
3563
|
+
receipt, token_addr, account_addr, to_addr, decimals)
|
|
3564
|
+
if moved is not None:
|
|
3565
|
+
if abs(moved - amount) > max(1e-6, abs(amount) * 1e-4):
|
|
3566
|
+
print(f" NOTE withdraw moved {moved} {symbol} of {amount} executed "
|
|
3567
|
+
f"(logging actual)", file=sys.stderr)
|
|
3568
|
+
actual = moved
|
|
3528
3569
|
px = token_price(symbol)
|
|
3529
|
-
usd = round(
|
|
3570
|
+
usd = round(actual * px, 8) if px else None
|
|
3530
3571
|
rec = _flowledger.make_record(
|
|
3531
3572
|
ts=int(time.time()), ftype="withdraw", asset=symbol,
|
|
3532
|
-
token_amount=
|
|
3573
|
+
token_amount=actual, usd_value=usd,
|
|
3533
3574
|
tx=receipt["transactionHash"].hex(), block=receipt["blockNumber"],
|
|
3534
3575
|
source="live-withdraw",
|
|
3535
3576
|
)
|
|
@@ -910,7 +910,7 @@ def run_tick(
|
|
|
910
910
|
for s in supply_rows:
|
|
911
911
|
sym = s.get("symbol", "")
|
|
912
912
|
usd_val = s.get("usd", 0) or 0
|
|
913
|
-
raw_amt = s.get("amount", s.get("balance", 0)) or 0
|
|
913
|
+
raw_amt = float(s.get("amount", s.get("balance", 0)) or 0)
|
|
914
914
|
if sym.upper() == "USDC" or usd_val < 1 or raw_amt <= 0:
|
|
915
915
|
continue
|
|
916
916
|
swap_candidates.append((sym, usd_val, raw_amt))
|
|
@@ -1053,7 +1053,7 @@ def run_tick(
|
|
|
1053
1053
|
for s in supply_rows2:
|
|
1054
1054
|
sym = s.get("symbol", "")
|
|
1055
1055
|
usd_val = s.get("usd", 0) or 0
|
|
1056
|
-
raw_amt = s.get("amount", s.get("balance", 0)) or 0
|
|
1056
|
+
raw_amt = float(s.get("amount", s.get("balance", 0)) or 0)
|
|
1057
1057
|
if sym.upper() == "USDC" or usd_val < 1 or raw_amt <= 0:
|
|
1058
1058
|
continue
|
|
1059
1059
|
swap_candidates2.append((sym, usd_val, raw_amt))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: primecli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.2
|
|
4
4
|
Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
|
|
5
5
|
Author: Mnemosyne-quest contributors
|
|
6
6
|
License: MIT
|
|
@@ -47,7 +47,7 @@ Built for agent use:
|
|
|
47
47
|
- RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
|
|
48
48
|
- ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
|
|
49
49
|
|
|
50
|
-
**Current version:** 0.10.
|
|
50
|
+
**Current version:** 0.10.2 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
|
|
51
51
|
|
|
52
52
|
> **Breaking change in 0.5.0:** there is no longer a default signing key. Earlier versions silently fell back to a baked-in agent when no key was configured; that fallback has been removed. With no key configured, every command now fails closed with `No signing key found...`. Set a key explicitly (see [Configuration](#configuration)).
|
|
53
53
|
|
|
@@ -19,6 +19,7 @@ tests/test_aero_range_and_swap_fallback.py
|
|
|
19
19
|
tests/test_aero_rebalance.py
|
|
20
20
|
tests/test_bridge.py
|
|
21
21
|
tests/test_cross_file_identity.py
|
|
22
|
+
tests/test_flowledger_transferred_amount.py
|
|
22
23
|
tests/test_gas_limit.py
|
|
23
24
|
tests/test_gas_pricing.py
|
|
24
25
|
tests/test_health_meter.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "primecli"
|
|
7
|
-
version = "0.10.
|
|
7
|
+
version = "0.10.2"
|
|
8
8
|
description = "Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Regression guard for _flowledger.transferred_amount — the receipt-truth parser
|
|
2
|
+
behind the fund/withdraw flow logging fix (2026-06-23).
|
|
3
|
+
|
|
4
|
+
Background: `fund(asset, amount)` can pull LESS than `amount` when the EOA is short
|
|
5
|
+
(a leverage-open funds mostly from borrow, so the wallet only holds dust). The old
|
|
6
|
+
logger recorded the requested `amount` as contribution, inflating the PnL basis and
|
|
7
|
+
corrupting every downstream since-open PnL / ROI / effective-APR. The fix logs the
|
|
8
|
+
ACTUAL on-chain Transfer(EOA -> account) amount instead. These tests pin that parser.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from primecli import _flowledger as fl
|
|
14
|
+
|
|
15
|
+
# Real lowercase addresses from the incident (Base).
|
|
16
|
+
TOKEN = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" # USDC (6 decimals)
|
|
17
|
+
EOA = "0x0218f5b006fd43181018f584ed4be13c356b3428"
|
|
18
|
+
ACCT = "0x150619b111e21f0eac2232ff63f5f0027a47d331"
|
|
19
|
+
OTHER = "0x00000000000000000000000000000000deadbeef"
|
|
20
|
+
|
|
21
|
+
TRANSFER = fl._ERC20_TRANSFER_TOPIC
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _topic_addr(addr: str) -> str:
|
|
25
|
+
"""32-byte left-padded topic encoding of an address."""
|
|
26
|
+
return "0x" + addr.lower().removeprefix("0x").rjust(64, "0")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _transfer_log(token: str, frm: str, to: str, raw: int) -> dict:
|
|
30
|
+
return {
|
|
31
|
+
"address": token,
|
|
32
|
+
"topics": [TRANSFER, _topic_addr(frm), _topic_addr(to)],
|
|
33
|
+
"data": "0x" + format(raw, "064x"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _receipt(logs: list[dict]) -> dict:
|
|
38
|
+
return {"logs": logs}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_real_transfer_returns_actual_amount():
|
|
42
|
+
# 199.9 USDC genuinely funded -> parser returns 199.9, not whatever was requested.
|
|
43
|
+
rcpt = _receipt([_transfer_log(TOKEN, EOA, ACCT, 199_900_000)])
|
|
44
|
+
assert fl.transferred_amount(rcpt, TOKEN, EOA, ACCT, 6) == 199.9
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_partial_pull_returns_dust_not_requested():
|
|
48
|
+
# The ZORA phantom: fund(132 USDC) but only 0.08425 actually moved.
|
|
49
|
+
rcpt = _receipt([_transfer_log(TOKEN, EOA, ACCT, 84_250)])
|
|
50
|
+
assert fl.transferred_amount(rcpt, TOKEN, EOA, ACCT, 6) == 84_250 / 1e6
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_zero_value_transfer_returns_zero_not_none():
|
|
54
|
+
# A 0-value Transfer is still a Transfer: the fund pulled nothing -> 0.0, not None.
|
|
55
|
+
rcpt = _receipt([_transfer_log(TOKEN, EOA, ACCT, 0)])
|
|
56
|
+
assert fl.transferred_amount(rcpt, TOKEN, EOA, ACCT, 6) == 0.0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_no_matching_transfer_returns_none():
|
|
60
|
+
# No Transfer to the account at all -> None, so the caller falls back to the request.
|
|
61
|
+
rcpt = _receipt([_transfer_log(TOKEN, EOA, OTHER, 100_000_000)])
|
|
62
|
+
assert fl.transferred_amount(rcpt, TOKEN, EOA, ACCT, 6) is None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_wrong_token_ignored():
|
|
66
|
+
other_token = "0x4200000000000000000000000000000000000006" # WETH
|
|
67
|
+
rcpt = _receipt([_transfer_log(other_token, EOA, ACCT, 5_000_000)])
|
|
68
|
+
assert fl.transferred_amount(rcpt, TOKEN, EOA, ACCT, 6) is None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_sums_multiple_matching_transfers():
|
|
72
|
+
rcpt = _receipt([
|
|
73
|
+
_transfer_log(TOKEN, EOA, ACCT, 10_000_000),
|
|
74
|
+
_transfer_log(TOKEN, EOA, ACCT, 25_000_000),
|
|
75
|
+
])
|
|
76
|
+
assert fl.transferred_amount(rcpt, TOKEN, EOA, ACCT, 6) == 35.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_direction_matters_for_withdraw():
|
|
80
|
+
# Withdraw is account -> EOA. A fund-direction parse must not pick it up.
|
|
81
|
+
rcpt = _receipt([_transfer_log(TOKEN, ACCT, EOA, 50_000_000)])
|
|
82
|
+
assert fl.transferred_amount(rcpt, TOKEN, EOA, ACCT, 6) is None
|
|
83
|
+
assert fl.transferred_amount(rcpt, TOKEN, ACCT, EOA, 6) == 50.0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_checksum_and_hexbytes_inputs_normalise():
|
|
87
|
+
class _HB(bytes):
|
|
88
|
+
def hex(self):
|
|
89
|
+
return super().hex()
|
|
90
|
+
|
|
91
|
+
raw_topics = [
|
|
92
|
+
_HB(bytes.fromhex(TRANSFER.removeprefix("0x"))),
|
|
93
|
+
_HB(bytes.fromhex(_topic_addr(EOA).removeprefix("0x"))),
|
|
94
|
+
_HB(bytes.fromhex(_topic_addr(ACCT).removeprefix("0x"))),
|
|
95
|
+
]
|
|
96
|
+
rcpt = {"logs": [{
|
|
97
|
+
"address": _HB(bytes.fromhex(TOKEN.removeprefix("0x"))),
|
|
98
|
+
"topics": raw_topics,
|
|
99
|
+
"data": _HB((75_000_000).to_bytes(32, "big")),
|
|
100
|
+
}]}
|
|
101
|
+
# Pass checksum-style mixed-case addresses to confirm normalisation.
|
|
102
|
+
assert fl.transferred_amount(rcpt, TOKEN.upper(), EOA, ACCT, 6) == 75.0
|
|
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
|