clawpowers 2.0.0 → 2.2.0

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 (63) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/COMPATIBILITY.md +13 -0
  3. package/KNOWN_LIMITATIONS.md +19 -0
  4. package/LICENSING.md +10 -0
  5. package/README.md +201 -9
  6. package/SECURITY.md +33 -53
  7. package/dist/index.d.ts +638 -5
  8. package/dist/index.js +986 -58
  9. package/dist/index.js.map +1 -1
  10. package/native/Cargo.lock +4863 -0
  11. package/native/Cargo.toml +73 -0
  12. package/native/crates/canonical/Cargo.toml +24 -0
  13. package/native/crates/canonical/src/lib.rs +673 -0
  14. package/native/crates/compression/Cargo.toml +20 -0
  15. package/native/crates/compression/benches/compression_bench.rs +42 -0
  16. package/native/crates/compression/src/lib.rs +393 -0
  17. package/native/crates/evm-eth/Cargo.toml +13 -0
  18. package/native/crates/evm-eth/src/lib.rs +105 -0
  19. package/native/crates/fee/Cargo.toml +15 -0
  20. package/native/crates/fee/src/lib.rs +281 -0
  21. package/native/crates/index/Cargo.toml +16 -0
  22. package/native/crates/index/src/lib.rs +277 -0
  23. package/native/crates/policy/Cargo.toml +17 -0
  24. package/native/crates/policy/src/lib.rs +614 -0
  25. package/native/crates/security/Cargo.toml +22 -0
  26. package/native/crates/security/src/lib.rs +478 -0
  27. package/native/crates/tokens/Cargo.toml +13 -0
  28. package/native/crates/tokens/src/lib.rs +534 -0
  29. package/native/crates/verification/Cargo.toml +23 -0
  30. package/native/crates/verification/src/lib.rs +333 -0
  31. package/native/crates/wallet/Cargo.toml +20 -0
  32. package/native/crates/wallet/src/lib.rs +261 -0
  33. package/native/crates/x402/Cargo.toml +30 -0
  34. package/native/crates/x402/src/lib.rs +423 -0
  35. package/native/ffi/Cargo.toml +34 -0
  36. package/native/ffi/build.rs +4 -0
  37. package/native/ffi/index.node +0 -0
  38. package/native/ffi/src/lib.rs +352 -0
  39. package/native/ffi/tests/integration.rs +354 -0
  40. package/native/pyo3/Cargo.toml +26 -0
  41. package/native/pyo3/pyproject.toml +16 -0
  42. package/native/pyo3/src/lib.rs +407 -0
  43. package/native/pyo3/tests/test_smoke.py +180 -0
  44. package/native/wasm/Cargo.toml +44 -0
  45. package/native/wasm/pkg/.gitignore +6 -0
  46. package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -0
  47. package/native/wasm/pkg/clawpowers_wasm.js +872 -0
  48. package/native/wasm/pkg/clawpowers_wasm_bg.wasm +0 -0
  49. package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -0
  50. package/native/wasm/pkg/package.json +17 -0
  51. package/native/wasm/pkg-node/.gitignore +6 -0
  52. package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -0
  53. package/native/wasm/pkg-node/clawpowers_wasm.js +798 -0
  54. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm +0 -0
  55. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -0
  56. package/native/wasm/pkg-node/package.json +13 -0
  57. package/native/wasm/src/lib.rs +433 -0
  58. package/package.json +24 -3
  59. package/src/skills/catalog.ts +435 -0
  60. package/src/skills/executor.ts +56 -0
  61. package/src/skills/index.ts +3 -0
  62. package/src/skills/itp/SKILL.md +112 -0
  63. package/src/skills/loader.ts +193 -0
