@yodlpay/payment-decoder 1.2.0 → 1.3.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 CHANGED
@@ -38,6 +38,23 @@ Decode a Yodl payment transaction into a structured `YodlPayment` object.
38
38
  - `chainId` - Chain ID where the transaction was executed
39
39
  - `clients` - Map of chainId → PublicClient (must include all chains needed for bridges)
40
40
 
41
+ ### `detectChain(hash, clients)`
42
+
43
+ Auto-detect which chain a transaction belongs to by trying all configured chains in parallel. Useful when the chain ID is unknown.
44
+
45
+ - `hash` - Transaction hash to look up
46
+ - `clients` - Map of chainId → PublicClient
47
+
48
+ Returns `{ chainId, receipt }` — the receipt is included so it can be passed to `decodePayment` / `decodeYodlPayment` to avoid a duplicate RPC call.
49
+
50
+ ```ts
51
+ import { detectChain, decodeYodlPayment, createClients } from "@yodlpay/payment-decoder";
52
+
53
+ const clients = createClients();
54
+ const { chainId, receipt } = await detectChain("0x1234...abcd", clients);
55
+ const payment = await decodeYodlPayment("0x1234...abcd", chainId, clients, receipt);
56
+ ```
57
+
41
58
  ### `ChainClients`
42
59
 
43
60
  Type alias for the clients map: `Record<number, PublicClient>`
@@ -47,18 +64,22 @@ Type alias for the clients map: `Record<number, PublicClient>`
47
64
  Decode a transaction directly from the command line. Outputs JSON to stdout.
48
65
 
49
66
  ```bash
50
- bun run decode <txHash> <chainId>
67
+ bun run decode <txHash> [chainId]
51
68
  ```
52
69
 
53
70
  **Arguments:**
54
71
 
55
72
  - `txHash` - The transaction hash (64 hex characters with `0x` prefix)
56
- - `chainId` - The chain ID where the transaction occurred (e.g., `8453` for Base, `42161` for Arbitrum)
73
+ - `chainId` *(optional)* - The chain ID where the transaction occurred (e.g., `8453` for Base, `42161` for Arbitrum). If omitted, the chain is auto-detected.
57
74
 
58
- **Example:**
75
+ **Examples:**
59
76
 
60
77
  ```bash
78
+ # With explicit chain ID
61
79
  bun run decode 0xe7ecad85dcfb6b4e25c833fa3617a45cf34df505cb698e04ad7d75a39032158f 42161
80
+
81
+ # Auto-detect chain
82
+ bun run decode 0xe7ecad85dcfb6b4e25c833fa3617a45cf34df505cb698e04ad7d75a39032158f
62
83
  ```
63
84
 
64
85
  The CLI uses public RPC endpoints. For production use, configure your own RPC URLs via the library API.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,16 @@
1
- import { PublicClient, Address, Hex, Log } from 'viem';
1
+ import { PublicClient, Hex, TransactionReceipt, Address, Log } from 'viem';
2
2
  import { TokenInfo } from '@yodlpay/tokenlists';
3
3
 
4
4
  type ChainClients = Record<number, PublicClient>;
5
5
  declare function createClients(): ChainClients;
6
+ /**
7
+ * Auto-detect which chain a transaction belongs to by trying all configured
8
+ * chains in parallel. Returns both the chain ID and the receipt for reuse.
9
+ */
10
+ declare function detectChain(hash: Hex, clients: ChainClients): Promise<{
11
+ chainId: number;
12
+ receipt: TransactionReceipt;
13
+ }>;
6
14
 
7
15
  type ServiceProvider = "relay";
