primecli 0.7.2__tar.gz → 0.7.3__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 (24) hide show
  1. {primecli-0.7.2 → primecli-0.7.3}/PKG-INFO +2 -2
  2. {primecli-0.7.2 → primecli-0.7.3}/README.md +1 -1
  3. {primecli-0.7.2 → primecli-0.7.3}/primecli/arbprime.py +122 -188
  4. {primecli-0.7.2 → primecli-0.7.3}/primecli/degenprime.py +96 -106
  5. {primecli-0.7.2 → primecli-0.7.3}/primecli/deltaprime.py +123 -194
  6. {primecli-0.7.2 → primecli-0.7.3}/primecli.egg-info/PKG-INFO +2 -2
  7. {primecli-0.7.2 → primecli-0.7.3}/primecli.egg-info/SOURCES.txt +1 -0
  8. {primecli-0.7.2 → primecli-0.7.3}/pyproject.toml +1 -1
  9. {primecli-0.7.2 → primecli-0.7.3}/tests/test_cross_file_identity.py +1 -0
  10. primecli-0.7.3/tests/test_gas_limit.py +151 -0
  11. {primecli-0.7.2 → primecli-0.7.3}/LICENSE +0 -0
  12. {primecli-0.7.2 → primecli-0.7.3}/primecli/__init__.py +0 -0
  13. {primecli-0.7.2 → primecli-0.7.3}/primecli/health_monitor.py +0 -0
  14. {primecli-0.7.2 → primecli-0.7.3}/primecli.egg-info/dependency_links.txt +0 -0
  15. {primecli-0.7.2 → primecli-0.7.3}/primecli.egg-info/entry_points.txt +0 -0
  16. {primecli-0.7.2 → primecli-0.7.3}/primecli.egg-info/requires.txt +0 -0
  17. {primecli-0.7.2 → primecli-0.7.3}/primecli.egg-info/top_level.txt +0 -0
  18. {primecli-0.7.2 → primecli-0.7.3}/setup.cfg +0 -0
  19. {primecli-0.7.2 → primecli-0.7.3}/tests/test_gas_pricing.py +0 -0
  20. {primecli-0.7.2 → primecli-0.7.3}/tests/test_health_meter.py +0 -0
  21. {primecli-0.7.2 → primecli-0.7.3}/tests/test_health_monitor.py +0 -0
  22. {primecli-0.7.2 → primecli-0.7.3}/tests/test_paraswap_validator.py +0 -0
  23. {primecli-0.7.2 → primecli-0.7.3}/tests/test_redstone_encoding.py +0 -0
  24. {primecli-0.7.2 → primecli-0.7.3}/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.7.2
3
+ Version: 0.7.3
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.7.0 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.7.3 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.7.0 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.7.3 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
 
@@ -692,6 +692,20 @@ def _set_gas_price(w3, tx_dict):
692
692
  # Legacy chain — use gasPrice instead
693
693
  tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
694
694
 
