clawpowers 1.1.4 → 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 (131) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/COMPATIBILITY.md +13 -0
  3. package/KNOWN_LIMITATIONS.md +19 -0
  4. package/LICENSE +44 -0
  5. package/LICENSING.md +10 -0
  6. package/README.md +378 -210
  7. package/SECURITY.md +52 -0
  8. package/dist/index.d.ts +1477 -0
  9. package/dist/index.js +3464 -0
  10. package/dist/index.js.map +1 -0
  11. package/native/Cargo.lock +4863 -0
  12. package/native/Cargo.toml +73 -0
  13. package/native/crates/canonical/Cargo.toml +24 -0
  14. package/native/crates/canonical/src/lib.rs +673 -0
  15. package/native/crates/compression/Cargo.toml +20 -0
  16. package/native/crates/compression/benches/compression_bench.rs +42 -0
  17. package/native/crates/compression/src/lib.rs +393 -0
  18. package/native/crates/evm-eth/Cargo.toml +13 -0
  19. package/native/crates/evm-eth/src/lib.rs +105 -0
  20. package/native/crates/fee/Cargo.toml +15 -0
  21. package/native/crates/fee/src/lib.rs +281 -0
  22. package/native/crates/index/Cargo.toml +16 -0
  23. package/native/crates/index/src/lib.rs +277 -0
  24. package/native/crates/policy/Cargo.toml +17 -0
  25. package/native/crates/policy/src/lib.rs +614 -0
  26. package/native/crates/security/Cargo.toml +22 -0
  27. package/native/crates/security/src/lib.rs +478 -0
  28. package/native/crates/tokens/Cargo.toml +13 -0
  29. package/native/crates/tokens/src/lib.rs +534 -0
  30. package/native/crates/verification/Cargo.toml +23 -0
  31. package/native/crates/verification/src/lib.rs +333 -0
  32. package/native/crates/wallet/Cargo.toml +20 -0
  33. package/native/crates/wallet/src/lib.rs +261 -0
  34. package/native/crates/x402/Cargo.toml +30 -0
  35. package/native/crates/x402/src/lib.rs +423 -0
  36. package/native/ffi/Cargo.toml +34 -0
  37. package/native/ffi/build.rs +4 -0
  38. package/native/ffi/index.node +0 -0
  39. package/native/ffi/src/lib.rs +352 -0
  40. package/native/ffi/tests/integration.rs +354 -0
  41. package/native/pyo3/Cargo.toml +26 -0
  42. package/native/pyo3/pyproject.toml +16 -0
  43. package/native/pyo3/src/lib.rs +407 -0
  44. package/native/pyo3/tests/test_smoke.py +180 -0
  45. package/native/wasm/Cargo.toml +44 -0
  46. package/native/wasm/pkg/.gitignore +6 -0
  47. package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -0
  48. package/native/wasm/pkg/clawpowers_wasm.js +872 -0
  49. package/native/wasm/pkg/clawpowers_wasm_bg.wasm +0 -0
  50. package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -0
  51. package/native/wasm/pkg/package.json +17 -0
  52. package/native/wasm/pkg-node/.gitignore +6 -0
  53. package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -0
  54. package/native/wasm/pkg-node/clawpowers_wasm.js +798 -0
  55. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm +0 -0
  56. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -0
  57. package/native/wasm/pkg-node/package.json +13 -0
  58. package/native/wasm/src/lib.rs +433 -0
  59. package/package.json +71 -44
  60. package/src/skills/catalog.ts +435 -0
  61. package/src/skills/executor.ts +56 -0
  62. package/src/skills/index.ts +3 -0
  63. package/src/skills/itp/SKILL.md +112 -0
  64. package/src/skills/loader.ts +193 -0
  65. package/.claude-plugin/manifest.json +0 -19
  66. package/.codex/INSTALL.md +0 -36
  67. package/.cursor-plugin/manifest.json +0 -21
  68. package/.opencode/INSTALL.md +0 -52
  69. package/ARCHITECTURE.md +0 -69
  70. package/bin/clawpowers.js +0 -625
  71. package/bin/clawpowers.sh +0 -91
  72. package/docs/demo/clawpowers-demo.cast +0 -197
  73. package/docs/demo/clawpowers-demo.gif +0 -0
  74. package/docs/launch-images/25-skills-breakdown.jpg +0 -0
  75. package/docs/launch-images/clawpowers-vs-superpowers.jpg +0 -0
  76. package/docs/launch-images/economic-code-optimization.jpg +0 -0
  77. package/docs/launch-images/native-vs-bridge-2.jpg +0 -0
  78. package/docs/launch-images/native-vs-bridge.jpg +0 -0
  79. package/docs/launch-images/post1-hero-lobster.jpg +0 -0
  80. package/docs/launch-images/post2-dashboard.jpg +0 -0
  81. package/docs/launch-images/post3-superpowers.jpg +0 -0
  82. package/docs/launch-images/post4-before-after.jpg +0 -0
  83. package/docs/launch-images/post5-install-now.jpg +0 -0
  84. package/docs/launch-images/ultimate-stack.jpg +0 -0
  85. package/docs/launch-posts.md +0 -76
  86. package/docs/quickstart-first-transaction.md +0 -204
  87. package/gemini-extension.json +0 -32
  88. package/hooks/session-start +0 -205
  89. package/hooks/session-start.cmd +0 -43
  90. package/hooks/session-start.js +0 -163
  91. package/runtime/demo/README.md +0 -78
  92. package/runtime/demo/x402-mock-server.js +0 -230
  93. package/runtime/feedback/analyze.js +0 -621
  94. package/runtime/feedback/analyze.sh +0 -546
  95. package/runtime/init.js +0 -210
  96. package/runtime/init.sh +0 -178
  97. package/runtime/metrics/collector.js +0 -361
  98. package/runtime/metrics/collector.sh +0 -308
  99. package/runtime/payments/ledger.js +0 -305
  100. package/runtime/payments/ledger.sh +0 -262
  101. package/runtime/payments/pipeline.js +0 -455
  102. package/runtime/persistence/store.js +0 -433
  103. package/runtime/persistence/store.sh +0 -303
  104. package/skill.json +0 -106
  105. package/skills/agent-bounties/SKILL.md +0 -553
  106. package/skills/agent-payments/SKILL.md +0 -479
  107. package/skills/brainstorming/SKILL.md +0 -233
  108. package/skills/content-pipeline/SKILL.md +0 -282
  109. package/skills/cross-project-knowledge/SKILL.md +0 -345
  110. package/skills/dispatching-parallel-agents/SKILL.md +0 -305
  111. package/skills/economic-code-optimization/SKILL.md +0 -265
  112. package/skills/executing-plans/SKILL.md +0 -255
  113. package/skills/finishing-a-development-branch/SKILL.md +0 -260
  114. package/skills/formal-verification-lite/SKILL.md +0 -441
  115. package/skills/learn-how-to-learn/SKILL.md +0 -235
  116. package/skills/market-intelligence/SKILL.md +0 -323
  117. package/skills/meta-skill-evolution/SKILL.md +0 -325
  118. package/skills/prospecting/SKILL.md +0 -454
  119. package/skills/receiving-code-review/SKILL.md +0 -225
  120. package/skills/requesting-code-review/SKILL.md +0 -206
  121. package/skills/security-audit/SKILL.md +0 -353
  122. package/skills/self-healing-code/SKILL.md +0 -369
  123. package/skills/subagent-driven-development/SKILL.md +0 -244
  124. package/skills/systematic-debugging/SKILL.md +0 -355
  125. package/skills/test-driven-development/SKILL.md +0 -416
  126. package/skills/using-clawpowers/SKILL.md +0 -160
  127. package/skills/using-git-worktrees/SKILL.md +0 -261
  128. package/skills/validator/SKILL.md +0 -281
  129. package/skills/verification-before-completion/SKILL.md +0 -254
  130. package/skills/writing-plans/SKILL.md +0 -276
  131. package/skills/writing-skills/SKILL.md +0 -260
