@yodlpay/payment-decoder 1.3.4 → 1.3.6

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,7 @@
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";
1
+ // src/bridges/bridge-payment.ts
2
+ import { parseDepositLogs as parseDepositLogs2, parseFillLogs as parseFillLogs2 } from "@across-protocol/app-sdk";
8
3
 
9
4
  // src/errors.ts
10
- import {
11
- AbiDecodingDataSizeTooSmallError,
12
- AbiEventSignatureEmptyTopicsError,
13
- AbiEventSignatureNotFoundError,
14
- DecodeLogTopicsMismatch
15
- } from "viem";
16
5
  var ExpectedDecodeError = class extends Error {
17
6
  constructor(message = "Expected decode error") {
18
7
  super(message);
@@ -31,141 +20,37 @@ var NoYodlEventError = class extends Error {
31
20
  this.name = "NoYodlEventError";
32
21
  }
33
22
  };
34
- function isExpectedDecodeError(error) {
35
- return error instanceof AbiEventSignatureNotFoundError || error instanceof AbiEventSignatureEmptyTopicsError || error instanceof AbiDecodingDataSizeTooSmallError || error instanceof DecodeLogTopicsMismatch;
36
- }
37
23
 
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
- }
24
+ // src/bridges/across-bridge.ts
25
+ import { parseDepositLogs, parseFillLogs } from "@across-protocol/app-sdk";
26
+ import { getRouter as getRouter2 } from "@yodlpay/tokenlists";
118
27
 
119
- // src/decode-utils.ts
28
+ // src/core/decode-utils.ts
120
29
  import { tokenlist } from "@yodlpay/tokenlists";
121
30
  import {
122
- decodeEventLog,
123
31
  erc20Abi as erc20Abi2,
124
32
  getAddress,
125
- isAddressEqual
33
+ isAddressEqual,
34
+ parseEventLogs
126
35
  } from "viem";
127
36
 
128
37
  // src/abi.ts
129
38
  import { getRouterAbi } from "@yodlpay/tokenlists";
39
+ import { parseAbi } from "viem";
130
40
  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
- ];
41
+ var relaySwapAbi = parseAbi([
42
+ "event Swap(address indexed sender, address indexed recipient, address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount)"
43
+ ]);
160
44
 
161
45
  // src/utils.ts
162
46
  import { getTokenByAddress } from "@yodlpay/tokenlists";
163
47
  import {
164
48
  erc20Abi,
165
49
  getContract,
166
- hexToString
50
+ hexToString,
51
+ stringify
167
52
  } from "viem";
168
- import { z as z2 } from "zod";
53
+ import { z } from "zod";
169
54
  function decodeMemo(memo) {
170
55
  if (!memo || memo === "0x") return "";
171
56
  try {
@@ -174,8 +59,8 @@ function decodeMemo(memo) {
174
59
  return "";
175
60
  }
176
61
  }
177
- var erc20String = z2.string().max(64).transform((s) => s.replace(/\p{Cc}/gu, ""));
178
- var erc20Decimals = z2.number().int().nonnegative().max(36);
62
+ var erc20String = z.string().max(64).transform((s) => s.replace(/\p{Cc}/gu, ""));
63
+ var erc20Decimals = z.number().int().nonnegative().max(36);
179
64
  async function resolveToken(address, chainId2, client) {
180
65
  try {
181
66
  return getTokenByAddress(address, chainId2);
@@ -196,98 +81,56 @@ async function resolveToken(address, chainId2, client) {
196
81
  }
197
82
  }
198
83
 
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
- }
84
+ // src/core/decode-utils.ts
239
85
  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
- );
86
+ const parsed = parseEventLogs({
87
+ abi: yodlAbi,
88
+ logs: logs.filter((l) => isAddressEqual(l.address, routerAddress)),
89
+ eventName: "Yodl",
90
+ strict: true
91
+ });
92
+ const first = parsed[0];
93
+ if (!first) return void 0;
94
+ const args = first.args;
95
+ return {
96
+ sender: args.sender,
97
+ receiver: args.receiver,
98
+ token: args.token,
99
+ amount: args.amount,
100
+ memo: decodeMemo(args.memo),
101
+ logIndex: first.logIndex ?? 0
102
+ };
260
103
  }