695
+ def _estimate_gas_limit(w3, tx_dict, fallback_gas: int, buffer_bps: int = 1250) -> int:
696
+ """Estimate gas for final calldata and add a buffer.
697
+
698
+ Solvency-gated swap paths append RedStone payloads and can vary materially by
699
+ route. A fixed cap can pass simulation at a high gas allowance, then revert
700
+ out-of-gas on broadcast. If the RPC cannot estimate, keep the old fixed cap.
701
+ """
702
+ try:
703
+ call_tx = {k: tx_dict[k] for k in ("from", "to", "data", "value") if k in tx_dict}
704
+ estimated = int(w3.eth.estimate_gas(call_tx))
705
+ return max(int(fallback_gas), (estimated * int(buffer_bps) + 999) // 1000)
706
+ except Exception:
707
+ return int(fallback_gas)
708
+
695
709
  def _set_gas_price_for(chain_id, w3, tx_dict):
696
710
  """Set gas fields for an EXPLICIT chain_id rather than the module CHAIN_ID. Needed by
697
711
  cross-chain flows (prime-bridge) where a tx may target Avalanche or Arbitrum regardless
@@ -732,6 +746,64 @@ def _agent_key(agent):
732
746
  raise RuntimeError(f"{var} not found in {path} (agent '{agent}').")
733
747
  return key
734
748
 
749
+ def _sign_and_send(w3, acct, tx, label, timeout=180, fallback_gas=3000000, buffer_bps=1250, gas_price_fn=None):
750
+ """Sign, send, wait for a tx with gas estimation + OOG retry + error surfacing.
751
+
752
+ Always estimates gas from final calldata (incl. RedStone payload) then adds a
753
+ buffer. If the tx fails with status=0 and gasUsed == gasLimit (out of gas),
754
+ retries once with 50% more buffer. Surfaces the gas stats on any failure.
755
+
756
+ Gas limit override logic:
757
+ - If tx dict has a non-None "gas" key, use that as the starting fallback_gas
758
+ (removes the dict key so estimation is authoritative).
759
+ This lets callers set a minimum via fallback_gas without hardcoding the
760
+ broadcast limit.
761
+ - Always runs _estimate_gas_limit to compute the final cap.
762
+ - Ignores any "gas" key set in the tx dict before calling this function.
763
+
764
+ Returns the receipt (status 0 or 1). Prints status and tx link.
765
+ """
766
+ # If caller left a stale gas value in the dict, discard it — estimation is authoritative
767
+ tx.pop("gas", None)
768
+ tx["gas"] = _estimate_gas_limit(w3, tx, fallback_gas, buffer_bps)
769
+ if gas_price_fn:
770
+ gas_price_fn(w3, tx)
771
+ else:
772
+ _set_gas_price(w3, tx)
773
+ signed = acct.sign_transaction(tx)
774
+ tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
775
+ receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
776
+ ok = receipt["status"] == 1
777
+ if ok:
778
+ print(f"{'✓'} {label} confirmed")
779
+ print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
780
+ return receipt
781
+
782
+ # Failure analysis
783
+ gas_used = receipt.get("gasUsed", 0)
784
+ gas_limit = tx.get("gas", 1)
785
+ is_oog = gas_used >= gas_limit
786
+ print(f"{'✗'} {label} failed (gasUsed={gas_used:,} / limit={gas_limit:,})")
787
+ if is_oog:
788
+ new_bps = buffer_bps * 3 // 2
789
+ print(f" Out of gas. Retrying with {new_bps//10}% buffer...")
790
+ tx["nonce"] = w3.eth.get_transaction_count(acct.address)
791
+ tx.pop("gas", None)
792
+ tx["gas"] = _estimate_gas_limit(w3, tx, int(fallback_gas * 1.5), new_bps)
793
+ signed = acct.sign_transaction(tx)
794
+ tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
795
+ receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
796
+ ok = receipt["status"] == 1
797
+ if ok:
798
+ print(f"{'✓'} {label} confirmed on retry")
799
+ print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
800
+ else:
801
+ print(f"{'✗'} {label} failed again on retry")
802
+ return receipt
803
+ print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
804
+ return receipt
805
+
806
+
735
807
  def resolve_private_key():
736
808
  # The same EVM key works on every chain, so each ARBPRIME_ var falls back to its
737
809
  # DELTAPRIME_ equivalent (exactly how degenprime falls back to DELTAPRIME_*).
@@ -1451,12 +1523,11 @@ def cmd_deposit(pool_name: str, amount: float, execute: bool = False):
1451
1523
  # Gas: the native path wraps ETH→WETH + does the pool accounting +
1452
1524
  # internal rate update, so it needs more than the ERC20 branch's 200k.
1453
1525
  # 500k clears it cleanly.
1454
- tx = contract.functions.depositNativeToken().build_transaction({
1526
+ dep_tx = contract.functions.depositNativeToken().build_transaction({
1455
1527
  "from": acct.address, "nonce": w3.eth.get_transaction_count(acct.address),
1456
1528
  "gas": 500000, "chainId": CHAIN_ID, "value": amount_wei,
1457
1529
  })
1458
- _set_gas_price(w3, tx)
1459
- signed = acct.sign_transaction(tx)
1530
+ receipt = _sign_and_send(w3, acct, dep_tx, "Deposit (native)", timeout=120, fallback_gas=500000)
1460
1531
  else:
1461
1532
  # Approve
1462
1533
  token = w3.eth.contract(address=Web3.to_checksum_address(cfg["token"]),
@@ -1475,14 +1546,8 @@ def cmd_deposit(pool_name: str, amount: float, execute: bool = False):
1475
1546
  "from": acct.address, "nonce": _dep_nonce + 1,
1476
1547
  "gas": 400000, "chainId": CHAIN_ID,
1477
1548
  })
1478
- _set_gas_price(w3, dep_tx)
1479
- signed = acct.sign_transaction(dep_tx)
1480
-
1481
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
1482
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
1549
+ receipt = _sign_and_send(w3, acct, dep_tx, f"Deposit {amount} {cfg['symbol']}", timeout=120, fallback_gas=400000)
1483
1550
  ok = receipt["status"] == 1
1484
- print(f"{'✓' if ok else '✗'} Deposit {amount} {cfg['symbol']} {'confirmed' if ok else 'failed'}")
1485
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
1486
1551
 
1487
1552
  def cmd_withdraw(pool_name: str, amount: float, execute: bool = False):
1488
1553
  """Pool-side (LENDER) withdraw — step 1 of a 24h delayed flow.
@@ -1522,13 +1587,8 @@ def cmd_withdraw(pool_name: str, amount: float, execute: bool = False):
1522
1587
  "from": acct.address, "nonce": w3.eth.get_transaction_count(acct.address),
1523
1588
  "gas": 400000, "chainId": CHAIN_ID,
1524
1589
  })
