primecli 0.4.0__tar.gz → 0.5.0__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.0
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.0 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.0 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.
@@ -667,6 +669,21 @@ def _set_gas_price(w3, tx_dict):
667
669
  tx_dict["maxPriorityFeePerGas"] = prio
668
670
  else: # Avalanche (43114) — legacy gasPrice
669
671
  tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 25 * 10**9)
672
+
673
+ def _set_gas_price_for(chain_id, w3, tx_dict):
674
+ """Set gas fields for an EXPLICIT chain_id rather than the module CHAIN_ID. Needed by
675
+ cross-chain flows (prime-bridge) where a tx may target Avalanche or Arbitrum regardless
676
+ of which tool built it. Arbitrum/Base (EIP-1559): maxFeePerGas + maxPriorityFeePerGas;
677
+ Avalanche (legacy): gasPrice with a 25 gwei floor."""
678
+ tx_dict.pop("gasPrice", None)
679
+ if chain_id in (42161, 8453): # Arbitrum, Base — EIP-1559
680
+ base = w3.eth.gas_price
681
+ prio = w3.eth.max_priority_fee
682
+ tx_dict["maxFeePerGas"] = max(int(base * 2), base + prio + 10**9)
683
+ tx_dict["maxPriorityFeePerGas"] = prio
684
+ else: # Avalanche (43114) — legacy gasPrice
685
+ tx_dict["gasPrice"] = max(int(w3.eth.gas_price * 2), 25 * 10**9)
686
+
670
687
  def _read_env_var(path, var):
671
688
  """Return the value of `var` from a KEY=VALUE env file, or None if absent."""
672
689
  try:
@@ -693,15 +710,26 @@ def _agent_key(agent):
693
710
  def resolve_private_key():
694
711
  # The same EVM key works on every chain, so each ARBPRIME_ var falls back to its
695
712
  # DELTAPRIME_ equivalent (exactly how degenprime falls back to DELTAPRIME_*).
696
- # 1. --as <agent> CLI flag (set in main)
713
+ # 1. --key <0xhex> CLI flag (set in main)
714
+ if _CLI_KEY:
715
+ return _CLI_KEY.strip()
716
+ # 2. --as <agent> CLI flag (set in main)
697
717
  if _SELECTED_AGENT:
698
718
  return _agent_key(_SELECTED_AGENT)
699
- # 2. raw key directly in the environment
719
+ # 3. raw key directly in the environment
700
720
  for env_var in ("ARBPRIME_PRIVATE_KEY", "DELTAPRIME_PRIVATE_KEY"):
701
721
  raw = os.environ.get(env_var)
702
722
  if raw:
703
723
  return raw.strip()
704
- # 3. explicit env-file + var-name
724
+ # 4. path to a file containing the 0x key
725
+ for path_var in ("ARBPRIME_KEY_FILE", "DELTAPRIME_KEY_FILE"):
726
+ key_file = os.environ.get(path_var)
727
+ if key_file:
728
+ try:
729
+ return Path(key_file).read_text().strip()
730
+ except FileNotFoundError:
731
+ raise RuntimeError(f"{path_var} points at {key_file} but the file does not exist.")
732
+ # 5. explicit env-file + var-name
705
733
  env_file = os.environ.get("ARBPRIME_ENV_FILE") or os.environ.get("DELTAPRIME_ENV_FILE")
706
734
  key_var = os.environ.get("ARBPRIME_KEY_VAR") or os.environ.get("DELTAPRIME_KEY_VAR")
707
735
  if env_file and key_var:
@@ -709,16 +737,25 @@ def resolve_private_key():
709
737
  if not key:
710
738
  raise RuntimeError(f"{key_var} not found in {env_file}.")
711
739
  return key
712
- # 4. named agent in the environment
740
+ # 6. named agent in the environment
713
741
  agent = os.environ.get("ARBPRIME_AGENT") or os.environ.get("DELTAPRIME_AGENT")
714
742
  if agent:
715
743
  return _agent_key(agent)
