@yodlpay/payment-decoder 1.3.3 → 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
@@ -41,10 +41,15 @@ interface BridgeInfo extends SwapInfo {
41
41
  destinationTxHash: Hex;
42
42
  tokenOutAmountGross: bigint;
43
43
  }
44
+ interface InferredSwapInfo extends Omit<SwapInfo, "service"> {
45
+ service?: undefined;
46
+ }
44
47
  type PaymentInfo = (PaymentEvent & {
45
48
  type: "direct";
46
49
  }) | (PaymentEvent & SwapInfo & {
47
50
  type: "swap";
51
+ }) | (PaymentEvent & InferredSwapInfo & {
52
+ type: "swap";
48
53
  }) | (PaymentEvent & BridgeInfo & {
49
54
  type: "bridge";
50
55
  });
@@ -79,6 +84,8 @@ interface DecodeBridgeOptions {
79
84
  includeAcross?: boolean;
80
85
  /** Receipt of the fill (destination) tx, used for Across fill-side lookups. */
81
86
  fillReceipt?: TransactionReceipt;
87
+ /** Receipt of the source tx, used to skip re-fetching when hash is the source. */
88
+ sourceReceipt?: TransactionReceipt;
82
89
  }
83
90
  declare function decodeBridgePayment(hash: Hex, clients: ChainClients, options?: DecodeBridgeOptions): Promise<Extract<PaymentInfo, {
84
91
  type: "bridge";
@@ -102,6 +109,10 @@ interface DecodedYodlEvent {
102
109
  */
103
110
  declare function decodeYodlFromLogs(logs: readonly Log[], routerAddress: Address): DecodedYodlEvent | undefined;
104
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
+
105
116
  declare class NoBridgeFoundError extends Error {
106
117
  constructor(message?: string);
107
118
  }
@@ -109,8 +120,4 @@ declare class NoYodlEventError extends Error {
109
120
  constructor(message?: string);
110
121
  }
111
122
 
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
-
116
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));
@@ -355,6 +195,57 @@ function findInputTransfer(transfers, sender) {
355
195
  if (!result) return void 0;
356
196
  return { token: result.token, amount: result.amount };
357
197
  }
198
+ function inferSwapFromTransfers(logs, sender, yodlToken) {
199
+ const transfers = extractTokenTransfers(logs);
200
+ const outgoingDifferentToken = transfers.filter(
201
+ (t) => isAddressEqual(t.from, sender) && !isAddressEqual(t.token, yodlToken)
202
+ );
203
+ if (outgoingDifferentToken.length === 0) return void 0;
204
+ const byToken = /* @__PURE__ */ new Map();
205
+ for (const t of outgoingDifferentToken) {
206
+ const key = getAddress(t.token);
207
+ const existing = byToken.get(key);
208
+ if (existing) {
209
+ existing.gross += t.amount;
210
+ } else {
211
+ byToken.set(key, { token: t.token, gross: t.amount, refund: 0n });
212
+ }
213
+ }
214
+ const incomingNonYodl = transfers.filter(
215
+ (t) => isAddressEqual(t.to, sender) && !isAddressEqual(t.token, yodlToken)
216
+ );
217
+ for (const t of incomingNonYodl) {
218
+ const key = getAddress(t.token);
219
+ const existing = byToken.get(key);
220
+ if (existing) {
221
+ existing.refund += t.amount;
222
+ }
223
+ }
224
+ let tokenIn;
225
+ let tokenInAmount = 0n;
226
+ for (const { token, gross, refund } of byToken.values()) {
227
+ const net = gross - refund;
228
+ if (net > tokenInAmount) {
229
+ tokenIn = token;
230
+ tokenInAmount = net;
231
+ }
232
+ }
233
+ if (!tokenIn || tokenInAmount <= 0n) return void 0;
234
+ const incomingYodlToken = transfers.filter(
235
+ (t) => isAddressEqual(t.to, sender) && isAddressEqual(t.token, yodlToken)
236
+ );
237
+ const tokenOutAmount = incomingYodlToken.reduce(
238
+ (sum, t) => sum + t.amount,
239
+ 0n
240
+ );
241
+ if (tokenOutAmount === 0n) return void 0;
242
+ return {
243
+ tokenIn,
244
+ tokenOut: yodlToken,
245
+ tokenInAmount,
246
+ tokenOutAmount
247
+ };
248
+ }
358
249
  function toBlockTimestamp(block) {
359
250
  return new Date(Number(block.timestamp) * 1e3);
360
251
  }
