primecli 0.4.0__tar.gz → 0.5.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primecli
3
- Version: 0.4.0
3
+ Version: 0.5.1
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
@@ -26,7 +26,7 @@ License-File: LICENSE
26
26
  Requires-Dist: web3<8,>=7.0
27
27
  Requires-Dist: eth-account>=0.13
28
28
  Requires-Dist: eth-keys>=0.5
29
- Requires-Dist: eth-abi>=5.0
29
+ Requires-Dist: eth-abi<7,>=5.0
30
30
  Requires-Dist: requests>=2.31
31
31
  Dynamic: license-file
32
32
 
@@ -48,13 +48,15 @@ Built for agent use:
48
48
  - RedStone-signed solvency math handled internally, with a regression test pinning the half-boundary `toFixed(8)` encoding.
49
49
  - ParaSwap calldata validated client-side against the on-chain executor allowlist before broadcast.
50
50
 
51
- **Current version:** 0.3.0. The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
51
+ **Current version:** 0.5.1 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
52
+
53
+ > **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)).
52
54
 
53
55
  ## Security and trust
54
56
 
55
57
  **This tool moves real on-chain funds.** Read before using.
56
58
 
57
- - You manage your own private key. The tool reads it from `DELTAPRIME_PRIVATE_KEY` or `DEGENPRIME_PRIVATE_KEY` (or a file path you point at via `*_KEY_FILE`, or a one-shot `--key` flag). It never writes the key anywhere.
59
+ - You manage your own private key. The tool reads it from `<TOOL>_PRIVATE_KEY` (e.g. `DELTAPRIME_PRIVATE_KEY`), or a file path you point at via `<TOOL>_KEY_FILE`, or a one-shot `--key` flag. It never writes the key anywhere. There is no default key: with nothing configured, commands fail closed.
58
60
  - Every state-changing command **previews by default**. You must pass `--execute` to broadcast. Don't pass `--execute` until you have read the preview and understand what it is about to do.
59
61
  - The RedStone payload, ParaSwap executor allowlist, and facet ABIs are pinned to specific on-chain state at the dates noted in the source. If DeltaPrime or DegenPrime upgrade their diamonds, the tool may need updating. Open an issue.
60
62
  - The DeltaPrimeLabs team is not affiliated with this project. This is community-maintained tooling.
@@ -123,7 +125,7 @@ State-changing commands preview by default. Add `--execute` to broadcast.
123
125
  |-------|----------|
124
126
  | Lending core | `pool-info [--json]`, `my-positions`, `deposit`, `withdraw` (24h delayed lender flow, step 1), `withdrawal-requests`, `execute-withdrawal-request --pool X [--index N]`, `cancel-withdrawal-request --pool X --index N`, `borrow`, `repay`, `fund` |
125
127
  | Prime Account | `create-prime-account` (alias `create-account`), `prime-summary`, `defi --json`, `withdraw-collateral`, `withdrawal-intents`, `execute-withdrawal` |
