@wlfi-agent/cli 1.4.12 → 1.4.14

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 (289) hide show
  1. package/Cargo.lock +3968 -0
  2. package/Cargo.toml +50 -0
  3. package/README.md +426 -6
  4. package/crates/vault-cli-admin/Cargo.toml +26 -0
  5. package/crates/vault-cli-admin/src/io_utils.rs +500 -0
  6. package/crates/vault-cli-admin/src/main.rs +3990 -0
  7. package/crates/vault-cli-admin/src/shared_config.rs +624 -0
  8. package/crates/vault-cli-admin/src/tui/amounts.rs +180 -0
  9. package/crates/vault-cli-admin/src/tui/token_rpc.rs +250 -0
  10. package/crates/vault-cli-admin/src/tui/utils.rs +82 -0
  11. package/crates/vault-cli-admin/src/tui.rs +3410 -0
  12. package/crates/vault-cli-agent/Cargo.toml +24 -0
  13. package/crates/vault-cli-agent/src/io_utils.rs +576 -0
  14. package/crates/vault-cli-agent/src/main.rs +833 -0
  15. package/crates/vault-cli-daemon/Cargo.toml +28 -0
  16. package/crates/vault-cli-daemon/src/bin/wlfi-agent-system-keychain.rs +216 -0
  17. package/crates/vault-cli-daemon/src/main.rs +644 -0
  18. package/crates/vault-cli-daemon/src/relay_sync.rs +894 -0
  19. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +167 -0
  20. package/crates/vault-daemon/Cargo.toml +32 -0
  21. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +1041 -0
  22. package/crates/vault-daemon/src/daemon_parts/core_helpers.rs +1256 -0
  23. package/crates/vault-daemon/src/daemon_parts/types_api_rpc.rs +622 -0
  24. package/crates/vault-daemon/src/lib.rs +54 -0
  25. package/crates/vault-daemon/src/persistence.rs +441 -0
  26. package/crates/vault-daemon/src/tests.rs +237 -0
  27. package/crates/vault-daemon/src/tests_parts/part1.rs +1224 -0
  28. package/crates/vault-daemon/src/tests_parts/part2.rs +1021 -0
  29. package/crates/vault-daemon/src/tests_parts/part3.rs +835 -0
  30. package/crates/vault-daemon/src/tests_parts/part4.rs +604 -0
  31. package/crates/vault-domain/Cargo.toml +20 -0
  32. package/crates/vault-domain/src/action.rs +849 -0
  33. package/crates/vault-domain/src/address.rs +51 -0
  34. package/crates/vault-domain/src/approval.rs +90 -0
  35. package/crates/vault-domain/src/constants.rs +4 -0
  36. package/crates/vault-domain/src/error.rs +54 -0
  37. package/crates/vault-domain/src/keys.rs +71 -0
  38. package/crates/vault-domain/src/lib.rs +42 -0
  39. package/crates/vault-domain/src/nonce.rs +102 -0
  40. package/crates/vault-domain/src/policy.rs +172 -0
  41. package/crates/vault-domain/src/request.rs +53 -0
  42. package/crates/vault-domain/src/scope.rs +24 -0
  43. package/crates/vault-domain/src/session.rs +50 -0
  44. package/crates/vault-domain/src/signature.rs +34 -0
  45. package/crates/vault-domain/src/tests.rs +651 -0
  46. package/crates/vault-domain/src/u128_as_decimal_string.rs +44 -0
  47. package/crates/vault-policy/Cargo.toml +17 -0
  48. package/crates/vault-policy/src/engine.rs +301 -0
  49. package/crates/vault-policy/src/error.rs +81 -0
  50. package/crates/vault-policy/src/lib.rs +17 -0
  51. package/crates/vault-policy/src/report.rs +34 -0
  52. package/crates/vault-policy/src/tests.rs +891 -0
  53. package/crates/vault-policy/src/tests_explain.rs +78 -0
  54. package/crates/vault-sdk-agent/Cargo.toml +21 -0
  55. package/crates/vault-sdk-agent/src/lib.rs +711 -0
  56. package/crates/vault-signer/Cargo.toml +25 -0
  57. package/crates/vault-signer/src/lib.rs +731 -0
  58. package/crates/vault-signer/tests/secure_enclave_acl.rs +54 -0
  59. package/crates/vault-transport-unix/Cargo.toml +24 -0
  60. package/crates/vault-transport-unix/src/lib.rs +1640 -0
  61. package/crates/vault-transport-xpc/Cargo.toml +25 -0
  62. package/crates/vault-transport-xpc/src/client_codec_api.rs +635 -0
  63. package/crates/vault-transport-xpc/src/lib.rs +680 -0
  64. package/crates/vault-transport-xpc/src/tests.rs +818 -0
  65. package/crates/vault-transport-xpc/tests/e2e_flow.rs +773 -0
  66. package/dist/cli.cjs +35088 -0
  67. package/dist/cli.cjs.map +1 -0
  68. package/package.json +49 -43
  69. package/packages/cache/.turbo/turbo-build.log +52 -0
  70. package/packages/cache/dist/chunk-2QFWMUXT.cjs +43 -0
  71. package/packages/cache/dist/chunk-2QFWMUXT.cjs.map +1 -0
  72. package/packages/cache/dist/chunk-4U63TZTQ.js +43 -0
  73. package/packages/cache/dist/chunk-4U63TZTQ.js.map +1 -0
  74. package/packages/cache/dist/chunk-ALQ6H7KG.cjs +404 -0
  75. package/packages/cache/dist/chunk-ALQ6H7KG.cjs.map +1 -0
  76. package/packages/cache/dist/chunk-FGJEEF5N.js +404 -0
  77. package/packages/cache/dist/chunk-FGJEEF5N.js.map +1 -0
  78. package/packages/cache/dist/chunk-UYNEHZHB.cjs +45 -0
  79. package/packages/cache/dist/chunk-UYNEHZHB.cjs.map +1 -0
  80. package/packages/cache/dist/chunk-VXVMPG3W.js +45 -0
  81. package/packages/cache/dist/chunk-VXVMPG3W.js.map +1 -0
  82. package/packages/cache/dist/client/index.cjs +11 -0
  83. package/packages/cache/dist/client/index.cjs.map +1 -0
  84. package/packages/cache/dist/client/index.d.cts +15 -0
  85. package/packages/cache/dist/client/index.d.ts +15 -0
  86. package/packages/cache/dist/client/index.js +11 -0
  87. package/packages/cache/dist/client/index.js.map +1 -0
  88. package/packages/cache/dist/errors/index.cjs +11 -0
  89. package/packages/cache/dist/errors/index.cjs.map +1 -0
  90. package/packages/cache/dist/errors/index.d.cts +26 -0
  91. package/packages/cache/dist/errors/index.d.ts +26 -0
  92. package/packages/cache/dist/errors/index.js +11 -0
  93. package/packages/cache/dist/errors/index.js.map +1 -0
  94. package/packages/cache/dist/index.cjs +29 -0
  95. package/packages/cache/dist/index.cjs.map +1 -0
  96. package/packages/cache/dist/index.d.cts +4 -0
  97. package/packages/cache/dist/index.d.ts +4 -0
  98. package/packages/cache/dist/index.js +29 -0
  99. package/packages/cache/dist/index.js.map +1 -0
  100. package/packages/cache/dist/service/index.cjs +15 -0
  101. package/packages/cache/dist/service/index.cjs.map +1 -0
  102. package/packages/cache/dist/service/index.d.cts +184 -0
  103. package/packages/cache/dist/service/index.d.ts +184 -0
  104. package/packages/cache/dist/service/index.js +15 -0
  105. package/packages/cache/dist/service/index.js.map +1 -0
  106. package/packages/cache/node_modules/.bin/jiti +17 -0
  107. package/packages/cache/node_modules/.bin/tsc +17 -0
  108. package/packages/cache/node_modules/.bin/tsserver +17 -0
  109. package/packages/cache/node_modules/.bin/tsup +17 -0
  110. package/packages/cache/node_modules/.bin/tsup-node +17 -0
  111. package/packages/cache/node_modules/.bin/tsx +17 -0
  112. package/packages/cache/node_modules/.bin/vitest +17 -0
  113. package/packages/cache/package.json +48 -0
  114. package/packages/cache/src/client/index.ts +56 -0
  115. package/packages/cache/src/errors/index.ts +53 -0
  116. package/packages/cache/src/index.ts +3 -0
  117. package/packages/cache/src/service/index.test.ts +263 -0
  118. package/packages/cache/src/service/index.ts +678 -0
  119. package/packages/cache/tsconfig.json +13 -0
  120. package/packages/cache/tsup.config.ts +13 -0
  121. package/packages/cache/vitest.config.ts +16 -0
  122. package/packages/config/.turbo/turbo-build.log +18 -0
  123. package/packages/config/dist/index.cjs +1037 -0
  124. package/packages/config/dist/index.cjs.map +1 -0
  125. package/packages/config/dist/index.d.ts +131 -0
  126. package/packages/config/node_modules/.bin/jiti +17 -0
  127. package/packages/config/node_modules/.bin/tsc +17 -0
  128. package/packages/config/node_modules/.bin/tsserver +17 -0
  129. package/packages/config/node_modules/.bin/tsup +17 -0
  130. package/packages/config/node_modules/.bin/tsup-node +17 -0
  131. package/packages/config/node_modules/.bin/tsx +17 -0
  132. package/packages/config/package.json +21 -0
  133. package/packages/config/src/index.js +1 -0
  134. package/packages/config/src/index.ts +1282 -0
  135. package/packages/config/tsconfig.json +4 -0
  136. package/packages/rpc/.turbo/turbo-build.log +32 -0
  137. package/packages/rpc/dist/_esm-BCLXDO2R.cjs +3660 -0
  138. package/packages/rpc/dist/_esm-BCLXDO2R.cjs.map +1 -0
  139. package/packages/rpc/dist/ccip-OWJLAW55.cjs +16 -0
  140. package/packages/rpc/dist/ccip-OWJLAW55.cjs.map +1 -0
  141. package/packages/rpc/dist/chunk-APQIFZ3B.cjs +6247 -0
  142. package/packages/rpc/dist/chunk-APQIFZ3B.cjs.map +1 -0
  143. package/packages/rpc/dist/chunk-CDO2GWRD.cjs +410 -0
  144. package/packages/rpc/dist/chunk-CDO2GWRD.cjs.map +1 -0
  145. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs +2249 -0
  146. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs.map +1 -0
  147. package/packages/rpc/dist/chunk-TZDTAHWR.cjs +44 -0
  148. package/packages/rpc/dist/chunk-TZDTAHWR.cjs.map +1 -0
  149. package/packages/rpc/dist/index.cjs +7342 -0
  150. package/packages/rpc/dist/index.cjs.map +1 -0
  151. package/packages/rpc/dist/index.d.ts +3857 -0
  152. package/packages/rpc/dist/secp256k1-WCNM675D.cjs +18 -0
  153. package/packages/rpc/dist/secp256k1-WCNM675D.cjs.map +1 -0
  154. package/packages/rpc/node_modules/.bin/jiti +17 -0
  155. package/packages/rpc/node_modules/.bin/tsc +17 -0
  156. package/packages/rpc/node_modules/.bin/tsserver +17 -0
  157. package/packages/rpc/node_modules/.bin/tsup +17 -0
  158. package/packages/rpc/node_modules/.bin/tsup-node +17 -0
  159. package/packages/rpc/node_modules/.bin/tsx +17 -0
  160. package/packages/rpc/package.json +25 -0
  161. package/packages/rpc/src/index.ts +206 -0
  162. package/packages/rpc/tsconfig.json +4 -0
  163. package/packages/typescript/base.json +36 -0
  164. package/packages/typescript/nextjs.json +17 -0
  165. package/packages/typescript/package.json +10 -0
  166. package/packages/ui/.turbo/turbo-build.log +44 -0
  167. package/packages/ui/dist/chunk-MOAFBKSA.js +11 -0
  168. package/packages/ui/dist/chunk-MOAFBKSA.js.map +1 -0
  169. package/packages/ui/dist/components/badge.d.ts +12 -0
  170. package/packages/ui/dist/components/badge.js +31 -0
  171. package/packages/ui/dist/components/badge.js.map +1 -0
  172. package/packages/ui/dist/components/button.d.ts +13 -0
  173. package/packages/ui/dist/components/button.js +40 -0
  174. package/packages/ui/dist/components/button.js.map +1 -0
  175. package/packages/ui/dist/components/card.d.ts +10 -0
  176. package/packages/ui/dist/components/card.js +39 -0
  177. package/packages/ui/dist/components/card.js.map +1 -0
  178. package/packages/ui/dist/components/input.d.ts +5 -0
  179. package/packages/ui/dist/components/input.js +28 -0
  180. package/packages/ui/dist/components/input.js.map +1 -0
  181. package/packages/ui/dist/components/label.d.ts +5 -0
  182. package/packages/ui/dist/components/label.js +13 -0
  183. package/packages/ui/dist/components/label.js.map +1 -0
  184. package/packages/ui/dist/components/separator.d.ts +5 -0
  185. package/packages/ui/dist/components/separator.js +13 -0
  186. package/packages/ui/dist/components/separator.js.map +1 -0
  187. package/packages/ui/dist/components/textarea.d.ts +5 -0
  188. package/packages/ui/dist/components/textarea.js +27 -0
  189. package/packages/ui/dist/components/textarea.js.map +1 -0
  190. package/packages/ui/dist/tailwind.d.ts +56 -0
  191. package/packages/ui/dist/tailwind.js +60 -0
  192. package/packages/ui/dist/tailwind.js.map +1 -0
  193. package/packages/ui/dist/utils/cn.d.ts +5 -0
  194. package/packages/ui/dist/utils/cn.js +7 -0
  195. package/packages/ui/dist/utils/cn.js.map +1 -0
  196. package/packages/ui/node_modules/.bin/jiti +17 -0
  197. package/packages/ui/node_modules/.bin/tsc +17 -0
  198. package/packages/ui/node_modules/.bin/tsserver +17 -0
  199. package/packages/ui/node_modules/.bin/tsup +17 -0
  200. package/packages/ui/node_modules/.bin/tsup-node +17 -0
  201. package/packages/ui/node_modules/.bin/tsx +17 -0
  202. package/packages/ui/package.json +69 -0
  203. package/packages/ui/src/components/badge.tsx +27 -0
  204. package/packages/ui/src/components/button.tsx +40 -0
  205. package/packages/ui/src/components/card.tsx +31 -0
  206. package/packages/ui/src/components/input.tsx +21 -0
  207. package/packages/ui/src/components/label.tsx +6 -0
  208. package/packages/ui/src/components/separator.tsx +6 -0
  209. package/packages/ui/src/components/textarea.tsx +20 -0
  210. package/packages/ui/src/globals.css +70 -0
  211. package/packages/ui/src/tailwind.ts +56 -0
  212. package/packages/ui/src/utils/cn.ts +6 -0
  213. package/packages/ui/tsconfig.json +20 -0
  214. package/packages/ui/tsup.config.ts +20 -0
  215. package/pnpm-workspace.yaml +4 -0
  216. package/scripts/install-rust-binaries.mjs +84 -0
  217. package/scripts/launchd/install-user-daemon.sh +358 -0
  218. package/scripts/launchd/run-vault-daemon.sh +5 -0
  219. package/scripts/launchd/run-wlfi-agent-daemon.sh +73 -0
  220. package/scripts/launchd/uninstall-user-daemon.sh +103 -0
  221. package/src/cli.ts +2121 -0
  222. package/src/lib/admin-guard.js +1 -0
  223. package/src/lib/admin-guard.ts +185 -0
  224. package/src/lib/admin-passthrough.ts +33 -0
  225. package/src/lib/admin-reset.ts +751 -0
  226. package/src/lib/admin-setup.ts +1612 -0
  227. package/src/lib/agent-auth-clear.js +1 -0
  228. package/src/lib/agent-auth-clear.ts +58 -0
  229. package/src/lib/agent-auth-forwarding.js +1 -0
  230. package/src/lib/agent-auth-forwarding.ts +149 -0
  231. package/src/lib/agent-auth-migrate.js +1 -0
  232. package/src/lib/agent-auth-migrate.ts +150 -0
  233. package/src/lib/agent-auth-revoke.ts +103 -0
  234. package/src/lib/agent-auth-rotate.ts +107 -0
  235. package/src/lib/agent-auth-token.js +1 -0
  236. package/src/lib/agent-auth-token.ts +25 -0
  237. package/src/lib/agent-auth.ts +89 -0
  238. package/src/lib/asset-broadcast.js +1 -0
  239. package/src/lib/asset-broadcast.ts +285 -0
  240. package/src/lib/bootstrap-artifacts.js +1 -0
  241. package/src/lib/bootstrap-artifacts.ts +205 -0
  242. package/src/lib/bootstrap-credentials.js +1 -0
  243. package/src/lib/bootstrap-credentials.ts +832 -0
  244. package/src/lib/config-amounts.js +1 -0
  245. package/src/lib/config-amounts.ts +189 -0
  246. package/src/lib/config-mutation.ts +27 -0
  247. package/src/lib/fs-trust.js +1 -0
  248. package/src/lib/fs-trust.ts +537 -0
  249. package/src/lib/keychain.js +1 -0
  250. package/src/lib/keychain.ts +225 -0
  251. package/src/lib/local-admin-access.ts +106 -0
  252. package/src/lib/network-selection.js +1 -0
  253. package/src/lib/network-selection.ts +71 -0
  254. package/src/lib/passthrough-security.js +1 -0
  255. package/src/lib/passthrough-security.ts +114 -0
  256. package/src/lib/rpc-guard.js +1 -0
  257. package/src/lib/rpc-guard.ts +7 -0
  258. package/src/lib/rust-spawn-options.js +1 -0
  259. package/src/lib/rust-spawn-options.ts +98 -0
  260. package/src/lib/rust.js +1 -0
  261. package/src/lib/rust.ts +143 -0
  262. package/src/lib/signed-tx.js +1 -0
  263. package/src/lib/signed-tx.ts +116 -0
  264. package/src/lib/status-repair-cli.ts +116 -0
  265. package/src/lib/sudo.js +1 -0
  266. package/src/lib/sudo.ts +172 -0
  267. package/src/lib/vault-password-forwarding.js +1 -0
  268. package/src/lib/vault-password-forwarding.ts +155 -0
  269. package/src/lib/wallet-profile.js +1 -0
  270. package/src/lib/wallet-profile.ts +332 -0
  271. package/src/lib/wallet-repair.js +1 -0
  272. package/src/lib/wallet-repair.ts +304 -0
  273. package/src/lib/wallet-setup.js +1 -0
  274. package/src/lib/wallet-setup.ts +1466 -0
  275. package/src/lib/wallet-status.js +1 -0
  276. package/src/lib/wallet-status.ts +640 -0
  277. package/tsconfig.base.json +17 -0
  278. package/tsconfig.json +10 -0
  279. package/tsup.config.ts +25 -0
  280. package/turbo.json +41 -0
  281. package/LICENSE.md +0 -1
  282. package/dist/wlfa/index.cjs +0 -250
  283. package/dist/wlfa/index.d.cts +0 -1
  284. package/dist/wlfa/index.d.ts +0 -1
  285. package/dist/wlfa/index.js +0 -250
  286. package/dist/wlfc/index.cjs +0 -1894
  287. package/dist/wlfc/index.d.cts +0 -1
  288. package/dist/wlfc/index.d.ts +0 -1
  289. package/dist/wlfc/index.js +0 -1894
