@pioneer-platform/pioneer-coins 9.3.0 → 9.3.2

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.
@@ -3,6 +3,33 @@ interface BaseTx {
3
3
  asset: string;
4
4
  destAddr: string;
5
5
  }
6
+ export interface MemoValidation {
7
+ ok: boolean;
8
+ errors: string[];
9
+ warnings: string[];
10
+ normalized?: string;
11
+ fields?: {
12
+ chain: string;
13
+ symbol: string;
14
+ destination: string;
15
+ limit?: string;
16
+ stream?: string;
17
+ affiliateTag?: string;
18
+ affiliateBP?: number;
19
+ };
20
+ suggestedFix?: string;
21
+ }
22
+ /**
23
+ * Comprehensive THORChain Swap Memo Validator
24
+ *
25
+ * Validates memos of the form:
26
+ * =:<CHAIN>.<SYMBOL>:<DEST>[:<limit>[/<stream]][:<affiliateTag>:<affiliateBP>]
27
+ *
28
+ * @param memoRaw - The raw memo string to validate
29
+ * @returns MemoValidation object with validation results
30
+ */
31
+ export declare function validateThorchainSwapMemo(memoRaw: string): MemoValidation;
32
+ export declare function validateMemo(memo: string): boolean;
6
33
  export declare function normalizeSwapMemo(memo: string): string;
7
34
  export declare function createMemo(tx: BaseTx): string;
8
35
  export declare function parseMemo(memo: string): BaseTx;
package/lib/thorchain.js CHANGED
@@ -4,6 +4,8 @@
4
4
  ----------------------
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.validateThorchainSwapMemo = validateThorchainSwapMemo;
8
+ exports.validateMemo = validateMemo;
7
9
  exports.normalizeSwapMemo = normalizeSwapMemo;
8
10
  exports.createMemo = createMemo;
9
11
  exports.parseMemo = parseMemo;
@@ -198,7 +200,236 @@ function buildNoOpMemo(tx) {
198
200
  ];
199
201
  return truncateMemo(parts.filter(function (part) { return part; }).join(':'));
200
202
  }
