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.
- package/CHANGELOG.md +186 -160
- package/COMPATIBILITY.md +48 -13
- package/KNOWN_LIMITATIONS.md +20 -19
- package/LICENSE +44 -44
- package/LICENSING.md +10 -10
- package/README.md +486 -462
- package/SECURITY.md +52 -52
- package/dist/index.d.ts +17 -5
- package/dist/index.js +187 -92
- package/dist/index.js.map +1 -1
- package/native/Cargo.lock +4927 -4927
- package/native/Cargo.toml +73 -73
- package/native/crates/canonical/Cargo.toml +24 -24
- package/native/crates/canonical/src/lib.rs +677 -673
- package/native/crates/compression/Cargo.toml +20 -20
- package/native/crates/compression/benches/compression_bench.rs +42 -42
- package/native/crates/compression/src/lib.rs +393 -393
- package/native/crates/evm-eth/Cargo.toml +13 -13
- package/native/crates/evm-eth/src/lib.rs +105 -105
- package/native/crates/fee/Cargo.toml +15 -15
- package/native/crates/fee/src/lib.rs +281 -281
- package/native/crates/index/Cargo.toml +16 -16
- package/native/crates/index/src/lib.rs +277 -277
- package/native/crates/policy/Cargo.toml +17 -17
- package/native/crates/policy/src/lib.rs +614 -614
- package/native/crates/security/Cargo.toml +22 -22
- package/native/crates/security/src/lib.rs +478 -478
- package/native/crates/tokens/Cargo.toml +13 -13
- package/native/crates/tokens/src/lib.rs +534 -534
- package/native/crates/verification/Cargo.toml +23 -23
- package/native/crates/verification/src/lib.rs +333 -333
- package/native/crates/wallet/Cargo.toml +20 -20
- package/native/crates/wallet/src/lib.rs +261 -261
- package/native/crates/x402/Cargo.toml +30 -30
- package/native/crates/x402/src/lib.rs +423 -423
- package/native/ffi/Cargo.toml +34 -34
- package/native/ffi/build.rs +4 -4
- package/native/ffi/src/lib.rs +352 -352
- package/native/ffi/tests/integration.rs +354 -354
- package/native/pyo3/Cargo.toml +26 -26
- package/native/pyo3/pyproject.toml +16 -16
- package/native/pyo3/src/lib.rs +407 -407
- package/native/pyo3/tests/test_smoke.py +180 -180
- package/native/wasm/Cargo.toml +47 -44
- package/native/wasm/pkg/.gitignore +6 -6
- package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -208
- package/native/wasm/pkg/clawpowers_wasm.js +872 -872
- package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -40
- package/native/wasm/pkg/package.json +16 -16
- package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -143
- package/native/wasm/pkg-node/clawpowers_wasm.js +798 -798
- package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -40
- package/native/wasm/pkg-node/package.json +12 -12
- package/native/wasm/src/lib.rs +433 -433
- package/package.json +13 -8
- package/scripts/build-wasm.mjs +59 -0
- package/scripts/generate_hermes_wrappers.py +211 -0
- package/scripts/hermes_wrapper_overrides.json +184 -0
- package/scripts/run-python-script.mjs +48 -0
- package/scripts/verify-consumer-install.mjs +109 -0
- package/scripts/verify-wasm-artifacts.mjs +26 -3
- package/scripts/verify_hermes_wrappers.py +154 -0
- package/skill.json +20 -0
- package/skills/1password/SKILL.md +34 -0
- package/skills/README.md +44 -0
- package/skills/agent-nexus-2/SKILL.md +34 -0
- package/skills/apple-notes/SKILL.md +34 -0
- package/skills/apple-reminders/SKILL.md +34 -0
- package/skills/autoresearch/SKILL.md +43 -0
- package/skills/bear-notes/SKILL.md +34 -0
- package/skills/blogwatcher/SKILL.md +34 -0
- package/skills/blucli/SKILL.md +34 -0
- package/skills/bluebubbles/SKILL.md +34 -0
- package/skills/business-strategy/SKILL.md +41 -0
- package/skills/camsnap/SKILL.md +34 -0
- package/skills/canvas/SKILL.md +34 -0
- package/skills/clawhub/SKILL.md +34 -0
- package/skills/coding-agent/SKILL.md +34 -0
- package/skills/coding-discipline.skill/SKILL.md +34 -0
- package/skills/content-writer/SKILL.md +41 -0
- package/skills/discord/SKILL.md +34 -0
- package/skills/eightctl/SKILL.md +34 -0
- package/skills/execution-validation.skill/SKILL.md +34 -0
- package/skills/gemini/SKILL.md +34 -0
- package/skills/gh-issues/SKILL.md +34 -0
- package/skills/gifgrep/SKILL.md +34 -0
- package/skills/github/SKILL.md +41 -0
- package/skills/gog/SKILL.md +34 -0
- package/skills/goplaces/SKILL.md +34 -0
- package/skills/healthcheck/SKILL.md +34 -0
- package/skills/himalaya/SKILL.md +34 -0
- package/skills/humanize/SKILL.md +41 -0
- package/skills/imsg/SKILL.md +34 -0
- package/skills/itp/SKILL.md +112 -0
- package/skills/mcporter/SKILL.md +34 -0
- package/skills/model-usage/SKILL.md +34 -0
- package/skills/nano-pdf/SKILL.md +34 -0
- package/skills/node-connect/SKILL.md +34 -0
- package/skills/notion/SKILL.md +34 -0
- package/skills/obsidian/SKILL.md +34 -0
- package/skills/openai-whisper/SKILL.md +34 -0
- package/skills/openai-whisper-api/SKILL.md +34 -0
- package/skills/openhue/SKILL.md +34 -0
- package/skills/oracle/SKILL.md +34 -0
- package/skills/ordercli/SKILL.md +34 -0
- package/skills/peekaboo/SKILL.md +34 -0
- package/skills/polyclaw/SKILL.md +34 -0
- package/skills/prospector/SKILL.md +41 -0
- package/skills/rsi.skill/SKILL.md +34 -0
- package/skills/sag/SKILL.md +34 -0
- package/skills/security/SKILL.md +41 -0
- package/skills/session-logs/SKILL.md +34 -0
- package/skills/sherpa-onnx-tts/SKILL.md +34 -0
- package/skills/skill-creator/SKILL.md +34 -0
- package/skills/slack/SKILL.md +34 -0
- package/skills/songsee/SKILL.md +34 -0
- package/skills/sonoscli/SKILL.md +34 -0
- package/skills/spotify-player/SKILL.md +34 -0
- package/skills/strykr-prism/SKILL.md +41 -0
- package/skills/summarize/SKILL.md +34 -0
- package/skills/taskbridge/SKILL.md +34 -0
- package/skills/things-mac/SKILL.md +34 -0
- package/skills/tmux/SKILL.md +34 -0
- package/skills/trello/SKILL.md +34 -0
- package/skills/validator-agent/SKILL.md +41 -0
- package/skills/video-frames/SKILL.md +34 -0
- package/skills/voice-call/SKILL.md +34 -0
- package/skills/wacli/SKILL.md +34 -0
- package/skills/weather/SKILL.md +34 -0
- package/skills/webmcp-payments/SKILL.md +41 -0
- package/skills/xurl/SKILL.md +34 -0
- package/src/skills/catalog.ts +435 -435
- package/src/skills/executor.ts +56 -56
- package/src/skills/index.ts +3 -3
- package/src/skills/itp/SKILL.md +112 -112
- package/src/skills/loader.ts +262 -193
- package/native/ffi/index.node +0 -0
- package/native/wasm/pkg-node/.gitignore +0 -6
|
@@ -1,281 +1,281 @@
|
|
|
1
|
-
//! clawpowers-fee — Fee schedule calculation for transactions and swaps.
|
|
2
|
-
//!
|
|
3
|
-
//! [`FeeSchedule`] holds basis-point rates for different operation types and
|
|
4
|
-
//! computes the fee for a given [`TokenAmount`].
|
|
5
|
-
|
|
6
|
-
use alloy_primitives::Address;
|
|
7
|
-
use clawpowers_tokens::{TokenAmount, TokenError};
|
|
8
|
-
use serde::{Deserialize, Serialize};
|
|
9
|
-
use thiserror::Error;
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Constants
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
/// Default transaction fee: 77 bps = 0.77%.
|
|
16
|
-
pub const DEFAULT_TX_FEE_BPS: u64 = 77;
|
|
17
|
-
|
|
18
|
-
/// Default swap fee: 30 bps = 0.30%.
|
|
19
|
-
pub const DEFAULT_SWAP_FEE_BPS: u64 = 30;
|
|
20
|
-
|
|
21
|
-
/// Placeholder fee recipient address used by [`FeeSchedule::default`].
|
|
22
|
-
pub const PLACEHOLDER_FEE_RECIPIENT: Address = Address::ZERO;
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// FeeType
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
/// Fee classification.
|
|
29
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
30
|
-
#[serde(rename_all = "snake_case")]
|
|
31
|
-
pub enum FeeType {
|
|
32
|
-
/// Standard on-chain transaction fee.
|
|
33
|
-
Transaction,
|
|
34
|
-
/// DEX swap fee.
|
|
35
|
-
Swap,
|
|
36
|
-
/// Custom fee with explicit basis points.
|
|
37
|
-
Custom(u64),
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Errors
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
/// Errors produced by fee operations.
|
|
45
|
-
#[derive(Debug, Error, PartialEq, Eq)]
|
|
46
|
-
pub enum FeeError {
|
|
47
|
-
/// Token arithmetic failed.
|
|
48
|
-
#[error("token arithmetic error: {0}")]
|
|
49
|
-
Token(String),
|
|
50
|
-
/// Basis-point rate exceeds 10 000 (100%).
|
|
51
|
-
#[error("invalid basis points: {0} (max 10000)")]
|
|
52
|
-
InvalidBps(u64),
|
|
53
|
-
/// Computed fee would exceed the gross amount.
|
|
54
|
-
#[error("fee ({fee_bps} bps) would exceed gross amount")]
|
|
55
|
-
FeeExceedsAmount {
|
|
56
|
-
/// The basis-point rate that caused the violation.
|
|
57
|
-
fee_bps: u64,
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
impl From<TokenError> for FeeError {
|
|
62
|
-
fn from(e: TokenError) -> Self {
|
|
63
|
-
FeeError::Token(e.to_string())
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// FeeCalculation
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
/// The result of a fee calculation.
|
|
72
|
-
#[derive(Debug, Clone)]
|
|
73
|
-
pub struct FeeCalculation {
|
|
74
|
-
/// The original, pre-fee amount.
|
|
75
|
-
pub gross_amount: TokenAmount,
|
|
76
|
-
/// The fee portion deducted from `gross_amount`.
|
|
77
|
-
pub fee_amount: TokenAmount,
|
|
78
|
-
/// The amount remaining after fee deduction.
|
|
79
|
-
pub net_amount: TokenAmount,
|
|
80
|
-
/// The address that will receive the fee.
|
|
81
|
-
pub fee_recipient: Address,
|
|
82
|
-
/// The type of fee that was applied.
|
|
83
|
-
pub fee_type: FeeType,
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// FeeSchedule
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
|
|
90
|
-
/// Configures fee rates and the recipient address for the protocol.
|
|
91
|
-
#[derive(Debug, Clone)]
|
|
92
|
-
pub struct FeeSchedule {
|
|
93
|
-
/// Transaction fee in basis points (default: 77 = 0.77%).
|
|
94
|
-
pub tx_fee_bps: u64,
|
|
95
|
-
/// Swap fee in basis points (default: 30 = 0.30%).
|
|
96
|
-
pub swap_fee_bps: u64,
|
|
97
|
-
/// Address that accumulates collected fees.
|
|
98
|
-
pub fee_recipient: Address,
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
impl Default for FeeSchedule {
|
|
102
|
-
fn default() -> Self {
|
|
103
|
-
Self {
|
|
104
|
-
tx_fee_bps: DEFAULT_TX_FEE_BPS,
|
|
105
|
-
swap_fee_bps: DEFAULT_SWAP_FEE_BPS,
|
|
106
|
-
fee_recipient: PLACEHOLDER_FEE_RECIPIENT,
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
impl FeeSchedule {
|
|
112
|
-
/// Creates a new [`FeeSchedule`] with explicit parameters.
|
|
113
|
-
pub fn new(tx_fee_bps: u64, swap_fee_bps: u64, fee_recipient: Address) -> Self {
|
|
114
|
-
Self {
|
|
115
|
-
tx_fee_bps,
|
|
116
|
-
swap_fee_bps,
|
|
117
|
-
fee_recipient,
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/// Returns the effective basis-point rate for the given `fee_type`.
|
|
122
|
-
fn bps_for(&self, fee_type: &FeeType) -> u64 {
|
|
123
|
-
match fee_type {
|
|
124
|
-
FeeType::Transaction => self.tx_fee_bps,
|
|
125
|
-
FeeType::Swap => self.swap_fee_bps,
|
|
126
|
-
FeeType::Custom(bps) => *bps,
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/// Calculates a fee for `amount` and the given `fee_type`.
|
|
131
|
-
///
|
|
132
|
-
/// # Errors
|
|
133
|
-
///
|
|
134
|
-
/// - [`FeeError::InvalidBps`] — rate exceeds 10 000.
|
|
135
|
-
/// - [`FeeError::Token`] — arithmetic overflow.
|
|
136
|
-
/// - [`FeeError::FeeExceedsAmount`] — fee ≥ gross.
|
|
137
|
-
pub fn calculate(
|
|
138
|
-
&self,
|
|
139
|
-
amount: TokenAmount,
|
|
140
|
-
fee_type: FeeType,
|
|
141
|
-
) -> Result<FeeCalculation, FeeError> {
|
|
142
|
-
let bps = self.bps_for(&fee_type);
|
|
143
|
-
if bps > 10_000 {
|
|
144
|
-
return Err(FeeError::InvalidBps(bps));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let fee_amount = amount
|
|
148
|
-
.checked_mul_bps(bps)
|
|
149
|
-
.ok_or_else(|| FeeError::Token("overflow in checked_mul_bps".to_string()))?;
|
|
150
|
-
|
|
151
|
-
let net_amount = amount
|
|
152
|
-
.sub(&fee_amount)
|
|
153
|
-
.map_err(|_| FeeError::FeeExceedsAmount { fee_bps: bps })?;
|
|
154
|
-
|
|
155
|
-
Ok(FeeCalculation {
|
|
156
|
-
gross_amount: amount,
|
|
157
|
-
fee_amount,
|
|
158
|
-
net_amount,
|
|
159
|
-
fee_recipient: self.fee_recipient,
|
|
160
|
-
fee_type,
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// Tests
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
#[cfg(test)]
|
|
170
|
-
mod tests {
|
|
171
|
-
use super::*;
|
|
172
|
-
use alloy_primitives::Address;
|
|
173
|
-
|
|
174
|
-
fn usdc(amount: f64) -> TokenAmount {
|
|
175
|
-
TokenAmount::from_human(amount, 6)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
fn eth(amount: f64) -> TokenAmount {
|
|
179
|
-
TokenAmount::from_human(amount, 18)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
#[test]
|
|
183
|
-
fn default_fee_schedule_values() {
|
|
184
|
-
let s = FeeSchedule::default();
|
|
185
|
-
assert_eq!(s.tx_fee_bps, 77);
|
|
186
|
-
assert_eq!(s.swap_fee_bps, 30);
|
|
187
|
-
assert_eq!(s.fee_recipient, Address::ZERO);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
#[test]
|
|
191
|
-
fn tx_fee_usdc_1000() {
|
|
192
|
-
let s = FeeSchedule::default();
|
|
193
|
-
let calc = s.calculate(usdc(1000.0), FeeType::Transaction).unwrap();
|
|
194
|
-
// 1000 USDC × 77/10000 = 7.7 USDC
|
|
195
|
-
assert!((calc.fee_amount.to_human() - 7.7).abs() < 0.000_001);
|
|
196
|
-
assert!((calc.net_amount.to_human() - 992.3).abs() < 0.000_001);
|
|
197
|
-
assert_eq!(calc.fee_type, FeeType::Transaction);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
#[test]
|
|
201
|
-
fn tx_fee_usdc_1() {
|
|
202
|
-
let s = FeeSchedule::default();
|
|
203
|
-
let calc = s.calculate(usdc(1.0), FeeType::Transaction).unwrap();
|
|
204
|
-
// 1 USDC × 77/10000 = 0.0077 USDC
|
|
205
|
-
assert!((calc.fee_amount.to_human() - 0.0077).abs() < 0.000_001);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
#[test]
|
|
209
|
-
fn swap_fee_usdc_1000() {
|
|
210
|
-
let s = FeeSchedule::default();
|
|
211
|
-
let calc = s.calculate(usdc(1000.0), FeeType::Swap).unwrap();
|
|
212
|
-
// 1000 USDC × 30/10000 = 3 USDC
|
|
213
|
-
assert!((calc.fee_amount.to_human() - 3.0).abs() < 0.000_001);
|
|
214
|
-
assert_eq!(calc.fee_type, FeeType::Swap);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
#[test]
|
|
218
|
-
fn swap_fee_custom_50bps() {
|
|
219
|
-
let s = FeeSchedule::new(30, 50, Address::ZERO);
|
|
220
|
-
let calc = s.calculate(usdc(200.0), FeeType::Swap).unwrap();
|
|
221
|
-
// 200 USDC × 50/10000 = 1 USDC
|
|
222
|
-
assert!((calc.fee_amount.to_human() - 1.0).abs() < 0.000_001);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
#[test]
|
|
226
|
-
fn tx_fee_eth_1() {
|
|
227
|
-
let s = FeeSchedule::default();
|
|
228
|
-
let calc = s.calculate(eth(1.0), FeeType::Transaction).unwrap();
|
|
229
|
-
// 1 ETH × 77/10000 = 0.0077 ETH
|
|
230
|
-
assert!((calc.fee_amount.to_human() - 0.0077).abs() < 0.000_001);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
#[test]
|
|
234
|
-
fn swap_fee_eth_10() {
|
|
235
|
-
let s = FeeSchedule::default();
|
|
236
|
-
let calc = s.calculate(eth(10.0), FeeType::Swap).unwrap();
|
|
237
|
-
// 10 ETH × 30/10000 = 0.03 ETH
|
|
238
|
-
assert!((calc.fee_amount.to_human() - 0.03).abs() < 0.000_001);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
#[test]
|
|
242
|
-
fn zero_amount_returns_zero_fee() {
|
|
243
|
-
let s = FeeSchedule::default();
|
|
244
|
-
let calc = s.calculate(usdc(0.0), FeeType::Transaction).unwrap();
|
|
245
|
-
assert!(calc.fee_amount.is_zero());
|
|
246
|
-
assert!(calc.net_amount.is_zero());
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
#[test]
|
|
250
|
-
fn custom_fee_type_100bps() {
|
|
251
|
-
let s = FeeSchedule::default();
|
|
252
|
-
let calc = s.calculate(usdc(100.0), FeeType::Custom(100)).unwrap();
|
|
253
|
-
// 100 USDC × 100/10000 = 1 USDC
|
|
254
|
-
assert!((calc.fee_amount.to_human() - 1.0).abs() < 0.000_001);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
#[test]
|
|
258
|
-
fn fee_recipient_propagated() {
|
|
259
|
-
use alloy_primitives::address;
|
|
260
|
-
let recipient = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
|
|
261
|
-
let s = FeeSchedule::new(77, 30, recipient);
|
|
262
|
-
let calc = s.calculate(usdc(500.0), FeeType::Transaction).unwrap();
|
|
263
|
-
assert_eq!(calc.fee_recipient, recipient);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
#[test]
|
|
267
|
-
fn gross_equals_fee_plus_net() {
|
|
268
|
-
let s = FeeSchedule::default();
|
|
269
|
-
let gross = usdc(123.456789);
|
|
270
|
-
let calc = s.calculate(gross.clone(), FeeType::Swap).unwrap();
|
|
271
|
-
let reconstructed = calc.fee_amount.add(&calc.net_amount).unwrap();
|
|
272
|
-
assert_eq!(reconstructed, calc.gross_amount);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
#[test]
|
|
276
|
-
fn bps_over_10000_is_rejected() {
|
|
277
|
-
let s = FeeSchedule::new(10_001, 30, Address::ZERO);
|
|
278
|
-
let err = s.calculate(usdc(1.0), FeeType::Transaction).unwrap_err();
|
|
279
|
-
assert_eq!(err, FeeError::InvalidBps(10_001));
|
|
280
|
-
}
|
|
281
|
-
}
|
|
1
|
+
//! clawpowers-fee — Fee schedule calculation for transactions and swaps.
|
|
2
|
+
//!
|
|
3
|
+
//! [`FeeSchedule`] holds basis-point rates for different operation types and
|
|
4
|
+
//! computes the fee for a given [`TokenAmount`].
|
|
5
|
+
|
|
6
|
+
use alloy_primitives::Address;
|
|
7
|
+
use clawpowers_tokens::{TokenAmount, TokenError};
|
|
8
|
+
use serde::{Deserialize, Serialize};
|
|
9
|
+
use thiserror::Error;
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Constants
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/// Default transaction fee: 77 bps = 0.77%.
|
|
16
|
+
pub const DEFAULT_TX_FEE_BPS: u64 = 77;
|
|
17
|
+
|
|
18
|
+
/// Default swap fee: 30 bps = 0.30%.
|
|
19
|
+
pub const DEFAULT_SWAP_FEE_BPS: u64 = 30;
|
|
20
|
+
|
|
21
|
+
/// Placeholder fee recipient address used by [`FeeSchedule::default`].
|
|
22
|
+
pub const PLACEHOLDER_FEE_RECIPIENT: Address = Address::ZERO;
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// FeeType
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/// Fee classification.
|
|
29
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
30
|
+
#[serde(rename_all = "snake_case")]
|
|
31
|
+
pub enum FeeType {
|
|
32
|
+
/// Standard on-chain transaction fee.
|
|
33
|
+
Transaction,
|
|
34
|
+
/// DEX swap fee.
|
|
35
|
+
Swap,
|
|
36
|
+
/// Custom fee with explicit basis points.
|
|
37
|
+
Custom(u64),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Errors
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/// Errors produced by fee operations.
|
|
45
|
+
#[derive(Debug, Error, PartialEq, Eq)]
|
|
46
|
+
pub enum FeeError {
|
|
47
|
+
/// Token arithmetic failed.
|
|
48
|
+
#[error("token arithmetic error: {0}")]
|
|
49
|
+
Token(String),
|
|
50
|
+
/// Basis-point rate exceeds 10 000 (100%).
|
|
51
|
+
#[error("invalid basis points: {0} (max 10000)")]
|
|
52
|
+
InvalidBps(u64),
|
|
53
|
+
/// Computed fee would exceed the gross amount.
|
|
54
|
+
#[error("fee ({fee_bps} bps) would exceed gross amount")]
|
|
55
|
+
FeeExceedsAmount {
|
|
56
|
+
/// The basis-point rate that caused the violation.
|
|
57
|
+
fee_bps: u64,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
impl From<TokenError> for FeeError {
|
|
62
|
+
fn from(e: TokenError) -> Self {
|
|
63
|
+
FeeError::Token(e.to_string())
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// FeeCalculation
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/// The result of a fee calculation.
|
|
72
|
+
#[derive(Debug, Clone)]
|
|
73
|
+
pub struct FeeCalculation {
|
|
74
|
+
/// The original, pre-fee amount.
|
|
75
|
+
pub gross_amount: TokenAmount,
|
|
76
|
+
/// The fee portion deducted from `gross_amount`.
|
|
77
|
+
pub fee_amount: TokenAmount,
|
|
78
|
+
/// The amount remaining after fee deduction.
|
|
79
|
+
pub net_amount: TokenAmount,
|
|
80
|
+
/// The address that will receive the fee.
|
|
81
|
+
pub fee_recipient: Address,
|
|
82
|
+
/// The type of fee that was applied.
|
|
83
|
+
pub fee_type: FeeType,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// FeeSchedule
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/// Configures fee rates and the recipient address for the protocol.
|
|
91
|
+
#[derive(Debug, Clone)]
|
|
92
|
+
pub struct FeeSchedule {
|
|
93
|
+
/// Transaction fee in basis points (default: 77 = 0.77%).
|
|
94
|
+
pub tx_fee_bps: u64,
|
|
95
|
+
/// Swap fee in basis points (default: 30 = 0.30%).
|
|
96
|
+
pub swap_fee_bps: u64,
|
|
97
|
+
/// Address that accumulates collected fees.
|
|
98
|
+
pub fee_recipient: Address,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl Default for FeeSchedule {
|
|
102
|
+
fn default() -> Self {
|
|
103
|
+
Self {
|
|
104
|
+
tx_fee_bps: DEFAULT_TX_FEE_BPS,
|
|
105
|
+
swap_fee_bps: DEFAULT_SWAP_FEE_BPS,
|
|
106
|
+
fee_recipient: PLACEHOLDER_FEE_RECIPIENT,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
impl FeeSchedule {
|
|
112
|
+
/// Creates a new [`FeeSchedule`] with explicit parameters.
|
|
113
|
+
pub fn new(tx_fee_bps: u64, swap_fee_bps: u64, fee_recipient: Address) -> Self {
|
|
114
|
+
Self {
|
|
115
|
+
tx_fee_bps,
|
|
116
|
+
swap_fee_bps,
|
|
117
|
+
fee_recipient,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Returns the effective basis-point rate for the given `fee_type`.
|
|
122
|
+
fn bps_for(&self, fee_type: &FeeType) -> u64 {
|
|
123
|
+
match fee_type {
|
|
124
|
+
FeeType::Transaction => self.tx_fee_bps,
|
|
125
|
+
FeeType::Swap => self.swap_fee_bps,
|
|
126
|
+
FeeType::Custom(bps) => *bps,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Calculates a fee for `amount` and the given `fee_type`.
|
|
131
|
+
///
|
|
132
|
+
/// # Errors
|
|
133
|
+
///
|
|
134
|
+
/// - [`FeeError::InvalidBps`] — rate exceeds 10 000.
|
|
135
|
+
/// - [`FeeError::Token`] — arithmetic overflow.
|
|
136
|
+
/// - [`FeeError::FeeExceedsAmount`] — fee ≥ gross.
|
|
137
|
+
pub fn calculate(
|
|
138
|
+
&self,
|
|
139
|
+
amount: TokenAmount,
|
|
140
|
+
fee_type: FeeType,
|
|
141
|
+
) -> Result<FeeCalculation, FeeError> {
|
|
142
|
+
let bps = self.bps_for(&fee_type);
|
|
143
|
+
if bps > 10_000 {
|
|
144
|
+
return Err(FeeError::InvalidBps(bps));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let fee_amount = amount
|
|
148
|
+
.checked_mul_bps(bps)
|
|
149
|
+
.ok_or_else(|| FeeError::Token("overflow in checked_mul_bps".to_string()))?;
|
|
150
|
+
|
|
151
|
+
let net_amount = amount
|
|
152
|
+
.sub(&fee_amount)
|
|
153
|
+
.map_err(|_| FeeError::FeeExceedsAmount { fee_bps: bps })?;
|
|
154
|
+
|
|
155
|
+
Ok(FeeCalculation {
|
|
156
|
+
gross_amount: amount,
|
|
157
|
+
fee_amount,
|
|
158
|
+
net_amount,
|
|
159
|
+
fee_recipient: self.fee_recipient,
|
|
160
|
+
fee_type,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Tests
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
#[cfg(test)]
|
|
170
|
+
mod tests {
|
|
171
|
+
use super::*;
|
|
172
|
+
use alloy_primitives::Address;
|
|
173
|
+
|
|
174
|
+
fn usdc(amount: f64) -> TokenAmount {
|
|
175
|
+
TokenAmount::from_human(amount, 6)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn eth(amount: f64) -> TokenAmount {
|
|
179
|
+
TokenAmount::from_human(amount, 18)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn default_fee_schedule_values() {
|
|
184
|
+
let s = FeeSchedule::default();
|
|
185
|
+
assert_eq!(s.tx_fee_bps, 77);
|
|
186
|
+
assert_eq!(s.swap_fee_bps, 30);
|
|
187
|
+
assert_eq!(s.fee_recipient, Address::ZERO);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn tx_fee_usdc_1000() {
|
|
192
|
+
let s = FeeSchedule::default();
|
|
193
|
+
let calc = s.calculate(usdc(1000.0), FeeType::Transaction).unwrap();
|
|
194
|
+
// 1000 USDC × 77/10000 = 7.7 USDC
|
|
195
|
+
assert!((calc.fee_amount.to_human() - 7.7).abs() < 0.000_001);
|
|
196
|
+
assert!((calc.net_amount.to_human() - 992.3).abs() < 0.000_001);
|
|
197
|
+
assert_eq!(calc.fee_type, FeeType::Transaction);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[test]
|
|
201
|
+
fn tx_fee_usdc_1() {
|
|
202
|
+
let s = FeeSchedule::default();
|
|
203
|
+
let calc = s.calculate(usdc(1.0), FeeType::Transaction).unwrap();
|
|
204
|
+
// 1 USDC × 77/10000 = 0.0077 USDC
|
|
205
|
+
assert!((calc.fee_amount.to_human() - 0.0077).abs() < 0.000_001);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#[test]
|
|
209
|
+
fn swap_fee_usdc_1000() {
|
|
210
|
+
let s = FeeSchedule::default();
|
|
211
|
+
let calc = s.calculate(usdc(1000.0), FeeType::Swap).unwrap();
|
|
212
|
+
// 1000 USDC × 30/10000 = 3 USDC
|
|
213
|
+
assert!((calc.fee_amount.to_human() - 3.0).abs() < 0.000_001);
|
|
214
|
+
assert_eq!(calc.fee_type, FeeType::Swap);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn swap_fee_custom_50bps() {
|
|
219
|
+
let s = FeeSchedule::new(30, 50, Address::ZERO);
|
|
220
|
+
let calc = s.calculate(usdc(200.0), FeeType::Swap).unwrap();
|
|
221
|
+
// 200 USDC × 50/10000 = 1 USDC
|
|
222
|
+
assert!((calc.fee_amount.to_human() - 1.0).abs() < 0.000_001);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[test]
|
|
226
|
+
fn tx_fee_eth_1() {
|
|
227
|
+
let s = FeeSchedule::default();
|
|
228
|
+
let calc = s.calculate(eth(1.0), FeeType::Transaction).unwrap();
|
|
229
|
+
// 1 ETH × 77/10000 = 0.0077 ETH
|
|
230
|
+
assert!((calc.fee_amount.to_human() - 0.0077).abs() < 0.000_001);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn swap_fee_eth_10() {
|
|
235
|
+
let s = FeeSchedule::default();
|
|
236
|
+
let calc = s.calculate(eth(10.0), FeeType::Swap).unwrap();
|
|
237
|
+
// 10 ETH × 30/10000 = 0.03 ETH
|
|
238
|
+
assert!((calc.fee_amount.to_human() - 0.03).abs() < 0.000_001);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn zero_amount_returns_zero_fee() {
|
|
243
|
+
let s = FeeSchedule::default();
|
|
244
|
+
let calc = s.calculate(usdc(0.0), FeeType::Transaction).unwrap();
|
|
245
|
+
assert!(calc.fee_amount.is_zero());
|
|
246
|
+
assert!(calc.net_amount.is_zero());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#[test]
|
|
250
|
+
fn custom_fee_type_100bps() {
|
|
251
|
+
let s = FeeSchedule::default();
|
|
252
|
+
let calc = s.calculate(usdc(100.0), FeeType::Custom(100)).unwrap();
|
|
253
|
+
// 100 USDC × 100/10000 = 1 USDC
|
|
254
|
+
assert!((calc.fee_amount.to_human() - 1.0).abs() < 0.000_001);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
fn fee_recipient_propagated() {
|
|
259
|
+
use alloy_primitives::address;
|
|
260
|
+
let recipient = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
|
|
261
|
+
let s = FeeSchedule::new(77, 30, recipient);
|
|
262
|
+
let calc = s.calculate(usdc(500.0), FeeType::Transaction).unwrap();
|
|
263
|
+
assert_eq!(calc.fee_recipient, recipient);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#[test]
|
|
267
|
+
fn gross_equals_fee_plus_net() {
|
|
268
|
+
let s = FeeSchedule::default();
|
|
269
|
+
let gross = usdc(123.456789);
|
|
270
|
+
let calc = s.calculate(gross.clone(), FeeType::Swap).unwrap();
|
|
271
|
+
let reconstructed = calc.fee_amount.add(&calc.net_amount).unwrap();
|
|
272
|
+
assert_eq!(reconstructed, calc.gross_amount);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn bps_over_10000_is_rejected() {
|
|
277
|
+
let s = FeeSchedule::new(10_001, 30, Address::ZERO);
|
|
278
|
+
let err = s.calculate(usdc(1.0), FeeType::Transaction).unwrap_err();
|
|
279
|
+
assert_eq!(err, FeeError::InvalidBps(10_001));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "clawpowers-index"
|
|
3
|
-
version.workspace = true
|
|
4
|
-
edition.workspace = true
|
|
5
|
-
license.workspace = true
|
|
6
|
-
|
|
7
|
-
[dependencies]
|
|
8
|
-
serde = { workspace = true }
|
|
9
|
-
serde_json = { workspace = true }
|
|
10
|
-
thiserror = { workspace = true }
|
|
11
|
-
tracing = { workspace = true }
|
|
12
|
-
uuid = { workspace = true }
|
|
13
|
-
clawpowers-compression = { path = "../compression" }
|
|
14
|
-
|
|
15
|
-
[dev-dependencies]
|
|
16
|
-
rand = { workspace = true }
|
|
1
|
+
[package]
|
|
2
|
+
name = "clawpowers-index"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
license.workspace = true
|
|
6
|
+
|
|
7
|
+
[dependencies]
|
|
8
|
+
serde = { workspace = true }
|
|
9
|
+
serde_json = { workspace = true }
|
|
10
|
+
thiserror = { workspace = true }
|
|
11
|
+
tracing = { workspace = true }
|
|
12
|
+
uuid = { workspace = true }
|
|
13
|
+
clawpowers-compression = { path = "../compression" }
|
|
14
|
+
|
|
15
|
+
[dev-dependencies]
|
|
16
|
+
rand = { workspace = true }
|