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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ All notable changes to this project will be documented in this file.
- Ship shell-completion scripts in the client installer and recommend `bash-completion` so apt/dnf pull it in when available. `build/` is added to `.gitignore`.
- Migrate all six `exchange` verbs (`create`, `update`, `list`, `get`, `delete`, `set-device`) to the RFC-20 conforming shape on top of the shared CLI helpers. Every verb is now `pub async fn execute(self, ctx: &CliContext, client: &C, out: &mut W) -> eyre::Result<()>`, consumes the helpers (`require!`, `render_collection`, `render_record`, `print_signature`), and the update/delete/set-device paths route their pubkey-or-code argument through a new `resolve_exchange_pk` helper in `smartcontract/cli/src/helpers.rs`. The pre-existing BGP community range check in `exchange update` is preserved. `exchange set-device` retains its legacy `Option<String>::and_then` semantics for `--device1` / `--device2` (an unknown device silently resolves to `None`, which clears the slot) under an explanatory comment. `controlplane/doublezero-admin`, the unified `doublezero` binary, and the serviceability dispatcher all forward `&ctx` and await every exchange arm. Behavior is byte-identical: table layout, JSON schema, `Signature: <sig>` line, and `--json` / `--json-compact` semantics match pre-refactor output exactly; all 7 exchange unit tests pass without assertion changes.
- Migrate all five `contributor` verbs (`create`, `update`, `list`, `get`, `delete`) to the RFC-20 conforming shape on top of the shared CLI helpers. Every verb is now `pub async fn execute(self, ctx: &CliContext, client: &C, out: &mut W) -> eyre::Result<()>`, consumes the helpers (`require!`, `render_collection`, `render_record`, `print_signature`), and `update` / `delete` route their pubkey-or-code argument through a new `resolve_contributor_pk` helper in `smartcontract/cli/src/helpers.rs`. The duplicate-code precondition in `create` and `update` is preserved, as is the `owner = "me"` short-circuit in `create` that resolves to the payer. `update`'s pubkey resolution now goes through the shared helper rather than an in-line `Pubkey::from_str` (the old code-by-pubkey path was a code-or-pubkey path despite the variable name; the resolver accepts both). `controlplane/doublezero-admin`, the unified `doublezero` binary, and the serviceability dispatcher all forward `&ctx` and await every contributor arm. Behavior is byte-identical: table layout, JSON schema, `Signature: <sig>` line, and `--json` / `--json-compact` semantics match pre-refactor output exactly; all 5 contributor unit tests pass without assertion changes.
- Migrate all six `accesspass` verbs (`set`, `close`, `list`, `get`, `user-balances`, `fund`) and all six `resource` verbs (`allocate`, `create`, `deallocate`, `get`, `close`, `verify`), plus the eight leaf single-file verbs (`address`, `balance`, `init`, `migrate`, `keygen`, `export`, `config get`, `config set`), to the RFC-20 `pub async fn execute(self, ctx: &CliContext, client, out) -> eyre::Result<()>` signature. The five small leaf verbs (`address`, `balance`, `init`, `migrate`, `keygen`) also adopt the `require!` macro and (where applicable) the `print_signature` helper since their bodies were one-line readiness checks paired with a single `Signature:` write. The larger and more idiosyncratic verbs (`config get`/`set` which manipulate the persisted YAML, `export` which serializes the whole graph, the accesspass and resource verbs which contain bespoke output and progress-spinner logic) keep their existing bodies for now and only get the signature flip; helper adoption for those lands opportunistically in follow-up PRs. `controlplane/doublezero-admin`, the unified `doublezero` binary, and the serviceability dispatcher all forward `&ctx` and await every accesspass, resource, and leaf-verb arm. `config get`/`set` tests gain a per-file `block_on` shim and a `cli_context_default_for_tests()` import so the existing sync `#[test]` bodies can still drive the now-async `execute`. The bespoke `accesspass fund` signature (`R: BufRead` for stdin) is preserved — only the `_ctx` parameter is inserted after `self`. Behavior is byte-identical: table layouts, JSON schemas, `Signature:` lines, the `fund` interactive flow, and the `config` text output all match the pre-refactor strings exactly; all 345 unit tests pass without assertion changes.
- Migrate all eight `tenant` verbs (`create`, `update`, `list`, `get`, `delete`, `administrator add`, `administrator remove`, `update-payment-status`) and all six `permission` verbs (`set`, `suspend`, `resume`, `delete`, `get`, `list`) to the RFC-20 conforming shape on top of the shared CLI helpers. Every verb is now `pub async fn execute(self, ctx: &CliContext, client: &C, out: &mut W) -> eyre::Result<()>`, consumes the helpers (`require!`, `render_collection`, `render_record`, `print_signature`), and tenant verbs that accept a pubkey-or-code identifier (`update`, `delete`, `add-administrator`, `remove-administrator`, `update-payment-status`) route through a new `resolve_tenant_pk` helper in `smartcontract/cli/src/helpers.rs`. The duplicate-code precondition in `tenant create` is preserved, as is the `administrator = "me"` short-circuit. `tenant delete`'s bespoke two-line output ("✓ Tenant 'X' deleted successfully\n Signature: ..."), its cascade-delete progress spinners, and its reference-count polling loop are preserved with manual `writeln!` calls and an explanatory comment. `permission set`'s bespoke two-line aligned output ("Signature: ..." + "Permissions: ...") is preserved the same way. Permission verbs derive the on-chain PDA from `user_payer` rather than going through a pubkey-or-code resolver. `controlplane/doublezero-admin`, the unified `doublezero` binary, and the serviceability dispatcher all forward `&ctx` and await every tenant and permission arm. Behavior is byte-identical: table layout, JSON schema, `Signature:` line shape, and `--json` / `--json-compact` semantics match pre-refactor output exactly; all 18 tenant and 18 permission unit tests pass without assertion changes.

