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.
Files changed (31) hide show
  1. {primecli-0.10.1 → primecli-0.10.2}/PKG-INFO +2 -2
  2. {primecli-0.10.1 → primecli-0.10.2}/README.md +1 -1
  3. {primecli-0.10.1 → primecli-0.10.2}/primecli/_flowledger.py +60 -0
  4. {primecli-0.10.1 → primecli-0.10.2}/primecli/arbprime.py +51 -10
  5. {primecli-0.10.1 → primecli-0.10.2}/primecli/degenprime.py +82 -21
  6. {primecli-0.10.1 → primecli-0.10.2}/primecli/deltaprime.py +51 -10
  7. {primecli-0.10.1 → primecli-0.10.2}/primecli/health_monitor.py +2 -2
  8. {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/PKG-INFO +2 -2
  9. {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/SOURCES.txt +1 -0
  10. {primecli-0.10.1 → primecli-0.10.2}/pyproject.toml +1 -1
  11. primecli-0.10.2/tests/test_flowledger_transferred_amount.py +102 -0
  12. {primecli-0.10.1 → primecli-0.10.2}/LICENSE +0 -0
  13. {primecli-0.10.1 → primecli-0.10.2}/primecli/__init__.py +0 -0
  14. {primecli-0.10.1 → primecli-0.10.2}/primecli/_wallets.py +0 -0
  15. {primecli-0.10.1 → primecli-0.10.2}/primecli/bridge.py +0 -0
  16. {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/dependency_links.txt +0 -0
  17. {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/entry_points.txt +0 -0
  18. {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/requires.txt +0 -0
  19. {primecli-0.10.1 → primecli-0.10.2}/primecli.egg-info/top_level.txt +0 -0
  20. {primecli-0.10.1 → primecli-0.10.2}/setup.cfg +0 -0
  21. {primecli-0.10.1 → primecli-0.10.2}/tests/test_aero_range_and_swap_fallback.py +0 -0
  22. {primecli-0.10.1 → primecli-0.10.2}/tests/test_aero_rebalance.py +0 -0
  23. {primecli-0.10.1 → primecli-0.10.2}/tests/test_bridge.py +0 -0
  24. {primecli-0.10.1 → primecli-0.10.2}/tests/test_cross_file_identity.py +0 -0
  25. {primecli-0.10.1 → primecli-0.10.2}/tests/test_gas_limit.py +0 -0
  26. {primecli-0.10.1 → primecli-0.10.2}/tests/test_gas_pricing.py +0 -0
  27. {primecli-0.10.1 → primecli-0.10.2}/tests/test_health_meter.py +0 -0
  28. {primecli-0.10.1 → primecli-0.10.2}/tests/test_health_monitor.py +0 -0
  29. {primecli-0.10.1 → primecli-0.10.2}/tests/test_paraswap_validator.py +0 -0
  30. {primecli-0.10.1 → primecli-0.10.2}/tests/test_redstone_encoding.py +0 -0
  31. {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.1
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.1 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
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.1 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
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(pa, symbol, amount, receipt)
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) -> None:
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(amount * px, 8) if px else None
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=amount, usd_value=usd,
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(pa, symbol, executed_amount, receipt)
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) -> None:
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(amount * px, 8) if px else None
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=amount, usd_value=usd,
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(pa, symbol, amount, receipt)
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) -> None:
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(amount * px, 8) if px else None
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=amount, usd_value=usd,
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
- from_sym, to_sym = from_sym.upper(), to_sym.upper()
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(pa, symbol, executed_amount, receipt)
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) -> None:
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(amount * px, 8) if px else None
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=amount, usd_value=usd,
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. Filter inventory > $5; fallback for pool tokens
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
- if not valuable:
4254
- print(" No assets have RedStone prices above $5. Falling back to raw pool-token balances.")
4255
- for sym in (sym0, sym1):
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). Aborting.")
4399
- return False
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
- return
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(pa, symbol, amount, receipt)
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) -> None:
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(amount * px, 8) if px else None
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=amount, usd_value=usd,
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(pa, symbol, executed_amount, receipt)
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) -> None:
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(amount * px, 8) if px else None
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=amount, usd_value=usd,
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.1
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.1 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
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.1"
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