@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.
Files changed (93) hide show
  1. package/Cargo.lock +5 -0
  2. package/README.md +61 -28
  3. package/crates/vault-cli-admin/src/io_utils.rs +149 -1
  4. package/crates/vault-cli-admin/src/main.rs +639 -16
  5. package/crates/vault-cli-admin/src/shared_config.rs +18 -18
  6. package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
  7. package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
  8. package/crates/vault-cli-admin/src/tui.rs +1205 -120
  9. package/crates/vault-cli-agent/Cargo.toml +1 -0
  10. package/crates/vault-cli-agent/src/io_utils.rs +163 -2
  11. package/crates/vault-cli-agent/src/main.rs +648 -32
  12. package/crates/vault-cli-daemon/Cargo.toml +4 -0
  13. package/crates/vault-cli-daemon/src/main.rs +617 -67
  14. package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
  15. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
  16. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
  17. package/crates/vault-daemon/src/persistence.rs +637 -100
  18. package/crates/vault-daemon/src/tests.rs +1013 -3
  19. package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
  20. package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
  21. package/crates/vault-domain/src/nonce.rs +4 -0
  22. package/crates/vault-domain/src/tests.rs +616 -0
  23. package/crates/vault-policy/src/engine.rs +55 -32
  24. package/crates/vault-policy/src/tests.rs +195 -0
  25. package/crates/vault-sdk-agent/src/lib.rs +415 -22
  26. package/crates/vault-signer/Cargo.toml +3 -0
  27. package/crates/vault-signer/src/lib.rs +266 -40
  28. package/crates/vault-transport-unix/src/lib.rs +653 -5
  29. package/crates/vault-transport-xpc/src/tests.rs +531 -3
  30. package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
  31. package/dist/cli.cjs +756 -194
  32. package/dist/cli.cjs.map +1 -1
  33. package/package.json +5 -2
  34. package/packages/cache/.turbo/turbo-build.log +20 -20
  35. package/packages/cache/coverage/clover.xml +529 -394
  36. package/packages/cache/coverage/coverage-final.json +2 -2
  37. package/packages/cache/coverage/index.html +21 -21
  38. package/packages/cache/coverage/src/client/index.html +1 -1
  39. package/packages/cache/coverage/src/client/index.ts.html +1 -1
  40. package/packages/cache/coverage/src/errors/index.html +1 -1
  41. package/packages/cache/coverage/src/errors/index.ts.html +12 -12
  42. package/packages/cache/coverage/src/index.html +1 -1
  43. package/packages/cache/coverage/src/index.ts.html +1 -1
  44. package/packages/cache/coverage/src/service/index.html +21 -21
  45. package/packages/cache/coverage/src/service/index.ts.html +769 -313
  46. package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
  47. package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
  48. package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
  49. package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
  50. package/packages/cache/dist/index.cjs +2 -2
  51. package/packages/cache/dist/index.js +1 -1
  52. package/packages/cache/dist/service/index.cjs +2 -2
  53. package/packages/cache/dist/service/index.js +1 -1
  54. package/packages/cache/node_modules/.bin/tsc +2 -2
  55. package/packages/cache/node_modules/.bin/tsserver +2 -2
  56. package/packages/cache/node_modules/.bin/tsup +2 -2
  57. package/packages/cache/node_modules/.bin/tsup-node +2 -2
  58. package/packages/cache/node_modules/.bin/vitest +4 -4
  59. package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  60. package/packages/cache/src/service/index.test.ts +165 -19
  61. package/packages/cache/src/service/index.ts +38 -1
  62. package/packages/config/.turbo/turbo-build.log +4 -4
  63. package/packages/config/dist/index.cjs +0 -17
  64. package/packages/config/dist/index.cjs.map +1 -1
  65. package/packages/config/src/index.ts +0 -17
  66. package/packages/rpc/.turbo/turbo-build.log +11 -11
  67. package/packages/rpc/dist/index.cjs +0 -17
  68. package/packages/rpc/dist/index.cjs.map +1 -1
  69. package/packages/rpc/src/index.js +1 -0
  70. package/packages/ui/node_modules/.bin/tsc +2 -2
  71. package/packages/ui/node_modules/.bin/tsserver +2 -2
  72. package/packages/ui/node_modules/.bin/tsup +2 -2
  73. package/packages/ui/node_modules/.bin/tsup-node +2 -2
  74. package/scripts/install-cli-launcher.mjs +37 -0
  75. package/scripts/install-rust-binaries.mjs +47 -0
  76. package/scripts/run-tests-isolated.mjs +210 -0
  77. package/src/cli.ts +310 -50
  78. package/src/lib/admin-reset.ts +101 -33
  79. package/src/lib/admin-setup.ts +285 -55
  80. package/src/lib/agent-auth-migrate.ts +5 -1
  81. package/src/lib/asset-broadcast.ts +15 -4
  82. package/src/lib/config-amounts.ts +6 -4
  83. package/src/lib/hidden-tty-prompt.js +1 -0
  84. package/src/lib/hidden-tty-prompt.ts +105 -0
  85. package/src/lib/keychain.ts +1 -0
  86. package/src/lib/local-admin-access.ts +4 -29
  87. package/src/lib/rust.ts +129 -33
  88. package/src/lib/signed-tx.ts +1 -0
  89. package/src/lib/sudo.ts +15 -5
  90. package/src/lib/wallet-profile.ts +3 -0
  91. package/src/lib/wallet-setup.ts +52 -0
  92. package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
  93. package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
