Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ Canonical top-level aliases accepted by the CLI:

API wallets can sign trading actions for the approving master account, but they cannot withdraw. Use the master or subaccount address for info queries. When `api-wallet create` generates a local agent keypair, it prints the private key once before submitting `approveAgent` for that address; store that key securely because the CLI does not automatically recover it later.

Signer and acting-account flags intentionally share the same selector grammar where it is safe: raw `0x` address, stored wallet/account alias, or stored wallet/account id. Their roles differ. Global `--account` / `--wallet` selects the key that signs the action; per-command `--on-behalf-of` selects the protocol account, subaccount, or vault supplied as Hyperliquid `vaultAddress` for that action.

### Trading and transfers

| Command | Description |
Expand All @@ -284,13 +286,13 @@ API wallets can sign trading actions for the approving master account, but they
| `orders scale --coin <COIN> --side buy\|sell --start-price <PX> --end-price <PX> --total-size <SIZE> --orders <N>` | Create an evenly spaced batch of limit orders. |
| `orders batch-create --orders-file <PATH>` | Create a batch of limit orders from JSON. |
| `orders create --coin <COIN> --side buy\|sell [--take-profit <PX>] [--stop-loss <PX>] [--grouping normal-tpsl] ...` | Create a parent order with fixed-size TP/SL children. |
| `orders tpsl --coin <COIN> (--take-profit <PX>\|--stop-loss <PX>) [--grouping position-tpsl] [--side buy\|sell] [--size <SIZE>] [--margin-mode cross\|isolated]` | Create TP/SL orders attached to the current position. |
| `orders cancel (ORDER_ID\|--cloid <CLOID>)` | Cancel by order ID or client order ID. |
| `orders cancel-all [--coin <COIN>] [--dex <DEX>] [-y]` | Cancel all open orders, optionally filtered by coin or DEX. |
| `orders modify (ORDER_ID\|--cloid <CLOID>) [--price <PRICE>] [--trigger-price <PRICE>] [--size <SIZE>]` | Modify an existing order. |
| `orders twap-create --coin <COIN> --side buy\|sell --size <SIZE> --duration <SECONDS> [--dex <DEX>] [--margin-mode cross\|isolated] [-y]` | Create a TWAP order. |
| `orders twap-cancel <TWAP_ID> --coin <COIN> [--dex <DEX>]` | Cancel a TWAP order. |
| `orders schedule-cancel (--in <DURATION>\|--clear)` | Configure a dead man's switch. |
| `orders tpsl --coin <COIN> (--take-profit <PX>\|--stop-loss <PX>) [--grouping position-tpsl] [--side buy\|sell] [--size <SIZE>] [--margin-mode cross\|isolated] [--on-behalf-of <ACCOUNT_SELECTOR>]` | Create TP/SL orders attached to the current position. `--on-behalf-of` targets the acting subaccount/vault position. |
| `orders cancel (ORDER_ID\|--cloid <CLOID>) [--on-behalf-of <ACCOUNT_SELECTOR>]` | Cancel by order ID or client order ID. `--on-behalf-of` manages an order for the acting subaccount/vault. |
| `orders cancel-all [--coin <COIN>] [--dex <DEX>] [--on-behalf-of <ACCOUNT_SELECTOR>] [-y]` | Cancel all open orders, optionally filtered by coin or DEX. |
| `orders modify (ORDER_ID\|--cloid <CLOID>) [--price <PRICE>] [--trigger-price <PRICE>] [--size <SIZE>] [--on-behalf-of <ACCOUNT_SELECTOR>]` | Modify an existing order. |
| `orders twap-create --coin <COIN> --side buy\|sell --size <SIZE> --duration <SECONDS> [--dex <DEX>] [--margin-mode cross\|isolated] [--on-behalf-of <ACCOUNT_SELECTOR>] [-y]` | Create a TWAP order. |
| `orders twap-cancel <TWAP_ID> --coin <COIN> [--dex <DEX>] [--on-behalf-of <ACCOUNT_SELECTOR>]` | Cancel a TWAP order. |
| `orders schedule-cancel (--in <DURATION>\|--clear) [--on-behalf-of <ACCOUNT_SELECTOR>] [-y]` | Configure or clear a dead man's switch. Mainnet set and clear flows prompt unless `--yes` is supplied. |
| `positions list [-w] [--max-ticks <TICKS>]` | List open positions. |
| `positions update-leverage --coin <COIN> --leverage <N> [--isolated]` | Update leverage. |
| `positions update-margin --coin <COIN> --amount <AMOUNT>` | Add or remove isolated margin. |
Expand Down
151 changes: 133 additions & 18 deletions src/cli_runtime.rs