8
16
  interface Webhook {
@@ -27,8 +35,8 @@ interface SwapInfo {
27
35
  service: ServiceProvider;
28
36
  }
29
37
  interface BridgeInfo extends SwapInfo {
30
- originChainId: number;
31
- originTxHash: Hex;
38
+ sourceChainId: number;
39
+ sourceTxHash: Hex;
32
40
  destinationChainId: number;
33
41
  destinationTxHash: Hex;
34
42
  tokenOutAmountGross: bigint;
@@ -59,8 +67,8 @@ interface YodlPayment {
59
67
  data: Record<string, unknown>;
60
68
  tokenIn: TokenInInfo;
61
69
  tokenOut: TokenOutInfo;
62
- originChainId: number;
63
- originTxHash: Hex;
70
+ sourceChainId: number;
71
+ sourceTxHash: Hex;
64
72
  destinationChainId: number;
65
73
  destinationTxHash: Hex;
66
74
  solver: string | null;
@@ -77,6 +85,7 @@ interface DecodedYodlEvent {
77
85
  token: Address;
78
86
  amount: bigint;
79
87
  memo: string;
88
+ logIndex: number;
80
89
  }
81
90
  /**
82
91
  * Decode Yodl payment event from transaction logs.
@@ -91,7 +100,7 @@ declare class NoYodlEventError extends Error {
91
100
  constructor(message?: string);
92
101
  }
93
102
 
94
- declare function decodePayment(hash: Hex, chainId: number, clients: ChainClients): Promise<PaymentInfo>;
103
+ declare function decodePayment(hash: Hex, chainId: number, clients: ChainClients, cachedReceipt?: TransactionReceipt): Promise<PaymentInfo>;
95
104
 
96
105
  /**
97
106
  * Decode a bridge transaction given any hash (source or destination).
@@ -105,6 +114,6 @@ declare function decodeBridgePayment(hash: Hex, clients: ChainClients): Promise<
105
114
  type: "bridge";
106
115
  }>>;
107
116
 
108
- declare function decodeYodlPayment(txHash: Hex, chainId: number, clients: ChainClients): Promise<YodlPayment>;
117
+ declare function decodeYodlPayment(txHash: Hex, chainId: number, clients: ChainClients, cachedReceipt?: TransactionReceipt): Promise<YodlPayment>;
109
118
 
110
- export { type BridgeInfo, type ChainClients, type DecodedYodlEvent, NoBridgeFoundError, NoYodlEventError, type PaymentEvent, type PaymentInfo, type ServiceProvider, type SwapInfo, type TokenInInfo, type TokenOutInfo, type Webhook, type YodlPayment, createClients, decodeBridgePayment, decodePayment, decodeYodlFromLogs, decodeYodlPayment };
119
+ export { type BridgeInfo, type ChainClients, type DecodedYodlEvent, NoBridgeFoundError, NoYodlEventError, type PaymentEvent, type PaymentInfo, type ServiceProvider, type SwapInfo, type TokenInInfo, type TokenOutInfo, type Webhook, type YodlPayment, createClients, decodeBridgePayment, decodePayment, decodeYodlFromLogs, decodeYodlPayment, detectChain };
package/dist/index.js CHANGED
@@ -1,12 +1,19 @@
1
1
  // src/clients.ts
2
2
  import { chains } from "@yodlpay/tokenlists";
3
- import { createPublicClient, http } from "viem";
3
+ import {
4
+ createPublicClient,
5
+ http
6
+ } from "viem";
7
+ var rpcOverrides = {
8
+ 137: "https://polygon-bor-rpc.publicnode.com"
9
+ // polygon-rpc.com is down
10
+ };
4
11
  function createClients() {
5
12
  const clients = {};
6
13
  for (const chain of chains) {
7
14
  clients[chain.id] = createPublicClient({
8
15
  chain,
9
- transport: http()
16
+ transport: http(rpcOverrides[chain.id])
10
17
  });
11
18
  }
12
19
  return clients;
@@ -16,6 +23,25 @@ function getClient(clients, chainId) {
16
23
  if (!client) throw new Error(`No client configured for chain ${chainId}`);
17
24
  return client;
18
25
  }
26
+ async function detectChain(hash, clients) {
27
+ const entries = Object.entries(clients).map(([id, client]) => ({
28
+ chainId: Number(id),
29
+ client
30
+ }));
31
+ const results = await Promise.allSettled(
32
+ entries.map(async ({ chainId, client }) => {
33
+ const receipt = await client.getTransactionReceipt({ hash });
34
+ return { chainId, receipt };
35
+ })
36
+ );
37
+ const found = results.find(
38
+ (r) => r.status === "fulfilled"
39
+ );
40
+ if (!found) {
41
+ throw new Error(`Transaction ${hash} not found on any configured chain`);
42
+ }
43
+ return found.value;
44
+ }
19
45
 
20
46
  // src/decode-utils.ts
21
47
  import {
@@ -146,14 +172,15 @@ function decodeYodlFromLogs(logs, routerAddress) {
146
172
  address: routerAddress,
147
173
  context: "Yodl event"
148
174
  },
149
- (decoded) => {
175
+ (decoded, log) => {
150
176
  const args = decoded.args;
151
177
  return {
152
178
  sender: args.sender,
153
179
  receiver: args.receiver,
154
180
  token: args.token,
155
181
  amount: args.amount,
156
- memo: decodeMemo(args.memo)
182
+ memo: decodeMemo(args.memo),
183
+ logIndex: log.logIndex ?? 0
157
184
  };
158
185
  }
159
186
  );
@@ -220,11 +247,13 @@ import { chains as chains2 } from "@yodlpay/tokenlists";
220
247
  import { isAddress, isHex } from "viem";
221
248
  import { z } from "zod";
222
249
  var validChainIds = chains2.map((c) => c.id);
223
- var ArgsSchema = z.tuple([
224
- z.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash").transform((val) => val),
225
- z.coerce.number().int().refine((id) => validChainIds.includes(id), {
226
- message: `Chain ID must be one of: ${validChainIds.join(", ")}`
227
- })
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), {
252
+ message: `Chain ID must be one of: ${validChainIds.join(", ")}`
253
+ });
254
+ var ArgsSchema = z.union([
255
+ z.tuple([txHashSchema, chainIdSchema]),
256
+ z.tuple([txHashSchema])
228
257
  ]);
229
258
  var WebhooksSchema = z.array(
230
259
  z.object({
@@ -374,8 +403,8 @@ function parseRelayResponse(request) {
374
403
  throw new NoBridgeFoundError();
375
404
  }
376
405
  return {
377
- originChainId: inTx.chainId,
378
- originTxHash: inTx.hash,
406
+ sourceChainId: inTx.chainId,
407
+ sourceTxHash: inTx.hash,
379
408
  destinationChainId: outTx.chainId,
380
409
  destinationTxHash: outTx.hash,
381
410
  inputAmountGross: BigInt(data?.metadata?.currencyIn?.amount ?? 0)
@@ -384,18 +413,18 @@ function parseRelayResponse(request) {
384
413
  async function decodeBridgePayment(hash, clients) {
385
414
  const request = await fetchRelayRequest(hash);
386
415
  const {
387
- originChainId,
388
- originTxHash,
416
+ sourceChainId,
417
+ sourceTxHash,
389
418
  destinationChainId,
390
419
  destinationTxHash,
391
420
  inputAmountGross
392
421
  } = parseRelayResponse(request);
393
422
  const destProvider = getClient(clients, destinationChainId);
394
- const sourceProvider = getClient(clients, originChainId);
423
+ const sourceProvider = getClient(clients, sourceChainId);
395
424
  const { address: routerAddress } = getRouter(destinationChainId);
396
425
  const [destReceipt, sourceReceipt] = await Promise.all([
397
426
  destProvider.getTransactionReceipt({ hash: destinationTxHash }),
398
- sourceProvider.getTransactionReceipt({ hash: originTxHash })
427
+ sourceProvider.getTransactionReceipt({ hash: sourceTxHash })
399
428
  ]);
400
429
  const yodlEvent = decodeYodlFromLogs(destReceipt.logs, routerAddress);
401
430
  if (!yodlEvent) {
@@ -406,7 +435,7 @@ async function decodeBridgePayment(hash, clients) {
406
435
  const [destBlock, destTx, sender] = await Promise.all([
407
436
  destProvider.getBlock({ blockNumber: destReceipt.blockNumber }),
408
437
  destProvider.getTransaction({ hash: destinationTxHash }),
409
- extractSenderFromSource(sourceReceipt, sourceProvider, originTxHash)
438
+ extractSenderFromSource(sourceReceipt, sourceProvider, sourceTxHash)
410
439
  ]);
411
440
  const tokens = resolveBridgeTokens(
412
441
  sourceReceipt,
@@ -414,12 +443,12 @@ async function decodeBridgePayment(hash, clients) {
414
443
  yodlEvent,
415
444
  sender,
416
445
  inputAmountGross,
417
- originChainId,
446
+ sourceChainId,
418
447
  destinationChainId
419
448
  );
420
449
  const bridge = {
421
- originChainId,
422
- originTxHash,
450
+ sourceChainId,
451
+ sourceTxHash,
423
452
  destinationChainId,
424
453
  destinationTxHash,
425
454
  ...tokens,
@@ -447,16 +476,17 @@ async function tryDecodeBridge(hash, clients) {
447
476
  return void 0;
448
477
  }
449
478
  }
450
- async function decodePayment(hash, chainId, clients) {
479
+ async function decodePayment(hash, chainId, clients, cachedReceipt) {
451
480
  const provider = getClient(clients, chainId);
452
- const receipt = await provider.getTransactionReceipt({ hash });
481
+ const receipt = cachedReceipt ?? await provider.getTransactionReceipt({ hash });
453
482
  const { address: routerAddress } = getRouter2(chainId);
454
483
  const [block, tx] = await Promise.all([
455
484
  provider.getBlock({ blockNumber: receipt.blockNumber }),
456
485
  provider.getTransaction({ hash })
457
486
  ]);
458
487
  const yodlEvent = decodeYodlFromLogs(receipt.logs, routerAddress);
459
- const swapEvent = decodeSwapFromLogs(receipt.logs);
488
+ const swapLogs = yodlEvent ? receipt.logs.filter((l) => (l.logIndex ?? 0) < yodlEvent.logIndex) : receipt.logs;
489
+ const swapEvent = decodeSwapFromLogs(swapLogs);
460
490
  if (yodlEvent) {
461
491
  const blockTimestamp = toBlockTimestamp(block);
462
492
  const webhooks = extractEmbeddedParams(tx.input);
@@ -475,7 +505,10 @@ async function decodePayment(hash, chainId, clients) {
475
505
 
476
506
  // src/yodl-payment.ts
477
507
  import { getTokenByAddress as getTokenByAddress2 } from "@yodlpay/tokenlists";
478
- import { formatUnits, zeroAddress } from "viem";
508
+ import {
509
+ formatUnits,
510
+ zeroAddress
511
+ } from "viem";
479
512
  function buildTokenInfo(params) {
480
513
  const inToken = getTokenByAddress2(params.tokenIn, params.inChainId);
481
514
  const outToken = getTokenByAddress2(params.tokenOut, params.outChainId);
@@ -513,8 +546,8 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
513
546
  inChainId: chainId,
514
547
  outChainId: chainId
515
548
  }),
516
- originChainId: chainId,
517
- originTxHash: txHash,
549
+ sourceChainId: chainId,
550
+ sourceTxHash: txHash,
518
551
  destinationChainId: chainId,
519
552
  destinationTxHash: txHash,
520
553
  solver: null
@@ -531,8 +564,8 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
531
564
  inChainId: chainId,
532
565
  outChainId: chainId
533
566
  }),
534
- originChainId: chainId,
535
- originTxHash: txHash,
567
+ sourceChainId: chainId,
568
+ sourceTxHash: txHash,
536
569
  destinationChainId: chainId,
537
570
  destinationTxHash: txHash,
538
571
  solver: paymentInfo.service ?? null
@@ -546,11 +579,11 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
546
579
  tokenOut: paymentInfo.tokenOut,
547
580
  tokenOutAmountGross: paymentInfo.tokenOutAmountGross,
548
581
  tokenOutAmountNet: paymentInfo.amount,
549
- inChainId: paymentInfo.originChainId,
582
+ inChainId: paymentInfo.sourceChainId,
550
583
  outChainId: paymentInfo.destinationChainId
551
584
  }),
552
- originChainId: paymentInfo.originChainId,
553
- originTxHash: paymentInfo.originTxHash,
585
+ sourceChainId: paymentInfo.sourceChainId,
586
+ sourceTxHash: paymentInfo.sourceTxHash,
554
587
  destinationChainId: paymentInfo.destinationChainId,
555
588
  destinationTxHash: paymentInfo.destinationTxHash,
556
589
  solver: paymentInfo.service ?? null
@@ -558,8 +591,13 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
558
591
  }
559
592
  }
560
593
  }
561
- async function decodeYodlPayment(txHash, chainId, clients) {
562
- const paymentInfo = await decodePayment(txHash, chainId, clients);
594
+ async function decodeYodlPayment(txHash, chainId, clients, cachedReceipt) {
595
+ const paymentInfo = await decodePayment(
596
+ txHash,
597
+ chainId,
598
+ clients,
599
+ cachedReceipt
600
+ );
563
601
  const firstWebhook = paymentInfo.webhooks[0];
564
602
  const processorAddress = firstWebhook?.webhookAddress ?? zeroAddress;
565
603
  const processorMemo = firstWebhook?.memo ?? "";
@@ -582,5 +620,6 @@ export {
582
620
  decodeBridgePayment,
583
621
  decodePayment,
584
622
  decodeYodlFromLogs,
585
- decodeYodlPayment
623
+ decodeYodlPayment,
624
+ detectChain
586
625
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yodlpay/payment-decoder",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Decode Yodl payment hashes into structured payment data",
5
5
  "keywords": [
6
6
  "yodl",