201
- // Main function to create a memo from any transaction type
203
+ // Basic destination sanity check by chain
204
+ function basicDestCheck(chain, dest) {
205
+ if (chain === 'BTC') {
206
+ // BTC: accept base58 (1..,3..) or bech32 (bc1.., tb1..)
207
+ var isB58 = /^[123mn][1-9A-HJ-NP-Za-km-z]{24,}$/;
208
+ var isBech32 = /^(bc1|tb1)[a-z0-9]{20,}$/;
209
+ if (!isB58.test(dest) && !isBech32.test(dest)) {
210
+ return 'destination does not look like a valid BTC address';
211
+ }
212
+ }
213
+ if (chain === 'ETH') {
214
+ if (!/^0x[a-fA-F0-9]{40}$/.test(dest)) {
215
+ return 'destination is not a valid Ethereum address';
216
+ }
217
+ }
218
+ if (chain === 'DASH') {
219
+ if (!/^X[1-9A-HJ-NP-Za-km-z]{33}$/.test(dest)) {
220
+ return 'destination does not look like a valid DASH address';
221
+ }
222
+ }
223
+ return undefined;
224
+ }
225
+ /**
226
+ * Comprehensive THORChain Swap Memo Validator
227
+ *
228
+ * Validates memos of the form:
229
+ * =:<CHAIN>.<SYMBOL>:<DEST>[:<limit>[/<stream]][:<affiliateTag>:<affiliateBP>]
230
+ *
231
+ * @param memoRaw - The raw memo string to validate
232
+ * @returns MemoValidation object with validation results
233
+ */
234
+ function validateThorchainSwapMemo(memoRaw) {
235
+ var errors = [];
236
+ var warnings = [];
237
+ var MAX_MEMO_LEN = 250;
238
+ var ASCII_SAFE = /^[\x20-\x7E]+$/; // printable ASCII (no control chars)
239
+ if (!memoRaw || memoRaw.trim() === '') {
240
+ return { ok: false, errors: ['memo is empty'], warnings: warnings };
241
+ }
242
+ var memo = memoRaw.trim();
243
+ if (memo.length > MAX_MEMO_LEN) {
244
+ errors.push("memo exceeds ".concat(MAX_MEMO_LEN, " bytes"));
245
+ }
246
+ if (!ASCII_SAFE.test(memo)) {
247
+ errors.push('memo must be printable ASCII (no control characters)');
248
+ }
249
+ if (!memo.startsWith('=:')) {
250
+ errors.push('memo must start with "=:<CHAIN>.<SYMBOL>:"');
251
+ }
252
+ // Early exit if start is wrong
253
+ if (errors.length)
254
+ return { ok: false, errors: errors, warnings: warnings };
255
+ // Split by colon
256
+ var parts = memo.slice(2).split(':');
257
+ if (parts.length < 2) {
258
+ errors.push('memo missing destination segment');
259
+ return { ok: false, errors: errors, warnings: warnings };
260
+ }
261
+ var chainDotSymbol = parts[0];
262
+ var shorthandFix;
263
+ // Check if it's a shorthand that needs expansion
264
+ var maybeSh = chainDotSymbol.toLowerCase();
265
+ if (assetShortCodeMap[maybeSh]) {
266
+ // This is a shorthand - it's invalid in the memo itself
267
+ errors.push("shorthand \"".concat(chainDotSymbol, "\" not allowed - must use full CHAIN.SYMBOL format"));
268
+ shorthandFix = assetShortCodeMap[maybeSh];
269
+ if (shorthandFix) {
270
+ chainDotSymbol = shorthandFix;
271
+ warnings.push("expanded shorthand \"".concat(parts[0], "\" to \"").concat(shorthandFix, "\""));
272
+ }
273
+ }
274
+ var cs = chainDotSymbol.split('.');
275
+ if (cs.length !== 2) {
276
+ errors.push('first segment must be "<CHAIN>.<SYMBOL>" (e.g., BTC.BTC)');
277
+ if (cs.length === 1 && !assetShortCodeMap[cs[0].toLowerCase()]) {
278
+ // Not even a valid shorthand
279
+ errors.push("\"".concat(cs[0], "\" is not a valid chain identifier"));
280
+ }
281
+ return { ok: false, errors: errors, warnings: warnings };
282
+ }
283
+ var chain = cs[0];
284
+ var symbol = cs[1];
285
+ if (!/^[A-Z0-9]+$/.test(chain) || !/^[A-Z0-9\-]+$/.test(symbol)) {
286
+ errors.push('CHAIN and SYMBOL must be uppercase alphanumerics');
287
+ }
288
+ var destination = parts[1];
289
+ if (!destination)
290
+ errors.push('destination is empty');
291
+ // Optional limit/stream
292
+ var limit;
293
+ var stream;
294
+ if (parts[2] !== undefined && parts[2] !== '') {
295
+ var ls = parts[2].split('/');
296
+ limit = ls[0];
297
+ if (limit && !/^\d+$/.test(limit)) {
298
+ errors.push('limit must be a non-negative integer');
299
+ }
300
+ if (ls.length > 1) {
301
+ stream = ls[1];
302
+ if (stream && !/^\d+$/.test(stream)) {
303
+ errors.push('stream must be a non-negative integer');
304
+ }
305
+ }
306
+ }
307
+ // Optional affiliate
308
+ var affiliateTag;
309
+ var affiliateBP;
310
+ if (parts[3] !== undefined && parts[3] !== '') {
311
+ affiliateTag = parts[3];
312
+ if (/\s/.test(affiliateTag))
313
+ warnings.push('affiliateTag should not contain spaces');
314
+ }
315
+ if (parts[4] !== undefined && parts[4] !== '') {
316
+ if (!/^\d+$/.test(parts[4])) {
317
+ errors.push('affiliateBP must be integer 0..1000');
318
+ }
319
+ else {
320
+ affiliateBP = Number(parts[4]);
321
+ if (affiliateBP < 0 || affiliateBP > 1000) {
322
+ errors.push('affiliateBP out of range (0..1000)');
323
+ }
324
+ }
325
+ }
326
+ // Destination validation - convert warnings to errors for critical issues
327
+ var destErr = basicDestCheck(chain, destination);
328
+ if (destErr) {
329
+ // For ETH, a bad address is an error, not a warning
330
+ if (chain === 'ETH' && destination.length < 42) {
331
+ errors.push(destErr);
332
+ }
333
+ else {
334
+ warnings.push(destErr);
335
+ }
336
+ }
337
+ var normalized = "=:" +
338
+ "".concat(chain, ".").concat(symbol, ":") +
339
+ "".concat(destination) +
340
+ (limit !== undefined || stream !== undefined
341
+ ? ":".concat(limit !== null && limit !== void 0 ? limit : '0').concat(stream !== undefined ? "/".concat(stream) : '')
342
+ : '') +
343
+ (affiliateTag !== undefined
344
+ ? ":".concat(affiliateTag, ":").concat(affiliateBP !== null && affiliateBP !== void 0 ? affiliateBP : 0)
345
+ : '');
346
+ return {
347
+ ok: errors.length === 0,
348
+ errors: errors,
349
+ warnings: warnings,
350
+ normalized: normalized,
351
+ fields: {
352
+ chain: chain,
353
+ symbol: symbol,
354
+ destination: destination,
355
+ limit: limit,
356
+ stream: stream,
357
+ affiliateTag: affiliateTag,
358
+ affiliateBP: affiliateBP,
359
+ },
360
+ suggestedFix: shorthandFix ? normalized : undefined,
361
+ };
362
+ }
363
+ // Validate a THORChain memo - returns true if valid, false otherwise
364
+ function validateMemo(memo) {
365
+ try {
366
+ if (!memo || memo.trim() === '')
367
+ return false;
368
+ var trimmed = memo.trim();
369
+ // Check basic format
370
+ if (!trimmed.startsWith('=:') && !trimmed.startsWith('SWAP:') &&
371
+ !trimmed.startsWith('DEPOSIT:') && !trimmed.startsWith('WITHDRAW:') &&
372
+ !trimmed.startsWith('LOAN+:') && !trimmed.startsWith('LOAN-:') &&
373
+ !trimmed.startsWith('ADD:') && !trimmed.startsWith('WD:') &&
374
+ !trimmed.startsWith('BOND:') && !trimmed.startsWith('UNBOND:') &&
375
+ !trimmed.startsWith('MIGRATE:') && !trimmed.startsWith('*NOOP*:')) {
376
+ return false;
377
+ }
378
+ // For swap memos, validate format more strictly
379
+ if (trimmed.startsWith('=:') || trimmed.startsWith('SWAP:')) {
380
+ var prefix = trimmed.startsWith('=:') ? '=:' : 'SWAP:';
381
+ var parts = trimmed.slice(prefix.length).split(':');
382
+ // Must have at least asset and destination
383
+ if (parts.length < 2)
384
+ return false;
385
+ // Check asset format (must be CHAIN.SYMBOL)
386
+ var asset = parts[0];
387
+ // Don't allow single-letter shorthands in validation
388
+ if (asset.length === 1)
389
+ return false;
390
+ var assetParts = asset.split('.');
391
+ if (assetParts.length !== 2)
392
+ return false;
393
+ var chain = assetParts[0], symbol = assetParts[1];
394
+ // Must be uppercase alphanumeric (symbol can have dash)
395
+ if (!/^[A-Z0-9]+$/.test(chain))
396
+ return false;
397
+ if (!/^[A-Z0-9\-]+$/.test(symbol))
398
+ return false;
399
+ // Must have destination
400
+ var destination = parts[1];
401
+ if (!destination || destination.trim() === '')
402
+ return false;
403
+ // Check optional limit/stream format
404
+ if (parts[2] && parts[2] !== '') {
405
+ var limitStream = parts[2].split('/');
406
+ // Limit must be integer if present
407
+ if (limitStream[0] && !/^\d+$/.test(limitStream[0]))
408
+ return false;
409
+ // Stream must be integer if present
410
+ if (limitStream[1] && !/^\d+$/.test(limitStream[1]))
411
+ return false;
412
+ }
413
+ // Check affiliate BP if present
414
+ if (parts[4] && parts[4] !== '') {
415
+ if (!/^\d+$/.test(parts[4]))
416
+ return false;
417
+ var bp = parseInt(parts[4]);
418
+ if (bp < 0 || bp > 1000)
419
+ return false;
420
+ }
421
+ }
422
+ // Check memo length
423
+ if (Buffer.from(trimmed, 'utf-8').length > 250)
424
+ return false;
425
+ // If we made it here, memo is valid
426
+ return true;
427
+ }
428
+ catch (error) {
429
+ // Any error means invalid memo
430
+ return false;
431
+ }
432
+ }
202
433
  // Normalize and validate swap memo