261
104
  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
- );
105
+ const parsed = parseEventLogs({
106
+ abi: relaySwapAbi,
107
+ logs: [...logs],
108
+ eventName: "Swap",
109
+ strict: true
110
+ });
111
+ const first = parsed[0];
112
+ if (!first) return void 0;
113
+ return {
114
+ tokenIn: first.args.inputToken,
115
+ tokenOut: first.args.outputToken,
116
+ tokenInAmount: first.args.inputAmount,
117
+ tokenOutAmount: first.args.outputAmount,
118
+ service: "relay"
119
+ };
276
120
  }
277
121
  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
- );
122
+ const parsed = parseEventLogs({
123
+ abi: erc20Abi2,
124
+ logs: [...logs],
125
+ eventName: "Transfer",
126
+ strict: false
127
+ });
128
+ return parsed.flatMap((log) => {
129
+ if (!log.args) return [];
130
+ const { from, to, value } = log.args;
131
+ if (!from || !to || value === void 0) return [];
132
+ return [{ token: log.address, from, to, amount: value }];
133
+ });
291
134
  }
292
135
  function isKnownToken(address) {
293
136
  return tokenlist.some((t) => isAddressEqual(t.address, address));
@@ -418,12 +261,12 @@ function buildPaymentEvent(yodlEvent, webhooks, blockTimestamp, senderOverride)
418
261
  };
419
262
  }
420
263
 
421
- // src/embedded-params.ts
264
+ // src/core/embedded-params.ts
422
265
  import { decodeFunctionData, toFunctionSelector } from "viem";
423
266
 
424
267
  // src/validation.ts
425
- import { isAddress, isHex } from "viem";
426
- import { z as z3 } from "zod";
268
+ import { isAddress, isHash, isHex } from "viem";
269
+ import { z as z2 } from "zod";
427
270
 
428
271
  // src/chains.ts
429
272
  import { chains as allChains } from "@yodlpay/tokenlists";
@@ -431,25 +274,25 @@ var chains = allChains.filter((c) => !c.testnet);
431
274
 
432
275
  // src/validation.ts
433
276
  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), {
277
+ var txHashSchema = z2.string().refine((val) => isHash(val), "Invalid transaction hash");
278
+ var chainIdSchema = z2.coerce.number().int().refine((id) => validChainIds.includes(id), {
436
279
  message: `Chain ID must be one of: ${validChainIds.join(", ")}`
437
280
  });
438
- var ArgsSchema = z3.union([
439
- z3.tuple([txHashSchema, chainIdSchema]),
440
- z3.tuple([txHashSchema])
281
+ var ArgsSchema = z2.union([
282
+ z2.tuple([txHashSchema, chainIdSchema]),
283
+ z2.tuple([txHashSchema])
441
284
  ]);
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)))
285
+ var WebhooksSchema = z2.array(
286
+ z2.object({
287
+ webhookAddress: z2.string().refine((val) => isAddress(val)),
288
+ payload: z2.array(z2.string().refine((val) => isHex(val)))
446
289
  }).transform((webhook) => ({
447
290
  ...webhook,
448
291
  memo: webhook.payload[0] ? decodeMemo(webhook.payload[0]) : ""
449
292
  }))
450
293
  ).catch([]);
451
294
 