@@ -0,0 +1,333 @@
1
+ //! Exact verification pipeline for TurboMemory.
2
+ //!
3
+ //! The [`VerificationPipeline`] fetches records from a [`CanonicalStore`],
4
+ //! recomputes content hashes, and checks TTL / quarantine metadata before
5
+ //! returning a typed [`VerificationResult`].
6
+
7
+ use chrono::{Duration, Utc};
8
+ use clawpowers_canonical::{CanonicalRecord, CanonicalStore, compute_sha256};
9
+ use thiserror::Error;
10
+ use uuid::Uuid;
11
+
12
+ /// Errors returned by the verification pipeline.
13
+ #[derive(Debug, Error)]
14
+ pub enum VerificationError {
15
+ /// Underlying canonical store error.
16
+ #[error("store error: {0}")]
17
+ Store(#[from] clawpowers_canonical::CanonicalError),
18
+ }
19
+
20
+ /// A shorthand result type for [`VerificationError`].
21
+ pub type Result<T> = std::result::Result<T, VerificationError>;
22
+
23
+ // ─── Verification Result ─────────────────────────────────────────────────────
24
+
25
+ /// The outcome of verifying a single record.
26
+ #[derive(Debug)]
27
+ pub enum VerificationResult {
28
+ /// Record is present, hash is valid, not expired, and not quarantined.
29
+ Verified(CanonicalRecord),
30
+
31
+ /// The stored hash does not match the recomputed hash.
32
+ IntegrityFailed {
33
+ /// Record identifier.
34
+ id: Uuid,
35
+ /// Hash value stored in the database.
36
+ expected_hash: String,
37
+ /// Hash value computed from the stored content.
38
+ actual_hash: String,
39
+ },
40
+
41
+ /// The record has a `ttl_seconds` metadata field and the record has
42
+ /// outlived it.
43
+ Expired {
44
+ /// Record identifier.
45
+ id: Uuid,
46
+ /// How far past the TTL deadline the record is.
47
+ ttl_exceeded_by: Duration,
48
+ },
49
+
50
+ /// The record has `"quarantined": true` in its metadata.
51
+ Quarantined {
52
+ /// Record identifier.
53
+ id: Uuid,
54
+ /// Reason, extracted from `metadata["quarantine_reason"]` if present.
55
+ reason: String,
56
+ },
57
+
58
+ /// No record with the given id exists in the store.
59
+ NotFound(Uuid),
60
+ }
61
+
62
+ // ─── Pipeline ────────────────────────────────────────────────────────────────
63
+
64
+ /// Verification pipeline wrapping a [`CanonicalStore`].
65
+ pub struct VerificationPipeline {
66
+ store: CanonicalStore,
67
+ }
68
+
69
+ impl VerificationPipeline {
70
+ /// Create a new pipeline backed by `store`.
71
+ pub fn new(store: CanonicalStore) -> Self {
72
+ Self { store }
73
+ }
74
+
75
+ /// Access the underlying canonical store.
76
+ pub fn store(&self) -> &CanonicalStore {
77
+ &self.store
78
+ }
79
+
80
+ // ── Single Verification ───────────────────────────────────────────────
81
+
82
+ /// Verify a single record identified by `id`.
83
+ ///
84
+ /// Steps:
85
+ /// 1. Fetch — [`VerificationResult::NotFound`] if absent.
86
+ /// 2. Hash check — [`VerificationResult::IntegrityFailed`] on mismatch.
87
+ /// 3. TTL check — [`VerificationResult::Expired`] if past `metadata["ttl_seconds"]`.
88
+ /// 4. Quarantine check — [`VerificationResult::Quarantined`] if
89
+ /// `metadata["quarantined"] == true`.
90
+ /// 5. Otherwise [`VerificationResult::Verified`].
91
+ pub fn verify(&self, id: &Uuid) -> Result<VerificationResult> {
92
+ // Step 1: Fetch.
93
+ let record = match self.store.get(id)? {
94
+ None => return Ok(VerificationResult::NotFound(*id)),
95
+ Some(r) => r,
96
+ };
97
+
98
+ // Step 2: Hash check.
99
+ let actual_hash = compute_sha256(&record.content);
100
+ if actual_hash != record.content_hash {
101
+ return Ok(VerificationResult::IntegrityFailed {
102
+ id: *id,
103
+ expected_hash: record.content_hash.clone(),
104
+ actual_hash,
105
+ });
106
+ }
107
+
108
+ // Step 3: TTL check.
109
+ if let Some(ttl_val) = record.metadata.get("ttl_seconds")
110
+ && let Some(ttl_secs) = ttl_val.as_i64()
111
+ {
112
+ let deadline = record.created_at + Duration::seconds(ttl_secs);
113
+ let now = Utc::now();
114
+ if now > deadline {
115
+ let exceeded_by = now - deadline;
116
+ return Ok(VerificationResult::Expired {
117
+ id: *id,
118
+ ttl_exceeded_by: exceeded_by,
119
+ });
120
+ }
121
+ }
122
+
123
+ // Step 4: Quarantine check.
124
+ if record
125
+ .metadata
126
+ .get("quarantined")
127
+ .and_then(|v| v.as_bool())
128
+ .unwrap_or(false)
129
+ {
130
+ let reason = record
131
+ .metadata
132
+ .get("quarantine_reason")
133
+ .and_then(|v| v.as_str())
134
+ .unwrap_or("unknown")
135
+ .to_string();
136
+ return Ok(VerificationResult::Quarantined { id: *id, reason });
137
+ }
138
+
139
+ Ok(VerificationResult::Verified(record))
140
+ }
141
+
142
+ // ── Batch Verification ────────────────────────────────────────────────
143
+
144
+ /// Verify a batch of records; each id is verified independently.
145
+ pub fn verify_batch(&self, ids: &[Uuid]) -> Result<Vec<VerificationResult>> {
146
+ ids.iter().map(|id| self.verify(id)).collect()
147
+ }
148
+ }
149
+
150
+ // ─── Tests ───────────────────────────────────────────────────────────────────
151
+
152
+ #[cfg(test)]
153
+ mod tests {
154
+ use super::*;
155
+ use clawpowers_canonical::{CanonicalRecord, CanonicalStore};
156
+ use serde_json::json;
157
+
158
+ fn make_pipeline() -> VerificationPipeline {
159
+ let store = CanonicalStore::in_memory().expect("in-memory store");
160
+ VerificationPipeline::new(store)
161
+ }
162
+
163
+ fn insert(
164
+ pipeline: &VerificationPipeline,
165
+ namespace: &str,
166
+ content: &str,
167
+ metadata: serde_json::Value,
168
+ ) -> Uuid {
169
+ let rec = CanonicalRecord::new(namespace, content, None, metadata, "test");
170
+ let id = rec.id;
171
+ pipeline.store().insert(&rec).expect("insert");
172
+ id
173
+ }
174
+
175
+ // ── Verified ─────────────────────────────────────────────────────────
176
+
177
+ #[test]
178
+ fn test_verify_valid_record() {
179
+ let p = make_pipeline();
180
+ let id = insert(&p, "ns", "hello world", json!({}));
181
+ match p.verify(&id).expect("verify") {
182
+ VerificationResult::Verified(r) => assert_eq!(r.id, id),
183
+ other => panic!("expected Verified, got {other:?}"),
184
+ }
185
+ }
186
+
187
+ #[test]
188
+ fn test_verify_correct_hash_is_verified() {
189
+ let p = make_pipeline();
190
+ let id = insert(&p, "ns", "content for integrity check", json!({}));
191
+ assert!(matches!(
192
+ p.verify(&id).expect("verify"),
193
+ VerificationResult::Verified(_)
194
+ ));
195
+ }
196
+
197
+ // ── Not Found ─────────────────────────────────────────────────────────
198
+
199
+ #[test]
200
+ fn test_verify_not_found() {
201
+ let p = make_pipeline();
202
+ let missing = Uuid::new_v4();
203
+ match p.verify(&missing).expect("verify") {
204
+ VerificationResult::NotFound(id) => assert_eq!(id, missing),
205
+ other => panic!("expected NotFound, got {other:?}"),
206
+ }
207
+ }
208
+
209
+ // ── Integrity (via hash comparison) ───────────────────────────────────
210
+
211
+ #[test]
212
+ fn test_verify_integrity_passes_for_fresh_record() {
213
+ let p = make_pipeline();
214
+ let id = insert(&p, "ns", "fresh record content", json!({}));
215
+ // Freshly inserted record should always pass hash check.
216
+ assert!(matches!(
217
+ p.verify(&id).expect("v"),
218
+ VerificationResult::Verified(_)
219
+ ));
220
+ }
221
+
222
+ #[test]
223
+ fn test_integrity_check_uses_sha256() {
224
+ let content = "check sha256 content";
225
+ let expected_hash = compute_sha256(content);
226
+ let p = make_pipeline();
227
+ let rec = CanonicalRecord::new("ns", content, None, json!({}), "test");
228
+ assert_eq!(rec.content_hash, expected_hash);
229
+ let id = p.store().insert(&rec).expect("insert");
230
+ assert!(matches!(
231
+ p.verify(&id).expect("v"),
232
+ VerificationResult::Verified(_)
233
+ ));
234
+ }
235
+
236
+ // ── TTL ───────────────────────────────────────────────────────────────
237
+
238
+ #[test]
239
+ fn test_verify_expired_ttl() {
240
+ let p = make_pipeline();
241
+ let id = insert(&p, "ns", "old content", json!({"ttl_seconds": -1}));
242
+ match p.verify(&id).expect("verify") {
243
+ VerificationResult::Expired { id: eid, .. } => assert_eq!(eid, id),
244
+ other => panic!("expected Expired, got {other:?}"),
245
+ }
246
+ }
247
+
248
+ #[test]
249
+ fn test_verify_future_ttl_is_valid() {
250
+ let p = make_pipeline();
251
+ let id = insert(&p, "ns", "fresh content", json!({"ttl_seconds": 3600}));
252
+ assert!(matches!(
253
+ p.verify(&id).expect("v"),
254
+ VerificationResult::Verified(_)
255
+ ));
256
+ }
257
+
258
+ #[test]
259
+ fn test_no_ttl_field_is_valid() {
260
+ let p = make_pipeline();
261
+ let id = insert(&p, "ns", "no ttl here", json!({"source": "test"}));
262
+ assert!(matches!(
263
+ p.verify(&id).expect("v"),
264
+ VerificationResult::Verified(_)
265
+ ));
266
+ }
267
+
268
+ // ── Quarantine ────────────────────────────────────────────────────────
269
+
270
+ #[test]
271
+ fn test_verify_quarantined_with_reason() {
272
+ let p = make_pipeline();
273
+ let id = insert(
274
+ &p,
275
+ "ns",
276
+ "suspicious content",
277
+ json!({"quarantined": true, "quarantine_reason": "policy violation"}),
278
+ );
279
+ match p.verify(&id).expect("verify") {
280
+ VerificationResult::Quarantined { id: qid, reason } => {
281
+ assert_eq!(qid, id);
282
+ assert_eq!(reason, "policy violation");
283
+ }
284
+ other => panic!("expected Quarantined, got {other:?}"),
285
+ }
286
+ }
287
+
288
+ #[test]
289
+ fn test_verify_quarantined_default_reason() {
290
+ let p = make_pipeline();
291
+ let id = insert(&p, "ns", "another bad record", json!({"quarantined": true}));
292
+ match p.verify(&id).expect("verify") {
293
+ VerificationResult::Quarantined { reason, .. } => assert_eq!(reason, "unknown"),
294
+ other => panic!("expected Quarantined, got {other:?}"),
295
+ }
296
+ }
297
+
298
+ // ── Batch ─────────────────────────────────────────────────────────────
299
+
300
+ #[test]
301
+ fn test_verify_batch_mixed_results() {
302
+ let p = make_pipeline();
303
+ let valid_id = insert(&p, "ns", "valid batch record", json!({}));
304
+ let expired_id = insert(&p, "ns", "expired batch", json!({"ttl_seconds": -1}));
305
+ let missing_id = Uuid::new_v4();
306
+
307
+ let results = p
308
+ .verify_batch(&[valid_id, expired_id, missing_id])
309
+ .expect("batch");
310
+ assert_eq!(results.len(), 3);
311
+ assert!(matches!(results[0], VerificationResult::Verified(_)));
312
+ assert!(matches!(results[1], VerificationResult::Expired { .. }));
313
+ assert!(matches!(results[2], VerificationResult::NotFound(_)));
314
+ }
315
+
316
+ #[test]
317
+ fn test_verify_batch_empty() {
318
+ let p = make_pipeline();
319
+ let results = p.verify_batch(&[]).expect("batch");
320
+ assert!(results.is_empty());
321
+ }
322
+
323
+ #[test]
324
+ fn test_verify_batch_all_not_found() {
325
+ let p = make_pipeline();
326
+ let ids = vec![Uuid::new_v4(), Uuid::new_v4()];
327
+ let results = p.verify_batch(&ids).expect("batch");
328
+ assert_eq!(results.len(), 2);
329
+ for r in results {
330
+ assert!(matches!(r, VerificationResult::NotFound(_)));
331
+ }
332
+ }
333
+ }
@@ -0,0 +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 }
@@ -0,0 +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
+ }
@@ -0,0 +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 }