Accept base58 signers + support delegate (API) keys - #16
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- BulletConfig: update item 3 to mention "private_key" (alias "private_key_hex") accepting hex or base58. Update item 4 to mention "BB_BULLET_PRIVATE_KEY" (env preferred) and "BB_BULLET_PRIVATE_KEY_HEX" (alias/fallback). - load_deposit_keypair doc: mention BB_BULLET_PRIVATE_KEY is tried first, then BB_BULLET_PRIVATE_KEY_HEX as fallback. - bullet_config_from_env doc: same — BB_BULLET_PRIVATE_KEY (preferred) or BB_BULLET_PRIVATE_KEY_HEX (fallback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace unwrap_or_default() with proper error handling to avoid silently masking body-read failures. Mark the error as retryable to match network-error classification of the .send() call above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-precedence doc comment Updated all example configs to reference BB_BULLET_PRIVATE_KEY (accepts base58 or hex) instead of the stale BB_BULLET_PRIVATE_KEY_HEX alias. Fixed doc comment on bullet_config_from_env to correctly state the env var precedence: BB_BULLET_KEY_FILE wins, then the key-string env vars, then the default keystore. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
4 issues found across 20 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
… README clarity Addresses automated PR review (cubic + Cursor Bugbot): - main.rs: treat empty BB_BULLET_PRIVATE_KEY as absent (don't shadow the _HEX alias); drop stale empty private_key_hex before inserting the private_key alias so serde doesn't see a duplicate field. Shared bullet_key_from_env() helper across the three resolution sites. - delegate.rs: classify delegateOf 5xx/429 as retryable. - README: separate the BB_BULLET_PRIVATE_KEY key-string from the BB_BULLET_KEY_FILE keystore-path so the path isn't mistaken for a value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A HL API wallet has no account of its own — positions, balances, and fills live on the master account. The adapter read everything (user_state, open_orders, UserFills/OrderUpdates) from the agent's own address, so API wallets silently broke position tracking. Mirrors the Bullet delegate fix. - HyperliquidConfig: add optional account_address (env BB_HYPERLIQUID_ACCOUNT_ADDRESS). - connection.rs: resolve the read/subscription address (configured master, else the signer's own address); sign with the agent wallet and vault_address: None, matching the SDK's approve_agent pattern. Unit-tested resolver. - main.rs: merge BB_HYPERLIQUID_ACCOUNT_ADDRESS into the HL config (config wins, empty env treated as absent). - docs: README/AGENTS/.env.example — API-wallet setup, and clarify the bot does not auto-load .env (export vars or source it; prefer a Bullet keystore file). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A delegate key pointed at the wrong network 404s on delegateOf and is silently treated as its own account, leading to opaque 'account not found' failures downstream. Log at resolution time whether the signer resolved to itself (with a network-mismatch hint) or to a master, so the failure mode is diagnosable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in funding-arb example The funding-arb example wires both venues but its Hyperliquid comment only mentioned the signing key, so a user with an API wallet would miss the account_address and hit empty reads. Also note that each venue's network must match where the delegate/account lives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… or mainnet) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Symmetric with the Bullet delegate hint: when account_address is unset, an API/agent wallet would silently read empty state from its own address. Log at connect time whether reads target the signer's own address (with a hint to set BB_HYPERLIQUID_ACCOUNT_ADDRESS) or a configured master. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live mainnet/testnet delegateOf returns HTTP 400 (not the spec'd 404) for a
non-delegate address, with an 'is not a delegate' message. The previous code
treated 400 as a hard error, so a normal main-wallet key would abort the bot at
startup. Resolve to self on 404 OR 400-with-'is not a delegate'; a 400 for a
malformed address ('invalid address') stays a real error. Caught by an e2e
smoke against the live endpoint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Breaking changes (no back-compat aliases): - Bullet: canonical env BB_BULLET_PRIVATE_KEY, config field `private_key` (was private_key_hex). Key string is base58 (Phantom/delegation export); hex still parses. - Hyperliquid: canonical env BB_HYPERLIQUID_PRIVATE_KEY, config field `private_key` (was private_key_hex/_HEX). - keygen now writes a base58 secret to a 0600 file (default ~/.config/bullet/ id.key) instead of a Solana JSON keystore; key_file means "file containing a base58/hex key string" and is read via keypair_from_secret. - New bullet key helpers: generate_base58(), keypair_from_key_file(). Adds getrandom dep (no secret accessor on the SDK Keypair). Standard: paste the key exactly as the venue's UI gives it — base58 for Bullet, hex (0x) for Hyperliquid. Docs/examples/.env.example updated throughout. Verified end-to-end on testnet: keygen→key_file→connect→delegateOf round-trips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
3 issues found across 18 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/exchanges/hyperliquid/src/config.rs">
<violation number="1" location="crates/exchanges/hyperliquid/src/config.rs:18">
P1: Renamed `private_key_hex` to `private_key` without backward-compatible serde alias, silently breaking existing TOML configs</violation>
</file>
<file name="crates/exchanges/bullet/src/config.rs">
<violation number="1" location="crates/exchanges/bullet/src/config.rs:32">
P1: Missing `alias = "private_key_hex"` on renamed config field breaks backward compatibility</violation>
</file>
<file name="crates/bb-bot/src/main.rs">
<violation number="1" location="crates/bb-bot/src/main.rs:539">
P2: New `keypair_from_key_file` and path change break old JSON keystores</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Standardizes key management so the surface is identical on every venue and the typical path (paste exported strings into .env, run) just works: - Auto-load ./.env at startup via dotenvy; --env-file <path> override; real env vars take precedence. Removes the manual source-then-run step. - bb_core::keys::resolve_key_string: one shared key-file→inline resolver both adapters call (file preferred, then inline string). read_key_file unifies the file-read path. Unit-tested. - Hyperliquid gains key_file / BB_HYPERLIQUID_KEY_FILE, matching Bullet — closes the file/env asymmetry. - Docs/.env.example lead with the env-string path (base58 Bullet, hex HL) and reflect that .env is now auto-loaded; key file documented as the secret-off- the-env alternative. Verified on testnet: .env auto-load → key_file → connect round-trips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
3 issues found across 13 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…unts HL 'unified account' mode pools collateral in the spot balance; the perp clearinghouse only reports per-position isolated margin. Our get_balances read the perp clearinghouse, so it under-reported a unified account's collateral (e.g. $2 of margin-in-use instead of ~$997 of buying power). - Detect unified mode at connect via the userAbstraction info endpoint (raw request through the SDK's http client; SDK 0.6 has no typed method). - HyperliquidBroker.get_balances branches: unified → user_token_balances (spotClearinghouseState), available = total - hold; standard → perp clearinghouse (prior behavior). Positions still read from the perp clearinghouse, which reports them in both modes. - convert::spot_state_to_balances maps the spot balances; extracted subscribe_feeds to keep connect() under the line limit. Unit tests for the detection parser and the spot-balance mapping. Confirmed live on testnet: userAbstraction returns 'unifiedAccount'; spot shows ~997 USDC vs ~$2 perp accountValue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…leg on partial entry Two bugs found by a live testnet placement test: 1. HL rejected orders with 'Order has invalid price' because the adapter sent the raw price as limit_px. HL perps allow at most 5 significant figures, so an aggressive IoC price like 64710.599 (6 sig figs) was rejected. Add convert::hl_round_price (round_sf(5)) and apply it in place_orders + amend. 2. funding-arb's incomplete-entry handler only cancelled orders and went Flat — but an aggressive (IoC) leg that already filled was orphaned (and, being 'Flat', it would re-enter and accumulate). Now flatten_filled_leg queries the live exchange position (internal inventory lags the async fill event) and closes any filled leg reduce-only before going Flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
flatten was Bullet-only, so a delta-neutral funding-arb position could leave the HL leg open with no CLI to close it. Extract flatten_broker (generic over Broker) and run it for Bullet and, when HL env is set, Hyperliquid (hyperliquid_config_from_env). Used this to close an orphaned HL test leg. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HL echoes cloids on fills/order-updates as '0x'+32 hex (no hyphens), but the client-id map was keyed by Uuid::to_string() (hyphenated). So original_client_id missed, fills came back with an unmapped client_id, and strategies discarded their own HL fills as 'external' — leaving inventory at 0 (funding-arb never confirmed entry; emergency_flatten missed the HL leg). normalize_cloid parses 0x/simple/hyphenated to the canonical Uuid string before lookup. Round-trip test now exercises the actual HL wire format. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
5 issues found across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
enable_signal_shutdown only awaited tokio::signal::ctrl_c() (SIGINT), so a SIGTERM — what docker stop, systemd, and Kubernetes send — bypassed graceful wind_down and left positions open (no emergency_flatten). wait_for_shutdown_signal now selects over Ctrl-C and (on Unix) SIGTERM, so a managed shutdown runs wind_down to completion. Verified on testnet: SIGTERM with an open delta-neutral funding-arb position now triggers emergency_flatten and closes both legs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment |
From automated review of the placement-test fixes: - flatten: skip a venue gracefully if its keys aren't configured (was: always required Bullet keys, so an HL-only setup failed at the Bullet connect). - keys: treat an empty key_file path as absent so it falls back to the inline key; filter empty BB_BULLET_KEY_FILE in load_deposit_keypair / bullet_config_from_env (the run path is covered by resolve_key_string). - funding-arb incomplete entry: only go Flat if cleanup is confirmed; otherwise request shutdown + MANUAL INTERVENTION rather than pretending to be flat while a leg may be open. - funding-arb flatten_filled_leg: cap the close to order_size so a larger pre-existing position on a shared wallet isn't disturbed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the empty-key-file handling: load_config no longer inserts an empty key_file path (from BB_BULLET_KEY_FILE= / BB_HYPERLIQUID_KEY_FILE=) into the exchange table, so an empty env var can't shadow the inline key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ME around testing vs production
The testnet faucet host rejects requests without a browser User-Agent
("Forbidden"), so the curl that keygen printed (and the README documented)
failed for a fresh user. Print/document it with -H "User-Agent: Mozilla/5.0".
README onboarding reframed: 'Quick start — testing (testnet)' leads with the
keygen→faucet→deposit→run path (no wallet/UI, the recommended way to try it);
'Production (mainnet)' covers delegate/API wallets with an explicit warning not
to run mainnet with a keygen burner. Verified the full testnet path end-to-end
on a fresh burner: keygen → faucet → deposit → run (places orders) → flatten.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the copy-paste curl in the quick start. Resolves the address from the same key material as deposit and calls the testnet faucet with a browser User-Agent. The faucet is Cloudflare-protected and rate-limited, so on a 4xx the command prints a clear pointer to the web faucet (https://app.testnet.bullet.xyz) rather than a raw error. keygen and the README now lead with this command. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… capped) Adds MockBroker.set_positions and a test for the asymmetric incomplete-entry path: one leg fills, the other is rejected. Asserts the filled leg is flattened reduce-only and the close is capped to order_size (so a larger pre-existing position on a shared wallet isn't disturbed) — covering the path that was only reasoned about before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cargo-audit flagged RUSTSEC-2026-0185 (quinn-proto < 0.11.15: remote memory exhaustion via unbounded out-of-order stream reassembly). quinn-proto is a transitive dep via reqwest's HTTP/3 stack (unused here). 0.11.15 is the patched version and is within reqwest's range, so a lockfile bump clears it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- main.rs: load only ./.env (from_path, no parent-directory search) so a stray parent .env with different credentials can't be picked up from a subdir. - hyperliquid connection: retry the userAbstraction (unified-account) probe with backoff; on persistent failure warn loudly instead of silently locking the perp balance mode for the session. - hyperliquid convert: spot_state_to_balances uses parse_decimal_or_warn (warns on malformed numerics) instead of silently coercing to zero. - hyperliquid_config_from_env: filter empty BB_HYPERLIQUID_KEY_FILE so flatten skips HL cleanly instead of logging a connect failure. - bb-core keys test: unique temp path per process (no parallel-run collision). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
flatten_filled_leg caps the reduce-only close to order_size (shared-wallet protection). But when the live exchange size EXCEEDS order_size, the cap leaves a remainder — previously it still returned true, so the actor went Flat and could re-enter on residual exposure. Now it returns true only when the live size <= order_size (fully closed); a larger position closes our size, logs, and returns false so the caller halts (request_shutdown + MANUAL INTERVENTION) instead of pretending flat. Test now asserts the actor requests shutdown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sitions - bullet delegate: resolve_account_address now retries transient (network/5xx/429) failures (3 attempts, backoff) before failing connect, so a brief API blip doesn't block a delegate key. Extracted resolve_account_once. - funding-arb emergency_flatten: size closes from the LIVE exchange position (via get_positions) instead of internal inventory, so a fill that landed within the shutdown window isn't skipped on exit. Capped to order_size (shared-wallet protection); logs MANUAL INTERVENTION on remainder/failure. Consistent with the reconnect reconcile, which also reads live positions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0f03852. Configure here.
| /// if `key_file` is not set. | ||
| #[serde(default = "default_secret")] | ||
| pub private_key_hex: SecretString, | ||
| pub private_key: SecretString, |
There was a problem hiding this comment.
Legacy hex key names dropped
High Severity
The PR text says private_key_hex and BB_BULLET_PRIVATE_KEY_HEX (and the Hyperliquid hex env names) still work as aliases, but the code only accepts private_key / BB_BULLET_PRIVATE_KEY and BB_HYPERLIQUID_PRIVATE_KEY. TOML fields under the old names are ignored by serde, and load_config never reads the old env vars, so existing setups can fail at connect with “no key material” or appear configured while the key is empty.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 0f03852. Configure here.
There was a problem hiding this comment.
The dropped legacy names are intentional — this PR is a deliberate breaking change (commit 064fbce, feat!: standardize on base58 for Bullet, hex for Hyperliquid), per the maintainer's call to not carry back-compat aliases. The finding was correct that the PR description was stale/contradictory: an early line still said the old names 'keep working as aliases' while the breaking-changes section said they're removed. Fixed the PR description to consistently state the old *_hex names are removed (no aliases). Code behavior is unchanged and correct.


Why
Two UX gaps made the starter repo hard to use with real Bullet wallets:
Plus: there was no from-scratch onboarding guide for the recommended real-usage path (bring your own wallet → deposit via UI → trade with a scoped delegate).
What changed
Base58 keys
keypair_from_secretin the Bullet adapter auto-detects hex vs base58 (handles Phantom/Solana 64-byte exports and 32-byte seeds).private_key/ envBB_BULLET_PRIVATE_KEY(accept base58 or hex). Breaking change: the oldprivate_key_hex/BB_BULLET_PRIVATE_KEY_HEXnames (and the Hyperliquid*_HEXnames) are removed — there are no back-compat aliases (intentional, see the breaking-changes note below).Delegate support
GET /api/v1/delegateOf(200→ masterparent;404→ not a delegate → self).account_balance/account_info/query_open_orders) and theuser_orderssubscription now target the resolved master address. Transaction signing is unchanged — it still uses the delegate keypair.depositstays signer-only (delegates can't deposit);flatten/observeget delegate resolution for free via the shared connect path.Docs
.env.example, and links to Bullet's machine-readable API docs. Example configs updated to the canonical env name.Verification
delegateOfmapping (200→parent, 404→self, 500→error).cargo clippy --all-targetsclean,cargo +nightly fmt --checkclean.Notes / follow-ups
delegateOf'sexpiresAt/flagsare logged but not enforced.🤖 Generated with Claude Code
Summary by cubic
Adds base58 Bullet key support, master-account reads for delegate/API wallets, auto-loads
.env, and a testnet faucet command. Improves Hyperliquid handling, funding‑arb safety (including shutdown cleanup), and adds graceful SIGTERM shutdown.New Features
private_keyaccepts Bullet base58 (or hex) and Hyperliquid hex;key_filepoints to a file containing that string. Resolver preferskey_file→private_key; empty env values are ignored..envauto-loads from./.env(override with--env-file); real env vars win.keygenwrites a base58 secret to~/.config/bullet/id.key(0600)./api/v1/delegateOfwith transient retries (5xx/429); 404 or 400 “is not a delegate” → self. Reads anduser_orderssubscribe to the master; orders are signed by the delegate. Logs includesignerandaccount.account_address(BB_HYPERLIQUID_ACCOUNT_ADDRESS) to read from the main account when using an API/agent key. Detect unified accounts viauserAbstraction(with retries; warn on failure) and read collateral from spot (available = total - hold). Round order prices to 5 significant figures and normalize cloid formats so fills map to original client IDs. Addkey_file/BB_HYPERLIQUID_KEY_FILE.bb-bot faucetfunds the testnet wallet (sets a browser User‑Agent; suggests the web faucet on 4xx).flattencancels and closes positions on Bullet and Hyperliquid when creds are set, and skips unconfigured venues. Funding‑arb flattens any filled leg on partial entry, caps the close toorder_size, and only goes Flat when cleanup is confirmed; otherwise it requests shutdown. On shutdown (SIGTERM/Ctrl‑C), emergency flatten reads live exchange positions (not internal inventory), caps closes toorder_size, and logs MANUAL INTERVENTION if a remainder/failure is detected.quinn-prototo0.11.15to address RUSTSEC‑2026‑0185.Migration
private_key/BB_BULLET_PRIVATE_KEY(base58 preferred; hex still parses).private_key_hex/BB_BULLET_PRIVATE_KEY_HEXare removed.keygennow creates a base58 key file (id.key), not a JSON keystore. Delegates cannot deposit; use a master key file or non‑delegate key fordeposit. Ensurenetworkmatches where the delegate was created.private_key/BB_HYPERLIQUID_PRIVATE_KEY(hex) orkey_file/BB_HYPERLIQUID_KEY_FILE. When using an API/agent wallet, setaccount_address(orBB_HYPERLIQUID_ACCOUNT_ADDRESS); leave unset only if using your main wallet key. Ensurenetworkmatches your account’s network.Written for commit 0f03852. Summary will update on new commits.