@wlfi-agent/cli 1.4.17 → 1.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +5 -0
- package/README.md +61 -28
- package/crates/vault-cli-admin/src/io_utils.rs +149 -1
- package/crates/vault-cli-admin/src/main.rs +639 -16
- package/crates/vault-cli-admin/src/shared_config.rs +18 -18
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
- package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
- package/crates/vault-cli-admin/src/tui.rs +1205 -120
- package/crates/vault-cli-agent/Cargo.toml +1 -0
- package/crates/vault-cli-agent/src/io_utils.rs +163 -2
- package/crates/vault-cli-agent/src/main.rs +648 -32
- package/crates/vault-cli-daemon/Cargo.toml +4 -0
- package/crates/vault-cli-daemon/src/main.rs +617 -67
- package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
- package/crates/vault-daemon/src/persistence.rs +637 -100
- package/crates/vault-daemon/src/tests.rs +1013 -3
- package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
- package/crates/vault-domain/src/nonce.rs +4 -0
- package/crates/vault-domain/src/tests.rs +616 -0
- package/crates/vault-policy/src/engine.rs +55 -32
- package/crates/vault-policy/src/tests.rs +195 -0
- package/crates/vault-sdk-agent/src/lib.rs +415 -22
- package/crates/vault-signer/Cargo.toml +3 -0
- package/crates/vault-signer/src/lib.rs +266 -40
- package/crates/vault-transport-unix/src/lib.rs +653 -5
- package/crates/vault-transport-xpc/src/tests.rs +531 -3
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
- package/dist/cli.cjs +756 -194
- package/dist/cli.cjs.map +1 -1
- package/package.json +5 -2
- package/packages/cache/.turbo/turbo-build.log +20 -20
- package/packages/cache/coverage/clover.xml +529 -394
- package/packages/cache/coverage/coverage-final.json +2 -2
- package/packages/cache/coverage/index.html +21 -21
- package/packages/cache/coverage/src/client/index.html +1 -1
- package/packages/cache/coverage/src/client/index.ts.html +1 -1
- package/packages/cache/coverage/src/errors/index.html +1 -1
- package/packages/cache/coverage/src/errors/index.ts.html +12 -12
- package/packages/cache/coverage/src/index.html +1 -1
- package/packages/cache/coverage/src/index.ts.html +1 -1
- package/packages/cache/coverage/src/service/index.html +21 -21
- package/packages/cache/coverage/src/service/index.ts.html +769 -313
- package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
- package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
- package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
- package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
- package/packages/cache/dist/index.cjs +2 -2
- package/packages/cache/dist/index.js +1 -1
- package/packages/cache/dist/service/index.cjs +2 -2
- package/packages/cache/dist/service/index.js +1 -1
- package/packages/cache/node_modules/.bin/tsc +2 -2
- package/packages/cache/node_modules/.bin/tsserver +2 -2
- package/packages/cache/node_modules/.bin/tsup +2 -2
- package/packages/cache/node_modules/.bin/tsup-node +2 -2
- package/packages/cache/node_modules/.bin/vitest +4 -4
- package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/cache/src/service/index.test.ts +165 -19
- package/packages/cache/src/service/index.ts +38 -1
- package/packages/config/.turbo/turbo-build.log +4 -4
- package/packages/config/dist/index.cjs +0 -17
- package/packages/config/dist/index.cjs.map +1 -1
- package/packages/config/src/index.ts +0 -17
- package/packages/rpc/.turbo/turbo-build.log +11 -11
- package/packages/rpc/dist/index.cjs +0 -17
- package/packages/rpc/dist/index.cjs.map +1 -1
- package/packages/rpc/src/index.js +1 -0
- package/packages/ui/node_modules/.bin/tsc +2 -2
- package/packages/ui/node_modules/.bin/tsserver +2 -2
- package/packages/ui/node_modules/.bin/tsup +2 -2
- package/packages/ui/node_modules/.bin/tsup-node +2 -2
- package/scripts/install-cli-launcher.mjs +37 -0
- package/scripts/install-rust-binaries.mjs +47 -0
- package/scripts/run-tests-isolated.mjs +210 -0
- package/src/cli.ts +310 -50
- package/src/lib/admin-reset.ts +101 -33
- package/src/lib/admin-setup.ts +285 -55
- package/src/lib/agent-auth-migrate.ts +5 -1
- package/src/lib/asset-broadcast.ts +15 -4
- package/src/lib/config-amounts.ts +6 -4
- package/src/lib/hidden-tty-prompt.js +1 -0
- package/src/lib/hidden-tty-prompt.ts +105 -0
- package/src/lib/keychain.ts +1 -0
- package/src/lib/local-admin-access.ts +4 -29
- package/src/lib/rust.ts +129 -33
- package/src/lib/signed-tx.ts +1 -0
- package/src/lib/sudo.ts +15 -5
- package/src/lib/wallet-profile.ts +3 -0
- package/src/lib/wallet-setup.ts +52 -0
- package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
- package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
|
@@ -14,6 +14,12 @@ mod io_utils;
|
|
|
14
14
|
|
|
15
15
|
use io_utils::*;
|
|
16
16
|
|
|
17
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
18
|
+
enum CommandRunOutcome {
|
|
19
|
+
Completed,
|
|
20
|
+
ManualApprovalRequired,
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
#[derive(Debug, Parser)]
|
|
18
24
|
#[command(name = "wlfi-agent-agent")]
|
|
19
25
|
#[command(about = "Agent CLI for sending signing requests through daemon policy checks")]
|
|
@@ -224,7 +230,35 @@ async fn main() -> Result<()> {
|
|
|
224
230
|
|
|
225
231
|
let sdk = AgentSdk::new_with_key_id_and_token(daemon, cli.agent_key_id, agent_auth_token);
|
|
226
232
|
|
|
227
|
-
match
|
|
233
|
+
match run_command(
|
|
234
|
+
cli.command,
|
|
235
|
+
cli.quiet,
|
|
236
|
+
&daemon_socket,
|
|
237
|
+
output_format,
|
|
238
|
+
&output_target,
|
|
239
|
+
&sdk,
|
|
240
|
+
)
|
|
241
|
+
.await?
|
|
242
|
+
{
|
|
243
|
+
CommandRunOutcome::Completed => {}
|
|
244
|
+
CommandRunOutcome::ManualApprovalRequired => std::process::exit(2),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
Ok(())
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async fn run_command<A>(
|
|
251
|
+
command: Commands,
|
|
252
|
+
quiet: bool,
|
|
253
|
+
daemon_socket: &Path,
|
|
254
|
+
output_format: OutputFormat,
|
|
255
|
+
output_target: &OutputTarget,
|
|
256
|
+
sdk: &A,
|
|
257
|
+
) -> Result<CommandRunOutcome>
|
|
258
|
+
where
|
|
259
|
+
A: AgentOperations + ?Sized,
|
|
260
|
+
{
|
|
261
|
+
match command {
|
|
228
262
|
Commands::Transfer {
|
|
229
263
|
network,
|
|
230
264
|
token,
|
|
@@ -233,18 +267,18 @@ async fn main() -> Result<()> {
|
|
|
233
267
|
} => {
|
|
234
268
|
let token_str = token.to_string();
|
|
235
269
|
let to_str = to.to_string();
|
|
236
|
-
print_status("submitting transfer request", output_format,
|
|
270
|
+
print_status("submitting transfer request", output_format, quiet);
|
|
237
271
|
let signature = match await_signature_or_handle_manual_approval(
|
|
238
272
|
"transfer",
|
|
239
|
-
|
|
273
|
+
daemon_socket,
|
|
240
274
|
output_format,
|
|
241
|
-
|
|
275
|
+
output_target,
|
|
242
276
|
sdk.transfer(network, token, to, amount_wei),
|
|
243
277
|
)
|
|
244
278
|
.await?
|
|
245
279
|
{
|
|
246
280
|
Some(signature) => signature,
|
|
247
|
-
None =>
|
|
281
|
+
None => return Ok(CommandRunOutcome::ManualApprovalRequired),
|
|
248
282
|
};
|
|
249
283
|
let output = AgentCommandOutput {
|
|
250
284
|
command: "transfer".to_string(),
|
|
@@ -262,8 +296,8 @@ async fn main() -> Result<()> {
|
|
|
262
296
|
raw_tx_hex: None,
|
|
263
297
|
tx_hash_hex: None,
|
|
264
298
|
};
|
|
265
|
-
print_status("transfer request signed", output_format,
|
|
266
|
-
print_agent_output(&output, output_format,
|
|
299
|
+
print_status("transfer request signed", output_format, quiet);
|
|
300
|
+
print_agent_output(&output, output_format, output_target)?;
|
|
267
301
|
}
|
|
268
302
|
Commands::TransferNative {
|
|
269
303
|
network,
|
|
@@ -274,19 +308,19 @@ async fn main() -> Result<()> {
|
|
|
274
308
|
print_status(
|
|
275
309
|
"submitting native transfer request",
|
|
276
310
|
output_format,
|
|
277
|
-
|
|
311
|
+
quiet,
|
|
278
312
|
);
|
|
279
313
|
let signature = match await_signature_or_handle_manual_approval(
|
|
280
314
|
"transfer-native",
|
|
281
|
-
|
|
315
|
+
daemon_socket,
|
|
282
316
|
output_format,
|
|
283
|
-
|
|
317
|
+
output_target,
|
|
284
318
|
sdk.transfer_native(network, to, amount_wei),
|
|
285
319
|
)
|
|
286
320
|
.await?
|
|
287
321
|
{
|
|
288
322
|
Some(signature) => signature,
|
|
289
|
-
None =>
|
|
323
|
+
None => return Ok(CommandRunOutcome::ManualApprovalRequired),
|
|
290
324
|
};
|
|
291
325
|
let output = AgentCommandOutput {
|
|
292
326
|
command: "transfer-native".to_string(),
|
|
@@ -304,8 +338,8 @@ async fn main() -> Result<()> {
|
|
|
304
338
|
raw_tx_hex: None,
|
|
305
339
|
tx_hash_hex: None,
|
|
306
340
|
};
|
|
307
|
-
print_status("native transfer request signed", output_format,
|
|
308
|
-
print_agent_output(&output, output_format,
|
|
341
|
+
print_status("native transfer request signed", output_format, quiet);
|
|
342
|
+
print_agent_output(&output, output_format, output_target)?;
|
|
309
343
|
}
|
|
310
344
|
Commands::Approve {
|
|
311
345
|
network,
|
|
@@ -315,18 +349,18 @@ async fn main() -> Result<()> {
|
|
|
315
349
|
} => {
|
|
316
350
|
let token_str = token.to_string();
|
|
317
351
|
let spender_str = spender.to_string();
|
|
318
|
-
print_status("submitting approve request", output_format,
|
|
352
|
+
print_status("submitting approve request", output_format, quiet);
|
|
319
353
|
let signature = match await_signature_or_handle_manual_approval(
|
|
320
354
|
"approve",
|
|
321
|
-
|
|
355
|
+
daemon_socket,
|
|
322
356
|
output_format,
|
|
323
|
-
|
|
357
|
+
output_target,
|
|
324
358
|
sdk.approve(network, token, spender, amount_wei),
|
|
325
359
|
)
|
|
326
360
|
.await?
|
|
327
361
|
{
|
|
328
362
|
Some(signature) => signature,
|
|
329
|
-
None =>
|
|
363
|
+
None => return Ok(CommandRunOutcome::ManualApprovalRequired),
|
|
330
364
|
};
|
|
331
365
|
let output = AgentCommandOutput {
|
|
332
366
|
command: "approve".to_string(),
|
|
@@ -344,8 +378,8 @@ async fn main() -> Result<()> {
|
|
|
344
378
|
raw_tx_hex: None,
|
|
345
379
|
tx_hash_hex: None,
|
|
346
380
|
};
|
|
347
|
-
print_status("approve request signed", output_format,
|
|
348
|
-
print_agent_output(&output, output_format,
|
|
381
|
+
print_status("approve request signed", output_format, quiet);
|
|
382
|
+
print_agent_output(&output, output_format, output_target)?;
|
|
349
383
|
}
|
|
350
384
|
Commands::Broadcast {
|
|
351
385
|
network,
|
|
@@ -379,18 +413,18 @@ async fn main() -> Result<()> {
|
|
|
379
413
|
let asset = action.asset();
|
|
380
414
|
let counterparty = action.recipient();
|
|
381
415
|
|
|
382
|
-
print_status("submitting broadcast request", output_format,
|
|
416
|
+
print_status("submitting broadcast request", output_format, quiet);
|
|
383
417
|
let signature = match await_signature_or_handle_manual_approval(
|
|
384
418
|
"broadcast",
|
|
385
|
-
|
|
419
|
+
daemon_socket,
|
|
386
420
|
output_format,
|
|
387
|
-
|
|
421
|
+
output_target,
|
|
388
422
|
sdk.broadcast_tx(tx),
|
|
389
423
|
)
|
|
390
424
|
.await?
|
|
391
425
|
{
|
|
392
426
|
Some(signature) => signature,
|
|
393
|
-
None =>
|
|
427
|
+
None => return Ok(CommandRunOutcome::ManualApprovalRequired),
|
|
394
428
|
};
|
|
395
429
|
let output = AgentCommandOutput {
|
|
396
430
|
command: "broadcast".to_string(),
|
|
@@ -408,12 +442,12 @@ async fn main() -> Result<()> {
|
|
|
408
442
|
raw_tx_hex: signature.raw_tx_hex,
|
|
409
443
|
tx_hash_hex: signature.tx_hash_hex,
|
|
410
444
|
};
|
|
411
|
-
print_status("broadcast request signed", output_format,
|
|
412
|
-
print_agent_output(&output, output_format,
|
|
445
|
+
print_status("broadcast request signed", output_format, quiet);
|
|
446
|
+
print_agent_output(&output, output_format, output_target)?;
|
|
413
447
|
}
|
|
414
448
|
}
|
|
415
449
|
|
|
416
|
-
Ok(
|
|
450
|
+
Ok(CommandRunOutcome::Completed)
|
|
417
451
|
}
|
|
418
452
|
|
|
419
453
|
async fn await_signature_or_handle_manual_approval(
|
|
@@ -436,9 +470,9 @@ async fn await_signature_or_handle_manual_approval(
|
|
|
436
470
|
relay_url,
|
|
437
471
|
frontend_url,
|
|
438
472
|
cli_approval_command: format!(
|
|
439
|
-
"wlfi-agent admin approve-manual-approval-request --approval-request-id {}
|
|
440
|
-
|
|
441
|
-
|
|
473
|
+
"wlfi-agent admin --daemon-socket {} approve-manual-approval-request --approval-request-id {}",
|
|
474
|
+
daemon_socket.display(),
|
|
475
|
+
approval_request_id
|
|
442
476
|
),
|
|
443
477
|
};
|
|
444
478
|
print_manual_approval_required_output(&output, format, target)?;
|
|
@@ -555,16 +589,223 @@ fn parse_non_negative_u64(input: &str) -> Result<u64, String> {
|
|
|
555
589
|
#[cfg(test)]
|
|
556
590
|
mod tests {
|
|
557
591
|
use super::{
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
592
|
+
await_signature_or_handle_manual_approval, ensure_output_parent,
|
|
593
|
+
print_manual_approval_required_output, resolve_agent_auth_token, resolve_output_format,
|
|
594
|
+
resolve_output_target, run_command, should_print_status, write_output_file, Cli,
|
|
595
|
+
CommandRunOutcome, Commands, ManualApprovalRequiredOutput, OutputFormat, OutputTarget,
|
|
561
596
|
};
|
|
597
|
+
use async_trait::async_trait;
|
|
562
598
|
use clap::Parser;
|
|
599
|
+
use std::path::{Path, PathBuf};
|
|
600
|
+
use std::sync::Mutex;
|
|
563
601
|
use std::fs;
|
|
564
602
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
603
|
+
use tokio::runtime::Builder;
|
|
604
|
+
use uuid::Uuid;
|
|
605
|
+
use vault_daemon::DaemonError;
|
|
606
|
+
use vault_domain::{BroadcastTx, EvmAddress, Signature};
|
|
607
|
+
use vault_sdk_agent::{AgentOperations, AgentSdkError};
|
|
565
608
|
|
|
566
609
|
const TEST_AGENT_KEY_ID: &str = "11111111-1111-1111-1111-111111111111";
|
|
567
610
|
|
|
611
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
612
|
+
enum FakeCall {
|
|
613
|
+
Transfer {
|
|
614
|
+
chain_id: u64,
|
|
615
|
+
token: EvmAddress,
|
|
616
|
+
to: EvmAddress,
|
|
617
|
+
amount_wei: u128,
|
|
618
|
+
},
|
|
619
|
+
TransferNative {
|
|
620
|
+
chain_id: u64,
|
|
621
|
+
to: EvmAddress,
|
|
622
|
+
amount_wei: u128,
|
|
623
|
+
},
|
|
624
|
+
Approve {
|
|
625
|
+
chain_id: u64,
|
|
626
|
+
token: EvmAddress,
|
|
627
|
+
spender: EvmAddress,
|
|
628
|
+
amount_wei: u128,
|
|
629
|
+
},
|
|
630
|
+
Broadcast(BroadcastTx),
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
#[derive(Clone)]
|
|
634
|
+
enum FakeOutcome {
|
|
635
|
+
Signature(Signature),
|
|
636
|
+
ManualApprovalRequired {
|
|
637
|
+
approval_request_id: Uuid,
|
|
638
|
+
relay_url: Option<String>,
|
|
639
|
+
frontend_url: Option<String>,
|
|
640
|
+
},
|
|
641
|
+
Serialization(String),
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
struct FakeAgentOps {
|
|
645
|
+
calls: Mutex<Vec<FakeCall>>,
|
|
646
|
+
outcome: FakeOutcome,
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
impl FakeAgentOps {
|
|
650
|
+
fn result(&self) -> Result<Signature, AgentSdkError> {
|
|
651
|
+
match &self.outcome {
|
|
652
|
+
FakeOutcome::Signature(signature) => Ok(signature.clone()),
|
|
653
|
+
FakeOutcome::ManualApprovalRequired {
|
|
654
|
+
approval_request_id,
|
|
655
|
+
relay_url,
|
|
656
|
+
frontend_url,
|
|
657
|
+
} => Err(AgentSdkError::Daemon(DaemonError::ManualApprovalRequired {
|
|
658
|
+
approval_request_id: *approval_request_id,
|
|
659
|
+
relay_url: relay_url.clone(),
|
|
660
|
+
frontend_url: frontend_url.clone(),
|
|
661
|
+
})),
|
|
662
|
+
FakeOutcome::Serialization(err) => {
|
|
663
|
+
Err(AgentSdkError::Serialization(err.clone()))
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#[async_trait]
|
|
670
|
+
impl AgentOperations for FakeAgentOps {
|
|
671
|
+
async fn approve(
|
|
672
|
+
&self,
|
|
673
|
+
chain_id: u64,
|
|
674
|
+
token: EvmAddress,
|
|
675
|
+
spender: EvmAddress,
|
|
676
|
+
amount_wei: u128,
|
|
677
|
+
) -> Result<Signature, AgentSdkError> {
|
|
678
|
+
self.calls.lock().expect("lock").push(FakeCall::Approve {
|
|
679
|
+
chain_id,
|
|
680
|
+
token,
|
|
681
|
+
spender,
|
|
682
|
+
amount_wei,
|
|
683
|
+
});
|
|
684
|
+
self.result()
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async fn transfer(
|
|
688
|
+
&self,
|
|
689
|
+
chain_id: u64,
|
|
690
|
+
token: EvmAddress,
|
|
691
|
+
to: EvmAddress,
|
|
692
|
+
amount_wei: u128,
|
|
693
|
+
) -> Result<Signature, AgentSdkError> {
|
|
694
|
+
self.calls.lock().expect("lock").push(FakeCall::Transfer {
|
|
695
|
+
chain_id,
|
|
696
|
+
token,
|
|
697
|
+
to,
|
|
698
|
+
amount_wei,
|
|
699
|
+
});
|
|
700
|
+
self.result()
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async fn transfer_native(
|
|
704
|
+
&self,
|
|
705
|
+
chain_id: u64,
|
|
706
|
+
to: EvmAddress,
|
|
707
|
+
amount_wei: u128,
|
|
708
|
+
) -> Result<Signature, AgentSdkError> {
|
|
709
|
+
self.calls
|
|
710
|
+
.lock()
|
|
711
|
+
.expect("lock")
|
|
712
|
+
.push(FakeCall::TransferNative {
|
|
713
|
+
chain_id,
|
|
714
|
+
to,
|
|
715
|
+
amount_wei,
|
|
716
|
+
});
|
|
717
|
+
self.result()
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async fn permit2_permit(
|
|
721
|
+
&self,
|
|
722
|
+
_permit: vault_domain::Permit2Permit,
|
|
723
|
+
) -> Result<Signature, AgentSdkError> {
|
|
724
|
+
panic!("unused in test");
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async fn eip3009_transfer_with_authorization(
|
|
728
|
+
&self,
|
|
729
|
+
_authorization: vault_domain::Eip3009Transfer,
|
|
730
|
+
) -> Result<Signature, AgentSdkError> {
|
|
731
|
+
panic!("unused in test");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async fn eip3009_receive_with_authorization(
|
|
735
|
+
&self,
|
|
736
|
+
_authorization: vault_domain::Eip3009Transfer,
|
|
737
|
+
) -> Result<Signature, AgentSdkError> {
|
|
738
|
+
panic!("unused in test");
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async fn sign_erc20_calldata(
|
|
742
|
+
&self,
|
|
743
|
+
_chain_id: u64,
|
|
744
|
+
_token: EvmAddress,
|
|
745
|
+
_calldata: Vec<u8>,
|
|
746
|
+
) -> Result<Signature, AgentSdkError> {
|
|
747
|
+
panic!("unused in test");
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async fn broadcast_tx(&self, tx: BroadcastTx) -> Result<Signature, AgentSdkError> {
|
|
751
|
+
self.calls.lock().expect("lock").push(FakeCall::Broadcast(tx));
|
|
752
|
+
self.result()
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async fn reserve_broadcast_nonce(
|
|
756
|
+
&self,
|
|
757
|
+
_chain_id: u64,
|
|
758
|
+
_min_nonce: u64,
|
|
759
|
+
) -> Result<vault_domain::NonceReservation, AgentSdkError> {
|
|
760
|
+
panic!("unused in test");
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async fn release_broadcast_nonce(
|
|
764
|
+
&self,
|
|
765
|
+
_reservation_id: Uuid,
|
|
766
|
+
) -> Result<(), AgentSdkError> {
|
|
767
|
+
panic!("unused in test");
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
fn test_runtime() -> tokio::runtime::Runtime {
|
|
772
|
+
Builder::new_current_thread()
|
|
773
|
+
.enable_all()
|
|
774
|
+
.build()
|
|
775
|
+
.expect("runtime")
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
fn sample_signature() -> Signature {
|
|
779
|
+
Signature {
|
|
780
|
+
bytes: vec![0xaa, 0xbb, 0xcc],
|
|
781
|
+
r_hex: Some("0x01".to_string()),
|
|
782
|
+
s_hex: Some("0x02".to_string()),
|
|
783
|
+
v: Some(1),
|
|
784
|
+
raw_tx_hex: Some("0x1234".to_string()),
|
|
785
|
+
tx_hash_hex: Some("0xabcd".to_string()),
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
fn temp_path(prefix: &str, ext: &str) -> PathBuf {
|
|
790
|
+
std::env::temp_dir().join(format!(
|
|
791
|
+
"{prefix}-{}-{}.{}",
|
|
792
|
+
std::process::id(),
|
|
793
|
+
SystemTime::now()
|
|
794
|
+
.duration_since(UNIX_EPOCH)
|
|
795
|
+
.expect("time")
|
|
796
|
+
.as_nanos(),
|
|
797
|
+
ext
|
|
798
|
+
))
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
fn socket_path() -> PathBuf {
|
|
802
|
+
temp_path("wlfi-agent-cli-socket", "sock")
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
fn read_file(path: &Path) -> String {
|
|
806
|
+
fs::read_to_string(path).expect("read output")
|
|
807
|
+
}
|
|
808
|
+
|
|
568
809
|
#[test]
|
|
569
810
|
fn resolve_output_format_defaults_to_text() {
|
|
570
811
|
let format = resolve_output_format(None, false).expect("format");
|
|
@@ -830,4 +1071,379 @@ mod tests {
|
|
|
830
1071
|
assert!(rendered.contains("--agent-auth-token"));
|
|
831
1072
|
assert!(rendered.contains("--agent-auth-token-stdin"));
|
|
832
1073
|
}
|
|
1074
|
+
|
|
1075
|
+
#[test]
|
|
1076
|
+
fn manual_approval_output_renders_text_and_json() {
|
|
1077
|
+
let text_path = temp_path("manual-approval-output", "txt");
|
|
1078
|
+
let json_path = temp_path("manual-approval-output", "json");
|
|
1079
|
+
let output = ManualApprovalRequiredOutput {
|
|
1080
|
+
command: "transfer".to_string(),
|
|
1081
|
+
approval_request_id: Uuid::nil().to_string(),
|
|
1082
|
+
relay_url: Some("https://relay.example".to_string()),
|
|
1083
|
+
frontend_url: Some("https://frontend.example/approvals/1".to_string()),
|
|
1084
|
+
cli_approval_command: "wlfi-agent admin approve".to_string(),
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
print_manual_approval_required_output(
|
|
1088
|
+
&output,
|
|
1089
|
+
OutputFormat::Text,
|
|
1090
|
+
&OutputTarget::File {
|
|
1091
|
+
path: text_path.clone(),
|
|
1092
|
+
overwrite: false,
|
|
1093
|
+
},
|
|
1094
|
+
)
|
|
1095
|
+
.expect("text output");
|
|
1096
|
+
let text = read_file(&text_path);
|
|
1097
|
+
assert!(text.contains("Command: transfer"));
|
|
1098
|
+
assert!(text.contains("Approval Request ID: 00000000-0000-0000-0000-000000000000"));
|
|
1099
|
+
assert!(text.contains("Frontend Approval URL: https://frontend.example/approvals/1"));
|
|
1100
|
+
assert!(text.contains("Relay URL: https://relay.example"));
|
|
1101
|
+
|
|
1102
|
+
print_manual_approval_required_output(
|
|
1103
|
+
&output,
|
|
1104
|
+
OutputFormat::Json,
|
|
1105
|
+
&OutputTarget::File {
|
|
1106
|
+
path: json_path.clone(),
|
|
1107
|
+
overwrite: false,
|
|
1108
|
+
},
|
|
1109
|
+
)
|
|
1110
|
+
.expect("json output");
|
|
1111
|
+
let json = read_file(&json_path);
|
|
1112
|
+
assert!(json.contains("\"command\": \"transfer\""));
|
|
1113
|
+
assert!(json.contains("\"relay_url\": \"https://relay.example\""));
|
|
1114
|
+
|
|
1115
|
+
fs::remove_file(&text_path).expect("cleanup text");
|
|
1116
|
+
fs::remove_file(&json_path).expect("cleanup json");
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
#[test]
|
|
1120
|
+
fn await_signature_handles_success_manual_approval_and_error_paths() {
|
|
1121
|
+
let runtime = test_runtime();
|
|
1122
|
+
let output_path = temp_path("manual-approval-await", "json");
|
|
1123
|
+
let output_target = OutputTarget::File {
|
|
1124
|
+
path: output_path.clone(),
|
|
1125
|
+
overwrite: false,
|
|
1126
|
+
};
|
|
1127
|
+
let daemon_socket = socket_path();
|
|
1128
|
+
|
|
1129
|
+
let signature = runtime
|
|
1130
|
+
.block_on(await_signature_or_handle_manual_approval(
|
|
1131
|
+
"transfer",
|
|
1132
|
+
&daemon_socket,
|
|
1133
|
+
OutputFormat::Json,
|
|
1134
|
+
&output_target,
|
|
1135
|
+
std::future::ready(Ok(sample_signature())),
|
|
1136
|
+
))
|
|
1137
|
+
.expect("success path");
|
|
1138
|
+
assert_eq!(signature, Some(sample_signature()));
|
|
1139
|
+
assert!(!output_path.exists());
|
|
1140
|
+
|
|
1141
|
+
let manual = runtime
|
|
1142
|
+
.block_on(await_signature_or_handle_manual_approval(
|
|
1143
|
+
"approve",
|
|
1144
|
+
&daemon_socket,
|
|
1145
|
+
OutputFormat::Json,
|
|
1146
|
+
&output_target,
|
|
1147
|
+
std::future::ready(Err(AgentSdkError::Daemon(
|
|
1148
|
+
DaemonError::ManualApprovalRequired {
|
|
1149
|
+
approval_request_id: Uuid::nil(),
|
|
1150
|
+
relay_url: Some("https://relay.example".to_string()),
|
|
1151
|
+
frontend_url: None,
|
|
1152
|
+
},
|
|
1153
|
+
))),
|
|
1154
|
+
))
|
|
1155
|
+
.expect("manual approval path");
|
|
1156
|
+
assert_eq!(manual, None);
|
|
1157
|
+
let rendered = read_file(&output_path);
|
|
1158
|
+
assert!(rendered.contains("\"approval_request_id\": \"00000000-0000-0000-0000-000000000000\""));
|
|
1159
|
+
assert!(rendered.contains(&format!(
|
|
1160
|
+
"wlfi-agent admin --daemon-socket {} approve-manual-approval-request --approval-request-id 00000000-0000-0000-0000-000000000000",
|
|
1161
|
+
daemon_socket.display()
|
|
1162
|
+
)));
|
|
1163
|
+
|
|
1164
|
+
let err = runtime
|
|
1165
|
+
.block_on(await_signature_or_handle_manual_approval(
|
|
1166
|
+
"approve",
|
|
1167
|
+
&daemon_socket,
|
|
1168
|
+
OutputFormat::Json,
|
|
1169
|
+
&output_target,
|
|
1170
|
+
std::future::ready(Err(AgentSdkError::Daemon(DaemonError::Transport(
|
|
1171
|
+
"boom".to_string(),
|
|
1172
|
+
)))),
|
|
1173
|
+
))
|
|
1174
|
+
.expect_err("transport error must bubble");
|
|
1175
|
+
assert!(err.to_string().contains("daemon call failed"));
|
|
1176
|
+
|
|
1177
|
+
fs::remove_file(&output_path).expect("cleanup");
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
#[test]
|
|
1181
|
+
fn run_command_covers_success_and_manual_approval_flows() {
|
|
1182
|
+
let runtime = test_runtime();
|
|
1183
|
+
let daemon_socket = socket_path();
|
|
1184
|
+
let token: EvmAddress = "0x1000000000000000000000000000000000000000"
|
|
1185
|
+
.parse()
|
|
1186
|
+
.expect("token");
|
|
1187
|
+
let to: EvmAddress = "0x2000000000000000000000000000000000000000"
|
|
1188
|
+
.parse()
|
|
1189
|
+
.expect("recipient");
|
|
1190
|
+
let spender: EvmAddress = "0x3000000000000000000000000000000000000000"
|
|
1191
|
+
.parse()
|
|
1192
|
+
.expect("spender");
|
|
1193
|
+
|
|
1194
|
+
let transfer_output = temp_path("transfer-output", "json");
|
|
1195
|
+
let transfer_ops = FakeAgentOps {
|
|
1196
|
+
calls: Mutex::new(Vec::new()),
|
|
1197
|
+
outcome: FakeOutcome::Signature(sample_signature()),
|
|
1198
|
+
};
|
|
1199
|
+
let outcome = runtime
|
|
1200
|
+
.block_on(run_command(
|
|
1201
|
+
Commands::Transfer {
|
|
1202
|
+
network: 1,
|
|
1203
|
+
token: token.clone(),
|
|
1204
|
+
to: to.clone(),
|
|
1205
|
+
amount_wei: 7,
|
|
1206
|
+
},
|
|
1207
|
+
true,
|
|
1208
|
+
&daemon_socket,
|
|
1209
|
+
OutputFormat::Json,
|
|
1210
|
+
&OutputTarget::File {
|
|
1211
|
+
path: transfer_output.clone(),
|
|
1212
|
+
overwrite: false,
|
|
1213
|
+
},
|
|
1214
|
+
&transfer_ops,
|
|
1215
|
+
))
|
|
1216
|
+
.expect("transfer run");
|
|
1217
|
+
assert_eq!(outcome, CommandRunOutcome::Completed);
|
|
1218
|
+
assert_eq!(
|
|
1219
|
+
transfer_ops.calls.lock().expect("lock").as_slice(),
|
|
1220
|
+
&[FakeCall::Transfer {
|
|
1221
|
+
chain_id: 1,
|
|
1222
|
+
token: token.clone(),
|
|
1223
|
+
to: to.clone(),
|
|
1224
|
+
amount_wei: 7,
|
|
1225
|
+
}]
|
|
1226
|
+
);
|
|
1227
|
+
let transfer_json = read_file(&transfer_output);
|
|
1228
|
+
assert!(transfer_json.contains("\"command\": \"transfer\""));
|
|
1229
|
+
assert!(transfer_json.contains("\"asset\": \"erc20:0x1000000000000000000000000000000000000000\""));
|
|
1230
|
+
fs::remove_file(&transfer_output).expect("cleanup transfer");
|
|
1231
|
+
|
|
1232
|
+
let native_output = temp_path("native-output", "json");
|
|
1233
|
+
let native_ops = FakeAgentOps {
|
|
1234
|
+
calls: Mutex::new(Vec::new()),
|
|
1235
|
+
outcome: FakeOutcome::Signature(sample_signature()),
|
|
1236
|
+
};
|
|
1237
|
+
let outcome = runtime
|
|
1238
|
+
.block_on(run_command(
|
|
1239
|
+
Commands::TransferNative {
|
|
1240
|
+
network: 10,
|
|
1241
|
+
to: to.clone(),
|
|
1242
|
+
amount_wei: 9,
|
|
1243
|
+
},
|
|
1244
|
+
true,
|
|
1245
|
+
&daemon_socket,
|
|
1246
|
+
OutputFormat::Json,
|
|
1247
|
+
&OutputTarget::File {
|
|
1248
|
+
path: native_output.clone(),
|
|
1249
|
+
overwrite: false,
|
|
1250
|
+
},
|
|
1251
|
+
&native_ops,
|
|
1252
|
+
))
|
|
1253
|
+
.expect("native run");
|
|
1254
|
+
assert_eq!(outcome, CommandRunOutcome::Completed);
|
|
1255
|
+
assert_eq!(
|
|
1256
|
+
native_ops.calls.lock().expect("lock").as_slice(),
|
|
1257
|
+
&[FakeCall::TransferNative {
|
|
1258
|
+
chain_id: 10,
|
|
1259
|
+
to: to.clone(),
|
|
1260
|
+
amount_wei: 9,
|
|
1261
|
+
}]
|
|
1262
|
+
);
|
|
1263
|
+
let native_json = read_file(&native_output);
|
|
1264
|
+
assert!(native_json.contains("\"command\": \"transfer-native\""));
|
|
1265
|
+
assert!(native_json.contains("\"asset\": \"native_eth\""));
|
|
1266
|
+
fs::remove_file(&native_output).expect("cleanup native");
|
|
1267
|
+
|
|
1268
|
+
let approve_output = temp_path("approve-output", "json");
|
|
1269
|
+
let approve_ops = FakeAgentOps {
|
|
1270
|
+
calls: Mutex::new(Vec::new()),
|
|
1271
|
+
outcome: FakeOutcome::Signature(sample_signature()),
|
|
1272
|
+
};
|
|
1273
|
+
let outcome = runtime
|
|
1274
|
+
.block_on(run_command(
|
|
1275
|
+
Commands::Approve {
|
|
1276
|
+
network: 137,
|
|
1277
|
+
token: token.clone(),
|
|
1278
|
+
spender: spender.clone(),
|
|
1279
|
+
amount_wei: 11,
|
|
1280
|
+
},
|
|
1281
|
+
true,
|
|
1282
|
+
&daemon_socket,
|
|
1283
|
+
OutputFormat::Json,
|
|
1284
|
+
&OutputTarget::File {
|
|
1285
|
+
path: approve_output.clone(),
|
|
1286
|
+
overwrite: false,
|
|
1287
|
+
},
|
|
1288
|
+
&approve_ops,
|
|
1289
|
+
))
|
|
1290
|
+
.expect("approve run");
|
|
1291
|
+
assert_eq!(outcome, CommandRunOutcome::Completed);
|
|
1292
|
+
assert_eq!(
|
|
1293
|
+
approve_ops.calls.lock().expect("lock").as_slice(),
|
|
1294
|
+
&[FakeCall::Approve {
|
|
1295
|
+
chain_id: 137,
|
|
1296
|
+
token: token.clone(),
|
|
1297
|
+
spender: spender.clone(),
|
|
1298
|
+
amount_wei: 11,
|
|
1299
|
+
}]
|
|
1300
|
+
);
|
|
1301
|
+
let approve_json = read_file(&approve_output);
|
|
1302
|
+
assert!(approve_json.contains("\"command\": \"approve\""));
|
|
1303
|
+
assert!(approve_json.contains("\"counterparty\": \"0x3000000000000000000000000000000000000000\""));
|
|
1304
|
+
fs::remove_file(&approve_output).expect("cleanup approve");
|
|
1305
|
+
|
|
1306
|
+
let broadcast_output = temp_path("broadcast-output", "json");
|
|
1307
|
+
let broadcast_ops = FakeAgentOps {
|
|
1308
|
+
calls: Mutex::new(Vec::new()),
|
|
1309
|
+
outcome: FakeOutcome::Signature(sample_signature()),
|
|
1310
|
+
};
|
|
1311
|
+
let broadcast_command = Commands::Broadcast {
|
|
1312
|
+
network: 8453,
|
|
1313
|
+
nonce: 2,
|
|
1314
|
+
to: to.clone(),
|
|
1315
|
+
value_wei: 13,
|
|
1316
|
+
data_hex: "0xdeadbeef".to_string(),
|
|
1317
|
+
gas_limit: 21000,
|
|
1318
|
+
max_fee_per_gas_wei: 100,
|
|
1319
|
+
max_priority_fee_per_gas_wei: 3,
|
|
1320
|
+
tx_type: 0x02,
|
|
1321
|
+
delegation_enabled: false,
|
|
1322
|
+
};
|
|
1323
|
+
let outcome = runtime
|
|
1324
|
+
.block_on(run_command(
|
|
1325
|
+
broadcast_command,
|
|
1326
|
+
true,
|
|
1327
|
+
&daemon_socket,
|
|
1328
|
+
OutputFormat::Json,
|
|
1329
|
+
&OutputTarget::File {
|
|
1330
|
+
path: broadcast_output.clone(),
|
|
1331
|
+
overwrite: false,
|
|
1332
|
+
},
|
|
1333
|
+
&broadcast_ops,
|
|
1334
|
+
))
|
|
1335
|
+
.expect("broadcast run");
|
|
1336
|
+
assert_eq!(outcome, CommandRunOutcome::Completed);
|
|
1337
|
+
assert_eq!(
|
|
1338
|
+
broadcast_ops.calls.lock().expect("lock").len(),
|
|
1339
|
+
1
|
|
1340
|
+
);
|
|
1341
|
+
let broadcast_json = read_file(&broadcast_output);
|
|
1342
|
+
assert!(broadcast_json.contains("\"command\": \"broadcast\""));
|
|
1343
|
+
assert!(broadcast_json.contains("\"estimated_max_gas_spend_wei\":"));
|
|
1344
|
+
assert!(broadcast_json.contains("\"delegation_enabled\": false"));
|
|
1345
|
+
fs::remove_file(&broadcast_output).expect("cleanup broadcast");
|
|
1346
|
+
|
|
1347
|
+
let manual_output = temp_path("manual-output", "json");
|
|
1348
|
+
let manual_ops = FakeAgentOps {
|
|
1349
|
+
calls: Mutex::new(Vec::new()),
|
|
1350
|
+
outcome: FakeOutcome::ManualApprovalRequired {
|
|
1351
|
+
approval_request_id: Uuid::nil(),
|
|
1352
|
+
relay_url: Some("https://relay.example".to_string()),
|
|
1353
|
+
frontend_url: Some("https://frontend.example/approval".to_string()),
|
|
1354
|
+
},
|
|
1355
|
+
};
|
|
1356
|
+
let outcome = runtime
|
|
1357
|
+
.block_on(run_command(
|
|
1358
|
+
Commands::Transfer {
|
|
1359
|
+
network: 1,
|
|
1360
|
+
token: token.clone(),
|
|
1361
|
+
to: to.clone(),
|
|
1362
|
+
amount_wei: 1,
|
|
1363
|
+
},
|
|
1364
|
+
true,
|
|
1365
|
+
&daemon_socket,
|
|
1366
|
+
OutputFormat::Json,
|
|
1367
|
+
&OutputTarget::File {
|
|
1368
|
+
path: manual_output.clone(),
|
|
1369
|
+
overwrite: false,
|
|
1370
|
+
},
|
|
1371
|
+
&manual_ops,
|
|
1372
|
+
))
|
|
1373
|
+
.expect("manual approval run");
|
|
1374
|
+
assert_eq!(outcome, CommandRunOutcome::ManualApprovalRequired);
|
|
1375
|
+
let manual_json = read_file(&manual_output);
|
|
1376
|
+
assert!(manual_json.contains("\"relay_url\": \"https://relay.example\""));
|
|
1377
|
+
fs::remove_file(&manual_output).expect("cleanup manual");
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
#[test]
|
|
1381
|
+
fn run_command_bubbles_sdk_errors() {
|
|
1382
|
+
let runtime = test_runtime();
|
|
1383
|
+
let output_path = temp_path("agent-run-error", "json");
|
|
1384
|
+
let ops = FakeAgentOps {
|
|
1385
|
+
calls: Mutex::new(Vec::new()),
|
|
1386
|
+
outcome: FakeOutcome::Serialization("bad payload".to_string()),
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
let err = runtime
|
|
1390
|
+
.block_on(run_command(
|
|
1391
|
+
Commands::TransferNative {
|
|
1392
|
+
network: 1,
|
|
1393
|
+
to: "0x2000000000000000000000000000000000000000"
|
|
1394
|
+
.parse()
|
|
1395
|
+
.expect("to"),
|
|
1396
|
+
amount_wei: 3,
|
|
1397
|
+
},
|
|
1398
|
+
true,
|
|
1399
|
+
Path::new("/tmp/wlfi.sock"),
|
|
1400
|
+
OutputFormat::Json,
|
|
1401
|
+
&OutputTarget::File {
|
|
1402
|
+
path: output_path,
|
|
1403
|
+
overwrite: false,
|
|
1404
|
+
},
|
|
1405
|
+
&ops,
|
|
1406
|
+
))
|
|
1407
|
+
.expect_err("sdk error must bubble");
|
|
1408
|
+
assert!(err.to_string().contains("failed to serialize action payload"));
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
#[test]
|
|
1412
|
+
fn run_command_rejects_invalid_broadcast_payloads_before_sdk_call() {
|
|
1413
|
+
let runtime = test_runtime();
|
|
1414
|
+
let output_path = temp_path("agent-run-invalid-broadcast", "json");
|
|
1415
|
+
let ops = FakeAgentOps {
|
|
1416
|
+
calls: Mutex::new(Vec::new()),
|
|
1417
|
+
outcome: FakeOutcome::Signature(sample_signature()),
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
let err = runtime
|
|
1421
|
+
.block_on(run_command(
|
|
1422
|
+
Commands::Broadcast {
|
|
1423
|
+
network: 1,
|
|
1424
|
+
nonce: 0,
|
|
1425
|
+
to: "0x2000000000000000000000000000000000000000"
|
|
1426
|
+
.parse()
|
|
1427
|
+
.expect("to"),
|
|
1428
|
+
value_wei: 0,
|
|
1429
|
+
data_hex: "0x".to_string(),
|
|
1430
|
+
gas_limit: 21_000,
|
|
1431
|
+
max_fee_per_gas_wei: 1,
|
|
1432
|
+
max_priority_fee_per_gas_wei: 0,
|
|
1433
|
+
tx_type: 0x02,
|
|
1434
|
+
delegation_enabled: true,
|
|
1435
|
+
},
|
|
1436
|
+
true,
|
|
1437
|
+
Path::new("/tmp/wlfi.sock"),
|
|
1438
|
+
OutputFormat::Json,
|
|
1439
|
+
&OutputTarget::File {
|
|
1440
|
+
path: output_path,
|
|
1441
|
+
overwrite: false,
|
|
1442
|
+
},
|
|
1443
|
+
&ops,
|
|
1444
|
+
))
|
|
1445
|
+
.expect_err("invalid broadcast must fail before sdk call");
|
|
1446
|
+
assert!(err.to_string().contains("invalid broadcast transaction payload"));
|
|
1447
|
+
assert!(ops.calls.lock().expect("lock").is_empty());
|
|
1448
|
+
}
|
|
833
1449
|
}
|