Large diffs are not rendered by default.

52 changes: 51 additions & 1 deletion src/command_catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,12 @@
"description": "HIP-3 DEX name.",
"input_kind": "venue_selector"
},
{
"id": "on_behalf_of",
"long": "on-behalf-of",
"description": "Acting-account selector for vaultAddress: subaccount/vault address, stored account alias, or stored account id.",
"input_kind": "signer_selector"
},
{
"id": "take_profit",
"long": "take-profit",
Expand Down Expand Up @@ -2274,6 +2280,12 @@
"long": "cloid",
"description": "Client order id.",
"input_kind": "identifier"
},
{
"id": "on_behalf_of",
"long": "on-behalf-of",
"description": "Acting-account selector for vaultAddress: subaccount/vault address, stored account alias, or stored account id.",
"input_kind": "signer_selector"
}
],
"lifecycle": "live_mutating",
Expand Down Expand Up @@ -2302,6 +2314,12 @@
"description": "HIP-3 DEX name for the coin filter.",
"input_kind": "venue_selector"
},
{
"id": "on_behalf_of",
"long": "on-behalf-of",
"description": "Acting-account selector for vaultAddress: subaccount/vault address, stored account alias, or stored account id.",
"input_kind": "signer_selector"
},
{
"id": "yes",
"long": "yes",
Expand Down Expand Up @@ -2363,6 +2381,12 @@
"long": "size",
"description": "Replacement size.",
"input_kind": "amount"
},
{
"id": "on_behalf_of",
"long": "on-behalf-of",
"description": "Acting-account selector for vaultAddress: subaccount/vault address, stored account alias, or stored account id.",
"input_kind": "signer_selector"
}
],
"lifecycle": "live_mutating",
Expand Down Expand Up @@ -2392,6 +2416,12 @@
"description": "HIP-3 DEX name.",
"input_kind": "venue_selector"
},
{
"id": "on_behalf_of",
"long": "on-behalf-of",
"description": "Acting-account selector for vaultAddress: subaccount/vault address, stored account alias, or stored account id.",
"input_kind": "signer_selector"
},
{
"id": "side",
"long": "side",
Expand Down Expand Up @@ -2469,6 +2499,12 @@
"long": "dex",
"description": "HIP-3 DEX name.",
"input_kind": "venue_selector"
},
{
"id": "on_behalf_of",
"long": "on-behalf-of",
"description": "Acting-account selector for vaultAddress: subaccount/vault address, stored account alias, or stored account id.",
"input_kind": "signer_selector"
}
],
"lifecycle": "live_mutating",
Expand Down Expand Up @@ -2563,13 +2599,27 @@
"required": false,
"description": "Remove an existing scheduled cancel trigger instead of setting one.",
"input_kind": "boolean"
},
{
"id": "on_behalf_of",
"long": "on-behalf-of",
"description": "Acting-account selector for vaultAddress: subaccount/vault address, stored account alias, or stored account id.",
"input_kind": "signer_selector"
},
{
"id": "yes",
"long": "yes",
"short": "y",
"arg_type": "boolean",
"description": "Skip confirmation prompt for deliberate automation.",
"input_kind": "boolean"
}
],
"lifecycle": "live_mutating",
"risk": "funds_movement",
"dry_run": "optional",
"raw_payload": "dry_run_only",
"confirmation": "none",
"confirmation": "prompt",
"ows_signer": "experimental_feature_gated"
},
{
Expand Down
86 changes: 66 additions & 20 deletions src/commands/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ mod validation;

pub use args::*;
use planning::{
CreateOrderSubmission, prepare_batch_create_order_plan, prepare_cancel_all_orders_plan,
prepare_cancel_order_plan, prepare_create_order_plan, prepare_modify_order_plan,
prepare_position_tpsl_batch, prepare_scale_batch, prepare_schedule_cancel_plan,
prepare_twap_cancel_plan, prepare_twap_create_plan,
CreateOrderSubmission, ScheduleCancelPlan, prepare_batch_create_order_plan,
prepare_cancel_all_orders_plan, prepare_cancel_order_plan, prepare_create_order_plan,
prepare_modify_order_plan, prepare_position_tpsl_batch, prepare_scale_batch,
prepare_schedule_cancel_plan, prepare_twap_cancel_plan, prepare_twap_create_plan,
};
pub use planning::{
OrderDryRunPlan, batch_create_dry_run_args, batch_create_dry_run_plan, cancel_all_dry_run_plan,
Expand Down Expand Up @@ -386,16 +386,13 @@ async fn place_order_batch(
pub async fn tpsl(
context: OrderExecutionContext<'_>,
args: &TpslArgs,
vault_address: Option<Address>,
format: OutputFormat,
) -> Result<(), anyhow::Error> {
let start = Instant::now();
let prepared = prepare_position_tpsl_batch(
context.client,
context.resolver,
context.submission.signer,
args,
)
.await?;
let user = vault_address.unwrap_or_else(|| context.submission.signer.query_address());
let prepared =
prepare_position_tpsl_batch(context.client, context.resolver, user, args).await?;
if context.submission.require_mainnet_confirmation
&& !args.yes
&& !confirm_mainnet_tpsl_batch(&prepared, format)?
Expand All @@ -412,7 +409,7 @@ pub async fn tpsl(
context.submission.chain,
context.submission.signer,
prepared.batch.clone(),
None,
vault_address,
)
.await?;
let rows = tpsl_confirmation_rows(&prepared, statuses)?;
Expand All @@ -429,6 +426,7 @@ pub async fn cancel(
context: OrderExecutionContext<'_>,
user: Address,
args: &CancelArgs,
vault_address: Option<Address>,
format: OutputFormat,
) -> Result<(), anyhow::Error> {
context
Expand All @@ -444,7 +442,7 @@ pub async fn cancel(
context.submission.signer,
plan.action,
nonce,
None,
vault_address,
"cancel failed",
)
.await?;
Expand All @@ -471,6 +469,7 @@ pub async fn cancel_all(
context: OrderExecutionContext<'_>,
user: Address,
args: &CancelAllArgs,
vault_address: Option<Address>,
format: OutputFormat,
) -> Result<(), anyhow::Error> {
context
Expand All @@ -493,7 +492,7 @@ pub async fn cancel_all(
context.submission.signer,
action,
nonce,
None,
vault_address,
"cancel-all failed",
)
.await?;
Expand Down Expand Up @@ -521,6 +520,7 @@ pub async fn modify(
context: OrderExecutionContext<'_>,
user: Address,
args: &ModifyArgs,
vault_address: Option<Address>,
format: OutputFormat,
) -> Result<(), anyhow::Error> {
context
Expand All @@ -536,7 +536,7 @@ pub async fn modify(
context.submission.signer,
plan.action,
nonce,
None,
vault_address,
"modify failed",
)
.await?;
Expand All @@ -560,6 +560,7 @@ pub async fn twap_create(
context: OrderSubmissionContext<'_>,
resolver: &AssetResolver,
args: &TwapCreateArgs,
vault_address: Option<Address>,
format: OutputFormat,
) -> Result<(), anyhow::Error> {
let start = Instant::now();
Expand All @@ -580,7 +581,7 @@ pub async fn twap_create(
context.chain,
context.signer,
&plan.action,
actions::RawL1ActionMetadata::new(actions::nonce_now()),
actions::RawL1ActionMetadata::new(actions::nonce_now()).with_vault_address(vault_address),
"twap-create rejected",
)
.await?;
Expand Down Expand Up @@ -609,6 +610,7 @@ pub async fn twap_cancel(
context: OrderSubmissionContext<'_>,
resolver: &AssetResolver,
args: &TwapCancelArgs,
vault_address: Option<Address>,
format: OutputFormat,
) -> Result<(), anyhow::Error> {
let start = Instant::now();
Expand All @@ -619,7 +621,7 @@ pub async fn twap_cancel(
context.chain,
context.signer,
&plan.action,
actions::RawL1ActionMetadata::new(actions::nonce_now()),
actions::RawL1ActionMetadata::new(actions::nonce_now()).with_vault_address(vault_address),
"twap-cancel rejected",
)
.await?;
Expand All @@ -643,19 +645,35 @@ pub async fn twap_cancel(
pub async fn schedule_cancel(
context: OrderSubmissionContext<'_>,
args: &ScheduleCancelArgs,
vault_address: Option<Address>,
format: OutputFormat,
) -> Result<(), anyhow::Error> {
let start = Instant::now();
context.signer.ensure_can_attempt_live_signing()?;
let plan = prepare_schedule_cancel_plan(args, Utc::now())?;
let mut plan = prepare_schedule_cancel_plan(args, Utc::now())?;
if context.require_mainnet_confirmation
&& !args.yes
&& !confirm_mainnet_schedule_cancel(&plan, format)?
{
return Err(CliError::Configuration(
"Mainnet schedule-cancel confirmation required; action cancelled. Rerun with --yes for deliberate automation."
.to_string(),
)
.into());
}
if plan.scheduled_at.is_some() {
plan = prepare_schedule_cancel_plan(args, Utc::now())?;
}
let nonce = actions::nonce_now();
if plan.scheduled_at.is_some() {
actions::send_l1_action(
actions::send_l1_action_raw(
context.api_base_url,
context.chain,
context.signer,
plan.action,
nonce,
vault_address,
"schedule-cancel rejected",
)
.await?;
} else {
Expand All @@ -667,7 +685,7 @@ pub async fn schedule_cancel(
action_type: "scheduleCancel",
time: None,
},
actions::RawL1ActionMetadata::new(nonce),
actions::RawL1ActionMetadata::new(nonce).with_vault_address(vault_address),
"schedule-cancel clear rejected",
)
.await?;
Expand Down Expand Up @@ -1625,6 +1643,32 @@ fn confirm_cancel_all(coin: Option<&str>, format: OutputFormat) -> Result<bool,
Ok(is_yes(input.trim()))
}

fn confirm_mainnet_schedule_cancel(
plan: &ScheduleCancelPlan,
format: OutputFormat,
) -> Result<bool, CliError> {
let mut stderr = io::stderr();
let prompt = if let Some(scheduled_at) = plan.scheduled_at.as_ref() {
format!(
"Schedule mainnet cancel-all for {}? [y/N] ",
scheduled_at.to_rfc3339()
)
} else {
"Clear mainnet scheduled cancel-all? [y/N] ".to_string()
};
write!(stderr, "{}", warning_prompt(&prompt, format))
.map_err(|err| CliError::Internal(anyhow::anyhow!(err)))?;
stderr
.flush()
.map_err(|err| CliError::Internal(anyhow::anyhow!(err)))?;

let mut input = String::new();
io::stdin()
.read_line(&mut input)
.map_err(|err| CliError::Internal(anyhow::anyhow!(err)))?;
Ok(is_yes(input.trim()))
}

async fn market_mid_price(client: &HttpClient, lookup: &MidLookup) -> Result<Decimal, CliError> {
let mids = client
.all_mids(lookup.dex.clone())
Expand Down Expand Up @@ -2117,6 +2161,7 @@ mod tests {
grouping: PositionTpslGroupingArg::PositionTpsl,
side: Some(OrderSide::Sell),
size: Some(Decimal::new(1, 1)),
on_behalf_of: None,
margin_mode: None,
yes: false,
cloid: Some("not-a-hex-value".to_string()),
Expand Down Expand Up @@ -2155,6 +2200,7 @@ mod tests {
grouping: PositionTpslGroupingArg::PositionTpsl,
side: None,
size: None,
on_behalf_of: None,
margin_mode: None,
yes: false,
cloid: None,
Expand Down
Loading
Loading