@yodlpay/payment-decoder 1.3.4 → 1.3.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.d.ts CHANGED
@@ -84,6 +84,8 @@ interface DecodeBridgeOptions {
84
84
  includeAcross?: boolean;
85
85
  /** Receipt of the fill (destination) tx, used for Across fill-side lookups. */
86
86
  fillReceipt?: TransactionReceipt;
87
+ /** Receipt of the source tx, used to skip re-fetching when hash is the source. */
88
+ sourceReceipt?: TransactionReceipt;
87
89
  }
88
90
  declare function decodeBridgePayment(hash: Hex, clients: ChainClients, options?: DecodeBridgeOptions): Promise<Extract<PaymentInfo, {
89
91
  type: "bridge";
@@ -107,6 +109,10 @@ interface DecodedYodlEvent {
107
109
  */
108
110
  declare function decodeYodlFromLogs(logs: readonly Log[], routerAddress: Address): DecodedYodlEvent | undefined;
109
111
 
112
+ declare function decodePayment(hash: Hex, chainId: number, clients: ChainClients, cachedReceipt?: TransactionReceipt): Promise<PaymentInfo>;
113
+
114
+ declare function decodeYodlPayment(txHash: Hex, chainId: number, clients: ChainClients, cachedReceipt?: TransactionReceipt): Promise<YodlPayment>;
115
+
110
116
  declare class NoBridgeFoundError extends Error {
111
117
  constructor(message?: string);
112
118
  }
@@ -114,8 +120,4 @@ declare class NoYodlEventError extends Error {
114
120
  constructor(message?: string);
115
121
  }
116
122
 
117
- declare function decodePayment(hash: Hex, chainId: number, clients: ChainClients, cachedReceipt?: TransactionReceipt): Promise<PaymentInfo>;
118
-
119
- declare function decodeYodlPayment(txHash: Hex, chainId: number, clients: ChainClients, cachedReceipt?: TransactionReceipt): Promise<YodlPayment>;
120
-
121
123
  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,18 +1,4 @@
1
- // src/across-bridge.ts
2
- import { parseDepositLogs, parseFillLogs } from "@across-protocol/app-sdk";
3
- import { getRouter as getRouter2 } from "@yodlpay/tokenlists";
4
-
5
- // src/across-client.ts
6
- import { isHash } from "viem";
7
- import { z } from "zod";
8
-
9
1
  // src/errors.ts
10
- import {
11
- AbiDecodingDataSizeTooSmallError,
12
- AbiEventSignatureEmptyTopicsError,
13
- AbiEventSignatureNotFoundError,
14
- DecodeLogTopicsMismatch
15
- } from "viem";
16
2
  var ExpectedDecodeError = class extends Error {
17
3
  constructor(message = "Expected decode error") {
18
4
  super(message);
@@ -31,141 +17,37 @@ var NoYodlEventError = class extends Error {
31
17
  this.name = "NoYodlEventError";
32
18
  }
33
19
  };
34
- function isExpectedDecodeError(error) {
35
- return error instanceof AbiEventSignatureNotFoundError || error instanceof AbiEventSignatureEmptyTopicsError || error instanceof AbiDecodingDataSizeTooSmallError || error instanceof DecodeLogTopicsMismatch;
36
- }
37
20
 
38
- // src/across-client.ts
39
- var ACROSS_API = "https://app.across.to/api/deposit/status";
40
- var ACROSS_TIMEOUT_MS = 8e3;
41
- var chainId = z.union([
42
- z.number().int().positive(),
43
- z.string().regex(/^\d+$/).transform(Number)
44
- ]).pipe(z.number().int().positive());
45
- var txHash = z.string().refine((v) => isHash(v));
46
- var nullableTxHash = txHash.nullish().transform((v) => v ?? null);
47
- var depositId = z.union([
48
- z.string().regex(/^\d+$/),
49
- z.number().int().nonnegative().transform((v) => String(v))
50
- ]);
51
- var AcrossDepositStatusSchema = z.object({
52
- status: z.enum(["filled", "pending", "expired", "refunded"]),
53
- originChainId: chainId,
54
- destinationChainId: chainId,
55
- depositId,
56
- depositTxnRef: txHash.optional(),
57
- depositTxHash: txHash.optional(),
58
- fillTxnRef: nullableTxHash,
59
- fillTx: nullableTxHash,
60
- actionsSucceeded: z.boolean().nullish().transform((v) => v ?? null)
61
- }).transform((d) => {
62
- const depositTxnRef = d.depositTxnRef ?? d.depositTxHash;
63
- if (!depositTxnRef) {
64
- throw new Error("Missing depositTxnRef and depositTxHash");
65
- }
66
- return {
67
- status: d.status,
68
- originChainId: d.originChainId,
69
- destinationChainId: d.destinationChainId,
70
- depositId: d.depositId,
71
- depositTxnRef,
72
- fillTxnRef: d.fillTxnRef ?? d.fillTx,
73
- actionsSucceeded: d.actionsSucceeded
74
- };
75
- });
76
- async function fetchAcrossDepositByDepositId(originChainId, depositId2) {
77
- const url = new URL(ACROSS_API);
78
- url.searchParams.set("originChainId", String(originChainId));
79
- url.searchParams.set("depositId", depositId2);
80
- return fetchAcrossDeposit(url);
81
- }
82
- async function fetchAcrossDeposit(url) {
83
- const controller = new AbortController();
84
- const timeout = setTimeout(() => controller.abort(), ACROSS_TIMEOUT_MS);
85
- let response;
86
- try {
87
- response = await fetch(url, { signal: controller.signal });
88
- } catch (error) {
89
- const message = error instanceof Error && error.name === "AbortError" ? `Across API request timed out after ${ACROSS_TIMEOUT_MS}ms` : "Across API request failed";
90
- throw new Error(message, { cause: error });
91
- } finally {
92
- clearTimeout(timeout);
93
- }
94
- if (response.status === 404) throw new NoBridgeFoundError();
95
- if (!response.ok) {
96
- const body = await response.text().catch(() => "");
97
- const kind = response.status === 429 || response.status >= 500 ? "temporary error" : "error";
98
- throw new Error(
99
- `Across API ${kind} (${response.status})${body ? ` - ${body}` : ""}`
100
- );
101
- }
102
- const data = await response.json().catch(() => {
103
- throw new Error("Across API returned invalid JSON");
104
- });
105
- const result = AcrossDepositStatusSchema.safeParse(data);
106
- if (!result.success) {
107
- throw new Error(
108
- `Across API response validation failed: ${result.error.message}`
109
- );
110
- }
111
- return result.data;
112
- }
113
- async function fetchAcrossDepositByTx(hash) {
114
- const url = new URL(ACROSS_API);
115
- url.searchParams.set("depositTxnRef", hash);
116
- return fetchAcrossDeposit(url);
117
- }
21
+ // src/bridges/across-bridge.ts
22
+ import { parseDepositLogs, parseFillLogs } from "@across-protocol/app-sdk";
23
+ import { getRouter as getRouter2 } from "@yodlpay/tokenlists";
118
24
 
119
- // src/decode-utils.ts
25
+ // src/core/decode-utils.ts
120
26
  import { tokenlist } from "@yodlpay/tokenlists";
121
27
  import {
122
- decodeEventLog,
123
28
  erc20Abi as erc20Abi2,
124
29
  getAddress,
125
- isAddressEqual
30
+ isAddressEqual,
31
+ parseEventLogs
126
32
  } from "viem";
127
33
 
128
34
  // src/abi.ts
129
35
  import { getRouterAbi } from "@yodlpay/tokenlists";
36
+ import { parseAbi } from "viem";
130
37
  var yodlAbi = getRouterAbi("0.8");
131
- var relaySwapAbi = [
132
- {
133
- type: "event",
134
- name: "Swap",
135
- inputs: [
136
- { name: "sender", type: "address", indexed: true },
137
- { name: "recipient", type: "address", indexed: true },
138
- { name: "inputToken", type: "address", indexed: false },
139
- { name: "outputToken", type: "address", indexed: false },
140
- { name: "inputAmount", type: "uint256", indexed: false },
141
- { name: "outputAmount", type: "uint256", indexed: false }
142
- ]
143
- }
144
- ];
145
- var entryPointAbi = [
146
- {
147
- type: "event",
148
- name: "UserOperationEvent",
149
- inputs: [
150
- { name: "userOpHash", type: "bytes32", indexed: true },
151
- { name: "sender", type: "address", indexed: true },
152
- { name: "paymaster", type: "address", indexed: true },
153
- { name: "nonce", type: "uint256", indexed: false },
154
- { name: "success", type: "bool", indexed: false },
155
- { name: "actualGasCost", type: "uint256", indexed: false },
156
- { name: "actualGasUsed", type: "uint256", indexed: false }
157
- ]
158
- }
159
- ];
38
+ var relaySwapAbi = parseAbi([
39
+ "event Swap(address indexed sender, address indexed recipient, address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount)"
40
+ ]);
160
41
 
161
42
  // src/utils.ts
162
43
  import { getTokenByAddress } from "@yodlpay/tokenlists";
163
44
  import {
164
45
  erc20Abi,
165
46
  getContract,
166
- hexToString
47
+ hexToString,
48
+ stringify
167
49
  } from "viem";
168
- import { z as z2 } from "zod";
50
+ import { z } from "zod";
169
51
  function decodeMemo(memo) {
170
52
  if (!memo || memo === "0x") return "";
171
53
  try {
@@ -174,8 +56,8 @@ function decodeMemo(memo) {
174
56
  return "";
175
57
  }
176
58
  }
177
- var erc20String = z2.string().max(64).transform((s) => s.replace(/\p{Cc}/gu, ""));
178
- var erc20Decimals = z2.number().int().nonnegative().max(36);
59
+ var erc20String = z.string().max(64).transform((s) => s.replace(/\p{Cc}/gu, ""));
60
+ var erc20Decimals = z.number().int().nonnegative().max(36);
179
61
  async function resolveToken(address, chainId2, client) {
180
62
  try {
181
63
  return getTokenByAddress(address, chainId2);
@@ -196,98 +78,56 @@ async function resolveToken(address, chainId2, client) {
196
78
  }
197
79
  }
198
80
 
199
- // src/decode-utils.ts
200
- function* matchingEvents(logs, options) {
201
- const { abi, eventName, address, context } = options;
202
- for (const log of logs) {
203
- if (address && !isAddressEqual(log.address, address)) {
204
- continue;
205
- }
206
- try {
207
- const decoded = decodeEventLog({
208
- abi,
209
- data: log.data,
210
- topics: log.topics
211
- });
212
- if (decoded.eventName === eventName) {
213
- yield { decoded, log };
214
- }
215
- } catch (error) {
216
- if (!isExpectedDecodeError(error)) {
217
- const contextStr = context ? ` (${context})` : "";
218
- console.warn(
219
- `[payment-decoder] Unexpected error decoding ${eventName}${contextStr}:`,
220
- error
221
- );
222
- }
223
- }
224
- }
225
- }
226
- function findEventInLogs(logs, options, transform) {
227
- for (const { decoded, log } of matchingEvents(logs, options)) {
228
- return transform(decoded, log);
229
- }
230
- return void 0;
231
- }
232
- function collectEventsFromLogs(logs, options, transform) {
233
- const results = [];
234
- for (const { decoded, log } of matchingEvents(logs, options)) {
235
- results.push(transform(decoded, log));
236
- }
237
- return results;
238
- }
81
+ // src/core/decode-utils.ts
239
82
  function decodeYodlFromLogs(logs, routerAddress) {
240
- return findEventInLogs(
241
- logs,
242
- {
243
- abi: yodlAbi,
244
- eventName: "Yodl",
245
- address: routerAddress,
246
- context: "Yodl event"
247
- },
248
- (decoded, log) => {
249
- const args = decoded.args;
250
- return {
251
- sender: args.sender,
252
- receiver: args.receiver,
253
- token: args.token,
254
- amount: args.amount,
255
- memo: decodeMemo(args.memo),
256
- logIndex: log.logIndex ?? 0
257
- };
258
- }
259
- );
83
+ const parsed = parseEventLogs({
84
+ abi: yodlAbi,
85
+ logs: logs.filter((l) => isAddressEqual(l.address, routerAddress)),
86
+ eventName: "Yodl",
87
+ strict: true
88
+ });
89
+ const first = parsed[0];
90
+ if (!first) return void 0;
91
+ const args = first.args;
92
+ return {
93
+ sender: args.sender,
94
+ receiver: args.receiver,
95
+ token: args.token,
96
+ amount: args.amount,
97
+ memo: decodeMemo(args.memo),
98
+ logIndex: first.logIndex ?? 0
99
+ };
260
100
  }
261
101
  function decodeSwapFromLogs(logs) {
262
- return findEventInLogs(
263
- logs,
264
- { abi: relaySwapAbi, eventName: "Swap", context: "Relay Swap" },
265
- (decoded) => {
266
- const args = decoded.args;
267
- return {
268
- tokenIn: args.inputToken,
269
- tokenOut: args.outputToken,
270
- tokenInAmount: args.inputAmount,
271
- tokenOutAmount: args.outputAmount,
272
- service: "relay"
273
- };
274
- }
275
- );
102
+ const parsed = parseEventLogs({
103
+ abi: relaySwapAbi,
104
+ logs: [...logs],
105
+ eventName: "Swap",
106
+ strict: true
107
+ });
108
+ const first = parsed[0];
109
+ if (!first) return void 0;
110
+ return {
111
+ tokenIn: first.args.inputToken,
112
+ tokenOut: first.args.outputToken,
113
+ tokenInAmount: first.args.inputAmount,
114
+ tokenOutAmount: first.args.outputAmount,
115
+ service: "relay"
116
+ };
276
117
  }
277
118
  function extractTokenTransfers(logs) {
278
- return collectEventsFromLogs(
279
- logs,
280
- { abi: erc20Abi2, eventName: "Transfer", context: "ERC20 Transfer" },
281
- (decoded, log) => {
282
- const args = decoded.args;
283
- return {
284
- token: log.address,
285
- from: args.from,
286
- to: args.to,
287
- amount: args.value
288
- };
289
- }
290
- );
119
+ const parsed = parseEventLogs({
120
+ abi: erc20Abi2,
121
+ logs: [...logs],
122
+ eventName: "Transfer",
123
+ strict: false
124
+ });
125
+ return parsed.flatMap((log) => {
126
+ if (!log.args) return [];
127
+ const { from, to, value } = log.args;
128
+ if (!from || !to || value === void 0) return [];
129
+ return [{ token: log.address, from, to, amount: value }];
130
+ });
291
131
  }
292
132
  function isKnownToken(address) {
293
133
  return tokenlist.some((t) => isAddressEqual(t.address, address));
@@ -418,12 +258,12 @@ function buildPaymentEvent(yodlEvent, webhooks, blockTimestamp, senderOverride)
418
258
  };
419
259
  }
420
260
 
421
- // src/embedded-params.ts
261
+ // src/core/embedded-params.ts
422
262
  import { decodeFunctionData, toFunctionSelector } from "viem";
423
263
 
424
264
  // src/validation.ts
425
- import { isAddress, isHex } from "viem";
426
- import { z as z3 } from "zod";
265
+ import { isAddress, isHash, isHex } from "viem";
266
+ import { z as z2 } from "zod";
427
267
 
428
268
  // src/chains.ts
429
269
  import { chains as allChains } from "@yodlpay/tokenlists";
@@ -431,25 +271,25 @@ var chains = allChains.filter((c) => !c.testnet);
431
271
 
432
272
  // src/validation.ts
433
273
  var validChainIds = chains.map((c) => c.id);
434
- var txHashSchema = z3.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash").transform((val) => val);
435
- var chainIdSchema = z3.coerce.number().int().refine((id) => validChainIds.includes(id), {
274
+ var txHashSchema = z2.string().refine((val) => isHash(val), "Invalid transaction hash");
275
+ var chainIdSchema = z2.coerce.number().int().refine((id) => validChainIds.includes(id), {
436
276
  message: `Chain ID must be one of: ${validChainIds.join(", ")}`
437
277
  });
438
- var ArgsSchema = z3.union([
439
- z3.tuple([txHashSchema, chainIdSchema]),
440
- z3.tuple([txHashSchema])
278
+ var ArgsSchema = z2.union([
279
+ z2.tuple([txHashSchema, chainIdSchema]),
280
+ z2.tuple([txHashSchema])
441
281
  ]);
442
- var WebhooksSchema = z3.array(
443
- z3.object({
444
- webhookAddress: z3.string().refine((val) => isAddress(val)),
445
- payload: z3.array(z3.string().refine((val) => isHex(val)))
282
+ var WebhooksSchema = z2.array(
283
+ z2.object({
284
+ webhookAddress: z2.string().refine((val) => isAddress(val)),
285
+ payload: z2.array(z2.string().refine((val) => isHex(val)))
446
286
  }).transform((webhook) => ({
447
287
  ...webhook,
448
288
  memo: webhook.payload[0] ? decodeMemo(webhook.payload[0]) : ""
449
289
  }))
450
290
  ).catch([]);
451
291
 
452
- // src/embedded-params.ts
292
+ // src/core/embedded-params.ts
453
293
  function getYodlSelector() {
454
294
  const yodlFunction = yodlAbi.find(
455
295
  (i) => i.type === "function" && i.name === "yodlWithToken"
@@ -479,13 +319,96 @@ function extractEmbeddedParams(data) {
479
319
  return [];
480
320
  }
481
321
 
482
- // src/relay-bridge.ts
322
+ // src/bridges/across-client.ts
323
+ import { isHash as isHash2 } from "viem";
324
+ import { z as z3 } from "zod";
325
+ var ACROSS_API = "https://app.across.to/api/deposit/status";
326
+ var ACROSS_TIMEOUT_MS = 8e3;
327
+ var chainId = z3.union([
328
+ z3.number().int().positive(),
329
+ z3.string().regex(/^\d+$/).transform(Number)
330
+ ]).pipe(z3.number().int().positive());
331
+ var txHash = z3.string().refine((v) => isHash2(v));
332
+ var nullableTxHash = txHash.nullish().transform((v) => v ?? null);
333
+ var depositId = z3.union([
334
+ z3.string().regex(/^\d+$/),
335
+ z3.number().int().nonnegative().transform((v) => String(v))
336
+ ]);
337
+ var AcrossDepositStatusSchema = z3.object({
338
+ status: z3.enum(["filled", "pending", "expired", "refunded"]),
339
+ originChainId: chainId,
340
+ destinationChainId: chainId,
341
+ depositId,
342
+ depositTxnRef: txHash.optional(),
343
+ depositTxHash: txHash.optional(),
344
+ fillTxnRef: nullableTxHash,
345
+ fillTx: nullableTxHash,
346
+ actionsSucceeded: z3.boolean().nullish().transform((v) => v ?? null)
347
+ }).transform((d) => {
348
+ const depositTxnRef = d.depositTxnRef ?? d.depositTxHash;
349
+ if (!depositTxnRef) {
350
+ throw new Error("Missing depositTxnRef and depositTxHash");
351
+ }
352
+ return {
353
+ status: d.status,
354
+ originChainId: d.originChainId,
355
+ destinationChainId: d.destinationChainId,
356
+ depositId: d.depositId,
357
+ depositTxnRef,
358
+ fillTxnRef: d.fillTxnRef ?? d.fillTx,
359
+ actionsSucceeded: d.actionsSucceeded
360
+ };
361
+ });
362
+ async function fetchAcrossDepositByDepositId(originChainId, depositId2) {
363
+ const url = new URL(ACROSS_API);
364
+ url.searchParams.set("originChainId", String(originChainId));
365
+ url.searchParams.set("depositId", depositId2);
366
+ return fetchAcrossDeposit(url);
367
+ }
368
+ async function fetchAcrossDeposit(url) {
369
+ const controller = new AbortController();
370
+ const timeout = setTimeout(() => controller.abort(), ACROSS_TIMEOUT_MS);
371
+ let response;
372
+ try {
373
+ response = await fetch(url, { signal: controller.signal });
374
+ } catch (error) {
375
+ const message = error instanceof Error && error.name === "AbortError" ? `Across API request timed out after ${ACROSS_TIMEOUT_MS}ms` : "Across API request failed";
376
+ throw new Error(message, { cause: error });
377
+ } finally {
378
+ clearTimeout(timeout);
379
+ }
380
+ if (response.status === 404) throw new NoBridgeFoundError();
381
+ if (!response.ok) {
382
+ const body = await response.text().catch(() => "");
383
+ const kind = response.status === 429 || response.status >= 500 ? "temporary error" : "error";
384
+ throw new Error(
385
+ `Across API ${kind} (${response.status})${body ? ` - ${body}` : ""}`
386
+ );
387
+ }
388
+ const data = await response.json().catch(() => {
389
+ throw new Error("Across API returned invalid JSON");
390
+ });
391
+ const result = AcrossDepositStatusSchema.safeParse(data);
392
+ if (!result.success) {
393
+ throw new Error(
394
+ `Across API response validation failed: ${result.error.message}`
395
+ );
396
+ }
397
+ return result.data;
398
+ }
399
+ async function fetchAcrossDepositByTx(hash) {
400
+ const url = new URL(ACROSS_API);
401
+ url.searchParams.set("depositTxnRef", hash);
402
+ return fetchAcrossDeposit(url);
403
+ }
404
+
405
+ // src/bridges/relay-bridge.ts
483
406
  import { getRouter } from "@yodlpay/tokenlists";
484
407
  import {
485
- decodeEventLog as decodeEventLog2,
486
- isAddressEqual as isAddressEqual2
408
+ isAddressEqual as isAddressEqual2,
409
+ parseEventLogs as parseEventLogs2
487
410
  } from "viem";
488
- import { entryPoint08Address } from "viem/account-abstraction";
411
+ import { entryPoint08Abi, entryPoint08Address } from "viem/account-abstraction";
489
412
 
490
413
  // src/clients.ts
491
414
  import {
@@ -503,7 +426,7 @@ function createClients() {
503
426
  for (const chain of chains) {
504
427
  clients[chain.id] = createPublicClient({
505
428
  chain,
506
- transport: http(rpcOverrides[chain.id])
429
+ transport: http(rpcOverrides[chain.id], { batch: true })
507
430
  });
508
431
  }
509
432
  return clients;
@@ -533,7 +456,7 @@ async function detectChain(hash, clients) {
533
456
  return found.value;
534
457
  }
535
458
 
536
- // src/relay-client.ts
459
+ // src/bridges/relay-client.ts
537
460
  var RELAY_API = "https://api.relay.link";
538
461
  async function getRelayRequests(params) {
539
462
  const url = new URL(`${RELAY_API}/requests/v2`);
@@ -557,7 +480,7 @@ async function fetchRelayRequest(hash) {
557
480
  return request;
558
481
  }
559
482
 
560
- // src/relay-bridge.ts
483
+ // src/bridges/relay-bridge.ts
561
484
  async function calculateOutputAmountGross(inputAmount, inputToken, inputChainId, outputToken, outputChainId, clients) {
562
485
  const [{ decimals: inputDecimals }, { decimals: outputDecimals }] = await Promise.all([
563
486
  resolveToken(inputToken, inputChainId, getClient(clients, inputChainId)),
@@ -573,19 +496,15 @@ async function calculateOutputAmountGross(inputAmount, inputToken, inputChainId,
573
496
  return inputAmount * 10n ** BigInt(-decimalDiff);
574
497
  }
575
498
  async function extractSenderFromSource(sourceReceipt, sourceProvider, sourceHash) {
576
- for (const log of sourceReceipt.logs) {
577
- if (!isAddressEqual2(log.address, entryPoint08Address)) continue;
578
- try {
579
- const decoded = decodeEventLog2({
580
- abi: entryPointAbi,
581
- data: log.data,
582
- topics: log.topics
583
- });
584
- if (decoded.eventName === "UserOperationEvent") {
585
- return decoded.args.sender;
586
- }
587
- } catch {
588
- }
499
+ const userOps = parseEventLogs2({
500
+ abi: entryPoint08Abi,
501
+ logs: sourceReceipt.logs.filter(
502
+ (l) => isAddressEqual2(l.address, entryPoint08Address)
503
+ ),
504
+ eventName: "UserOperationEvent"
505
+ });
506
+ if (userOps[0]) {
507
+ return userOps[0].args.sender;
589
508
  }
590
509
  const sourceTx = await sourceProvider.getTransaction({ hash: sourceHash });
591
510
  return sourceTx.from;
@@ -639,7 +558,7 @@ function parseRelayResponse(request) {
639
558
  inputAmountGross: BigInt(data?.metadata?.currencyIn?.amount ?? 0)
640
559
  };
641
560
  }
642
- async function decodeRelayBridgePayment(hash, clients) {
561
+ async function decodeRelayBridgePayment(hash, clients, options = {}) {
643
562
  const request = await fetchRelayRequest(hash);
644
563
  const {
645
564
  sourceChainId,
@@ -654,9 +573,12 @@ async function decodeRelayBridgePayment(hash, clients) {
654
573
  throw new NoBridgeFoundError();
655
574
  }
656
575
  const { address: routerAddress } = getRouter(destinationChainId);
576
+ const hashLower = hash.toLowerCase();
577
+ const cachedDestReceipt = options.fillReceipt && destinationTxHash.toLowerCase() === hashLower ? options.fillReceipt : void 0;
578
+ const cachedSourceReceipt = options.sourceReceipt && sourceTxHash.toLowerCase() === hashLower ? options.sourceReceipt : void 0;
657
579
  const [destReceipt, sourceReceipt] = await Promise.all([
658
- destProvider.getTransactionReceipt({ hash: destinationTxHash }),
659
- sourceProvider.getTransactionReceipt({ hash: sourceTxHash })
580
+ cachedDestReceipt ?? destProvider.getTransactionReceipt({ hash: destinationTxHash }),
581
+ cachedSourceReceipt ?? sourceProvider.getTransactionReceipt({ hash: sourceTxHash })
660
582
  ]);
661
583
  const yodlEvent = decodeYodlFromLogs(destReceipt.logs, routerAddress);
662
584
  if (!yodlEvent) {
@@ -696,7 +618,7 @@ async function decodeRelayBridgePayment(hash, clients) {
696
618
  return { type: "bridge", ...payment, ...bridge };
697
619
  }
698
620
 
699
- // src/across-bridge.ts
621
+ // src/bridges/across-bridge.ts
700
622
  async function resolveAcrossStatus(hash, fillReceipt) {
701
623
  try {
702
624
  return await fetchAcrossDepositByTx(hash);
@@ -713,13 +635,13 @@ async function resolveAcrossStatus(hash, fillReceipt) {
713
635
  Number(fillLog.originChainId),
714
636
  String(fillLog.depositId)
715
637
  );
716
- if (status.fillTxnRef !== hash) {
638
+ if (!status.fillTxnRef || status.fillTxnRef.toLowerCase() !== hash.toLowerCase()) {
717
639
  throw new NoBridgeFoundError();
718
640
  }
719
641
  return status;
720
642
  }
721
- async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
722
- const status = await resolveAcrossStatus(hash, fillReceipt);
643
+ async function decodeAcrossBridgePayment(hash, clients, options = {}) {
644
+ const status = await resolveAcrossStatus(hash, options.fillReceipt);
723
645
  if (status.status !== "filled" || !status.fillTxnRef) {
724
646
  throw new NoBridgeFoundError();
725
647
  }
@@ -736,9 +658,12 @@ async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
736
658
  throw new NoBridgeFoundError();
737
659
  }
738
660
  const { address: routerAddress } = getRouter2(destinationChainId);
661
+ const hashLower = hash.toLowerCase();
662
+ const cachedDestReceipt = options.fillReceipt && destinationTxHash.toLowerCase() === hashLower ? options.fillReceipt : void 0;
663
+ const cachedSourceReceipt = options.sourceReceipt && sourceTxHash.toLowerCase() === hashLower ? options.sourceReceipt : void 0;
739
664
  const [sourceReceipt, destReceipt] = await Promise.all([
740
- sourceProvider.getTransactionReceipt({ hash: sourceTxHash }),
741
- destProvider.getTransactionReceipt({ hash: destinationTxHash })
665
+ cachedSourceReceipt ?? sourceProvider.getTransactionReceipt({ hash: sourceTxHash }),
666
+ cachedDestReceipt ?? destProvider.getTransactionReceipt({ hash: destinationTxHash })
742
667
  ]);
743
668
  const yodlEvent = decodeYodlFromLogs(destReceipt.logs, routerAddress);
744
669
  if (!yodlEvent) {
@@ -805,12 +730,15 @@ async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
805
730
  return { type: "bridge", ...payment, ...bridge };
806
731
  }
807
732
 
808
- // src/bridge-payment.ts
733
+ // src/bridges/bridge-payment.ts
809
734
  async function decodeBridgePayment(hash, clients, options = {}) {
810
735
  const includeAcross = options.includeAcross ?? true;
811
736
  let relayError;
812
737
  try {
813
- return await decodeRelayBridgePayment(hash, clients);
738
+ return await decodeRelayBridgePayment(hash, clients, {
739
+ fillReceipt: options.fillReceipt,
740
+ sourceReceipt: options.sourceReceipt
741
+ });
814
742
  } catch (error) {
815
743
  relayError = error;
816
744
  if (!includeAcross) {
@@ -818,7 +746,10 @@ async function decodeBridgePayment(hash, clients, options = {}) {
818
746
  }
819
747
  }
820
748
  try {
821
- return await decodeAcrossBridgePayment(hash, clients, options.fillReceipt);
749
+ return await decodeAcrossBridgePayment(hash, clients, {
750
+ fillReceipt: options.fillReceipt,
751
+ sourceReceipt: options.sourceReceipt
752
+ });
822
753
  } catch (acrossError) {
823
754
  if (!(relayError instanceof NoBridgeFoundError) && !(acrossError instanceof NoBridgeFoundError)) {
824
755
  throw new Error("Both Relay and Across bridge decoders failed", {
@@ -835,7 +766,7 @@ async function decodeBridgePayment(hash, clients, options = {}) {
835
766
  }
836
767
  }
837
768
 
838
- // src/payment-decoder.ts
769
+ // src/core/payment-decoder.ts
839
770
  import { getRouter as getRouter3 } from "@yodlpay/tokenlists";
840
771
  async function tryDecodeBridge(hash, clients, options = {}) {
841
772
  try {
@@ -851,24 +782,35 @@ async function decodePayment(hash, chainId2, clients, cachedReceipt) {
851
782
  const provider = getClient(clients, chainId2);
852
783
  const receipt = cachedReceipt ?? await provider.getTransactionReceipt({ hash });
853
784
  const { address: routerAddress } = getRouter3(chainId2);
854
- const [block, tx] = await Promise.all([
855
- provider.getBlock({ blockNumber: receipt.blockNumber }),
856
- provider.getTransaction({ hash })
857
- ]);
858
785
  const yodlEvent = decodeYodlFromLogs(receipt.logs, routerAddress);
859
786
  const swapLogs = yodlEvent ? receipt.logs.filter((l) => (l.logIndex ?? 0) < yodlEvent.logIndex) : receipt.logs;
860
787
  const swapEvent = decodeSwapFromLogs(swapLogs);
861
788
  if (yodlEvent) {
862
- const blockTimestamp = toBlockTimestamp(block);
863
- const webhooks = extractEmbeddedParams(tx.input);
864
- const paymentEvent = buildPaymentEvent(yodlEvent, webhooks, blockTimestamp);
865
789
  if (swapEvent) {
866
- return { type: "swap", ...paymentEvent, ...swapEvent };
790
+ const [block2, tx2] = await Promise.all([
791
+ provider.getBlock({ blockNumber: receipt.blockNumber }),
792
+ provider.getTransaction({ hash })
793
+ ]);
794
+ const paymentEvent2 = buildPaymentEvent(
795
+ yodlEvent,
796
+ extractEmbeddedParams(tx2.input),
797
+ toBlockTimestamp(block2)
798
+ );
799
+ return { type: "swap", ...paymentEvent2, ...swapEvent };
867
800
  }
868
801
  const bridgeResult2 = await tryDecodeBridge(hash, clients, {
869
802
  fillReceipt: receipt
870
803
  });
871
804
  if (bridgeResult2) return bridgeResult2;
805
+ const [block, tx] = await Promise.all([
806
+ provider.getBlock({ blockNumber: receipt.blockNumber }),
807
+ provider.getTransaction({ hash })
808
+ ]);
809
+ const paymentEvent = buildPaymentEvent(
810
+ yodlEvent,
811
+ extractEmbeddedParams(tx.input),
812
+ toBlockTimestamp(block)
813
+ );
872
814
  const inferredSwap = inferSwapFromTransfers(
873
815
  swapLogs,
874
816
  yodlEvent.sender,
@@ -879,12 +821,14 @@ async function decodePayment(hash, chainId2, clients, cachedReceipt) {
879
821
  }
880
822
  return { type: "direct", ...paymentEvent };
881
823
  }
882
- const bridgeResult = await tryDecodeBridge(hash, clients);
824
+ const bridgeResult = await tryDecodeBridge(hash, clients, {
825
+ sourceReceipt: receipt
826
+ });
883
827
  if (bridgeResult) return bridgeResult;
884
828
  throw new NoYodlEventError();
885
829
  }
886
830
 
887
- // src/yodl-payment.ts
831
+ // src/core/yodl-payment.ts
888
832
  import {
889
833
  formatUnits,
890
834
  zeroAddress
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yodlpay/payment-decoder",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
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.4.4",
42
- "@relayprotocol/relay-sdk": "^5.2.0",
41
+ "@biomejs/biome": "^2.4.8",
42
+ "@relayprotocol/relay-sdk": "^5.2.1",
43
43
  "@semantic-release/changelog": "^6.0.3",
44
44
  "@semantic-release/git": "^10.0.1",
45
45
  "@types/bun": "latest",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@across-protocol/app-sdk": "^0.5.0",
52
- "@yodlpay/tokenlists": "^1.1.12"
52
+ "@yodlpay/tokenlists": "^1.1.13"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "typescript": "^5",