452
- // src/embedded-params.ts
295
+ // src/core/embedded-params.ts
453
296
  function getYodlSelector() {
454
297
  const yodlFunction = yodlAbi.find(
455
298
  (i) => i.type === "function" && i.name === "yodlWithToken"
@@ -479,13 +322,96 @@ function extractEmbeddedParams(data) {
479
322
  return [];
480
323
  }
481
324
 
482
- // src/relay-bridge.ts
325
+ // src/bridges/across-client.ts
326
+ import { isHash as isHash2 } from "viem";
327
+ import { z as z3 } from "zod";
328
+ var ACROSS_API = "https://app.across.to/api/deposit/status";
329
+ var ACROSS_TIMEOUT_MS = 8e3;
330
+ var chainId = z3.union([
331
+ z3.number().int().positive(),
332
+ z3.string().regex(/^\d+$/).transform(Number)
333
+ ]).pipe(z3.number().int().positive());
334
+ var txHash = z3.string().refine((v) => isHash2(v));
335
+ var nullableTxHash = txHash.nullish().transform((v) => v ?? null);
336
+ var depositId = z3.union([
337
+ z3.string().regex(/^\d+$/),
338
+ z3.number().int().nonnegative().transform((v) => String(v))
339
+ ]);
340
+ var AcrossDepositStatusSchema = z3.object({
341
+ status: z3.enum(["filled", "pending", "expired", "refunded"]),
342
+ originChainId: chainId,
343
+ destinationChainId: chainId,
344
+ depositId,
345
+ depositTxnRef: txHash.optional(),
346
+ depositTxHash: txHash.optional(),
347
+ fillTxnRef: nullableTxHash,
348
+ fillTx: nullableTxHash,
349
+ actionsSucceeded: z3.boolean().nullish().transform((v) => v ?? null)
350
+ }).transform((d) => {
351
+ const depositTxnRef = d.depositTxnRef ?? d.depositTxHash;
352
+ if (!depositTxnRef) {
353
+ throw new Error("Missing depositTxnRef and depositTxHash");
354
+ }
355
+ return {
356
+ status: d.status,
357
+ originChainId: d.originChainId,
358
+ destinationChainId: d.destinationChainId,
359
+ depositId: d.depositId,
360
+ depositTxnRef,
361
+ fillTxnRef: d.fillTxnRef ?? d.fillTx,
362
+ actionsSucceeded: d.actionsSucceeded
363
+ };
364
+ });
365
+ async function fetchAcrossDepositByDepositId(originChainId, depositId2) {
366
+ const url = new URL(ACROSS_API);
367
+ url.searchParams.set("originChainId", String(originChainId));
368
+ url.searchParams.set("depositId", depositId2);
369
+ return fetchAcrossDeposit(url);
370
+ }
371
+ async function fetchAcrossDeposit(url) {
372
+ const controller = new AbortController();
373
+ const timeout = setTimeout(() => controller.abort(), ACROSS_TIMEOUT_MS);
374
+ let response;
375
+ try {
376
+ response = await fetch(url, { signal: controller.signal });
377
+ } catch (error) {
378
+ const message = error instanceof Error && error.name === "AbortError" ? `Across API request timed out after ${ACROSS_TIMEOUT_MS}ms` : "Across API request failed";
379
+ throw new Error(message, { cause: error });
380
+ } finally {
381
+ clearTimeout(timeout);
382
+ }
383
+ if (response.status === 404) throw new NoBridgeFoundError();
384
+ if (!response.ok) {
385
+ const body = await response.text().catch(() => "");
386
+ const kind = response.status === 429 || response.status >= 500 ? "temporary error" : "error";
387
+ throw new Error(
388
+ `Across API ${kind} (${response.status})${body ? ` - ${body}` : ""}`
389
+ );
390
+ }
391
+ const data = await response.json().catch(() => {
392
+ throw new Error("Across API returned invalid JSON");
393
+ });
394
+ const result = AcrossDepositStatusSchema.safeParse(data);
395
+ if (!result.success) {
396
+ throw new Error(
397
+ `Across API response validation failed: ${result.error.message}`
398
+ );
399
+ }
400
+ return result.data;
401
+ }
402
+ async function fetchAcrossDepositByTx(hash) {
403
+ const url = new URL(ACROSS_API);
404
+ url.searchParams.set("depositTxnRef", hash);
405
+ return fetchAcrossDeposit(url);
406
+ }
407
+
408
+ // src/bridges/relay-bridge.ts
483
409
  import { getRouter } from "@yodlpay/tokenlists";
484
410
  import {
485
- decodeEventLog as decodeEventLog2,
486
- isAddressEqual as isAddressEqual2
411
+ isAddressEqual as isAddressEqual2,
412
+ parseEventLogs as parseEventLogs2
487
413
  } from "viem";
488
- import { entryPoint08Address } from "viem/account-abstraction";
414
+ import { entryPoint08Abi, entryPoint08Address } from "viem/account-abstraction";
489
415
 
490
416
  // src/clients.ts
491
417
  import {
@@ -503,7 +429,7 @@ function createClients() {
503
429
  for (const chain of chains) {
504
430
  clients[chain.id] = createPublicClient({
505
431
  chain,
506
- transport: http(rpcOverrides[chain.id])
432
+ transport: http(rpcOverrides[chain.id], { batch: true })
507
433
  });
508
434
  }
509
435
  return clients;
@@ -533,7 +459,7 @@ async function detectChain(hash, clients) {
533
459
  return found.value;
534
460
  }
535
461
 
536
- // src/relay-client.ts
462
+ // src/bridges/relay-client.ts
537
463
  var RELAY_API = "https://api.relay.link";
538
464
  async function getRelayRequests(params) {
539
465
  const url = new URL(`${RELAY_API}/requests/v2`);
@@ -557,7 +483,7 @@ async function fetchRelayRequest(hash) {
557
483
  return request;
558
484
  }
559
485
 
560
- // src/relay-bridge.ts
486
+ // src/bridges/relay-bridge.ts
561
487
  async function calculateOutputAmountGross(inputAmount, inputToken, inputChainId, outputToken, outputChainId, clients) {
562
488
  const [{ decimals: inputDecimals }, { decimals: outputDecimals }] = await Promise.all([
563
489
  resolveToken(inputToken, inputChainId, getClient(clients, inputChainId)),
@@ -572,23 +498,18 @@ async function calculateOutputAmountGross(inputAmount, inputToken, inputChainId,
572
498
  if (decimalDiff > 0) return inputAmount / 10n ** BigInt(decimalDiff);
573
499
  return inputAmount * 10n ** BigInt(-decimalDiff);
574
500
  }
575
- 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
- }
501
+ function extractSenderFromSource(sourceReceipt) {
502
+ const userOps = parseEventLogs2({
503
+ abi: entryPoint08Abi,
504
+ logs: sourceReceipt.logs.filter(
505
+ (l) => isAddressEqual2(l.address, entryPoint08Address)
506
+ ),
507
+ eventName: "UserOperationEvent"
508
+ });
509
+ if (userOps[0]) {
510
+ return userOps[0].args.sender;
589
511
  }
590
- const sourceTx = await sourceProvider.getTransaction({ hash: sourceHash });
591
- return sourceTx.from;
512
+ return sourceReceipt.from;
592
513
  }
593
514
  async function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inputAmountGross, inputChainId, outputChainId, clients) {
594
515
  const sourceTransfers = extractTokenTransfers(sourceReceipt.logs);
@@ -639,7 +560,7 @@ function parseRelayResponse(request) {
639
560
  inputAmountGross: BigInt(data?.metadata?.currencyIn?.amount ?? 0)
640
561
  };
641
562
  }
642
- async function decodeRelayBridgePayment(hash, clients) {
563
+ async function decodeRelayBridgePayment(hash, clients, options = {}) {
643
564
  const request = await fetchRelayRequest(hash);
644
565
  const {
645
566
  sourceChainId,
@@ -654,9 +575,12 @@ async function decodeRelayBridgePayment(hash, clients) {
654
575
  throw new NoBridgeFoundError();
655
576
  }
656
577
  const { address: routerAddress } = getRouter(destinationChainId);
578
+ const hashLower = hash.toLowerCase();
579
+ const cachedDestReceipt = options.fillReceipt && destinationTxHash.toLowerCase() === hashLower ? options.fillReceipt : void 0;
580
+ const cachedSourceReceipt = options.sourceReceipt && sourceTxHash.toLowerCase() === hashLower ? options.sourceReceipt : void 0;
657
581
  const [destReceipt, sourceReceipt] = await Promise.all([
658
- destProvider.getTransactionReceipt({ hash: destinationTxHash }),
659
- sourceProvider.getTransactionReceipt({ hash: sourceTxHash })
582
+ cachedDestReceipt ?? destProvider.getTransactionReceipt({ hash: destinationTxHash }),
583
+ cachedSourceReceipt ?? sourceProvider.getTransactionReceipt({ hash: sourceTxHash })
660
584
  ]);
661
585
  const yodlEvent = decodeYodlFromLogs(destReceipt.logs, routerAddress);
662
586
  if (!yodlEvent) {
@@ -664,10 +588,10 @@ async function decodeRelayBridgePayment(hash, clients) {
664
588
  "No Yodl event found in destination transaction"
665
589
  );
666
590
  }
667
- const [destBlock, destTx, sender] = await Promise.all([
591
+ const sender = extractSenderFromSource(sourceReceipt);
592
+ const [destBlock, destTx] = await Promise.all([
668
593
  destProvider.getBlock({ blockNumber: destReceipt.blockNumber }),
669
- destProvider.getTransaction({ hash: destinationTxHash }),
670
- extractSenderFromSource(sourceReceipt, sourceProvider, sourceTxHash)
594
+ destProvider.getTransaction({ hash: destinationTxHash })
671
595
  ]);
672
596
  const tokens = await resolveBridgeTokens(
673
597
  sourceReceipt,
@@ -696,30 +620,27 @@ async function decodeRelayBridgePayment(hash, clients) {
696
620
  return { type: "bridge", ...payment, ...bridge };
697
621
  }
698
622
 
699
- // src/across-bridge.ts
700
- async function resolveAcrossStatus(hash, fillReceipt) {
701
- try {
702
- return await fetchAcrossDepositByTx(hash);
703
- } catch (error) {
704
- if (!(error instanceof NoBridgeFoundError) || !fillReceipt) {
705
- throw error;
623
+ // src/bridges/across-bridge.ts
624
+ async function resolveAcrossStatus(hash, fillReceipt, cachedFillLog) {
625
+ const fillLog = cachedFillLog !== void 0 ? cachedFillLog : fillReceipt ? parseFillLogs(fillReceipt.logs) : null;
626
+ if (fillLog) {
627
+ const status = await fetchAcrossDepositByDepositId(
628
+ Number(fillLog.originChainId),
629
+ String(fillLog.depositId)
630
+ );
631
+ if (status.fillTxnRef?.toLowerCase() === hash.toLowerCase()) {
632
+ return status;
706
633
  }
634
+ return await fetchAcrossDepositByTx(hash);
707
635
  }
708
- const fillLog = parseFillLogs(fillReceipt.logs);
709
- if (!fillLog) {
710
- throw new NoBridgeFoundError();
711
- }
712
- const status = await fetchAcrossDepositByDepositId(
713
- Number(fillLog.originChainId),
714
- String(fillLog.depositId)
715
- );
716
- if (status.fillTxnRef !== hash) {
717
- throw new NoBridgeFoundError();
718
- }
719
- return status;
636
+ return await fetchAcrossDepositByTx(hash);
720
637
  }
721
- async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
722
- const status = await resolveAcrossStatus(hash, fillReceipt);
638
+ async function decodeAcrossBridgePayment(hash, clients, options = {}) {
639
+ const status = await resolveAcrossStatus(
640
+ hash,
641
+ options.fillReceipt,
642
+ options.parsedFillLog
643
+ );
723
644
  if (status.status !== "filled" || !status.fillTxnRef) {
724
645
  throw new NoBridgeFoundError();
725
646
  }
@@ -736,18 +657,21 @@ async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
736
657
  throw new NoBridgeFoundError();
737
658
  }
738
659
  const { address: routerAddress } = getRouter2(destinationChainId);
660
+ const hashLower = hash.toLowerCase();
661
+ const cachedDestReceipt = options.fillReceipt && destinationTxHash.toLowerCase() === hashLower ? options.fillReceipt : void 0;
662
+ const cachedSourceReceipt = options.sourceReceipt && sourceTxHash.toLowerCase() === hashLower ? options.sourceReceipt : void 0;
739
663
  const [sourceReceipt, destReceipt] = await Promise.all([
740
- sourceProvider.getTransactionReceipt({ hash: sourceTxHash }),
741
- destProvider.getTransactionReceipt({ hash: destinationTxHash })
664
+ cachedSourceReceipt ?? sourceProvider.getTransactionReceipt({ hash: sourceTxHash }),
665
+ cachedDestReceipt ?? destProvider.getTransactionReceipt({ hash: destinationTxHash })
742
666
  ]);
743
667
  const yodlEvent = decodeYodlFromLogs(destReceipt.logs, routerAddress);
744
668
  if (!yodlEvent) {
745
669
  throw new NoBridgeFoundError();
746
670
  }
747
- const [destBlock, destTx, sender] = await Promise.all([
671
+ const sender = extractSenderFromSource(sourceReceipt);
672
+ const [destBlock, destTx] = await Promise.all([
748
673
  destProvider.getBlock({ blockNumber: destReceipt.blockNumber }),
749
- destProvider.getTransaction({ hash: destinationTxHash }),
750
- extractSenderFromSource(sourceReceipt, sourceProvider, sourceTxHash)
674
+ destProvider.getTransaction({ hash: destinationTxHash })
751
675
  ]);
752
676
  const depositLog = parseDepositLogs(sourceReceipt.logs);
753
677
  const depositData = depositLog && depositLog.depositId === BigInt(status.depositId) ? {
@@ -805,12 +729,25 @@ async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
805
729
  return { type: "bridge", ...payment, ...bridge };
806
730
  }
807
731
 
808
- // src/bridge-payment.ts
732
+ // src/bridges/bridge-payment.ts
809
733
  async function decodeBridgePayment(hash, clients, options = {}) {
810
734
  const includeAcross = options.includeAcross ?? true;
735
+ const parsedFillLog = options.fillReceipt ? parseFillLogs2(options.fillReceipt.logs) : null;
736
+ const parsedDepositLog = options.sourceReceipt ? parseDepositLogs2(options.sourceReceipt.logs) : null;
737
+ const hasAcrossEvents = !!(parsedFillLog || parsedDepositLog);
738
+ if (includeAcross && hasAcrossEvents) {
739
+ return await decodeAcrossBridgePayment(hash, clients, {
740
+ fillReceipt: options.fillReceipt,
741
+ sourceReceipt: options.sourceReceipt,
742
+ parsedFillLog
743
+ });
744
+ }
811
745
  let relayError;
812
746
  try {
813
- return await decodeRelayBridgePayment(hash, clients);
747
+ return await decodeRelayBridgePayment(hash, clients, {
748
+ fillReceipt: options.fillReceipt,
749
+ sourceReceipt: options.sourceReceipt
750
+ });
814
751
  } catch (error) {
815
752
  relayError = error;
816
753
  if (!includeAcross) {
@@ -818,7 +755,10 @@ async function decodeBridgePayment(hash, clients, options = {}) {
818
755
  }
819
756
  }
820
757
  try {
821
- return await decodeAcrossBridgePayment(hash, clients, options.fillReceipt);
758
+ return await decodeAcrossBridgePayment(hash, clients, {
759
+ fillReceipt: options.fillReceipt,
760
+ sourceReceipt: options.sourceReceipt
761
+ });
822
762
  } catch (acrossError) {
823
763
  if (!(relayError instanceof NoBridgeFoundError) && !(acrossError instanceof NoBridgeFoundError)) {
824
764
  throw new Error("Both Relay and Across bridge decoders failed", {
@@ -835,8 +775,9 @@ async function decodeBridgePayment(hash, clients, options = {}) {
835
775
  }
836
776
  }
837
777
 
838
- // src/payment-decoder.ts
778
+ // src/core/payment-decoder.ts
839
779
  import { getRouter as getRouter3 } from "@yodlpay/tokenlists";
780
+ import { isAddressEqual as isAddressEqual3 } from "viem";
840
781
  async function tryDecodeBridge(hash, clients, options = {}) {
841
782
  try {
842
783
  return await decodeBridgePayment(hash, clients, options);
@@ -851,24 +792,42 @@ async function decodePayment(hash, chainId2, clients, cachedReceipt) {
851
792
  const provider = getClient(clients, chainId2);
852
793
  const receipt = cachedReceipt ?? await provider.getTransactionReceipt({ hash });
853
794
  const { address: routerAddress } = getRouter3(chainId2);
854
- const [block, tx] = await Promise.all([
855
- provider.getBlock({ blockNumber: receipt.blockNumber }),
856
- provider.getTransaction({ hash })
857
- ]);
858
795
  const yodlEvent = decodeYodlFromLogs(receipt.logs, routerAddress);
859
796
  const swapLogs = yodlEvent ? receipt.logs.filter((l) => (l.logIndex ?? 0) < yodlEvent.logIndex) : receipt.logs;
860
797
  const swapEvent = decodeSwapFromLogs(swapLogs);
861
798
  if (yodlEvent) {
862
- const blockTimestamp = toBlockTimestamp(block);
863
- const webhooks = extractEmbeddedParams(tx.input);
864
- const paymentEvent = buildPaymentEvent(yodlEvent, webhooks, blockTimestamp);
865
799
  if (swapEvent) {
866
- return { type: "swap", ...paymentEvent, ...swapEvent };
800
+ const [block2, tx2] = await Promise.all([
801
+ provider.getBlock({ blockNumber: receipt.blockNumber }),
802
+ provider.getTransaction({ hash })
803
+ ]);
804
+ const paymentEvent2 = buildPaymentEvent(
805
+ yodlEvent,
806
+ extractEmbeddedParams(tx2.input),
807
+ toBlockTimestamp(block2)
808
+ );
809
+ return { type: "swap", ...paymentEvent2, ...swapEvent };
867
810
  }
868
- const bridgeResult2 = await tryDecodeBridge(hash, clients, {
869
- fillReceipt: receipt
870
- });
871
- if (bridgeResult2) return bridgeResult2;
811
+ const effectiveSender = extractSenderFromSource(receipt);
812
+ const isSameChainPayment = isAddressEqual3(
813
+ effectiveSender,
814
+ yodlEvent.sender
815
+ );
816
+ if (!isSameChainPayment) {
817
+ const bridgeResult2 = await tryDecodeBridge(hash, clients, {
818
+ fillReceipt: receipt
819
+ });
820
+ if (bridgeResult2) return bridgeResult2;
821
+ }
822
+ const [block, tx] = await Promise.all([
823
+ provider.getBlock({ blockNumber: receipt.blockNumber }),
824
+ provider.getTransaction({ hash })
825
+ ]);
826
+ const paymentEvent = buildPaymentEvent(
827
+ yodlEvent,
828
+ extractEmbeddedParams(tx.input),
829
+ toBlockTimestamp(block)
830
+ );
872
831
  const inferredSwap = inferSwapFromTransfers(
873
832
  swapLogs,
874
833
  yodlEvent.sender,
@@ -879,18 +838,26 @@ async function decodePayment(hash, chainId2, clients, cachedReceipt) {
879
838
  }
880
839
  return { type: "direct", ...paymentEvent };
881
840
  }
882
- const bridgeResult = await tryDecodeBridge(hash, clients);
841
+ const bridgeResult = await tryDecodeBridge(hash, clients, {
842
+ sourceReceipt: receipt
843
+ });
883
844
  if (bridgeResult) return bridgeResult;
884
845
  throw new NoYodlEventError();
885
846
  }
886
847
 
887
- // src/yodl-payment.ts
848
+ // src/core/yodl-payment.ts
888
849
  import {
889
850
  formatUnits,
851
+ isAddressEqual as isAddressEqual4,
890
852
  zeroAddress
891
853
  } from "viem";
892
854
  async function buildTokenInfo(params, clients) {
893
- const [inToken, outToken] = await Promise.all([
855
+ const sameToken = isAddressEqual4(params.tokenIn, params.tokenOut) && params.inChainId === params.outChainId;
856
+ const [inToken, outToken] = sameToken ? await resolveToken(
857
+ params.tokenIn,
858
+ params.inChainId,
859
+ getClient(clients, params.inChainId)
860
+ ).then((t) => [t, t]) : await Promise.all([
894
861
  resolveToken(
895
862
  params.tokenIn,
896
863
  params.inChainId,
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.6",
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",