@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
@@ -89,7 +89,7 @@ pub trait AgentOperations: Send + Sync {
89
89
  async fn reserve_broadcast_nonce(
90
90
  &self,
91
91
  chain_id: u64,
92
- min_nonce: u64,
92
+ nonce: u64,
93
93
  ) -> Result<NonceReservation, AgentSdkError>;
94
94
 
95
95
  /// Explicitly releases an unused nonce reservation.
@@ -244,7 +244,21 @@ where
244
244
  }
245
245
 
246
246
  async fn broadcast_tx(&self, mut tx: BroadcastTx) -> Result<Signature, AgentSdkError> {
247
- let reservation = self.reserve_broadcast_nonce(tx.chain_id, tx.nonce).await?;
247
+ let requested_nonce = tx.nonce;
248
+ let reservation = self
249
+ .reserve_broadcast_nonce(tx.chain_id, requested_nonce)
250
+ .await?;
251
+ if reservation.nonce != requested_nonce {
252
+ let _ = self
253
+ .release_broadcast_nonce(reservation.reservation_id)
254
+ .await;
255
+ return Err(AgentSdkError::Daemon(DaemonError::InvalidNonceReservation(
256
+ format!(
257
+ "requested exact nonce {requested_nonce} for chain_id {} but daemon reserved {}",
258
+ tx.chain_id, reservation.nonce
259
+ ),
260
+ )));
261
+ }
248
262
  tx.nonce = reservation.nonce;
249
263
 
250
264
  match self.sign_action(AgentAction::BroadcastTx { tx }).await {
@@ -261,7 +275,7 @@ where
261
275
  async fn reserve_broadcast_nonce(
262
276
  &self,
263
277
  chain_id: u64,
264
- min_nonce: u64,
278
+ nonce: u64,
265
279
  ) -> Result<NonceReservation, AgentSdkError> {
266
280
  let now = OffsetDateTime::now_utc();
267
281
  let request = NonceReservationRequest {
@@ -269,7 +283,8 @@ where
269
283
  agent_key_id: self.agent_key_id,
270
284
  agent_auth_token: self.agent_auth_token.to_string(),
271
285
  chain_id,
272
- min_nonce,
286
+ min_nonce: nonce,
287
+ exact_nonce: true,
273
288
  requested_at: now,
274
289
  expires_at: now + Self::REQUEST_TTL,
275
290
  };
@@ -302,10 +317,10 @@ mod tests {
302
317
  use uuid::Uuid;
303
318
  use vault_daemon::{DaemonError, KeyManagerDaemonApi};
304
319
  use vault_domain::{
305
- AdminSession, AgentAction, AgentCredentials, AgentKey, BroadcastTx, EvmAddress, Lease,
306
- ManualApprovalDecision, ManualApprovalRequest, NonceReleaseRequest, NonceReservation,
307
- NonceReservationRequest, PolicyAttachment, RelayConfig, SignRequest, Signature,
308
- SpendingPolicy, VaultKey,
320
+ AdminSession, AgentAction, AgentCredentials, AgentKey, BroadcastTx, EntityScope,
321
+ EvmAddress, Lease, ManualApprovalDecision, ManualApprovalRequest, NonceReleaseRequest,
322
+ NonceReservation, NonceReservationRequest, PolicyAttachment, RelayConfig, SignRequest,
323
+ Signature, SpendingPolicy, VaultKey, DomainError,
309
324
  };
310
325
  use vault_policy::PolicyEvaluation;
311
326
  use vault_signer::KeyCreateRequest;
@@ -315,6 +330,8 @@ mod tests {
315
330
  #[derive(Default)]
316
331
  pub(super) struct RecordingDaemon {
317
332
  pub(super) request: Mutex<Option<SignRequest>>,
333
+ pub(super) nonce_request: Mutex<Option<NonceReservationRequest>>,
334
+ pub(super) released_reservation: Mutex<Option<Uuid>>,
318
335
  fail_sign: bool,
319
336
  reserved_nonce_override: Option<u64>,
320
337
  }
@@ -440,6 +457,7 @@ mod tests {
440
457
  &self,
441
458
  request: NonceReservationRequest,
442
459
  ) -> Result<NonceReservation, DaemonError> {
460
+ *self.nonce_request.lock().expect("lock") = Some(request.clone());
443
461
  Ok(NonceReservation {
444
462
  reservation_id: Uuid::new_v4(),
445
463
  agent_key_id: request.agent_key_id,
@@ -451,7 +469,8 @@ mod tests {
451
469
  })
452
470
  }
453
471
 
454
- async fn release_nonce(&self, _request: NonceReleaseRequest) -> Result<(), DaemonError> {
472
+ async fn release_nonce(&self, request: NonceReleaseRequest) -> Result<(), DaemonError> {
473
+ *self.released_reservation.lock().expect("lock") = Some(request.reservation_id);
455
474
  Ok(())
456
475
  }
457
476
 
@@ -476,6 +495,178 @@ mod tests {
476
495
  }
477
496
  }
478
497
 
498
+ fn sample_admin_session() -> AdminSession {
499
+ AdminSession {
500
+ vault_password: "vault-password".to_string(),
501
+ lease: Lease {
502
+ lease_id: Uuid::new_v4(),
503
+ issued_at: OffsetDateTime::UNIX_EPOCH,
504
+ expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(5),
505
+ },
506
+ }
507
+ }
508
+
509
+ fn sample_policy() -> SpendingPolicy {
510
+ SpendingPolicy::new(
511
+ 0,
512
+ vault_domain::PolicyType::PerTxMaxSpending,
513
+ 100,
514
+ EntityScope::All,
515
+ EntityScope::All,
516
+ EntityScope::All,
517
+ )
518
+ .expect("policy")
519
+ }
520
+
521
+ fn sample_sign_request() -> SignRequest {
522
+ SignRequest {
523
+ request_id: Uuid::new_v4(),
524
+ agent_key_id: Uuid::new_v4(),
525
+ agent_auth_token: "agent-secret-token".to_string(),
526
+ payload: br#"{"kind":"transfer_native"}"#.to_vec(),
527
+ action: AgentAction::TransferNative {
528
+ chain_id: 1,
529
+ to: "0x1111111111111111111111111111111111111111"
530
+ .parse()
531
+ .expect("to"),
532
+ amount_wei: 1,
533
+ },
534
+ requested_at: OffsetDateTime::UNIX_EPOCH,
535
+ expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(2),
536
+ }
537
+ }
538
+
539
+ fn sample_nonce_release_request() -> NonceReleaseRequest {
540
+ NonceReleaseRequest {
541
+ request_id: Uuid::new_v4(),
542
+ agent_key_id: Uuid::new_v4(),
543
+ agent_auth_token: "agent-secret-token".to_string(),
544
+ reservation_id: Uuid::new_v4(),
545
+ requested_at: OffsetDateTime::UNIX_EPOCH,
546
+ expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(2),
547
+ }
548
+ }
549
+
550
+ fn sample_nonce_reservation_request() -> NonceReservationRequest {
551
+ NonceReservationRequest {
552
+ request_id: Uuid::new_v4(),
553
+ agent_key_id: Uuid::new_v4(),
554
+ agent_auth_token: "agent-secret-token".to_string(),
555
+ chain_id: 1,
556
+ min_nonce: 7,
557
+ exact_nonce: false,
558
+ requested_at: OffsetDateTime::UNIX_EPOCH,
559
+ expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(2),
560
+ }
561
+ }
562
+
563
+ #[tokio::test]
564
+ async fn recording_daemon_not_used_paths_return_transport_errors() {
565
+ let daemon = RecordingDaemon::default();
566
+ let session = sample_admin_session();
567
+ let policy = sample_policy();
568
+ let request = sample_sign_request();
569
+ let reservation_request = sample_nonce_reservation_request();
570
+ let release_request = sample_nonce_release_request();
571
+
572
+ assert!(matches!(
573
+ daemon.issue_lease("pw").await,
574
+ Err(DaemonError::Transport(message)) if message == "not used"
575
+ ));
576
+ assert!(matches!(
577
+ daemon.add_policy(&session, policy.clone()).await,
578
+ Err(DaemonError::Transport(message)) if message == "not used"
579
+ ));
580
+ assert!(matches!(
581
+ daemon.list_policies(&session).await,
582
+ Err(DaemonError::Transport(message)) if message == "not used"
583
+ ));
584
+ assert!(matches!(
585
+ daemon.disable_policy(&session, Uuid::new_v4()).await,
586
+ Err(DaemonError::Transport(message)) if message == "not used"
587
+ ));
588
+ assert!(matches!(
589
+ daemon
590
+ .create_vault_key(&session, KeyCreateRequest::Generate)
591
+ .await,
592
+ Err(DaemonError::Transport(message)) if message == "not used"
593
+ ));
594
+ assert!(matches!(
595
+ daemon
596
+ .export_vault_private_key(&session, Uuid::new_v4())
597
+ .await,
598
+ Err(DaemonError::Transport(message)) if message == "not used"
599
+ ));
600
+ assert!(matches!(
601
+ daemon
602
+ .create_agent_key(&session, Uuid::new_v4(), PolicyAttachment::AllPolicies)
603
+ .await,
604
+ Err(DaemonError::Transport(message)) if message == "not used"
605
+ ));
606
+ assert!(matches!(
607
+ daemon.rotate_agent_auth_token(&session, Uuid::new_v4()).await,
608
+ Err(DaemonError::Transport(message)) if message == "not used"
609
+ ));
610
+ assert!(matches!(
611
+ daemon.revoke_agent_key(&session, Uuid::new_v4()).await,
612
+ Err(DaemonError::Transport(message)) if message == "not used"
613
+ ));
614
+ assert!(matches!(
615
+ daemon.list_manual_approval_requests(&session).await,
616
+ Err(DaemonError::Transport(message)) if message == "not used"
617
+ ));
618
+ assert!(matches!(
619
+ daemon
620
+ .decide_manual_approval_request(
621
+ &session,
622
+ Uuid::new_v4(),
623
+ ManualApprovalDecision::Approve,
624
+ None
625
+ )
626
+ .await,
627
+ Err(DaemonError::Transport(message)) if message == "not used"
628
+ ));
629
+ assert!(matches!(
630
+ daemon
631
+ .set_relay_config(&session, Some("https://relay.example".to_string()), None)
632
+ .await,
633
+ Err(DaemonError::Transport(message)) if message == "not used"
634
+ ));
635
+ assert!(matches!(
636
+ daemon.get_relay_config(&session).await,
637
+ Err(DaemonError::Transport(message)) if message == "not used"
638
+ ));
639
+ assert!(matches!(
640
+ daemon.evaluate_for_agent(request.clone()).await,
641
+ Err(DaemonError::Transport(message)) if message == "not used"
642
+ ));
643
+ assert!(matches!(
644
+ daemon.explain_for_agent(request).await,
645
+ Err(DaemonError::Transport(message)) if message == "not used"
646
+ ));
647
+
648
+ let reservation = daemon
649
+ .reserve_nonce(reservation_request.clone())
650
+ .await
651
+ .expect("reserve nonce");
652
+ assert_eq!(reservation.chain_id, reservation_request.chain_id);
653
+ assert_eq!(reservation.nonce, reservation_request.min_nonce);
654
+ assert_eq!(
655
+ daemon
656
+ .nonce_request
657
+ .lock()
658
+ .expect("lock")
659
+ .clone()
660
+ .expect("captured nonce request")
661
+ .exact_nonce,
662
+ reservation_request.exact_nonce
663
+ );
664
+ daemon
665
+ .release_nonce(release_request)
666
+ .await
667
+ .expect("release nonce");
668
+ }
669
+
479
670
  #[tokio::test]
480
671
  async fn approve_sends_canonical_action_payload_and_auth_token() {
481
672
  let daemon = Arc::new(RecordingDaemon::default());
@@ -518,10 +709,109 @@ mod tests {
518
709
  assert_eq!(decoded, captured.action);
519
710
  }
520
711
 
712
+ #[tokio::test]
713
+ async fn constructors_transfer_native_and_reservation_helpers_use_expected_identity() {
714
+ let daemon = Arc::new(RecordingDaemon::default());
715
+ let key_id = Uuid::new_v4();
716
+ let sdk = AgentSdk::new_with_key_id_and_token(
717
+ daemon.clone(),
718
+ key_id,
719
+ "agent-secret-token".to_string(),
720
+ );
721
+
722
+ let recipient: EvmAddress = "0x2100000000000000000000000000000000000000"
723
+ .parse()
724
+ .expect("recipient");
725
+ let signature = sdk
726
+ .transfer_native(1, recipient.clone(), 5)
727
+ .await
728
+ .expect("transfer native");
729
+ assert_eq!(signature.bytes, vec![0x11, 0x22]);
730
+
731
+ let captured = daemon
732
+ .request
733
+ .lock()
734
+ .expect("lock")
735
+ .clone()
736
+ .expect("captured request");
737
+ assert_eq!(captured.agent_key_id, key_id);
738
+ assert_eq!(captured.agent_auth_token, "agent-secret-token");
739
+ assert_eq!(
740
+ captured.action,
741
+ AgentAction::TransferNative {
742
+ chain_id: 1,
743
+ to: recipient,
744
+ amount_wei: 5
745
+ }
746
+ );
747
+
748
+ let reservation = sdk
749
+ .reserve_broadcast_nonce(56, 99)
750
+ .await
751
+ .expect("reserve nonce");
752
+ assert_eq!(reservation.chain_id, 56);
753
+ assert_eq!(reservation.nonce, 99);
754
+ let captured_reservation = daemon
755
+ .nonce_request
756
+ .lock()
757
+ .expect("lock")
758
+ .clone()
759
+ .expect("captured nonce request");
760
+ assert_eq!(captured_reservation.chain_id, 56);
761
+ assert_eq!(captured_reservation.min_nonce, 99);
762
+ assert!(captured_reservation.exact_nonce);
763
+ sdk.release_broadcast_nonce(reservation.reservation_id)
764
+ .await
765
+ .expect("release nonce");
766
+ }
767
+
768
+ #[tokio::test]
769
+ async fn sign_erc20_calldata_and_invalid_actions_propagate_domain_errors() {
770
+ let daemon = Arc::new(RecordingDaemon::default());
771
+ let sdk = AgentSdk::new(daemon.clone(), test_credentials());
772
+ let encoded = vec![
773
+ 0x09, 0x5e, 0xa7, 0xb3, // approve(address,uint256)
774
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
775
+ 0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x44,
776
+ 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
777
+ 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
778
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
779
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
780
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
781
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c,
782
+ ];
783
+
784
+ let signature = sdk
785
+ .sign_erc20_calldata(
786
+ 1,
787
+ "0x3300000000000000000000000000000000000000"
788
+ .parse()
789
+ .expect("token"),
790
+ encoded,
791
+ )
792
+ .await
793
+ .expect("sign erc20 calldata");
794
+ assert_eq!(signature.bytes, vec![0x11, 0x22]);
795
+
796
+ let err = sdk
797
+ .transfer_native(
798
+ 1,
799
+ "0x5500000000000000000000000000000000000000"
800
+ .parse()
801
+ .expect("to"),
802
+ 0,
803
+ )
804
+ .await
805
+ .expect_err("invalid action");
806
+ assert!(matches!(err, AgentSdkError::Domain(DomainError::InvalidAmount)));
807
+ }
808
+
521
809
  #[tokio::test]
522
810
  async fn transfer_propagates_daemon_errors() {
523
811
  let daemon = Arc::new(RecordingDaemon {
524
812
  request: Mutex::new(None),
813
+ nonce_request: Mutex::new(None),
814
+ released_reservation: Mutex::new(None),
525
815
  fail_sign: true,
526
816
  reserved_nonce_override: None,
527
817
  });
@@ -580,9 +870,11 @@ mod tests {
580
870
  }
581
871
 
582
872
  #[tokio::test]
583
- async fn broadcast_tx_uses_reserved_nonce_from_daemon() {
873
+ async fn broadcast_tx_rejects_mismatched_reserved_nonce_from_daemon() {
584
874
  let daemon = Arc::new(RecordingDaemon {
585
875
  request: Mutex::new(None),
876
+ nonce_request: Mutex::new(None),
877
+ released_reservation: Mutex::new(None),
586
878
  fail_sign: false,
587
879
  reserved_nonce_override: Some(1),
588
880
  });
@@ -603,22 +895,34 @@ mod tests {
603
895
  delegation_enabled: false,
604
896
  };
605
897
 
606
- sdk.broadcast_tx(tx.clone()).await.expect("broadcast");
898
+ let err = sdk
899
+ .broadcast_tx(tx.clone())
900
+ .await
901
+ .expect_err("mismatched reserved nonce must fail");
902
+ assert!(matches!(
903
+ err,
904
+ AgentSdkError::Daemon(DaemonError::InvalidNonceReservation(message))
905
+ if message.contains("requested exact nonce 0")
906
+ && message.contains("chain_id 56")
907
+ && message.contains("reserved 1")
908
+ ));
607
909
 
608
- let captured = daemon
609
- .request
910
+ assert!(daemon.request.lock().expect("lock").is_none());
911
+ assert!(
912
+ daemon
913
+ .released_reservation
914
+ .lock()
915
+ .expect("lock")
916
+ .is_some()
917
+ );
918
+ let captured_reservation = daemon
919
+ .nonce_request
610
920
  .lock()
611
921
  .expect("lock")
612
922
  .clone()
613
- .expect("captured request");
614
- match captured.action {
615
- AgentAction::BroadcastTx { tx: signed_tx } => {
616
- assert_eq!(signed_tx.nonce, 1);
617
- assert_eq!(signed_tx.chain_id, tx.chain_id);
618
- assert_eq!(signed_tx.to, tx.to);
619
- }
620
- other => panic!("unexpected action: {other:?}"),
621
- }
923
+ .expect("captured nonce request");
924
+ assert!(captured_reservation.exact_nonce);
925
+ assert_eq!(captured_reservation.min_nonce, tx.nonce);
622
926
  }
623
927
  }
624
928
 
@@ -708,4 +1012,93 @@ mod typed_data_tests {
708
1012
  let decoded: AgentAction = serde_json::from_slice(&captured.payload).expect("decode");
709
1013
  assert_eq!(decoded, captured.action);
710
1014
  }
1015
+
1016
+ #[tokio::test]
1017
+ async fn transfer_and_eip3009_transfer_sends_canonical_action_payload() {
1018
+ let daemon = Arc::new(RecordingDaemon::default());
1019
+ let sdk = AgentSdk::new(daemon.clone(), test_credentials());
1020
+
1021
+ let token = "0x3000000000000000000000000000000000000000"
1022
+ .parse()
1023
+ .expect("token");
1024
+ let to = "0x5000000000000000000000000000000000000000"
1025
+ .parse()
1026
+ .expect("to");
1027
+ let signature = sdk
1028
+ .transfer(1, token, to, 9)
1029
+ .await
1030
+ .expect("transfer");
1031
+ assert_eq!(signature.bytes, vec![0x11, 0x22]);
1032
+
1033
+ let transfer_action = daemon
1034
+ .request
1035
+ .lock()
1036
+ .expect("lock")
1037
+ .clone()
1038
+ .expect("captured request")
1039
+ .action;
1040
+ assert!(matches!(transfer_action, AgentAction::Transfer { .. }));
1041
+
1042
+ let authorization = Eip3009Transfer {
1043
+ chain_id: 1,
1044
+ token: "0x3000000000000000000000000000000000000000"
1045
+ .parse()
1046
+ .expect("token"),
1047
+ token_name: "USD Coin".to_string(),
1048
+ token_version: Some("2".to_string()),
1049
+ from: "0x4000000000000000000000000000000000000000"
1050
+ .parse()
1051
+ .expect("from"),
1052
+ to: "0x5000000000000000000000000000000000000000"
1053
+ .parse()
1054
+ .expect("to"),
1055
+ amount_wei: 9,
1056
+ valid_after: 1,
1057
+ valid_before: 2,
1058
+ nonce_hex: "0x5555555555555555555555555555555555555555555555555555555555555555"
1059
+ .to_string(),
1060
+ };
1061
+
1062
+ let signature = sdk
1063
+ .eip3009_transfer_with_authorization(authorization.clone())
1064
+ .await
1065
+ .expect("eip3009 transfer");
1066
+ assert_eq!(signature.bytes, vec![0x11, 0x22]);
1067
+
1068
+ let captured = daemon
1069
+ .request
1070
+ .lock()
1071
+ .expect("lock")
1072
+ .clone()
1073
+ .expect("captured request");
1074
+ assert_eq!(
1075
+ captured.action,
1076
+ AgentAction::Eip3009TransferWithAuthorization { authorization }
1077
+ );
1078
+ let decoded: AgentAction = serde_json::from_slice(&captured.payload).expect("decode");
1079
+ assert_eq!(decoded, captured.action);
1080
+
1081
+ let erc20_transfer = vec![
1082
+ 0xa9, 0x05, 0x9c, 0xbb, // transfer(address,uint256)
1083
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1084
+ 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55,
1085
+ 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
1086
+ 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
1087
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1088
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1089
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
1090
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
1091
+ ];
1092
+ let signature = sdk
1093
+ .sign_erc20_calldata(
1094
+ 1,
1095
+ "0x6000000000000000000000000000000000000000"
1096
+ .parse()
1097
+ .expect("token"),
1098
+ erc20_transfer,
1099
+ )
1100
+ .await
1101
+ .expect("erc20 transfer");
1102
+ assert_eq!(signature.bytes, vec![0x11, 0x22]);
1103
+ }
711
1104
  }
@@ -5,6 +5,9 @@ edition.workspace = true
5
5
  license.workspace = true
6
6
  authors.workspace = true
7
7
 
8
+ [features]
9
+ interactive-secure-enclave-tests = []
10
+
8
11
  [dependencies]
9
12
  async-trait.workspace = true
10
13
  core-foundation.workspace = true