@yodlpay/payment-decoder 1.3.0 → 1.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.
Files changed (2) hide show
  1. package/dist/index.js +176 -64
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -44,9 +44,11 @@ async function detectChain(hash, clients) {
44
44
  }
45
45
 
46
46
  // src/decode-utils.ts
47
+ import { tokenlist } from "@yodlpay/tokenlists";
47
48
  import {
48
49
  decodeEventLog,
49
- erc20Abi,
50
+ erc20Abi as erc20Abi2,
51
+ getAddress,
50
52
  isAddressEqual
51
53
  } from "viem";
52
54
 
@@ -113,7 +115,13 @@ function isExpectedDecodeError(error) {
113
115
  }
114
116
 
115
117
  // src/utils.ts
116
- import { hexToString } from "viem";
118
+ import { getTokenByAddress } from "@yodlpay/tokenlists";
119
+ import {
120
+ erc20Abi,
121
+ getContract,
122
+ hexToString
123
+ } from "viem";
124
+ import { z } from "zod";
117
125
  function decodeMemo(memo) {
118
126
  if (!memo || memo === "0x") return "";
119
127
  try {
@@ -122,6 +130,27 @@ function decodeMemo(memo) {
122
130
  return "";
123
131
  }
124
132
  }
133
+ var erc20String = z.string().max(64).transform((s) => s.replace(/\p{Cc}/gu, ""));
134
+ var erc20Decimals = z.number().int().nonnegative().max(36);
135
+ async function resolveToken(address, chainId, client) {
136
+ try {
137
+ return getTokenByAddress(address, chainId);
138
+ } catch {
139
+ const token = getContract({ address, abi: erc20Abi, client });
140
+ const [name, symbol, decimals] = await Promise.all([
141
+ token.read.name(),
142
+ token.read.symbol(),
143
+ token.read.decimals()
144
+ ]);
145
+ return {
146
+ chainId,
147
+ address,
148
+ name: erc20String.parse(name),
149
+ symbol: erc20String.parse(symbol),
150
+ decimals: erc20Decimals.parse(decimals)
151
+ };
152
+ }
153
+ }
125
154
 
126
155
  // src/decode-utils.ts
127
156
  function* matchingEvents(logs, options) {
@@ -204,7 +233,7 @@ function decodeSwapFromLogs(logs) {
204
233
  function extractTokenTransfers(logs) {
205
234
  return collectEventsFromLogs(
206
235
  logs,
207
- { abi: erc20Abi, eventName: "Transfer", context: "ERC20 Transfer" },
236
+ { abi: erc20Abi2, eventName: "Transfer", context: "ERC20 Transfer" },
208
237
  (decoded, log) => {
209
238
  const args = decoded.args;
210
239
  return {
@@ -216,13 +245,71 @@ function extractTokenTransfers(logs) {
216
245
  }
217
246
  );
218
247
  }
248
+ function isKnownToken(address) {
249
+ return tokenlist.some((t) => isAddressEqual(t.address, address));
250
+ }
251
+ function transferSignature(t) {
252
+ return `${getAddress(t.from)}:${getAddress(t.to)}:${t.amount}`;
253
+ }
254
+ function areMirrorTransfers(a, b) {
255
+ if (a.length !== b.length) return false;
256
+ const counts = /* @__PURE__ */ new Map();
257
+ for (const t of a) {
258
+ const key = transferSignature(t);
259
+ counts.set(key, (counts.get(key) ?? 0) + 1);
260
+ }
261
+ for (const t of b) {
262
+ const key = transferSignature(t);
263
+ const count = counts.get(key);
264
+ if (!count) return false;
265
+ if (count === 1) counts.delete(key);
266
+ else counts.set(key, count - 1);
267
+ }
268
+ return counts.size === 0;
269
+ }
270
+ function detectMirrorTokens(allTransfers) {
271
+ const mirrors = /* @__PURE__ */ new Map();
272
+ const byToken = /* @__PURE__ */ new Map();
273
+ for (const t of allTransfers) {
274
+ const key = getAddress(t.token);
275
+ const group = byToken.get(key);
276
+ group ? group.push(t) : byToken.set(key, [t]);
277
+ }
278
+ const tokens = [...byToken.keys()];
279
+ for (const [i, tokenA] of tokens.entries()) {
280
+ if (mirrors.has(tokenA)) continue;
281
+ const transfersA = byToken.get(tokenA) ?? [];
282
+ for (const tokenB of tokens.slice(i + 1)) {
283
+ if (mirrors.has(tokenB)) continue;
284
+ const transfersB = byToken.get(tokenB) ?? [];
285
+ if (!areMirrorTransfers(transfersA, transfersB)) continue;
286
+ if (isKnownToken(tokenB) && !isKnownToken(tokenA)) {
287
+ mirrors.set(tokenA, tokenB);
288
+ } else {
289
+ mirrors.set(tokenB, tokenA);
290
+ }
291
+ }
292
+ }
293
+ return mirrors;
294
+ }
219
295
  function findInputTransfer(transfers, sender) {
220
296
  const fromSender = transfers.filter((t) => isAddressEqual(t.from, sender));
221
297
  if (fromSender.length === 0) return void 0;
222
- const largest = fromSender.reduce(
223
- (max, t) => t.amount > max.amount ? t : max
298
+ const maxAmount = fromSender.reduce(
299
+ (max, t) => t.amount > max ? t.amount : max,
300
+ 0n
224
301
  );
225
- return { token: largest.token, amount: largest.amount };
302
+ let candidates = fromSender.filter((t) => t.amount === maxAmount);
303
+ if (candidates.length > 1) {
304
+ const mirrors = detectMirrorTokens(transfers);
305
+ const filtered = candidates.filter(
306
+ (t) => !mirrors.has(getAddress(t.token))
307
+ );
308
+ if (filtered.length > 0) candidates = filtered;
309
+ }
310
+ const result = candidates.find((t) => isKnownToken(t.token)) ?? candidates[0];
311
+ if (!result) return void 0;
312
+ return { token: result.token, amount: result.amount };
226
313
  }
227
314
  function toBlockTimestamp(block) {
228
315
  return new Date(Number(block.timestamp) * 1e3);
@@ -245,20 +332,20 @@ import { decodeFunctionData, toFunctionSelector } from "viem";
245
332
  // src/validation.ts
246
333
  import { chains as chains2 } from "@yodlpay/tokenlists";
247
334
  import { isAddress, isHex } from "viem";
248
- import { z } from "zod";
335
+ import { z as z2 } from "zod";
249
336
  var validChainIds = chains2.map((c) => c.id);
250
- var txHashSchema = z.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash").transform((val) => val);
251
- var chainIdSchema = z.coerce.number().int().refine((id) => validChainIds.includes(id), {
337
+ var txHashSchema = z2.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash").transform((val) => val);
338
+ var chainIdSchema = z2.coerce.number().int().refine((id) => validChainIds.includes(id), {
252
339
  message: `Chain ID must be one of: ${validChainIds.join(", ")}`
253
340
  });
254
- var ArgsSchema = z.union([
255
- z.tuple([txHashSchema, chainIdSchema]),
256
- z.tuple([txHashSchema])
341
+ var ArgsSchema = z2.union([
342
+ z2.tuple([txHashSchema, chainIdSchema]),
343
+ z2.tuple([txHashSchema])
257
344
  ]);
258
- var WebhooksSchema = z.array(
259
- z.object({
260
- webhookAddress: z.string().refine((val) => isAddress(val)),
261
- payload: z.array(z.string().refine((val) => isHex(val)))
345
+ var WebhooksSchema = z2.array(
346
+ z2.object({
347
+ webhookAddress: z2.string().refine((val) => isAddress(val)),
348
+ payload: z2.array(z2.string().refine((val) => isHex(val)))
262
349
  }).transform((webhook) => ({
263
350
  ...webhook,
264
351
  memo: webhook.payload[0] ? decodeMemo(webhook.payload[0]) : ""
@@ -296,7 +383,7 @@ function extractEmbeddedParams(data) {
296
383
  }
297
384
 
298
385
  // src/relay-bridge.ts
299
- import { getRouter, getTokenByAddress } from "@yodlpay/tokenlists";
386
+ import { getRouter } from "@yodlpay/tokenlists";
300
387
  import {
301
388
  decodeEventLog as decodeEventLog2,
302
389
  isAddressEqual as isAddressEqual2
@@ -330,15 +417,15 @@ async function fetchRelayRequest(hash) {
330
417
  }
331
418
 
332
419
  // src/relay-bridge.ts
333
- function calculateOutputAmountGross(inputAmount, inputToken, inputChainId, outputToken, outputChainId) {
334
- const { decimals: inputDecimals } = getTokenByAddress(
335
- inputToken,
336
- inputChainId
337
- );
338
- const { decimals: outputDecimals } = getTokenByAddress(
339
- outputToken,
340
- outputChainId
341
- );
420
+ async function calculateOutputAmountGross(inputAmount, inputToken, inputChainId, outputToken, outputChainId, clients) {
421
+ const [{ decimals: inputDecimals }, { decimals: outputDecimals }] = await Promise.all([
422
+ resolveToken(inputToken, inputChainId, getClient(clients, inputChainId)),
423
+ resolveToken(
424
+ outputToken,
425
+ outputChainId,
426
+ getClient(clients, outputChainId)
427
+ )
428
+ ]);
342
429
  const decimalDiff = inputDecimals - outputDecimals;
343
430
  if (decimalDiff === 0) return inputAmount;
344
431
  if (decimalDiff > 0) return inputAmount / 10n ** BigInt(decimalDiff);
@@ -362,7 +449,7 @@ async function extractSenderFromSource(sourceReceipt, sourceProvider, sourceHash
362
449
  const sourceTx = await sourceProvider.getTransaction({ hash: sourceHash });
363
450
  return sourceTx.from;
364
451
  }
365
- function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inputAmountGross, inputChainId, outputChainId) {
452
+ async function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inputAmountGross, inputChainId, outputChainId, clients) {
366
453
  const sourceTransfers = extractTokenTransfers(sourceReceipt.logs);
367
454
  const inputTransfer = findInputTransfer(sourceTransfers, sender);
368
455
  const tokenIn = inputTransfer?.token;
@@ -379,12 +466,13 @@ function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inpu
379
466
  if (swapEvent !== void 0) {
380
467
  tokenOutAmountGross = swapEvent.tokenOutAmount;
381
468
  } else if (inputAmountGross > 0n && tokenIn && tokenOut) {
382
- tokenOutAmountGross = calculateOutputAmountGross(
469
+ tokenOutAmountGross = await calculateOutputAmountGross(
383
470
  inputAmountGross,
384
471
  tokenIn,
385
472
  inputChainId,
386
473
  tokenOut,
387
- outputChainId
474
+ outputChainId,
475
+ clients
388
476
  );
389
477
  }
390
478
  return {
@@ -437,14 +525,15 @@ async function decodeBridgePayment(hash, clients) {
437
525
  destProvider.getTransaction({ hash: destinationTxHash }),
438
526
  extractSenderFromSource(sourceReceipt, sourceProvider, sourceTxHash)
439
527
  ]);
440
- const tokens = resolveBridgeTokens(
528
+ const tokens = await resolveBridgeTokens(
441
529
  sourceReceipt,
442
530
  destReceipt,
443
531
  yodlEvent,
444
532
  sender,
445
533
  inputAmountGross,
446
534
  sourceChainId,
447
- destinationChainId
535
+ destinationChainId,
536
+ clients
448
537
  );
449
538
  const bridge = {
450
539
  sourceChainId,
@@ -504,14 +593,23 @@ async function decodePayment(hash, chainId, clients, cachedReceipt) {
504
593
  }
505
594
 
506
595
  // src/yodl-payment.ts
507
- import { getTokenByAddress as getTokenByAddress2 } from "@yodlpay/tokenlists";
508
596
  import {
509
597
  formatUnits,
510
598
  zeroAddress
511
599
  } from "viem";
512
- function buildTokenInfo(params) {
513
- const inToken = getTokenByAddress2(params.tokenIn, params.inChainId);
514
- const outToken = getTokenByAddress2(params.tokenOut, params.outChainId);
600
+ async function buildTokenInfo(params, clients) {
601
+ const [inToken, outToken] = await Promise.all([
602
+ resolveToken(
603
+ params.tokenIn,
604
+ params.inChainId,
605
+ getClient(clients, params.inChainId)
606
+ ),
607
+ resolveToken(
608
+ params.tokenOut,
609
+ params.outChainId,
610
+ getClient(clients, params.outChainId)
611
+ )
612
+ ]);
515
613
  return {
516
614
  tokenIn: {
517
615
  ...inToken,
@@ -533,19 +631,22 @@ function buildTokenInfo(params) {
533
631
  }
534
632
  };
535
633
  }
536
- function extractTokenInfo(paymentInfo, chainId, txHash) {
634
+ async function extractTokenInfo(paymentInfo, chainId, txHash, clients) {
537
635
  switch (paymentInfo.type) {
538
636
  case "direct": {
539
637
  return {
540
- ...buildTokenInfo({
541
- tokenIn: paymentInfo.token,
542
- tokenInAmount: paymentInfo.amount,
543
- tokenOut: paymentInfo.token,
544
- tokenOutAmountGross: paymentInfo.amount,
545
- tokenOutAmountNet: paymentInfo.amount,
546
- inChainId: chainId,
547
- outChainId: chainId
548
- }),
638
+ ...await buildTokenInfo(
639
+ {
640
+ tokenIn: paymentInfo.token,
641
+ tokenInAmount: paymentInfo.amount,
642
+ tokenOut: paymentInfo.token,
643
+ tokenOutAmountGross: paymentInfo.amount,
644
+ tokenOutAmountNet: paymentInfo.amount,
645
+ inChainId: chainId,
646
+ outChainId: chainId
647
+ },
648
+ clients
649
+ ),
549
650
  sourceChainId: chainId,
550
651
  sourceTxHash: txHash,
551
652
  destinationChainId: chainId,
@@ -555,15 +656,18 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
555
656
  }
556
657
  case "swap": {
557
658
  return {
558
- ...buildTokenInfo({
559
- tokenIn: paymentInfo.tokenIn,
560
- tokenInAmount: paymentInfo.tokenInAmount,
561
- tokenOut: paymentInfo.token,
562
- tokenOutAmountGross: paymentInfo.tokenOutAmount,
563
- tokenOutAmountNet: paymentInfo.amount,
564
- inChainId: chainId,
565
- outChainId: chainId
566
- }),
659
+ ...await buildTokenInfo(
660
+ {
661
+ tokenIn: paymentInfo.tokenIn,
662
+ tokenInAmount: paymentInfo.tokenInAmount,
663
+ tokenOut: paymentInfo.token,
664
+ tokenOutAmountGross: paymentInfo.tokenOutAmount,
665
+ tokenOutAmountNet: paymentInfo.amount,
666
+ inChainId: chainId,
667
+ outChainId: chainId
668
+ },
669
+ clients
670
+ ),
567
671
  sourceChainId: chainId,
568
672
  sourceTxHash: txHash,
569
673
  destinationChainId: chainId,
@@ -573,15 +677,18 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
573
677
  }
574
678
  case "bridge": {
575
679
  return {
576
- ...buildTokenInfo({
577
- tokenIn: paymentInfo.tokenIn,
578
- tokenInAmount: paymentInfo.tokenInAmount,
579
- tokenOut: paymentInfo.tokenOut,
580
- tokenOutAmountGross: paymentInfo.tokenOutAmountGross,
581
- tokenOutAmountNet: paymentInfo.amount,
582
- inChainId: paymentInfo.sourceChainId,
583
- outChainId: paymentInfo.destinationChainId
584
- }),
680
+ ...await buildTokenInfo(
681
+ {
682
+ tokenIn: paymentInfo.tokenIn,
683
+ tokenInAmount: paymentInfo.tokenInAmount,
684
+ tokenOut: paymentInfo.tokenOut,
685
+ tokenOutAmountGross: paymentInfo.tokenOutAmountGross,
686
+ tokenOutAmountNet: paymentInfo.amount,
687
+ inChainId: paymentInfo.sourceChainId,
688
+ outChainId: paymentInfo.destinationChainId
689
+ },
690
+ clients
691
+ ),
585
692
  sourceChainId: paymentInfo.sourceChainId,
586
693
  sourceTxHash: paymentInfo.sourceTxHash,
587
694
  destinationChainId: paymentInfo.destinationChainId,
@@ -601,7 +708,12 @@ async function decodeYodlPayment(txHash, chainId, clients, cachedReceipt) {
601
708
  const firstWebhook = paymentInfo.webhooks[0];
602
709
  const processorAddress = firstWebhook?.webhookAddress ?? zeroAddress;
603
710
  const processorMemo = firstWebhook?.memo ?? "";
604
- const tokenInfo = extractTokenInfo(paymentInfo, chainId, txHash);
711
+ const tokenInfo = await extractTokenInfo(
712
+ paymentInfo,
713
+ chainId,
714
+ txHash,
715
+ clients
716
+ );
605
717
  return {
606
718
  senderAddress: paymentInfo.sender,
607
719
  receiverAddress: paymentInfo.receiver,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yodlpay/payment-decoder",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Decode Yodl payment hashes into structured payment data",
5
5
  "keywords": [
6
6
  "yodl",
@@ -38,8 +38,8 @@
38
38
  "prepublishOnly": "bun run build"
39
39
  },
40
40
  "devDependencies": {
41
- "@biomejs/biome": "^2.3.14",
42
- "@relayprotocol/relay-sdk": "^5.1.0",
41
+ "@biomejs/biome": "^2.4.4",
42
+ "@relayprotocol/relay-sdk": "^5.2.0",
43
43
  "@semantic-release/changelog": "^6.0.3",
44
44
  "@semantic-release/git": "^10.0.1",
45
45
  "@types/bun": "latest",
@@ -48,7 +48,7 @@
48
48
  "zod": "^4.3.6"
49
49
  },
50
50
  "dependencies": {
51
- "@yodlpay/tokenlists": "^1.1.7"
51
+ "@yodlpay/tokenlists": "^1.1.12"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "typescript": "^5",