126
- | Swaps | `swap --from S --to S --amount N [--via yak\|paraswap] [--slippage P]` (⚠️ `--via paraswap` blocked upstream, see [issue #2](https://github.com/Mnemosyne-quest/primecli/issues/2)), `swap-debt --from S --to S --amount N [--slippage P]` (⚠️ also blocked upstream — same allowlist) |
128
+ | Swaps | `swap --from S --to S --amount N [--via yak\|paraswap] [--slippage P]` (`--via yak` default; `--via paraswap` validates the API calldata and patches a non-whitelisted executor to a known-good one before broadcast), `swap-debt --from S --to S --amount N [--slippage P]` (same ParaSwap executor handling) |
127
129
  | GMX V2 LP (async, keeper-executed) | `gmx-positions`, `gmx-deposit --market M --amount N [--side long\|short]`, `gmx-withdraw --market M --amount N` |
128
130
  | TraderJoe V2 LB | `lb-positions`, `lb-add --pair P --amount-x N --amount-y N [--shape spot\|curve\|bidask] [--range R]`, `lb-remove --pair P` |
129
131
  | sJOE staking | `sjoe-position`, `sjoe-stake --amount N`, `sjoe-unstake --amount N`, `sjoe-claim` |
@@ -181,11 +183,14 @@ Note: DeltaPrime has TWO deployments on Arbitrum; `arbprime` targets the live on
181
183
  | `DEGENPRIME_KEY_FILE` | falls back to `DELTAPRIME_KEY_FILE` | Path to key file for Base. |
182
184
  | `DEGENPRIME_RPC` | `https://base.publicnode.com` | Base RPC. |
183
185
  | `ARBPRIME_PRIVATE_KEY` | falls back to `DELTAPRIME_PRIVATE_KEY` | Your Arbitrum signing key. Same EVM key works on all three chains. |
186
+ | `ARBPRIME_KEY_FILE` | falls back to `DELTAPRIME_KEY_FILE` | Path to key file for Arbitrum. |
184
187
  | `ARBPRIME_AGENT` | falls back to `DELTAPRIME_AGENT` | Named-agent key selection (multi-wallet setups). |
185
188
  | `ARBPRIME_RPC` | `https://arb1.arbitrum.io/rpc` | Arbitrum One RPC. |
186
189
 
187
190
  The CLI also accepts a per-command `--key <0xhex>` override that takes precedence over all env vars. Handy for one-off operations from a shell where you don't want to persist the key.
188
191
 
192
+ **Key resolution.** `deltaprime` and `arbprime` resolve the signing key in this order (first hit wins): `--key` > `--as <agent>` > `<TOOL>_PRIVATE_KEY` > `<TOOL>_KEY_FILE` > `<TOOL>_ENV_FILE` + `<TOOL>_KEY_VAR` > `<TOOL>_AGENT` env > error. `arbprime`'s `ARBPRIME_*` vars each fall back to the `DELTAPRIME_*` equivalent. `degenprime` is simpler — `--key` > `DEGENPRIME_PRIVATE_KEY` > `DEGENPRIME_KEY_FILE`, with `DELTAPRIME_PRIVATE_KEY` / `DELTAPRIME_KEY_FILE` as fallbacks (it has no `--as` / agent-table mechanism). If none resolve, the command exits 1 with `No signing key found...`.
193
+
189
194
  A copy-paste template is at [examples/env.example](examples/env.example).
190
195
 
191
196
  ## What's covered
@@ -239,6 +244,19 @@ A copy-paste template is at [examples/env.example](examples/env.example).
239
244
  | Leveraged-long zap macro | full (GM-terminal) |
240
245
  | Penpie / Beefy / Sushi facets | not yet (live on-chain; deferred by scope) |
241
246
 
247
+ ### PRIME bridge (`deltaprime` and `arbprime`)
248
+
249
+ Both tools expose a `prime-bridge` subcommand that moves the PRIME token between Avalanche and Arbitrum over LayerZero's OFT:
250
+
251
+ ```bash
252
+ deltaprime prime-bridge --from arb --amount 100 [--execute] # Arbitrum -> Avalanche
253
+ arbprime prime-bridge --from avax --amount 100 [--execute] # Avalanche -> Arbitrum
254
+ ```
255
+
256
+ `--from` takes `avax` or `arb` (the source chain; destination is the other one). The default `--from` differs per tool — `arb` for `deltaprime`, `avax` for `arbprime` — so always pass it explicitly. Without `--execute` it previews the LayerZero native fee, balance, and the two steps (ERC-20 approve, then `sendFrom()`); with `--execute` it broadcasts both. Gas price and `chainId` are set per the source chain.
257
+
258
+ The standalone `prime-bridge.py` script that shipped earlier was removed in 0.5.0 — the subcommand is the only supported entry point.
259
+
242
260
  ## Documentation
243
261
 
244
262
  - [DeltaPrime reference](docs/deltaprime-reference.md): protocol model, addresses, facet map, RedStone integration, full command table, GMX / LB / PRIME flows.
@@ -253,7 +271,7 @@ A copy-paste template is at [examples/env.example](examples/env.example).
253
271
 
254
272
  1. **Preview by default.** Every state-changing command prints a structured preview and stops unless `--execute` is passed. An agent can call any command speculatively, parse the preview, decide whether to broadcast, then re-run with `--execute`.
255
273
  2. **Predictable, parseable stdout.** Read-only commands (`pool-info` (also `--json`), `my-positions`, `prime-summary`, `summary`, `withdrawal-intents`, `lb-positions`, `gmx-positions`, `aerodrome-positions`, `sjoe-position`, `prime-tier`, `defi --json`) emit fixed-format tables or JSON. `defi --json` is a one-shot full positions snapshot.
256
- 3. **Clean failure modes.** Configuration errors do not print stack traces. A missing key prints `deltaprime: No signing key found. Set DELTAPRIME_PRIVATE_KEY ...` to stderr and exits 1.
274
+ 3. **Clean failure modes.** Configuration errors do not print stack traces. A missing key prints `deltaprime: No signing key found. Pass --key <0xhex> or --as <agent>, or set DELTAPRIME_PRIVATE_KEY ...` to stderr and exits 1.
257
275
 
258
276
  Full agent integration guide (Claude Code skill template, MCP notes, recommended guardrails): [docs/agent-integration.md](docs/agent-integration.md).
259
277
 
@@ -280,7 +298,7 @@ Common failure modes and their fixes:
280
298
  - **`RedStone gateway unreachable` on a read.** `prime-summary` / `summary` fall back to balances-only when the RedStone gateway is down. On `--execute` of a solvency-gated write, the call cannot proceed without a payload; wait and retry, or try the alternate gateway via the env override.
281
299
  - **Swap fails on-chain with `InvalidExecutor`.** ParaSwap rotated an executor that is not in the tool's mirror of the on-chain allowlist. The tool patches to a known-good fallback automatically; if reverts persist, the on-chain allowlist itself has likely rotated. Open an issue.
282
300
  - **GMX deposit reverts `InsufficientNumberOfUniqueSigners(0,3)`.** A required RedStone feed was missing from the appended payload. This was the load-bearing fix on the GMX path (24-05-2026). If you hit it on a current build, capture the tx hash and open an issue.
283
- - **GMX deposit accepted but no GM minted.** The execution fee was below the keeper's threshold and the request expired (refund without mint). Re-run; the tool floors gas at 25 gwei in the fee estimator to clear the keeper's bar.
301
+ - **GMX deposit accepted but no GM minted.** The execution fee was below the keeper's threshold and the request expired (refund without mint). Re-run; the tool floors gas at 1 gwei (and pads 2×) in the fee estimator to clear the keeper's bar.
284
302
  - **`createLoan` succeeded but `getLoansForOwner` returns empty.** The factory's owner→loans map lags a beat behind the receipt. The tool polls for up to 12s; rerun `my-positions` shortly after if it timed out.
285
303
 
286
304
  If your failure is not on this list and the on-chain revert reason is opaque, capture the tx hash, the exact CLI invocation, and the preview output, and file an issue.
@@ -16,13 +16,15 @@ 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.3.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.5.1 The 0.x line is pre-1.0, so breaking changes are possible. See [Releases](https://github.com/Mnemosyne-quest/primecli/releases).
20
+
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)).
20
22
 
21
23
  ## Security and trust
22
24
 
23
25
  **This tool moves real on-chain funds.** Read before using.
24
26
 
25
- - You manage your own private key. The tool reads it from `DELTAPRIME_PRIVATE_KEY` or `DEGENPRIME_PRIVATE_KEY` (or a file path you point at via `*_KEY_FILE`, or a one-shot `--key` flag). It never writes the key anywhere.
27
+ - You manage your own private key. The tool reads it from `<TOOL>_PRIVATE_KEY` (e.g. `DELTAPRIME_PRIVATE_KEY`), or a file path you point at via `<TOOL>_KEY_FILE`, or a one-shot `--key` flag. It never writes the key anywhere. There is no default key: with nothing configured, commands fail closed.
26
28
  - Every state-changing command **previews by default**. You must pass `--execute` to broadcast. Don't pass `--execute` until you have read the preview and understand what it is about to do.
27
29
  - The RedStone payload, ParaSwap executor allowlist, and facet ABIs are pinned to specific on-chain state at the dates noted in the source. If DeltaPrime or DegenPrime upgrade their diamonds, the tool may need updating. Open an issue.
28
30
  - The DeltaPrimeLabs team is not affiliated with this project. This is community-maintained tooling.
@@ -91,7 +93,7 @@ State-changing commands preview by default. Add `--execute` to broadcast.
91
93
  |-------|----------|
92
94
  | Lending core | `pool-info [--json]`, `my-positions`, `deposit`, `withdraw` (24h delayed lender flow, step 1), `withdrawal-requests`, `execute-withdrawal-request --pool X [--index N]`, `cancel-withdrawal-request --pool X --index N`, `borrow`, `repay`, `fund` |
93
95
  | Prime Account | `create-prime-account` (alias `create-account`), `prime-summary`, `defi --json`, `withdraw-collateral`, `withdrawal-intents`, `execute-withdrawal` |
94
- | Swaps | `swap --from S --to S --amount N [--via yak\|paraswap] [--slippage P]` (⚠️ `--via paraswap` blocked upstream, see [issue #2](https://github.com/Mnemosyne-quest/primecli/issues/2)), `swap-debt --from S --to S --amount N [--slippage P]` (⚠️ also blocked upstream — same allowlist) |
96
+ | Swaps | `swap --from S --to S --amount N [--via yak\|paraswap] [--slippage P]` (`--via yak` default; `--via paraswap` validates the API calldata and patches a non-whitelisted executor to a known-good one before broadcast), `swap-debt --from S --to S --amount N [--slippage P]` (same ParaSwap executor handling) |
95
97
  | GMX V2 LP (async, keeper-executed) | `gmx-positions`, `gmx-deposit --market M --amount N [--side long\|short]`, `gmx-withdraw --market M --amount N` |
96
98
  | TraderJoe V2 LB | `lb-positions`, `lb-add --pair P --amount-x N --amount-y N [--shape spot\|curve\|bidask] [--range R]`, `lb-remove --pair P` |
97
99
  | sJOE staking | `sjoe-position`, `sjoe-stake --amount N`, `sjoe-unstake --amount N`, `sjoe-claim` |
@@ -149,11 +151,14 @@ Note: DeltaPrime has TWO deployments on Arbitrum; `arbprime` targets the live on
149
151
  | `DEGENPRIME_KEY_FILE` | falls back to `DELTAPRIME_KEY_FILE` | Path to key file for Base. |
150
152
  | `DEGENPRIME_RPC` | `https://base.publicnode.com` | Base RPC. |
151
153
  | `ARBPRIME_PRIVATE_KEY` | falls back to `DELTAPRIME_PRIVATE_KEY` | Your Arbitrum signing key. Same EVM key works on all three chains. |
154
+ | `ARBPRIME_KEY_FILE` | falls back to `DELTAPRIME_KEY_FILE` | Path to key file for Arbitrum. |
152
155
  | `ARBPRIME_AGENT` | falls back to `DELTAPRIME_AGENT` | Named-agent key selection (multi-wallet setups). |
153
156
  | `ARBPRIME_RPC` | `https://arb1.arbitrum.io/rpc` | Arbitrum One RPC. |
154
157
 
155
158
  The CLI also accepts a per-command `--key <0xhex>` override that takes precedence over all env vars. Handy for one-off operations from a shell where you don't want to persist the key.
156
159
 
160
+ **Key resolution.** `deltaprime` and `arbprime` resolve the signing key in this order (first hit wins): `--key` > `--as <agent>` > `<TOOL>_PRIVATE_KEY` > `<TOOL>_KEY_FILE` > `<TOOL>_ENV_FILE` + `<TOOL>_KEY_VAR` > `<TOOL>_AGENT` env > error. `arbprime`'s `ARBPRIME_*` vars each fall back to the `DELTAPRIME_*` equivalent. `degenprime` is simpler — `--key` > `DEGENPRIME_PRIVATE_KEY` > `DEGENPRIME_KEY_FILE`, with `DELTAPRIME_PRIVATE_KEY` / `DELTAPRIME_KEY_FILE` as fallbacks (it has no `--as` / agent-table mechanism). If none resolve, the command exits 1 with `No signing key found...`.
161
+
157
162
  A copy-paste template is at [examples/env.example](examples/env.example).
158
163
 
159
164
  ## What's covered
@@ -207,6 +212,19 @@ A copy-paste template is at [examples/env.example](examples/env.example).
207
212
  | Leveraged-long zap macro | full (GM-terminal) |
208
213
  | Penpie / Beefy / Sushi facets | not yet (live on-chain; deferred by scope) |
209
214
 
215
+ ### PRIME bridge (`deltaprime` and `arbprime`)
216
+
217
+ Both tools expose a `prime-bridge` subcommand that moves the PRIME token between Avalanche and Arbitrum over LayerZero's OFT:
218
+
219
+ ```bash
220
+ deltaprime prime-bridge --from arb --amount 100 [--execute] # Arbitrum -> Avalanche
221
+ arbprime prime-bridge --from avax --amount 100 [--execute] # Avalanche -> Arbitrum
222
+ ```
223
+
224
+ `--from` takes `avax` or `arb` (the source chain; destination is the other one). The default `--from` differs per tool — `arb` for `deltaprime`, `avax` for `arbprime` — so always pass it explicitly. Without `--execute` it previews the LayerZero native fee, balance, and the two steps (ERC-20 approve, then `sendFrom()`); with `--execute` it broadcasts both. Gas price and `chainId` are set per the source chain.
225
+
226
+ The standalone `prime-bridge.py` script that shipped earlier was removed in 0.5.0 — the subcommand is the only supported entry point.
227
+
210
228
  ## Documentation
211
229
 
212
230
  - [DeltaPrime reference](docs/deltaprime-reference.md): protocol model, addresses, facet map, RedStone integration, full command table, GMX / LB / PRIME flows.
@@ -221,7 +239,7 @@ A copy-paste template is at [examples/env.example](examples/env.example).
221
239
 
222
240
  1. **Preview by default.** Every state-changing command prints a structured preview and stops unless `--execute` is passed. An agent can call any command speculatively, parse the preview, decide whether to broadcast, then re-run with `--execute`.
223
241
  2. **Predictable, parseable stdout.** Read-only commands (`pool-info` (also `--json`), `my-positions`, `prime-summary`, `summary`, `withdrawal-intents`, `lb-positions`, `gmx-positions`, `aerodrome-positions`, `sjoe-position`, `prime-tier`, `defi --json`) emit fixed-format tables or JSON. `defi --json` is a one-shot full positions snapshot.
224
- 3. **Clean failure modes.** Configuration errors do not print stack traces. A missing key prints `deltaprime: No signing key found. Set DELTAPRIME_PRIVATE_KEY ...` to stderr and exits 1.
242
+ 3. **Clean failure modes.** Configuration errors do not print stack traces. A missing key prints `deltaprime: No signing key found. Pass --key <0xhex> or --as <agent>, or set DELTAPRIME_PRIVATE_KEY ...` to stderr and exits 1.
225
243
 
226
244
  Full agent integration guide (Claude Code skill template, MCP notes, recommended guardrails): [docs/agent-integration.md](docs/agent-integration.md).
227
245
 
@@ -248,7 +266,7 @@ Common failure modes and their fixes:
248
266
  - **`RedStone gateway unreachable` on a read.** `prime-summary` / `summary` fall back to balances-only when the RedStone gateway is down. On `--execute` of a solvency-gated write, the call cannot proceed without a payload; wait and retry, or try the alternate gateway via the env override.
249
267
  - **Swap fails on-chain with `InvalidExecutor`.** ParaSwap rotated an executor that is not in the tool's mirror of the on-chain allowlist. The tool patches to a known-good fallback automatically; if reverts persist, the on-chain allowlist itself has likely rotated. Open an issue.
250
268
  - **GMX deposit reverts `InsufficientNumberOfUniqueSigners(0,3)`.** A required RedStone feed was missing from the appended payload. This was the load-bearing fix on the GMX path (24-05-2026). If you hit it on a current build, capture the tx hash and open an issue.
251
- - **GMX deposit accepted but no GM minted.** The execution fee was below the keeper's threshold and the request expired (refund without mint). Re-run; the tool floors gas at 25 gwei in the fee estimator to clear the keeper's bar.
269
+ - **GMX deposit accepted but no GM minted.** The execution fee was below the keeper's threshold and the request expired (refund without mint). Re-run; the tool floors gas at 1 gwei (and pads 2×) in the fee estimator to clear the keeper's bar.
252
270
  - **`createLoan` succeeded but `getLoansForOwner` returns empty.** The factory's owner→loans map lags a beat behind the receipt. The tool polls for up to 12s; rerun `my-positions` shortly after if it timed out.
253
271
 
254
272
  If your failure is not on this list and the on-chain revert reason is opaque, capture the tx hash, the exact CLI invocation, and the preview output, and file an issue.
@@ -234,19 +234,21 @@ ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
234
234
  # every chain, so the ARBPRIME_ env vars fall back to the DELTAPRIME_ equivalents.
235
235
  #
236
236
  # Key resolution order (first hit wins; see resolve_private_key):
237
- # 1. --as <agent> CLI flag -> AGENTS[<agent>]
238
- # 2. ARBPRIME_PRIVATE_KEY / DELTAPRIME_PRIVATE_KEY env var -> raw 0x… key
239
- # 3. ARBPRIME_ENV_FILE+ARBPRIME_KEY_VAR / DELTAPRIME_* equivalent -> read <var> from <file>
240
- # 4. ARBPRIME_AGENT / DELTAPRIME_AGENT env var -> AGENTS[<agent>]
241
- # 5. DEFAULT_AGENT -> AGENTS[DEFAULT_AGENT]
237
+ # 1. --key <0xhex> CLI flag -> raw 0x… key (one-off)
238
+ # 2. --as <agent> CLI flag -> AGENTS[<agent>]
239
+ # 3. ARBPRIME_PRIVATE_KEY / DELTAPRIME_PRIVATE_KEY env var -> raw 0x… key
240
+ # 4. ARBPRIME_KEY_FILE / DELTAPRIME_KEY_FILE env var -> path to a file with the 0x key
241
+ # 5. ARBPRIME_ENV_FILE+ARBPRIME_KEY_VAR / DELTAPRIME_* equivalent -> read <var> from <file>
242
+ # 6. ARBPRIME_AGENT / DELTAPRIME_AGENT env var -> AGENTS[<agent>]
243
+ # If none resolve, fail closed (no silent default key).
242
244
  #
243
- # To add another wallet: add a row to AGENTS, or export ARBPRIME_PRIVATE_KEY.
245
+ # To add another wallet: add a row to AGENTS, export ARBPRIME_PRIVATE_KEY, or pass --key.
244
246
  AGENTS = {
245
247
  "parakletos": ("/root/.openclaw/.env", "PARAKLETOS_EVM_PRIVATE_KEY"),
246
248
  "paraklaudios": ("/root/paraklaudios/.credentials.env", "PARAKLAUDIOS_EVM_PRIVATE_KEY"),
247
249
  }
248
- DEFAULT_AGENT = "parakletos" # preserves original behavior when nothing else is set
249
250
  _SELECTED_AGENT = None # set by the --as CLI flag in main()
251
+ _CLI_KEY = None # set by the --key CLI flag in main()
250
252
  # Core protocol addresses — the LIVE Arbitrum deployment (DeploymentConstants.sol),
251
253
  # on-chain verified 2026-06-03. The stale *TUP.json deployment (factory 0x97f4C81…)
252
254
  # has only ETH+USDC pools — NOT used here.
@@ -658,7 +660,10 @@ def _set_gas_price(w3, tx_dict):
658
660
  """Set appropriate gas price fields for the chain, replacing the legacy gasPrice approach.
659
661
  On EIP-1559 chains (Arbitrum, Base): sets maxFeePerGas + maxPriorityFeePerGas with a 2x
660
662
  base-fee hedge (base + prio + 1 gwei buffer). On Avalanche (legacy): sets gasPrice at
661
- 2x base fee with a 25 gwei floor, matching the old _tx_gas_price semantics for this chain."""
663
+ 2x base fee with a 1 gwei floor. (25 gwei was the pre-Etna C-chain minimum;
664
+ ACP-125 (Dec 2024) lowered the min base fee to 1 nAVAX — base now sits at ~0.01
665
+ nAVAX, so a 25 gwei floor overpaid ~2500x and inflated the upfront balance
666
+ requirement past small EOAs.)"""
662
667
  tx_dict.pop("gasPrice", None)
663
668
  if CHAIN_ID in (42161, 8453): # Arbitrum, Base — EIP-1559
664
669
  base = w3.eth.gas_price
@@ -666,7 +671,22 @@ def _set_gas_price(w3, tx_dict):
666
671
  tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
667
672
  tx_dict["maxPriorityFeePerGas"] = prio
668
673
  else: # Avalanche (43114) — legacy gasPrice
669
- tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 25 * 10**9)
674
+ tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
675
+
676
+ def _set_gas_price_for(chain_id, w3, tx_dict):
677
+ """Set gas fields for an EXPLICIT chain_id rather than the module CHAIN_ID. Needed by
678
+ cross-chain flows (prime-bridge) where a tx may target Avalanche or Arbitrum regardless
679
+ of which tool built it. Arbitrum/Base (EIP-1559): maxFeePerGas + maxPriorityFeePerGas;
680
+ Avalanche (legacy): gasPrice with a 1 gwei floor (post-Etna; see _set_gas_price)."""
681
+ tx_dict.pop("gasPrice", None)
682
+ if chain_id in (42161, 8453): # Arbitrum, Base — EIP-1559
683
+ base = w3.eth.gas_price
684
+ prio = w3.eth.max_priority_fee
685
+ tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
686
+ tx_dict["maxPriorityFeePerGas"] = prio
687
+ else: # Avalanche (43114) — legacy gasPrice
688
+ tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 1 * 10**9)
689
+
670
690
  def _read_env_var(path, var):
671
691
  """Return the value of `var` from a KEY=VALUE env file, or None if absent."""
672
692
  try:
@@ -693,15 +713,26 @@ def _agent_key(agent):
693
713
  def resolve_private_key():
694
714
  # The same EVM key works on every chain, so each ARBPRIME_ var falls back to its
695
715
  # DELTAPRIME_ equivalent (exactly how degenprime falls back to DELTAPRIME_*).
696
- # 1. --as <agent> CLI flag (set in main)
716
+ # 1. --key <0xhex> CLI flag (set in main)
717
+ if _CLI_KEY:
718
+ return _CLI_KEY.strip()
719
+ # 2. --as <agent> CLI flag (set in main)
697
720
  if _SELECTED_AGENT:
698
721
  return _agent_key(_SELECTED_AGENT)
699
- # 2. raw key directly in the environment
722
+ # 3. raw key directly in the environment
700
723
  for env_var in ("ARBPRIME_PRIVATE_KEY", "DELTAPRIME_PRIVATE_KEY"):
701
724
  raw = os.environ.get(env_var)
702
725
  if raw:
703
726
  return raw.strip()
704
- # 3. explicit env-file + var-name
727
+ # 4. path to a file containing the 0x key
728
+ for path_var in ("ARBPRIME_KEY_FILE", "DELTAPRIME_KEY_FILE"):
729
+ key_file = os.environ.get(path_var)
730
+ if key_file:
731
+ try:
732
+ return Path(key_file).read_text().strip()
733
+ except FileNotFoundError:
734
+ raise RuntimeError(f"{path_var} points at {key_file} but the file does not exist.")
735
+ # 5. explicit env-file + var-name
705
736
  env_file = os.environ.get("ARBPRIME_ENV_FILE") or os.environ.get("DELTAPRIME_ENV_FILE")
706
737
  key_var = os.environ.get("ARBPRIME_KEY_VAR") or os.environ.get("DELTAPRIME_KEY_VAR")
707
738
  if env_file and key_var:
@@ -709,16 +740,25 @@ def resolve_private_key():
709
740
  if not key:
710
741
  raise RuntimeError(f"{key_var} not found in {env_file}.")
711
742
  return key
712
- # 4. named agent in the environment
743
+ # 6. named agent in the environment
713
744
  agent = os.environ.get("ARBPRIME_AGENT") or os.environ.get("DELTAPRIME_AGENT")
714
745
  if agent:
715
746
  return _agent_key(agent)
716
- # 5. default agent (back-compat with the original Parakletos-only behavior)
717
- return _agent_key(DEFAULT_AGENT)
747
+ # No silent default fail closed.
748
+ raise RuntimeError(
749
+ "No signing key found. Pass --key <0xhex> or --as <agent>, or set "
750
+ "ARBPRIME_PRIVATE_KEY (raw 0x... key), ARBPRIME_KEY_FILE (path to a file with "
751
+ "the key), or ARBPRIME_ENV_FILE + ARBPRIME_KEY_VAR. DELTAPRIME_* equivalents "
752
+ "also work (same key, all chains)."
753
+ )
718
754
 
719
755
  def get_account() -> Account:
720
756
  return Account.from_key(resolve_private_key())
721
757
 
758
+ def to_wei_units(amount, decimals):
759
+ """Convert a human amount to integer base units without float drift."""
760
+ return int(Decimal(str(amount)) * (10 ** int(decimals)))
761
+
722
762
  def get_pool_contract(pool_name: str):
723
763
  """Pool proxy contract bound directly to the hand-curated POOL_ABI (no block-explorer
724
764
  ABI fetch — Arbiscan is display-only here; the hand-curated subset covers every
@@ -1376,7 +1416,7 @@ def cmd_my_positions():
1376
1416
  def cmd_deposit(pool_name: str, amount: float, execute: bool = False):
1377
1417
  contract, cfg, w3 = get_pool_contract(pool_name)
1378
1418
  acct = get_account()
1379
- amount_wei = int(amount * 10**cfg["decimals"])
1419
+ amount_wei = to_wei_units(amount, cfg["decimals"])
1380
1420
 
1381
1421
  if not execute:
1382
1422
  print(f"Preview: Deposit {amount} {cfg['symbol']} into {pool_name.upper()} pool")
@@ -1433,7 +1473,7 @@ def cmd_withdraw(pool_name: str, amount: float, execute: bool = False):
1433
1473
  """
1434
1474
  contract, cfg, w3 = get_pool_contract(pool_name)
1435
1475
  acct = get_account()
1436
- amount_wei = int(amount * 10**cfg["decimals"])
1476
+ amount_wei = to_wei_units(amount, cfg["decimals"])
1437
1477
 
1438
1478
  # getBalanceOf is the lender's current deposit balance — sane upper bound for
1439
1479
  # the intent amount. The pool reverts "Amount must be greater than zero" on 0,
@@ -1670,7 +1710,7 @@ def cmd_create_prime_account(execute: bool = False, fund_pool: str = None, fund_
1670
1710
  print(f"Preview: Create a new Prime Account for {acct.address}")
1671
1711
  if funding:
1672
1712
  symbol = cfg["symbol"]
1673
- amount_wei = int(fund_amount * 10**cfg["decimals"])
1713
+ amount_wei = to_wei_units(fund_amount, cfg["decimals"])
1674
1714
  print(f" Factory: {FACTORY_PROXY} (SmartLoansFactory.createAndFundLoan())")
1675
1715
  print(f" Approves the factory to spend {fund_amount} {symbol}, then")
1676
1716
  print(f" calls createAndFundLoan(bytes32 '{symbol}', {amount_wei}) — creates + funds in one go.")
@@ -1683,7 +1723,7 @@ def cmd_create_prime_account(execute: bool = False, fund_pool: str = None, fund_
1683
1723
 
1684
1724
  if funding:
1685
1725
  symbol = cfg["symbol"]
1686
- amount_wei = int(fund_amount * 10**cfg["decimals"])
1726
+ amount_wei = to_wei_units(fund_amount, cfg["decimals"])
1687
1727
  # createAndFundLoan does token.transferFrom(msg.sender, factory, amount),
1688
1728
  # so approve the factory first.
1689
1729
  token = w3.eth.contract(address=Web3.to_checksum_address(cfg["token"]),
@@ -1746,7 +1786,7 @@ def cmd_fund(pool_name: str, amount: float, execute: bool = False):
1746
1786
  return
1747
1787
 
1748
1788
  symbol = pool_to_asset_symbol(pool_name)
1749
- amount_wei = int(amount * 10**cfg["decimals"])
1789
+ amount_wei = to_wei_units(amount, cfg["decimals"])
1750
1790
  pa_cs = Web3.to_checksum_address(pa)
1751
1791
 
1752
1792
  if not execute:
@@ -1980,7 +2020,7 @@ def cmd_borrow(pool_name: str, amount: float, execute: bool = False):
1980
2020
  return
1981
2021
 
1982
2022
  symbol = pool_to_asset_symbol(pool_name)
1983
- amount_wei = int(amount * 10**cfg["decimals"])
2023
+ amount_wei = to_wei_units(amount, cfg["decimals"])
1984
2024
  if not execute:
1985
2025
  print(f"Preview: Borrow {amount} {symbol} into Prime Account {pa}")
1986
2026
  print(f" Calls borrow(bytes32 '{symbol}', {amount_wei}) on the Prime Account")
@@ -2027,7 +2067,7 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
2027
2067
  # The facet's repay reverts if amount > debt OR amount > in-account balance.
2028
2068
  # Cap to min(requested, debt, in_account) so callers don't need to know either
2029
2069
  # exact figure — pass an overshoot like 9999 and it clips cleanly.
2030
- requested_wei = int(amount * 10**cfg["decimals"])
2070
+ requested_wei = to_wei_units(amount, cfg["decimals"])
2031
2071
  debt_wei = pool.functions.getBorrowed(pa_cs).call()
2032
2072
  in_acct_wei = account.functions.getBalance(asset_b32(symbol)).call()
2033
2073
  if debt_wei == 0:
@@ -2297,7 +2337,7 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
2297
2337
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
2298
2338
 
2299
2339
  from_cfg, to_cfg = SWAP_ASSETS[from_sym], SWAP_ASSETS[to_sym]
2300
- amount_in = int(amount * 10**from_cfg["decimals"])
2340
+ amount_in = to_wei_units(amount, from_cfg["decimals"])
2301
2341
 
2302
2342
  # In-account balance check (oracle-free view).
2303
2343
  in_balance = account.functions.getBalance(asset_b32(from_sym)).call()
@@ -2395,7 +2435,7 @@ def _calc_swap_debt_amounts(w3, account, from_sym, to_sym, amount):
2395
2435
  borrowed = from_pool.functions.getBorrowed(pa_cs).call()
2396
2436
  if borrowed == 0:
2397
2437
  raise ValueError("zero_old_debt")
2398
- repay_amount = min(int(amount * 10**from_cfg["decimals"]), borrowed)
2438
+ repay_amount = min(to_wei_units(amount, from_cfg["decimals"]), borrowed)
2399
2439
 
2400
2440
  # Value-match the new borrow to the repay using the facet's own RedStone prices.
2401
2441
  feeds = prime_account_price_feeds(account)
@@ -2749,7 +2789,7 @@ def cmd_withdraw_collateral(pool_name: str, amount: float, execute: bool = False
2749
2789
  return
2750
2790
 
2751
2791
  symbol = pool_to_asset_symbol(pool_name)
2752
- amount_wei = int(amount * 10**cfg["decimals"])
2792
+ amount_wei = to_wei_units(amount, cfg["decimals"])
2753
2793
  pa_cs = Web3.to_checksum_address(pa)
2754
2794
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
2755
2795
 
@@ -3244,7 +3284,7 @@ def cmd_gmx_deposit(market: str, amount: float, is_long: bool | None = None,
3244
3284
  pa_cs = Web3.to_checksum_address(pa)
3245
3285
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
3246
3286
 
3247
- amount_wei = int(amount * 10**dep_cfg["decimals"])
3287
+ amount_wei = to_wei_units(amount, dep_cfg["decimals"])
3248
3288
  in_balance = account.functions.getBalance(asset_b32(dep_sym)).call()
3249
3289
  if amount_wei > in_balance:
3250
3290
  print(f"Prime Account holds only {in_balance / 10**dep_cfg['decimals']:.6f} {dep_sym} "
@@ -3354,7 +3394,7 @@ def cmd_gmx_withdraw(market: str, amount: float, slippage_pct: float = 1.0,
3354
3394
  gm_cs = Web3.to_checksum_address(mkt["gm_token"])
3355
3395
  erc = json.loads('[{"inputs":[{"name":"a","type":"address"}],"name":"balanceOf","outputs":[{"type":"uint256"}],"stateMutability":"view","type":"function"}]')
3356
3396
  gm_bal = w3.eth.contract(address=gm_cs, abi=erc).functions.balanceOf(pa_cs).call()
3357
- gm_amount = int(amount * 10**GM_TOKEN_DECIMALS)
3397
+ gm_amount = to_wei_units(amount, GM_TOKEN_DECIMALS)
3358
3398
  if gm_bal == 0:
3359
3399
  print(f"Prime Account holds no {mkt['gm_feed']} GM tokens — nothing to withdraw.")
3360
3400
  return
@@ -3600,7 +3640,7 @@ def cmd_glv_deposit(vault_key: str, amount: float, is_long: bool | None = None,
3600
3640
  dep_meta = long_meta if is_long_eff else short_meta
3601
3641
  dep_sym = dep_meta["symbol"]
3602
3642
 
3603
- amount_wei = int(amount * 10**dep_meta["decimals"])
3643
+ amount_wei = to_wei_units(amount, dep_meta["decimals"])
3604
3644
  in_balance = account.functions.getBalance(asset_b32(dep_sym)).call()
3605
3645
  if amount_wei > in_balance:
3606
3646
  print(f"Prime Account holds only {in_balance / 10**dep_meta['decimals']:.6f} {dep_sym} "
@@ -3717,7 +3757,7 @@ def cmd_glv_withdraw(vault_key: str, amount: float, target_market: str = None,
3717
3757
  glv_cs = Web3.to_checksum_address(vault["glv_token"])
3718
3758
  erc = json.loads('[{"inputs":[{"name":"a","type":"address"}],"name":"balanceOf","outputs":[{"type":"uint256"}],"stateMutability":"view","type":"function"}]')
3719
3759
  glv_bal = w3.eth.contract(address=glv_cs, abi=erc).functions.balanceOf(pa_cs).call()
3720
- glv_amount = int(amount * 10**GLV_TOKEN_DECIMALS)
3760
+ glv_amount = to_wei_units(amount, GLV_TOKEN_DECIMALS)
3721
3761
  if glv_bal == 0:
3722
3762
  print(f"Prime Account holds no [{vault_key}] GLV tokens — nothing to withdraw.")
3723
3763
  return
@@ -4055,8 +4095,8 @@ def cmd_lb_add(pair_key: str, amount_x: float, amount_y: float, shape: str = "sp
4055
4095
  pa_cs = Web3.to_checksum_address(pa)
4056
4096
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
4057
4097
 
4058
- amount_x_wei = int(amount_x * 10**x_cfg["decimals"]) if has_x else 0
4059
- amount_y_wei = int(amount_y * 10**y_cfg["decimals"]) if has_y else 0
4098
+ amount_x_wei = to_wei_units(amount_x, x_cfg["decimals"]) if has_x else 0
4099
+ amount_y_wei = to_wei_units(amount_y, y_cfg["decimals"]) if has_y else 0
4060
4100
 
4061
4101
  # In-account balances (oracle-free), keyed by the TokenManager symbol the facet uses.
4062
4102
  bal_x = account.functions.getBalance(asset_b32(x_cfg["symbol"])).call()
@@ -4382,7 +4422,7 @@ def cmd_prime_activate(amount: float = None, execute: bool = False):
4382
4422
  print(f"Prime Account {pa} is already in PREMIUM tier — nothing to do.")
4383
4423
  return
4384
4424
 
4385
- deposit_wei = int(amount * 10**PRIME_TOKEN["decimals"]) if amount else 0
4425
+ deposit_wei = to_wei_units(amount, PRIME_TOKEN["decimals"]) if amount else 0
4386
4426
  eoa_prime = prime.functions.balanceOf(acct.address).call()
4387
4427
  in_acct_prime = account.functions.getBalance(asset_b32(PRIME_TOKEN["symbol"])).call()
4388
4428
  # depositPrime caps to the EOA balance on-chain; mirror that for an honest preview.
@@ -4490,7 +4530,7 @@ def cmd_prime_deposit(amount: float, execute: bool = False):
4490
4530
 
4491
4531
  eoa_prime = prime.functions.balanceOf(acct.address).call()
4492
4532
  in_acct_prime = account.functions.getBalance(asset_b32(PRIME_TOKEN["symbol"])).call()
4493
- deposit_wei = int(amount * 10**PRIME_TOKEN["decimals"])
4533
+ deposit_wei = to_wei_units(amount, PRIME_TOKEN["decimals"])
4494
4534
  deposit_wei = min(deposit_wei, eoa_prime) # depositPrime caps to the EOA balance on-chain
4495
4535
 
4496
4536
  print(f"PRIME deposit into Prime Account {pa}")
@@ -4602,7 +4642,7 @@ def cmd_prime_unstake(amount: float, execute: bool = False):
4602
4642
  if staked == 0:
4603
4643
  print(f"Prime Account {pa} has no staked PRIME — nothing to unstake.")
4604
4644
  return
4605
- amount_wei = int(amount * 10**PRIME_TOKEN["decimals"])
4645
+ amount_wei = to_wei_units(amount, PRIME_TOKEN["decimals"])
4606
4646
  if amount_wei > staked:
4607
4647
  print(f"Staked PRIME is {staked / 10**PRIME_TOKEN['decimals']:,.6f}; clamping unstake to that.")
4608
4648
  amount_wei = staked
@@ -4649,7 +4689,7 @@ def cmd_prime_repay(amount: float, execute: bool = False):
4649
4689
 
4650
4690
  _tier, _staked, recorded_debt = account.functions.getLeverageTierFullInfo().call()
4651
4691
  in_acct_prime = account.functions.getBalance(asset_b32(PRIME_TOKEN["symbol"])).call()
4652
- amount_wei = int(amount * 10**PRIME_TOKEN["decimals"])
4692
+ amount_wei = to_wei_units(amount, PRIME_TOKEN["decimals"])
4653
4693
 
4654
4694
  print(f"PRIME repay debt: {amount} PRIME on Prime Account {pa}")
4655
4695
  print(f" Recorded PRIME debt: {recorded_debt / 10**PRIME_TOKEN['decimals']:,.6f} "
@@ -5141,7 +5181,7 @@ def _bridge_estimate_lz_fee(w3, src_cfg: dict, dst_lz_chain_id: int, amount_wei:
5141
5181
  "stateMutability": "view", "type": "function"}])))
5142
5182
  native, zro = ep.functions.estimateFees(
5143
5183
  dst_lz_chain_id, Web3.to_checksum_address(src_cfg["bridge_target"]),
5144
- _bridge_lz_payload(acct.address if 'acct' in dir() else get_account().address, amount_wei),
5184
+ _bridge_lz_payload(get_account().address, amount_wei),
5145
5185
  False, BRIDGE_ADAPTER_PARAMS).call()
5146
5186
  return native, zro
5147
5187
 
@@ -5164,7 +5204,8 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
5164
5204
  dst_key = "arb" if src_key == "avax" else "avax"
5165
5205
  src_cfg = BRIDGE_CHAIN[src_key]
5166
5206
  dst_cfg = BRIDGE_CHAIN[dst_key]
5167
- amount_wei = int(amount * 10**18)
5207
+ src_chain_id = src_cfg["chain_id"]
5208
+ amount_wei = to_wei_units(amount, 18)
5168
5209
 
5169
5210
  w3 = _bridge_w3(src_key)
5170
5211
  acct = get_account()
@@ -5218,8 +5259,8 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
5218
5259
  "name": "approve", "outputs": [{"name": "", "type": "bool"}], "type": "function"}])))
5219
5260
  atx = app_c.functions.approve(bridge_target, amount_wei).build_transaction(
5220
5261
  {"from": wallet, "nonce": w3.eth.get_transaction_count(wallet),
5221
- "gas": 100000, "chainId": src_cfg["chain_id"]})
5222
- _set_gas_price(w3, atx)
5262
+ "gas": 100000, "chainId": src_chain_id})
5263
+ _set_gas_price_for(src_chain_id, w3, atx)
5223
5264
  signed = acct.sign_transaction(atx)
5224
5265
  tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
5225
5266
  _ = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
@@ -5240,8 +5281,8 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
5240
5281
  print(f" sendFrom() via LayerZero (LZ {src_cfg['lz_chain_id']} → {dst_cfg['lz_chain_id']})...")
5241
5282
  tx = {"from": wallet, "to": bridge_target, "data": bytes.fromhex(calldata_hex),
5242
5283
  "nonce": w3.eth.get_transaction_count(wallet), "gas": 500000,
5243
- "value": native_fee, "chainId": src_cfg["chain_id"]}
5244
- _set_gas_price(w3, tx)
5284
+ "value": native_fee, "chainId": src_chain_id}
5285
+ _set_gas_price_for(src_chain_id, w3, tx)
5245
5286
  signed = acct.sign_transaction(tx)
5246
5287
  tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
5247
5288
  receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300)
@@ -5252,7 +5293,7 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
5252
5293
  def main():
5253
5294
  args = sys.argv[1:] if len(sys.argv) > 1 else []
5254
5295
  # Global wallet selector: --as <agent>, stripped before command dispatch.
5255
- global _SELECTED_AGENT
5296
+ global _SELECTED_AGENT, _CLI_KEY
5256
5297
  if "--as" in args:
5257
5298
  i = args.index("--as")
5258
5299
  if i + 1 >= len(args):
@@ -5260,6 +5301,14 @@ def main():
5260
5301
  return
5261
5302
  _SELECTED_AGENT = args[i + 1]
5262
5303
  del args[i:i + 2]
5304
+ # Global signing-key override: --key <0xhex>, stripped before command dispatch.
5305
+ if "--key" in args:
5306
+ i = args.index("--key")
5307
+ if i + 1 >= len(args):
5308
+ print("--key requires a hex key. Example: --key 0xabc...")
5309
+ return
5310
+ _CLI_KEY = args[i + 1]
5311
+ del args[i:i + 2]
5263
5312
  if not args or args[0] in ("-h", "--help"):
5264
5313
  print(__doc__)
5265
5314
  return