716
- # 5. default agent (back-compat with the original Parakletos-only behavior)
717
- return _agent_key(DEFAULT_AGENT)
744
+ # No silent default fail closed.
745
+ raise RuntimeError(
746
+ "No signing key found. Pass --key <0xhex> or --as <agent>, or set "
747
+ "ARBPRIME_PRIVATE_KEY (raw 0x... key), ARBPRIME_KEY_FILE (path to a file with "
748
+ "the key), or ARBPRIME_ENV_FILE + ARBPRIME_KEY_VAR. DELTAPRIME_* equivalents "
749
+ "also work (same key, all chains)."
750
+ )
718
751
 
719
752
  def get_account() -> Account:
720
753
  return Account.from_key(resolve_private_key())
721
754
 
755
+ def to_wei_units(amount, decimals):
756
+ """Convert a human amount to integer base units without float drift."""
757
+ return int(Decimal(str(amount)) * (10 ** int(decimals)))
758
+
722
759
  def get_pool_contract(pool_name: str):
723
760
  """Pool proxy contract bound directly to the hand-curated POOL_ABI (no block-explorer
724
761
  ABI fetch — Arbiscan is display-only here; the hand-curated subset covers every
@@ -1376,7 +1413,7 @@ def cmd_my_positions():
1376
1413
  def cmd_deposit(pool_name: str, amount: float, execute: bool = False):
1377
1414
  contract, cfg, w3 = get_pool_contract(pool_name)
1378
1415
  acct = get_account()
1379
- amount_wei = int(amount * 10**cfg["decimals"])
1416
+ amount_wei = to_wei_units(amount, cfg["decimals"])
1380
1417
 
1381
1418
  if not execute:
1382
1419
  print(f"Preview: Deposit {amount} {cfg['symbol']} into {pool_name.upper()} pool")
@@ -1433,7 +1470,7 @@ def cmd_withdraw(pool_name: str, amount: float, execute: bool = False):
1433
1470
  """
1434
1471
  contract, cfg, w3 = get_pool_contract(pool_name)
1435
1472
  acct = get_account()
1436
- amount_wei = int(amount * 10**cfg["decimals"])
1473
+ amount_wei = to_wei_units(amount, cfg["decimals"])
1437
1474
 
1438
1475
  # getBalanceOf is the lender's current deposit balance — sane upper bound for
1439
1476
  # the intent amount. The pool reverts "Amount must be greater than zero" on 0,