@@ -367,33 +258,38 @@ function buildPaymentEvent(yodlEvent, webhooks, blockTimestamp, senderOverride)
367
258
  };
368
259
  }
369
260
 
370
- // src/embedded-params.ts
261
+ // src/core/embedded-params.ts
371
262
  import { decodeFunctionData, toFunctionSelector } from "viem";
372
263
 
373
264
  // src/validation.ts
374
- import { chains } from "@yodlpay/tokenlists";
375
- import { isAddress, isHex } from "viem";
376
- import { z as z3 } from "zod";
265
+ import { isAddress, isHash, isHex } from "viem";
266
+ import { z as z2 } from "zod";
267
+
268
+ // src/chains.ts
269
+ import { chains as allChains } from "@yodlpay/tokenlists";
270
+ var chains = allChains.filter((c) => !c.testnet);
271
+
272
+ // src/validation.ts
377
273
  var validChainIds = chains.map((c) => c.id);
378
- var txHashSchema = z3.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash").transform((val) => val);
379
- 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), {
380
276
  message: `Chain ID must be one of: ${validChainIds.join(", ")}`
381
277
  });
382
- var ArgsSchema = z3.union([
383
- z3.tuple([txHashSchema, chainIdSchema]),
384
- z3.tuple([txHashSchema])
278
+ var ArgsSchema = z2.union([
279
+ z2.tuple([txHashSchema, chainIdSchema]),
280
+ z2.tuple([txHashSchema])
385
281
  ]);
386
- var WebhooksSchema = z3.array(
387
- z3.object({
388
- webhookAddress: z3.string().refine((val) => isAddress(val)),
389
- 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)))
390
286
  }).transform((webhook) => ({
391
287
  ...webhook,
392
288
  memo: webhook.payload[0] ? decodeMemo(webhook.payload[0]) : ""
393
289
  }))
394
290
  ).catch([]);
395
291
 