@@ -2,6 +2,7 @@ use std::collections::BTreeSet;
2
2
 
3
3
  use alloy_primitives::U256;
4
4
  use alloy_sol_types::{sol, SolCall};
5
+ use serde::{Deserialize, Serialize};
5
6
  use time::OffsetDateTime;
6
7
  use uuid::Uuid;
7
8
 
@@ -53,6 +54,22 @@ fn manual_approval_capability_token_rejects_invalid_secret() {
53
54
  assert!(matches!(err, DomainError::InvalidRelayCapabilitySecret));
54
55
  }
55
56
 
57
+ #[test]
58
+ fn manual_approval_capability_helpers_reject_blank_and_short_inputs() {
59
+ let approval_request_id = Uuid::new_v4();
60
+ let err = manual_approval_capability_token(" ", approval_request_id)
61
+ .expect_err("must reject blank secret");
62
+ assert!(matches!(err, DomainError::InvalidRelayCapabilitySecret));
63
+
64
+ let err = manual_approval_capability_token("11", approval_request_id)
65
+ .expect_err("must reject short secret");
66
+ assert!(matches!(err, DomainError::InvalidRelayCapabilitySecret));
67
+
68
+ let err =
69
+ manual_approval_capability_hash(" ").expect_err("must reject blank capability token");
70
+ assert!(matches!(err, DomainError::InvalidRelayCapabilityToken));
71
+ }
72
+
56
73
  #[test]
