clawpowers 2.2.5 → 2.2.7

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 (138) hide show
  1. package/CHANGELOG.md +186 -160
  2. package/COMPATIBILITY.md +48 -13
  3. package/KNOWN_LIMITATIONS.md +20 -19
  4. package/LICENSE +44 -44
  5. package/LICENSING.md +10 -10
  6. package/README.md +486 -462
  7. package/SECURITY.md +52 -52
  8. package/dist/index.d.ts +17 -5
  9. package/dist/index.js +187 -92
  10. package/dist/index.js.map +1 -1
  11. package/native/Cargo.lock +4927 -4927
  12. package/native/Cargo.toml +73 -73
  13. package/native/crates/canonical/Cargo.toml +24 -24
  14. package/native/crates/canonical/src/lib.rs +677 -673
  15. package/native/crates/compression/Cargo.toml +20 -20
  16. package/native/crates/compression/benches/compression_bench.rs +42 -42
  17. package/native/crates/compression/src/lib.rs +393 -393
  18. package/native/crates/evm-eth/Cargo.toml +13 -13
  19. package/native/crates/evm-eth/src/lib.rs +105 -105
  20. package/native/crates/fee/Cargo.toml +15 -15
  21. package/native/crates/fee/src/lib.rs +281 -281
  22. package/native/crates/index/Cargo.toml +16 -16
  23. package/native/crates/index/src/lib.rs +277 -277
  24. package/native/crates/policy/Cargo.toml +17 -17
  25. package/native/crates/policy/src/lib.rs +614 -614
  26. package/native/crates/security/Cargo.toml +22 -22
  27. package/native/crates/security/src/lib.rs +478 -478
  28. package/native/crates/tokens/Cargo.toml +13 -13
  29. package/native/crates/tokens/src/lib.rs +534 -534
  30. package/native/crates/verification/Cargo.toml +23 -23
  31. package/native/crates/verification/src/lib.rs +333 -333
  32. package/native/crates/wallet/Cargo.toml +20 -20
  33. package/native/crates/wallet/src/lib.rs +261 -261
  34. package/native/crates/x402/Cargo.toml +30 -30
  35. package/native/crates/x402/src/lib.rs +423 -423
  36. package/native/ffi/Cargo.toml +34 -34
  37. package/native/ffi/build.rs +4 -4
  38. package/native/ffi/src/lib.rs +352 -352
  39. package/native/ffi/tests/integration.rs +354 -354
  40. package/native/pyo3/Cargo.toml +26 -26
  41. package/native/pyo3/pyproject.toml +16 -16
  42. package/native/pyo3/src/lib.rs +407 -407
  43. package/native/pyo3/tests/test_smoke.py +180 -180
  44. package/native/wasm/Cargo.toml +47 -44
  45. package/native/wasm/pkg/.gitignore +6 -6
  46. package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -208
  47. package/native/wasm/pkg/clawpowers_wasm.js +872 -872
  48. package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -40
  49. package/native/wasm/pkg/package.json +16 -16
  50. package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -143
  51. package/native/wasm/pkg-node/clawpowers_wasm.js +798 -798
  52. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -40
  53. package/native/wasm/pkg-node/package.json +12 -12
  54. package/native/wasm/src/lib.rs +433 -433
  55. package/package.json +13 -8
  56. package/scripts/build-wasm.mjs +59 -0
  57. package/scripts/generate_hermes_wrappers.py +211 -0
  58. package/scripts/hermes_wrapper_overrides.json +184 -0
  59. package/scripts/run-python-script.mjs +48 -0
  60. package/scripts/verify-consumer-install.mjs +109 -0
  61. package/scripts/verify-wasm-artifacts.mjs +26 -3
  62. package/scripts/verify_hermes_wrappers.py +154 -0
  63. package/skill.json +20 -0
  64. package/skills/1password/SKILL.md +34 -0
  65. package/skills/README.md +44 -0
  66. package/skills/agent-nexus-2/SKILL.md +34 -0
  67. package/skills/apple-notes/SKILL.md +34 -0
  68. package/skills/apple-reminders/SKILL.md +34 -0
  69. package/skills/autoresearch/SKILL.md +43 -0
  70. package/skills/bear-notes/SKILL.md +34 -0
  71. package/skills/blogwatcher/SKILL.md +34 -0
  72. package/skills/blucli/SKILL.md +34 -0
  73. package/skills/bluebubbles/SKILL.md +34 -0
  74. package/skills/business-strategy/SKILL.md +41 -0
  75. package/skills/camsnap/SKILL.md +34 -0
  76. package/skills/canvas/SKILL.md +34 -0
  77. package/skills/clawhub/SKILL.md +34 -0
  78. package/skills/coding-agent/SKILL.md +34 -0
  79. package/skills/coding-discipline.skill/SKILL.md +34 -0
  80. package/skills/content-writer/SKILL.md +41 -0
  81. package/skills/discord/SKILL.md +34 -0
  82. package/skills/eightctl/SKILL.md +34 -0
  83. package/skills/execution-validation.skill/SKILL.md +34 -0
  84. package/skills/gemini/SKILL.md +34 -0
  85. package/skills/gh-issues/SKILL.md +34 -0
  86. package/skills/gifgrep/SKILL.md +34 -0
  87. package/skills/github/SKILL.md +41 -0
  88. package/skills/gog/SKILL.md +34 -0
  89. package/skills/goplaces/SKILL.md +34 -0
  90. package/skills/healthcheck/SKILL.md +34 -0
  91. package/skills/himalaya/SKILL.md +34 -0
  92. package/skills/humanize/SKILL.md +41 -0
  93. package/skills/imsg/SKILL.md +34 -0
  94. package/skills/itp/SKILL.md +112 -0
  95. package/skills/mcporter/SKILL.md +34 -0
  96. package/skills/model-usage/SKILL.md +34 -0
  97. package/skills/nano-pdf/SKILL.md +34 -0
  98. package/skills/node-connect/SKILL.md +34 -0
  99. package/skills/notion/SKILL.md +34 -0
  100. package/skills/obsidian/SKILL.md +34 -0
  101. package/skills/openai-whisper/SKILL.md +34 -0
  102. package/skills/openai-whisper-api/SKILL.md +34 -0
  103. package/skills/openhue/SKILL.md +34 -0
  104. package/skills/oracle/SKILL.md +34 -0
  105. package/skills/ordercli/SKILL.md +34 -0
  106. package/skills/peekaboo/SKILL.md +34 -0
  107. package/skills/polyclaw/SKILL.md +34 -0
  108. package/skills/prospector/SKILL.md +41 -0
  109. package/skills/rsi.skill/SKILL.md +34 -0
  110. package/skills/sag/SKILL.md +34 -0
  111. package/skills/security/SKILL.md +41 -0
  112. package/skills/session-logs/SKILL.md +34 -0
  113. package/skills/sherpa-onnx-tts/SKILL.md +34 -0
  114. package/skills/skill-creator/SKILL.md +34 -0
  115. package/skills/slack/SKILL.md +34 -0
  116. package/skills/songsee/SKILL.md +34 -0
  117. package/skills/sonoscli/SKILL.md +34 -0
  118. package/skills/spotify-player/SKILL.md +34 -0
  119. package/skills/strykr-prism/SKILL.md +41 -0
  120. package/skills/summarize/SKILL.md +34 -0
  121. package/skills/taskbridge/SKILL.md +34 -0
  122. package/skills/things-mac/SKILL.md +34 -0
  123. package/skills/tmux/SKILL.md +34 -0
  124. package/skills/trello/SKILL.md +34 -0
  125. package/skills/validator-agent/SKILL.md +41 -0
  126. package/skills/video-frames/SKILL.md +34 -0
  127. package/skills/voice-call/SKILL.md +34 -0
  128. package/skills/wacli/SKILL.md +34 -0
  129. package/skills/weather/SKILL.md +34 -0
  130. package/skills/webmcp-payments/SKILL.md +41 -0
  131. package/skills/xurl/SKILL.md +34 -0
  132. package/src/skills/catalog.ts +435 -435
  133. package/src/skills/executor.ts +56 -56
  134. package/src/skills/index.ts +3 -3
  135. package/src/skills/itp/SKILL.md +112 -112
  136. package/src/skills/loader.ts +262 -193
  137. package/native/ffi/index.node +0 -0
  138. package/native/wasm/pkg-node/.gitignore +0 -6