1525
- _set_gas_price(w3, tx)
1526
- signed = acct.sign_transaction(tx)
1527
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
1528
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
1590
+ receipt = _sign_and_send(w3, acct, tx, "Lender withdrawal intent", fallback_gas=400000)
1529
1591
  ok = receipt["status"] == 1
1530
- print(f"{'✓' if ok else '✗'} Lender withdrawal intent {'registered' if ok else 'failed'}")
1531
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
1532
1592
 
1533
1593
  def cmd_withdrawal_requests():
1534
1594
  """Read-only: list pending lender withdrawal intents per pool, with current deposit.
@@ -1648,13 +1708,8 @@ def cmd_execute_withdrawal_request(pool_name: str, index: int = None, execute: b
1648
1708
  "gas": 600000, "chainId": CHAIN_ID,
1649
1709
  "data": data,
1650
1710
  }
1651
- _set_gas_price(w3, tx)
1652
- signed = acct.sign_transaction(tx)
1653
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
1654
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
1711
+ receipt = _sign_and_send(w3, acct, tx, "Execute lender withdrawal", fallback_gas=600000)
1655
1712
  ok = receipt["status"] == 1
1656
- print(f"{'✓' if ok else '✗'} Execute lender withdrawal {'confirmed' if ok else 'failed'}")
1657
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
1658
1713
 
1659
1714
  def cmd_cancel_withdrawal_request(pool_name: str, index: int, execute: bool = False):
1660
1715
  """Cancel a pending lender withdrawal intent on the pool via
@@ -1687,13 +1742,8 @@ def cmd_cancel_withdrawal_request(pool_name: str, index: int, execute: bool = Fa
1687
1742
  "from": acct.address, "nonce": w3.eth.get_transaction_count(acct.address),
1688
1743
  "gas": 300000, "chainId": CHAIN_ID,
1689
1744
  })
1690
- _set_gas_price(w3, tx)
1691
- signed = acct.sign_transaction(tx)
1692
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
1693
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
1745
+ receipt = _sign_and_send(w3, acct, tx, "Cancel lender withdrawal intent", fallback_gas=300000)
1694
1746
  ok = receipt["status"] == 1
1695
- print(f"{'✓' if ok else '✗'} Cancel lender withdrawal intent {'confirmed' if ok else 'failed'}")
1696
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
1697
1747
 
1698
1748
  # ─── Prime Account commands ──────────────────────────────────────────────────
1699
1749
 
@@ -1769,14 +1819,9 @@ def cmd_create_prime_account(execute: bool = False, fund_pool: str = None, fund_
1769
1819
  "from": acct.address, "nonce": w3.eth.get_transaction_count(acct.address),
1770
1820
  "gas": 4000000, "chainId": CHAIN_ID,
1771
1821
  })
1772
- _set_gas_price(w3, tx)
1773
- signed = acct.sign_transaction(tx)
1774
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
1775
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
1776
- ok = receipt["status"] == 1
1777
1822
  label = "Create+fund Prime Account" if funding else "Create Prime Account"
1778
- print(f"{'✓' if ok else '✗'} {label} {'confirmed' if ok else 'failed'}")
1779
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
1823
+ receipt = _sign_and_send(w3, acct, tx, label, fallback_gas=4000000)
1824
+ ok = receipt["status"] == 1
1780
1825
  if ok:
1781
1826
  # getLoanForOwner can lag a beat behind the receipt; poll briefly so we
1782
1827
  # print the new account address instead of None right after creation.
@@ -1824,11 +1869,10 @@ def cmd_fund(pool_name: str, amount: float, execute: bool = False):
1824
1869
 
1825
1870
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
1826
1871
  if cfg["native"]:
1827
- tx = account.functions.depositNativeToken().build_transaction({
1872
+ fund_tx = account.functions.depositNativeToken().build_transaction({
1828
1873
  "from": acct.address, "nonce": w3.eth.get_transaction_count(acct.address),
1829
1874
  "gas": 3000000, "chainId": CHAIN_ID, "value": amount_wei,
1830
1875
  })
1831
- _set_gas_price(w3, tx)
1832
1876
  signed = acct.sign_transaction(tx)
1833
1877
  else:
1834
1878
  token = w3.eth.contract(address=Web3.to_checksum_address(cfg["token"]),
@@ -1848,15 +1892,9 @@ def cmd_fund(pool_name: str, amount: float, execute: bool = False):
1848
1892
  "from": acct.address, "nonce": _fund_nonce,
1849
1893
  "gas": 3000000, "chainId": CHAIN_ID,
1850
1894
  })
1851
- _set_gas_price(w3, fund_tx)
1852
- signed = acct.sign_transaction(fund_tx)
1853
-
1854
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
1855
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
1856
- ok = receipt["status"] == 1
1857
- print(f"{'✓' if ok else '✗'} Fund {amount} {symbol} {'confirmed' if ok else 'failed'}")
1858
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
1859
- return ok
1895
+ receipt = _sign_and_send(w3, acct, fund_tx, f"Fund {amount} {symbol}", fallback_gas=3000000)
1896
+ ok = receipt["status"] == 1
1897
+ return ok
1860
1898
 
1861
1899
  def _prices_usd(w3, account, symbols: list, payload: bytes) -> dict:
1862
1900
  """Best-effort per-symbol USD price map via the RedStone-gated getPrices view (1e8-scaled).
@@ -2276,15 +2314,10 @@ def cmd_borrow(pool_name: str, amount: float, execute: bool = False):
2276
2314
  tx = {
2277
2315
  "from": acct.address, "to": pa_cs, "data": data,
2278
2316
  "nonce": w3.eth.get_transaction_count(acct.address),
2279
- "gas": 4000000, "chainId": CHAIN_ID,
2317
+ "chainId": CHAIN_ID,
2280
2318
  }
2281
- _set_gas_price(w3, tx)
2282
- signed = acct.sign_transaction(tx)
2283
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
2284
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
2319
+ receipt = _sign_and_send(w3, acct, tx, f"Borrow {amount} {symbol}", fallback_gas=4000000)
2285
2320
  ok = receipt["status"] == 1
2286
- print(f"{'✓' if ok else '✗'} Borrow {amount} {symbol} {'confirmed' if ok else 'failed'}")
2287
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
2288
2321
  return ok
2289
2322
 
2290
2323
  def cmd_repay(pool_name: str, amount: float, execute: bool = False):
@@ -2365,14 +2398,9 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
2365
2398
  "nonce": w3.eth.get_transaction_count(acct.address),
2366
2399
  "gas": 4000000, "chainId": CHAIN_ID,
2367
2400
  }
2368
- _set_gas_price(w3, tx)
2369
- signed = acct.sign_transaction(tx)
2370
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
2371
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
2401
+ receipt = _sign_and_send(w3, acct, tx, f"Repay {amount_wei / 10**cfg['decimals']:.6f} {symbol}", fallback_gas=4000000)
2372
2402
  ok = receipt["status"] == 1
2373
2403
  repaid = amount_wei / 10**cfg['decimals']
2374
- print(f"{'✓' if ok else '✗'} Repay {repaid:.6f} {symbol} {'confirmed' if ok else 'failed'}")
2375
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
2376
2404
  if not ok:
2377
2405
  _print_revert_reason(w3, tx, receipt)
2378
2406
 
@@ -2612,16 +2640,10 @@ def _swap_via_paraswap(w3, acct, pa_cs, account, from_sym, to_sym, from_cfg, to_
2612
2640
  tx = {
2613
2641
  "from": acct.address, "to": pa_cs, "data": data,
2614
2642
  "nonce": w3.eth.get_transaction_count(acct.address),
2615
- "gas": 3000000, "chainId": CHAIN_ID,
2643
+ "chainId": CHAIN_ID,
2616
2644
  }
2617
- _set_gas_price(w3, tx)
2618
- signed = acct.sign_transaction(tx)
2619
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
2620
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
2645
+ receipt = _sign_and_send(w3, acct, tx, f"Swap {amount} {from_sym} -> {to_sym}", fallback_gas=3000000)
2621
2646
  ok = receipt["status"] == 1
2622
- print(f"{'✓' if ok else '✗'} Swap {amount} {from_sym} -> {to_sym} "
2623
- f"{'confirmed' if ok else 'failed'}")
2624
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
2625
2647
  return ok
2626
2648
 
2627
2649
  def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.0,
@@ -2721,16 +2743,10 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
2721
2743
  tx = {
2722
2744
  "from": acct.address, "to": pa_cs, "data": data,
2723
2745
  "nonce": w3.eth.get_transaction_count(acct.address),
2724
- "gas": 3000000, "chainId": CHAIN_ID,
2746
+ "chainId": CHAIN_ID,
2725
2747
  }
2726
- _set_gas_price(w3, tx)
2727
- signed = acct.sign_transaction(tx)
2728
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
2729
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
2748
+ receipt = _sign_and_send(w3, acct, tx, f"Swap {amount} {from_sym} -> {to_sym}", fallback_gas=3000000)
2730
2749
  ok = receipt["status"] == 1
2731
- print(f"{'✓' if ok else '✗'} Swap {amount} {from_sym} -> {to_sym} "
2732
- f"{'confirmed' if ok else 'failed'}")
2733
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
2734
2750
  return ok
2735
2751
 
2736
2752
  # ─── Swap debt / refinance (SwapDebtFacet) ───────────────────────────────────
@@ -2909,18 +2925,13 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2909
2925
  tx = {
2910
2926
  "from": acct.address, "to": pa_cs, "data": base_borrow + exec_payload.hex(),
2911
2927
  "nonce": w3.eth.get_transaction_count(acct.address),
2912
- "gas": 4000000, "chainId": CHAIN_ID,
2928
+ "chainId": CHAIN_ID,
2913
2929
  }
2914
- _set_gas_price(w3, tx)
2915
- signed = acct.sign_transaction(tx)
2916
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
2917
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
2930
+ receipt = _sign_and_send(w3, acct, tx, "Borrow (swap-debt fallback)", fallback_gas=4000000)
2918
2931
  if receipt["status"] != 1:
2919
2932
  print(f" ✗ Borrow failed — aborting fallback sequence.")
2920
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
2921
2933
  return
2922
- print(f" ✓ Borrowed {borrow_amount / 10**to_cfg['decimals']:.6f} {to_sym}")
2923
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
2934
+
2924
2935
 
2925
2936
  # Re-check health after borrow (fresh payload).
2926
2937
  fresh_payload = build_redstone_payload(prime_account_price_feeds(account))
@@ -2952,15 +2963,11 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2952
2963
  tx2 = {
2953
2964
  "from": acct.address, "to": pa_cs, "data": base_swap + swap_payload2.hex(),
2954
2965
  "nonce": w3.eth.get_transaction_count(acct.address),
2955
- "gas": 3000000, "chainId": CHAIN_ID,
2966
+ "chainId": CHAIN_ID,
2956
2967
  }
2957
- _set_gas_price(w3, tx2)
2958
- signed2 = acct.sign_transaction(tx2)
2959
- tx_hash2 = w3.eth.send_raw_transaction(signed2.raw_transaction)
2960
- receipt2 = w3.eth.wait_for_transaction_receipt(tx_hash2, timeout=180)
2968
+ receipt2 = _sign_and_send(w3, acct, tx2, "Swap (swap-debt fallback)", fallback_gas=3000000)
2961
2969
  if receipt2["status"] != 1:
2962
2970
  print(f" ✗ Swap failed — aborting fallback sequence.")
2963
- print(f" Tx: {EXPLORER}/tx/{tx_hash2.hex()}")
2964
2971
  print(f" Steps completed: 1 (borrow). You may need to manually swap & repay.")
2965
2972
  return
2966
2973
  print(f" ✓ Swapped {to_sym} -> {from_sym}")
@@ -2990,15 +2997,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
2990
2997
  tx3 = {
2991
2998
  "from": acct.address, "to": pa_cs, "data": base_reply + repay_payload3.hex(),
2992
2999
  "nonce": w3.eth.get_transaction_count(acct.address),
2993
- "gas": 4000000, "chainId": CHAIN_ID,
3000
+ "chainId": CHAIN_ID,
2994
3001
  }
2995
- _set_gas_price(w3, tx3)
2996
- signed3 = acct.sign_transaction(tx3)
2997
- tx_hash3 = w3.eth.send_raw_transaction(signed3.raw_transaction)
2998
- receipt3 = w3.eth.wait_for_transaction_receipt(tx_hash3, timeout=180)
3002
+ receipt3 = _sign_and_send(w3, acct, tx3, f"Repaid (swap-debt fallback)", fallback_gas=4000000)
2999
3003
  ok3 = receipt3["status"] == 1
3000
- print(f"{' ✓' if ok3 else ' ✗'} Repaid {actual_reply/10**from_cfg['decimals']:.6f} {from_sym} {'confirmed' if ok3 else 'failed'}")
3001
- print(f" Tx: {EXPLORER}/tx/{tx_hash3.hex()}")
3002
3004
  if not ok3:
3003
3005
  print(f" Steps completed: 1 (borrow), 2 (swap). Repay failed — check manually.")
3004
3006
  else:
@@ -3097,15 +3099,10 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
3097
3099
  tx = {
3098
3100
  "from": acct.address, "to": pa_cs, "data": data,
3099
3101
  "nonce": w3.eth.get_transaction_count(acct.address),
3100
- "gas": 4000000, "chainId": CHAIN_ID,
3102
+ "chainId": CHAIN_ID,
3101
3103
  }
3102
- _set_gas_price(w3, tx)
3103
- signed = acct.sign_transaction(tx)
3104
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
3105
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
3104
+ receipt = _sign_and_send(w3, acct, tx, f"Swap debt {from_sym} -> {to_sym}", fallback_gas=4000000)
3106
3105
  ok = receipt["status"] == 1
3107
- print(f"{'✓' if ok else '✗'} Swap debt {from_sym} -> {to_sym} {'confirmed' if ok else 'failed'}")
3108
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
3109
3106
 
3110
3107
  # ─── Collateral withdrawal (WithdrawalIntentFacet) ──────────────────────────
3111
3108
  # Pulling collateral out of the Prime Account to the EOA is a two-step, time-delayed
@@ -3216,13 +3213,8 @@ def cmd_cancel_withdrawal(pool_name: str, index: int, execute: bool = False):
3216
3213
  "from": acct.address, "nonce": w3.eth.get_transaction_count(acct.address),
3217
3214
  "gas": 500000, "chainId": CHAIN_ID,
3218
3215
  })
3219
- _set_gas_price(w3, tx)
3220
- signed = acct.sign_transaction(tx)
3221
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
3222
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
3216
+ receipt = _sign_and_send(w3, acct, tx, f"Cancel withdrawal [{index}]", timeout=120, fallback_gas=1000000)
3223
3217
  ok = receipt["status"] == 1
3224
- print(f"{'✓' if ok else '✗'} Withdrawal intent [{index}] {'cancelled' if ok else 'failed'}")
3225
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
3226
3218
 
3227
3219
 
3228
3220
  def cmd_withdrawal_intents():
@@ -3337,13 +3329,8 @@ def cmd_execute_withdrawal(pool_name: str, index: int = None, execute: bool = Fa
3337
3329
  "nonce": w3.eth.get_transaction_count(acct.address),
3338
3330
  "gas": 3000000, "chainId": CHAIN_ID,
3339
3331
  }
3340
- _set_gas_price(w3, tx)
3341
- signed = acct.sign_transaction(tx)
3342
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
3343
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
3332
+ receipt = _sign_and_send(w3, acct, tx, "Execute withdrawal", fallback_gas=3000000)
3344
3333
  ok = receipt["status"] == 1
3345
- print(f"{'✓' if ok else '✗'} Execute withdrawal {'confirmed' if ok else 'failed'}")
3346
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
3347
3334
 
3348
3335
  # ─── GMX V2 GM / GM+ LP (GmxV2FacetArbitrum / GmxV2PlusFacetArbitrum) ─────────
3349
3336
  # GM tokens are GMX V2 market LP (two-sided long+short for GM, single-sided for GM+).
@@ -3709,13 +3696,8 @@ def cmd_gmx_deposit(market: str, amount: float, is_long: bool | None = None,
3709
3696
  "nonce": w3.eth.get_transaction_count(acct.address),
3710
3697
  "gas": 5000000, "chainId": CHAIN_ID,
3711
3698
  }
3712
- _set_gas_price(w3, tx)
3713
- signed = acct.sign_transaction(tx)
3714
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
3715
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
3699
+ receipt = _sign_and_send(w3, acct, tx, f"GMX {kind} deposit request", fallback_gas=5000000)
3716
3700
  ok = receipt["status"] == 1
3717
- print(f"{'✓' if ok else '✗'} GMX {kind} deposit request {'submitted' if ok else 'failed'}")
3718
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
3719
3701
  if ok:
3720
3702
  print(" Request queued — wait for the GMX keeper callback to mint the GM tokens.")
3721
3703
  return ok
@@ -3843,13 +3825,8 @@ def cmd_gmx_withdraw(market: str, amount: float, slippage_pct: float = 1.0,
3843
3825
  "nonce": w3.eth.get_transaction_count(acct.address),
3844
3826
  "gas": 5000000, "chainId": CHAIN_ID,
3845
3827
  }
3846
- _set_gas_price(w3, tx)
3847
- signed = acct.sign_transaction(tx)
3848
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
3849
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
3828
+ receipt = _sign_and_send(w3, acct, tx, f"GMX {kind} withdrawal request", fallback_gas=5000000)
3850
3829
  ok = receipt["status"] == 1
3851
- print(f"{'✓' if ok else '✗'} GMX {kind} withdrawal request {'submitted' if ok else 'failed'}")
3852
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
3853
3830
  if ok:
3854
3831
  print(" Request queued — wait for the GMX keeper callback to return the underlying(s).")
3855
3832
 
@@ -4069,13 +4046,8 @@ def cmd_glv_deposit(vault_key: str, amount: float, is_long: bool | None = None,
4069
4046
  "nonce": w3.eth.get_transaction_count(acct.address),
4070
4047
  "gas": 5000000, "chainId": CHAIN_ID,
4071
4048
  }
4072
- _set_gas_price(w3, tx)
4073
- signed = acct.sign_transaction(tx)
4074
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
4075
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
4049
+ receipt = _sign_and_send(w3, acct, tx, "GLV deposit request", fallback_gas=5000000)
4076
4050
  ok = receipt["status"] == 1
4077
- print(f"{'✓' if ok else '✗'} GLV deposit request {'submitted' if ok else 'failed'}")
4078
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
4079
4051
  if ok:
4080
4052
  print(" Request queued — wait for the GMX keeper callback to mint the GLV tokens.")
4081
4053
  return ok
@@ -4190,13 +4162,8 @@ def cmd_glv_withdraw(vault_key: str, amount: float, target_market: str = None,
4190
4162
  "nonce": w3.eth.get_transaction_count(acct.address),
4191
4163
  "gas": 5000000, "chainId": CHAIN_ID,
4192
4164
  }
4193
- _set_gas_price(w3, tx)
4194
- signed = acct.sign_transaction(tx)
4195
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
4196
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
4165
+ receipt = _sign_and_send(w3, acct, tx, "GLV withdrawal request", fallback_gas=5000000)
4197
4166
  ok = receipt["status"] == 1
4198
- print(f"{'✓' if ok else '✗'} GLV withdrawal request {'submitted' if ok else 'failed'}")
4199
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
4200
4167
  if ok:
4201
4168
  print(" Request queued — wait for the GMX keeper callback to return the underlying(s).")
4202
4169
  return ok
@@ -4541,13 +4508,8 @@ def cmd_lb_add(pair_key: str, amount_x: float, amount_y: float, shape: str = "sp
4541
4508
  "nonce": w3.eth.get_transaction_count(acct.address),
4542
4509
  "gas": gas, "chainId": CHAIN_ID,
4543
4510
  }
4544
- _set_gas_price(w3, tx)
4545
- signed = acct.sign_transaction(tx)
4546
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
4547
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
4511
+ receipt = _sign_and_send(w3, acct, tx, "LB add", fallback_gas=5000000)
4548
4512
  ok = receipt["status"] == 1
4549
- print(f"{'✓' if ok else '✗'} LB add {'confirmed' if ok else 'failed'}")
4550
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
4551
4513
  return ok
4552
4514
 
4553
4515
 
@@ -4639,13 +4601,8 @@ def cmd_lb_remove(pair_key: str, slippage_pct: float = 1.0, execute: bool = Fals
4639
4601
  "nonce": w3.eth.get_transaction_count(acct.address),
4640
4602
  "gas": 5000000, "chainId": CHAIN_ID,
4641
4603
  }
4642
- _set_gas_price(w3, tx)
4643
- signed = acct.sign_transaction(tx)
4644
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
4645
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)
4604
+ receipt = _sign_and_send(w3, acct, tx, "LB remove", fallback_gas=5000000)
4646
4605
  ok = receipt["status"] == 1
4647
- print(f"{'✓' if ok else '✗'} LB remove {'confirmed' if ok else 'failed'}")
4648
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
4649
4606
 
4650
4607
  # ─── PRIME-token leverage tiers (PrimeLeverageFacet) ─────────────────────────
4651
4608
 
@@ -4841,11 +4798,8 @@ def cmd_prime_activate(amount: float = None, execute: bool = False):
4841
4798
  "nonce": nonce + 1,
4842
4799
  "gas": 3000000, "chainId": CHAIN_ID,
4843
4800
  }
4844
- _set_gas_price(w3, dep_tx)
4845
- dep_hash = w3.eth.send_raw_transaction(acct.sign_transaction(dep_tx).raw_transaction)
4846
- dep_ok = w3.eth.wait_for_transaction_receipt(dep_hash, timeout=180)["status"] == 1
4847
- print(f"{'✓' if dep_ok else '✗'} depositPrime {'confirmed' if dep_ok else 'failed'}")
4848
- print(f" Tx: {EXPLORER}/tx/{dep_hash.hex()}")
4801
+ receipt = _sign_and_send(w3, acct, dep_tx, "depositPrime", fallback_gas=3000000)
4802
+ dep_ok = receipt["status"] == 1
4849
4803
  if not dep_ok:
4850
4804
  print(" Aborting — not activating PREMIUM after a failed deposit.")
4851
4805
  return
@@ -4860,11 +4814,8 @@ def cmd_prime_activate(amount: float = None, execute: bool = False):
4860
4814
  "nonce": w3.eth.get_transaction_count(acct.address),
4861
4815
  "gas": 3000000, "chainId": CHAIN_ID,
4862
4816
  }
4863
- _set_gas_price(w3, tx)
4864
- tx_hash = w3.eth.send_raw_transaction(acct.sign_transaction(tx).raw_transaction)
4865
- ok = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)["status"] == 1
4866
- print(f"{'✓' if ok else '✗'} PREMIUM tier {'activated' if ok else 'activation failed'}")
4867
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
4817
+ receipt = _sign_and_send(w3, acct, tx, "PREMIUM tier activate", fallback_gas=3000000)
4818
+ ok = receipt["status"] == 1
4868
4819
 
4869
4820
  def cmd_prime_deposit(amount: float, execute: bool = False):
4870
4821
  """Deposit PRIME from the wallet (EOA) INTO the Prime Account, without activating PREMIUM.
@@ -4919,11 +4870,8 @@ def cmd_prime_deposit(amount: float, execute: bool = False):
4919
4870
  "nonce": nonce + 1,
4920
4871
  "gas": 3000000, "chainId": CHAIN_ID,
4921
4872
  }
