use aleph_sdk::aggregate_models::corechannel::NodeHash;
use aleph_sdk::credit::PriceSource;
use aleph_types::chain::Address;
use aleph_types::item_hash::ItemHash;
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use url::Url;
use crate::common::resolve_address;
fn parse_address(s: &str) -> Result<Address, String> {
resolve_address(s).map_err(|e| e.to_string())
}
fn parse_price_source(s: &str) -> Result<PriceSource, String> {
s.parse::<PriceSource>().map_err(|e| e.to_string())
}
pub fn parse_size_to_mib(s: &str) -> Result<u64, String> {
let s = s.trim();
let unit_start = s
.find(|c: char| c.is_alphabetic())
.ok_or_else(|| format!("missing unit in size '{s}' (use e.g. 20GB, 1024MB, 1TiB)"))?;
let (num_str, unit) = s.split_at(unit_start);
let value: f64 = num_str
.trim()
.parse()
.map_err(|_| format!("invalid number in size '{s}'"))?;
if value < 0.0 {
return Err(format!("size cannot be negative: '{s}'"));
}
let mib = match unit.to_lowercase().as_str() {
"mib" => value,
"gib" => value * 1024.0,
"tib" => value * 1024.0 * 1024.0,
"mb" => value * 1_000_000.0 / 1_048_576.0,
"gb" => value * 1_000_000_000.0 / 1_048_576.0,
"tb" => value * 1_000_000_000_000.0 / 1_048_576.0,
_ => {
return Err(format!(
"unknown size unit '{unit}' (use MB, GB, TB, MiB, GiB, TiB)"
));
}
};
let mib_rounded = mib.round() as u64;
if mib_rounded == 0 {
return Err(format!("size too small: '{s}' rounds to 0 MiB"));
}
Ok(mib_rounded)
}
#[derive(Clone, Debug)]
pub enum ImageRef {
Preset(String),
Hash(ItemHash),
}
pub fn parse_image_ref(s: &str) -> Result<ImageRef, String> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err("image cannot be empty".to_string());
}
match ItemHash::try_from(trimmed) {
Ok(h) => Ok(ImageRef::Hash(h)),
Err(_) => Ok(ImageRef::Preset(trimmed.to_string())),
}
}
const LONG_VERSION: &str = concat!(
env!("CARGO_PKG_VERSION"),
" (",
env!("ALEPH_GIT_COMMIT"),
" ",
env!("ALEPH_COMMIT_DATE"),
")"
);
#[derive(Parser)]
#[command(name = "aleph", version, long_version = LONG_VERSION, about = "Aleph CLI")]
pub struct Cli {
#[arg(long)]
pub ccn: Option<String>,
#[arg(long, global = true)]
pub json: bool,
#[arg(long)]
pub network: Option<String>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
Account {
#[clap(subcommand)]
command: AccountCommand,
},
Aggregate {
#[clap(subcommand)]
command: AggregateCommand,
},
Authorization {
#[clap(subcommand)]
command: AuthorizationCommand,
},
Config {
#[clap(subcommand)]
command: ConfigCommand,
},
Domain {
#[clap(subcommand)]
command: DomainCommand,
},
File {
#[clap(subcommand)]
command: FileCommand,
},
Instance {
#[clap(subcommand)]
command: InstanceCommand,
},
Message {
#[clap(subcommand)]
command: MessageCommand,
},
Node {
#[clap(subcommand)]
command: NodeCommand,
},
Post {
#[clap(subcommand)]
command: PostCommand,
},
Program {
#[clap(subcommand)]
command: ProgramCommand,
},
Credit {
#[clap(subcommand)]
command: CreditCommand,
},
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
Website {
#[clap(subcommand)]
command: WebsiteCommand,
},
}
#[derive(Subcommand)]
pub enum MessageCommand {
#[command(long_about = "\
Forget messages on the network. Two scopes are supported:
<HASHES>... Forget specific messages, one per item hash. Each hash \
identifies a single message (POST, AGGREGATE element, STORE, PROGRAM, or \
INSTANCE). The network cascades the appropriate cleanup based on the \
target's type: forgetting a STORE releases the file pin, forgetting a POST \
also forgets its amends, forgetting a PROGRAM/INSTANCE tears down its VM, \
and forgetting an AGGREGATE element rebuilds the aggregate from the \
elements that remain.
--aggregates <HASH>... Forget an entire aggregate, identified by the \
item hash of any of its element messages. This forgets *every* AGGREGATE \
message sent by the same address under the same key — not just the one \
hash you passed. Use this when you want the whole aggregate gone, not just \
one update to it.
Examples:
# Forget two specific messages
aleph message forget abc123... def456...
# Forget the entire aggregate keyed at the (sender, key) of this element
aleph message forget --aggregates abc123...
# Combine: forget some specific messages AND wipe an aggregate
aleph message forget abc123... --aggregates def456... --reason \"superseded\"
Forget is irreversible. You can only forget messages your own address owns \
(or that you have an authorization to forget on behalf of).")]
Forget(ForgetArgs),
Get(GetMessageArgs),
List(Box<MessageListArgs>),
Retry(RetryArgs),
Sync(Box<SyncArgs>),
}
#[derive(Args)]
pub struct SyncArgs {
#[arg(long)]
pub source: String,
#[arg(long)]
pub target: String,
#[arg(long, default_value = "200")]
pub count: u32,
#[arg(long)]
pub dry_run: bool,
#[command(flatten)]
pub filter: MessageFilterCli,
}
#[derive(Args)]
pub struct GetMessageArgs {
pub item_hash: ItemHash,
}
#[derive(Args)]
pub struct RetryArgs {
pub item_hash: ItemHash,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Subcommand)]
pub enum PostCommand {
Amend(PostAmendArgs),
Create(PostCreateArgs),
List(Box<PostListArgs>),
}
#[derive(Args)]
pub struct PostListArgs {
#[arg(long, default_value = "1")]
pub api_version: u8,
#[command(flatten)]
pub filter: PostFilterCli,
}
use aleph_sdk::client::{MessageFilter, PostFilter, SortBy, SortOrder};
use aleph_types::message::{MessageStatus, MessageType};
use aleph_types::timestamp::Timestamp;
use chrono::{DateTime, FixedOffset, Utc};
use std::str::FromStr;
fn parse_timestamp(s: &str) -> Result<Timestamp, String> {
if let Ok(timestamp) = s.parse::<f64>() {
return Ok(Timestamp::from(timestamp));
}
let timestamp = DateTime::<FixedOffset>::from_str(s)
.map_err(|e| e.to_string())?
.timestamp();
Ok(Timestamp::from(timestamp as f64))
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum MessageTypeCli {
Aggregate,
Forget,
Instance,
Program,
Post,
Store,
}
impl From<MessageTypeCli> for MessageType {
fn from(v: MessageTypeCli) -> Self {
match v {
MessageTypeCli::Aggregate => MessageType::Aggregate,
MessageTypeCli::Forget => MessageType::Forget,
MessageTypeCli::Instance => MessageType::Instance,
MessageTypeCli::Post => MessageType::Post,
MessageTypeCli::Program => MessageType::Program,
MessageTypeCli::Store => MessageType::Store,
}
}
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum MessageStatusCli {
Pending,
Processed,
Removing,
Removed,
Forgotten,
}
impl From<MessageStatusCli> for MessageStatus {
fn from(v: MessageStatusCli) -> Self {
match v {
MessageStatusCli::Pending => MessageStatus::Pending,
MessageStatusCli::Processed => MessageStatus::Processed,
MessageStatusCli::Removing => MessageStatus::Removing,
MessageStatusCli::Removed => MessageStatus::Removed,
MessageStatusCli::Forgotten => MessageStatus::Forgotten,
}
}
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum SortByCli {
Time,
TxTime,
}
impl From<SortByCli> for SortBy {
fn from(v: SortByCli) -> Self {
match v {
SortByCli::Time => SortBy::Time,
SortByCli::TxTime => SortBy::TxTime,
}
}
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum SortOrderCli {
Asc,
Desc,
}
impl From<SortOrderCli> for SortOrder {
fn from(v: SortOrderCli) -> Self {
match v {
SortOrderCli::Asc => SortOrder::Asc,
SortOrderCli::Desc => SortOrder::Desc,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub enum FrameworkCli {
None,
Nextjs,
React,
Vue,
Gatsby,
Svelte,
Nuxt,
Angular,
Other,
}
impl std::fmt::Display for FrameworkCli {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
FrameworkCli::None => "none",
FrameworkCli::Nextjs => "nextjs",
FrameworkCli::React => "react",
FrameworkCli::Vue => "vue",
FrameworkCli::Gatsby => "gatsby",
FrameworkCli::Svelte => "svelte",
FrameworkCli::Nuxt => "nuxt",
FrameworkCli::Angular => "angular",
FrameworkCli::Other => "other",
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn framework_display_round_trip() {
assert_eq!(FrameworkCli::Nextjs.to_string(), "nextjs");
assert_eq!(FrameworkCli::Other.to_string(), "other");
}
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum ChainCli {
Arb,
Aurora,
Avax,
Base,
Blast,
Bob,
Bsc,
Cyber,
Eth,
Etherlink,
Frax,
Hype,
Ink,
Lens,
Linea,
Lisk,
Metis,
Mode,
Op,
Pol,
Stt,
Sonic,
Unichain,
Wld,
Zora,
Sol,
Es,
}
impl From<ChainCli> for aleph_types::chain::Chain {
fn from(v: ChainCli) -> Self {
use aleph_types::chain::Chain;
match v {
ChainCli::Arb => Chain::Arbitrum,
ChainCli::Aurora => Chain::Aurora,
ChainCli::Avax => Chain::Avax,
ChainCli::Base => Chain::Base,
ChainCli::Blast => Chain::Blast,
ChainCli::Bob => Chain::Bob,
ChainCli::Bsc => Chain::Bsc,
ChainCli::Cyber => Chain::Cyber,
ChainCli::Eth => Chain::Ethereum,
ChainCli::Etherlink => Chain::Etherlink,
ChainCli::Frax => Chain::Fraxtal,
ChainCli::Hype => Chain::Hype,
ChainCli::Ink => Chain::Ink,
ChainCli::Lens => Chain::Lens,
ChainCli::Linea => Chain::Linea,
ChainCli::Lisk => Chain::Lisk,
ChainCli::Metis => Chain::Metis,
ChainCli::Mode => Chain::Mode,
ChainCli::Op => Chain::Optimism,
ChainCli::Pol => Chain::Pol,
ChainCli::Stt => Chain::Somnia,
ChainCli::Sonic => Chain::Sonic,
ChainCli::Unichain => Chain::Unichain,
ChainCli::Wld => Chain::Worldchain,
ChainCli::Zora => Chain::Zora,
ChainCli::Sol => Chain::Sol,
ChainCli::Es => Chain::Eclipse,
}
}
}
#[derive(Debug, Clone, Args)]
pub struct IdentityArgs {
#[arg(long)]
pub account: Option<String>,
#[arg(long)]
pub private_key: Option<String>,
#[arg(long, value_enum)]
pub chain: Option<ChainCli>,
}
#[derive(Debug, Clone, Args)]
pub struct SigningArgs {
#[command(flatten)]
pub identity: IdentityArgs,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Clone, Args)]
pub struct MessageFilterCli {
#[arg(long, value_delimiter = ',', value_enum)]
pub message_type: Option<MessageTypeCli>,
#[arg(long, value_delimiter = ',', value_enum)]
pub message_types: Option<Vec<MessageTypeCli>>,
#[arg(long, value_delimiter = ',')]
pub content_types: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
pub content_keys: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
pub content_hashes: Option<Vec<ItemHash>>,
#[arg(long, value_delimiter = ',')]
pub refs: Option<Vec<String>>,
#[arg(long, value_delimiter = ',', value_parser = parse_address)]
pub addresses: Option<Vec<Address>>,
#[arg(long, value_delimiter = ',', value_parser = parse_address)]
pub owners: Option<Vec<Address>>,
#[arg(long, value_delimiter = ',')]
pub tags: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
pub hashes: Option<Vec<ItemHash>>,
#[arg(long, value_delimiter = ',')]
pub channels: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
pub chains: Option<Vec<String>>,
#[arg(long, value_parser = parse_timestamp)]
pub start_date: Option<Timestamp>,
#[arg(long, value_parser = parse_timestamp)]
pub end_date: Option<Timestamp>,
#[arg(long, value_enum)]
pub sort_by: Option<SortByCli>,
#[arg(long, value_enum)]
pub sort_order: Option<SortOrderCli>,
#[arg(long, value_delimiter = ',')]
pub message_statuses: Option<Vec<MessageStatusCli>>,
}
#[derive(Debug, Clone, Args)]
pub struct MessageListArgs {
#[arg(long, default_value = "200")]
pub count: u32,
#[command(flatten)]
pub filter: MessageFilterCli,
}
impl From<MessageFilterCli> for MessageFilter {
fn from(c: MessageFilterCli) -> Self {
MessageFilter {
message_type: c.message_type.map(Into::into),
message_types: c
.message_types
.map(|v| v.into_iter().map(Into::into).collect()),
content_types: c.content_types,
content_keys: c.content_keys,
content_hashes: c.content_hashes,
refs: c.refs,
addresses: c.addresses,
owners: c.owners,
tags: c.tags,
hashes: c.hashes,
channels: c.channels,
chains: c.chains,
start_date: c.start_date,
end_date: c.end_date,
sort_by: c.sort_by.map(Into::into),
sort_order: c.sort_order.map(Into::into),
message_statuses: c
.message_statuses
.map(|v| v.into_iter().map(Into::into).collect()),
}
}
}
#[derive(Debug, Clone, Args)]
pub struct PostFilterCli {
#[arg(long, value_delimiter = ',', value_parser = parse_address)]
pub addresses: Option<Vec<Address>>,
#[arg(long, value_delimiter = ',')]
pub hashes: Option<Vec<ItemHash>>,
#[arg(long, value_delimiter = ',')]
pub refs: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
pub post_types: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
pub tags: Option<Vec<String>>,
#[arg(long, value_delimiter = ',')]
pub channels: Option<Vec<String>>,
#[arg(long, value_parser = parse_timestamp)]
pub start_date: Option<Timestamp>,
#[arg(long, value_parser = parse_timestamp)]
pub end_date: Option<Timestamp>,
#[arg(long, value_enum)]
pub sort_by: Option<SortByCli>,
#[arg(long, value_enum)]
pub sort_order: Option<SortOrderCli>,
#[arg(long, default_value = "200")]
pub pagination: u32,
#[arg(long, default_value = "1")]
pub page: u32,
}
impl From<PostFilterCli> for PostFilter {
fn from(c: PostFilterCli) -> Self {
PostFilter {
addresses: c.addresses,
hashes: c.hashes,
refs: c.refs,
post_types: c.post_types,
tags: c.tags,
channels: c.channels,
start_date: c.start_date,
end_date: c.end_date,
sort_by: c.sort_by.map(Into::into),
sort_order: c.sort_order.map(Into::into),
}
}
}
#[derive(Args)]
pub struct PostCreateArgs {
#[arg(long = "type")]
pub post_type: String,
#[arg(long)]
pub content: Option<String>,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct PostAmendArgs {
#[arg(long = "ref")]
pub reference: ItemHash,
#[arg(long)]
pub content: Option<String>,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Subcommand)]
pub enum AggregateCommand {
Create(AggregateCreateArgs),
Get(AggregateGetArgs),
List(AggregateListArgs),
Forget(AggregateForgetArgs),
}
#[derive(Args)]
pub struct AggregateGetArgs {
pub key: String,
#[arg(long)]
pub address: Option<String>,
}
#[derive(Args)]
pub struct AggregateListArgs {
#[arg(long)]
pub address: Option<String>,
}
#[derive(Args)]
pub struct AggregateForgetArgs {
pub hashes: Vec<ItemHash>,
#[arg(long)]
pub reason: Option<String>,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[arg(short = 'y', long)]
pub yes: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct AggregateCreateArgs {
#[arg(long)]
pub key: String,
#[arg(long)]
pub content: Option<String>,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct ForgetArgs {
pub hashes: Vec<ItemHash>,
#[arg(long, value_delimiter = ',')]
pub aggregates: Option<Vec<ItemHash>>,
#[arg(long)]
pub reason: Option<String>,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[arg(short = 'y', long)]
pub yes: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
pub enum NodeCommand {
Amend(AmendNodeArgs),
CreateCcn(CreateCcnArgs),
CreateCrn(CreateCrnArgs),
Drop(DropNodeArgs),
Link(LinkCrnArgs),
List(NodeListArgs),
Stake(StakeArgs),
Unlink(UnlinkCrnArgs),
Unstake(UnstakeArgs),
}
#[derive(Debug, Clone, Args)]
#[command(group = clap::ArgGroup::new("scope").args(["address", "all"]))]
pub struct NodeListArgs {
#[arg(long)]
pub address: Option<String>,
#[arg(long)]
pub all: bool,
#[arg(long, value_enum, rename_all = "lowercase")]
pub r#type: Option<NodeTypeCli>,
#[arg(long)]
pub corechannel_address: Option<String>,
#[command(flatten)]
pub identity: IdentityArgs,
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum NodeTypeCli {
Ccn,
Crn,
}
#[derive(Args)]
pub struct CreateCcnArgs {
#[arg(value_name = "NAME")]
pub name: String,
#[arg(long)]
pub multiaddress: String,
#[arg(long)]
pub network_tag: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct CreateCrnArgs {
#[arg(value_name = "NAME")]
pub name: String,
#[arg(long)]
pub url: String,
#[arg(long)]
pub network_tag: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct LinkCrnArgs {
#[arg(long)]
pub crn: NodeHash,
#[arg(long)]
pub network_tag: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct UnlinkCrnArgs {
#[arg(long)]
pub crn: NodeHash,
#[arg(long)]
pub network_tag: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct StakeArgs {
#[arg(long)]
pub node: NodeHash,
#[arg(long)]
pub network_tag: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct UnstakeArgs {
#[arg(long)]
pub node: NodeHash,
#[arg(long)]
pub network_tag: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct DropNodeArgs {
#[arg(long)]
pub node: NodeHash,
#[arg(long)]
pub network_tag: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct AmendNodeArgs {
#[arg(long)]
pub node: NodeHash,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub multiaddress: Option<String>,
#[arg(long)]
pub url: Option<String>,
#[arg(long)]
pub picture: Option<String>,
#[arg(long)]
pub banner: Option<String>,
#[arg(long)]
pub description: Option<String>,
#[arg(long)]
pub reward: Option<String>,
#[arg(long)]
pub stream_reward: Option<String>,
#[arg(long)]
pub manager: Option<String>,
#[arg(long, value_delimiter = ',')]
pub authorized: Option<Vec<String>>,
#[arg(long)]
pub locked: Option<bool>,
#[arg(long)]
pub registration_url: Option<String>,
#[arg(long)]
pub terms_and_conditions: Option<String>,
#[arg(long)]
pub network_tag: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Subcommand)]
pub enum AccountCommand {
Alias {
#[clap(subcommand)]
command: AliasCommand,
},
Balance(AccountBalanceArgs),
#[command(long_about = "\
Generate a fresh private key and store it in the OS keychain under the \
given name. The key never touches disk. Use the resulting account by \
passing `--account <NAME>` to signing commands, or set it as the default \
with `aleph account use <NAME>`.
To import an existing key (private-key, keystore file, or Ledger), use \
`aleph account import` instead.
Examples:
aleph account create alice # EVM (default chain: eth)
aleph account create alice --chain sol # Solana
aleph account use alice # set as default for signing")]
Create(AccountCreateArgs),
Remove(AccountRemoveArgs),
Export(AccountExportArgs),
#[command(long_about = "\
Import an existing key into the OS keychain under the given name. Three \
sources are supported, mutually exclusive:
--private-key <HEX> Hex-encoded private key on the command line. If \
omitted (and no other source given), the key is read from stdin so it does \
not appear in shell history.
--from-file <PATH> Read from a file containing a raw 32-byte binary \
key or a hex-encoded text key.
--ledger Use a Ledger hardware wallet. Combine with \
`--derivation-path` to override the default BIP44 path, and \
`--ledger-count` to fetch more than the default 5 candidate addresses.
Examples:
aleph account import alice --private-key 0xabcd1234...
aleph account import alice --from-file ~/keys/alice.key
aleph account import alice --ledger
aleph account import alice --chain sol --ledger
echo \"0xabcd1234...\" | aleph account import alice # via stdin")]
Import(AccountImportArgs),
List,
Migrate(AccountMigrateArgs),
Show(AccountShowArgs),
Use(AccountUseArgs),
}
#[derive(Args)]
pub struct AccountCreateArgs {
#[arg(value_name = "NAME")]
pub name: String,
#[arg(long, value_enum, default_value = "eth")]
pub chain: ChainCli,
}
#[derive(Args)]
pub struct AccountImportArgs {
#[arg(value_name = "NAME")]
pub name: String,
#[arg(long, value_enum, default_value = "eth")]
pub chain: ChainCli,
#[arg(long, conflicts_with = "ledger")]
pub private_key: Option<String>,
#[arg(long, conflicts_with_all = ["private_key", "ledger"])]
pub from_file: Option<PathBuf>,
#[arg(long)]
pub ledger: bool,
#[arg(long, requires = "ledger")]
pub derivation_path: Option<String>,
#[arg(long, requires = "ledger", default_value = "5")]
pub ledger_count: usize,
}
#[derive(Args)]
pub struct AccountMigrateArgs {
#[arg(long)]
pub python_home: Option<PathBuf>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Args)]
pub struct AccountShowArgs {
pub name: Option<String>,
}
#[derive(Args)]
pub struct AccountBalanceArgs {
pub address: Option<String>,
}
#[derive(Args)]
pub struct AccountRemoveArgs {
pub name: String,
#[arg(short = 'y', long)]
pub yes: bool,
}
#[derive(Args)]
pub struct AccountUseArgs {
pub name: String,
}
#[derive(Args)]
pub struct AccountExportArgs {
pub name: String,
#[arg(long)]
pub yes: bool,
}
#[derive(Subcommand)]
pub enum AliasCommand {
Add(AliasAddArgs),
List,
Remove(AliasRemoveArgs),
}
#[derive(Args)]
pub struct AliasAddArgs {
pub name: String,
pub address: String,
}
#[derive(Args)]
pub struct AliasRemoveArgs {
pub name: String,
}
#[derive(Subcommand)]
pub enum FileCommand {
#[command(long_about = "\
Delete files by their content hash (IPFS CID or native hex). Each hash is \
resolved to the STORE message that pins the file for the owner, and that \
message is forgotten - releasing the pin.
Use `aleph message forget` instead when you need to forget a specific \
STORE *message* by its item hash (e.g. to remove a duplicate pin while \
keeping the file alive via the others).
Forget is irreversible. You can only delete files owned by your own \
address (or that you have an authorization to forget on behalf of).
Examples:
aleph file delete Qmabc... # IPFS CID
aleph file delete 9675a23e... # native hex
aleph file delete Qmabc... QmDef... --reason \"superseded\"
aleph file delete Qmabc... -y --on-behalf-of 0x...")]
Delete(FileDeleteArgs),
Download(FileDownloadArgs),
List(FileListArgs),
Pin(FilePinArgs),
#[command(long_about = "\
Upload a file (or directory) and create a STORE message announcing it on \
the network. The signed STORE message anchors a content-addressed pin: \
its item hash is the file's hash, and the network keeps the content as \
long as the pin is paid for.
Storage engine defaults to `storage` (Aleph native, ≤ 100 MB) for files \
and `ipfs` for directories. Pass `--storage-engine ipfs` to put a single \
file on IPFS instead. Payment defaults to credits; pass \
`--payment-type hold` to fall back to locked-stake.
Use `--ref <NAME>` to give the file a stable user-defined identifier \
(e.g. `report/latest`) — useful for in-place updates and for downloading \
later via `aleph file download --ref ...`.
Examples:
aleph file upload ./report.pdf
aleph file upload ./website/ # directory → IPFS
aleph file upload ./big.bin --storage-engine ipfs
aleph file upload ./report.pdf --ref reports/q4
aleph file upload ./data.bin --channel my-channel")]
Upload(FileUploadArgs),
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum StorageEngineCli {
Storage,
Ipfs,
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum PaymentTypeCli {
Hold,
Credit,
}
#[derive(Args)]
pub struct FileUploadArgs {
pub path: std::path::PathBuf,
#[arg(long, value_enum)]
pub storage_engine: Option<StorageEngineCli>,
#[arg(long, value_enum)]
pub payment_type: Option<PaymentTypeCli>,
#[arg(long)]
pub channel: Option<String>,
#[arg(long = "ref")]
pub reference: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[arg(long, env = "ALEPH_IPFS_GATEWAY")]
pub ipfs_gateway: Option<Url>,
#[arg(long, default_value_t = false)]
pub use_gateway_relay: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct FilePinArgs {
pub item_hash: ItemHash,
#[arg(long, value_enum)]
pub payment_type: Option<PaymentTypeCli>,
#[arg(long)]
pub channel: Option<String>,
#[arg(long = "ref")]
pub reference: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct FileDownloadArgs {
#[arg(conflicts_with_all = ["message_hash", "reference"])]
pub hash: Option<ItemHash>,
#[arg(long, conflicts_with_all = ["hash", "reference"])]
pub message_hash: Option<ItemHash>,
#[arg(long = "ref", conflicts_with_all = ["hash", "message_hash"])]
pub reference: Option<String>,
#[arg(long, requires = "reference")]
pub owner: Option<String>,
#[arg(short, long)]
pub output: Option<std::path::PathBuf>,
#[arg(long)]
pub stdout: bool,
}
#[derive(Args)]
pub struct FileListArgs {
#[arg(long)]
pub address: Option<String>,
#[arg(long, default_value = "25")]
pub count: u32,
#[arg(long, value_enum, default_value = "desc")]
pub sort_order: SortOrderCli,
}
#[derive(Args)]
pub struct FileDeleteArgs {
pub hashes: Vec<ItemHash>,
#[arg(long)]
pub reason: Option<String>,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[arg(short = 'y', long)]
pub yes: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
pub enum InstanceCommand {
#[command(long_about = "\
Create a new VM instance. Sizing is specified one of three ways:
--size <SLUG> e.g. `1vcpu-2gb`, `2vcpu-4gb`, \
`4vcpu-8gb`, `8vcpu-16gb`.
--vcpus N --memory <SIZE> --disk-size <SIZE>
Custom sizing. Sizes accept \
human-readable forms like `4GB`, `512MiB`, `1TiB`.
--gpu <MODEL> [...] GPU instance. Use \
`aleph instance price --list-gpus` to list models. `--gpu <MODEL>` alone \
sizes the VM at the GPU's minimum. GPU sizes scale in compute-unit steps \
(1 vCPU + 6 GiB each), so `--size` accepts the minimum or any larger multiple \
(e.g. `4vcpu-24gb`, `5vcpu-30gb`); `--vcpus`/`--memory` work too. `--disk-size` \
is optional for GPU instances.
Required: NAME (positional), `--image`, and at least one \
`--ssh-pubkey-file`. Image accepts a preset name (`ubuntu22`, `ubuntu24`, \
`debian12`) or an item hash (hex or IPFS CID).
Pin to a specific compute node with `--crn-hash <HASH>`. For an \
interactive walkthrough that prompts for any missing fields and lets you \
pick a CRN from a list, pass `-i` / `--interactive`.
Volumes can be added with `--persistent-volume`, `--ephemeral-volume`, or \
`--immutable-volume` (each can be repeated).
Examples:
aleph instance create web --image ubuntu24 --size 1vcpu-2gb \\
--ssh-pubkey-file ~/.ssh/id_ed25519.pub
aleph instance create gpu-job --image ubuntu24 --gpu h100 \\
--ssh-pubkey-file ~/.ssh/id_ed25519.pub
aleph instance create db --image ubuntu24 --size 4vcpu-8gb \\
--persistent-volume name=data,mount=/data,size=100GB \\
--ssh-pubkey-file ~/.ssh/id_ed25519.pub
aleph instance create -i web # interactive prompts for everything else")]
Create(InstanceCreateArgs),
#[command(long_about = "\
Delete an instance. Forgets the corresponding INSTANCE message.
This command does ONLY the FORGET. It does NOT:
- erase the VM on the CRN (run `aleph instance erase` first)
- remove port forwards (run `aleph instance port-forward delete`)
- stop a streaming flow (Superfluid flow stays open on PAYG)
Run those subcommands separately if you need that cleanup.
Examples:
aleph instance delete a41fb91c3e68
aleph instance delete a41fb91c3e68 --reason \"decommission\"
aleph instance delete a41fb91c3e68 --dry-run --json
")]
Delete(InstanceDeleteArgs),
Erase(CrnArgs),
List(InstanceListArgs),
Logs(CrnArgs),
#[command(visible_alias = "pfw")]
PortForward {
#[clap(subcommand)]
command: PortForwardCommand,
},
#[command(long_about = "\
Show pricing for an instance configuration.
There are three ways to specify the instance:
1. By size slug: --size 1vcpu-2gb
2. By resources: --vcpus 4 --memory 8GB --disk-size 100GB
3. By GPU model: --gpu h100
GPU instances have a minimum size determined by the model, then scale in \
compute-unit steps (1 vCPU + 6 GiB each). --size accepts the minimum or any \
larger multiple (e.g. 4vcpu-24gb, 5vcpu-30gb); --vcpus/--memory work too. \
GPU and confidential instances use separate pricing tiers and cannot \
be combined.
Examples:
aleph instance price --size 4vcpu-8gb
aleph instance price --vcpus 2 --memory 4GB --disk-size 50GB
aleph instance price --gpu h100
aleph instance price --gpu h100 --size 32vcpu-192gb
aleph instance price --list-gpus # list available GPU models (recommended)
aleph instance price --gpu # same, kept as a shortcut
aleph instance price --size 1vcpu-2gb --confidential")]
Price(InstancePriceArgs),
Reboot(CrnArgs),
#[command(long_about = "\
Show details of a single VM instance.
By default, gathers data without authenticated calls: the INSTANCE message
from the CCN, plus scheduler placement and status.
Pass --verbose to additionally reach the allocated CRN for live networking
(IPv4/IPv6, mapped host ports) and pull the owner's port-forwarding
aggregate. These extras add up to three HTTP calls; skip them for a fast
lookup.
For the raw INSTANCE message, use `aleph message get <hash>`.
VM_ID accepts a unique hash prefix (e.g. the 12-char hash shown by `aleph
instance list`); the scheduler matches it server-side.
Examples:
aleph instance show a41fb91c3e68
aleph instance show a41fb91c3e68 --verbose
aleph instance show a41fb91c3e68 --json")]
Show(InstanceShowArgs),
#[command(long_about = "\
Open an SSH session to a dispatched VM instance.
The instance must already be dispatched: the scheduler is queried to find
which CRN owns it, then the CRN's `/about/executions/list` endpoint is
consulted to discover the VM's IPv6 address. SSH is then exec'd with that
target.
Pass `--crn` (a node hash or URL) to skip scheduler discovery. Extra arguments after `--`
are forwarded verbatim to `ssh` (e.g. to run a remote command).
Examples:
aleph instance ssh <vm-hash>
aleph instance ssh <vm-hash> --user ubuntu --identity ~/.ssh/id_ed25519
aleph instance ssh <vm-hash> -- uptime")]
Ssh(InstanceSshArgs),
Start(CrnStartArgs),
Stop(CrnArgs),
#[command(subcommand)]
Backup(InstanceBackupCommand),
#[command(subcommand)]
Confidential(ConfidentialCommand),
}
#[derive(Args)]
pub struct InstanceListArgs {
#[arg(long)]
pub address: Option<String>,
}
#[derive(Args)]
pub struct InstanceShowArgs {
pub vm_id: String,
#[arg(short = 'v', long)]
pub verbose: bool,
}
#[derive(Args)]
pub struct InstanceDeleteArgs {
pub vm_id: String,
#[arg(long, default_value = "User deletion")]
pub reason: String,
#[arg(short = 'y', long)]
pub yes: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct InstancePriceArgs {
#[arg(long)]
pub size: Option<String>,
#[arg(long)]
pub vcpus: Option<u32>,
#[arg(long, value_parser = parse_size_to_mib)]
pub memory: Option<u64>,
#[arg(long, value_parser = parse_size_to_mib)]
pub disk_size: Option<u64>,
#[arg(long, num_args = 0..=1, default_missing_value = "")]
pub gpu: Option<String>,
#[arg(long, conflicts_with = "gpu")]
pub list_gpus: bool,
#[arg(long)]
pub confidential: bool,
}
#[derive(Args)]
pub struct InstanceCreateArgs {
#[arg(value_name = "NAME")]
pub name: String,
#[arg(
long,
value_parser = parse_image_ref,
required_unless_present = "interactive"
)]
pub image: Option<ImageRef>,
#[arg(long, value_parser = parse_size_to_mib)]
pub disk_size: Option<u64>,
#[arg(long)]
pub size: Option<String>,
#[arg(long)]
pub vcpus: Option<u32>,
#[arg(long, value_parser = parse_size_to_mib)]
pub memory: Option<u64>,
#[arg(long, required_unless_present = "interactive")]
pub ssh_pubkey_file: Vec<PathBuf>,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub persistent_volume: Option<Vec<String>>,
#[arg(long)]
pub ephemeral_volume: Option<Vec<String>>,
#[arg(long)]
pub immutable_volume: Option<Vec<String>>,
#[arg(long)]
pub confidential: bool,
#[arg(long, value_parser = parse_image_ref)]
pub confidential_firmware: Option<ImageRef>,
#[arg(long)]
pub gpu: Option<Vec<String>>,
#[arg(long)]
pub crn_hash: Option<NodeHash>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[arg(short = 'i', long)]
pub interactive: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Subcommand)]
pub enum AuthorizationCommand {
Add(AuthorizationAddArgs),
List(AuthorizationListArgs),
Received(AuthorizationReceivedArgs),
Revoke(AuthorizationRevokeArgs),
}
#[derive(Debug, Clone, Args)]
pub struct AuthorizationListArgs {
#[arg(long)]
pub address: Option<String>,
#[arg(long)]
pub delegate: Option<String>,
#[command(flatten)]
pub identity: IdentityArgs,
}
#[derive(Debug, Clone, Args)]
pub struct AuthorizationReceivedArgs {
#[arg(long)]
pub address: Option<String>,
#[arg(long)]
pub granter: Option<String>,
#[command(flatten)]
pub identity: IdentityArgs,
}
#[derive(Debug, Clone, Args)]
pub struct AuthorizationAddArgs {
pub delegate_address: String,
#[arg(long = "delegate-chain", value_enum)]
pub delegate_chain: Option<ChainCli>,
#[arg(long, value_delimiter = ',')]
pub channels: Vec<String>,
#[arg(long, value_delimiter = ',', value_enum)]
pub message_types: Vec<MessageTypeCli>,
#[arg(long, value_delimiter = ',')]
pub post_types: Vec<String>,
#[arg(long, value_delimiter = ',')]
pub aggregate_keys: Vec<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Debug, Clone, Args)]
#[command(group(clap::ArgGroup::new("target").required(true)))]
pub struct AuthorizationRevokeArgs {
#[arg(group = "target")]
pub delegate_address: Option<String>,
#[arg(long, group = "target")]
pub all: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct CrnArgs {
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
pub vm_id: String,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct CrnStartArgs {
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
pub vm_id: String,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Subcommand)]
pub enum InstanceBackupCommand {
Create(InstanceBackupCreateArgs),
Info(InstanceBackupInfoArgs),
Download(InstanceBackupDownloadArgs),
Delete(InstanceBackupDeleteArgs),
Restore(InstanceBackupRestoreArgs),
}
#[derive(Args)]
pub struct InstanceBackupCreateArgs {
pub vm_id: String,
#[arg(long)]
pub include_volumes: bool,
#[arg(long)]
pub skip_fsfreeze: bool,
#[arg(long)]
pub follow: bool,
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct InstanceBackupInfoArgs {
pub vm_id: String,
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct InstanceBackupDownloadArgs {
pub vm_id_or_url: String,
#[arg(short, long)]
pub output: Option<std::path::PathBuf>,
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct InstanceBackupDeleteArgs {
pub vm_id: String,
pub backup_id: String,
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
#[command(group(
clap::ArgGroup::new("restore_source")
.required(true)
.args(["file", "volume_ref"]),
))]
pub struct InstanceBackupRestoreArgs {
pub vm_id: String,
#[arg(short, long, group = "restore_source")]
pub file: Option<std::path::PathBuf>,
#[arg(long, group = "restore_source")]
pub volume_ref: Option<String>,
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct InstanceSshArgs {
pub vm_id: String,
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
#[arg(long, default_value = "root")]
pub user: String,
#[arg(long, default_value_t = 22)]
pub port: u16,
#[arg(long)]
pub identity: Option<std::path::PathBuf>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub ssh_args: Vec<String>,
}
#[derive(Subcommand)]
pub enum ConfigCommand {
Ccn {
#[clap(subcommand)]
command: CcnCommand,
},
Network {
#[clap(subcommand)]
command: NetworkCommand,
},
}
#[derive(Subcommand)]
pub enum CcnCommand {
Add(CcnAddArgs),
List(CcnListArgs),
Remove(CcnRemoveArgs),
Show(CcnShowArgs),
Use(CcnUseArgs),
}
#[derive(Args)]
pub struct CcnAddArgs {
pub name: String,
pub url: String,
#[arg(long)]
pub network: Option<String>,
}
#[derive(Args)]
pub struct CcnListArgs {
#[arg(long, conflicts_with = "all")]
pub network: Option<String>,
#[arg(long)]
pub all: bool,
}
#[derive(Args)]
pub struct CcnUseArgs {
pub name: String,
#[arg(long)]
pub network: Option<String>,
}
#[derive(Args)]
pub struct CcnShowArgs {
pub name: Option<String>,
#[arg(long)]
pub network: Option<String>,
}
#[derive(Args)]
pub struct CcnRemoveArgs {
pub name: String,
#[arg(long)]
pub network: Option<String>,
#[arg(short = 'y', long)]
pub yes: bool,
}
#[derive(Subcommand)]
pub enum NetworkCommand {
Add(NetworkAddArgs),
List,
Remove(NetworkRemoveArgs),
Show(NetworkShowArgs),
Use(NetworkUseArgs),
Set(NetworkSetArgs),
}
#[derive(Args)]
pub struct NetworkEthereumArgs {
#[arg(long)]
pub rpc_url: Option<String>,
#[arg(long)]
pub credit_contract: Option<alloy_primitives::Address>,
#[arg(long)]
pub aleph_token: Option<alloy_primitives::Address>,
#[arg(long)]
pub usdc_token: Option<alloy_primitives::Address>,
#[arg(long, value_parser = parse_price_source)]
pub price_source: Option<aleph_sdk::credit::PriceSource>,
#[arg(long)]
pub explorer_tx_base: Option<String>,
}
#[derive(Args)]
pub struct NetworkAddArgs {
pub name: String,
#[arg(long)]
pub scheduler_url: Option<String>,
#[command(flatten)]
pub ethereum: NetworkEthereumArgs,
}
#[derive(Args)]
pub struct NetworkUseArgs {
pub name: String,
}
#[derive(Args)]
pub struct NetworkShowArgs {
pub name: Option<String>,
}
#[derive(Args)]
pub struct NetworkRemoveArgs {
pub name: String,
#[arg(short = 'y', long)]
pub yes: bool,
}
#[derive(Args)]
pub struct NetworkSetArgs {
#[arg(long)]
pub network: Option<String>,
#[arg(long)]
pub scheduler_url: Option<String>,
#[command(flatten)]
pub ethereum: NetworkEthereumArgs,
}
#[derive(Subcommand)]
pub enum CreditCommand {
#[command(long_about = "\
Buy Aleph credits by transferring ALEPH, USDC, or native ETH from your EVM \
account. ALEPH and USDC are ERC20 transfers; ETH is a plain value transfer to \
the network's credit purchase address. Either way the protocol mints credits \
to your address once the transfer is confirmed.
`--amount` is in human-readable token units (decimals OK), not credits. \
1 USD purchases 1,000,000 credits. Use `aleph account balance` afterwards \
to confirm the credits arrived.
Examples:
aleph credit buy --token aleph --amount 100
aleph credit buy --token usdc --amount 50.5
aleph credit buy --token eth --amount 0.05
aleph credit buy --token usdc --amount 25 --yes # skip confirmation
aleph credit buy --token aleph --amount 10 --rpc-url https://my-node.example")]
Buy(BuyCreditArgs),
#[command(long_about = "\
Transfer credits to another address. `--amount` is the number of credits \
(integer, not tokens) — 1 USD ≈ 1,000,000 credits. The recipient can be a \
hex address, a local account name, or an alias from `aleph account alias \
list`.
Optionally pass `--expiration <RFC3339>` to claw back any unspent portion \
after that time.
Examples:
aleph credit transfer --to bob --amount 1000000
aleph credit transfer --to 0xab12... --amount 500000
aleph credit transfer --to bob --amount 250000 \\
--expiration 2026-12-31T23:59:59Z")]
Transfer(TransferCreditArgs),
History(CreditHistoryArgs),
}
#[derive(Args)]
pub struct CreditHistoryArgs {
#[arg(long)]
pub address: Option<String>,
#[arg(long, default_value_t = 1)]
pub page: u32,
#[arg(long, default_value_t = 100)]
pub page_size: u32,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum CreditTokenCli {
Aleph,
Usdc,
Eth,
}
#[derive(Args)]
pub struct BuyCreditArgs {
#[arg(long, value_enum)]
pub token: CreditTokenCli,
#[arg(long)]
pub amount: String,
#[arg(long)]
pub rpc_url: Option<String>,
#[arg(short = 'y', long)]
pub yes: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
pub fn parse_rfc3339_utc(s: &str) -> Result<DateTime<Utc>, String> {
DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| format!("invalid RFC3339 timestamp '{s}': {e}"))
}
#[derive(Args)]
pub struct TransferCreditArgs {
#[arg(long)]
pub to: String,
#[arg(long)]
pub amount: u64,
#[arg(long, value_parser = parse_rfc3339_utc)]
pub expiration: Option<DateTime<Utc>>,
#[arg(long)]
pub channel: Option<String>,
#[arg(short = 'y', long)]
pub yes: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Subcommand)]
pub enum ProgramCommand {
Create(ProgramCreateArgs),
Update(ProgramUpdateArgs),
Delete(ProgramDeleteArgs),
List(ProgramListArgs),
Persist(ProgramPersistArgs),
Unpersist(ProgramPersistArgs),
Logs(ProgramLogsArgs),
Show(ProgramShowArgs),
}
#[derive(Args)]
pub struct ProgramCreateArgs {
pub path: PathBuf,
pub entrypoint: String,
#[arg(long)]
pub name: Option<String>,
#[arg(long, value_parser = parse_image_ref)]
pub runtime: Option<ImageRef>,
#[arg(long, conflicts_with_all = ["vcpus", "memory"])]
pub size: Option<String>,
#[arg(long)]
pub vcpus: Option<u32>,
#[arg(long, value_parser = parse_size_to_mib)]
pub memory: Option<u64>,
#[arg(long, default_value_t = 30)]
pub timeout_seconds: u32,
#[arg(long)]
pub internet: bool,
#[arg(long)]
pub persistent: bool,
#[arg(long)]
pub updatable: bool,
#[arg(long)]
pub env_vars: Option<String>,
#[arg(long)]
pub persistent_volume: Option<Vec<String>>,
#[arg(long)]
pub ephemeral_volume: Option<Vec<String>>,
#[arg(long)]
pub immutable_volume: Option<Vec<String>>,
#[arg(long, value_enum, default_value_t = StorageEngineCli::Storage)]
pub storage_engine: StorageEngineCli,
#[arg(long, value_enum, default_value_t = PaymentTypeCli::Hold)]
pub payment_type: PaymentTypeCli,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct ProgramUpdateArgs {
pub item_hash: ItemHash,
pub path: PathBuf,
#[arg(long)]
pub channel: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct ProgramDeleteArgs {
pub item_hash: ItemHash,
#[arg(long, default_value = "User deletion")]
pub reason: String,
#[arg(long)]
pub keep_code: bool,
#[arg(short = 'y', long)]
pub yes: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct ProgramListArgs {
#[arg(long)]
pub address: Option<String>,
}
#[derive(Args)]
pub struct ProgramShowArgs {
pub item_hash: ItemHash,
}
#[derive(Args)]
pub struct ProgramPersistArgs {
pub item_hash: ItemHash,
#[arg(long)]
pub keep_prev: bool,
#[arg(short = 'y', long)]
pub yes: bool,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct ProgramLogsArgs {
pub item_hash: ItemHash,
#[arg(long)]
pub crn: Url,
#[command(flatten)]
pub signing: SigningArgs,
}
#[cfg(test)]
mod credit_transfer_args_tests {
use super::*;
#[test]
fn parse_rfc3339_utc_accepts_z_suffix() {
let dt = parse_rfc3339_utc("2026-12-31T23:59:59Z").unwrap();
assert_eq!(dt.to_rfc3339(), "2026-12-31T23:59:59+00:00");
}
#[test]
fn parse_rfc3339_utc_accepts_offset_and_normalizes_to_utc() {
let dt = parse_rfc3339_utc("2026-12-31T23:59:59+01:00").unwrap();
assert_eq!(dt.to_rfc3339(), "2026-12-31T22:59:59+00:00");
}
#[test]
fn parse_rfc3339_utc_rejects_garbage() {
assert!(parse_rfc3339_utc("not a date").is_err());
}
fn assert_value_validation_err(args: &[&str]) {
match Cli::try_parse_from(args) {
Ok(_) => panic!("expected parse error for {args:?}"),
Err(e) => assert_eq!(e.kind(), clap::error::ErrorKind::ValueValidation),
}
}
#[test]
fn message_list_rejects_malformed_hashes_cleanly() {
assert_value_validation_err(&["aleph", "message", "list", "--hashes", "not-a-hash"]);
assert_value_validation_err(&[
"aleph",
"message",
"list",
"--content-hashes",
"definitely-not-hex",
]);
}
#[test]
fn post_list_rejects_malformed_hashes_cleanly() {
assert_value_validation_err(&["aleph", "post", "list", "--hashes", "not-a-hash"]);
}
#[test]
fn message_list_accepts_hex_addresses() {
let cli = Cli::try_parse_from([
"aleph",
"message",
"list",
"--addresses",
"0xABCD1234,0xEF560000",
"--owners",
"0xDEADBEEF",
])
.expect("clap parse");
match cli.command {
Commands::Message {
command: MessageCommand::List(args),
} => {
let addresses = args.filter.addresses.unwrap();
assert_eq!(addresses.len(), 2);
assert_eq!(addresses[0].to_string(), "0xABCD1234");
let owners = args.filter.owners.unwrap();
assert_eq!(owners.len(), 1);
assert_eq!(owners[0].to_string(), "0xDEADBEEF");
}
_ => panic!("expected message list"),
}
}
#[test]
fn post_list_accepts_hex_addresses() {
let cli =
Cli::try_parse_from(["aleph", "post", "list", "--addresses", "0xABCD1234"]).unwrap();
match cli.command {
Commands::Post {
command: PostCommand::List(args),
} => {
let addresses = args.filter.addresses.unwrap();
assert_eq!(addresses.len(), 1);
assert_eq!(addresses[0].to_string(), "0xABCD1234");
}
_ => panic!("expected post list"),
}
}
}
#[cfg(test)]
mod port_forward_args_tests {
use super::*;
#[test]
fn list_accepts_address_and_vm_id() {
let cli = Cli::try_parse_from([
"aleph",
"instance",
"port-forward",
"list",
"--address",
"0xABCD1234",
"--vm-id",
"1111111111111111111111111111111111111111111111111111111111111111",
])
.expect("clap parse");
match cli.command {
Commands::Instance {
command:
InstanceCommand::PortForward {
command: PortForwardCommand::List(args),
},
} => {
assert_eq!(args.address.as_deref(), Some("0xABCD1234"));
assert!(args.vm_id.is_some());
}
_ => panic!("expected port-forward list"),
}
}
#[test]
fn create_rejects_port_zero() {
match Cli::try_parse_from([
"aleph",
"instance",
"port-forward",
"create",
"1111111111111111111111111111111111111111111111111111111111111111",
"0",
]) {
Ok(_) => panic!("expected parse error for port 0"),
Err(e) => assert_eq!(e.kind(), clap::error::ErrorKind::ValueValidation),
}
}
#[test]
fn create_rejects_port_above_max() {
let result = Cli::try_parse_from([
"aleph",
"instance",
"port-forward",
"create",
"1111111111111111111111111111111111111111111111111111111111111111",
"65536",
]);
match result {
Ok(_) => panic!("expected parse error for port 65536"),
Err(e) => assert_eq!(e.kind(), clap::error::ErrorKind::ValueValidation),
}
}
#[test]
fn create_defaults_tcp_true_udp_false() {
let cli = Cli::try_parse_from([
"aleph",
"instance",
"port-forward",
"create",
"1111111111111111111111111111111111111111111111111111111111111111",
"443",
])
.expect("clap parse");
match cli.command {
Commands::Instance {
command:
InstanceCommand::PortForward {
command: PortForwardCommand::Create(args),
},
} => {
assert_eq!(args.port, 443);
assert!(args.tcp);
assert!(!args.udp);
}
_ => panic!("expected port-forward create"),
}
}
#[test]
fn create_accepts_explicit_udp_only() {
let cli = Cli::try_parse_from([
"aleph",
"instance",
"port-forward",
"create",
"1111111111111111111111111111111111111111111111111111111111111111",
"5353",
"--tcp",
"false",
"--udp",
"true",
])
.expect("clap parse");
match cli.command {
Commands::Instance {
command:
InstanceCommand::PortForward {
command: PortForwardCommand::Create(args),
},
} => {
assert!(!args.tcp);
assert!(args.udp);
}
_ => panic!("expected port-forward create"),
}
}
#[test]
fn delete_accepts_optional_port() {
let cli = Cli::try_parse_from([
"aleph",
"instance",
"port-forward",
"delete",
"1111111111111111111111111111111111111111111111111111111111111111",
])
.expect("clap parse");
match cli.command {
Commands::Instance {
command:
InstanceCommand::PortForward {
command: PortForwardCommand::Delete(args),
},
} => {
assert!(args.port.is_none());
}
_ => panic!("expected port-forward delete"),
}
}
#[test]
fn create_accepts_hash_prefix() {
let cli = Cli::try_parse_from([
"aleph",
"instance",
"port-forward",
"create",
"a41fb91c3e68",
"443",
])
.expect("clap parse");
match cli.command {
Commands::Instance {
command:
InstanceCommand::PortForward {
command: PortForwardCommand::Create(args),
},
} => {
assert_eq!(args.vm_id, "a41fb91c3e68");
assert_eq!(args.port, 443);
}
_ => panic!("expected port-forward create"),
}
}
#[test]
fn pfw_alias_resolves_to_port_forward() {
let cli = Cli::try_parse_from(["aleph", "instance", "pfw", "list"]).expect("clap parse");
assert!(matches!(
cli.command,
Commands::Instance {
command: InstanceCommand::PortForward {
command: PortForwardCommand::List(_),
},
}
));
}
}
#[derive(Subcommand)]
pub enum DomainCommand {
List(DomainListArgs),
Add(DomainAddArgs),
Attach(DomainAttachArgs),
Detach(DomainDetachArgs),
Remove(DomainRemoveArgs),
}
#[derive(Args)]
pub struct DomainListArgs {
#[arg(long)]
pub address: Option<String>,
}
#[derive(Args)]
pub struct DomainAddArgs {
pub domain: String,
#[arg(long)]
pub target: String,
#[arg(long, value_enum, default_value = "ipfs")]
pub kind: DomainKindCli,
#[arg(long)]
pub catch_all_path: Option<String>,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct DomainAttachArgs {
pub domain: String,
#[arg(long = "to")]
pub target: String,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct DomainDetachArgs {
pub domain: String,
#[arg(long)]
pub yes: bool,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct DomainRemoveArgs {
pub domain: String,
#[arg(long)]
pub yes: bool,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub enum DomainKindCli {
Ipfs,
Program,
Instance,
}
#[derive(Subcommand)]
pub enum WebsiteCommand {
List(WebsiteListArgs),
Show(WebsiteShowArgs),
Deploy(WebsiteDeployArgs),
Update(WebsiteUpdateArgs),
Delete(WebsiteDeleteArgs),
}
#[derive(Args)]
pub struct WebsiteListArgs {
#[arg(long)]
pub address: Option<String>,
}
#[derive(Args)]
pub struct WebsiteShowArgs {
pub name: String,
#[arg(long)]
pub address: Option<String>,
}
#[derive(Args)]
pub struct WebsiteDeployArgs {
pub name: String,
pub path: std::path::PathBuf,
#[arg(long, value_enum, default_value = "none")]
pub framework: FrameworkCli,
#[arg(long)]
pub tag: Vec<String>,
#[arg(long)]
pub payment_chain: Option<String>,
#[arg(long, value_enum)]
pub payment_type: Option<PaymentTypeCli>,
#[arg(long)]
pub domain: Vec<String>,
#[arg(long)]
pub volume_id: Option<String>,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct WebsiteUpdateArgs {
pub name: String,
pub path: std::path::PathBuf,
#[arg(long, value_enum)]
pub framework: Option<FrameworkCli>,
#[arg(long)]
pub tag: Option<Vec<String>>,
#[arg(long)]
pub domain: Vec<String>,
#[arg(long)]
pub skip_domain_update: bool,
#[arg(long)]
pub volume_id: Option<String>,
#[arg(long)]
pub idempotent: bool,
#[arg(long)]
pub channel: Option<String>,
#[arg(long, value_enum)]
pub payment_type: Option<PaymentTypeCli>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct WebsiteDeleteArgs {
pub name: String,
#[arg(long)]
pub yes: bool,
#[arg(long)]
pub channel: Option<String>,
#[arg(long)]
pub on_behalf_of: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[cfg(test)]
mod instance_delete_args_tests {
use super::*;
const HASH: &str = "a41fb91c3e68370759b72338dd1947f18e2ed883837aec5dc731d5f427f90564";
#[test]
fn parses_minimal_invocation() {
let cli = Cli::try_parse_from(["aleph", "instance", "delete", HASH]).expect("clap parse");
match cli.command {
Commands::Instance {
command: InstanceCommand::Delete(args),
} => {
assert_eq!(args.vm_id.to_string(), HASH);
assert_eq!(args.reason, "User deletion");
assert!(!args.yes);
assert!(!args.signing.dry_run);
}
_ => panic!("expected instance delete"),
}
}
#[test]
fn accepts_reason_and_yes() {
let cli = Cli::try_parse_from([
"aleph",
"instance",
"delete",
HASH,
"--reason",
"decommission",
"-y",
])
.expect("clap parse");
match cli.command {
Commands::Instance {
command: InstanceCommand::Delete(args),
} => {
assert_eq!(args.reason, "decommission");
assert!(args.yes);
}
_ => panic!("expected instance delete"),
}
}
#[test]
fn rejects_missing_vm_id() {
let result = Cli::try_parse_from(["aleph", "instance", "delete"]);
assert!(result.is_err());
}
#[test]
fn accepts_hash_prefix() {
let cli = Cli::try_parse_from(["aleph", "instance", "delete", "a41fb91c3e68"])
.expect("clap parse");
match cli.command {
Commands::Instance {
command: InstanceCommand::Delete(args),
} => {
assert_eq!(args.vm_id, "a41fb91c3e68");
}
_ => panic!("expected instance delete"),
}
}
#[test]
fn accepts_dry_run_flag() {
let cli = Cli::try_parse_from(["aleph", "instance", "delete", HASH, "--dry-run"])
.expect("clap parse");
match cli.command {
Commands::Instance {
command: InstanceCommand::Delete(args),
} => {
assert!(args.signing.dry_run);
}
_ => panic!("expected instance delete"),
}
}
}
#[derive(Subcommand)]
pub enum PortForwardCommand {
List(PortForwardListArgs),
Create(PortForwardCreateArgs),
Update(PortForwardUpdateArgs),
Delete(PortForwardDeleteArgs),
Refresh(PortForwardRefreshArgs),
}
#[derive(Args)]
pub struct PortForwardListArgs {
#[arg(long)]
pub address: Option<String>,
#[arg(long)]
pub vm_id: Option<String>,
}
#[derive(Args)]
pub struct PortForwardCreateArgs {
pub vm_id: String,
#[arg(value_parser = clap::value_parser!(u16).range(1..=65535))]
pub port: u16,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub tcp: bool,
#[arg(long, default_value_t = false, action = clap::ArgAction::Set)]
pub udp: bool,
#[arg(long)]
pub channel: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct PortForwardUpdateArgs {
pub vm_id: String,
#[arg(value_parser = clap::value_parser!(u16).range(1..=65535))]
pub port: u16,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
pub tcp: bool,
#[arg(long, default_value_t = false, action = clap::ArgAction::Set)]
pub udp: bool,
#[arg(long)]
pub channel: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct PortForwardDeleteArgs {
pub vm_id: String,
#[arg(long, value_parser = clap::value_parser!(u16).range(1..=65535))]
pub port: Option<u16>,
#[arg(short = 'y', long)]
pub yes: bool,
#[arg(long)]
pub channel: Option<String>,
#[command(flatten)]
pub signing: SigningArgs,
}
#[derive(Args)]
pub struct PortForwardRefreshArgs {
pub vm_id: String,
#[command(flatten)]
pub identity: IdentityArgs,
}
#[derive(Subcommand)]
pub enum ConfidentialCommand {
InitSession(ConfidentialInitSessionArgs),
Start(ConfidentialStartArgs),
Create(ConfidentialCreateArgs),
}
#[derive(Args)]
pub struct ConfidentialInitSessionArgs {
pub vm_id: String,
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
#[command(flatten)]
pub identity: IdentityArgs,
#[arg(long, default_value_t = 0x1)]
pub policy: u32,
#[arg(long)]
pub keep_session: bool,
#[arg(long)]
pub debug: bool,
}
#[derive(Args)]
pub struct ConfidentialStartArgs {
pub vm_id: String,
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
#[command(flatten)]
pub identity: IdentityArgs,
#[arg(long)]
pub firmware_hash: Option<String>,
#[arg(long)]
pub firmware_file: Option<std::path::PathBuf>,
#[arg(long)]
pub secret: Option<String>,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub debug: bool,
}
#[derive(Args)]
pub struct ConfidentialCreateArgs {
pub vm_id: Option<String>,
#[arg(long, alias = "crn-url")]
pub crn: Option<String>,
#[command(flatten)]
pub identity: IdentityArgs,
#[arg(long, default_value_t = 0x1)]
pub policy: u32,
#[arg(long)]
pub keep_session: bool,
#[arg(long)]
pub firmware_hash: Option<String>,
#[arg(long)]
pub firmware_file: Option<std::path::PathBuf>,
#[arg(long)]
pub secret: Option<String>,
#[arg(long)]
pub debug: bool,
}
#[cfg(test)]
mod confidential_parser_tests {
use super::*;
use clap::Parser;
fn parse(args: &[&str]) -> Cli {
Cli::try_parse_from(args).expect("parse")
}
#[test]
fn init_session_accepts_hash_prefix() {
let cli = parse(&[
"aleph",
"instance",
"confidential",
"init-session",
"236328f6",
]);
let Commands::Instance {
command: InstanceCommand::Confidential(ConfidentialCommand::InitSession(a)),
} = cli.command
else {
panic!("wrong subcommand");
};
assert_eq!(a.vm_id, "236328f6");
assert_eq!(a.policy, 0x1);
assert!(!a.keep_session);
}
#[test]
fn start_accepts_secret_and_json() {
let cli = parse(&[
"aleph",
"instance",
"confidential",
"start",
"236328f6",
"--secret",
"hunter2",
"--json",
]);
let Commands::Instance {
command: InstanceCommand::Confidential(ConfidentialCommand::Start(a)),
} = cli.command
else {
panic!("wrong subcommand");
};
assert_eq!(a.secret.as_deref(), Some("hunter2"));
assert!(a.json);
}
#[test]
fn create_accepts_no_positional() {
let cli = parse(&["aleph", "instance", "confidential", "create"]);
let Commands::Instance {
command: InstanceCommand::Confidential(ConfidentialCommand::Create(a)),
} = cli.command
else {
panic!("wrong subcommand");
};
assert_eq!(a.vm_id, None);
}
}