@wlfi-agent/cli 1.4.16 → 1.4.18

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.
Files changed (97) hide show
  1. package/Cargo.lock +26 -20
  2. package/Cargo.toml +1 -1
  3. package/README.md +61 -28
  4. package/crates/vault-cli-admin/src/io_utils.rs +149 -1
  5. package/crates/vault-cli-admin/src/main.rs +639 -16
  6. package/crates/vault-cli-admin/src/shared_config.rs +18 -18
  7. package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
  8. package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
  9. package/crates/vault-cli-admin/src/tui.rs +1205 -120
  10. package/crates/vault-cli-agent/Cargo.toml +1 -0
  11. package/crates/vault-cli-agent/src/io_utils.rs +163 -2
  12. package/crates/vault-cli-agent/src/main.rs +648 -32
  13. package/crates/vault-cli-daemon/Cargo.toml +4 -0
  14. package/crates/vault-cli-daemon/src/main.rs +617 -67
  15. package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
  16. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
  17. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
  18. package/crates/vault-daemon/src/persistence.rs +637 -100
  19. package/crates/vault-daemon/src/tests.rs +1013 -3
  20. package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
  21. package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
  22. package/crates/vault-domain/src/nonce.rs +4 -0
  23. package/crates/vault-domain/src/tests.rs +616 -0
  24. package/crates/vault-policy/src/engine.rs +55 -32
  25. package/crates/vault-policy/src/tests.rs +195 -0
  26. package/crates/vault-sdk-agent/src/lib.rs +415 -22
  27. package/crates/vault-signer/Cargo.toml +3 -0
  28. package/crates/vault-signer/src/lib.rs +266 -40
  29. package/crates/vault-transport-unix/src/lib.rs +653 -5
  30. package/crates/vault-transport-xpc/src/tests.rs +531 -3
  31. package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
  32. package/dist/cli.cjs +663 -190
  33. package/dist/cli.cjs.map +1 -1
  34. package/package.json +5 -2
  35. package/packages/cache/.turbo/turbo-build.log +53 -52
  36. package/packages/cache/coverage/clover.xml +529 -394
  37. package/packages/cache/coverage/coverage-final.json +2 -2
  38. package/packages/cache/coverage/index.html +21 -21
  39. package/packages/cache/coverage/src/client/index.html +1 -1
  40. package/packages/cache/coverage/src/client/index.ts.html +1 -1
  41. package/packages/cache/coverage/src/errors/index.html +1 -1
  42. package/packages/cache/coverage/src/errors/index.ts.html +12 -12
  43. package/packages/cache/coverage/src/index.html +1 -1
  44. package/packages/cache/coverage/src/index.ts.html +1 -1
  45. package/packages/cache/coverage/src/service/index.html +21 -21
  46. package/packages/cache/coverage/src/service/index.ts.html +769 -313
  47. package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
  48. package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
  49. package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
  50. package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
  51. package/packages/cache/dist/index.cjs +2 -2
  52. package/packages/cache/dist/index.js +1 -1
  53. package/packages/cache/dist/service/index.cjs +2 -2
  54. package/packages/cache/dist/service/index.js +1 -1
  55. package/packages/cache/node_modules/.bin/tsc +2 -2
  56. package/packages/cache/node_modules/.bin/tsserver +2 -2
  57. package/packages/cache/node_modules/.bin/tsup +2 -2
  58. package/packages/cache/node_modules/.bin/tsup-node +2 -2
  59. package/packages/cache/node_modules/.bin/vitest +4 -4
  60. package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  61. package/packages/cache/src/service/index.test.ts +165 -19
  62. package/packages/cache/src/service/index.ts +38 -1
  63. package/packages/config/.turbo/turbo-build.log +18 -17
  64. package/packages/config/dist/index.cjs +0 -17
  65. package/packages/config/dist/index.cjs.map +1 -1
  66. package/packages/config/src/index.ts +0 -17
  67. package/packages/rpc/.turbo/turbo-build.log +32 -31
  68. package/packages/rpc/dist/index.cjs +0 -17
  69. package/packages/rpc/dist/index.cjs.map +1 -1
  70. package/packages/rpc/src/index.js +1 -0
  71. package/packages/ui/.turbo/turbo-build.log +44 -43
  72. package/packages/ui/dist/components/badge.d.ts +1 -1
  73. package/packages/ui/dist/components/button.d.ts +1 -1
  74. package/packages/ui/node_modules/.bin/tsc +2 -2
  75. package/packages/ui/node_modules/.bin/tsserver +2 -2
  76. package/packages/ui/node_modules/.bin/tsup +2 -2
  77. package/packages/ui/node_modules/.bin/tsup-node +2 -2
  78. package/scripts/install-cli-launcher.mjs +37 -0
  79. package/scripts/install-rust-binaries.mjs +112 -0
  80. package/scripts/run-tests-isolated.mjs +210 -0
  81. package/src/cli.ts +310 -50
  82. package/src/lib/admin-reset.ts +15 -30
  83. package/src/lib/admin-setup.ts +246 -55
  84. package/src/lib/agent-auth-migrate.ts +5 -1
  85. package/src/lib/asset-broadcast.ts +15 -4
  86. package/src/lib/config-amounts.ts +6 -4
  87. package/src/lib/hidden-tty-prompt.js +1 -0
  88. package/src/lib/hidden-tty-prompt.ts +105 -0
  89. package/src/lib/keychain.ts +1 -0
  90. package/src/lib/local-admin-access.ts +4 -29
  91. package/src/lib/rust.ts +129 -33
  92. package/src/lib/signed-tx.ts +1 -0
  93. package/src/lib/sudo.ts +15 -5
  94. package/src/lib/wallet-profile.ts +3 -0
  95. package/src/lib/wallet-setup.ts +52 -0
  96. package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
  97. 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 cli.command {
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, cli.quiet);
270
+ print_status("submitting transfer request", output_format, quiet);
237
271
  let signature = match await_signature_or_handle_manual_approval(
238
272
  "transfer",
239
- &daemon_socket,
273
+ daemon_socket,
240
274
  output_format,
241
- &output_target,
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 => std::process::exit(2),
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, cli.quiet);
266
- print_agent_output(&output, output_format, &output_target)?;
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
- cli.quiet,
311
+ quiet,
278
312
  );