4922
- _set_gas_price(w3, dep_tx)
4923
- dep_hash = w3.eth.send_raw_transaction(acct.sign_transaction(dep_tx).raw_transaction)
4924
- dep_ok = w3.eth.wait_for_transaction_receipt(dep_hash, timeout=180)["status"] == 1
4925
- print(f"{'✓' if dep_ok else '✗'} depositPrime {'confirmed' if dep_ok else 'failed'}")
4926
- print(f" Tx: {EXPLORER}/tx/{dep_hash.hex()}")
4873
+ receipt = _sign_and_send(w3, acct, dep_tx, "depositPrime", fallback_gas=3000000)
4874
+ dep_ok = receipt["status"] == 1
4927
4875
 
4928
4876
  def cmd_prime_deactivate(withdraw: bool = False, execute: bool = False):
4929
4877
  """Drop back to BASIC tier (deactivatePremiumTier(withdrawStake)). The facet REPAYS ALL PRIME
@@ -4972,11 +4920,8 @@ def cmd_prime_deactivate(withdraw: bool = False, execute: bool = False):
4972
4920
  "nonce": w3.eth.get_transaction_count(acct.address),
4973
4921
  "gas": 3000000, "chainId": CHAIN_ID,
4974
4922
  }
4975
- _set_gas_price(w3, tx)
4976
- tx_hash = w3.eth.send_raw_transaction(acct.sign_transaction(tx).raw_transaction)
4977
- ok = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)["status"] == 1
4978
- print(f"{'✓' if ok else '✗'} PREMIUM tier {'deactivated' if ok else 'deactivation failed'}")
4979
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
4923
+ receipt = _sign_and_send(w3, acct, tx, "PREMIUM tier deactivate", fallback_gas=3000000)
4924
+ ok = receipt["status"] == 1
4980
4925
 
4981
4926
  def cmd_prime_unstake(amount: float, execute: bool = False):
4982
4927
  """Unstake PRIME from the leverage stake back into the account (unstakePrime). onlyOwner, NOT