@@ -0,0 +1,835 @@
1
+ #[tokio::test]
2
+ async fn broadcast_unknown_selector_is_allowed_when_policies_allow_it() {
3
+ let daemon = InMemoryDaemon::new(
4
+ "vault-password",
5
+ SoftwareSignerBackend::default(),
6
+ DaemonConfig::default(),
7
+ )
8
+ .expect("daemon");
9
+
10
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
11
+ let session = AdminSession {
12
+ vault_password: "vault-password".to_string(),
13
+ lease,
14
+ };
15
+
16
+ daemon
17
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
18
+ .await
19
+ .expect("add per-tx policy");
20
+ daemon
21
+ .add_policy(&session, policy_per_chain_gas(1, 1_000_000_000_000_000))
22
+ .await
23
+ .expect("add gas policy");
24
+
25
+ let key = daemon
26
+ .create_vault_key(&session, KeyCreateRequest::Generate)
27
+ .await
28
+ .expect("key");
29
+ let agent_credentials = daemon
30
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
31
+ .await
32
+ .expect("agent");
33
+
34
+ let request = sign_request(
35
+ &agent_credentials,
36
+ AgentAction::BroadcastTx {
37
+ tx: BroadcastTx {
38
+ chain_id: 1,
39
+ nonce: 0,
40
+ to: "0x1000000000000000000000000000000000000000"
41
+ .parse()
42
+ .expect("to"),
43
+ value_wei: 1,
44
+ data_hex: "0xdeadbeef".to_string(),
45
+ gas_limit: 21_000,
46
+ max_fee_per_gas_wei: 1_000_000_000,
47
+ max_priority_fee_per_gas_wei: 1_000_000_000,
48
+ tx_type: 0x02,
49
+ delegation_enabled: false,
50
+ },
51
+ },
52
+ );
53
+ reserve_nonce_for_agent(&daemon, &agent_credentials, 1, 0).await;
54
+
55
+ let signature = daemon.sign_for_agent(request).await.expect("must sign");
56
+ assert!(!signature.bytes.is_empty(), "signature should be non-empty");
57
+ }
58
+
59
+ #[tokio::test]
60
+ async fn eip1559_broadcast_signature_artifacts_are_consistent() {
61
+ let daemon = InMemoryDaemon::new(
62
+ "vault-password",
63
+ SoftwareSignerBackend::default(),
64
+ DaemonConfig::default(),
65
+ )
66
+ .expect("daemon");
67
+
68
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
69
+ let session = AdminSession {
70
+ vault_password: "vault-password".to_string(),
71
+ lease,
72
+ };
73
+
74
+ daemon
75
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
76
+ .await
77
+ .expect("add per-tx policy");
78
+ daemon
79
+ .add_policy(&session, policy_per_chain_gas(1, 1_000_000_000_000_000))
80
+ .await
81
+ .expect("add gas policy");
82
+
83
+ let key = daemon
84
+ .create_vault_key(&session, KeyCreateRequest::Generate)
85
+ .await
86
+ .expect("key");
87
+ let agent_credentials = daemon
88
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
89
+ .await
90
+ .expect("agent");
91
+
92
+ let tx = BroadcastTx {
93
+ chain_id: 1,
94
+ nonce: 7,
95
+ to: "0x1000000000000000000000000000000000000000"
96
+ .parse()
97
+ .expect("to"),
98
+ value_wei: 123,
99
+ data_hex: "0x".to_string(),
100
+ gas_limit: 21_000,
101
+ max_fee_per_gas_wei: 1_000_000_000,
102
+ max_priority_fee_per_gas_wei: 100_000_000,
103
+ tx_type: 0x02,
104
+ delegation_enabled: false,
105
+ };
106
+ let request = sign_request(
107
+ &agent_credentials,
108
+ AgentAction::BroadcastTx { tx: tx.clone() },
109
+ );
110
+ reserve_nonce_for_agent(&daemon, &agent_credentials, tx.chain_id, tx.nonce).await;
111
+
112
+ let signature = daemon.sign_for_agent(request).await.expect("must sign");
113
+ let r_hex = signature.r_hex.expect("eip1559 r must be present");
114
+ let s_hex = signature.s_hex.expect("eip1559 s must be present");
115
+ let v = signature.v.expect("eip1559 v must be present");
116
+ let raw_tx_hex = signature
117
+ .raw_tx_hex
118
+ .expect("eip1559 raw tx bytes must be present");
119
+ let tx_hash_hex = signature
120
+ .tx_hash_hex
121
+ .expect("eip1559 tx hash must be present");
122
+ assert!(v <= 1, "typed tx y-parity must be 0 or 1");
123
+
124
+ let parsed_der = K256Signature::from_der(&signature.bytes).expect("DER signature");
125
+ let compact = parsed_der.to_bytes();
126
+ assert_eq!(r_hex, format!("0x{}", hex::encode(&compact[..32])));
127
+ assert_eq!(s_hex, format!("0x{}", hex::encode(&compact[32..])));
128
+
129
+ let raw_tx = hex::decode(
130
+ raw_tx_hex
131
+ .strip_prefix("0x")
132
+ .expect("raw tx must be 0x-prefixed"),
133
+ )
134
+ .expect("raw tx hex");
135
+ assert_eq!(raw_tx.first().copied(), Some(0x02));
136
+ assert_eq!(
137
+ tx_hash_hex,
138
+ format!("0x{}", hex::encode(keccak256(&raw_tx).0))
139
+ );
140
+
141
+ let signing_message = tx.eip1559_signing_message().expect("signing message");
142
+ let digest = keccak256(&signing_message).0;
143
+ let recovery_id = RecoveryId::from_byte(v as u8).expect("valid recovery id");
144
+ let recovered = VerifyingKey::recover_from_prehash(&digest, &parsed_der, recovery_id)
145
+ .expect("recover verifying key");
146
+ let expected_verifying_key = VerifyingKey::from_sec1_bytes(
147
+ &hex::decode(&key.public_key_hex).expect("public key hex"),
148
+ )
149
+ .expect("public key bytes");
150
+ assert_eq!(recovered, expected_verifying_key);
151
+ }
152
+
153
+ #[tokio::test]
154
+ async fn non_eip1559_broadcast_is_rejected_for_signing() {
155
+ let daemon = InMemoryDaemon::new(
156
+ "vault-password",
157
+ SoftwareSignerBackend::default(),
158
+ DaemonConfig::default(),
159
+ )
160
+ .expect("daemon");
161
+
162
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
163
+ let session = AdminSession {
164
+ vault_password: "vault-password".to_string(),
165
+ lease,
166
+ };
167
+
168
+ daemon
169
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
170
+ .await
171
+ .expect("add per-tx policy");
172
+
173
+ let key = daemon
174
+ .create_vault_key(&session, KeyCreateRequest::Generate)
175
+ .await
176
+ .expect("key");
177
+ let agent_credentials = daemon
178
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
179
+ .await
180
+ .expect("agent");
181
+
182
+ let request = sign_request(
183
+ &agent_credentials,
184
+ AgentAction::BroadcastTx {
185
+ tx: BroadcastTx {
186
+ chain_id: 1,
187
+ nonce: 0,
188
+ to: "0x4000000000000000000000000000000000000000"
189
+ .parse()
190
+ .expect("to"),
191
+ value_wei: 0,
192
+ data_hex: "0x".to_string(),
193
+ gas_limit: 21_000,
194
+ max_fee_per_gas_wei: 1_000_000_000,
195
+ max_priority_fee_per_gas_wei: 1_000_000_000,
196
+ tx_type: 0x04,
197
+ delegation_enabled: false,
198
+ },
199
+ },
200
+ );
201
+ reserve_nonce_for_agent(&daemon, &agent_credentials, 1, 0).await;
202
+
203
+ let err = daemon
204
+ .sign_for_agent(request)
205
+ .await
206
+ .expect_err("non-eip1559 broadcast tx must be rejected");
207
+ assert!(matches!(
208
+ err,
209
+ DaemonError::Signer(vault_signer::SignerError::Unsupported(message))
210
+ if message.contains("unsupported for signing")
211
+ ));
212
+ }
213
+
214
+ #[tokio::test]
215
+ async fn broadcast_with_delegation_is_rejected_even_for_eip7702() {
216
+ let daemon = InMemoryDaemon::new(
217
+ "vault-password",
218
+ SoftwareSignerBackend::default(),
219
+ DaemonConfig::default(),
220
+ )
221
+ .expect("daemon");
222
+
223
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
224
+ let session = AdminSession {
225
+ vault_password: "vault-password".to_string(),
226
+ lease,
227
+ };
228
+
229
+ daemon
230
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
231
+ .await
232
+ .expect("add policy");
233
+
234
+ let key = daemon
235
+ .create_vault_key(&session, KeyCreateRequest::Generate)
236
+ .await
237
+ .expect("key");
238
+ let agent_credentials = daemon
239
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
240
+ .await
241
+ .expect("agent");
242
+
243
+ let request = sign_request(
244
+ &agent_credentials,
245
+ AgentAction::BroadcastTx {
246
+ tx: BroadcastTx {
247
+ chain_id: 1,
248
+ nonce: 0,
249
+ to: "0x2000000000000000000000000000000000000000"
250
+ .parse()
251
+ .expect("to"),
252
+ value_wei: 0,
253
+ data_hex: "0x".to_string(),
254
+ gas_limit: 21_000,
255
+ max_fee_per_gas_wei: 1_000_000_000,
256
+ max_priority_fee_per_gas_wei: 1_000_000_000,
257
+ tx_type: 0x02,
258
+ delegation_enabled: true,
259
+ },
260
+ },
261
+ );
262
+ let err = daemon
263
+ .sign_for_agent(request)
264
+ .await
265
+ .expect_err("must reject delegation even for eip-7702");
266
+ assert!(matches!(err, DaemonError::PayloadActionMismatch));
267
+ }
268
+
269
+ #[tokio::test]
270
+ async fn per_chain_gas_policy_rejects_over_limit_broadcast() {
271
+ let daemon = InMemoryDaemon::new(
272
+ "vault-password",
273
+ SoftwareSignerBackend::default(),
274
+ DaemonConfig::default(),
275
+ )
276
+ .expect("daemon");
277
+
278
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
279
+ let session = AdminSession {
280
+ vault_password: "vault-password".to_string(),
281
+ lease,
282
+ };
283
+
284
+ daemon
285
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
286
+ .await
287
+ .expect("add per-tx policy");
288
+ daemon
289
+ .add_policy(&session, policy_per_chain_gas(1, 500_000_000_000_000))
290
+ .await
291
+ .expect("add gas policy");
292
+
293
+ let key = daemon
294
+ .create_vault_key(&session, KeyCreateRequest::Generate)
295
+ .await
296
+ .expect("key");
297
+ let agent_credentials = daemon
298
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
299
+ .await
300
+ .expect("agent");
301
+
302
+ let request = sign_request(
303
+ &agent_credentials,
304
+ AgentAction::BroadcastTx {
305
+ tx: BroadcastTx {
306
+ chain_id: 1,
307
+ nonce: 0,
308
+ to: "0x3000000000000000000000000000000000000000"
309
+ .parse()
310
+ .expect("to"),
311
+ value_wei: 0,
312
+ data_hex: "0xdeadbeef".to_string(),
313
+ gas_limit: 1_000_000,
314
+ max_fee_per_gas_wei: 1_000_000_000,
315
+ max_priority_fee_per_gas_wei: 1_000_000_000,
316
+ tx_type: 0x02,
317
+ delegation_enabled: false,
318
+ },
319
+ },
320
+ );
321
+
322
+ let err = daemon
323
+ .sign_for_agent(request)
324
+ .await
325
+ .expect_err("gas policy must reject oversized tx");
326
+ assert!(matches!(
327
+ err,
328
+ DaemonError::Policy(PolicyError::GasLimitExceeded { .. })
329
+ ));
330
+ }
331
+
332
+ #[tokio::test]
333
+ async fn broadcast_fee_and_calldata_policies_are_enforced() {
334
+ let daemon = InMemoryDaemon::new(
335
+ "vault-password",
336
+ SoftwareSignerBackend::default(),
337
+ DaemonConfig::default(),
338
+ )
339
+ .expect("daemon");
340
+
341
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
342
+ let session = AdminSession {
343
+ vault_password: "vault-password".to_string(),
344
+ lease,
345
+ };
346
+
347
+ daemon
348
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
349
+ .await
350
+ .expect("add per-tx spend policy");
351
+ daemon
352
+ .add_policy(
353
+ &session,
354
+ SpendingPolicy::new(
355
+ 1,
356
+ PolicyType::PerTxMaxFeePerGas,
357
+ 1_000_000_000,
358
+ EntityScope::All,
359
+ EntityScope::All,
360
+ EntityScope::All,
361
+ )
362
+ .expect("policy"),
363
+ )
364
+ .await
365
+ .expect("add max-fee policy");
366
+ daemon
367
+ .add_policy(
368
+ &session,
369
+ SpendingPolicy::new(
370
+ 2,
371
+ PolicyType::PerTxMaxPriorityFeePerGas,
372
+ 500_000_000,
373
+ EntityScope::All,
374
+ EntityScope::All,
375
+ EntityScope::All,
376
+ )
377
+ .expect("policy"),
378
+ )
379
+ .await
380
+ .expect("add priority-fee policy");
381
+ daemon
382
+ .add_policy(
383
+ &session,
384
+ SpendingPolicy::new(
385
+ 3,
386
+ PolicyType::PerTxMaxCalldataBytes,
387
+ 3,
388
+ EntityScope::All,
389
+ EntityScope::All,
390
+ EntityScope::All,
391
+ )
392
+ .expect("policy"),
393
+ )
394
+ .await
395
+ .expect("add calldata policy");
396
+
397
+ let key = daemon
398
+ .create_vault_key(&session, KeyCreateRequest::Generate)
399
+ .await
400
+ .expect("key");
401
+ let agent_credentials = daemon
402
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
403
+ .await
404
+ .expect("agent");
405
+
406
+ let fee_err = daemon
407
+ .sign_for_agent(sign_request(
408
+ &agent_credentials,
409
+ AgentAction::BroadcastTx {
410
+ tx: BroadcastTx {
411
+ chain_id: 1,
412
+ nonce: 0,
413
+ to: "0x3000000000000000000000000000000000000000"
414
+ .parse()
415
+ .expect("to"),
416
+ value_wei: 0,
417
+ data_hex: "0x".to_string(),
418
+ gas_limit: 21_000,
419
+ max_fee_per_gas_wei: 2_000_000_000,
420
+ max_priority_fee_per_gas_wei: 100_000_000,
421
+ tx_type: 0x02,
422
+ delegation_enabled: false,
423
+ },
424
+ },
425
+ ))
426
+ .await
427
+ .expect_err("max fee per gas must reject request");
428
+ assert!(matches!(
429
+ fee_err,
430
+ DaemonError::Policy(PolicyError::MaxFeePerGasLimitExceeded { .. })
431
+ ));
432
+
433
+ let priority_err = daemon
434
+ .sign_for_agent(sign_request(
435
+ &agent_credentials,
436
+ AgentAction::BroadcastTx {
437
+ tx: BroadcastTx {
438
+ chain_id: 1,
439
+ nonce: 0,
440
+ to: "0x3000000000000000000000000000000000000000"
441
+ .parse()
442
+ .expect("to"),
443
+ value_wei: 0,
444
+ data_hex: "0x".to_string(),
445
+ gas_limit: 21_000,
446
+ max_fee_per_gas_wei: 1_000_000_000,
447
+ max_priority_fee_per_gas_wei: 600_000_000,
448
+ tx_type: 0x02,
449
+ delegation_enabled: false,
450
+ },
451
+ },
452
+ ))
453
+ .await
454
+ .expect_err("max priority fee per gas must reject request");
455
+ assert!(matches!(
456
+ priority_err,
457
+ DaemonError::Policy(PolicyError::PriorityFeePerGasLimitExceeded { .. })
458
+ ));
459
+
460
+ let calldata_err = daemon
461
+ .sign_for_agent(sign_request(
462
+ &agent_credentials,
463
+ AgentAction::BroadcastTx {
464
+ tx: BroadcastTx {
465
+ chain_id: 1,
466
+ nonce: 0,
467
+ to: "0x3000000000000000000000000000000000000000"
468
+ .parse()
469
+ .expect("to"),
470
+ value_wei: 0,
471
+ data_hex: "0xdeadbeef".to_string(),
472
+ gas_limit: 21_000,
473
+ max_fee_per_gas_wei: 1_000_000_000,
474
+ max_priority_fee_per_gas_wei: 100_000_000,
475
+ tx_type: 0x02,
476
+ delegation_enabled: false,
477
+ },
478
+ },
479
+ ))
480
+ .await
481
+ .expect_err("calldata bytes limit must reject request");
482
+ assert!(matches!(
483
+ calldata_err,
484
+ DaemonError::Policy(PolicyError::CalldataBytesLimitExceeded { .. })
485
+ ));
486
+ }
487
+
488
+ #[tokio::test]
489
+ async fn daily_tx_count_policy_is_enforced() {
490
+ let daemon = InMemoryDaemon::new(
491
+ "vault-password",
492
+ SoftwareSignerBackend::default(),
493
+ DaemonConfig::default(),
494
+ )
495
+ .expect("daemon");
496
+
497
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
498
+ let session = AdminSession {
499
+ vault_password: "vault-password".to_string(),
500
+ lease,
501
+ };
502
+
503
+ daemon
504
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
505
+ .await
506
+ .expect("add per-tx spend policy");
507
+ daemon
508
+ .add_policy(
509
+ &session,
510
+ SpendingPolicy::new(
511
+ 1,
512
+ PolicyType::DailyMaxTxCount,
513
+ 1,
514
+ EntityScope::All,
515
+ EntityScope::All,
516
+ EntityScope::All,
517
+ )
518
+ .expect("policy"),
519
+ )
520
+ .await
521
+ .expect("add tx count policy");
522
+
523
+ let key = daemon
524
+ .create_vault_key(&session, KeyCreateRequest::Generate)
525
+ .await
526
+ .expect("key");
527
+ let agent_credentials = daemon
528
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
529
+ .await
530
+ .expect("agent");
531
+
532
+ daemon
533
+ .sign_for_agent(sign_request(
534
+ &agent_credentials,
535
+ AgentAction::Transfer {
536
+ chain_id: 1,
537
+ token: "0x1000000000000000000000000000000000000000"
538
+ .parse()
539
+ .expect("token"),
540
+ to: "0x2000000000000000000000000000000000000000"
541
+ .parse()
542
+ .expect("recipient"),
543
+ amount_wei: 1,
544
+ },
545
+ ))
546
+ .await
547
+ .expect("first tx should pass");
548
+
549
+ let err = daemon
550
+ .sign_for_agent(sign_request(
551
+ &agent_credentials,
552
+ AgentAction::Transfer {
553
+ chain_id: 1,
554
+ token: "0x1000000000000000000000000000000000000000"
555
+ .parse()
556
+ .expect("token"),
557
+ to: "0x2000000000000000000000000000000000000000"
558
+ .parse()
559
+ .expect("recipient"),
560
+ amount_wei: 1,
561
+ },
562
+ ))
563
+ .await
564
+ .expect_err("daily tx count must reject second tx");
565
+ assert!(matches!(
566
+ err,
567
+ DaemonError::Policy(PolicyError::TxCountLimitExceeded { .. })
568
+ ));
569
+ }
570
+
571
+ #[tokio::test]
572
+ async fn fee_policy_allows_non_broadcast_actions_without_tx_metadata() {
573
+ let daemon = InMemoryDaemon::new(
574
+ "vault-password",
575
+ SoftwareSignerBackend::default(),
576
+ DaemonConfig::default(),
577
+ )
578
+ .expect("daemon");
579
+
580
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
581
+ let session = AdminSession {
582
+ vault_password: "vault-password".to_string(),
583
+ lease,
584
+ };
585
+
586
+ daemon
587
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
588
+ .await
589
+ .expect("add spend policy");
590
+ daemon
591
+ .add_policy(
592
+ &session,
593
+ SpendingPolicy::new(
594
+ 1,
595
+ PolicyType::PerTxMaxFeePerGas,
596
+ 1_000_000_000,
597
+ EntityScope::All,
598
+ EntityScope::All,
599
+ EntityScope::All,
600
+ )
601
+ .expect("policy"),
602
+ )
603
+ .await
604
+ .expect("add fee policy");
605
+
606
+ let key = daemon
607
+ .create_vault_key(&session, KeyCreateRequest::Generate)
608
+ .await
609
+ .expect("key");
610
+ let agent_credentials = daemon
611
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
612
+ .await
613
+ .expect("agent");
614
+
615
+ daemon
616
+ .sign_for_agent(sign_request(
617
+ &agent_credentials,
618
+ AgentAction::Transfer {
619
+ chain_id: 1,
620
+ token: "0x1000000000000000000000000000000000000000"
621
+ .parse()
622
+ .expect("token"),
623
+ to: "0x2000000000000000000000000000000000000000"
624
+ .parse()
625
+ .expect("recipient"),
626
+ amount_wei: 1,
627
+ },
628
+ ))
629
+ .await
630
+ .expect("non-broadcast action should skip fee metadata policy");
631
+ }
632
+
633
+ #[tokio::test]
634
+ async fn gas_policy_allows_non_broadcast_actions_without_tx_metadata() {
635
+ let daemon = InMemoryDaemon::new(
636
+ "vault-password",
637
+ SoftwareSignerBackend::default(),
638
+ DaemonConfig::default(),
639
+ )
640
+ .expect("daemon");
641
+
642
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
643
+ let session = AdminSession {
644
+ vault_password: "vault-password".to_string(),
645
+ lease,
646
+ };
647
+
648
+ daemon
649
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
650
+ .await
651
+ .expect("add spend policy");
652
+ daemon
653
+ .add_policy(
654
+ &session,
655
+ SpendingPolicy::new(
656
+ 1,
657
+ PolicyType::PerChainMaxGasSpend,
658
+ 1_000_000_000_000_000,
659
+ EntityScope::All,
660
+ EntityScope::All,
661
+ EntityScope::All,
662
+ )
663
+ .expect("policy"),
664
+ )
665
+ .await
666
+ .expect("add gas policy");
667
+
668
+ let key = daemon
669
+ .create_vault_key(&session, KeyCreateRequest::Generate)
670
+ .await
671
+ .expect("key");
672
+ let agent_credentials = daemon
673
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
674
+ .await
675
+ .expect("agent");
676
+
677
+ daemon
678
+ .sign_for_agent(sign_request(
679
+ &agent_credentials,
680
+ AgentAction::Transfer {
681
+ chain_id: 1,
682
+ token: "0x1000000000000000000000000000000000000000"
683
+ .parse()
684
+ .expect("token"),
685
+ to: "0x2000000000000000000000000000000000000000"
686
+ .parse()
687
+ .expect("recipient"),
688
+ amount_wei: 1,
689
+ },
690
+ ))
691
+ .await
692
+ .expect("non-broadcast action should skip gas metadata policy");
693
+ }
694
+
695
+ #[tokio::test]
696
+ async fn permit2_signature_artifacts_are_recoverable() {
697
+ let daemon = InMemoryDaemon::new(
698
+ "vault-password",
699
+ SoftwareSignerBackend::default(),
700
+ DaemonConfig::default(),
701
+ )
702
+ .expect("daemon");
703
+
704
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
705
+ let session = AdminSession {
706
+ vault_password: "vault-password".to_string(),
707
+ lease,
708
+ };
709
+ daemon
710
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
711
+ .await
712
+ .expect("add policy");
713
+
714
+ let key = daemon
715
+ .create_vault_key(&session, KeyCreateRequest::Generate)
716
+ .await
717
+ .expect("key");
718
+ let agent_credentials = daemon
719
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
720
+ .await
721
+ .expect("agent");
722
+
723
+ let action = AgentAction::Permit2Permit {
724
+ permit: vault_domain::Permit2Permit {
725
+ chain_id: 1,
726
+ permit2_contract: "0x000000000022d473030f116ddee9f6b43ac78ba3"
727
+ .parse()
728
+ .expect("permit2"),
729
+ token: "0x1000000000000000000000000000000000000000"
730
+ .parse()
731
+ .expect("token"),
732
+ spender: "0x2000000000000000000000000000000000000000"
733
+ .parse()
734
+ .expect("spender"),
735
+ amount_wei: 42,
736
+ expiration: 100,
737
+ nonce: 1,
738
+ sig_deadline: 200,
739
+ },
740
+ };
741
+
742
+ let signature = daemon
743
+ .sign_for_agent(sign_request(&agent_credentials, action.clone()))
744
+ .await
745
+ .expect("must sign permit2");
746
+ let parsed_der = K256Signature::from_der(&signature.bytes).expect("DER signature");
747
+ let digest = action
748
+ .signing_hash()
749
+ .expect("typed hash")
750
+ .expect("typed action");
751
+ let recovery_id = RecoveryId::from_byte(signature.v.expect("v") as u8).expect("recovery id");
752
+ let recovered = VerifyingKey::recover_from_prehash(&digest, &parsed_der, recovery_id)
753
+ .expect("recover verifying key");
754
+ let expected_verifying_key = VerifyingKey::from_sec1_bytes(
755
+ &hex::decode(&key.public_key_hex).expect("public key hex"),
756
+ )
757
+ .expect("public key bytes");
758
+ assert_eq!(recovered, expected_verifying_key);
759
+ assert!(signature.raw_tx_hex.is_none());
760
+ assert!(signature.tx_hash_hex.is_none());
761
+ assert!(signature.r_hex.is_some());
762
+ assert!(signature.s_hex.is_some());
763
+ }
764
+
765
+ #[tokio::test]
766
+ async fn eip3009_signature_artifacts_are_recoverable() {
767
+ let daemon = InMemoryDaemon::new(
768
+ "vault-password",
769
+ SoftwareSignerBackend::default(),
770
+ DaemonConfig::default(),
771
+ )
772
+ .expect("daemon");
773
+
774
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
775
+ let session = AdminSession {
776
+ vault_password: "vault-password".to_string(),
777
+ lease,
778
+ };
779
+ daemon
780
+ .add_policy(&session, policy_all_per_tx(1_000_000_000_000_000_000))
781
+ .await
782
+ .expect("add policy");
783
+
784
+ let key = daemon
785
+ .create_vault_key(&session, KeyCreateRequest::Generate)
786
+ .await
787
+ .expect("key");
788
+ let agent_credentials = daemon
789
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
790
+ .await
791
+ .expect("agent");
792
+
793
+ let action = AgentAction::Eip3009TransferWithAuthorization {
794
+ authorization: vault_domain::Eip3009Transfer {
795
+ chain_id: 1,
796
+ token: "0x3000000000000000000000000000000000000000"
797
+ .parse()
798
+ .expect("token"),
799
+ token_name: "USD Coin".to_string(),
800
+ token_version: Some("2".to_string()),
801
+ from: "0x4000000000000000000000000000000000000000"
802
+ .parse()
803
+ .expect("from"),
804
+ to: "0x5000000000000000000000000000000000000000"
805
+ .parse()
806
+ .expect("to"),
807
+ amount_wei: 77,
808
+ valid_after: 10,
809
+ valid_before: 20,
810
+ nonce_hex: format!("0x{}", hex::encode([0x11u8; 32])),
811
+ },
812
+ };
813
+
814
+ let signature = daemon
815
+ .sign_for_agent(sign_request(&agent_credentials, action.clone()))
816
+ .await
817
+ .expect("must sign eip3009");
818
+ let parsed_der = K256Signature::from_der(&signature.bytes).expect("DER signature");
819
+ let digest = action
820
+ .signing_hash()
821
+ .expect("typed hash")
822
+ .expect("typed action");
823
+ let recovery_id = RecoveryId::from_byte(signature.v.expect("v") as u8).expect("recovery id");
824
+ let recovered = VerifyingKey::recover_from_prehash(&digest, &parsed_der, recovery_id)
825
+ .expect("recover verifying key");
826
+ let expected_verifying_key = VerifyingKey::from_sec1_bytes(
827
+ &hex::decode(&key.public_key_hex).expect("public key hex"),
828
+ )
829
+ .expect("public key bytes");
830
+ assert_eq!(recovered, expected_verifying_key);
831
+ assert!(signature.raw_tx_hex.is_none());
832
+ assert!(signature.tx_hash_hex.is_none());
833
+ assert!(signature.r_hex.is_some());
834
+ assert!(signature.s_hex.is_some());
835
+ }