396
- // src/embedded-params.ts
292
+ // src/core/embedded-params.ts
397
293
  function getYodlSelector() {
398
294
  const yodlFunction = yodlAbi.find(
399
295
  (i) => i.type === "function" && i.name === "yodlWithToken"
@@ -423,16 +319,98 @@ function extractEmbeddedParams(data) {
423
319
  return [];
424
320
  }
425
321
 
426
- // 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
427
406
  import { getRouter } from "@yodlpay/tokenlists";
428
407
  import {
429
- decodeEventLog as decodeEventLog2,
430
- isAddressEqual as isAddressEqual2
408
+ isAddressEqual as isAddressEqual2,
409
+ parseEventLogs as parseEventLogs2
431
410
  } from "viem";
432
- import { entryPoint08Address } from "viem/account-abstraction";
411
+ import { entryPoint08Abi, entryPoint08Address } from "viem/account-abstraction";
433
412
 
434
413
  // src/clients.ts
435
- import { chains as chains2 } from "@yodlpay/tokenlists";
436
414
  import {
437
415
  createPublicClient,
438
416
  http
@@ -445,10 +423,10 @@ var rpcOverrides = {
445
423
  };
446
424
  function createClients() {
447
425
  const clients = {};
448
- for (const chain of chains2) {
426
+ for (const chain of chains) {
449
427
  clients[chain.id] = createPublicClient({
450
428
  chain,
451
- transport: http(rpcOverrides[chain.id])
429
+ transport: http(rpcOverrides[chain.id], { batch: true })
452
430
  });
453
431
  }
454
432
  return clients;
@@ -478,12 +456,10 @@ async function detectChain(hash, clients) {
478
456
  return found.value;
479
457
  }
480
458
 
481
- // src/relay-client.ts
482
- var MAINNET_RELAY_API = "https://api.relay.link";
483
- var TESTNET_RELAY_API = "https://api.testnets.relay.link";
484
- async function getRelayRequests(params, options) {
485
- const baseUrl = options?.testnet ? TESTNET_RELAY_API : MAINNET_RELAY_API;
486
- const url = new URL(`${baseUrl}/requests/v2`);
459
+ // src/bridges/relay-client.ts
460
+ var RELAY_API = "https://api.relay.link";
461
+ async function getRelayRequests(params) {
462
+ const url = new URL(`${RELAY_API}/requests/v2`);
487
463
  for (const [key, value] of Object.entries(params)) {
488
464
  if (value !== void 0) {
489
465
  url.searchParams.set(key, String(value));
@@ -504,7 +480,7 @@ async function fetchRelayRequest(hash) {
504
480
  return request;
505
481
  }
506
482
 
507
- // src/relay-bridge.ts
483
+ // src/bridges/relay-bridge.ts
508
484
  async function calculateOutputAmountGross(inputAmount, inputToken, inputChainId, outputToken, outputChainId, clients) {
509
485
  const [{ decimals: inputDecimals }, { decimals: outputDecimals }] = await Promise.all([
510
486
  resolveToken(inputToken, inputChainId, getClient(clients, inputChainId)),
@@ -520,19 +496,15 @@ async function calculateOutputAmountGross(inputAmount, inputToken, inputChainId,
520
496
  return inputAmount * 10n ** BigInt(-decimalDiff);
521
497
  }
522
498
  async function extractSenderFromSource(sourceReceipt, sourceProvider, sourceHash) {
523
- for (const log of sourceReceipt.logs) {
524
- if (!isAddressEqual2(log.address, entryPoint08Address)) continue;
525
- try {
526
- const decoded = decodeEventLog2({
527
- abi: entryPointAbi,
528
- data: log.data,
529
- topics: log.topics
530
- });
531
- if (decoded.eventName === "UserOperationEvent") {
532
- return decoded.args.sender;
533
- }
534
- } catch {
535
- }
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;
536
508
  }
537
509
  const sourceTx = await sourceProvider.getTransaction({ hash: sourceHash });
538
510
  return sourceTx.from;
@@ -586,7 +558,7 @@ function parseRelayResponse(request) {
586
558
  inputAmountGross: BigInt(data?.metadata?.currencyIn?.amount ?? 0)
587
559
  };
588
560
  }
589
- async function decodeRelayBridgePayment(hash, clients) {
561
+ async function decodeRelayBridgePayment(hash, clients, options = {}) {
590
562
  const request = await fetchRelayRequest(hash);
591
563
  const {
592
564
  sourceChainId,
@@ -601,9 +573,12 @@ async function decodeRelayBridgePayment(hash, clients) {
601
573
  throw new NoBridgeFoundError();
602
574
  }
603
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;
604
579
  const [destReceipt, sourceReceipt] = await Promise.all([
605
- destProvider.getTransactionReceipt({ hash: destinationTxHash }),
606
- sourceProvider.getTransactionReceipt({ hash: sourceTxHash })
580
+ cachedDestReceipt ?? destProvider.getTransactionReceipt({ hash: destinationTxHash }),
581
+ cachedSourceReceipt ?? sourceProvider.getTransactionReceipt({ hash: sourceTxHash })
607
582
  ]);
608
583
  const yodlEvent = decodeYodlFromLogs(destReceipt.logs, routerAddress);
609
584
  if (!yodlEvent) {
@@ -643,7 +618,7 @@ async function decodeRelayBridgePayment(hash, clients) {
643
618
  return { type: "bridge", ...payment, ...bridge };
644
619
  }
645
620
 
646
- // src/across-bridge.ts
621
+ // src/bridges/across-bridge.ts
647
622
  async function resolveAcrossStatus(hash, fillReceipt) {
648
623
  try {
649
624
  return await fetchAcrossDepositByTx(hash);
@@ -660,13 +635,13 @@ async function resolveAcrossStatus(hash, fillReceipt) {
660
635
  Number(fillLog.originChainId),
661
636
  String(fillLog.depositId)
662
637
  );
663
- if (status.fillTxnRef !== hash) {
638
+ if (!status.fillTxnRef || status.fillTxnRef.toLowerCase() !== hash.toLowerCase()) {
664
639
  throw new NoBridgeFoundError();
665
640
  }
666
641
  return status;
667
642
  }
668
- async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
669
- const status = await resolveAcrossStatus(hash, fillReceipt);
643
+ async function decodeAcrossBridgePayment(hash, clients, options = {}) {
644
+ const status = await resolveAcrossStatus(hash, options.fillReceipt);
670
645
  if (status.status !== "filled" || !status.fillTxnRef) {
671
646
  throw new NoBridgeFoundError();
672
647
  }
@@ -683,9 +658,12 @@ async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
683
658
  throw new NoBridgeFoundError();
684
659
  }
685
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;
686
664
  const [sourceReceipt, destReceipt] = await Promise.all([
687
- sourceProvider.getTransactionReceipt({ hash: sourceTxHash }),
688
- destProvider.getTransactionReceipt({ hash: destinationTxHash })
665
+ cachedSourceReceipt ?? sourceProvider.getTransactionReceipt({ hash: sourceTxHash }),
666
+ cachedDestReceipt ?? destProvider.getTransactionReceipt({ hash: destinationTxHash })
689
667
  ]);
690
668
  const yodlEvent = decodeYodlFromLogs(destReceipt.logs, routerAddress);
691
669
  if (!yodlEvent) {
@@ -752,12 +730,15 @@ async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
752
730
  return { type: "bridge", ...payment, ...bridge };
753
731
  }
754
732
 
755
- // src/bridge-payment.ts
733
+ // src/bridges/bridge-payment.ts
756
734
  async function decodeBridgePayment(hash, clients, options = {}) {
757
735
  const includeAcross = options.includeAcross ?? true;
758
736
  let relayError;
759
737
  try {
760
- return await decodeRelayBridgePayment(hash, clients);
738
+ return await decodeRelayBridgePayment(hash, clients, {
739
+ fillReceipt: options.fillReceipt,
740
+ sourceReceipt: options.sourceReceipt
741
+ });
761
742
  } catch (error) {
762
743
  relayError = error;
763
744
  if (!includeAcross) {
@@ -765,7 +746,10 @@ async function decodeBridgePayment(hash, clients, options = {}) {
765
746
  }
766
747
  }
767
748
  try {
768
- return await decodeAcrossBridgePayment(hash, clients, options.fillReceipt);
749
+ return await decodeAcrossBridgePayment(hash, clients, {
750
+ fillReceipt: options.fillReceipt,
751
+ sourceReceipt: options.sourceReceipt
752
+ });
769
753
  } catch (acrossError) {
770
754
  if (!(relayError instanceof NoBridgeFoundError) && !(acrossError instanceof NoBridgeFoundError)) {
771
755
  throw new Error("Both Relay and Across bridge decoders failed", {
@@ -782,7 +766,7 @@ async function decodeBridgePayment(hash, clients, options = {}) {
782
766
  }
783
767
  }
784
768
 
785
- // src/payment-decoder.ts
769
+ // src/core/payment-decoder.ts
786
770
  import { getRouter as getRouter3 } from "@yodlpay/tokenlists";
787
771
  async function tryDecodeBridge(hash, clients, options = {}) {
788
772
  try {
@@ -798,32 +782,53 @@ async function decodePayment(hash, chainId2, clients, cachedReceipt) {
798
782
  const provider = getClient(clients, chainId2);
799
783
  const receipt = cachedReceipt ?? await provider.getTransactionReceipt({ hash });
800
784
  const { address: routerAddress } = getRouter3(chainId2);
801
- const [block, tx] = await Promise.all([
802
- provider.getBlock({ blockNumber: receipt.blockNumber }),
803
- provider.getTransaction({ hash })
804
- ]);
805
785
  const yodlEvent = decodeYodlFromLogs(receipt.logs, routerAddress);
806
786
  const swapLogs = yodlEvent ? receipt.logs.filter((l) => (l.logIndex ?? 0) < yodlEvent.logIndex) : receipt.logs;
807
787
  const swapEvent = decodeSwapFromLogs(swapLogs);
808
788
  if (yodlEvent) {
809
- const blockTimestamp = toBlockTimestamp(block);
810
- const webhooks = extractEmbeddedParams(tx.input);
811
- const paymentEvent = buildPaymentEvent(yodlEvent, webhooks, blockTimestamp);
812
789
  if (swapEvent) {
813
- 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 };
814
800
  }
815
801
  const bridgeResult2 = await tryDecodeBridge(hash, clients, {
816
802
  fillReceipt: receipt
817
803
  });
818
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
+ );
814
+ const inferredSwap = inferSwapFromTransfers(
815
+ swapLogs,
816
+ yodlEvent.sender,
817
+ yodlEvent.token
818
+ );
819
+ if (inferredSwap) {
820
+ return { type: "swap", ...paymentEvent, ...inferredSwap };
821
+ }
819
822
  return { type: "direct", ...paymentEvent };
820
823
  }
821
- const bridgeResult = await tryDecodeBridge(hash, clients);
824
+ const bridgeResult = await tryDecodeBridge(hash, clients, {
825
+ sourceReceipt: receipt
826
+ });
822
827
  if (bridgeResult) return bridgeResult;
823
828
  throw new NoYodlEventError();
824
829
  }
825
830
 
826
- // src/yodl-payment.ts
831
+ // src/core/yodl-payment.ts
827
832
  import {
828
833
  formatUnits,
829
834
  zeroAddress
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yodlpay/payment-decoder",
3
- "version": "1.3.3",
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",