@@ -5021,11 +4966,8 @@ def cmd_prime_unstake(amount: float, execute: bool = False):
5021
4966
  "nonce": w3.eth.get_transaction_count(acct.address),
5022
4967
  "gas": 3000000, "chainId": CHAIN_ID,
5023
4968
  }
5024
- _set_gas_price(w3, tx)
5025
- tx_hash = w3.eth.send_raw_transaction(acct.sign_transaction(tx).raw_transaction)
5026
- ok = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)["status"] == 1
5027
- print(f"{'✓' if ok else '✗'} PRIME unstake {'confirmed' if ok else 'failed'}")
5028
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
4969
+ receipt = _sign_and_send(w3, acct, tx, "PRIME unstake", fallback_gas=3000000)
4970
+ ok = receipt["status"] == 1
5029
4971
 
5030
4972
  def cmd_prime_repay(amount: float, execute: bool = False):
5031
4973
  """Repay accrued PRIME rent-debt (repayPrimeDebt) using in-account PRIME. onlyOwner, NOT
@@ -5071,11 +5013,8 @@ def cmd_prime_repay(amount: float, execute: bool = False):
5071
5013
  "nonce": w3.eth.get_transaction_count(acct.address),
5072
5014
  "gas": 3000000, "chainId": CHAIN_ID,
5073
5015
  }
5074
- _set_gas_price(w3, tx)
5075
- tx_hash = w3.eth.send_raw_transaction(acct.sign_transaction(tx).raw_transaction)
5076
- ok = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=180)["status"] == 1
5077
- print(f"{'✓' if ok else '✗'} PRIME debt repay {'confirmed' if ok else 'failed'}")
5078
- print(f" Tx: {EXPLORER}/tx/{tx_hash.hex()}")
5016
+ receipt = _sign_and_send(w3, acct, tx, "PRIME debt repay", fallback_gas=3000000)
5017
+ ok = receipt["status"] == 1
5079
5018
 
5080
5019
  # ─── Zaps (tool-level macros) ────────────────────────────────────────────────
5081
5020
  # DeltaPrime zaps are NOT a separate on-chain facet (capabilities §7) — they are front-end
@@ -5640,13 +5579,9 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
5640
5579
  tx = {"from": wallet, "to": bridge_target, "data": bytes.fromhex(calldata_hex),
5641
5580
  "nonce": w3.eth.get_transaction_count(wallet), "gas": 500000,
5642
5581
  "value": native_fee, "chainId": src_chain_id}
5643
- _set_gas_price_for(src_chain_id, w3, tx)
5644
- signed = acct.sign_transaction(tx)
5645
- tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
5646
- receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300)
5582
+ receipt = _sign_and_send(w3, acct, tx, "Bridge", timeout=300, fallback_gas=500000,
5583
+ gas_price_fn=lambda _w, _tx: _set_gas_price_for(src_chain_id, _w, _tx))
5647
5584
  ok = receipt["status"] == 1
5648
- print(f"{'✓' if ok else '✗'} Bridge {'submitted' if ok else 'failed'}")
5649
- print(f" Tx: {src_cfg['explorer']}/{tx_hash.hex()}")
5650
5585
 
5651
5586
  def main():
5652
5587
  check_version()
@@ -6027,4 +5962,3 @@ def main():
6027
5962
 
6028
5963
  if __name__ == "__main__":
6029
5964
  main()
6030
-