203
434
  function normalizeSwapMemo(memo) {
204
435
  // Ensure "=:" prefix and fully qualified asset
@@ -379,3 +610,31 @@ function parseMemo(memo) {
379
610
  throw new Error("Unsupported memo type: ".concat(type));
380
611
  }
381
612
  }
613
+ /**
614
+ * Thorchain Swap Memo Validator
615
+ *
616
+ * Supports memos of the form:
617
+ * =:<CHAIN>.<SYMBOL>:<DEST>[:<limit>[/<stream]][:<affiliateTag>:<affiliateBP>]
618
+ *
619
+ * Examples:
620
+ * =:BTC.BTC:bc1qxyz...
621
+ * =:BTC.BTC:bc1qxyz...:120000
622
+ * =:BTC.BTC:1JNYtQ...:120000/30
623
+ * =:ETH.ETH:0xabc...:0:agg:25
624
+ */
625
+ var SHORTHANDS = {
626
+ 'b': 'BTC.BTC',
627
+ 'btc': 'BTC.BTC',
628
+ 'e': 'ETH.ETH',
629
+ 'eth': 'ETH.ETH',
630
+ 'r': 'THOR.RUNE',
631
+ 'd': 'DOGE.DOGE',
632
+ 'l': 'LTC.LTC',
633
+ 'c': 'BCH.BCH',
634
+ 'a': 'AVAX.AVAX',
635
+ 's': 'BSC.BNB',
636
+ 'n': 'BNB.BNB',
637
+ 'g': 'GAIA.ATOM',
638
+ };
639
+ var MAX_MEMO_LEN = 250;
640
+ var ASCII_SAFE = /^[\x20-\x7E]+$/; // printable ASCII (no control chars)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pioneer-platform/pioneer-coins",
3
- "version": "9.3.0",
3
+ "version": "9.3.2",
4
4
  "main": "./lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "_moduleAliases": {