@@ -1,20 +1,20 @@
1
- [package]
2
- name = "clawpowers-wallet"
3
- version.workspace = true
4
- edition.workspace = true
5
- license.workspace = true
6
-
7
- [dependencies]
8
- alloy-signer = { workspace = true }
9
- alloy-signer-local = { workspace = true }
10
- alloy-primitives = { workspace = true }
11
- serde = { workspace = true }
12
- serde_json = { workspace = true }
13
- zeroize = { workspace = true }
14
- thiserror = { workspace = true }
15
- tracing = { workspace = true }
16
- uuid = { workspace = true }
17
- rand = { workspace = true }
18
-
19
- [dev-dependencies]
20
- tokio = { workspace = true }
1
+ [package]
2
+ name = "clawpowers-wallet"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+
7
+ [dependencies]
8
+ alloy-signer = { workspace = true }
9
+ alloy-signer-local = { workspace = true }
10
+ alloy-primitives = { workspace = true }
11
+ serde = { workspace = true }
12
+ serde_json = { workspace = true }
13
+ zeroize = { workspace = true }
14
+ thiserror = { workspace = true }
15
+ tracing = { workspace = true }
16
+ uuid = { workspace = true }
17
+ rand = { workspace = true }
18
+
19
+ [dev-dependencies]
20
+ tokio = { workspace = true }
@@ -1,261 +1,261 @@
1
- //! Key management for the ClawPowers agent wallet system.
2
- //!
3
- //! Provides [`AgentWallet`] — a thin wrapper around an `alloy-signer-local`
4
- //! [`PrivateKeySigner`] that adds identity metadata and guarantees private-key
5
- //! scrubbing on drop via [`Zeroize`].
6
-
7
- use alloy_primitives::{Address, Signature};
8
- use alloy_signer::SignerSync;
9
- use alloy_signer_local::PrivateKeySigner;
10
- use std::time::{SystemTime, UNIX_EPOCH};
11
- use thiserror::Error;
12
- use uuid::Uuid;
13
- use zeroize::Zeroize;
14
-
15
- // ── WalletError ───────────────────────────────────────────────────────────────
16
-
17
- /// Errors produced by wallet operations.
18
- #[derive(Debug, Error)]
19
- pub enum WalletError {
20
- /// Failed to parse or import a private key.
21
- #[error("Invalid private key: {0}")]
22
- InvalidPrivateKey(String),
23
- /// Signing operation failed.
24
- #[error("Signing failed: {0}")]
25
- SigningFailed(String),
26
- }
27
-
28
- // ── AgentWallet ───────────────────────────────────────────────────────────────
29
-
30
- /// An agent-controlled Ethereum wallet.
31
- ///
32
- /// Wraps a [`PrivateKeySigner`] together with a unique wallet ID and creation
33
- /// timestamp. On drop the inner signing key is zeroed in memory via the
34
- /// `ZeroizeOnDrop` impl already present on `k256::ecdsa::SigningKey`.
35
- ///
36
- /// # Example
37
- /// ```
38
- /// use clawpowers_wallet::AgentWallet;
39
- /// let wallet = AgentWallet::generate();
40
- /// println!("address: {}", wallet.address());
41
- /// ```
42
- pub struct AgentWallet {
43
- signer: PrivateKeySigner,
44
- /// Unique, stable identifier for this wallet instance.
45
- pub wallet_id: Uuid,
46
- /// Unix timestamp (seconds since epoch) when this wallet was created.
47
- pub created_at_secs: u64,
48
- /// Holds a zeroed copy of the raw private key bytes for the explicit
49
- /// `Zeroize` impl below. Cleared on every call to `zeroize()`.
50
- key_bytes: [u8; 32],
51
- }
52
-
53
- impl std::fmt::Debug for AgentWallet {
54
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55
- f.debug_struct("AgentWallet")
56
- .field("wallet_id", &self.wallet_id)
57
- .field("created_at_secs", &self.created_at_secs)
58
- .field("address", &self.signer.address())
59
- .finish_non_exhaustive()
60
- }
61
- }
62
-
63
- impl AgentWallet {
64
- /// Generates a brand-new random keypair.
65
- pub fn generate() -> Self {
66
- let signer = PrivateKeySigner::random();
67
- let key_bytes = signer.credential().to_bytes().into();
68
- Self {
69
- signer,
70
- wallet_id: Uuid::new_v4(),
71
- created_at_secs: Self::now_secs(),
72
- key_bytes,
73
- }
74
- }
75
-
76
- /// Imports an existing private key from a hex string (with or without `0x`
77
- /// prefix).
78
- ///
79
- /// # Errors
80
- /// Returns [`WalletError::InvalidPrivateKey`] if the hex is invalid or does
81
- /// not represent a valid secp256k1 scalar.
82
- pub fn from_private_key(hex: &str) -> Result<Self, WalletError> {
83
- let trimmed = hex.trim().strip_prefix("0x").unwrap_or(hex.trim());
84
- let signer = trimmed
85
- .parse::<PrivateKeySigner>()
86
- .map_err(|e| WalletError::InvalidPrivateKey(e.to_string()))?;
87
- let key_bytes = signer.credential().to_bytes().into();
88
- Ok(Self {
89
- signer,
90
- wallet_id: Uuid::new_v4(),
91
- created_at_secs: Self::now_secs(),
92
- key_bytes,
93
- })
94
- }
95
-
96
- fn now_secs() -> u64 {
97
- SystemTime::now()
98
- .duration_since(UNIX_EPOCH)
99
- .unwrap_or_default()
100
- .as_secs()
101
- }
102
-
103
- /// Returns the Ethereum address corresponding to this wallet's public key.
104
- pub fn address(&self) -> Address {
105
- self.signer.address()
106
- }
107
-
108
- /// Signs an arbitrary byte payload using EIP-191 message hashing.
109
- ///
110
- /// # Errors
111
- /// Returns [`WalletError::SigningFailed`] on cryptographic errors.
112
- pub fn sign_message(&self, msg: &[u8]) -> Result<Signature, WalletError> {
113
- self.signer
114
- .sign_message_sync(msg)
115
- .map_err(|e| WalletError::SigningFailed(e.to_string()))
116
- }
117
- }
118
-
119
- impl Zeroize for AgentWallet {
120
- /// Zeroes the cached private-key bytes held in this struct.
121
- ///
122
- /// The inner `PrivateKeySigner` (and thereby the `k256::ecdsa::SigningKey`)
123
- /// automatically zeroes its own memory on drop via `ZeroizeOnDrop`.
124
- fn zeroize(&mut self) {
125
- self.key_bytes.zeroize();
126
- }
127
- }
128
-
129
- impl Drop for AgentWallet {
130
- fn drop(&mut self) {
131
- self.zeroize();
132
- }
133
- }
134
-
135
- // ── Tests ─────────────────────────────────────────────────────────────────────
136
-
137
- #[cfg(test)]
138
- mod tests {
139
- use super::*;
140
- use alloy_primitives::eip191_hash_message;
141
-
142
- /// Helper: known test private key from Ethereum test vectors.
143
- const TEST_PRIVKEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
144
-
145
- #[test]
146
- fn test_generate_produces_valid_address() {
147
- let w = AgentWallet::generate();
148
- // Address must not be the zero address.
149
- assert_ne!(w.address(), Address::ZERO);
150
- }
151
-
152
- #[test]
153
- fn test_generate_unique_ids() {
154
- let w1 = AgentWallet::generate();
155
- let w2 = AgentWallet::generate();
156
- assert_ne!(w1.wallet_id, w2.wallet_id);
157
- }
158
-
159
- #[test]
160
- fn test_generate_different_keys() {
161
- let w1 = AgentWallet::generate();
162
- let w2 = AgentWallet::generate();
163
- assert_ne!(w1.address(), w2.address());
164
- }
165
-
166
- #[test]
167
- fn test_from_private_key_valid() {
168
- let w = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
169
- // Known address for the Hardhat account #0 key.
170
- let expected: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
171
- .parse()
172
- .expect("parse address");
173
- assert_eq!(w.address(), expected);
174
- }
175
-
176
- #[test]
177
- fn test_from_private_key_without_0x_prefix() {
178
- let hex_no_prefix = TEST_PRIVKEY.trim_start_matches("0x");
179
- let w = AgentWallet::from_private_key(hex_no_prefix).expect("should accept no-0x form");
180
- let expected: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
181
- .parse()
182
- .expect("parse address");
183
- assert_eq!(w.address(), expected);
184
- }
185
-
186
- #[test]
187
- fn test_from_private_key_invalid_returns_err() {
188
- let result = AgentWallet::from_private_key("not_a_hex_key");
189
- assert!(result.is_err());
190
- assert!(matches!(result, Err(WalletError::InvalidPrivateKey(_))));
191
- }
192
-
193
- #[test]
194
- fn test_sign_message_produces_signature() {
195
- let w = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
196
- let msg = b"hello clawpowers";
197
- let sig = w.sign_message(msg).expect("sign should succeed");
198
- // Verify the signature recovers to the correct address.
199
- let recovered = sig
200
- .recover_address_from_prehash(&eip191_hash_message(msg))
201
- .expect("recover address");
202
- assert_eq!(recovered, w.address());
203
- }
204
-
205
- #[test]
206
- fn test_sign_message_deterministic_for_same_key() {
207
- let w1 = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
208
- let w2 = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
209
- let msg = b"determinism test";
210
- let s1 = w1.sign_message(msg).expect("sign 1");
211
- let s2 = w2.sign_message(msg).expect("sign 2");
212
- assert_eq!(s1, s2);
213
- }
214
-
215
- #[test]
216
- fn test_zeroize_clears_key_bytes() {
217
- let mut w = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
218
- // Before zeroize, key_bytes should be non-zero.
219
- let nonzero_before = w.key_bytes.iter().any(|&b| b != 0);
220
- assert!(
221
- nonzero_before,
222
- "key_bytes should be non-zero before zeroize"
223
- );
224
-
225
- w.zeroize();
226
- let all_zero = w.key_bytes.iter().all(|&b| b == 0);
227
- assert!(
228
- all_zero,
229
- "key_bytes should be zeroed after explicit zeroize"
230
- );
231
- }
232
-
233
- #[test]
234
- fn test_debug_does_not_expose_private_key() {
235
- let w = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
236
- let debug_str = format!("{w:?}");
237
- // Make sure the raw private key hex is not in the debug output.
238
- assert!(
239
- !debug_str.contains("ac0974"),
240
- "private key bytes must not appear in Debug output: {debug_str}"
241
- );
242
- }
243
-
244
- #[test]
245
- fn test_wallet_created_at_is_recent() {
246
- let before = SystemTime::now()
247
- .duration_since(UNIX_EPOCH)
248
- .unwrap_or_default()
249
- .as_secs();
250
- let w = AgentWallet::generate();
251
- let after = SystemTime::now()
252
- .duration_since(UNIX_EPOCH)
253
- .unwrap_or_default()
254
- .as_secs();
255
- assert!(
256
- w.created_at_secs >= before && w.created_at_secs <= after,
257
- "created_at_secs={} not in [{before}, {after}]",
258
- w.created_at_secs
259
- );
260
- }
261
- }
1
+ //! Key management for the ClawPowers agent wallet system.
2
+ //!
3
+ //! Provides [`AgentWallet`] — a thin wrapper around an `alloy-signer-local`
4
+ //! [`PrivateKeySigner`] that adds identity metadata and guarantees private-key
5
+ //! scrubbing on drop via [`Zeroize`].
6
+
7
+ use alloy_primitives::{Address, Signature};
8
+ use alloy_signer::SignerSync;
9
+ use alloy_signer_local::PrivateKeySigner;
10
+ use std::time::{SystemTime, UNIX_EPOCH};
11
+ use thiserror::Error;
12
+ use uuid::Uuid;
13
+ use zeroize::Zeroize;
14
+
15
+ // ── WalletError ───────────────────────────────────────────────────────────────
16
+
17
+ /// Errors produced by wallet operations.
18
+ #[derive(Debug, Error)]
19
+ pub enum WalletError {
20
+ /// Failed to parse or import a private key.
21
+ #[error("Invalid private key: {0}")]
22
+ InvalidPrivateKey(String),
23
+ /// Signing operation failed.
24
+ #[error("Signing failed: {0}")]
25
+ SigningFailed(String),
26
+ }
27
+
28
+ // ── AgentWallet ───────────────────────────────────────────────────────────────
29
+
30
+ /// An agent-controlled Ethereum wallet.
31
+ ///
32
+ /// Wraps a [`PrivateKeySigner`] together with a unique wallet ID and creation
33
+ /// timestamp. On drop the inner signing key is zeroed in memory via the
34
+ /// `ZeroizeOnDrop` impl already present on `k256::ecdsa::SigningKey`.
35
+ ///
36
+ /// # Example
37
+ /// ```
38
+ /// use clawpowers_wallet::AgentWallet;
39
+ /// let wallet = AgentWallet::generate();
40
+ /// println!("address: {}", wallet.address());
41
+ /// ```
42
+ pub struct AgentWallet {
43
+ signer: PrivateKeySigner,
44
+ /// Unique, stable identifier for this wallet instance.
45
+ pub wallet_id: Uuid,
46
+ /// Unix timestamp (seconds since epoch) when this wallet was created.
47
+ pub created_at_secs: u64,
48
+ /// Holds a zeroed copy of the raw private key bytes for the explicit
49
+ /// `Zeroize` impl below. Cleared on every call to `zeroize()`.
50
+ key_bytes: [u8; 32],
51
+ }
52
+
53
+ impl std::fmt::Debug for AgentWallet {
54
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55
+ f.debug_struct("AgentWallet")
56
+ .field("wallet_id", &self.wallet_id)
57
+ .field("created_at_secs", &self.created_at_secs)
58
+ .field("address", &self.signer.address())
59
+ .finish_non_exhaustive()
60
+ }
61
+ }
62
+
63
+ impl AgentWallet {
64
+ /// Generates a brand-new random keypair.
65
+ pub fn generate() -> Self {
66
+ let signer = PrivateKeySigner::random();
67
+ let key_bytes = signer.credential().to_bytes().into();
68
+ Self {
69
+ signer,
70
+ wallet_id: Uuid::new_v4(),
71
+ created_at_secs: Self::now_secs(),
72
+ key_bytes,
73
+ }
74
+ }
75
+
76
+ /// Imports an existing private key from a hex string (with or without `0x`
77
+ /// prefix).
78
+ ///
79
+ /// # Errors
80
+ /// Returns [`WalletError::InvalidPrivateKey`] if the hex is invalid or does
81
+ /// not represent a valid secp256k1 scalar.
82
+ pub fn from_private_key(hex: &str) -> Result<Self, WalletError> {
83
+ let trimmed = hex.trim().strip_prefix("0x").unwrap_or(hex.trim());
84
+ let signer = trimmed
85
+ .parse::<PrivateKeySigner>()
86
+ .map_err(|e| WalletError::InvalidPrivateKey(e.to_string()))?;
87
+ let key_bytes = signer.credential().to_bytes().into();
88
+ Ok(Self {
89
+ signer,
90
+ wallet_id: Uuid::new_v4(),
91
+ created_at_secs: Self::now_secs(),
92
+ key_bytes,
93
+ })
94
+ }
95
+
96
+ fn now_secs() -> u64 {
97
+ SystemTime::now()
98
+ .duration_since(UNIX_EPOCH)
99
+ .unwrap_or_default()
100
+ .as_secs()
101
+ }
102
+
103
+ /// Returns the Ethereum address corresponding to this wallet's public key.
104
+ pub fn address(&self) -> Address {
105
+ self.signer.address()
106
+ }
107
+
108
+ /// Signs an arbitrary byte payload using EIP-191 message hashing.
109
+ ///
110
+ /// # Errors
111
+ /// Returns [`WalletError::SigningFailed`] on cryptographic errors.
112
+ pub fn sign_message(&self, msg: &[u8]) -> Result<Signature, WalletError> {
113
+ self.signer
114
+ .sign_message_sync(msg)
115
+ .map_err(|e| WalletError::SigningFailed(e.to_string()))
116
+ }
117
+ }
118
+
119
+ impl Zeroize for AgentWallet {
120
+ /// Zeroes the cached private-key bytes held in this struct.
121
+ ///
122
+ /// The inner `PrivateKeySigner` (and thereby the `k256::ecdsa::SigningKey`)
123
+ /// automatically zeroes its own memory on drop via `ZeroizeOnDrop`.
124
+ fn zeroize(&mut self) {
125
+ self.key_bytes.zeroize();
126
+ }
127
+ }
128
+
129
+ impl Drop for AgentWallet {
130
+ fn drop(&mut self) {
131
+ self.zeroize();
132
+ }
133
+ }
134
+
135
+ // ── Tests ─────────────────────────────────────────────────────────────────────
136
+
137
+ #[cfg(test)]
138
+ mod tests {
139
+ use super::*;
140
+ use alloy_primitives::eip191_hash_message;
141
+
142
+ /// Helper: known test private key from Ethereum test vectors.
143
+ const TEST_PRIVKEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
144
+
145
+ #[test]
146
+ fn test_generate_produces_valid_address() {
147
+ let w = AgentWallet::generate();
148
+ // Address must not be the zero address.
149
+ assert_ne!(w.address(), Address::ZERO);
150
+ }
151
+
152
+ #[test]
153
+ fn test_generate_unique_ids() {
154
+ let w1 = AgentWallet::generate();
155
+ let w2 = AgentWallet::generate();
156
+ assert_ne!(w1.wallet_id, w2.wallet_id);
157
+ }
158
+
159
+ #[test]
160
+ fn test_generate_different_keys() {
161
+ let w1 = AgentWallet::generate();
162
+ let w2 = AgentWallet::generate();
163
+ assert_ne!(w1.address(), w2.address());
164
+ }
165
+
166
+ #[test]
167
+ fn test_from_private_key_valid() {
168
+ let w = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
169
+ // Known address for the Hardhat account #0 key.
170
+ let expected: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
171
+ .parse()
172
+ .expect("parse address");
173
+ assert_eq!(w.address(), expected);
174
+ }
175
+
176
+ #[test]
177
+ fn test_from_private_key_without_0x_prefix() {
178
+ let hex_no_prefix = TEST_PRIVKEY.trim_start_matches("0x");
179
+ let w = AgentWallet::from_private_key(hex_no_prefix).expect("should accept no-0x form");
180
+ let expected: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
181
+ .parse()
182
+ .expect("parse address");
183
+ assert_eq!(w.address(), expected);
184
+ }
185
+
186
+ #[test]
187
+ fn test_from_private_key_invalid_returns_err() {
188
+ let result = AgentWallet::from_private_key("not_a_hex_key");
189
+ assert!(result.is_err());
190
+ assert!(matches!(result, Err(WalletError::InvalidPrivateKey(_))));
191
+ }
192
+
193
+ #[test]
194
+ fn test_sign_message_produces_signature() {
195
+ let w = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
196
+ let msg = b"hello clawpowers";
197
+ let sig = w.sign_message(msg).expect("sign should succeed");
198
+ // Verify the signature recovers to the correct address.
199
+ let recovered = sig
200
+ .recover_address_from_prehash(&eip191_hash_message(msg))
201
+ .expect("recover address");
202
+ assert_eq!(recovered, w.address());
203
+ }
204
+
205
+ #[test]
206
+ fn test_sign_message_deterministic_for_same_key() {
207
+ let w1 = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
208
+ let w2 = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
209
+ let msg = b"determinism test";
210
+ let s1 = w1.sign_message(msg).expect("sign 1");
211
+ let s2 = w2.sign_message(msg).expect("sign 2");
212
+ assert_eq!(s1, s2);
213
+ }
214
+
215
+ #[test]
216
+ fn test_zeroize_clears_key_bytes() {
217
+ let mut w = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
218
+ // Before zeroize, key_bytes should be non-zero.
219
+ let nonzero_before = w.key_bytes.iter().any(|&b| b != 0);
220
+ assert!(
221
+ nonzero_before,
222
+ "key_bytes should be non-zero before zeroize"
223
+ );
224
+
225
+ w.zeroize();
226
+ let all_zero = w.key_bytes.iter().all(|&b| b == 0);
227
+ assert!(
228
+ all_zero,
229
+ "key_bytes should be zeroed after explicit zeroize"
230
+ );
231
+ }
232
+
233
+ #[test]
234
+ fn test_debug_does_not_expose_private_key() {
235
+ let w = AgentWallet::from_private_key(TEST_PRIVKEY).expect("valid key");
236
+ let debug_str = format!("{w:?}");
237
+ // Make sure the raw private key hex is not in the debug output.
238
+ assert!(
239
+ !debug_str.contains("ac0974"),
240
+ "private key bytes must not appear in Debug output: {debug_str}"
241
+ );
242
+ }
243
+
244
+ #[test]
245
+ fn test_wallet_created_at_is_recent() {
246
+ let before = SystemTime::now()
247
+ .duration_since(UNIX_EPOCH)
248
+ .unwrap_or_default()
249
+ .as_secs();
250
+ let w = AgentWallet::generate();
251
+ let after = SystemTime::now()
252
+ .duration_since(UNIX_EPOCH)
253
+ .unwrap_or_default()
254
+ .as_secs();
255
+ assert!(
256
+ w.created_at_secs >= before && w.created_at_secs <= after,
257
+ "created_at_secs={} not in [{before}, {after}]",
258
+ w.created_at_secs
259
+ );
260
+ }
261
+ }
@@ -1,30 +1,30 @@
1
- [package]
2
- name = "clawpowers-x402"
3
- version.workspace = true
4
- edition.workspace = true
5
- license.workspace = true
6
-
7
- [features]
8
- default = ["native"]
9
- native = []
10
- wasm = []
11
-
12
- [dependencies]
13
- serde = { workspace = true }
14
- serde_json = { workspace = true }
15
- thiserror = { workspace = true }
16
- tracing = { workspace = true }
17
- alloy-primitives = { workspace = true }
18
- alloy-signer = { workspace = true }
19
- clawpowers-fee = { path = "../fee" }
20
- clawpowers-tokens = { path = "../tokens" }
21
-
22
- [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
23
- reqwest = { workspace = true }
24
- tokio = { workspace = true }
25
-
26
- [target.'cfg(target_arch = "wasm32")'.dependencies]
27
- reqwest = { version = "0.12", features = ["json"] }
28
-
29
- [dev-dependencies]
30
- tokio = { workspace = true }
1
+ [package]
2
+ name = "clawpowers-x402"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+
7
+ [features]
8
+ default = ["native"]
9
+ native = []
10
+ wasm = []
11
+
12
+ [dependencies]
13
+ serde = { workspace = true }
14
+ serde_json = { workspace = true }
15
+ thiserror = { workspace = true }
16
+ tracing = { workspace = true }
17
+ alloy-primitives = { workspace = true }
18
+ alloy-signer = { workspace = true }
19
+ clawpowers-fee = { path = "../fee" }
20
+ clawpowers-tokens = { path = "../tokens" }
21
+
22
+ [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
23
+ reqwest = { workspace = true }
24
+ tokio = { workspace = true }
25
+
26
+ [target.'cfg(target_arch = "wasm32")'.dependencies]
27
+ reqwest = { version = "0.12", features = ["json"] }
28
+
29
+ [dev-dependencies]
30
+ tokio = { workspace = true }