decdn_common/cli/pool.rs
1//! `decdn pool` — client-side payment-pool lifecycle commands.
2//!
3//! Sibling of [`super::fetch`]: where `decdn get` opens/reuses the caller's own
4//! `PaymentPool` deposit and pays for delivery from it, the `pool` subcommands
5//! manage that deposit directly. `list`/`status` is a read-only dump of the
6//! tracked buyer pools and their per-lane voucher watermark; `open` escrows a
7//! fresh deposit (`openPool`); `top-up` adds funds to a pool the caller owns;
8//! `close` starts the on-chain grace-window close (`closePool`); `reclaim`
9//! refunds the residual once that window has elapsed (`reclaim`, callable by
10//! anyone); `assign` issues an owner-signed spending capability delegating a
11//! bounded spend on the pool to a delegate signer key, printing the `dcap1:`
12//! token to hand off. Each of `top-up`/`close`/`reclaim`/`assign` names its
13//! target pool explicitly via `--pool` — there is no implicit per-provider
14//! lookup, because one pool fans out to every provider the owner pays
15//! (ADR 003). `close` and `reclaim` also accept `--all`, which enumerates every
16//! pool this keystore owns on-chain (`getPools`) and acts on each eligible one.
17//! `list --all` enumerates the same set read-only, which is the only way to see
18//! a pool the local store has lost.
19//!
20//! These commands own the **client's** buyer store, `buyer-pools.redb`. A
21//! `decdn-node` daemon keeps its own buyer pools in `buyer.redb` in its data
22//! dir; the two files share a table format and nothing else. A data dir holding
23//! any of the daemon's stores — not just `buyer.redb`, which is the file a
24//! reset loses — is a node's. Pointed at one, `list` reports the node's pools
25//! over the admin RPC while the daemon runs, because redb's lock leaves no disk
26//! path to that file; with the daemon stopped the lock is gone, so `list` reads
27//! the same `buyer.redb` directly and labels the listing as a disk read.
28//! The commands that would escrow into a store the node never reads —
29//! `open`, `top-up`, and the chain-enumerating `close --all` / `reclaim --all` —
30//! refuse. `close --pool` and `reclaim --pool` still run, and say plainly that
31//! the daemon's own record was left untouched.
32
33use std::path::PathBuf;
34
35use clap::{Args, Subcommand};
36
37/// `decdn pool <subcommand>`.
38#[derive(Args, Debug)]
39pub struct PoolArgs {
40 /// Pool lifecycle subcommand.
41 #[command(subcommand)]
42 pub command: PoolCommand,
43}
44
45/// Client-side payment-pool lifecycle operations.
46#[derive(Subcommand, Debug)]
47pub enum PoolCommand {
48 /// List the tracked buyer pools and their per-lane voucher watermark.
49 ///
50 /// Read-only. The store-backed listings name the store they read — on the
51 /// `store=` line, or the `store` field under `--json` — and `--all` names
52 /// the chain instead, carrying no `store` field at all.
53 ///
54 /// Four `source` values, and they do not emit the same `--json` pool
55 /// objects, so read `source` first:
56 ///
57 /// - `client_store` — the CLI's own `buyer-pools.redb`, read directly.
58 /// - `daemon` — a running `decdn-node`, over the admin RPC, because it
59 /// holds its `buyer.redb` exclusively while it runs.
60 /// - `node_store_offline` — a stopped node's `buyer.redb`, read off disk: a
61 /// post-mortem after a crash, or a host down for maintenance.
62 /// - `chain` — `--all`, which reads no local store to build its list.
63 ///
64 /// The two stores are separate: a client listing says nothing about a node,
65 /// and vice versa.
66 #[command(visible_alias = "status")]
67 List(PoolListArgs),
68 /// Open a fresh `PaymentPool` deposit, escrowing `--deposit-micro-usdc`
69 /// USDC (`openPool`). The pool fans out to every provider paid from it
70 /// (ADR 003) — there is no per-provider open.
71 Open(PoolOpenArgs),
72 /// Add funds to a pool the caller owns (`topUp`).
73 TopUp(PoolTopUpArgs),
74 /// Start the grace-window close on a pool the caller owns (`closePool`).
75 /// Redemptions stay valid until the dispute deadline; run `pool reclaim`
76 /// after it elapses to recover the residual deposit.
77 Close(PoolCloseArgs),
78 /// Refund the residual deposit of a pool once its grace window has
79 /// elapsed (`reclaim`; callable by anyone, but only the owner receives
80 /// funds).
81 Reclaim(PoolReclaimArgs),
82 /// Issue an owner-signed spending capability delegating a bounded spend on
83 /// a pool the caller owns to a delegate `--signer` key, and print the
84 /// `dcap1:` token to hand to that delegated client (ADR 003 §Capability
85 /// delegation). Offline-capable: only the owner keystore is required to
86 /// sign. An owner check that cannot be performed warns and issues the
87 /// token anyway. An owner check that reaches the chain and finds a
88 /// different owner, or no such pool, fails the command, because that
89 /// token would be rejected at redemption.
90 Assign(PoolAssignArgs),
91}
92
93/// `decdn pool assign` flags.
94#[derive(Args, Debug)]
95pub struct PoolAssignArgs {
96 /// The pool to delegate spend on (0x-prefixed 32-byte `poolId`). The caller
97 /// must own it — the capability is signed with the owner keystore.
98 #[arg(long, value_name = "0xHASH")]
99 pub pool: String,
100
101 /// The delegate's Ethereum address (0x-prefixed) — the voucher-signing key
102 /// authorized to spend against the pool under this capability.
103 #[arg(long, value_name = "0xADDR")]
104 pub signer: String,
105
106 /// The delegate's cumulative spend ceiling, in micro-USDC (USDC base units).
107 #[arg(long, value_name = "MICRO_USDC")]
108 pub cap_micro_usdc: u64,
109
110 /// Capability lifetime in seconds from now — the absolute Unix expiry is
111 /// `now + this`. `now` is the chain head's time, or the local clock when
112 /// the RPC cannot be read. Mutually exclusive with `--expiry-at`; exactly
113 /// one is required.
114 #[arg(long, value_name = "SECS", conflicts_with = "expiry_at")]
115 pub expiry_secs: Option<u64>,
116
117 /// Absolute Unix-seconds expiry. Mutually exclusive with `--expiry-secs`;
118 /// exactly one is required.
119 #[arg(long, value_name = "UNIX_TS", conflicts_with = "expiry_secs")]
120 pub expiry_at: Option<u64>,
121
122 /// Shared chain + store coordinates.
123 #[command(flatten)]
124 pub chain: PoolChainArgs,
125}
126
127/// `decdn pool open` flags.
128#[derive(Debug, clap::Args)]
129pub struct PoolOpenArgs {
130 /// Deposit to escrow, in micro-USDC (USDC base units).
131 #[arg(long, value_name = "MICRO_USDC")]
132 pub deposit_micro_usdc: u64,
133 /// Chain coordinates and the payment-pool address.
134 #[command(flatten)]
135 pub chain: PoolChainArgs,
136}
137
138/// `decdn pool top-up` flags.
139#[derive(Args, Debug)]
140pub struct PoolTopUpArgs {
141 /// The pool to fund (0x-prefixed 32-byte `poolId`, from `pool open`'s output
142 /// or `pool list`).
143 #[arg(long, value_name = "0xHASH")]
144 pub pool: String,
145
146 /// Amount to add, in micro-USDC (USDC base units).
147 #[arg(long, value_name = "MICRO_USDC")]
148 pub amount_micro_usdc: u64,
149
150 /// Shared chain + store coordinates.
151 #[command(flatten)]
152 pub chain: PoolChainArgs,
153}
154
155/// `decdn pool close` flags.
156///
157/// Targets exactly one of a single `--pool` or `--all`. `--all` enumerates every
158/// pool this keystore owns from chain (`getPools`) and closes the ones that are
159/// still `Open`.
160#[derive(Args, Debug)]
161#[command(group(clap::ArgGroup::new("close_target").required(true).args(["pool", "all"])))]
162pub struct PoolCloseArgs {
163 /// The pool to close (0x-prefixed 32-byte `poolId`). Mutually exclusive
164 /// with `--all`.
165 #[arg(long, value_name = "0xHASH")]
166 pub pool: Option<String>,
167
168 /// Close every pool this keystore owns that is currently `Open`, discovered
169 /// on-chain via `getPools`. Pools already closing or closed are skipped;
170 /// one pool's failure does not abort the rest. Mutually exclusive with
171 /// `--pool`.
172 #[arg(long)]
173 pub all: bool,
174
175 /// Shared chain + store coordinates.
176 #[command(flatten)]
177 pub chain: PoolChainArgs,
178}
179
180/// `decdn pool reclaim` flags.
181///
182/// Targets exactly one of a single `--pool` or `--all`. `--all` enumerates every
183/// pool this keystore owns from chain (`getPools`) and reclaims the ones whose
184/// dispute window has elapsed.
185#[derive(Args, Debug)]
186#[command(group(clap::ArgGroup::new("reclaim_target").required(true).args(["pool", "all"])))]
187pub struct PoolReclaimArgs {
188 /// The pool to reclaim (0x-prefixed 32-byte `poolId`). Its status and
189 /// dispute deadline are checked on-chain before the keystore is unlocked;
190 /// a pool that is not reclaimable is refused with its cause. Mutually
191 /// exclusive with `--all`.
192 #[arg(long, value_name = "0xHASH")]
193 pub pool: Option<String>,
194
195 /// Reclaim every pool this keystore owns whose grace window has elapsed,
196 /// discovered on-chain via `getPools`. Pools still `Open`, still inside
197 /// their dispute window, or already reclaimed are skipped; one pool's
198 /// failure does not abort the rest. Mutually exclusive with `--pool`.
199 #[arg(long)]
200 pub all: bool,
201
202 /// Shared chain + store coordinates.
203 #[command(flatten)]
204 pub chain: PoolChainArgs,
205}
206
207/// Shared chain + store coordinates for `decdn pool` subcommands.
208///
209/// The chain coordinates resolve flag > `[blockchain]` config; the keystore and
210/// data dir resolve flag > `[blockchain]`/`[identity]` config > default, exactly
211/// as `decdn get` does. Flattened into each subcommand's flags so every
212/// `pool` command shares one precedence path.
213#[derive(Args, Debug)]
214pub struct PoolChainArgs {
215 /// JSON-RPC endpoint of the chain (else `blockchain.rpc_url`).
216 #[arg(long, value_name = "URL")]
217 pub rpc_url: Option<String>,
218
219 /// `PaymentPool` contract address — the on-chain call target and the
220 /// EIP-712 `verifyingContract` (else `blockchain.payment_pool_address`).
221 #[arg(long, value_name = "ADDRESS")]
222 pub payment_pool_address: Option<String>,
223
224 /// EVM chain id for the EIP-712 domain (else `blockchain.chain_id` >
225 /// default).
226 #[arg(long, value_name = "ID")]
227 pub chain_id: Option<u64>,
228
229 /// Ethereum keystore path (else `blockchain.eth_keystore` > under the data
230 /// dir). Password from `$DECDN_KEYSTORE_PASSWORD`, else
231 /// `--keystore-password-file`, else a TTY prompt.
232 #[arg(long, value_name = "PATH")]
233 pub keystore: Option<PathBuf>,
234
235 /// File whose contents are the keystore password. Consulted after the
236 /// `DECDN_KEYSTORE_PASSWORD` env var and before an interactive prompt on a
237 /// TTY. A single trailing newline is stripped; a file that is empty after
238 /// that strip is a deliberate empty password, not an absent source. A path
239 /// that does not exist falls through; one that exists but cannot be read is
240 /// an error.
241 #[arg(long, value_name = "PATH", env = "DECDN_KEYSTORE_PASSWORD_FILE")]
242 pub keystore_password_file: Option<PathBuf>,
243
244 /// Data dir holding the buyer-pool store (else `identity.data_dir` >
245 /// default).
246 #[arg(long, value_name = "PATH")]
247 pub data_dir: Option<PathBuf>,
248}
249
250/// `decdn pool list` / `status` flags.
251///
252/// Read-only. The default listing needs no chain coordinates and no keystore:
253/// it reads the data dir's client buyer store — unless that data dir belongs to
254/// a `decdn-node` daemon, in which case it asks the daemon over the admin RPC
255/// while that daemon runs and reads its store off disk when it does not, and
256/// the admin flags below apply. `--all` is the other view, and it does need the
257/// chain flags and the keystore.
258#[derive(Args, Debug)]
259pub struct PoolListArgs {
260 /// List every pool this keystore owns ON CHAIN (`getPools`), in every
261 /// lifecycle state, marking which ones the local store tracks.
262 ///
263 /// The view that survives a reset `identity.data_dir`, because it reads no
264 /// local file to build its list. When the two views disagree, believe the
265 /// chain: the disagreement is itself the diagnostic.
266 ///
267 /// Sends no transaction and writes no pool record, so unlike `close --all`
268 /// / `reclaim --all` it is NOT refused on a node's data dir. Needs
269 /// `--rpc-url`, `--payment-pool-address`, and the keystore whose address it
270 /// enumerates by.
271 #[arg(long)]
272 pub all: bool,
273
274 /// Emit the pools as JSON instead of the aligned table.
275 #[arg(long)]
276 pub json: bool,
277
278 /// Base URL of the daemon's admin HTTP surface, used only when the data
279 /// dir turns out to be a node's. Also read from `DECDN_ADMIN_URL`; if
280 /// unset, the port is derived from `observability.admin_port` in the
281 /// config file, then the built-in default.
282 #[arg(long, value_name = "URL", env = "DECDN_ADMIN_URL")]
283 pub admin_url: Option<String>,
284
285 /// Roundtrip timeout in milliseconds for that admin call. Must be > 0 —
286 /// jsonrpsee reads a zero duration as "never" rather than "immediately".
287 /// Under `--all` a zero skips the daemon lookup and leaves `TRACKED`
288 /// unknown instead of failing.
289 #[arg(long, value_name = "MS", default_value_t = 5_000)]
290 pub timeout_ms: u64,
291
292 /// Chain + store coordinates. Only `--data-dir` matters to the default
293 /// listing; the rest are what `--all` enumerates with.
294 #[command(flatten)]
295 pub chain: PoolChainArgs,
296}