57
74
  fn address_deserialize_rejects_invalid_values() {
58
75
  let invalid =
@@ -88,6 +105,110 @@ fn policy_set_cannot_be_empty() {
88
105
  assert!(matches!(result, Err(DomainError::EmptyPolicySet)));
89
106
  }
90
107
 
108
+ #[test]
109
+ fn policy_attachment_applies_to_all_and_selected_ids() {
110
+ let first = Uuid::new_v4();
111
+ let second = Uuid::new_v4();
112
+ let attachment = PolicyAttachment::policy_set(BTreeSet::from([first])).expect("policy set");
113
+
114
+ assert!(PolicyAttachment::AllPolicies.applies_to(first));
115
+ assert!(attachment.applies_to(first));
116
+ assert!(!attachment.applies_to(second));
117
+ }
118
+
119
+ #[test]
120
+ fn spending_policy_rejects_invalid_ranges_and_network_sets() {
121
+ let recipient: EvmAddress = "0x1111111111111111111111111111111111111111"
122
+ .parse()
123
+ .expect("recipient");
124
+ let asset = AssetId::Erc20(
125
+ "0x2222222222222222222222222222222222222222"
126
+ .parse()
127
+ .expect("token"),
128
+ );
129
+
130
+ let err = SpendingPolicy::new(
131
+ 1,
132
+ PolicyType::PerTxMaxSpending,
133
+ 0,
134
+ EntityScope::All,
135
+ EntityScope::All,
136
+ EntityScope::All,
137
+ )
138
+ .expect_err("zero max amount");
139
+ assert!(matches!(err, DomainError::InvalidAmount));
140
+
141
+ let err = SpendingPolicy::new_manual_approval(
142
+ 1,
143
+ 0,
144
+ 10,
145
+ EntityScope::Set(BTreeSet::from([recipient.clone()])),
146
+ EntityScope::Set(BTreeSet::from([asset.clone()])),
147
+ EntityScope::Set(BTreeSet::from([1])),
148
+ )
149
+ .expect_err("zero min amount");
150
+ assert!(matches!(err, DomainError::InvalidAmount));
151
+
152
+ let err = SpendingPolicy::new_manual_approval(
153
+ 1,
154
+ 11,
155
+ 10,
156
+ EntityScope::Set(BTreeSet::from([recipient.clone()])),
157
+ EntityScope::Set(BTreeSet::from([asset.clone()])),
158
+ EntityScope::Set(BTreeSet::from([1])),
159
+ )
160
+ .expect_err("min greater than max");
161
+ assert!(matches!(err, DomainError::InvalidAmount));
162
+
163
+ let err = SpendingPolicy::new(
164
+ 1,
165
+ PolicyType::PerTxMaxSpending,
166
+ 10,
167
+ EntityScope::Set(BTreeSet::from([recipient])),
168
+ EntityScope::Set(BTreeSet::from([asset])),
169
+ EntityScope::Set(BTreeSet::from([0])),
170
+ )
171
+ .expect_err("zero chain id");
172
+ assert!(matches!(err, DomainError::InvalidChainId));
173
+ }
174
+
175
+ #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
176
+ struct U128Wrapper {
177
+ #[serde(with = "super::u128_as_decimal_string")]
178
+ value: u128,
179
+ }
180
+
181
+ #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
182
+ struct OptionalU128Wrapper {
183
+ #[serde(with = "super::u128_as_decimal_string::option")]
184
+ value: Option<u128>,
185
+ }
186
+
187
+ #[test]
188
+ fn u128_decimal_string_helpers_roundtrip_and_reject_invalid_values() {
189
+ let encoded = serde_json::to_string(&U128Wrapper { value: 42 }).expect("encode");
190
+ assert_eq!(encoded, r#"{"value":"42"}"#);
191
+ let decoded: U128Wrapper = serde_json::from_str(&encoded).expect("decode");
192
+ assert_eq!(decoded, U128Wrapper { value: 42 });
193
+
194
+ let some = serde_json::to_string(&OptionalU128Wrapper { value: Some(7) }).expect("encode");
195
+ assert_eq!(some, r#"{"value":"7"}"#);
196
+ let decoded: OptionalU128Wrapper = serde_json::from_str(&some).expect("decode");
197
+ assert_eq!(decoded, OptionalU128Wrapper { value: Some(7) });
198
+
199
+ let none = serde_json::to_string(&OptionalU128Wrapper { value: None }).expect("encode");
200
+ assert_eq!(none, r#"{"value":null}"#);
201
+ let decoded: OptionalU128Wrapper = serde_json::from_str(&none).expect("decode");
202
+ assert_eq!(decoded, OptionalU128Wrapper { value: None });
203
+
204
+ let err = serde_json::from_str::<U128Wrapper>(r#"{"value":"nope"}"#).expect_err("invalid u128");
205
+ assert!(err.to_string().contains("invalid digit"));
206
+
207
+ let err =
208
+ serde_json::from_str::<OptionalU128Wrapper>(r#"{"value":"bad"}"#).expect_err("invalid option u128");
209
+ assert!(err.to_string().contains("invalid digit"));
210
+ }
211
+
91
212
  #[test]
92
213
  fn parse_erc20_transfer_call_succeeds() {
93
214
  let to = alloy_primitives::Address::from([0x11; 20]);
@@ -430,6 +551,32 @@ sol! {
430
551
 
431
552
  function permit(address owner, PermitSingle permitSingle, bytes signature);
432
553
  function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature);
554
+ function receiveWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature);
555
+ }
556
+
557
+ #[test]
558
+ fn asset_id_display_formats_native_and_erc20_variants() {
559
+ let token: EvmAddress = "0x1234000000000000000000000000000000000000"
560
+ .parse()
561
+ .expect("token");
562
+
563
+ assert_eq!(AssetId::NativeEth.to_string(), "native_eth");
564
+ assert_eq!(AssetId::Erc20(token).to_string(), "erc20:0x1234000000000000000000000000000000000000");
565
+ }
566
+
567
+ #[test]
568
+ fn spending_policy_rejects_empty_network_sets() {
569
+ let err = SpendingPolicy::new(
570
+ 1,
571
+ PolicyType::PerTxMaxSpending,
572
+ 10,
573
+ EntityScope::All,
574
+ EntityScope::All,
575
+ EntityScope::Set(BTreeSet::new()),
576
+ )
577
+ .expect_err("empty network set must fail");
578
+
579
+ assert!(matches!(err, DomainError::InvalidChainId));
433
580
  }
434
581
 
435
582
  #[test]
@@ -617,6 +764,474 @@ fn permit2_and_eip3009_actions_produce_signing_hashes() {
617
764
  assert_ne!(transfer_hash, receive_hash);
618
765
  }
619
766
 
767
+ #[test]
768
+ fn permit2_and_eip3009_validation_reject_invalid_inputs() {
769
+ let base_permit = Permit2Permit {
770
+ chain_id: 1,
771
+ permit2_contract: "0x000000000022d473030f116ddee9f6b43ac78ba3"
772
+ .parse()
773
+ .expect("permit2"),
774
+ token: "0x1111111111111111111111111111111111111111"
775
+ .parse()
776
+ .expect("token"),
777
+ spender: "0x2222222222222222222222222222222222222222"
778
+ .parse()
779
+ .expect("spender"),
780
+ amount_wei: 123,
781
+ expiration: 100,
782
+ nonce: 7,
783
+ sig_deadline: 200,
784
+ };
785
+ assert!(matches!(
786
+ Permit2Permit {
787
+ chain_id: 0,
788
+ ..base_permit.clone()
789
+ }
790
+ .validate(),
791
+ Err(DomainError::InvalidChainId)
792
+ ));
793
+ assert!(matches!(
794
+ Permit2Permit {
795
+ amount_wei: 0,
796
+ ..base_permit.clone()
797
+ }
798
+ .validate(),
799
+ Err(DomainError::InvalidAmount)
800
+ ));
801
+ assert!(matches!(
802
+ Permit2Permit {
803
+ expiration: (1u64 << 48),
804
+ ..base_permit.clone()
805
+ }
806
+ .validate(),
807
+ Err(DomainError::AmountOutOfRange)
808
+ ));
809
+ assert!(matches!(
810
+ Permit2Permit {
811
+ nonce: (1u64 << 48),
812
+ ..base_permit
813
+ }
814
+ .validate(),
815
+ Err(DomainError::AmountOutOfRange)
816
+ ));
817
+
818
+ let base_eip3009 = Eip3009Transfer {
819
+ chain_id: 1,
820
+ token: "0x3333333333333333333333333333333333333333"
821
+ .parse()
822
+ .expect("token"),
823
+ token_name: "USD Coin".to_string(),
824
+ token_version: Some(String::new()),
825
+ from: "0x4444444444444444444444444444444444444444"
826
+ .parse()
827
+ .expect("from"),
828
+ to: "0x5555555555555555555555555555555555555555"
829
+ .parse()
830
+ .expect("to"),
831
+ amount_wei: 456,
832
+ valid_after: 10,
833
+ valid_before: 20,
834
+ nonce_hex: format!("0x{}", hex::encode([0xabu8; 32])),
835
+ };
836
+ let _ = base_eip3009
837
+ .transfer_signing_hash()
838
+ .expect("empty version should be ignored");
839
+ assert!(matches!(
840
+ Eip3009Transfer {
841
+ chain_id: 0,
842
+ ..base_eip3009.clone()
843
+ }
844
+ .validate(),
845
+ Err(DomainError::InvalidChainId)
846
+ ));
847
+ assert!(matches!(
848
+ Eip3009Transfer {
849
+ amount_wei: 0,
850
+ ..base_eip3009.clone()
851
+ }
852
+ .validate(),
853
+ Err(DomainError::InvalidAmount)
854
+ ));
855
+ assert!(matches!(
856
+ Eip3009Transfer {
857
+ token_name: " ".to_string(),
858
+ ..base_eip3009.clone()
859
+ }
860
+ .validate(),
861
+ Err(DomainError::InvalidTypedDataDomain(_))
862
+ ));
863
+ assert!(matches!(
864
+ Eip3009Transfer {
865
+ valid_before: 10,
866
+ ..base_eip3009.clone()
867
+ }
868
+ .validate(),
869
+ Err(DomainError::InvalidAuthorizationWindow)
870
+ ));
871
+ assert!(matches!(
872
+ Eip3009Transfer {
873
+ nonce_hex: "0x1234".to_string(),
874
+ ..base_eip3009
875
+ }
876
+ .validate(),
877
+ Err(DomainError::InvalidTypedDataDomain(_))
878
+ ));
879
+ }
880
+
881
+ #[test]
882
+ fn broadcast_tx_validation_covers_remaining_error_paths() {
883
+ let base_tx = BroadcastTx {
884
+ chain_id: 1,
885
+ nonce: 0,
886
+ to: "0xf0109fc8df283027b6285cc889f5aa624eac1f55"
887
+ .parse()
888
+ .expect("to"),
889
+ value_wei: 0,
890
+ data_hex: "0x".to_string(),
891
+ gas_limit: 21_000,
892
+ max_fee_per_gas_wei: 1_000_000_000,
893
+ max_priority_fee_per_gas_wei: 1_000_000_000,
894
+ tx_type: 0x02,
895
+ delegation_enabled: false,
896
+ };
897
+
898
+ assert!(matches!(
899
+ BroadcastTx {
900
+ chain_id: 0,
901
+ ..base_tx.clone()
902
+ }
903
+ .validate(),
904
+ Err(DomainError::InvalidChainId)
905
+ ));
906
+ assert!(matches!(
907
+ BroadcastTx {
908
+ gas_limit: 0,
909
+ ..base_tx.clone()
910
+ }
911
+ .validate(),
912
+ Err(DomainError::InvalidGasConfiguration)
913
+ ));
914
+ assert!(matches!(
915
+ BroadcastTx {
916
+ max_priority_fee_per_gas_wei: 2_000_000_000,
917
+ ..base_tx.clone()
918
+ }
919
+ .validate(),
920
+ Err(DomainError::InvalidGasConfiguration)
921
+ ));
922
+ assert!(matches!(
923
+ BroadcastTx {
924
+ gas_limit: u64::MAX,
925
+ max_fee_per_gas_wei: u128::MAX,
926
+ ..base_tx.clone()
927
+ }
928
+ .validate(),
929
+ Err(DomainError::AmountOutOfRange)
930
+ ));
931
+ assert!(matches!(
932
+ base_tx
933
+ .clone()
934
+ .eip1559_signed_raw_transaction(2, [0u8; 32], [0u8; 32]),
935
+ Err(DomainError::InvalidSignatureParity)
936
+ ));
937
+ assert!(matches!(
938
+ BroadcastTx {
939
+ tx_type: EIP7702_TX_TYPE,
940
+ ..base_tx
941
+ }
942
+ .eip1559_signed_raw_transaction(1, [0u8; 32], [0u8; 32]),
943
+ Err(DomainError::UnsupportedTransactionType(_))
944
+ ));
945
+ }
946
+
947
+ #[test]
948
+ fn agent_action_helpers_cover_remaining_variants_and_none_paths() {
949
+ let approve = AgentAction::Approve {
950
+ chain_id: 1,
951
+ token: "0x1111111111111111111111111111111111111111"
952
+ .parse()
953
+ .expect("token"),
954
+ spender: "0x2222222222222222222222222222222222222222"
955
+ .parse()
956
+ .expect("spender"),
957
+ amount_wei: 12,
958
+ };
959
+ assert_eq!(
960
+ approve.recipient(),
961
+ "0x2222222222222222222222222222222222222222"
962
+ .parse()
963
+ .expect("spender")
964
+ );
965
+ assert_eq!(approve.max_fee_per_gas_wei(), None);
966
+ assert_eq!(approve.max_priority_fee_per_gas_wei(), None);
967
+ assert_eq!(approve.calldata_len_bytes(), None);
968
+ assert_eq!(approve.signing_hash().expect("non-typed action"), None);
969
+
970
+ let receive_authorization = Eip3009Transfer {
971
+ chain_id: 1,
972
+ token: "0x3333333333333333333333333333333333333333"
973
+ .parse()
974
+ .expect("token"),
975
+ token_name: "USD Coin".to_string(),
976
+ token_version: Some("2".to_string()),
977
+ from: "0x4444444444444444444444444444444444444444"
978
+ .parse()
979
+ .expect("from"),
980
+ to: "0x5555555555555555555555555555555555555555"
981
+ .parse()
982
+ .expect("to"),
983
+ amount_wei: 34,
984
+ valid_after: 10,
985
+ valid_before: 20,
986
+ nonce_hex: format!("0x{}", hex::encode([0xceu8; 32])),
987
+ };
988
+ let receive = AgentAction::Eip3009ReceiveWithAuthorization {
989
+ authorization: receive_authorization,
990
+ };
991
+ assert_eq!(
992
+ receive.asset(),
993
+ AssetId::Erc20(
994
+ "0x3333333333333333333333333333333333333333"
995
+ .parse()
996
+ .expect("token")
997
+ )
998
+ );
999
+ assert_eq!(
1000
+ receive.recipient(),
1001
+ "0x5555555555555555555555555555555555555555"
1002
+ .parse()
1003
+ .expect("to")
1004
+ );
1005
+
1006
+ assert!(matches!(
1007
+ AgentAction::TransferNative {
1008
+ chain_id: 1,
1009
+ to: "0x6666666666666666666666666666666666666666"
1010
+ .parse()
1011
+ .expect("to"),
1012
+ amount_wei: 0,
1013
+ }
1014
+ .validate(),
1015
+ Err(DomainError::InvalidAmount)
1016
+ ));
1017
+ assert!(matches!(
1018
+ AgentAction::TransferNative {
1019
+ chain_id: 0,
1020
+ to: "0x6666666666666666666666666666666666666666"
1021
+ .parse()
1022
+ .expect("to"),
1023
+ amount_wei: 1,
1024
+ }
1025
+ .validate(),
1026
+ Err(DomainError::InvalidChainId)
1027
+ ));
1028
+ }
1029
+
1030
+ #[test]
1031
+ fn action_from_erc20_calldata_covers_transfer_and_zero_chain_guard() {
1032
+ let to = alloy_primitives::Address::from([0x11; 20]);
1033
+ let calldata = transferCall {
1034
+ to,
1035
+ value: U256::from(42_u64),
1036
+ }
1037
+ .abi_encode();
1038
+ let token: EvmAddress = "0x3333333333333333333333333333333333333333"
1039
+ .parse()
1040
+ .expect("token");
1041
+
1042
+ assert!(matches!(
1043
+ action_from_erc20_calldata(0, token.clone(), &calldata),
1044
+ Err(DomainError::InvalidChainId)
1045
+ ));
1046
+ assert_eq!(
1047
+ action_from_erc20_calldata(1, token.clone(), &calldata).expect("transfer action"),
1048
+ AgentAction::Transfer {
1049
+ chain_id: 1,
1050
+ token,
1051
+ to: "0x1111111111111111111111111111111111111111"
1052
+ .parse()
1053
+ .expect("to"),
1054
+ amount_wei: 42,
1055
+ }
1056
+ );
1057
+ }
1058
+
1059
+ #[test]
1060
+ fn broadcast_action_derives_approve_and_receive_with_authorization_scope() {
1061
+ let approve_calldata = approveCall {
1062
+ spender: alloy_primitives::Address::from([0x77; 20]),
1063
+ value: U256::from(88_u64),
1064
+ }
1065
+ .abi_encode();
1066
+ let approve_action = AgentAction::BroadcastTx {
1067
+ tx: BroadcastTx {
1068
+ chain_id: 1,
1069
+ nonce: 0,
1070
+ to: "0x8888000000000000000000000000000000000000"
1071
+ .parse()
1072
+ .expect("token"),
1073
+ value_wei: 0,
1074
+ data_hex: format!("0x{}", hex::encode(approve_calldata)),
1075
+ gas_limit: 80_000,
1076
+ max_fee_per_gas_wei: 1_000_000_000,
1077
+ max_priority_fee_per_gas_wei: 1_000_000_000,
1078
+ tx_type: 0x02,
1079
+ delegation_enabled: false,
1080
+ },
1081
+ };
1082
+ assert_eq!(
1083
+ approve_action.recipient(),
1084
+ "0x7777777777777777777777777777777777777777"
1085
+ .parse()
1086
+ .expect("spender")
1087
+ );
1088
+ assert_eq!(approve_action.amount_wei(), 88);
1089
+
1090
+ let receive_calldata = receiveWithAuthorizationCall {
1091
+ from: alloy_primitives::Address::from([0x11; 20]),
1092
+ to: alloy_primitives::Address::from([0x22; 20]),
1093
+ value: U256::from(99_u64),
1094
+ validAfter: U256::from(1_u64),
1095
+ validBefore: U256::from(2_u64),
1096
+ nonce: [0x44; 32].into(),
1097
+ signature: vec![0xaa, 0xbb].into(),
1098
+ }
1099
+ .abi_encode();
1100
+ let receive_action = AgentAction::BroadcastTx {
1101
+ tx: BroadcastTx {
1102
+ chain_id: 1,
1103
+ nonce: 0,
1104
+ to: "0x9999000000000000000000000000000000000000"
1105
+ .parse()
1106
+ .expect("token"),
1107
+ value_wei: 0,
1108
+ data_hex: format!("0x{}", hex::encode(receive_calldata)),
1109
+ gas_limit: 90_000,
1110
+ max_fee_per_gas_wei: 1_000_000_000,
1111
+ max_priority_fee_per_gas_wei: 1_000_000_000,
1112
+ tx_type: 0x02,
1113
+ delegation_enabled: false,
1114
+ },
1115
+ };
1116
+ assert_eq!(
1117
+ receive_action.recipient(),
1118
+ "0x2222222222222222222222222222222222222222"
1119
+ .parse()
1120
+ .expect("recipient")
1121
+ );
1122
+ assert_eq!(receive_action.amount_wei(), 99);
1123
+ }
1124
+
1125
+ #[test]
1126
+ fn broadcast_tx_supports_long_rlp_payloads_and_rejects_out_of_range_token_amounts() {
1127
+ let tx = BroadcastTx {
1128
+ chain_id: 1,
1129
+ nonce: 0,
1130
+ to: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1131
+ .parse()
1132
+ .expect("to"),
1133
+ value_wei: 0,
1134
+ data_hex: format!("0x{}", "11".repeat(64)),
1135
+ gas_limit: 21_000,
1136
+ max_fee_per_gas_wei: 1_000_000_000,
1137
+ max_priority_fee_per_gas_wei: 1_000_000_000,
1138
+ tx_type: 0x02,
1139
+ delegation_enabled: false,
1140
+ };
1141
+ let raw = tx
1142
+ .eip1559_signed_raw_transaction(1, [0x11; 32], [0x22; 32])
1143
+ .expect("long payload should encode");
1144
+ assert_eq!(raw.first().copied(), Some(0x02));
1145
+
1146
+ let oversized_approve = approveCall {
1147
+ spender: alloy_primitives::Address::from([0x33; 20]),
1148
+ value: U256::from(u128::MAX) + U256::from(1u8),
1149
+ }
1150
+ .abi_encode();
1151
+ assert!(matches!(
1152
+ parse_erc20_call(&oversized_approve),
1153
+ Err(DomainError::AmountOutOfRange)
1154
+ ));
1155
+
1156
+ let oversized_permit = permitCall {
1157
+ owner: alloy_primitives::Address::from([0x66; 20]),
1158
+ permitSingle: PermitSingle {
1159
+ details: PermitDetails {
1160
+ token: alloy_primitives::Address::from([0x44; 20]),
1161
+ amount: alloy_primitives::U160::MAX,
1162
+ expiration: alloy_primitives::aliases::U48::from_be_slice(&100u64.to_be_bytes()[2..]),
1163
+ nonce: alloy_primitives::aliases::U48::from_be_slice(&7u64.to_be_bytes()[2..]),
1164
+ },
1165
+ spender: alloy_primitives::Address::from([0x55; 20]),
1166
+ sigDeadline: U256::from(1234u64),
1167
+ },
1168
+ signature: vec![0x12, 0x34].into(),
1169
+ }
1170
+ .abi_encode();
1171
+ let action = AgentAction::BroadcastTx {
1172
+ tx: BroadcastTx {
1173
+ chain_id: 1,
1174
+ nonce: 0,
1175
+ to: "0x9999000000000000000000000000000000000000"
1176
+ .parse()
1177
+ .expect("permit2 contract"),
1178
+ value_wei: 0,
1179
+ data_hex: format!("0x{}", hex::encode(oversized_permit)),
1180
+ gas_limit: 60_000,
1181
+ max_fee_per_gas_wei: 1_000_000_000,
1182
+ max_priority_fee_per_gas_wei: 1_000_000_000,
1183
+ tx_type: 0x02,
1184
+ delegation_enabled: false,
1185
+ },
1186
+ };
1187
+ assert_eq!(action.asset(), AssetId::NativeEth);
1188
+ }
1189
+
1190
+ #[test]
1191
+ fn invalid_hex_payloads_are_rejected_for_transactions_and_typed_nonces() {
1192
+ let tx = BroadcastTx {
1193
+ chain_id: 1,
1194
+ nonce: 0,
1195
+ to: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1196
+ .parse()
1197
+ .expect("to"),
1198
+ value_wei: 0,
1199
+ data_hex: "0x123".to_string(),
1200
+ gas_limit: 21_000,
1201
+ max_fee_per_gas_wei: 1_000_000_000,
1202
+ max_priority_fee_per_gas_wei: 1_000_000_000,
1203
+ tx_type: 0x02,
1204
+ delegation_enabled: false,
1205
+ };
1206
+ assert!(matches!(
1207
+ tx.validate(),
1208
+ Err(DomainError::InvalidTransactionDataHex)
1209
+ ));
1210
+
1211
+ let authorization = Eip3009Transfer {
1212
+ chain_id: 1,
1213
+ token: "0x3333333333333333333333333333333333333333"
1214
+ .parse()
1215
+ .expect("token"),
1216
+ token_name: "USD Coin".to_string(),
1217
+ token_version: Some("2".to_string()),
1218
+ from: "0x4444444444444444444444444444444444444444"
1219
+ .parse()
1220
+ .expect("from"),
1221
+ to: "0x5555555555555555555555555555555555555555"
1222
+ .parse()
1223
+ .expect("to"),
1224
+ amount_wei: 456,
1225
+ valid_after: 10,
1226
+ valid_before: 20,
1227
+ nonce_hex: "0xzz".to_string(),
1228
+ };
1229
+ assert!(matches!(
1230
+ authorization.validate(),
1231
+ Err(DomainError::InvalidTypedDataDomain(_))
1232
+ ));
1233
+ }
1234
+
620
1235
  #[test]
621
1236
  fn nonce_reservation_request_debug_redacts_agent_auth_token() {
622
1237
  let request = NonceReservationRequest {
@@ -625,6 +1240,7 @@ fn nonce_reservation_request_debug_redacts_agent_auth_token() {
625
1240
  agent_auth_token: "super-secret-token".to_string(),
626
1241
  chain_id: 1,
627
1242
  min_nonce: 7,
1243
+ exact_nonce: false,
628
1244
  requested_at: OffsetDateTime::UNIX_EPOCH,
629
1245
  expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(2),
630
1246
  };