@@ -1670,7 +1707,7 @@ def cmd_create_prime_account(execute: bool = False, fund_pool: str = None, fund_
1670
1707
  print(f"Preview: Create a new Prime Account for {acct.address}")
1671
1708
  if funding:
1672
1709
  symbol = cfg["symbol"]
1673
- amount_wei = int(fund_amount * 10**cfg["decimals"])
1710
+ amount_wei = to_wei_units(fund_amount, cfg["decimals"])
1674
1711
  print(f" Factory: {FACTORY_PROXY} (SmartLoansFactory.createAndFundLoan())")
1675
1712
  print(f" Approves the factory to spend {fund_amount} {symbol}, then")
1676
1713
  print(f" calls createAndFundLoan(bytes32 '{symbol}', {amount_wei}) — creates + funds in one go.")
@@ -1683,7 +1720,7 @@ def cmd_create_prime_account(execute: bool = False, fund_pool: str = None, fund_
1683
1720
 
1684
1721
  if funding:
1685
1722
  symbol = cfg["symbol"]
1686
- amount_wei = int(fund_amount * 10**cfg["decimals"])
1723
+ amount_wei = to_wei_units(fund_amount, cfg["decimals"])
1687
1724
  # createAndFundLoan does token.transferFrom(msg.sender, factory, amount),
1688
1725
  # so approve the factory first.
1689
1726
  token = w3.eth.contract(address=Web3.to_checksum_address(cfg["token"]),
@@ -1746,7 +1783,7 @@ def cmd_fund(pool_name: str, amount: float, execute: bool = False):
1746
1783
  return
1747
1784
 
1748
1785
  symbol = pool_to_asset_symbol(pool_name)
1749
- amount_wei = int(amount * 10**cfg["decimals"])
1786
+ amount_wei = to_wei_units(amount, cfg["decimals"])
1750
1787
  pa_cs = Web3.to_checksum_address(pa)
1751
1788
 
1752
1789
  if not execute:
@@ -1980,7 +2017,7 @@ def cmd_borrow(pool_name: str, amount: float, execute: bool = False):
1980
2017
  return
1981
2018
 
1982
2019
  symbol = pool_to_asset_symbol(pool_name)
1983
- amount_wei = int(amount * 10**cfg["decimals"])
2020
+ amount_wei = to_wei_units(amount, cfg["decimals"])
1984
2021
  if not execute:
1985
2022
  print(f"Preview: Borrow {amount} {symbol} into Prime Account {pa}")
1986
2023
  print(f" Calls borrow(bytes32 '{symbol}', {amount_wei}) on the Prime Account")
@@ -2027,7 +2064,7 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
2027
2064
  # The facet's repay reverts if amount > debt OR amount > in-account balance.
2028
2065
  # Cap to min(requested, debt, in_account) so callers don't need to know either
2029
2066
  # exact figure — pass an overshoot like 9999 and it clips cleanly.
2030
- requested_wei = int(amount * 10**cfg["decimals"])
2067
+ requested_wei = to_wei_units(amount, cfg["decimals"])
2031
2068
  debt_wei = pool.functions.getBorrowed(pa_cs).call()
2032
2069
  in_acct_wei = account.functions.getBalance(asset_b32(symbol)).call()
2033
2070
  if debt_wei == 0:
@@ -2297,7 +2334,7 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
2297
2334
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
2298
2335
 
2299
2336
  from_cfg, to_cfg = SWAP_ASSETS[from_sym], SWAP_ASSETS[to_sym]
2300
- amount_in = int(amount * 10**from_cfg["decimals"])
2337
+ amount_in = to_wei_units(amount, from_cfg["decimals"])
2301
2338
 
2302
2339
  # In-account balance check (oracle-free view).
2303
2340
  in_balance = account.functions.getBalance(asset_b32(from_sym)).call()
@@ -2395,7 +2432,7 @@ def _calc_swap_debt_amounts(w3, account, from_sym, to_sym, amount):
2395
2432
  borrowed = from_pool.functions.getBorrowed(pa_cs).call()
2396
2433
  if borrowed == 0:
2397
2434
  raise ValueError("zero_old_debt")
2398
- repay_amount = min(int(amount * 10**from_cfg["decimals"]), borrowed)
2435
+ repay_amount = min(to_wei_units(amount, from_cfg["decimals"]), borrowed)
2399
2436
 
2400
2437
  # Value-match the new borrow to the repay using the facet's own RedStone prices.
2401
2438
  feeds = prime_account_price_feeds(account)
@@ -2749,7 +2786,7 @@ def cmd_withdraw_collateral(pool_name: str, amount: float, execute: bool = False
2749
2786
  return
2750
2787
 
2751
2788
  symbol = pool_to_asset_symbol(pool_name)
2752
- amount_wei = int(amount * 10**cfg["decimals"])
2789
+ amount_wei = to_wei_units(amount, cfg["decimals"])
2753
2790
  pa_cs = Web3.to_checksum_address(pa)
2754
2791
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
2755
2792
 
@@ -3244,7 +3281,7 @@ def cmd_gmx_deposit(market: str, amount: float, is_long: bool | None = None,
3244
3281
  pa_cs = Web3.to_checksum_address(pa)
3245
3282
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
3246
3283
 
3247
- amount_wei = int(amount * 10**dep_cfg["decimals"])
3284
+ amount_wei = to_wei_units(amount, dep_cfg["decimals"])
3248
3285
  in_balance = account.functions.getBalance(asset_b32(dep_sym)).call()
3249
3286
  if amount_wei > in_balance:
3250
3287
  print(f"Prime Account holds only {in_balance / 10**dep_cfg['decimals']:.6f} {dep_sym} "
@@ -3354,7 +3391,7 @@ def cmd_gmx_withdraw(market: str, amount: float, slippage_pct: float = 1.0,
3354
3391
  gm_cs = Web3.to_checksum_address(mkt["gm_token"])
3355
3392
  erc = json.loads('[{"inputs":[{"name":"a","type":"address"}],"name":"balanceOf","outputs":[{"type":"uint256"}],"stateMutability":"view","type":"function"}]')
3356
3393
  gm_bal = w3.eth.contract(address=gm_cs, abi=erc).functions.balanceOf(pa_cs).call()
3357
- gm_amount = int(amount * 10**GM_TOKEN_DECIMALS)
3394
+ gm_amount = to_wei_units(amount, GM_TOKEN_DECIMALS)
3358
3395
  if gm_bal == 0:
3359
3396
  print(f"Prime Account holds no {mkt['gm_feed']} GM tokens — nothing to withdraw.")
3360
3397
  return
@@ -3600,7 +3637,7 @@ def cmd_glv_deposit(vault_key: str, amount: float, is_long: bool | None = None,
3600
3637
  dep_meta = long_meta if is_long_eff else short_meta
3601
3638
  dep_sym = dep_meta["symbol"]
3602
3639
 
3603
- amount_wei = int(amount * 10**dep_meta["decimals"])
3640
+ amount_wei = to_wei_units(amount, dep_meta["decimals"])
3604
3641
  in_balance = account.functions.getBalance(asset_b32(dep_sym)).call()
3605
3642
  if amount_wei > in_balance:
3606
3643
  print(f"Prime Account holds only {in_balance / 10**dep_meta['decimals']:.6f} {dep_sym} "
@@ -3717,7 +3754,7 @@ def cmd_glv_withdraw(vault_key: str, amount: float, target_market: str = None,
3717
3754
  glv_cs = Web3.to_checksum_address(vault["glv_token"])
3718
3755
  erc = json.loads('[{"inputs":[{"name":"a","type":"address"}],"name":"balanceOf","outputs":[{"type":"uint256"}],"stateMutability":"view","type":"function"}]')
3719
3756
  glv_bal = w3.eth.contract(address=glv_cs, abi=erc).functions.balanceOf(pa_cs).call()
3720
- glv_amount = int(amount * 10**GLV_TOKEN_DECIMALS)
3757
+ glv_amount = to_wei_units(amount, GLV_TOKEN_DECIMALS)
3721
3758
  if glv_bal == 0:
3722
3759
  print(f"Prime Account holds no [{vault_key}] GLV tokens — nothing to withdraw.")
3723
3760
  return
@@ -4055,8 +4092,8 @@ def cmd_lb_add(pair_key: str, amount_x: float, amount_y: float, shape: str = "sp
4055
4092
  pa_cs = Web3.to_checksum_address(pa)
4056
4093
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
4057
4094
 
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
4095
+ amount_x_wei = to_wei_units(amount_x, x_cfg["decimals"]) if has_x else 0
4096
+ amount_y_wei = to_wei_units(amount_y, y_cfg["decimals"]) if has_y else 0
4060
4097
 
4061
4098
  # In-account balances (oracle-free), keyed by the TokenManager symbol the facet uses.
4062
4099
  bal_x = account.functions.getBalance(asset_b32(x_cfg["symbol"])).call()
@@ -4382,7 +4419,7 @@ def cmd_prime_activate(amount: float = None, execute: bool = False):
4382
4419
  print(f"Prime Account {pa} is already in PREMIUM tier — nothing to do.")
4383
4420
  return
4384
4421
 
4385
- deposit_wei = int(amount * 10**PRIME_TOKEN["decimals"]) if amount else 0
4422
+ deposit_wei = to_wei_units(amount, PRIME_TOKEN["decimals"]) if amount else 0
4386
4423
  eoa_prime = prime.functions.balanceOf(acct.address).call()
4387
4424
  in_acct_prime = account.functions.getBalance(asset_b32(PRIME_TOKEN["symbol"])).call()
4388
4425
  # depositPrime caps to the EOA balance on-chain; mirror that for an honest preview.
@@ -4490,7 +4527,7 @@ def cmd_prime_deposit(amount: float, execute: bool = False):
4490
4527
 
4491
4528
  eoa_prime = prime.functions.balanceOf(acct.address).call()
4492
4529
  in_acct_prime = account.functions.getBalance(asset_b32(PRIME_TOKEN["symbol"])).call()
4493
- deposit_wei = int(amount * 10**PRIME_TOKEN["decimals"])
4530
+ deposit_wei = to_wei_units(amount, PRIME_TOKEN["decimals"])
4494
4531
  deposit_wei = min(deposit_wei, eoa_prime) # depositPrime caps to the EOA balance on-chain
4495
4532
 
4496
4533
  print(f"PRIME deposit into Prime Account {pa}")
@@ -4602,7 +4639,7 @@ def cmd_prime_unstake(amount: float, execute: bool = False):
4602
4639
  if staked == 0:
4603
4640
  print(f"Prime Account {pa} has no staked PRIME — nothing to unstake.")
4604
4641
  return
4605
- amount_wei = int(amount * 10**PRIME_TOKEN["decimals"])
4642
+ amount_wei = to_wei_units(amount, PRIME_TOKEN["decimals"])
4606
4643
  if amount_wei > staked:
4607
4644
  print(f"Staked PRIME is {staked / 10**PRIME_TOKEN['decimals']:,.6f}; clamping unstake to that.")
4608
4645
  amount_wei = staked
@@ -4649,7 +4686,7 @@ def cmd_prime_repay(amount: float, execute: bool = False):
4649
4686
 
4650
4687
  _tier, _staked, recorded_debt = account.functions.getLeverageTierFullInfo().call()
4651
4688
  in_acct_prime = account.functions.getBalance(asset_b32(PRIME_TOKEN["symbol"])).call()
4652
- amount_wei = int(amount * 10**PRIME_TOKEN["decimals"])
4689
+ amount_wei = to_wei_units(amount, PRIME_TOKEN["decimals"])
4653
4690
 
4654
4691
  print(f"PRIME repay debt: {amount} PRIME on Prime Account {pa}")
4655
4692
  print(f" Recorded PRIME debt: {recorded_debt / 10**PRIME_TOKEN['decimals']:,.6f} "
@@ -5141,7 +5178,7 @@ def _bridge_estimate_lz_fee(w3, src_cfg: dict, dst_lz_chain_id: int, amount_wei:
5141
5178
  "stateMutability": "view", "type": "function"}])))
5142
5179
  native, zro = ep.functions.estimateFees(
5143
5180
  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),
5181
+ _bridge_lz_payload(get_account().address, amount_wei),
5145
5182
  False, BRIDGE_ADAPTER_PARAMS).call()
5146
5183
  return native, zro
5147
5184
 
@@ -5164,7 +5201,8 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
5164
5201
  dst_key = "arb" if src_key == "avax" else "avax"
5165
5202
  src_cfg = BRIDGE_CHAIN[src_key]
5166
5203
  dst_cfg = BRIDGE_CHAIN[dst_key]
5167
- amount_wei = int(amount * 10**18)
5204
+ src_chain_id = src_cfg["chain_id"]
5205
+ amount_wei = to_wei_units(amount, 18)
5168
5206
 
5169
5207
  w3 = _bridge_w3(src_key)
5170
5208
  acct = get_account()
@@ -5218,8 +5256,8 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
5218
5256
  "name": "approve", "outputs": [{"name": "", "type": "bool"}], "type": "function"}])))
5219
5257
  atx = app_c.functions.approve(bridge_target, amount_wei).build_transaction(
5220
5258
  {"from": wallet, "nonce": w3.eth.get_transaction_count(wallet),
5221
- "gas": 100000, "chainId": src_cfg["chain_id"]})
5222
- _set_gas_price(w3, atx)
5259
+ "gas": 100000, "chainId": src_chain_id})
5260
+ _set_gas_price_for(src_chain_id, w3, atx)
5223
5261
  signed = acct.sign_transaction(atx)
