From 972caa8acaa093b268ef304f29f048b0dda90791 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Mon, 2 Mar 2026 18:44:32 +0000 Subject: [PATCH 1/4] update tenant get command --- smartcontract/cli/src/tenant/get.rs | 162 +++++++++++++++++++++++++--- 1 file changed, 148 insertions(+), 14 deletions(-) diff --git a/smartcontract/cli/src/tenant/get.rs b/smartcontract/cli/src/tenant/get.rs index c5cce3062..f03ffcf23 100644 --- a/smartcontract/cli/src/tenant/get.rs +++ b/smartcontract/cli/src/tenant/get.rs @@ -1,29 +1,87 @@ use crate::{doublezerocommand::CliCommand, validators::validate_pubkey_or_code}; use clap::Args; use doublezero_sdk::commands::tenant::get::GetTenantCommand; +use serde::Serialize; use std::io::Write; +use tabled::Tabled; #[derive(Args, Debug)] pub struct GetTenantCliCommand { /// Tenant pubkey or code #[arg(long, value_parser = validate_pubkey_or_code)] pub code: String, + /// Output as JSON + #[arg(long)] + pub json: bool, +} + +#[derive(Tabled, Serialize)] +struct ConfigDisplay { + #[tabled(rename = "account")] + pub account: String, + #[tabled(rename = "code")] + pub code: String, + #[tabled(rename = "vrf_id")] + pub vrf_id: u16, + #[tabled(rename = "metro_routing")] + pub metro_routing: bool, + #[tabled(rename = "route_liveness")] + pub route_liveness: bool, + #[tabled(rename = "reference_count")] + pub reference_count: u32, + #[tabled(rename = "owner")] + pub owner: String, +} + +#[derive(Tabled, Serialize)] +struct AdminDisplay { + #[tabled(rename = "administrator")] + pub administrator: String, } impl GetTenantCliCommand { pub fn execute(self, client: &C, out: &mut W) -> eyre::Result<()> { let (pubkey, tenant) = client.get_tenant(GetTenantCommand { - pubkey_or_code: self.code, + pubkey_or_code: self.code.clone(), })?; - writeln!(out, "account: {pubkey}")?; - writeln!(out, "code: {}", tenant.code)?; - writeln!(out, "vrf_id: {}", tenant.vrf_id)?; - writeln!(out, "metro_routing: {}", tenant.metro_routing)?; - writeln!(out, "route_liveness: {}", tenant.route_liveness)?; - writeln!(out, "reference_count: {}", tenant.reference_count)?; - writeln!(out, "owner: {}", tenant.owner)?; + let config = ConfigDisplay { + account: pubkey.to_string(), + code: tenant.code.clone(), + vrf_id: tenant.vrf_id, + metro_routing: tenant.metro_routing, + route_liveness: tenant.route_liveness, + reference_count: tenant.reference_count, + owner: tenant.owner.to_string(), + }; + let admin_rows: Vec = tenant + .administrators + .iter() + .map(|a| AdminDisplay { + administrator: a.to_string(), + }) + .collect(); + if self.json { + #[derive(Serialize)] + struct Output { + config: ConfigDisplay, + administrators: Vec, + } + let output = Output { + config, + administrators: admin_rows, + }; + let json = serde_json::to_string_pretty(&output)?; + writeln!(out, "{}", json)?; + } else { + let table = tabled::Table::new([config]); + writeln!(out, "{}", table)?; + if !admin_rows.is_empty() { + let admin_table = tabled::Table::new(admin_rows); + writeln!(out, "\n{}", admin_table)?; + } + } Ok(()) } } @@ -58,6 +116,13 @@ mod tests { billing: TenantBillingConfig::default(), }; + let tenant_with_admins = Tenant { + administrators: vec![Pubkey::from_str_const( + "FposHWrkvPP3VErBAWCd4ELWGuh2mgx2Wx6cuNEA4X2S", + )], + ..tenant.clone() + }; + let tenant_cloned = tenant.clone(); client .expect_get_tenant() @@ -72,38 +137,107 @@ mod tests { pubkey_or_code: "test-tenant".to_string(), })) .returning(move |_| Ok((tenant_pubkey, tenant_cloned2.clone()))); + let tenant_with_admins_cloned = tenant_with_admins.clone(); + client + .expect_get_tenant() + .with(predicate::eq(GetTenantCommand { + pubkey_or_code: "test-tenant-admin".to_string(), + })) + .returning(move |_| Ok((tenant_pubkey, tenant_with_admins_cloned.clone()))); client .expect_get_tenant() .returning(move |_| Err(eyre::eyre!("not found"))); - /*****************************************************************************************************/ // Expected failure let mut output = Vec::new(); let res = GetTenantCliCommand { code: Pubkey::new_unique().to_string(), + json: false, } .execute(&client, &mut output); assert!(res.is_err(), "I shouldn't find anything."); - // Expected success by pubkey + // Expected success by pubkey (no admins, table output) let mut output = Vec::new(); let res = GetTenantCliCommand { code: tenant_pubkey.to_string(), + json: false, } .execute(&client, &mut output); assert!(res.is_ok(), "I should find a item by pubkey"); let output_str = String::from_utf8(output).unwrap(); - assert_eq!( - output_str, - "account: BmrLoL9jzYo4yiPUsFhYFU8hgE3CD3Npt8tgbqvneMyB\ncode: test-tenant\nvrf_id: 100\nmetro_routing: true\nroute_liveness: false\nreference_count: 0\nowner: BmrLoL9jzYo4yiPUsFhYFU8hgE3CD3Npt8tgbqvneMyB\n" + assert!( + output_str.contains("account"), + "Should contain table header" + ); + assert!( + output_str.contains("test-tenant"), + "Should contain tenant code" + ); + assert!( + !output_str.contains("administrator"), + "Should not contain admin table" ); - // Expected success by code + // Expected success by code (no admins, JSON output) let mut output = Vec::new(); let res = GetTenantCliCommand { code: "test-tenant".to_string(), + json: true, } .execute(&client, &mut output); assert!(res.is_ok(), "I should find a item by code"); + let output_str = String::from_utf8(output).unwrap(); + assert!( + output_str.contains("\"config\""), + "Should contain config key in JSON" + ); + assert!( + output_str.contains("\"administrators\""), + "Should contain administrators key in JSON" + ); + assert!( + output_str.contains("test-tenant"), + "Should contain tenant code in JSON" + ); + + // Expected success with admins (table output) + let mut output = Vec::new(); + let res = GetTenantCliCommand { + code: "test-tenant-admin".to_string(), + json: false, + } + .execute(&client, &mut output); + assert!(res.is_ok(), "I should find a item by code with admins"); + let output_str = String::from_utf8(output).unwrap(); + assert!( + output_str.contains("administrator"), + "Should contain admin table header" + ); + assert!( + output_str.contains("FposHWrkvPP3VErBAWCd4ELWGuh2mgx2Wx6cuNEA4X2S"), + "Should contain admin pubkey" + ); + + // Expected success with admins (JSON output) + let mut output = Vec::new(); + let res = GetTenantCliCommand { + code: "test-tenant-admin".to_string(), + json: true, + } + .execute(&client, &mut output); + assert!( + res.is_ok(), + "I should find a item by code with admins (json)" + ); + let output_str = String::from_utf8(output).unwrap(); + assert!( + output_str.contains("\"administrator\""), + "Should contain admin key in JSON" + ); + assert!( + output_str.contains("FposHWrkvPP3VErBAWCd4ELWGuh2mgx2Wx6cuNEA4X2S"), + "Should contain admin pubkey in JSON" + ); } } From 0f029c218306b78e6a463b82ed8f752a1a76b11a Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Mon, 2 Mar 2026 18:53:44 +0000 Subject: [PATCH 2/4] smartcontract/cli: add changelog entry for tenant get improvements --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f90503641..682b18ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. - CLI - `doublezero resource verify` command will now suggest creating resources or create them with --fix + - Improve `tenant get` output: display config and administrators as formatted tables; add `--json` flag for machine-readable output - SDK - Fix multicast group deserialization in `smartcontract/sdk/go` to correctly read publisher and subscriber counts and align status enum with onchain definition - Smartcontract From 1474174a2361f160bf7d2979e5ee324d2f20d567 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Mon, 2 Mar 2026 20:02:50 +0000 Subject: [PATCH 3/4] e2e: update getTenantVrfID to use tenant get --json flag --- e2e/multi_tenant_vrf_test.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/e2e/multi_tenant_vrf_test.go b/e2e/multi_tenant_vrf_test.go index 19fa1266b..9691c160f 100644 --- a/e2e/multi_tenant_vrf_test.go +++ b/e2e/multi_tenant_vrf_test.go @@ -3,11 +3,11 @@ package e2e_test import ( + "encoding/json" "fmt" "log/slog" "os" "path/filepath" - "strconv" "strings" "sync" "testing" @@ -597,15 +597,19 @@ func TestE2E_TenantDeletionLifecycle(t *testing.T) { // `doublezero tenant get --code `. func getTenantVrfID(t *testing.T, dn *devnet.Devnet, tenantCode string) uint16 { t.Helper() - output, err := dn.Manager.Exec(t.Context(), []string{"doublezero", "tenant", "get", "--code", tenantCode}) + output, err := dn.Manager.Exec(t.Context(), []string{"doublezero", "tenant", "get", "--code", tenantCode, "--json"}) require.NoError(t, err) - for _, line := range strings.Split(string(output), "\n") { - if strings.HasPrefix(line, "vrf_id: ") { - val, err := strconv.ParseUint(strings.TrimPrefix(line, "vrf_id: "), 10, 16) - require.NoError(t, err) - return uint16(val) - } + type config struct { + VrfID uint16 `json:"vrf_id"` + } + type response struct { + Config config `json:"config"` + } + var resp response + err = json.Unmarshal(output, &resp) + require.NoError(t, err, "failed to parse JSON output: %s", string(output)) + if resp.Config.VrfID == 0 { + t.Fatalf("vrf_id not found in tenant get output for %s: %s", tenantCode, string(output)) } - t.Fatalf("vrf_id not found in tenant get output for %s: %s", tenantCode, string(output)) - return 0 + return resp.Config.VrfID } From 4c4812afa3ad1311a3260a1bbe27e842e0f4155b Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Mon, 2 Mar 2026 23:50:10 +0000 Subject: [PATCH 4/4] smartcontract/cli: remove redundant tabled rename attributes in tenant get --- smartcontract/cli/src/tenant/get.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/smartcontract/cli/src/tenant/get.rs b/smartcontract/cli/src/tenant/get.rs index f03ffcf23..4e6b05ba3 100644 --- a/smartcontract/cli/src/tenant/get.rs +++ b/smartcontract/cli/src/tenant/get.rs @@ -17,25 +17,17 @@ pub struct GetTenantCliCommand { #[derive(Tabled, Serialize)] struct ConfigDisplay { - #[tabled(rename = "account")] pub account: String, - #[tabled(rename = "code")] pub code: String, - #[tabled(rename = "vrf_id")] pub vrf_id: u16, - #[tabled(rename = "metro_routing")] pub metro_routing: bool, - #[tabled(rename = "route_liveness")] pub route_liveness: bool, - #[tabled(rename = "reference_count")] pub reference_count: u32, - #[tabled(rename = "owner")] pub owner: String, } #[derive(Tabled, Serialize)] struct AdminDisplay { - #[tabled(rename = "administrator")] pub administrator: String, }