intents-1click-rule-engine 1.0.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.
package/README.md ADDED
@@ -0,0 +1,473 @@
1
+ # intents-1click-rule-engine
2
+
3
+ Fee configuration rule engine for NEAR Intents 1Click API. Matches swap requests against configurable rules to determine fees.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add intents-1click-rule-engine
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { RuleEngine, getTotalBps, calculateFee, type FeeConfig } from "intents-1click-rule-engine";
15
+
16
+ const feeConfig: FeeConfig = {
17
+ version: "1.0.0",
18
+ default_fee: { type: "bps", bps: 20, recipient: "fees.near" },
19
+ rules: [
20
+ {
21
+ id: "usdc-swaps",
22
+ enabled: true,
23
+ priority: 100,
24
+ match: {
25
+ in: { symbol: "USDC", blockchain: "*" },
26
+ out: { symbol: "USDC", blockchain: "*" },
27
+ },
28
+ fee: { type: "bps", bps: 10, recipient: "fees.near" },
29
+ },
30
+ ],
31
+ };
32
+
33
+ // Create engine (validates config, throws on error)
34
+ const engine = new RuleEngine(feeConfig);
35
+
36
+ // Initialize token registry
37
+ await engine.initialize();
38
+
39
+ // Match a swap request
40
+ const result = engine.match({
41
+ originAsset: "nep141:eth-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.omft.near",
42
+ destinationAsset: "nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near",
43
+ });
44
+
45
+ console.log(result.matched); // true
46
+ console.log(result.fee); // { type: "bps", bps: 10, recipient: "fees.near" }
47
+ console.log(result.rule?.id); // "usdc-swaps"
48
+ console.log(getTotalBps(result.fee)); // 10
49
+
50
+ // Calculate fee amount
51
+ const feeAmount = calculateFee("1000000", getTotalBps(result.fee)); // "1000" (0.10% of 1000000)
52
+ ```
53
+
54
+ ## Fee Calculation
55
+
56
+ The `calculateFee` and `calculateAmountAfterFee` functions validate their inputs:
57
+
58
+ ```typescript
59
+ // Valid usage
60
+ calculateFee("1000000", 20); // "2000" (0.20% of 1000000)
61
+ calculateFee(1000000n, 20); // "2000" (bigint input also works)
62
+ calculateAmountAfterFee("1000000", 20); // "998000" (amount minus fee)
63
+
64
+ // Input validation - these throw descriptive errors:
65
+ calculateFee("abc", 20); // Error: not a valid integer string
66
+ calculateFee("12.5", 20); // Error: not a valid integer string
67
+ calculateFee("-100", 20); // Error: must be non-negative
68
+ calculateFee("1000", -5); // Error: bps must be non-negative
69
+ calculateFee("1000", 10001); // Error: bps exceeds maximum of 10000 (100%)
70
+ calculateFee("1000", 20.5); // Error: bps must be an integer
71
+ ```
72
+
73
+ The `bps` parameter must be between 0 and 10000 (0% to 100%).
74
+
75
+ ## Token Registry
76
+
77
+ The engine fetches the token list from `https://1click.chaindefuser.com/v0/tokens` to resolve asset IDs to their `blockchain` and `symbol`. This allows rules to match by symbol/blockchain instead of exact asset IDs.
78
+
79
+ ```
80
+ Swap Request Token Registry Lookup
81
+ ───────────── ─────────────────────
82
+ originAsset: "nep141:eth-..." → { blockchain: "eth", symbol: "USDC" }
83
+ destinationAsset: "nep141:sol-..." → { blockchain: "sol", symbol: "USDC" }
84
+ ```
85
+
86
+ Call `engine.initialize()` before matching to fetch the token list. The list is cached for 1 hour by default.
87
+
88
+ ## Rule Matching
89
+
90
+ Rules are evaluated by priority (highest first). The first matching rule wins. If no rules match, `default_fee` is used.
91
+
92
+ Each rule can match on:
93
+
94
+ - `assetId` - exact asset identifier (string or array)
95
+ - `blockchain` - chain identifier (e.g., `"eth"`, `"polygon"`)
96
+ - `symbol` - token symbol (e.g., `"USDC"`, `"WBTC"`)
97
+
98
+ Special patterns:
99
+ - `"*"` - wildcard, matches any value
100
+ - `"!value"` - negation, matches anything except `value`
101
+ - `["a", "b"]` - array, matches any value in the list (OR logic)
102
+
103
+ **Important:** All pattern matching is **case-sensitive**. `"USDC"` will not match `"usdc"` or `"Usdc"`. Ensure your rules use the exact casing from the token registry.
104
+
105
+ ## Fee Structure
106
+
107
+ Each fee requires a `type`, `bps`, and `recipient`. Fees can be a single object or an array for multiple recipients:
108
+
109
+ ### Single fee
110
+
111
+ ```typescript
112
+ fee: { type: "bps", bps: 20, recipient: "fees.near" }
113
+ ```
114
+
115
+ ### Multiple fees (split between recipients)
116
+
117
+ Each recipient can have their own fee amount:
118
+
119
+ ```typescript
120
+ fee: [
121
+ { type: "bps", bps: 14, recipient: "fees.near" }, // 0.14%
122
+ { type: "bps", bps: 6, recipient: "partner.near" }, // 0.06%
123
+ ]
124
+ // Total: 0.20%
125
+ ```
126
+
127
+ ## Rule Examples
128
+
129
+ ### Match by symbol (any chain)
130
+
131
+ ```typescript
132
+ {
133
+ id: "usdc-to-usdc",
134
+ enabled: true,
135
+ priority: 100,
136
+ match: {
137
+ in: { symbol: "USDC" },
138
+ out: { symbol: "USDC" },
139
+ },
140
+ fee: { type: "bps", bps: 10, recipient: "fees.near" },
141
+ }
142
+ ```
143
+
144
+ ### Match by blockchain route
145
+
146
+ ```typescript
147
+ {
148
+ id: "eth-to-polygon",
149
+ enabled: true,
150
+ priority: 100,
151
+ match: {
152
+ in: { blockchain: "eth" },
153
+ out: { blockchain: "polygon" },
154
+ },
155
+ fee: { type: "bps", bps: 15, recipient: "fees.near" },
156
+ }
157
+ ```
158
+
159
+ ### Match exact asset pair
160
+
161
+ ```typescript
162
+ {
163
+ id: "eth-usdc-to-polygon-usdc",
164
+ enabled: true,
165
+ priority: 200,
166
+ match: {
167
+ in: { assetId: "nep141:eth-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.omft.near" },
168
+ out: { assetId: "nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near" },
169
+ },
170
+ fee: { type: "bps", bps: 5, recipient: "fees.near" },
171
+ }
172
+ ```
173
+
174
+ ### Mixed matching (blockchain + symbol)
175
+
176
+ ```typescript
177
+ {
178
+ id: "eth-usdc-to-any-usdc",
179
+ enabled: true,
180
+ priority: 150,
181
+ match: {
182
+ in: { blockchain: "eth", symbol: "USDC" },
183
+ out: { symbol: "USDC", blockchain: "*" },
184
+ },
185
+ fee: { type: "bps", bps: 8, recipient: "fees.near" },
186
+ }
187
+ ```
188
+
189
+ ### Wildcard for any token on specific chain
190
+
191
+ ```typescript
192
+ {
193
+ id: "anything-to-solana",
194
+ enabled: true,
195
+ priority: 50,
196
+ match: {
197
+ in: { blockchain: "*" },
198
+ out: { blockchain: "sol" },
199
+ },
200
+ fee: { type: "bps", bps: 25, recipient: "fees.near" },
201
+ }
202
+ ```
203
+
204
+ ### Negation (exclude specific values)
205
+
206
+ ```typescript
207
+ {
208
+ id: "non-eth-swaps",
209
+ enabled: true,
210
+ priority: 100,
211
+ match: {
212
+ in: { blockchain: "!eth" },
213
+ out: { blockchain: "!eth" },
214
+ },
215
+ fee: { type: "bps", bps: 12, recipient: "fees.near" },
216
+ }
217
+ ```
218
+
219
+ ### Array values (match multiple options)
220
+
221
+ ```typescript
222
+ {
223
+ id: "stablecoin-swaps",
224
+ enabled: true,
225
+ priority: 100,
226
+ match: {
227
+ in: { symbol: ["USDC", "USDT", "DAI"] },
228
+ out: { symbol: ["USDC", "USDT", "DAI"] },
229
+ },
230
+ fee: { type: "bps", bps: 5, recipient: "fees.near" },
231
+ }
232
+ ```
233
+
234
+ ```typescript
235
+ {
236
+ id: "l2-to-l2",
237
+ enabled: true,
238
+ priority: 100,
239
+ match: {
240
+ in: { blockchain: ["arb", "polygon", "base", "op"] },
241
+ out: { blockchain: ["arb", "polygon", "base", "op"] },
242
+ },
243
+ fee: { type: "bps", bps: 8, recipient: "fees.near" },
244
+ }
245
+ ```
246
+
247
+ ### Array values for assetId
248
+
249
+ ```typescript
250
+ {
251
+ id: "specific-usdc-tokens",
252
+ enabled: true,
253
+ priority: 150,
254
+ match: {
255
+ in: {
256
+ assetId: [
257
+ "nep141:eth-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.omft.near",
258
+ "nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near",
259
+ ]
260
+ },
261
+ out: { symbol: "USDC" },
262
+ },
263
+ fee: { type: "bps", bps: 5, recipient: "fees.near" },
264
+ }
265
+ ```
266
+
267
+ ### Time-based rules (promotional periods)
268
+
269
+ Rules can have `valid_from` and `valid_until` timestamps. Dates must be valid ISO 8601 strings - invalid dates will throw an error during matching.
270
+
271
+ ```typescript
272
+ {
273
+ id: "new-year-promo",
274
+ enabled: true,
275
+ priority: 200,
276
+ valid_from: "2025-01-01T00:00:00Z",
277
+ valid_until: "2025-01-07T23:59:59Z",
278
+ match: {
279
+ in: { symbol: "*" },
280
+ out: { symbol: "*" },
281
+ },
282
+ fee: { type: "bps", bps: 0, recipient: "fees.near" },
283
+ }
284
+ ```
285
+
286
+ ```typescript
287
+ {
288
+ id: "summer-discount",
289
+ enabled: true,
290
+ priority: 150,
291
+ valid_from: "2025-06-01T00:00:00Z",
292
+ // No valid_until - runs indefinitely after start
293
+ match: {
294
+ in: { symbol: "USDC" },
295
+ out: { symbol: "USDC" },
296
+ },
297
+ fee: { type: "bps", bps: 5, recipient: "fees.near" },
298
+ }
299
+ ```
300
+
301
+ ### Multiple fee recipients
302
+
303
+ Split fees between multiple accounts (e.g., platform + partner):
304
+
305
+ ```typescript
306
+ {
307
+ id: "partner-referral",
308
+ enabled: true,
309
+ priority: 100,
310
+ match: {
311
+ in: { symbol: "USDC" },
312
+ out: { symbol: "USDC" },
313
+ },
314
+ fee: [
315
+ { type: "bps", bps: 7, recipient: "fees.near" },
316
+ { type: "bps", bps: 3, recipient: "partner.near" },
317
+ ],
318
+ }
319
+ // Total fee: 10 bps (0.10%), split 70/30
320
+ ```
321
+
322
+ ### Complete config example
323
+
324
+ ```typescript
325
+ const feeConfig = {
326
+ version: "1.0.0",
327
+ default_fee: { type: "bps", bps: 30, recipient: "fees.near" },
328
+ rules: [
329
+ // Most specific first (higher priority)
330
+ {
331
+ id: "eth-usdc-to-polygon-usdc",
332
+ enabled: true,
333
+ priority: 200,
334
+ match: {
335
+ in: { assetId: "nep141:eth-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.omft.near" },
336
+ out: { assetId: "nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near" },
337
+ },
338
+ fee: { type: "bps", bps: 5, recipient: "fees.near" },
339
+ },
340
+ // Chain + symbol combo
341
+ {
342
+ id: "eth-usdc-to-any",
343
+ enabled: true,
344
+ priority: 150,
345
+ match: {
346
+ in: { blockchain: "eth", symbol: "USDC" },
347
+ out: { blockchain: "*" },
348
+ },
349
+ fee: { type: "bps", bps: 12, recipient: "fees.near" },
350
+ },
351
+ // All stablecoin swaps
352
+ {
353
+ id: "usdc-swaps",
354
+ enabled: true,
355
+ priority: 100,
356
+ match: {
357
+ in: { symbol: "USDC" },
358
+ out: { symbol: "USDC" },
359
+ },
360
+ fee: { type: "bps", bps: 10, recipient: "fees.near" },
361
+ },
362
+ // Disabled rule (won't match)
363
+ {
364
+ id: "promo-free-swaps",
365
+ enabled: false,
366
+ priority: 300,
367
+ match: {
368
+ in: { blockchain: "*" },
369
+ out: { blockchain: "*" },
370
+ },
371
+ fee: { type: "bps", bps: 0, recipient: "fees.near" },
372
+ },
373
+ ],
374
+ };
375
+ ```
376
+
377
+ ### Priority ordering
378
+
379
+ - Higher priority rules are evaluated first
380
+ - First matching rule wins
381
+ - Use priority gaps (50, 100, 150, 200) to allow inserting rules later
382
+ - More specific rules should have higher priority
383
+
384
+ ## Rule Builder UI
385
+
386
+ A simple browser-based tool for building fee configurations is included:
387
+
388
+ **Online:** https://bonnevoyager.github.io/intents-1click-rule-engine/
389
+
390
+ **Local:**
391
+ ```bash
392
+ open index.html
393
+ ```
394
+
395
+ Features:
396
+ - Visual rule editor with all matching options
397
+ - Support for wildcards, negation, and arrays
398
+ - Multiple fee recipients
399
+ - Time-based rules
400
+ - Import/export JSON configurations
401
+ - No dependencies - works offline
402
+
403
+ ## Development
404
+
405
+ ```bash
406
+ bun install
407
+ bun test
408
+ bun run typecheck
409
+ ```
410
+
411
+ ## Publishing to npm
412
+
413
+ ### Prerequisites
414
+
415
+ 1. Create an [npm account](https://www.npmjs.com/signup) if you don't have one
416
+ 2. Login to npm:
417
+
418
+ ```bash
419
+ npm login
420
+ ```
421
+
422
+ ### Publish
423
+
424
+ 1. Update the version in `package.json`:
425
+
426
+ ```bash
427
+ # Patch release (1.0.0 -> 1.0.1)
428
+ npm version patch
429
+
430
+ # Minor release (1.0.0 -> 1.1.0)
431
+ npm version minor
432
+
433
+ # Major release (1.0.0 -> 2.0.0)
434
+ npm version major
435
+ ```
436
+
437
+ 2. Run tests to ensure everything works:
438
+
439
+ ```bash
440
+ bun test && bun run typecheck
441
+ ```
442
+
443
+ 3. Publish to npm:
444
+
445
+ ```bash
446
+ npm publish
447
+ ```
448
+
449
+ ### Publishing a scoped package
450
+
451
+ If you want to publish under a scope (e.g., `@myorg/intents-1click-rule-engine`):
452
+
453
+ 1. Update the `name` in `package.json`:
454
+
455
+ ```json
456
+ {
457
+ "name": "@myorg/intents-1click-rule-engine"
458
+ }
459
+ ```
460
+
461
+ 2. Publish with public access (scoped packages are private by default):
462
+
463
+ ```bash
464
+ npm publish --access public
465
+ ```
466
+
467
+ ### Verify publication
468
+
469
+ After publishing, verify the package is available:
470
+
471
+ ```bash
472
+ npm info intents-1click-rule-engine
473
+ ```
@@ -0,0 +1,6 @@
1
+ export * from "./src/types";
2
+ export * from "./src/matcher";
3
+ export * from "./src/token-registry";
4
+ export * from "./src/validator";
5
+ export * from "./src/rule-engine";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./src/types";
2
+ export * from "./src/matcher";
3
+ export * from "./src/token-registry";
4
+ export * from "./src/validator";
5
+ export * from "./src/rule-engine";
@@ -0,0 +1,15 @@
1
+ import type { FeeConfig, MatchResult, SwapRequest, TokenRegistry } from "./types";
2
+ export declare class RuleMatcher {
3
+ private rules;
4
+ private defaultFee;
5
+ private tokenRegistry;
6
+ constructor(config: FeeConfig, tokenRegistry: TokenRegistry);
7
+ private sortRulesByPriority;
8
+ private matchesSinglePattern;
9
+ private matchesValue;
10
+ private matchesToken;
11
+ private parseDate;
12
+ private isRuleValidNow;
13
+ match(request: SwapRequest): MatchResult;
14
+ }
15
+ //# sourceMappingURL=matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../../src/matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,SAAS,EAA2C,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEjI,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,aAAa,CAAgB;gBAEzB,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa;IAM3D,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,oBAAoB;IAQ5B,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,YAAY;IAyBpB,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,cAAc;IAatB,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW;CAuCzC"}
@@ -0,0 +1,110 @@
1
+ export class RuleMatcher {
2
+ rules;
3
+ defaultFee;
4
+ tokenRegistry;
5
+ constructor(config, tokenRegistry) {
6
+ this.defaultFee = config.default_fee;
7
+ this.rules = this.sortRulesByPriority(config.rules);
8
+ this.tokenRegistry = tokenRegistry;
9
+ }
10
+ sortRulesByPriority(rules) {
11
+ return [...rules].sort((a, b) => {
12
+ const priorityA = a.priority ?? 100;
13
+ const priorityB = b.priority ?? 100;
14
+ return priorityB - priorityA;
15
+ });
16
+ }
17
+ matchesSinglePattern(pattern, value) {
18
+ if (pattern === "*")
19
+ return true;
20
+ if (pattern.startsWith("!")) {
21
+ return value !== pattern.slice(1);
22
+ }
23
+ return pattern === value;
24
+ }
25
+ matchesValue(pattern, value) {
26
+ if (Array.isArray(pattern)) {
27
+ return pattern.some((p) => this.matchesSinglePattern(p, value));
28
+ }
29
+ return this.matchesSinglePattern(pattern, value);
30
+ }
31
+ matchesToken(matcher, token) {
32
+ const matchedBy = {};
33
+ if (matcher.assetId) {
34
+ if (!this.matchesValue(matcher.assetId, token.assetId)) {
35
+ return null;
36
+ }
37
+ matchedBy.assetId = true;
38
+ }
39
+ if (matcher.blockchain) {
40
+ if (!this.matchesValue(matcher.blockchain, token.blockchain)) {
41
+ return null;
42
+ }
43
+ matchedBy.blockchain = true;
44
+ }
45
+ if (matcher.symbol) {
46
+ if (!this.matchesValue(matcher.symbol, token.symbol)) {
47
+ return null;
48
+ }
49
+ matchedBy.symbol = true;
50
+ }
51
+ return { token, matchedBy };
52
+ }
53
+ parseDate(dateStr, fieldName) {
54
+ const timestamp = new Date(dateStr).getTime();
55
+ if (Number.isNaN(timestamp)) {
56
+ throw new Error(`Invalid ${fieldName}: "${dateStr}" is not a valid date string`);
57
+ }
58
+ return timestamp;
59
+ }
60
+ isRuleValidNow(rule) {
61
+ const now = Date.now();
62
+ if (rule.valid_from) {
63
+ const from = this.parseDate(rule.valid_from, "valid_from");
64
+ if (now < from)
65
+ return false;
66
+ }
67
+ if (rule.valid_until) {
68
+ const until = this.parseDate(rule.valid_until, "valid_until");
69
+ if (now > until)
70
+ return false;
71
+ }
72
+ return true;
73
+ }
74
+ match(request) {
75
+ const originToken = this.tokenRegistry.getToken(request.originAsset);
76
+ const destinationToken = this.tokenRegistry.getToken(request.destinationAsset);
77
+ if (!originToken || !destinationToken) {
78
+ return {
79
+ matched: false,
80
+ fee: this.defaultFee,
81
+ };
82
+ }
83
+ for (const rule of this.rules) {
84
+ if (!rule.enabled)
85
+ continue;
86
+ if (!this.isRuleValidNow(rule))
87
+ continue;
88
+ const inMatchInfo = this.matchesToken(rule.match.in, originToken);
89
+ const outMatchInfo = this.matchesToken(rule.match.out, destinationToken);
90
+ if (inMatchInfo && outMatchInfo) {
91
+ return {
92
+ matched: true,
93
+ rule,
94
+ fee: rule.fee,
95
+ matchDetails: {
96
+ originToken,
97
+ destinationToken,
98
+ in: inMatchInfo,
99
+ out: outMatchInfo,
100
+ },
101
+ };
102
+ }
103
+ }
104
+ return {
105
+ matched: false,
106
+ fee: this.defaultFee,
107
+ matchDetails: { originToken, destinationToken },
108
+ };
109
+ }
110
+ }
@@ -0,0 +1,21 @@
1
+ import type { Fee, FeeConfig, SwapRequest, MatchResult } from "./types";
2
+ export declare function getTotalBps(fee: Fee | Fee[]): number;
3
+ export declare function calculateFee(amount: string | bigint, bps: number): string;
4
+ export declare function calculateAmountAfterFee(amount: string | bigint, bps: number): string;
5
+ export interface RuleEngineOptions {
6
+ tokenRegistryUrl?: string;
7
+ tokenRegistryCacheTtlMs?: number;
8
+ }
9
+ export declare class RuleEngine {
10
+ private matcher;
11
+ private tokenRegistry;
12
+ private feeConfig;
13
+ constructor(feeConfig: FeeConfig, options?: RuleEngineOptions);
14
+ initialize(): Promise<void>;
15
+ ensureReady(): Promise<void>;
16
+ match(request: SwapRequest): MatchResult;
17
+ matchWithRefresh(request: SwapRequest): Promise<MatchResult>;
18
+ getTokenRegistrySize(): number;
19
+ getFeeConfig(): FeeConfig;
20
+ }
21
+ //# sourceMappingURL=rule-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-engine.d.ts","sourceRoot":"","sources":["../../src/rule-engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAUxE,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,EAAE,GAAG,MAAM,CAKpD;AAqCD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAKzE;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAKpF;AAED,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,SAAS,CAAY;gBAEjB,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,iBAAiB;IAcvD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAIlC,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW;IAIlC,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAKlE,oBAAoB,IAAI,MAAM;IAI9B,YAAY,IAAI,SAAS;CAG1B"}
@@ -0,0 +1,94 @@
1
+ import { RuleMatcher } from "./matcher";
2
+ import { CachedTokenRegistry } from "./token-registry";
3
+ import { validateConfig } from "./validator";
4
+ const DEFAULT_TOKEN_REGISTRY_URL = "https://1click.chaindefuser.com/v0/tokens";
5
+ const DEFAULT_CACHE_TTL_MS = 3600000; // 1 hour
6
+ const BPS_DIVISOR = 10000n;
7
+ const MAX_BPS = 10000; // 100% fee cap
8
+ export function getTotalBps(fee) {
9
+ if (Array.isArray(fee)) {
10
+ return fee.reduce((sum, f) => sum + f.bps, 0);
11
+ }
12
+ return fee.bps;
13
+ }
14
+ function parseAmount(amount) {
15
+ if (typeof amount === "bigint") {
16
+ return amount;
17
+ }
18
+ if (typeof amount !== "string") {
19
+ throw new Error(`Invalid amount: expected string or bigint, got ${typeof amount}`);
20
+ }
21
+ if (amount.trim() === "") {
22
+ throw new Error("Invalid amount: empty string");
23
+ }
24
+ if (!/^-?\d+$/.test(amount)) {
25
+ throw new Error(`Invalid amount: "${amount}" is not a valid integer string`);
26
+ }
27
+ const result = BigInt(amount);
28
+ if (result < 0n) {
29
+ throw new Error(`Invalid amount: "${amount}" must be non-negative`);
30
+ }
31
+ return result;
32
+ }
33
+ function validateBps(bps) {
34
+ if (typeof bps !== "number" || !Number.isFinite(bps)) {
35
+ throw new Error(`Invalid bps: expected a finite number, got ${bps}`);
36
+ }
37
+ if (!Number.isInteger(bps)) {
38
+ throw new Error(`Invalid bps: ${bps} must be an integer`);
39
+ }
40
+ if (bps < 0) {
41
+ throw new Error(`Invalid bps: ${bps} must be non-negative`);
42
+ }
43
+ if (bps > MAX_BPS) {
44
+ throw new Error(`Invalid bps: ${bps} exceeds maximum of ${MAX_BPS} (100%)`);
45
+ }
46
+ }
47
+ export function calculateFee(amount, bps) {
48
+ const amountBigInt = parseAmount(amount);
49
+ validateBps(bps);
50
+ const fee = (amountBigInt * BigInt(bps)) / BPS_DIVISOR;
51
+ return fee.toString();
52
+ }
53
+ export function calculateAmountAfterFee(amount, bps) {
54
+ const amountBigInt = parseAmount(amount);
55
+ validateBps(bps);
56
+ const fee = BigInt(calculateFee(amountBigInt, bps));
57
+ return (amountBigInt - fee).toString();
58
+ }
59
+ export class RuleEngine {
60
+ matcher;
61
+ tokenRegistry;
62
+ feeConfig;
63
+ constructor(feeConfig, options) {
64
+ const validation = validateConfig(feeConfig);
65
+ if (!validation.valid) {
66
+ throw new Error(`Invalid fee config: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join(", ")}`);
67
+ }
68
+ this.feeConfig = feeConfig;
69
+ this.tokenRegistry = new CachedTokenRegistry({
70
+ url: options?.tokenRegistryUrl ?? DEFAULT_TOKEN_REGISTRY_URL,
71
+ cacheTtlMs: options?.tokenRegistryCacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
72
+ });
73
+ this.matcher = new RuleMatcher(this.feeConfig, this.tokenRegistry);
74
+ }
75
+ async initialize() {
76
+ await this.tokenRegistry.refresh();
77
+ }
78
+ async ensureReady() {
79
+ await this.tokenRegistry.ensureFresh();
80
+ }
81
+ match(request) {
82
+ return this.matcher.match(request);
83
+ }
84
+ async matchWithRefresh(request) {
85
+ await this.ensureReady();
86
+ return this.match(request);
87
+ }
88
+ getTokenRegistrySize() {
89
+ return this.tokenRegistry.size;
90
+ }
91
+ getFeeConfig() {
92
+ return this.feeConfig;
93
+ }
94
+ }
@@ -0,0 +1,20 @@
1
+ import type { TokenInfo, TokenRegistry } from "./types";
2
+ export interface TokenRegistryConfig {
3
+ url: string;
4
+ cacheTtlMs: number;
5
+ }
6
+ export declare class CachedTokenRegistry implements TokenRegistry {
7
+ private config;
8
+ private cache;
9
+ private lastFetchTime;
10
+ private refreshPromise;
11
+ constructor(config: TokenRegistryConfig);
12
+ private isCacheValid;
13
+ refresh(): Promise<void>;
14
+ private doRefresh;
15
+ ensureFresh(): Promise<void>;
16
+ getToken(assetId: string): TokenInfo | undefined;
17
+ getAllTokens(): TokenInfo[];
18
+ get size(): number;
19
+ }
20
+ //# sourceMappingURL=token-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-registry.d.ts","sourceRoot":"","sources":["../../src/token-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAuBD,qBAAa,mBAAoB,YAAW,aAAa;IACvD,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,cAAc,CAA8B;gBAExC,MAAM,EAAE,mBAAmB;IAIvC,OAAO,CAAC,YAAY;IAId,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAchB,SAAS;IAoCjB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAMlC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAIhD,YAAY,IAAI,SAAS,EAAE;IAI3B,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -0,0 +1,78 @@
1
+ function isValidTokenResponse(token) {
2
+ if (typeof token !== "object" || token === null)
3
+ return false;
4
+ const t = token;
5
+ return (typeof t.assetId === "string" &&
6
+ typeof t.blockchain === "string" &&
7
+ typeof t.symbol === "string" &&
8
+ typeof t.decimals === "number");
9
+ }
10
+ export class CachedTokenRegistry {
11
+ config;
12
+ cache = new Map();
13
+ lastFetchTime = 0;
14
+ refreshPromise = null;
15
+ constructor(config) {
16
+ this.config = config;
17
+ }
18
+ isCacheValid() {
19
+ return Date.now() - this.lastFetchTime < this.config.cacheTtlMs;
20
+ }
21
+ async refresh() {
22
+ // If a refresh is already in progress, return the existing promise
23
+ if (this.refreshPromise) {
24
+ return this.refreshPromise;
25
+ }
26
+ this.refreshPromise = this.doRefresh();
27
+ try {
28
+ await this.refreshPromise;
29
+ }
30
+ finally {
31
+ this.refreshPromise = null;
32
+ }
33
+ }
34
+ async doRefresh() {
35
+ const response = await fetch(this.config.url);
36
+ if (!response.ok) {
37
+ throw new Error(`Failed to fetch tokens: ${response.status} ${response.statusText}`);
38
+ }
39
+ const data = await response.json();
40
+ if (!Array.isArray(data)) {
41
+ throw new Error(`Invalid token registry response: expected array, got ${typeof data}`);
42
+ }
43
+ const newCache = new Map();
44
+ const invalidTokens = [];
45
+ for (let i = 0; i < data.length; i++) {
46
+ const token = data[i];
47
+ if (!isValidTokenResponse(token)) {
48
+ invalidTokens.push(i);
49
+ continue;
50
+ }
51
+ newCache.set(token.assetId, {
52
+ assetId: token.assetId,
53
+ blockchain: token.blockchain,
54
+ symbol: token.symbol,
55
+ decimals: token.decimals,
56
+ });
57
+ }
58
+ if (invalidTokens.length > 0 && newCache.size === 0) {
59
+ throw new Error(`Invalid token registry response: all ${data.length} tokens failed validation`);
60
+ }
61
+ this.cache = newCache;
62
+ this.lastFetchTime = Date.now();
63
+ }
64
+ async ensureFresh() {
65
+ if (!this.isCacheValid()) {
66
+ await this.refresh();
67
+ }
68
+ }
69
+ getToken(assetId) {
70
+ return this.cache.get(assetId);
71
+ }
72
+ getAllTokens() {
73
+ return Array.from(this.cache.values());
74
+ }
75
+ get size() {
76
+ return this.cache.size;
77
+ }
78
+ }
@@ -0,0 +1,76 @@
1
+ export interface TokenInfo {
2
+ assetId: string;
3
+ blockchain: string;
4
+ symbol: string;
5
+ decimals: number;
6
+ }
7
+ /**
8
+ * Matcher for filtering tokens by blockchain, symbol, or assetId.
9
+ *
10
+ * **Important: All string matching is case-sensitive.**
11
+ * - `symbol: "USDC"` will NOT match a token with symbol `"usdc"` or `"Usdc"`
12
+ * - `blockchain: "eth"` will NOT match `"ETH"` or `"Eth"`
13
+ *
14
+ * Supported patterns:
15
+ * - Exact match: `"eth"` matches only `"eth"`
16
+ * - Wildcard: `"*"` matches any value
17
+ * - Negation: `"!eth"` matches any value except `"eth"`
18
+ * - Array (OR logic): `["eth", "base"]` matches `"eth"` or `"base"`
19
+ */
20
+ export interface TokenMatcher {
21
+ blockchain?: string | string[];
22
+ symbol?: string | string[];
23
+ assetId?: string | string[];
24
+ }
25
+ export interface RuleMatch {
26
+ in: TokenMatcher;
27
+ out: TokenMatcher;
28
+ }
29
+ export interface Fee {
30
+ type: "bps";
31
+ bps: number;
32
+ recipient: string;
33
+ }
34
+ export interface Rule {
35
+ id: string;
36
+ enabled: boolean;
37
+ priority?: number;
38
+ description?: string;
39
+ match: RuleMatch;
40
+ fee: Fee | Fee[];
41
+ valid_from?: string;
42
+ valid_until?: string;
43
+ }
44
+ export interface FeeConfig {
45
+ version: string;
46
+ default_fee: Fee | Fee[];
47
+ rules: Rule[];
48
+ }
49
+ export interface SwapRequest {
50
+ originAsset: string;
51
+ destinationAsset: string;
52
+ amount?: string;
53
+ }
54
+ export interface TokenMatchInfo {
55
+ token: TokenInfo;
56
+ matchedBy: {
57
+ assetId?: boolean;
58
+ blockchain?: boolean;
59
+ symbol?: boolean;
60
+ };
61
+ }
62
+ export interface MatchResult {
63
+ matched: boolean;
64
+ rule?: Rule;
65
+ fee: Fee | Fee[];
66
+ matchDetails?: {
67
+ originToken: TokenInfo;
68
+ destinationToken: TokenInfo;
69
+ in?: TokenMatchInfo;
70
+ out?: TokenMatchInfo;
71
+ };
72
+ }
73
+ export interface TokenRegistry {
74
+ getToken(assetId: string): TokenInfo | undefined;
75
+ }
76
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,YAAY,CAAC;IACjB,GAAG,EAAE,YAAY,CAAC;CACnB;AAED,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,KAAK,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,CAAC;IACjB,GAAG,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC;IACzB,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE;QACT,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,GAAG,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC;IACjB,YAAY,CAAC,EAAE;QACb,WAAW,EAAE,SAAS,CAAC;QACvB,gBAAgB,EAAE,SAAS,CAAC;QAC5B,EAAE,CAAC,EAAE,cAAc,CAAC;QACpB,GAAG,CAAC,EAAE,cAAc,CAAC;KACtB,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;CAClD"}
File without changes
@@ -0,0 +1,11 @@
1
+ import type { FeeConfig } from "./types";
2
+ export interface ValidationError {
3
+ path: string;
4
+ message: string;
5
+ }
6
+ export interface ValidationResult {
7
+ valid: boolean;
8
+ errors: ValidationError[];
9
+ }
10
+ export declare function validateConfig(config: FeeConfig): ValidationResult;
11
+ //# sourceMappingURL=validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAA2B,MAAM,SAAS,CAAC;AAElE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAwDD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAuGD,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG,gBAAgB,CAkClE"}
@@ -0,0 +1,175 @@
1
+ const NEAR_ACCOUNT_REGEX = /^(?:[a-z\d]+[-_])*[a-z\d]+(?:\.[a-z\d]+[-_]*[a-z\d]+)*$/;
2
+ const NEAR_IMPLICIT_ACCOUNT_REGEX = /^[a-f0-9]{64}$/;
3
+ const MAX_BPS = 10000; // 100% fee cap
4
+ function isValidNearAccount(account) {
5
+ if (account.length < 2 || account.length > 64)
6
+ return false;
7
+ return NEAR_ACCOUNT_REGEX.test(account) || NEAR_IMPLICIT_ACCOUNT_REGEX.test(account);
8
+ }
9
+ function isValidDateString(dateStr) {
10
+ const timestamp = new Date(dateStr).getTime();
11
+ return !Number.isNaN(timestamp);
12
+ }
13
+ function validateSingleFee(fee, path) {
14
+ const errors = [];
15
+ if (fee.type !== "bps") {
16
+ errors.push({ path: `${path}.type`, message: "fee.type must be 'bps'" });
17
+ }
18
+ if (typeof fee.bps !== "number" || fee.bps < 0) {
19
+ errors.push({ path: `${path}.bps`, message: "fee.bps must be a non-negative number" });
20
+ }
21
+ else if (fee.bps > MAX_BPS) {
22
+ errors.push({ path: `${path}.bps`, message: `fee.bps exceeds maximum of ${MAX_BPS} (100%)` });
23
+ }
24
+ if (!fee.recipient || typeof fee.recipient !== "string") {
25
+ errors.push({ path: `${path}.recipient`, message: "fee.recipient is required and must be a string" });
26
+ }
27
+ else if (!isValidNearAccount(fee.recipient)) {
28
+ errors.push({ path: `${path}.recipient`, message: "fee.recipient must be a valid NEAR account" });
29
+ }
30
+ return errors;
31
+ }
32
+ function validateFee(fee, path) {
33
+ const errors = [];
34
+ if (Array.isArray(fee)) {
35
+ if (fee.length === 0) {
36
+ errors.push({ path, message: "fee array must not be empty" });
37
+ return errors;
38
+ }
39
+ for (let i = 0; i < fee.length; i++) {
40
+ errors.push(...validateSingleFee(fee[i], `${path}[${i}]`));
41
+ }
42
+ }
43
+ else if (fee && typeof fee === "object") {
44
+ errors.push(...validateSingleFee(fee, path));
45
+ }
46
+ else {
47
+ errors.push({ path, message: "fee is required" });
48
+ }
49
+ return errors;
50
+ }
51
+ function isNonEmptyStringOrArray(value) {
52
+ if (typeof value === "string")
53
+ return value.length > 0;
54
+ if (Array.isArray(value))
55
+ return value.length > 0 && value.every((v) => typeof v === "string" && v.length > 0);
56
+ return false;
57
+ }
58
+ function validateTokenMatcher(matcher, path) {
59
+ const errors = [];
60
+ // Check for empty arrays
61
+ if (Array.isArray(matcher.blockchain) && matcher.blockchain.length === 0) {
62
+ errors.push({ path: `${path}.blockchain`, message: "blockchain array must not be empty" });
63
+ }
64
+ if (Array.isArray(matcher.symbol) && matcher.symbol.length === 0) {
65
+ errors.push({ path: `${path}.symbol`, message: "symbol array must not be empty" });
66
+ }
67
+ if (Array.isArray(matcher.assetId) && matcher.assetId.length === 0) {
68
+ errors.push({ path: `${path}.assetId`, message: "assetId array must not be empty" });
69
+ }
70
+ // Check for empty strings in arrays
71
+ if (Array.isArray(matcher.blockchain) && matcher.blockchain.some((v) => v === "")) {
72
+ errors.push({ path: `${path}.blockchain`, message: "blockchain array must not contain empty strings" });
73
+ }
74
+ if (Array.isArray(matcher.symbol) && matcher.symbol.some((v) => v === "")) {
75
+ errors.push({ path: `${path}.symbol`, message: "symbol array must not contain empty strings" });
76
+ }
77
+ if (Array.isArray(matcher.assetId) && matcher.assetId.some((v) => v === "")) {
78
+ errors.push({ path: `${path}.assetId`, message: "assetId array must not contain empty strings" });
79
+ }
80
+ const hasIdentifier = isNonEmptyStringOrArray(matcher.blockchain) ||
81
+ isNonEmptyStringOrArray(matcher.symbol) ||
82
+ isNonEmptyStringOrArray(matcher.assetId);
83
+ if (!hasIdentifier) {
84
+ errors.push({
85
+ path,
86
+ message: "At least one of blockchain, symbol, or assetId must be defined",
87
+ });
88
+ }
89
+ return errors;
90
+ }
91
+ function validateRule(rule, index) {
92
+ const errors = [];
93
+ const path = `rules[${index}]`;
94
+ if (!rule.id || typeof rule.id !== "string") {
95
+ errors.push({ path: `${path}.id`, message: "id is required and must be a string" });
96
+ }
97
+ if (typeof rule.enabled !== "boolean") {
98
+ errors.push({ path: `${path}.enabled`, message: "enabled is required and must be a boolean" });
99
+ }
100
+ if (rule.priority !== undefined && (typeof rule.priority !== "number" || rule.priority < 0)) {
101
+ errors.push({ path: `${path}.priority`, message: "priority must be a non-negative number" });
102
+ }
103
+ if (!rule.match) {
104
+ errors.push({ path: `${path}.match`, message: "match is required" });
105
+ }
106
+ else {
107
+ if (!rule.match.in) {
108
+ errors.push({ path: `${path}.match.in`, message: "match.in is required" });
109
+ }
110
+ else {
111
+ errors.push(...validateTokenMatcher(rule.match.in, `${path}.match.in`));
112
+ }
113
+ if (!rule.match.out) {
114
+ errors.push({ path: `${path}.match.out`, message: "match.out is required" });
115
+ }
116
+ else {
117
+ errors.push(...validateTokenMatcher(rule.match.out, `${path}.match.out`));
118
+ }
119
+ }
120
+ if (!rule.fee) {
121
+ errors.push({ path: `${path}.fee`, message: "fee is required" });
122
+ }
123
+ else {
124
+ errors.push(...validateFee(rule.fee, `${path}.fee`));
125
+ }
126
+ if (rule.valid_from !== undefined) {
127
+ if (typeof rule.valid_from !== "string") {
128
+ errors.push({ path: `${path}.valid_from`, message: "valid_from must be a string" });
129
+ }
130
+ else if (!isValidDateString(rule.valid_from)) {
131
+ errors.push({ path: `${path}.valid_from`, message: `valid_from "${rule.valid_from}" is not a valid date string` });
132
+ }
133
+ }
134
+ if (rule.valid_until !== undefined) {
135
+ if (typeof rule.valid_until !== "string") {
136
+ errors.push({ path: `${path}.valid_until`, message: "valid_until must be a string" });
137
+ }
138
+ else if (!isValidDateString(rule.valid_until)) {
139
+ errors.push({ path: `${path}.valid_until`, message: `valid_until "${rule.valid_until}" is not a valid date string` });
140
+ }
141
+ }
142
+ return errors;
143
+ }
144
+ export function validateConfig(config) {
145
+ const errors = [];
146
+ if (!config.version || typeof config.version !== "string") {
147
+ errors.push({ path: "version", message: "version is required and must be a string" });
148
+ }
149
+ if (!config.default_fee) {
150
+ errors.push({ path: "default_fee", message: "default_fee is required" });
151
+ }
152
+ else {
153
+ errors.push(...validateFee(config.default_fee, "default_fee"));
154
+ }
155
+ if (!Array.isArray(config.rules)) {
156
+ errors.push({ path: "rules", message: "rules must be an array" });
157
+ }
158
+ else {
159
+ const ruleIds = new Set();
160
+ for (let i = 0; i < config.rules.length; i++) {
161
+ const rule = config.rules[i];
162
+ errors.push(...validateRule(rule, i));
163
+ if (rule.id) {
164
+ if (ruleIds.has(rule.id)) {
165
+ errors.push({ path: `rules[${i}].id`, message: `Duplicate rule id: ${rule.id}` });
166
+ }
167
+ ruleIds.add(rule.id);
168
+ }
169
+ }
170
+ }
171
+ return {
172
+ valid: errors.length === 0,
173
+ errors,
174
+ };
175
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "intents-1click-rule-engine",
3
+ "version": "1.0.0",
4
+ "description": "Fee configuration rule engine for NEAR Intents 1Click API",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.build.json",
20
+ "prepublishOnly": "bun run build",
21
+ "test": "bun test",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "keywords": [
25
+ "near",
26
+ "intents",
27
+ "1click",
28
+ "fee",
29
+ "rules",
30
+ "swap"
31
+ ],
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "@types/bun": "latest",
35
+ "typescript": "^5"
36
+ }
37
+ }