5224
5262
  tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
5225
5263
  _ = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
@@ -5240,8 +5278,8 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
5240
5278
  print(f" sendFrom() via LayerZero (LZ {src_cfg['lz_chain_id']} → {dst_cfg['lz_chain_id']})...")
5241
5279
  tx = {"from": wallet, "to": bridge_target, "data": bytes.fromhex(calldata_hex),
5242
5280
  "nonce": w3.eth.get_transaction_count(wallet), "gas": 500000,
5243
- "value": native_fee, "chainId": src_cfg["chain_id"]}
5244
- _set_gas_price(w3, tx)
5281
+ "value": native_fee, "chainId": src_chain_id}
5282
+ _set_gas_price_for(src_chain_id, w3, tx)
5245
5283
  signed = acct.sign_transaction(tx)
5246
5284
  tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
5247
5285
  receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300)
@@ -5252,7 +5290,7 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
5252
5290
  def main():
5253
5291
  args = sys.argv[1:] if len(sys.argv) > 1 else []
5254
5292
  # Global wallet selector: --as <agent>, stripped before command dispatch.
5255
- global _SELECTED_AGENT
5293
+ global _SELECTED_AGENT, _CLI_KEY
5256
5294
  if "--as" in args:
5257
5295
  i = args.index("--as")
