-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmint_pn_pool.rs
More file actions
635 lines (585 loc) · 22.2 KB
/
Copy pathmint_pn_pool.rs
File metadata and controls
635 lines (585 loc) · 22.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
//! Pre-deploy a pool of PrivateNotes at a fixed nominal and persist the
//! result as JSON. Designed for orderbook scenarios that need many large
//! funded PNs ready before the test/script starts (avoids paying the
//! halo2 cost in the hot path).
//!
//! Each PN goes through the full production-grade flow:
//!
//! 1. Halo2 NACKL voucher proof (deposit) via `mint_voucher_via_giver`.
//! 2. `RootPN.deployPrivateNote` — chain materializes the PN.
//! 3. Halo2 SHELL voucher proof (gas) via `mint_voucher_via_giver`.
//! 4. `RootPN.sendEccShellToPrivateNote` — funds the PN's gas.
//! 5. Giver-side native vmshell top-up so the PN can run internal ops.
//!
//! Sequential by design — halo2 voucher proofs commit to the chain's
//! `final_layer_historical_hash_root` at proof generation time and the
//! contract verifies it against current chain state at submit. Parallel
//! mints don't fit the validity window.
//!
//! Output: `pn_pool.json` (default; override with `--output`) appended
//! incrementally so Ctrl-C / errors mid-run don't lose what already
//! deployed. Re-running against an existing output file is **additive**:
//! the existing notes are loaded, validated for endpoint/nominal/token_type
//! compatibility, and `--count` more notes are appended on top. Use
//! `--output` to start a fresh pool.
//!
//! Run: `cargo run --bin mint_pn_pool --release -- --count 5 --nominal N10000`
//! (release recommended — halo2 prover is CPU-bound).
//!
//! Output schema (see `Pool` / `PoolNote` below) — includes owner
//! public/secret keypair so downstream code can sign on behalf of the PN
//! owner. **The JSON contains secrets**; treat the file as private.
use std::path::Path;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use ackinacki_kit::contracts::dex::private_note::PrivateNote;
use ackinacki_kit::contracts::dex::root_pn::ParamsOfDeployPrivateNote;
use ackinacki_kit::contracts::dex::root_pn::ParamsOfGetPrivateNoteAddress;
use ackinacki_kit::contracts::dex::root_pn::ParamsOfSendEccShellToPrivateNote;
use ackinacki_kit::contracts::dex::root_pn::RootPn;
use ackinacki_kit::contracts::giver::v3::send_currency_with_flag_from_default_giver;
use ackinacki_kit::contracts::giver::v3::top_up_native_with_giver_if_below;
use ackinacki_kit::contracts::traits::AccountAccessor;
use ackinacki_kit::contracts::traits::AddressAccessor;
use ackinacki_kit::tvm_client::abi::Signer;
use ackinacki_kit::tvm_client::crypto::generate_random_sign_keys;
use ackinacki_kit::tvm_client::crypto::KeyPair;
use ackinacki_kit::tvm_client::ClientConfig;
use ackinacki_kit::tvm_client::ClientContext;
use dodex_sdk::halo2::giver_voucher::mint_voucher_via_giver;
use dodex_sdk::halo2::Halo2Paths;
use dodex_sdk::proof;
use serde::Deserialize;
use serde::Serialize;
/// ECC currency id for SHELL (gas token — always SHELL regardless of
/// deposit currency).
const CURRENCY_ID_SHELL: u32 = 2;
/// SHELL gas voucher amount per PN — covers oracle fees + per-message gas
/// for typical orderbook ops.
const ECC_SHELL_DEPOSIT_RAW: u64 = 100_000_000_000;
/// Deposit currency for the PN pool. Mirrors `proof::TokenType` but kept
/// local so the CLI doesn't pull in the full enum surface.
#[derive(Debug, Clone, Copy)]
enum TokenTypeArg {
Nackl,
Shell,
Usdc,
}
impl TokenTypeArg {
fn parse(s: &str) -> Result<Self, String> {
match s.to_ascii_lowercase().as_str() {
"nackl" | "1" => Ok(Self::Nackl),
"shell" | "2" => Ok(Self::Shell),
"usdc" | "3" => Ok(Self::Usdc),
other => Err(format!("unknown token-type `{other}` (use nackl|shell|usdc)")),
}
}
fn id(self) -> u32 {
match self {
Self::Nackl => 1,
Self::Shell => 2,
Self::Usdc => 3,
}
}
/// One whole token in raw units. NACKL/SHELL = 1e9, USDC = 1e6.
fn decimals(self) -> u64 {
match self {
Self::Usdc => 1_000_000,
_ => 1_000_000_000,
}
}
fn label(self) -> &'static str {
match self {
Self::Nackl => "NACKL",
Self::Shell => "SHELL",
Self::Usdc => "USDC",
}
}
}
/// Native vmshell top-up per PN — fuels the PN's internal-message
/// execution (separate from SHELL ECC gas).
const NATIVE_GAS_TOPUP_RAW: u64 = 20_000_000_000;
/// RootPN minimum native balance + top-up amount before we start minting
/// vouchers in bulk. Mirrors `dodex_sdk/tests/integration/common/pn.rs`.
const ROOTPN_NATIVE_MIN: u64 = 120_000_000_000;
const ROOTPN_NATIVE_TOPUP: u64 = 50_000_000_000;
/// SHELL ECC budget kept on RootPN — each `sendEccShellToPrivateNote`
/// burns some, so we top up generously before the run.
const ROOTPN_SHELL_BUDGET: u64 = 1_000_000_000_000;
#[derive(Debug, Clone, Copy)]
enum NominalArg {
N100,
N1000,
N10000,
}
impl NominalArg {
fn parse(s: &str) -> Result<Self, String> {
match s.to_ascii_lowercase().as_str() {
"n100" | "100" => Ok(Self::N100),
"n1000" | "1000" => Ok(Self::N1000),
"n10000" | "10000" => Ok(Self::N10000),
other => Err(format!("unknown nominal `{other}` (use N100|N1000|N10000)")),
}
}
fn count(self) -> u64 {
match self {
Self::N100 => 100,
Self::N1000 => 1_000,
Self::N10000 => 10_000,
}
}
fn raw_value(self, token_type: TokenTypeArg) -> u64 {
self.count() * token_type.decimals()
}
fn label(self) -> &'static str {
match self {
Self::N100 => "N100",
Self::N1000 => "N1000",
Self::N10000 => "N10000",
}
}
}
#[derive(Debug)]
struct Args {
count: usize,
output: PathBuf,
endpoint: String,
nominal: NominalArg,
token_type: TokenTypeArg,
/// `https://{endpoint}` form for halo2 stage A (witness export).
network_url: String,
}
impl Args {
fn parse() -> Result<Self, String> {
let mut count: usize = 5;
let mut output = PathBuf::from("pn_pool.json");
let mut endpoint = "shellnet.ackinacki.org".to_string();
let mut nominal = NominalArg::N10000;
let mut token_type = TokenTypeArg::Nackl;
let mut argv = std::env::args().skip(1);
while let Some(arg) = argv.next() {
match arg.as_str() {
"--count" | "-n" => {
let v = argv.next().ok_or("--count requires a value")?;
count = v.parse().map_err(|e| format!("--count: {e}"))?;
if count == 0 {
return Err("--count must be ≥ 1".into());
}
}
"--output" | "-o" => {
output = PathBuf::from(argv.next().ok_or("--output requires a value")?);
}
"--endpoint" | "-e" => {
endpoint = argv.next().ok_or("--endpoint requires a value")?;
}
"--nominal" => {
let v = argv.next().ok_or("--nominal requires a value")?;
nominal = NominalArg::parse(&v)?;
}
"--token-type" | "-t" => {
let v = argv.next().ok_or("--token-type requires a value")?;
token_type = TokenTypeArg::parse(&v)?;
}
"--help" | "-h" => return Err(usage()),
other => return Err(format!("unknown arg `{other}`\n\n{}", usage())),
}
}
let network_url = if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
endpoint.clone()
} else {
format!("https://{endpoint}")
};
Ok(Args { count, output, endpoint, nominal, token_type, network_url })
}
}
fn usage() -> String {
"usage: mint_pn_pool [--count N] [--output path] [--endpoint host] \
[--nominal N100|N1000|N10000] [--token-type nackl|shell|usdc]\n\n \
--count number of PrivateNotes to deploy (default 5)\n \
--output JSON output path (default ./pn_pool.json)\n \
--endpoint network host (default shellnet.ackinacki.org)\n \
--nominal PN deposit nominal (default N10000)\n \
--token-type deposit currency (default nackl)"
.to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Pool {
endpoint: String,
created_at_unix: u64,
nominal: String,
token_type: u32,
raw_value_per_pn: u64,
ecc_shell_deposit_per_pn: u64,
notes: Vec<PoolNote>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PoolNote {
address: String,
/// `depositIdentifierHash` as decimal uint256 string (the format the
/// kit/contract APIs accept directly).
deposit_identifier_hash: String,
/// PN owner public key (hex, no `0x`).
owner_public_key_hex: String,
/// PN owner secret key (hex, no `0x`). **Secret** — keep the JSON file
/// out of public stores.
owner_secret_key_hex: String,
deployed_at_unix: u64,
/// `true` once `sendEccShellToPrivateNote` succeeded.
shell_funded: bool,
/// `true` once the giver native top-up succeeded.
native_funded: bool,
}
fn now_unix() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock before unix epoch")
.as_secs()
}
fn create_tvm_context(endpoint: &str) -> Arc<ClientContext> {
let mut config = ClientConfig::default();
config.network.endpoints = Some(vec![endpoint.to_string()]);
// Match production: disable tvm_client's internal re-connect loop;
// we don't have a retry policy layered around bin calls, but at
// least we avoid turning a flaky shellnet into a self-amplifying storm.
config.network.max_reconnect_timeout = 0;
Arc::new(ClientContext::new(config).expect("create tvm client"))
}
fn save_pool(pool: &Pool, path: &Path) {
let json = serde_json::to_string_pretty(pool).expect("serialize pool");
std::fs::write(path, json).expect("write pool json");
}
/// Load an existing pool from `path` if it exists, validating that its
/// endpoint / nominal / token_type / raw_value match what this run would
/// produce. Otherwise initialise a fresh empty pool.
///
/// Compatibility check exists because `--count` is additive: appending
/// notes from a different nominal or endpoint into the same file would
/// silently corrupt the pool's invariants (callers assume all notes share
/// the same shape).
fn load_or_init_pool(path: &Path, args: &Args) -> Result<Pool, String> {
let want_tt = args.token_type.id();
if !path.exists() {
return Ok(Pool {
endpoint: args.endpoint.clone(),
created_at_unix: now_unix(),
nominal: args.nominal.label().to_string(),
token_type: want_tt,
raw_value_per_pn: args.nominal.raw_value(args.token_type),
ecc_shell_deposit_per_pn: ECC_SHELL_DEPOSIT_RAW,
notes: Vec::with_capacity(args.count),
});
}
let bytes = std::fs::read(path).map_err(|e| format!("read {}: {e}", path.display()))?;
let existing: Pool = serde_json::from_slice(&bytes)
.map_err(|e| format!("parse {} as Pool: {e}", path.display()))?;
let want_nominal = args.nominal.label();
let want_raw = args.nominal.raw_value(args.token_type);
if existing.endpoint != args.endpoint {
return Err(format!(
"existing pool endpoint `{}` != requested `{}`; pass --output to a fresh path",
existing.endpoint, args.endpoint,
));
}
if existing.nominal != want_nominal {
return Err(format!(
"existing pool nominal `{}` != requested `{}`; pools are nominal-homogeneous, \
pass --output to a fresh path for a different nominal",
existing.nominal, want_nominal,
));
}
if existing.token_type != want_tt {
return Err(format!(
"existing pool token_type `{}` != requested `{want_tt}` ({}); \
pools are token-homogeneous, pass --output to a fresh path",
existing.token_type,
args.token_type.label(),
));
}
if existing.raw_value_per_pn != want_raw {
return Err(format!(
"existing pool raw_value_per_pn `{}` != requested `{}`",
existing.raw_value_per_pn, want_raw,
));
}
if existing.ecc_shell_deposit_per_pn != ECC_SHELL_DEPOSIT_RAW {
return Err(format!(
"existing pool ecc_shell_deposit_per_pn `{}` != expected `{ECC_SHELL_DEPOSIT_RAW}`",
existing.ecc_shell_deposit_per_pn,
));
}
Ok(existing)
}
async fn ensure_root_pn_funded(context: &Arc<ClientContext>) -> Result<(), String> {
let root_pn = RootPn::new_default(context.clone());
eprintln!("[pool] waiting for RootPN ({}) to be Active…", root_pn.address());
root_pn
.wait_account(ackinacki_kit::contracts::account::ParamsOfWaitAccount {
status: ackinacki_kit::contracts::account::AccountStatus::Active,
attempts: Some(60),
attempts_timeout: Some(2_000),
})
.await
.map_err(|e| format!("wait RootPN active: {e:?}"))?;
eprintln!("[pool] topping up RootPN native gas…");
top_up_native_with_giver_if_below(
context.clone(),
&root_pn,
ROOTPN_NATIVE_MIN,
ROOTPN_NATIVE_TOPUP,
"RootPN",
)
.await
.map_err(|e| format!("top_up_native_with_giver_if_below RootPN: {e:?}"))?;
eprintln!("[pool] sending SHELL ECC budget to RootPN…");
let mut ecc = std::collections::HashMap::new();
ecc.insert(CURRENCY_ID_SHELL, ROOTPN_SHELL_BUDGET);
send_currency_with_flag_from_default_giver(
context.clone(),
RootPn::DEFAULT_ADDRESS,
50_000_000_000,
ecc,
1,
)
.await
.map_err(|e| format!("giver SHELL → RootPN: {e:?}"))?;
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
Ok(())
}
async fn deploy_one_pn(
context: Arc<ClientContext>,
network_url: &str,
paths: &Halo2Paths,
token_type: TokenTypeArg,
nominal_raw: u64,
keys: KeyPair,
) -> Result<PoolNote, String> {
let root_pn = RootPn::new_default(context.clone());
// 1. Halo2 deposit voucher in the chosen currency.
eprintln!(
" halo2 {} deposit voucher (this is the slow step)…",
token_type.label()
);
let deposit_zk = mint_voucher_via_giver(
context.clone(),
network_url.to_string(),
&keys.public,
token_type.id(),
nominal_raw,
false,
paths,
)
.await
.map_err(|e| format!("mint_voucher_via_giver (deposit): {e:?}"))?;
let dih_dec = proof::hex_u256_to_dec(&deposit_zk.deposit_identifier_hash_hex);
let epk_dec = proof::pubkey_to_dec(&keys.public);
// 2. Deploy PN against the deposit proof.
eprintln!(" RootPN.deployPrivateNote…");
root_pn
.deploy_private_note(
ParamsOfDeployPrivateNote {
zkproof: deposit_zk.proof,
deposit_identifier_hash: dih_dec.clone(),
final_layer_historical_hash_root: proof::hex_u256_to_dec(
&deposit_zk.final_layer_historical_hash_root_hex,
),
voucher_nominal_fr: proof::hex_u256_to_dec(&deposit_zk.voucher_nominal_fr_hex),
token_type_fr: proof::hex_u256_to_dec(&deposit_zk.token_type_fr_hex),
ephemeral_pubkey: epk_dec,
value: deposit_zk.voucher_value,
token_type: deposit_zk.voucher_token_type,
layer_number: deposit_zk.layer_number,
},
Signer::Keys { keys: keys.clone() },
)
.await
.map_err(|e| format!("deploy_private_note: {e:?}"))?;
let pn_address = root_pn
.get_private_note_address(ParamsOfGetPrivateNoteAddress {
deposit_identifier_hash: dih_dec.clone(),
})
.await
.map_err(|e| format!("get_private_note_address: {e:?}"))?
.private_note_address;
let pn = PrivateNote::new(context.clone(), &pn_address);
eprintln!(" waiting for PN {pn_address} to be Active…");
pn.wait_account(ackinacki_kit::contracts::account::ParamsOfWaitAccount {
status: ackinacki_kit::contracts::account::AccountStatus::Active,
attempts: Some(60),
attempts_timeout: Some(2_000),
})
.await
.map_err(|e| format!("wait PN active: {e:?}"))?;
let mut note = PoolNote {
address: pn_address.clone(),
deposit_identifier_hash: dih_dec.clone(),
owner_public_key_hex: keys.public.clone(),
owner_secret_key_hex: keys.secret.clone(),
deployed_at_unix: now_unix(),
shell_funded: false,
native_funded: false,
};
// 3. Halo2 SHELL gas voucher (sequentially, NOT in parallel — see header
// comment).
eprintln!(" halo2 SHELL gas voucher…");
let gas_zk = mint_voucher_via_giver(
context.clone(),
network_url.to_string(),
&keys.public,
CURRENCY_ID_SHELL,
ECC_SHELL_DEPOSIT_RAW,
true,
paths,
)
.await
.map_err(|e| format!("mint_voucher_via_giver (gas): {e:?}"))?;
eprintln!(" RootPN.sendEccShellToPrivateNote…");
root_pn
.send_ecc_shell_to_private_note(
ParamsOfSendEccShellToPrivateNote {
proof: gas_zk.proof,
nullifier_hash: proof::hex_u256_to_dec(&gas_zk.deposit_identifier_hash_hex),
deposit_identifier_hash: dih_dec.clone(),
final_layer_historical_hash_root: proof::hex_u256_to_dec(
&gas_zk.final_layer_historical_hash_root_hex,
),
voucher_nominal_fr: proof::hex_u256_to_dec(&gas_zk.voucher_nominal_fr_hex),
token_type_fr: proof::hex_u256_to_dec(&gas_zk.token_type_fr_hex),
value: gas_zk.voucher_value,
layer_number: gas_zk.layer_number,
recipient_ephemeral_pubkey: proof::pubkey_to_dec(&keys.public),
},
Signer::Keys { keys: keys.clone() },
)
.await
.map_err(|e| format!("send_ecc_shell_to_private_note: {e:?}"))?;
note.shell_funded = true;
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
// 4. Native gas top-up so the PN can execute internal messages.
eprintln!(" giver native gas top-up…");
send_currency_with_flag_from_default_giver(
context.clone(),
&pn_address,
NATIVE_GAS_TOPUP_RAW,
std::collections::HashMap::new(),
1,
)
.await
.map_err(|e| format!("giver native top-up: {e:?}"))?;
note.native_funded = true;
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
Ok(note)
}
#[tokio::main]
async fn main() -> ExitCode {
let args = match Args::parse() {
Ok(a) => a,
Err(e) => {
eprintln!("{e}");
return ExitCode::from(2);
}
};
eprintln!(
"[pool] minting {} PN(s) at {} ({}, raw={}) on {}",
args.count,
args.nominal.label(),
args.token_type.label(),
args.nominal.raw_value(args.token_type),
args.endpoint,
);
eprintln!("[pool] writing to {}", args.output.display());
let context = create_tvm_context(&args.endpoint);
let paths = Halo2Paths::from_env();
if !paths.srs_exists() {
eprintln!(
"[pool] SRS {} not found — generating it (~64 MB, one-time, CPU-bound)...",
paths.srs_path().display()
);
paths.ensure_srs();
eprintln!("[pool] SRS ready at {}", paths.srs_path().display());
}
if let Err(e) = paths.validate() {
eprintln!("[pool] halo2 paths invalid: {e:?}");
return ExitCode::FAILURE;
}
if let Err(e) = ensure_root_pn_funded(&context).await {
eprintln!("[pool] failed to fund RootPN: {e}");
return ExitCode::FAILURE;
}
let mut pool = match load_or_init_pool(&args.output, &args) {
Ok(p) => p,
Err(e) => {
eprintln!("[pool] {e}");
return ExitCode::FAILURE;
}
};
let starting_count = pool.notes.len();
if starting_count > 0 {
eprintln!(
"[pool] loaded {} existing note(s) from {}; will append {} more (target {})",
starting_count,
args.output.display(),
args.count,
starting_count + args.count,
);
}
pool.notes.reserve(args.count);
save_pool(&pool, &args.output);
let started = std::time::Instant::now();
for i in 0..args.count {
let t = std::time::Instant::now();
eprintln!("\n[pool] === note {}/{} ===", i + 1, args.count);
let keys = match generate_random_sign_keys(context.clone()) {
Ok(k) => k,
Err(e) => {
eprintln!("[pool] generate_random_sign_keys: {e:?}");
return ExitCode::FAILURE;
}
};
match deploy_one_pn(
context.clone(),
&args.network_url,
&paths,
args.token_type,
args.nominal.raw_value(args.token_type),
keys,
)
.await
{
Ok(note) => {
eprintln!(
"[pool] note {}/{} ready at {} in {:.1}s (cumulative {:.1}s)",
i + 1,
args.count,
note.address,
t.elapsed().as_secs_f64(),
started.elapsed().as_secs_f64(),
);
pool.notes.push(note);
save_pool(&pool, &args.output);
}
Err(e) => {
eprintln!(
"[pool] note {}/{} FAILED after {:.1}s: {e}",
i + 1,
args.count,
t.elapsed().as_secs_f64(),
);
eprintln!(
"[pool] partial pool ({} notes) saved to {}",
pool.notes.len(),
args.output.display()
);
return ExitCode::FAILURE;
}
}
}
eprintln!(
"\n[pool] DONE — {} new note(s) deployed in {:.1}s; pool now has {} total, saved to {}",
pool.notes.len() - starting_count,
started.elapsed().as_secs_f64(),
pool.notes.len(),
args.output.display(),
);
ExitCode::SUCCESS
}