## [v0.24.0](https://github.com/malbeclabs/doublezero/compare/client/v0.23.0...client/v0.24.0) - 2026-05-22
Expand Down
35 changes: 22 additions & 13 deletions controlplane/doublezero-admin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ async fn main() -> eyre::Result<()> {
let mut handle = stdout.lock();

let res = match app.command {
Command::Address(args) => args.execute(&client, &mut handle),
Command::Balance(args) => args.execute(&client, &mut handle),
Command::Address(args) => args.execute(&ctx, &client, &mut handle).await,
Command::Balance(args) => args.execute(&ctx, &client, &mut handle).await,

Command::Init(args) => args.execute(&client, &mut handle),
Command::Init(args) => args.execute(&ctx, &client, &mut handle).await,
Command::Config(command) => match command.command {
ConfigCommands::Get(args) => args.execute(&client, &mut handle),
ConfigCommands::Set(args) => args.execute(&client, &mut handle),
ConfigCommands::Get(args) => args.execute(&ctx, &client, &mut handle).await,
ConfigCommands::Set(args) => args.execute(&ctx, &client, &mut handle).await,
},
Command::GlobalConfig(command) => match command.command {
GlobalConfigCommands::Set(args) => args.execute(&client, &mut handle),
Expand Down Expand Up @@ -183,15 +183,24 @@ async fn main() -> eyre::Result<()> {
LinkCommands::Delete(args) => args.execute(&client, &mut handle),
},
Command::AccessPass(command) => match command.command {
cli::accesspass::AccessPassCommands::Set(args) => args.execute(&client, &mut handle),
cli::accesspass::AccessPassCommands::Close(args) => args.execute(&client, &mut handle),
cli::accesspass::AccessPassCommands::List(args) => args.execute(&client, &mut handle),
cli::accesspass::AccessPassCommands::Get(args) => args.execute(&client, &mut handle),
cli::accesspass::AccessPassCommands::Set(args) => {
args.execute(&ctx, &client, &mut handle).await
}
cli::accesspass::AccessPassCommands::Close(args) => {
args.execute(&ctx, &client, &mut handle).await
}
cli::accesspass::AccessPassCommands::List(args) => {
args.execute(&ctx, &client, &mut handle).await
}
cli::accesspass::AccessPassCommands::Get(args) => {
args.execute(&ctx, &client, &mut handle).await
}
cli::accesspass::AccessPassCommands::UserBalances(args) => {
args.execute(&client, &mut handle)
args.execute(&ctx, &client, &mut handle).await
}
cli::accesspass::AccessPassCommands::Fund(args) => {
args.execute(&client, &mut handle, &mut std::io::stdin().lock())
args.execute(&ctx, &client, &mut handle, &mut std::io::stdin().lock())
.await
}
},
Command::Permission(command) => match command.command {
Expand Down Expand Up @@ -274,8 +283,8 @@ async fn main() -> eyre::Result<()> {
Command::Migrate(args) => match args.command {
cli::migrate::MigrateCommands::FlexAlgo(cmd) => cmd.execute(&client, &mut handle),
},
Command::Export(args) => args.execute(&client, &mut handle),
Command::Keygen(args) => args.execute(&client, &mut handle),
Command::Export(args) => args.execute(&ctx, &client, &mut handle).await,
Command::Keygen(args) => args.execute(&ctx, &client, &mut handle).await,
Command::Log(args) => args.execute(&dzclient, &mut handle),
Command::Completion(args) => {
let mut cmd = App::command();
Expand Down
20 changes: 15 additions & 5 deletions smartcontract/cli/src/accesspass/close.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::{
requirements::{CHECK_BALANCE, CHECK_ID_JSON},
};
use clap::Args;
use doublezero_cli_core::CliContext;
use doublezero_sdk::commands::accesspass::close::CloseAccessPassCommand;
use solana_sdk::pubkey::Pubkey;
use std::io::Write;
Expand All @@ -15,7 +16,12 @@ pub struct CloseAccessPassCliCommand {
}

impl CloseAccessPassCliCommand {
pub fn execute<C: CliCommand, W: Write>(self, client: &C, out: &mut W) -> eyre::Result<()> {
pub async fn execute<C: CliCommand, W: Write>(
self,
_ctx: &CliContext,
client: &C,
out: &mut W,
) -> eyre::Result<()> {
// Check requirements
client.check_requirements(CHECK_ID_JSON | CHECK_BALANCE)?;

Expand All @@ -36,6 +42,7 @@ mod tests {
requirements::{CHECK_BALANCE, CHECK_ID_JSON},
tests::utils::create_test_client,
};
use doublezero_cli_core::testing::{block_on, cli_context_default_for_tests};
use doublezero_sdk::commands::accesspass::close::CloseAccessPassCommand;
use doublezero_serviceability::pda::get_accesspass_pda;
use mockall::predicate;
Expand Down Expand Up @@ -70,11 +77,14 @@ mod tests {
}))
.returning(move |_| Ok(signature));

let ctx = cli_context_default_for_tests();
let mut output = Vec::new();
let res = CloseAccessPassCliCommand {
pubkey: accesspass_pubkey,
}
.execute(&client, &mut output);
let res = block_on(
CloseAccessPassCliCommand {
pubkey: accesspass_pubkey,
}
.execute(&ctx, &client, &mut output),
);
assert!(res.is_ok());
let output_str = String::from_utf8(output).unwrap();
assert_eq!(
Expand Down
88 changes: 59 additions & 29 deletions smartcontract/cli/src/accesspass/fund.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::doublezerocommand::CliCommand;
use clap::Args;
use doublezero_cli_core::CliContext;
use doublezero_sdk::{
commands::{accesspass::list::ListAccessPassCommand, user::list::ListUserCommand},
UserType,
Expand Down Expand Up @@ -31,8 +32,9 @@ pub struct FundAccessPassCliCommand {
}

impl FundAccessPassCliCommand {
pub fn execute<C: CliCommand, W: Write, R: BufRead>(
pub async fn execute<C: CliCommand, W: Write, R: BufRead>(
self,
_ctx: &CliContext,
client: &C,
out: &mut W,
input: &mut R,
Expand Down Expand Up @@ -186,6 +188,7 @@ impl FundAccessPassCliCommand {
mod tests {
use super::*;
use crate::tests::utils::create_test_client;
use doublezero_cli_core::testing::{block_on, cli_context_default_for_tests};
use doublezero_sdk::AccountType;
use doublezero_serviceability::state::accesspass::{
AccessPass, AccessPassStatus, AccessPassType,
Expand Down Expand Up @@ -246,9 +249,14 @@ mod tests {
// balance > required (wallet_rent_min + needs_rent = 1_000_000 + 1_250_000 = 2_250_000)
let client = setup_client_with_balance(payer, 2_500_000);

let ctx = cli_context_default_for_tests();
let mut out = Vec::new();
let res =
FundAccessPassCliCommand::default().execute(&client, &mut out, &mut "".as_bytes());
let res = block_on(FundAccessPassCliCommand::default().execute(
&ctx,
&client,
&mut out,
&mut "".as_bytes(),
));

assert!(res.is_ok());
assert_eq!(
Expand All @@ -263,12 +271,15 @@ mod tests {
// balance = 500_000 < required (wallet_rent_min + needs_rent = 1_000_000 + 1_250_000 = 2_250_000), deficit = 1_750_000
let client = setup_client_with_balance(payer, 500_000);

let ctx = cli_context_default_for_tests();
let mut out = Vec::new();
let res = FundAccessPassCliCommand {
dry_run: true,
..Default::default()
}
.execute(&client, &mut out, &mut "".as_bytes());
let res = block_on(
FundAccessPassCliCommand {
dry_run: true,
..Default::default()
}
.execute(&ctx, &client, &mut out, &mut "".as_bytes()),
);

assert!(res.is_ok());
let output = String::from_utf8(out).unwrap();
Expand All @@ -287,9 +298,14 @@ mod tests {
.expect_transfer_sol()
.returning(|_, _| Ok(solana_sdk::signature::Signature::default()));

let ctx = cli_context_default_for_tests();
let mut out = Vec::new();
let res =
FundAccessPassCliCommand::default().execute(&client, &mut out, &mut "y\n".as_bytes());
let res = block_on(FundAccessPassCliCommand::default().execute(
&ctx,
&client,
&mut out,
&mut "y\n".as_bytes(),
));

assert!(res.is_ok());
let output = String::from_utf8(out).unwrap();
Expand All @@ -302,9 +318,14 @@ mod tests {
let payer = Pubkey::from_str_const("1111111FVAiSujNZVgYSc27t6zUTWoKfAGxbRzzPB");
let client = setup_client_with_balance(payer, 500_000);

let ctx = cli_context_default_for_tests();
let mut out = Vec::new();
let res =
FundAccessPassCliCommand::default().execute(&client, &mut out, &mut "n\n".as_bytes());
let res = block_on(FundAccessPassCliCommand::default().execute(
&ctx,
&client,
&mut out,
&mut "n\n".as_bytes(),
));

assert!(res.is_ok());
let output = String::from_utf8(out).unwrap();
Expand All @@ -320,13 +341,16 @@ mod tests {
// required = wallet_rent_min + max(needs_rent, min_balance) = 1_000_000 + max(1_250_000, 2_000_000) = 3_000_000, deficit = 1_500_000
let client = setup_client_with_balance(payer, 1_500_000);

let ctx = cli_context_default_for_tests();
let mut out = Vec::new();
let res = FundAccessPassCliCommand {
min_balance: Some(0.002), // 2_000_000 lamports
dry_run: true,
..Default::default()
}
.execute(&client, &mut out, &mut "".as_bytes());
let res = block_on(
FundAccessPassCliCommand {
min_balance: Some(0.002), // 2_000_000 lamports
dry_run: true,
..Default::default()
}
.execute(&ctx, &client, &mut out, &mut "".as_bytes()),
);

assert!(res.is_ok());
let output = String::from_utf8(out).unwrap();
Expand All @@ -341,13 +365,16 @@ mod tests {
// required = wallet_rent_min + max(needs_rent, min_balance) = 1_000_000 + max(1_250_000, 1) = 2_250_000, deficit = 1_750_000
let client = setup_client_with_balance(payer, 500_000);

let ctx = cli_context_default_for_tests();
let mut out = Vec::new();
let res = FundAccessPassCliCommand {
min_balance: Some(0.000000001),
dry_run: true,
..Default::default()
}
.execute(&client, &mut out, &mut "".as_bytes());
let res = block_on(
FundAccessPassCliCommand {
min_balance: Some(0.000000001),
dry_run: true,
..Default::default()
}
.execute(&ctx, &client, &mut out, &mut "".as_bytes()),
);

assert!(res.is_ok());
let output = String::from_utf8(out).unwrap();
Expand All @@ -363,12 +390,15 @@ mod tests {
.expect_transfer_sol()
.returning(|_, _| Ok(solana_sdk::signature::Signature::default()));

let ctx = cli_context_default_for_tests();
let mut out = Vec::new();
let res = FundAccessPassCliCommand {
force: true,
..Default::default()
}
.execute(&client, &mut out, &mut "".as_bytes());
let res = block_on(
FundAccessPassCliCommand {
force: true,
..Default::default()
}
.execute(&ctx, &client, &mut out, &mut "".as_bytes()),
);

assert!(res.is_ok());
let output = String::from_utf8(out).unwrap();
Expand Down
24 changes: 17 additions & 7 deletions smartcontract/cli/src/accesspass/get.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::doublezerocommand::CliCommand;
use clap::Args;
use doublezero_cli_core::CliContext;
use doublezero_sdk::commands::{
accesspass::get::GetAccessPassCommand, multicastgroup::list::ListMulticastGroupCommand,
tenant::list::ListTenantCommand,
Expand Down Expand Up @@ -42,7 +43,12 @@ struct AccessPassDisplay {
}

impl GetAccessPassCliCommand {
pub fn execute<C: CliCommand, W: Write>(self, client: &C, out: &mut W) -> eyre::Result<()> {
pub async fn execute<C: CliCommand, W: Write>(
self,
_ctx: &CliContext,
client: &C,
out: &mut W,
) -> eyre::Result<()> {
let epoch = client.get_epoch()?;

let (pubkey, accesspass) = client
Expand Down Expand Up @@ -124,6 +130,7 @@ impl GetAccessPassCliCommand {
#[cfg(test)]
mod tests {
use crate::{accesspass::get::GetAccessPassCliCommand, tests::utils::create_test_client};
use doublezero_cli_core::testing::{block_on, cli_context_default_for_tests};
use doublezero_sdk::{
commands::{
accesspass::get::GetAccessPassCommand, multicastgroup::list::ListMulticastGroupCommand,
Expand Down Expand Up @@ -221,13 +228,16 @@ mod tests {
Ok(map)
});

let ctx = cli_context_default_for_tests();
let mut output = Vec::new();
let res = GetAccessPassCliCommand {
client_ip,
user_payer,
json: false,
}
.execute(&client, &mut output);
let res = block_on(
GetAccessPassCliCommand {
client_ip,
user_payer,
json: false,
}
.execute(&ctx, &client, &mut output),
);
assert!(res.is_ok());
let output_str = String::from_utf8(output).unwrap();
let has_row = |header: &str, value: &str| {
Expand Down
Loading
Loading