5258
5296
  if i + 1 >= len(args):
@@ -5260,6 +5298,14 @@ def main():
5260
5298
  return
5261
5299
  _SELECTED_AGENT = args[i + 1]
5262
5300
  del args[i:i + 2]
5301
+ # Global signing-key override: --key <0xhex>, stripped before command dispatch.
5302
+ if "--key" in args:
5303
+ i = args.index("--key")
5304
+ if i + 1 >= len(args):
5305
+ print("--key requires a hex key. Example: --key 0xabc...")
5306
+ return
5307
+ _CLI_KEY = args[i + 1]
5308
+ del args[i:i + 2]
5263
5309
  if not args or args[0] in ("-h", "--help"):
5264
5310
  print(__doc__)
5265
5311
  return
@@ -274,6 +274,10 @@ def resolve_private_key():
274
274
  def get_account() -> Account:
275
275
  return Account.from_key(resolve_private_key())
276
276
 
277
+ def to_wei_units(amount, decimals):
278
+ """Convert a human amount to integer base units without float drift."""
279
+ return int(Decimal(str(amount)) * (10 ** int(decimals)))
280
+
277
281
  # Basescan's v1 API is deprecated (returns "switch to Etherscan API V2" since 2026),
278
282
  # and the v2 multichain endpoint (api.etherscan.io/v2/api?chainid=8453) requires an API
