@yodlpay/payment-decoder 1.3.0 → 1.3.1

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 +171 -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,66 @@ 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 = Map.groupBy(allTransfers, (t) => getAddress(t.token));
273
+ const tokens = [...byToken.keys()];
274
+ for (const [i, tokenA] of tokens.entries()) {
275
+ if (mirrors.has(tokenA)) continue;
276
+ const transfersA = byToken.get(tokenA) ?? [];
277
+ for (const tokenB of tokens.slice(i + 1)) {
278
+ if (mirrors.has(tokenB)) continue;
279
+ const transfersB = byToken.get(tokenB) ?? [];
280
+ if (!areMirrorTransfers(transfersA, transfersB)) continue;
281
+ if (isKnownToken(tokenB) && !isKnownToken(tokenA)) {
282
+ mirrors.set(tokenA, tokenB);
283
+ } else {
284
+ mirrors.set(tokenB, tokenA);
285
+ }
286
+ }
287
+ }
288
+ return mirrors;
289
+ }
219
290
  function findInputTransfer(transfers, sender) {
220
291
  const fromSender = transfers.filter((t) => isAddressEqual(t.from, sender));
221
292
  if (fromSender.length === 0) return void 0;
222
- const largest = fromSender.reduce(
223
- (max, t) => t.amount > max.amount ? t : max
293
+ const maxAmount = fromSender.reduce(
294
+ (max, t) => t.amount > max ? t.amount : max,
295
+ 0n
224
296
  );
225
- return { token: largest.token, amount: largest.amount };
297
+ let candidates = fromSender.filter((t) => t.amount === maxAmount);
298
+ if (candidates.length > 1) {
299
+ const mirrors = detectMirrorTokens(transfers);
300
+ const filtered = candidates.filter(
301
+ (t) => !mirrors.has(getAddress(t.token))
302
+ );
303
+ if (filtered.length > 0) candidates = filtered;
304
+ }
305
+ const result = candidates.find((t) => isKnownToken(t.token)) ?? candidates[0];
306
+ if (!result) return void 0;
307
+ return { token: result.token, amount: result.amount };
226
308
  }
227
309
  function toBlockTimestamp(block) {
228
310
  return new Date(Number(block.timestamp) * 1e3);
@@ -245,20 +327,20 @@ import { decodeFunctionData, toFunctionSelector } from "viem";
245
327
  // src/validation.ts
246
328
  import { chains as chains2 } from "@yodlpay/tokenlists";
247
329
  import { isAddress, isHex } from "viem";
248
- import { z } from "zod";
330
+ import { z as z2 } from "zod";
249
331
  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), {
332
+ var txHashSchema = z2.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash").transform((val) => val);
333
+ var chainIdSchema = z2.coerce.number().int().refine((id) => validChainIds.includes(id), {
252
334
  message: `Chain ID must be one of: ${validChainIds.join(", ")}`
253
335
  });
254
- var ArgsSchema = z.union([
255
- z.tuple([txHashSchema, chainIdSchema]),
256
- z.tuple([txHashSchema])
336
+ var ArgsSchema = z2.union([
337
+ z2.tuple([txHashSchema, chainIdSchema]),
338
+ z2.tuple([txHashSchema])
257
339
  ]);
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)))
340
+ var WebhooksSchema = z2.array(
341
+ z2.object({
342
+ webhookAddress: z2.string().refine((val) => isAddress(val)),
343
+ payload: z2.array(z2.string().refine((val) => isHex(val)))
262
344
  }).transform((webhook) => ({
263
345
  ...webhook,
264
346
  memo: webhook.payload[0] ? decodeMemo(webhook.payload[0]) : ""
@@ -296,7 +378,7 @@ function extractEmbeddedParams(data) {
296
378
  }
297
379
 
298
380
  // src/relay-bridge.ts
299
- import { getRouter, getTokenByAddress } from "@yodlpay/tokenlists";
381
+ import { getRouter } from "@yodlpay/tokenlists";
300
382
  import {
301
383
  decodeEventLog as decodeEventLog2,
302
384
  isAddressEqual as isAddressEqual2
@@ -330,15 +412,15 @@ async function fetchRelayRequest(hash) {
330
412
  }
331
413
 
332
414
  // 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
- );
415
+ async function calculateOutputAmountGross(inputAmount, inputToken, inputChainId, outputToken, outputChainId, clients) {
416
+ const [{ decimals: inputDecimals }, { decimals: outputDecimals }] = await Promise.all([
417
+ resolveToken(inputToken, inputChainId, getClient(clients, inputChainId)),
418
+ resolveToken(
419
+ outputToken,
420
+ outputChainId,
421
+ getClient(clients, outputChainId)
422
+ )
423
+ ]);
342
424
  const decimalDiff = inputDecimals - outputDecimals;
343
425
  if (decimalDiff === 0) return inputAmount;
344
426
  if (decimalDiff > 0) return inputAmount / 10n ** BigInt(decimalDiff);
@@ -362,7 +444,7 @@ async function extractSenderFromSource(sourceReceipt, sourceProvider, sourceHash
362
444
  const sourceTx = await sourceProvider.getTransaction({ hash: sourceHash });
363
445
  return sourceTx.from;
364
446
  }
365
- function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inputAmountGross, inputChainId, outputChainId) {
447
+ async function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inputAmountGross, inputChainId, outputChainId, clients) {
366
448
  const sourceTransfers = extractTokenTransfers(sourceReceipt.logs);
367
449
  const inputTransfer = findInputTransfer(sourceTransfers, sender);
368
450
  const tokenIn = inputTransfer?.token;
@@ -379,12 +461,13 @@ function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inpu
379
461
  if (swapEvent !== void 0) {
380
462
  tokenOutAmountGross = swapEvent.tokenOutAmount;
381
463
  } else if (inputAmountGross > 0n && tokenIn && tokenOut) {
382
- tokenOutAmountGross = calculateOutputAmountGross(
464
+ tokenOutAmountGross = await calculateOutputAmountGross(
383
465
  inputAmountGross,
384
466
  tokenIn,
385
467
  inputChainId,
386
468
  tokenOut,
387
- outputChainId
469
+ outputChainId,
470
+ clients
388
471
  );
389
472
  }
390
473
  return {
@@ -437,14 +520,15 @@ async function decodeBridgePayment(hash, clients) {
437
520
  destProvider.getTransaction({ hash: destinationTxHash }),
438
521
  extractSenderFromSource(sourceReceipt, sourceProvider, sourceTxHash)
439
522
  ]);
440
- const tokens = resolveBridgeTokens(
523
+ const tokens = await resolveBridgeTokens(
441
524
  sourceReceipt,
442
525
  destReceipt,
443
526
  yodlEvent,
444
527
  sender,
445
528
  inputAmountGross,
446
529
  sourceChainId,
447
- destinationChainId
530
+ destinationChainId,
531
+ clients
448
532
  );
449
533
  const bridge = {
450
534
  sourceChainId,
@@ -504,14 +588,23 @@ async function decodePayment(hash, chainId, clients, cachedReceipt) {
504
588
  }
505
589
 
506
590
  // src/yodl-payment.ts
507
- import { getTokenByAddress as getTokenByAddress2 } from "@yodlpay/tokenlists";
508
591
  import {
509
592
  formatUnits,
510
593
  zeroAddress
511
594
  } from "viem";
512
- function buildTokenInfo(params) {
513
- const inToken = getTokenByAddress2(params.tokenIn, params.inChainId);
514
- const outToken = getTokenByAddress2(params.tokenOut, params.outChainId);
595
+ async function buildTokenInfo(params, clients) {
596
+ const [inToken, outToken] = await Promise.all([
597
+ resolveToken(
598
+ params.tokenIn,
599
+ params.inChainId,
600
+ getClient(clients, params.inChainId)
601
+ ),
602
+ resolveToken(
603
+ params.tokenOut,
604
+ params.outChainId,
605
+ getClient(clients, params.outChainId)
606
+ )
607
+ ]);
515
608
  return {
516
609
  tokenIn: {
517
610
  ...inToken,
@@ -533,19 +626,22 @@ function buildTokenInfo(params) {
533
626
  }
534
627
  };
535
628
  }
536
- function extractTokenInfo(paymentInfo, chainId, txHash) {
629
+ async function extractTokenInfo(paymentInfo, chainId, txHash, clients) {
537
630
  switch (paymentInfo.type) {
538
631
  case "direct": {
539
632
  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
- }),
633
+ ...await buildTokenInfo(
634
+ {
635
+ tokenIn: paymentInfo.token,
636
+ tokenInAmount: paymentInfo.amount,
637
+ tokenOut: paymentInfo.token,
638
+ tokenOutAmountGross: paymentInfo.amount,
639
+ tokenOutAmountNet: paymentInfo.amount,
640
+ inChainId: chainId,
641
+ outChainId: chainId
642
+ },
643
+ clients
644
+ ),
549
645
  sourceChainId: chainId,
550
646
  sourceTxHash: txHash,
551
647
  destinationChainId: chainId,
@@ -555,15 +651,18 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
555
651
  }
556
652
  case "swap": {
557
653
  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
- }),
654
+ ...await buildTokenInfo(
655
+ {
656
+ tokenIn: paymentInfo.tokenIn,
657
+ tokenInAmount: paymentInfo.tokenInAmount,
658
+ tokenOut: paymentInfo.token,
659
+ tokenOutAmountGross: paymentInfo.tokenOutAmount,
660
+ tokenOutAmountNet: paymentInfo.amount,
661
+ inChainId: chainId,
662
+ outChainId: chainId
663
+ },
664
+ clients
665
+ ),
567
666
  sourceChainId: chainId,
568
667
  sourceTxHash: txHash,
569
668
  destinationChainId: chainId,
@@ -573,15 +672,18 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
573
672
  }
574
673
  case "bridge": {
575
674
  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
- }),
675
+ ...await buildTokenInfo(
676
+ {
677
+ tokenIn: paymentInfo.tokenIn,
678
+ tokenInAmount: paymentInfo.tokenInAmount,
679
+ tokenOut: paymentInfo.tokenOut,
680
+ tokenOutAmountGross: paymentInfo.tokenOutAmountGross,
681
+ tokenOutAmountNet: paymentInfo.amount,
682
+ inChainId: paymentInfo.sourceChainId,
683
+ outChainId: paymentInfo.destinationChainId
684
+ },
685
+ clients
686
+ ),
585
687
  sourceChainId: paymentInfo.sourceChainId,
586
688
  sourceTxHash: paymentInfo.sourceTxHash,
587
689
  destinationChainId: paymentInfo.destinationChainId,
@@ -601,7 +703,12 @@ async function decodeYodlPayment(txHash, chainId, clients, cachedReceipt) {
601
703
  const firstWebhook = paymentInfo.webhooks[0];
602
704
  const processorAddress = firstWebhook?.webhookAddress ?? zeroAddress;
603
705
  const processorMemo = firstWebhook?.memo ?? "";
604
- const tokenInfo = extractTokenInfo(paymentInfo, chainId, txHash);
706
+ const tokenInfo = await extractTokenInfo(
707
+ paymentInfo,
708
+ chainId,
709
+ txHash,
710
+ clients
711
+ );
605
712
  return {
606
713
  senderAddress: paymentInfo.sender,
607
714
  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.1",
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",