1use std::path::PathBuf;
11
12use anyhow::Result;
13use clap::{Parser, Subcommand};
14
15use auths_sdk::storage_layout::layout;
16
17use crate::commands::executable::ExecutableCommand;
18use crate::config::CliConfig;
19use crate::ux::format::{JsonResponse, is_json_mode};
20
21#[derive(Parser, Debug, Clone)]
23#[command(
24 about = "Aggregate treasury cap: allot bounded slices across sub-agents, reallocatable, with Σ ≤ cap.",
25 after_help = "Examples:
26 auths treasury open --manager my-key --cap calls:10
27 auths treasury allot --manager my-key --to did:keri:E… --amount 4
28 auths treasury reallocate --manager my-key --from did:keri:E… --to did:keri:E… --amount 2
29 auths treasury status --manager my-key"
30)]
31pub struct TreasuryCommand {
32 #[clap(subcommand)]
33 pub subcommand: TreasurySubcommand,
34}
35
36#[derive(Subcommand, Debug, Clone)]
38pub enum TreasurySubcommand {
39 Open {
41 #[arg(long, help = "Manager keychain alias (the ledger key).")]
42 manager: String,
43 #[arg(long, help = "Aggregate cap (calls:<N> or a plain integer).")]
44 cap: String,
45 },
46 Allot {
48 #[arg(long)]
49 manager: String,
50 #[arg(long = "to", help = "Sub-agent did:keri: to allot to.")]
51 to: String,
52 #[arg(long)]
53 amount: u64,
54 },
55 Reallocate {
57 #[arg(long)]
58 manager: String,
59 #[arg(long, help = "Source sub-agent did:keri:.")]
60 from: String,
61 #[arg(long = "to", help = "Target sub-agent did:keri:.")]
62 to: String,
63 #[arg(long)]
64 amount: u64,
65 },
66 Status {
68 #[arg(long)]
69 manager: String,
70 },
71 Credit {
73 #[arg(long = "to", help = "Sub-agent did:keri: to credit.")]
74 to: String,
75 #[arg(long, help = "Path to a recorded x402 SettlementResponse JSON.")]
76 settlement: PathBuf,
77 #[arg(
78 long = "claim-cents",
79 help = "Assert the credited amount (rejected if ≠ the recorded settlement)."
80 )]
81 claim_cents: Option<u64>,
82 },
83 Subdelegate {
85 #[arg(long)]
86 manager: String,
87 #[arg(
88 long,
89 help = "The sub-agent (parent) did:keri: doing the sub-delegation."
90 )]
91 parent: String,
92 #[arg(long, help = "The child worker did:keri:.")]
93 child: String,
94 #[arg(long)]
95 amount: u64,
96 },
97 Reclaim {
99 #[arg(long)]
100 manager: String,
101 #[arg(long = "agent", help = "The revoked sub-agent did:keri: to reclaim.")]
102 agent: String,
103 },
104}
105
106impl ExecutableCommand for TreasuryCommand {
107 fn execute(&self, ctx: &CliConfig) -> Result<()> {
108 let repo_path = layout::resolve_repo_path(ctx.repo_path.clone())?;
109 handle_treasury(self.clone(), repo_path)
110 }
111}
112
113fn parse_cap(s: &str) -> Result<u64> {
115 let raw = s.strip_prefix("calls:").unwrap_or(s).trim();
116 raw.parse::<u64>().map_err(|_| {
117 anyhow::anyhow!("invalid cap {s:?}: expected calls:<N> or a non-negative integer")
118 })
119}
120
121pub fn handle_treasury(cmd: TreasuryCommand, repo_path: PathBuf) -> Result<()> {
123 use auths_sdk::domains::treasury;
124 match cmd.subcommand {
125 TreasurySubcommand::Open { manager, cap } => {
126 let parent_cap = parse_cap(&cap)?;
127 let v = treasury::open(&repo_path, &manager, parent_cap).map_err(anyhow::Error::new)?;
128 emit(
129 "treasury open",
130 serde_json::json!({ "status": v.status(), "manager": manager, "parent_cap": parent_cap }),
131 &format!("treasury cap established for {manager}: {parent_cap}"),
132 )
133 }
134 TreasurySubcommand::Allot {
135 manager,
136 to,
137 amount,
138 } => {
139 let v =
140 treasury::allot(&repo_path, &manager, &to, amount).map_err(anyhow::Error::new)?;
141 emit(
142 "treasury allot",
143 serde_json::json!({ "status": v.status(), "manager": manager, "to": to, "amount": amount }),
144 &format!("allot {amount} → {to}: {}", v.status()),
145 )
146 }
147 TreasurySubcommand::Reallocate {
148 manager,
149 from,
150 to,
151 amount,
152 } => {
153 let v = treasury::reallocate(&repo_path, &manager, &from, &to, amount)
154 .map_err(anyhow::Error::new)?;
155 emit(
156 "treasury reallocate",
157 serde_json::json!({ "status": v.status(), "manager": manager, "from": from, "to": to, "amount": amount }),
158 &format!("reallocate {amount} {from}→{to}: {}", v.status()),
159 )
160 }
161 TreasurySubcommand::Status { manager } => {
162 let st = treasury::status(&repo_path, &manager).map_err(anyhow::Error::new)?;
163 if is_json_mode() {
164 JsonResponse::success("treasury status", &st).print()?;
165 } else {
166 println!(
167 "treasury {manager}: cap {} · committed {} · free {} · {}",
168 st.parent_cap, st.committed, st.free_pool, st.status
169 );
170 for s in &st.slices {
171 println!(" slice {} = {}", s.agent_did, s.amount);
172 }
173 }
174 Ok(())
175 }
176 TreasurySubcommand::Credit {
177 to,
178 settlement,
179 claim_cents,
180 } => {
181 let v = treasury::credit(&repo_path, &to, &settlement, claim_cents)
182 .map_err(anyhow::Error::new)?;
183 emit(
184 "treasury credit",
185 serde_json::json!({ "status": v.status(), "to": to, "credited_cents": v.credited_cents(), "rail": "x402", "direction": "inbound" }),
186 &format!(
187 "credit {to}: {} ({}c inbound)",
188 v.status(),
189 v.credited_cents()
190 ),
191 )
192 }
193 TreasurySubcommand::Subdelegate {
194 manager,
195 parent,
196 child,
197 amount,
198 } => {
199 let v = treasury::subdelegate(&repo_path, &manager, &parent, &child, amount)
200 .map_err(anyhow::Error::new)?;
201 emit(
202 "treasury subdelegate",
203 serde_json::json!({ "status": v.status(), "manager": manager, "parent": parent, "child": child, "amount": amount }),
204 &format!("subdelegate {amount} {parent}→{child}: {}", v.status()),
205 )
206 }
207 TreasurySubcommand::Reclaim { manager, agent } => {
208 let v = treasury::reclaim(&repo_path, &manager, &agent).map_err(anyhow::Error::new)?;
209 emit(
210 "treasury reclaim",
211 serde_json::json!({ "status": v.status(), "manager": manager, "agent": agent }),
212 &format!("reclaim {agent}: {}", v.status()),
213 )
214 }
215 }
216}
217
218fn emit(command: &str, data: serde_json::Value, human: &str) -> Result<()> {
220 if is_json_mode() {
221 JsonResponse::success(command, data).print()?;
222 } else {
223 println!("✓ {human}");
224 }
225 Ok(())
226}