279
283
  # key with no anonymous reads. Rather than depend on an API key for what is a tiny,
@@ -928,7 +932,7 @@ def cmd_my_positions():
928
932
  def cmd_deposit(pool_name: str, amount: float, execute: bool = False):
929
933
  contract, cfg, w3 = get_pool_contract(pool_name)
930
934
  acct = get_account()
931
- amount_wei = int(amount * 10**cfg["decimals"])
935
+ amount_wei = to_wei_units(amount, cfg["decimals"])
932
936
  print(f"Wallet: {acct.address}")
933
937
 
934
938
  if not execute:
@@ -984,7 +988,7 @@ def cmd_withdraw(pool_name: str, amount: float, execute: bool = False):
984
988
  The pool also exposes withdrawNativeToken — future --native flag could opt in."""
985
989
  contract, cfg, w3 = get_pool_contract(pool_name)
986
990
  acct = get_account()
987
- amount_wei = int(amount * 10**cfg["decimals"])
991
+ amount_wei = to_wei_units(amount, cfg["decimals"])
988
992
  print(f"Wallet: {acct.address}")
989
993
 
990
994
  if not execute:
@@ -1149,7 +1153,7 @@ def cmd_create_account(execute: bool = False, fund_pool: str = None, fund_amount
1149
1153
  print(f"Preview: Create a new Degen Account for {acct.address}")
1150
1154
  if funding:
1151
1155
  symbol = cfg["symbol"]
1152
- amount_wei = int(fund_amount * 10**cfg["decimals"])
1156
+ amount_wei = to_wei_units(fund_amount, cfg["decimals"])
1153
1157
  print(f" Factory: {FACTORY_PROXY} (SmartLoansFactory.createAndFundLoan())")
1154
1158
  print(f" Approves the factory to spend {fund_amount} {symbol}, then")
1155
1159
  print(f" calls createAndFundLoan(bytes32 '{symbol}', {amount_wei}) - creates + funds in one go.")
@@ -1162,7 +1166,7 @@ def cmd_create_account(execute: bool = False, fund_pool: str = None, fund_amount
1162
1166
 
1163
1167
  if funding:
1164
1168
  symbol = cfg["symbol"]
1165
- amount_wei = int(fund_amount * 10**cfg["decimals"])
1169
+ amount_wei = to_wei_units(fund_amount, cfg["decimals"])
1166
1170
  token = w3.eth.contract(address=Web3.to_checksum_address(cfg["token"]), abi=ERC20_ABI)
1167
1171
  app_tx = token.functions.approve(factory_cs, amount_wei).build_transaction({
1168
1172
  "from": acct.address, "nonce": w3.eth.get_transaction_count(acct.address),
@@ -1223,7 +1227,7 @@ def cmd_fund(pool_name: str, amount: float, execute: bool = False):
1223
1227
  return
1224
1228
 
1225
1229
  symbol = pool_to_asset_symbol(pool_name)
1226
- amount_wei = int(amount * 10**cfg["decimals"])
1230
+ amount_wei = to_wei_units(amount, cfg["decimals"])
1227
1231
  pa_cs = Web3.to_checksum_address(pa)
1228
1232
 
1229
1233
  if not execute:
@@ -1515,7 +1519,7 @@ def cmd_borrow(pool_name: str, amount: float, execute: bool = False):
1515
1519
  return
1516
1520
 
1517
1521
  symbol = pool_to_asset_symbol(pool_name)
1518
- amount_wei = int(amount * 10**cfg["decimals"])
1522
+ amount_wei = to_wei_units(amount, cfg["decimals"])
1519
1523
  if not execute:
1520
1524
  print(f"Preview: Borrow {amount} {symbol} into Degen Account {pa}")
1521
1525
  print(f" Calls borrow(bytes32 '{symbol}', {amount_wei}) on the Degen Account")
@@ -1569,7 +1573,7 @@ def cmd_repay(pool_name: str, amount: float, execute: bool = False):
1569
1573
  pa_cs = Web3.to_checksum_address(pa)
1570
1574
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
1571
1575
  pool, _, _ = get_pool_contract(pool_name)
1572
- requested_wei = int(amount * 10**cfg["decimals"])
1576
+ requested_wei = to_wei_units(amount, cfg["decimals"])
1573
1577
  debt_wei = pool.functions.getBorrowed(pa_cs).call()
1574
1578
  in_acct_wei = account.functions.getBalance(asset_b32(symbol)).call()
1575
1579
  if debt_wei == 0:
@@ -1767,7 +1771,7 @@ def cmd_swap(from_sym: str, to_sym: str, amount: float, slippage_pct: float = 1.
1767
1771
  "or any TokenManager-listed collateral symbol.")
1768
1772
  return
1769
1773
 
1770
- amount_in = int(amount * 10**from_cfg["decimals"])
1774
+ amount_in = to_wei_units(amount, from_cfg["decimals"])
1771
1775
  in_balance = account.functions.getBalance(asset_b32(from_sym)).call()
1772
1776
  if amount_in > in_balance:
1773
1777
  print(f"Degen Account holds only {in_balance / 10**from_cfg['decimals']:.6f} {from_sym} "
@@ -1884,7 +1888,7 @@ def cmd_swap_debt(from_sym: str, to_sym: str, amount: float, slippage_pct: float
1884
1888
  if borrowed == 0:
1885
1889
  print(f"Degen Account has no {from_sym} debt to refinance.")
1886
1890
  return
1887
- repay_amount = min(int(amount * 10**from_cfg["decimals"]), borrowed)
1891
+ repay_amount = min(to_wei_units(amount, from_cfg["decimals"]), borrowed)
1888
1892
 
1889
1893
  feeds = degen_account_price_feeds(account)
1890
1894
  for s in (from_sym, to_sym):
@@ -2003,7 +2007,7 @@ def cmd_withdraw_collateral(pool_name: str, amount: float, execute: bool = False
2003
2007
  return
2004
2008
 
2005
2009
  symbol = pool_to_asset_symbol(pool_name)
2006
- amount_wei = int(amount * 10**cfg["decimals"])
2010
+ amount_wei = to_wei_units(amount, cfg["decimals"])
2007
2011
  pa_cs = Web3.to_checksum_address(pa)
2008
2012
  account = w3.eth.contract(address=pa_cs, abi=PRIME_ACCOUNT_ABI)
2009
2013