279
313
  let signature = match await_signature_or_handle_manual_approval(
280
314
  "transfer-native",
281
- &daemon_socket,
315
+ daemon_socket,
282
316
  output_format,
283
- &output_target,
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 => std::process::exit(2),
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, cli.quiet);
308
- print_agent_output(&output, output_format, &output_target)?;
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, cli.quiet);
352
+ print_status("submitting approve request", output_format, quiet);
319
353
  let signature = match await_signature_or_handle_manual_approval(
320
354
  "approve",
321
- &daemon_socket,
355
+ daemon_socket,
322
356
  output_format,
323
- &output_target,
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 => std::process::exit(2),
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, cli.quiet);
348
- print_agent_output(&output, output_format, &output_target)?;
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, cli.quiet);
416
+ print_status("submitting broadcast request", output_format, quiet);
383
417
  let signature = match await_signature_or_handle_manual_approval(
384
418
  "broadcast",
385
- &daemon_socket,
419
+ daemon_socket,
386
420
  output_format,
387
- &output_target,
421
+ output_target,
388
422
  sdk.broadcast_tx(tx),
389
423
  )
390
424
  .await?
391
425
  {
392
426
  Some(signature) => signature,
393
- None => std::process::exit(2),
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, cli.quiet);
412
- print_agent_output(&output, output_format, &output_target)?;
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 {} --daemon-socket {}",
440
- approval_request_id,
441
- daemon_socket.display()
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
- ensure_output_parent, resolve_agent_auth_token, resolve_output_format,
559
- resolve_output_target, should_print_status, write_output_file, Cli, Commands, OutputFormat,
560
- OutputTarget,
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
  }