@@ -0,0 +1,534 @@
1
+ //! Token registry and decimal math for the ClawPowers agent wallet system.
2
+ //!
3
+ //! Provides [`TokenInfo`], [`TokenRegistry`], and [`TokenAmount`] — the
4
+ //! foundational types for expressing on-chain token quantities with correct
5
+ //! decimal semantics.
6
+
7
+ use alloy_primitives::{Address, U256, address};
8
+ use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
+ use std::collections::HashMap;
10
+ use std::fmt;
11
+ use thiserror::Error;
12
+
13
+ // ── Custom serde helpers for alloy types ─────────────────────────────────────
14
+
15
+ mod serde_address_opt {
16
+ use super::*;
17
+
18
+ pub fn serialize<S>(addr: &Option<Address>, s: S) -> Result<S::Ok, S::Error>
19
+ where
20
+ S: Serializer,
21
+ {
22
+ match addr {
23
+ Some(a) => s.serialize_some(&format!("{a:?}")),
24
+ None => s.serialize_none(),
25
+ }
26
+ }
27
+
28
+ pub fn deserialize<'de, D>(d: D) -> Result<Option<Address>, D::Error>
29
+ where
30
+ D: Deserializer<'de>,
31
+ {
32
+ let opt: Option<String> = Option::deserialize(d)?;
33
+ match opt {
34
+ None => Ok(None),
35
+ Some(s) => {
36
+ let trimmed = s.trim();
37
+ let hex = trimmed.strip_prefix("0x").unwrap_or(trimmed);
38
+ let mut bytes = [0u8; 20];
39
+ if hex.len() != 40 {
40
+ return Err(serde::de::Error::custom(format!(
41
+ "invalid address length: {hex}"
42
+ )));
43
+ }
44
+ for i in 0..20 {
45
+ bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16)
46
+ .map_err(|e| serde::de::Error::custom(e.to_string()))?;
47
+ }
48
+ Ok(Some(Address::from(bytes)))
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ mod serde_u256 {
55
+ use super::*;
56
+
57
+ pub fn serialize<S>(v: &U256, s: S) -> Result<S::Ok, S::Error>
58
+ where
59
+ S: Serializer,
60
+ {
61
+ s.serialize_str(&v.to_string())
62
+ }
63
+
64
+ pub fn deserialize<'de, D>(d: D) -> Result<U256, D::Error>
65
+ where
66
+ D: Deserializer<'de>,
67
+ {
68
+ let s = String::deserialize(d)?;
69
+ U256::from_str_radix(s.trim(), 10)
70
+ .map_err(|e| serde::de::Error::custom(format!("invalid U256: {e}")))
71
+ }
72
+ }
73
+
74
+ // ── TokenInfo ────────────────────────────────────────────────────────────────
75
+
76
+ /// Metadata about an ERC-20 token (or native asset).
77
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78
+ pub struct TokenInfo {
79
+ /// Ticker symbol, e.g. `"USDC"`.
80
+ pub symbol: String,
81
+ /// Number of decimal places, e.g. `6` for USDC.
82
+ pub decimals: u8,
83
+ /// Chain ID the token lives on (1 = Ethereum mainnet).
84
+ pub chain_id: u64,
85
+ /// Contract address. `None` for native assets such as ETH.
86
+ #[serde(with = "serde_address_opt")]
87
+ pub address: Option<Address>,
88
+ }
89
+
90
+ // ── TokenRegistry ─────────────────────────────────────────────────────────
91
+
92
+ /// A registry of well-known tokens, keyed by symbol.
93
+ ///
94
+ /// Constructed with [`TokenRegistry::default()`] which pre-populates a set of
95
+ /// common mainnet tokens.
96
+ #[derive(Debug, Clone)]
97
+ pub struct TokenRegistry {
98
+ tokens: HashMap<String, TokenInfo>,
99
+ }
100
+
101
+ impl TokenRegistry {
102
+ /// Creates an empty registry.
103
+ pub fn empty() -> Self {
104
+ Self {
105
+ tokens: HashMap::new(),
106
+ }
107
+ }
108
+
109
+ /// Registers a token, overwriting any existing entry with the same symbol.
110
+ pub fn register(&mut self, token: TokenInfo) {
111
+ self.tokens.insert(token.symbol.clone(), token);
112
+ }
113
+
114
+ /// Looks up a token by its ticker symbol.
115
+ pub fn get(&self, symbol: &str) -> Option<&TokenInfo> {
116
+ self.tokens.get(symbol)
117
+ }
118
+
119
+ /// Returns an iterator over all registered tokens.
120
+ pub fn iter(&self) -> impl Iterator<Item = &TokenInfo> {
121
+ self.tokens.values()
122
+ }
123
+ }
124
+
125
+ impl Default for TokenRegistry {
126
+ /// Returns a registry pre-populated with common mainnet tokens.
127
+ fn default() -> Self {
128
+ let mut r = Self::empty();
129
+ r.register(TokenInfo {
130
+ symbol: "USDC".to_string(),
131
+ decimals: 6,
132
+ chain_id: 1,
133
+ address: Some(address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")),
134
+ });
135
+ r.register(TokenInfo {
136
+ symbol: "USDT".to_string(),
137
+ decimals: 6,
138
+ chain_id: 1,
139
+ address: Some(address!("dAC17F958D2ee523a2206206994597C13D831ec7")),
140
+ });
141
+ r.register(TokenInfo {
142
+ symbol: "DAI".to_string(),
143
+ decimals: 18,
144
+ chain_id: 1,
145
+ address: Some(address!("6B175474E89094C44Da98b954EedeAC495271d0F")),
146
+ });
147
+ r.register(TokenInfo {
148
+ symbol: "WETH".to_string(),
149
+ decimals: 18,
150
+ chain_id: 1,
151
+ address: Some(address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")),
152
+ });
153
+ r.register(TokenInfo {
154
+ symbol: "ETH".to_string(),
155
+ decimals: 18,
156
+ chain_id: 1,
157
+ address: None,
158
+ });
159
+ r
160
+ }
161
+ }
162
+
163
+ // ── TokenAmount ──────────────────────────────────────────────────────────────
164
+
165
+ /// Error type for token arithmetic operations.
166
+ #[derive(Debug, Error, Clone, PartialEq, Eq)]
167
+ pub enum TokenError {
168
+ /// The two operands have mismatched decimal precision.
169
+ #[error("Decimal mismatch: lhs={lhs}, rhs={rhs}")]
170
+ DecimalMismatch { lhs: u8, rhs: u8 },
171
+ /// Arithmetic overflow or underflow.
172
+ #[error("Arithmetic overflow/underflow")]
173
+ Overflow,
174
+ }
175
+
176
+ /// A fixed-point token amount, backed by a [`U256`] raw integer.
177
+ ///
178
+ /// All arithmetic is decimal-aware. Use [`TokenAmount::from_human`] and
179
+ /// [`TokenAmount::to_human`] to cross the human-readable boundary.
180
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181
+ pub struct TokenAmount {
182
+ /// Raw integer representation (`human × 10^decimals`), serialised as a
183
+ /// decimal string for portability.
184
+ #[serde(with = "serde_u256")]
185
+ pub raw: U256,
186
+ /// Number of decimal places this amount is expressed with.
187
+ pub decimals: u8,
188
+ }
189
+
190
+ impl TokenAmount {
191
+ /// Creates a [`TokenAmount`] from a human-readable `f64` value.
192
+ ///
193
+ /// # Example
194
+ /// ```
195
+ /// use clawpowers_tokens::TokenAmount;
196
+ /// let one_usdc = TokenAmount::from_human(1.0, 6);
197
+ /// assert_eq!(one_usdc.raw, alloy_primitives::U256::from(1_000_000u64));
198
+ /// ```
199
+ pub fn from_human(human: f64, decimals: u8) -> Self {
200
+ let multiplier = 10f64.powi(i32::from(decimals));
201
+ let raw_f64 = (human * multiplier).floor();
202
+ let raw = if raw_f64 <= 0.0 {
203
+ U256::ZERO
204
+ } else {
205
+ let as_int = raw_f64 as u128;
206
+ U256::from(as_int)
207
+ };
208
+ Self { raw, decimals }
209
+ }
210
+
211
+ /// Converts back to a human-readable `f64`.
212
+ ///
213
+ /// Precision is limited by `f64` — do not use this for exact comparisons.
214
+ pub fn to_human(&self) -> f64 {
215
+ let divisor = 10f64.powi(i32::from(self.decimals));
216
+ let raw_str = self.raw.to_string();
217
+ let raw_f64: f64 = raw_str.parse().unwrap_or(0.0);
218
+ raw_f64 / divisor
219
+ }
220
+
221
+ /// Returns `true` if the amount is zero.
222
+ pub fn is_zero(&self) -> bool {
223
+ self.raw.is_zero()
224
+ }
225
+
226
+ /// Checked addition. Returns an error on overflow or decimal mismatch.
227
+ pub fn add(&self, other: &TokenAmount) -> Result<TokenAmount, TokenError> {
228
+ if self.decimals != other.decimals {
229
+ return Err(TokenError::DecimalMismatch {
230
+ lhs: self.decimals,
231
+ rhs: other.decimals,
232
+ });
233
+ }
234
+ let raw = self
235
+ .raw
236
+ .checked_add(other.raw)
237
+ .ok_or(TokenError::Overflow)?;
238
+ Ok(TokenAmount {
239
+ raw,
240
+ decimals: self.decimals,
241
+ })
242
+ }
243
+
244
+ /// Checked subtraction. Returns an error on underflow or decimal mismatch.
245
+ pub fn sub(&self, other: &TokenAmount) -> Result<TokenAmount, TokenError> {
246
+ if self.decimals != other.decimals {
247
+ return Err(TokenError::DecimalMismatch {
248
+ lhs: self.decimals,
249
+ rhs: other.decimals,
250
+ });
251
+ }
252
+ let raw = self
253
+ .raw
254
+ .checked_sub(other.raw)
255
+ .ok_or(TokenError::Overflow)?;
256
+ Ok(TokenAmount {
257
+ raw,
258
+ decimals: self.decimals,
259
+ })
260
+ }
261
+
262
+ /// Multiply by basis points (1 bps = 0.01%).
263
+ ///
264
+ /// Formula: `result = self × bps / 10_000`.
265
+ ///
266
+ /// Returns `None` on arithmetic overflow.
267
+ pub fn checked_mul_bps(&self, bps: u64) -> Option<TokenAmount> {
268
+ let numerator = self.raw.checked_mul(U256::from(bps))?;
269
+ let raw = numerator.checked_div(U256::from(10_000u64))?;
270
+ Some(TokenAmount {
271
+ raw,
272
+ decimals: self.decimals,
273
+ })
274
+ }
275
+
276
+ /// Creates a zero amount with the given decimal precision.
277
+ pub fn zero(decimals: u8) -> Self {
278
+ Self {
279
+ raw: U256::ZERO,
280
+ decimals,
281
+ }
282
+ }
283
+
284
+ /// Alias for [`TokenAmount::from_human`] accepting an `f64`.
285
+ ///
286
+ /// Provided for API symmetry — `from_human` already accepts `f64`.
287
+ pub fn from_human_f64(human: f64, decimals: u8) -> Self {
288
+ Self::from_human(human, decimals)
289
+ }
290
+
291
+ /// Scale by basis points (1 bps = 0.01%): `self × bps / 10_000`.
292
+ ///
293
+ /// Returns `None` on arithmetic overflow.
294
+ pub fn scale_bps(&self, bps: u32) -> Option<TokenAmount> {
295
+ self.checked_mul_bps(bps as u64)
296
+ }
297
+
298
+ /// Checked subtraction (alias exposing the same name used by the fee crate).
299
+ ///
300
+ /// Returns `None` on underflow or decimal mismatch.
301
+ pub fn checked_sub(&self, other: &TokenAmount) -> Option<TokenAmount> {
302
+ self.sub(other).ok()
303
+ }
304
+ }
305
+
306
+ // ─── FeeType ─────────────────────────────────────────────────────────────────
307
+
308
+ /// Fee classification used by the fee schedule.
309
+ ///
310
+ /// Defined here in `clawpowers-tokens` and re-exported by `clawpowers-fee` so
311
+ /// downstream consumers can `use clawpowers_fee::FeeType` or
312
+ /// `use clawpowers_tokens::FeeType` interchangeably.
313
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
314
+ #[serde(rename_all = "snake_case")]
315
+ pub enum FeeType {
316
+ /// Standard on-chain transaction fee.
317
+ Transaction,
318
+ /// DEX swap fee.
319
+ Swap,
320
+ /// Custom fee with explicit basis points.
321
+ Custom(u32),
322
+ }
323
+
324
+ impl fmt::Display for TokenAmount {
325
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326
+ write!(f, "{}", self.to_human())
327
+ }
328
+ }
329
+
330
+ impl PartialOrd for TokenAmount {
331
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
332
+ if self.decimals != other.decimals {
333
+ return None;
334
+ }
335
+ Some(self.raw.cmp(&other.raw))
336
+ }
337
+ }
338
+
339
+ // ── Tests ─────────────────────────────────────────────────────────────────────
340
+
341
+ #[cfg(test)]
342
+ mod tests {
343
+ use super::*;
344
+
345
+ // ── TokenInfo ──────────────────────────────────────────────────────────
346
+
347
+ #[test]
348
+ fn test_token_info_fields() {
349
+ let info = TokenInfo {
350
+ symbol: "USDC".to_string(),
351
+ decimals: 6,
352
+ chain_id: 1,
353
+ address: Some(address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")),
354
+ };
355
+ assert_eq!(info.symbol, "USDC");
356
+ assert_eq!(info.decimals, 6);
357
+ assert_eq!(info.chain_id, 1);
358
+ assert!(info.address.is_some());
359
+ }
360
+
361
+ #[test]
362
+ fn test_token_info_no_address_for_eth() {
363
+ let eth = TokenInfo {
364
+ symbol: "ETH".to_string(),
365
+ decimals: 18,
366
+ chain_id: 1,
367
+ address: None,
368
+ };
369
+ assert!(eth.address.is_none());
370
+ }
371
+
372
+ // ── TokenRegistry ──────────────────────────────────────────────────────
373
+
374
+ #[test]
375
+ fn test_registry_default_contains_all_tokens() {
376
+ let reg = TokenRegistry::default();
377
+ for sym in &["USDC", "USDT", "DAI", "WETH", "ETH"] {
378
+ assert!(reg.get(sym).is_some(), "missing token: {sym}");
379
+ }
380
+ }
381
+
382
+ #[test]
383
+ fn test_registry_usdc_decimals() {
384
+ let reg = TokenRegistry::default();
385
+ let usdc = reg.get("USDC").expect("USDC should be present");
386
+ assert_eq!(usdc.decimals, 6);
387
+ }
388
+
389
+ #[test]
390
+ fn test_registry_dai_decimals() {
391
+ let reg = TokenRegistry::default();
392
+ let dai = reg.get("DAI").expect("DAI should be present");
393
+ assert_eq!(dai.decimals, 18);
394
+ }
395
+
396
+ #[test]
397
+ fn test_registry_register_custom_token() {
398
+ let mut reg = TokenRegistry::empty();
399
+ reg.register(TokenInfo {
400
+ symbol: "MYTOKEN".to_string(),
401
+ decimals: 8,
402
+ chain_id: 137,
403
+ address: None,
404
+ });
405
+ let t = reg.get("MYTOKEN").expect("custom token");
406
+ assert_eq!(t.chain_id, 137);
407
+ assert_eq!(t.decimals, 8);
408
+ }
409
+
410
+ // ── TokenAmount ────────────────────────────────────────────────────────
411
+
412
+ #[test]
413
+ fn test_from_human_usdc_one_dollar() {
414
+ let one = TokenAmount::from_human(1.0, 6);
415
+ assert_eq!(one.raw, U256::from(1_000_000u64));
416
+ }
417
+
418
+ #[test]
419
+ fn test_from_human_eth_one() {
420
+ let one_eth = TokenAmount::from_human(1.0, 18);
421
+ assert_eq!(one_eth.raw, U256::from(1_000_000_000_000_000_000u128));
422
+ }
423
+
424
+ #[test]
425
+ fn test_to_human_round_trip() {
426
+ let human = 123.456_789;
427
+ let amt = TokenAmount::from_human(human, 6);
428
+ let back = amt.to_human();
429
+ assert!(
430
+ (back - human).abs() < 0.000_001,
431
+ "round-trip: got {back}, want ~{human}"
432
+ );
433
+ }
434
+
435
+ #[test]
436
+ fn test_from_human_zero() {
437
+ let zero = TokenAmount::from_human(0.0, 6);
438
+ assert!(zero.is_zero());
439
+ }
440
+
441
+ #[test]
442
+ fn test_add_same_decimals() {
443
+ let a = TokenAmount::from_human(1.0, 6);
444
+ let b = TokenAmount::from_human(2.0, 6);
445
+ let sum = a.add(&b).expect("addition should succeed");
446
+ assert_eq!(sum, TokenAmount::from_human(3.0, 6));
447
+ }
448
+
449
+ #[test]
450
+ fn test_sub_valid() {
451
+ let a = TokenAmount::from_human(5.0, 6);
452
+ let b = TokenAmount::from_human(2.0, 6);
453
+ let diff = a.sub(&b).expect("subtraction should succeed");
454
+ assert_eq!(diff, TokenAmount::from_human(3.0, 6));
455
+ }
456
+
457
+ #[test]
458
+ fn test_sub_underflow_returns_err() {
459
+ let a = TokenAmount::from_human(1.0, 6);
460
+ let b = TokenAmount::from_human(2.0, 6);
461
+ let result = a.sub(&b);
462
+ assert!(result.is_err());
463
+ assert_eq!(result.unwrap_err(), TokenError::Overflow);
464
+ }
465
+
466
+ #[test]
467
+ fn test_add_decimal_mismatch_returns_err() {
468
+ let a = TokenAmount::from_human(1.0, 6);
469
+ let b = TokenAmount::from_human(1.0, 18);
470
+ let result = a.add(&b);
471
+ assert!(matches!(result, Err(TokenError::DecimalMismatch { .. })));
472
+ }
473
+
474
+ #[test]
475
+ fn test_checked_mul_bps_50bps() {
476
+ // 50 bps = 0.5% of 1000 USDC = 5 USDC
477
+ let amount = TokenAmount::from_human(1000.0, 6);
478
+ let fee = amount.checked_mul_bps(50).expect("no overflow");
479
+ let expected = TokenAmount::from_human(5.0, 6);
480
+ assert_eq!(fee, expected);
481
+ }
482
+
483
+ #[test]
484
+ fn test_checked_mul_bps_100bps_is_1pct() {
485
+ let amount = TokenAmount::from_human(200.0, 6);
486
+ let result = amount.checked_mul_bps(100).expect("no overflow");
487
+ let expected = TokenAmount::from_human(2.0, 6);
488
+ assert_eq!(result, expected);
489
+ }
490
+
491
+ #[test]
492
+ fn test_checked_mul_bps_zero() {
493
+ let amount = TokenAmount::from_human(1000.0, 6);
494
+ let result = amount.checked_mul_bps(0).expect("no overflow");
495
+ assert!(result.is_zero());
496
+ }
497
+
498
+ #[test]
499
+ fn test_display_shows_human_amount() {
500
+ let amt = TokenAmount::from_human(42.5, 6);
501
+ let s = format!("{amt}");
502
+ assert!(s.contains("42.5"), "display: {s}");
503
+ }
504
+
505
+ #[test]
506
+ fn test_partial_ord_same_decimals() {
507
+ let a = TokenAmount::from_human(1.0, 6);
508
+ let b = TokenAmount::from_human(2.0, 6);
509
+ assert!(a < b);
510
+ assert!(b > a);
511
+ assert!(a <= a.clone());
512
+ }
513
+
514
+ #[test]
515
+ fn test_serde_round_trip() {
516
+ let amt = TokenAmount::from_human(99.99, 6);
517
+ let json = serde_json::to_string(&amt).expect("serialize");
518
+ let back: TokenAmount = serde_json::from_str(&json).expect("deserialize");
519
+ assert_eq!(amt, back);
520
+ }
521
+
522
+ #[test]
523
+ fn test_token_info_serde_round_trip() {
524
+ let info = TokenInfo {
525
+ symbol: "USDC".to_string(),
526
+ decimals: 6,
527
+ chain_id: 1,
528
+ address: Some(address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")),
529
+ };
530
+ let json = serde_json::to_string(&info).expect("serialize");
531
+ let back: TokenInfo = serde_json::from_str(&json).expect("deserialize");
532
+ assert_eq!(info, back);
533
+ }
534
+ }
@@ -0,0 +1,23 @@
1
+ [package]
2
+ name = "clawpowers-verification"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+
7
+ [features]
8
+ default = ["native"]
9
+ native = ["clawpowers-canonical/native"]
10
+ wasm = ["clawpowers-canonical/wasm"]
11
+
12
+ [dependencies]
13
+ serde = { workspace = true }
14
+ serde_json = { workspace = true }
15
+ sha2 = { workspace = true }
16
+ thiserror = { workspace = true }
17
+ tracing = { workspace = true }
18
+ chrono = { workspace = true }
19
+ uuid = { workspace = true }
20
+ clawpowers-canonical = { path = "../canonical", default-features = false }
21
+
22
+ [dev-dependencies]
23
+ rusqlite = { workspace = true }