intents-1click-rule-engine 1.0.3 → 1.0.5
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/dist/index.js +448 -4
- package/package.json +4 -3
- package/dist/src/matcher.js +0 -110
- package/dist/src/rule-engine.js +0 -94
- package/dist/src/token-registry.js +0 -78
- package/dist/src/types.js +0 -1
- package/dist/src/validator.js +0 -175
package/dist/index.js
CHANGED
|
@@ -1,4 +1,448 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/matcher.ts
|
|
2
|
+
class RuleMatcher {
|
|
3
|
+
rules;
|
|
4
|
+
defaultFee;
|
|
5
|
+
tokenRegistry;
|
|
6
|
+
constructor(config, tokenRegistry) {
|
|
7
|
+
this.defaultFee = config.default_fee;
|
|
8
|
+
this.rules = this.sortRulesByPriority(config.rules);
|
|
9
|
+
this.tokenRegistry = tokenRegistry;
|
|
10
|
+
}
|
|
11
|
+
sortRulesByPriority(rules) {
|
|
12
|
+
return [...rules].sort((a, b) => {
|
|
13
|
+
const priorityA = a.priority ?? 100;
|
|
14
|
+
const priorityB = b.priority ?? 100;
|
|
15
|
+
return priorityB - priorityA;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
matchesSinglePattern(pattern, value) {
|
|
19
|
+
if (pattern === "*")
|
|
20
|
+
return true;
|
|
21
|
+
if (pattern.startsWith("!")) {
|
|
22
|
+
return value !== pattern.slice(1);
|
|
23
|
+
}
|
|
24
|
+
return pattern === value;
|
|
25
|
+
}
|
|
26
|
+
matchesValue(pattern, value) {
|
|
27
|
+
if (Array.isArray(pattern)) {
|
|
28
|
+
return pattern.some((p) => this.matchesSinglePattern(p, value));
|
|
29
|
+
}
|
|
30
|
+
return this.matchesSinglePattern(pattern, value);
|
|
31
|
+
}
|
|
32
|
+
matchesToken(matcher, token) {
|
|
33
|
+
const matchedBy = {};
|
|
34
|
+
if (matcher.assetId) {
|
|
35
|
+
if (!this.matchesValue(matcher.assetId, token.assetId)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
matchedBy.assetId = true;
|
|
39
|
+
}
|
|
40
|
+
if (matcher.blockchain) {
|
|
41
|
+
if (!this.matchesValue(matcher.blockchain, token.blockchain)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
matchedBy.blockchain = true;
|
|
45
|
+
}
|
|
46
|
+
if (matcher.symbol) {
|
|
47
|
+
if (!this.matchesValue(matcher.symbol, token.symbol)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
matchedBy.symbol = true;
|
|
51
|
+
}
|
|
52
|
+
return { token, matchedBy };
|
|
53
|
+
}
|
|
54
|
+
parseDate(dateStr, fieldName) {
|
|
55
|
+
const timestamp = new Date(dateStr).getTime();
|
|
56
|
+
if (Number.isNaN(timestamp)) {
|
|
57
|
+
throw new Error(`Invalid ${fieldName}: "${dateStr}" is not a valid date string`);
|
|
58
|
+
}
|
|
59
|
+
return timestamp;
|
|
60
|
+
}
|
|
61
|
+
isRuleValidNow(rule) {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
if (rule.valid_from) {
|
|
64
|
+
const from = this.parseDate(rule.valid_from, "valid_from");
|
|
65
|
+
if (now < from)
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (rule.valid_until) {
|
|
69
|
+
const until = this.parseDate(rule.valid_until, "valid_until");
|
|
70
|
+
if (now > until)
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
match(request) {
|
|
76
|
+
const originToken = this.tokenRegistry.getToken(request.originAsset);
|
|
77
|
+
const destinationToken = this.tokenRegistry.getToken(request.destinationAsset);
|
|
78
|
+
if (!originToken || !destinationToken) {
|
|
79
|
+
return {
|
|
80
|
+
matched: false,
|
|
81
|
+
fee: this.defaultFee
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
for (const rule of this.rules) {
|
|
85
|
+
if (!rule.enabled)
|
|
86
|
+
continue;
|
|
87
|
+
if (!this.isRuleValidNow(rule))
|
|
88
|
+
continue;
|
|
89
|
+
const inMatchInfo = this.matchesToken(rule.match.in, originToken);
|
|
90
|
+
const outMatchInfo = this.matchesToken(rule.match.out, destinationToken);
|
|
91
|
+
if (inMatchInfo && outMatchInfo) {
|
|
92
|
+
return {
|
|
93
|
+
matched: true,
|
|
94
|
+
rule,
|
|
95
|
+
fee: rule.fee,
|
|
96
|
+
matchDetails: {
|
|
97
|
+
originToken,
|
|
98
|
+
destinationToken,
|
|
99
|
+
in: inMatchInfo,
|
|
100
|
+
out: outMatchInfo
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
matched: false,
|
|
107
|
+
fee: this.defaultFee,
|
|
108
|
+
matchDetails: { originToken, destinationToken }
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// src/token-registry.ts
|
|
113
|
+
function isValidTokenResponse(token) {
|
|
114
|
+
if (typeof token !== "object" || token === null)
|
|
115
|
+
return false;
|
|
116
|
+
const t = token;
|
|
117
|
+
return typeof t.assetId === "string" && typeof t.blockchain === "string" && typeof t.symbol === "string" && typeof t.decimals === "number";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class CachedTokenRegistry {
|
|
121
|
+
config;
|
|
122
|
+
cache = new Map;
|
|
123
|
+
lastFetchTime = 0;
|
|
124
|
+
refreshPromise = null;
|
|
125
|
+
constructor(config) {
|
|
126
|
+
this.config = config;
|
|
127
|
+
}
|
|
128
|
+
isCacheValid() {
|
|
129
|
+
return Date.now() - this.lastFetchTime < this.config.cacheTtlMs;
|
|
130
|
+
}
|
|
131
|
+
async refresh() {
|
|
132
|
+
if (this.refreshPromise) {
|
|
133
|
+
return this.refreshPromise;
|
|
134
|
+
}
|
|
135
|
+
this.refreshPromise = this.doRefresh();
|
|
136
|
+
try {
|
|
137
|
+
await this.refreshPromise;
|
|
138
|
+
} finally {
|
|
139
|
+
this.refreshPromise = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async doRefresh() {
|
|
143
|
+
const response = await fetch(this.config.url);
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(`Failed to fetch tokens: ${response.status} ${response.statusText}`);
|
|
146
|
+
}
|
|
147
|
+
const data = await response.json();
|
|
148
|
+
if (!Array.isArray(data)) {
|
|
149
|
+
throw new Error(`Invalid token registry response: expected array, got ${typeof data}`);
|
|
150
|
+
}
|
|
151
|
+
const newCache = new Map;
|
|
152
|
+
const invalidTokens = [];
|
|
153
|
+
for (let i = 0;i < data.length; i++) {
|
|
154
|
+
const token = data[i];
|
|
155
|
+
if (!isValidTokenResponse(token)) {
|
|
156
|
+
invalidTokens.push(i);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
newCache.set(token.assetId, {
|
|
160
|
+
assetId: token.assetId,
|
|
161
|
+
blockchain: token.blockchain,
|
|
162
|
+
symbol: token.symbol,
|
|
163
|
+
decimals: token.decimals
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (invalidTokens.length > 0 && newCache.size === 0) {
|
|
167
|
+
throw new Error(`Invalid token registry response: all ${data.length} tokens failed validation`);
|
|
168
|
+
}
|
|
169
|
+
this.cache = newCache;
|
|
170
|
+
this.lastFetchTime = Date.now();
|
|
171
|
+
}
|
|
172
|
+
async ensureFresh() {
|
|
173
|
+
if (!this.isCacheValid()) {
|
|
174
|
+
await this.refresh();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
getToken(assetId) {
|
|
178
|
+
return this.cache.get(assetId);
|
|
179
|
+
}
|
|
180
|
+
getAllTokens() {
|
|
181
|
+
return Array.from(this.cache.values());
|
|
182
|
+
}
|
|
183
|
+
get size() {
|
|
184
|
+
return this.cache.size;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// src/validator.ts
|
|
188
|
+
var NEAR_ACCOUNT_REGEX = /^(?:[a-z\d]+[-_])*[a-z\d]+(?:\.[a-z\d]+[-_]*[a-z\d]+)*$/;
|
|
189
|
+
var NEAR_IMPLICIT_ACCOUNT_REGEX = /^[a-f0-9]{64}$/;
|
|
190
|
+
var MAX_BPS = 1e4;
|
|
191
|
+
function isValidNearAccount(account) {
|
|
192
|
+
if (account.length < 2 || account.length > 64)
|
|
193
|
+
return false;
|
|
194
|
+
return NEAR_ACCOUNT_REGEX.test(account) || NEAR_IMPLICIT_ACCOUNT_REGEX.test(account);
|
|
195
|
+
}
|
|
196
|
+
function isValidDateString(dateStr) {
|
|
197
|
+
const timestamp = new Date(dateStr).getTime();
|
|
198
|
+
return !Number.isNaN(timestamp);
|
|
199
|
+
}
|
|
200
|
+
function validateSingleFee(fee, path) {
|
|
201
|
+
const errors = [];
|
|
202
|
+
if (fee.type !== "bps") {
|
|
203
|
+
errors.push({ path: `${path}.type`, message: "fee.type must be 'bps'" });
|
|
204
|
+
}
|
|
205
|
+
if (typeof fee.bps !== "number" || fee.bps < 0) {
|
|
206
|
+
errors.push({ path: `${path}.bps`, message: "fee.bps must be a non-negative number" });
|
|
207
|
+
} else if (fee.bps > MAX_BPS) {
|
|
208
|
+
errors.push({ path: `${path}.bps`, message: `fee.bps exceeds maximum of ${MAX_BPS} (100%)` });
|
|
209
|
+
}
|
|
210
|
+
if (!fee.recipient || typeof fee.recipient !== "string") {
|
|
211
|
+
errors.push({ path: `${path}.recipient`, message: "fee.recipient is required and must be a string" });
|
|
212
|
+
} else if (!isValidNearAccount(fee.recipient)) {
|
|
213
|
+
errors.push({ path: `${path}.recipient`, message: "fee.recipient must be a valid NEAR account" });
|
|
214
|
+
}
|
|
215
|
+
return errors;
|
|
216
|
+
}
|
|
217
|
+
function validateFee(fee, path) {
|
|
218
|
+
const errors = [];
|
|
219
|
+
if (Array.isArray(fee)) {
|
|
220
|
+
if (fee.length === 0) {
|
|
221
|
+
errors.push({ path, message: "fee array must not be empty" });
|
|
222
|
+
return errors;
|
|
223
|
+
}
|
|
224
|
+
for (let i = 0;i < fee.length; i++) {
|
|
225
|
+
errors.push(...validateSingleFee(fee[i], `${path}[${i}]`));
|
|
226
|
+
}
|
|
227
|
+
} else if (fee && typeof fee === "object") {
|
|
228
|
+
errors.push(...validateSingleFee(fee, path));
|
|
229
|
+
} else {
|
|
230
|
+
errors.push({ path, message: "fee is required" });
|
|
231
|
+
}
|
|
232
|
+
return errors;
|
|
233
|
+
}
|
|
234
|
+
function isNonEmptyStringOrArray(value) {
|
|
235
|
+
if (typeof value === "string")
|
|
236
|
+
return value.length > 0;
|
|
237
|
+
if (Array.isArray(value))
|
|
238
|
+
return value.length > 0 && value.every((v) => typeof v === "string" && v.length > 0);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
function validateTokenMatcher(matcher, path) {
|
|
242
|
+
const errors = [];
|
|
243
|
+
if (Array.isArray(matcher.blockchain) && matcher.blockchain.length === 0) {
|
|
244
|
+
errors.push({ path: `${path}.blockchain`, message: "blockchain array must not be empty" });
|
|
245
|
+
}
|
|
246
|
+
if (Array.isArray(matcher.symbol) && matcher.symbol.length === 0) {
|
|
247
|
+
errors.push({ path: `${path}.symbol`, message: "symbol array must not be empty" });
|
|
248
|
+
}
|
|
249
|
+
if (Array.isArray(matcher.assetId) && matcher.assetId.length === 0) {
|
|
250
|
+
errors.push({ path: `${path}.assetId`, message: "assetId array must not be empty" });
|
|
251
|
+
}
|
|
252
|
+
if (Array.isArray(matcher.blockchain) && matcher.blockchain.some((v) => v === "")) {
|
|
253
|
+
errors.push({ path: `${path}.blockchain`, message: "blockchain array must not contain empty strings" });
|
|
254
|
+
}
|
|
255
|
+
if (Array.isArray(matcher.symbol) && matcher.symbol.some((v) => v === "")) {
|
|
256
|
+
errors.push({ path: `${path}.symbol`, message: "symbol array must not contain empty strings" });
|
|
257
|
+
}
|
|
258
|
+
if (Array.isArray(matcher.assetId) && matcher.assetId.some((v) => v === "")) {
|
|
259
|
+
errors.push({ path: `${path}.assetId`, message: "assetId array must not contain empty strings" });
|
|
260
|
+
}
|
|
261
|
+
const hasIdentifier = isNonEmptyStringOrArray(matcher.blockchain) || isNonEmptyStringOrArray(matcher.symbol) || isNonEmptyStringOrArray(matcher.assetId);
|
|
262
|
+
if (!hasIdentifier) {
|
|
263
|
+
errors.push({
|
|
264
|
+
path,
|
|
265
|
+
message: "At least one of blockchain, symbol, or assetId must be defined"
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return errors;
|
|
269
|
+
}
|
|
270
|
+
function validateRule(rule, index) {
|
|
271
|
+
const errors = [];
|
|
272
|
+
const path = `rules[${index}]`;
|
|
273
|
+
if (!rule.id || typeof rule.id !== "string") {
|
|
274
|
+
errors.push({ path: `${path}.id`, message: "id is required and must be a string" });
|
|
275
|
+
}
|
|
276
|
+
if (typeof rule.enabled !== "boolean") {
|
|
277
|
+
errors.push({ path: `${path}.enabled`, message: "enabled is required and must be a boolean" });
|
|
278
|
+
}
|
|
279
|
+
if (rule.priority !== undefined && (typeof rule.priority !== "number" || rule.priority < 0)) {
|
|
280
|
+
errors.push({ path: `${path}.priority`, message: "priority must be a non-negative number" });
|
|
281
|
+
}
|
|
282
|
+
if (!rule.match) {
|
|
283
|
+
errors.push({ path: `${path}.match`, message: "match is required" });
|
|
284
|
+
} else {
|
|
285
|
+
if (!rule.match.in) {
|
|
286
|
+
errors.push({ path: `${path}.match.in`, message: "match.in is required" });
|
|
287
|
+
} else {
|
|
288
|
+
errors.push(...validateTokenMatcher(rule.match.in, `${path}.match.in`));
|
|
289
|
+
}
|
|
290
|
+
if (!rule.match.out) {
|
|
291
|
+
errors.push({ path: `${path}.match.out`, message: "match.out is required" });
|
|
292
|
+
} else {
|
|
293
|
+
errors.push(...validateTokenMatcher(rule.match.out, `${path}.match.out`));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!rule.fee) {
|
|
297
|
+
errors.push({ path: `${path}.fee`, message: "fee is required" });
|
|
298
|
+
} else {
|
|
299
|
+
errors.push(...validateFee(rule.fee, `${path}.fee`));
|
|
300
|
+
}
|
|
301
|
+
if (rule.valid_from !== undefined) {
|
|
302
|
+
if (typeof rule.valid_from !== "string") {
|
|
303
|
+
errors.push({ path: `${path}.valid_from`, message: "valid_from must be a string" });
|
|
304
|
+
} else if (!isValidDateString(rule.valid_from)) {
|
|
305
|
+
errors.push({ path: `${path}.valid_from`, message: `valid_from "${rule.valid_from}" is not a valid date string` });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (rule.valid_until !== undefined) {
|
|
309
|
+
if (typeof rule.valid_until !== "string") {
|
|
310
|
+
errors.push({ path: `${path}.valid_until`, message: "valid_until must be a string" });
|
|
311
|
+
} else if (!isValidDateString(rule.valid_until)) {
|
|
312
|
+
errors.push({ path: `${path}.valid_until`, message: `valid_until "${rule.valid_until}" is not a valid date string` });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return errors;
|
|
316
|
+
}
|
|
317
|
+
function validateConfig(config) {
|
|
318
|
+
const errors = [];
|
|
319
|
+
if (!config.version || typeof config.version !== "string") {
|
|
320
|
+
errors.push({ path: "version", message: "version is required and must be a string" });
|
|
321
|
+
}
|
|
322
|
+
if (!config.default_fee) {
|
|
323
|
+
errors.push({ path: "default_fee", message: "default_fee is required" });
|
|
324
|
+
} else {
|
|
325
|
+
errors.push(...validateFee(config.default_fee, "default_fee"));
|
|
326
|
+
}
|
|
327
|
+
if (!Array.isArray(config.rules)) {
|
|
328
|
+
errors.push({ path: "rules", message: "rules must be an array" });
|
|
329
|
+
} else {
|
|
330
|
+
const ruleIds = new Set;
|
|
331
|
+
for (let i = 0;i < config.rules.length; i++) {
|
|
332
|
+
const rule = config.rules[i];
|
|
333
|
+
errors.push(...validateRule(rule, i));
|
|
334
|
+
if (rule.id) {
|
|
335
|
+
if (ruleIds.has(rule.id)) {
|
|
336
|
+
errors.push({ path: `rules[${i}].id`, message: `Duplicate rule id: ${rule.id}` });
|
|
337
|
+
}
|
|
338
|
+
ruleIds.add(rule.id);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
valid: errors.length === 0,
|
|
344
|
+
errors
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
// src/rule-engine.ts
|
|
348
|
+
var DEFAULT_TOKEN_REGISTRY_URL = "https://1click.chaindefuser.com/v0/tokens";
|
|
349
|
+
var DEFAULT_CACHE_TTL_MS = 3600000;
|
|
350
|
+
var BPS_DIVISOR = 10000n;
|
|
351
|
+
var MAX_BPS2 = 1e4;
|
|
352
|
+
function getTotalBps(fee) {
|
|
353
|
+
if (Array.isArray(fee)) {
|
|
354
|
+
return fee.reduce((sum, f) => sum + f.bps, 0);
|
|
355
|
+
}
|
|
356
|
+
return fee.bps;
|
|
357
|
+
}
|
|
358
|
+
function parseAmount(amount) {
|
|
359
|
+
if (typeof amount === "bigint") {
|
|
360
|
+
return amount;
|
|
361
|
+
}
|
|
362
|
+
if (typeof amount !== "string") {
|
|
363
|
+
throw new Error(`Invalid amount: expected string or bigint, got ${typeof amount}`);
|
|
364
|
+
}
|
|
365
|
+
if (amount.trim() === "") {
|
|
366
|
+
throw new Error("Invalid amount: empty string");
|
|
367
|
+
}
|
|
368
|
+
if (!/^-?\d+$/.test(amount)) {
|
|
369
|
+
throw new Error(`Invalid amount: "${amount}" is not a valid integer string`);
|
|
370
|
+
}
|
|
371
|
+
const result = BigInt(amount);
|
|
372
|
+
if (result < 0n) {
|
|
373
|
+
throw new Error(`Invalid amount: "${amount}" must be non-negative`);
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
function validateBps(bps) {
|
|
378
|
+
if (typeof bps !== "number" || !Number.isFinite(bps)) {
|
|
379
|
+
throw new Error(`Invalid bps: expected a finite number, got ${bps}`);
|
|
380
|
+
}
|
|
381
|
+
if (!Number.isInteger(bps)) {
|
|
382
|
+
throw new Error(`Invalid bps: ${bps} must be an integer`);
|
|
383
|
+
}
|
|
384
|
+
if (bps < 0) {
|
|
385
|
+
throw new Error(`Invalid bps: ${bps} must be non-negative`);
|
|
386
|
+
}
|
|
387
|
+
if (bps > MAX_BPS2) {
|
|
388
|
+
throw new Error(`Invalid bps: ${bps} exceeds maximum of ${MAX_BPS2} (100%)`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function calculateFee(amount, bps) {
|
|
392
|
+
const amountBigInt = parseAmount(amount);
|
|
393
|
+
validateBps(bps);
|
|
394
|
+
const fee = amountBigInt * BigInt(bps) / BPS_DIVISOR;
|
|
395
|
+
return fee.toString();
|
|
396
|
+
}
|
|
397
|
+
function calculateAmountAfterFee(amount, bps) {
|
|
398
|
+
const amountBigInt = parseAmount(amount);
|
|
399
|
+
validateBps(bps);
|
|
400
|
+
const fee = BigInt(calculateFee(amountBigInt, bps));
|
|
401
|
+
return (amountBigInt - fee).toString();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
class RuleEngine {
|
|
405
|
+
matcher;
|
|
406
|
+
tokenRegistry;
|
|
407
|
+
feeConfig;
|
|
408
|
+
constructor(feeConfig, options) {
|
|
409
|
+
const validation = validateConfig(feeConfig);
|
|
410
|
+
if (!validation.valid) {
|
|
411
|
+
throw new Error(`Invalid fee config: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join(", ")}`);
|
|
412
|
+
}
|
|
413
|
+
this.feeConfig = feeConfig;
|
|
414
|
+
this.tokenRegistry = new CachedTokenRegistry({
|
|
415
|
+
url: options?.tokenRegistryUrl ?? DEFAULT_TOKEN_REGISTRY_URL,
|
|
416
|
+
cacheTtlMs: options?.tokenRegistryCacheTtlMs ?? DEFAULT_CACHE_TTL_MS
|
|
417
|
+
});
|
|
418
|
+
this.matcher = new RuleMatcher(this.feeConfig, this.tokenRegistry);
|
|
419
|
+
}
|
|
420
|
+
async initialize() {
|
|
421
|
+
await this.tokenRegistry.refresh();
|
|
422
|
+
}
|
|
423
|
+
async ensureReady() {
|
|
424
|
+
await this.tokenRegistry.ensureFresh();
|
|
425
|
+
}
|
|
426
|
+
match(request) {
|
|
427
|
+
return this.matcher.match(request);
|
|
428
|
+
}
|
|
429
|
+
async matchWithRefresh(request) {
|
|
430
|
+
await this.ensureReady();
|
|
431
|
+
return this.match(request);
|
|
432
|
+
}
|
|
433
|
+
getTokenRegistrySize() {
|
|
434
|
+
return this.tokenRegistry.size;
|
|
435
|
+
}
|
|
436
|
+
getFeeConfig() {
|
|
437
|
+
return this.feeConfig;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
export {
|
|
441
|
+
validateConfig,
|
|
442
|
+
getTotalBps,
|
|
443
|
+
calculateFee,
|
|
444
|
+
calculateAmountAfterFee,
|
|
445
|
+
RuleMatcher,
|
|
446
|
+
RuleEngine,
|
|
447
|
+
CachedTokenRegistry
|
|
448
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "intents-1click-rule-engine",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Fee configuration rule engine for NEAR Intents 1Click API",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/index.js"
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"dist"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
19
|
-
"build": "tsc -p tsconfig.build.json",
|
|
20
|
+
"build": "bun build index.ts --outdir dist --target node && bun tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
20
21
|
"prepublishOnly": "bun run build",
|
|
21
22
|
"test": "bun test",
|
|
22
23
|
"typecheck": "tsc --noEmit"
|
package/dist/src/matcher.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/src/rule-engine.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/src/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/src/validator.js
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
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
|
-
}
|