@yodlpay/payment-decoder 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.d.ts +15 -13
- package/dist/index.js +415 -123
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Decode Yodl payment transaction hashes into structured payment data. Supports direct payments, swaps, and cross-chain bridges.
|
|
4
4
|
|
|
5
|
+
Bridge discovery currently supports Relay (source + destination hashes) and Across source-chain deposits via Across status API.
|
|
6
|
+
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ declare function detectChain(hash: Hex, clients: ChainClients): Promise<{
|
|
|
12
12
|
receipt: TransactionReceipt;
|
|
13
13
|
}>;
|
|
14
14
|
|
|
15
|
-
type ServiceProvider = "relay";
|
|
15
|
+
type ServiceProvider = "relay" | "across";
|
|
16
16
|
interface Webhook {
|
|
17
17
|
webhookAddress: Address;
|
|
18
18
|
payload: readonly Hex[];
|
|
@@ -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
|
});
|
|
@@ -75,6 +80,15 @@ interface YodlPayment {
|
|
|
75
80
|
blockTimestamp: Date;
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
interface DecodeBridgeOptions {
|
|
84
|
+
includeAcross?: boolean;
|
|
85
|
+
/** Receipt of the fill (destination) tx, used for Across fill-side lookups. */
|
|
86
|
+
fillReceipt?: TransactionReceipt;
|
|
87
|
+
}
|
|
88
|
+
declare function decodeBridgePayment(hash: Hex, clients: ChainClients, options?: DecodeBridgeOptions): Promise<Extract<PaymentInfo, {
|
|
89
|
+
type: "bridge";
|
|
90
|
+
}>>;
|
|
91
|
+
|
|
78
92
|
/**
|
|
79
93
|
* Decoded Yodl event data from onchain logs.
|
|
80
94
|
* Does not include blockTimestamp or webhooks — callers provide those.
|
|
@@ -102,18 +116,6 @@ declare class NoYodlEventError extends Error {
|
|
|
102
116
|
|
|
103
117
|
declare function decodePayment(hash: Hex, chainId: number, clients: ChainClients, cachedReceipt?: TransactionReceipt): Promise<PaymentInfo>;
|
|
104
118
|
|
|
105
|
-
/**
|
|
106
|
-
* Decode a bridge transaction given any hash (source or destination).
|
|
107
|
-
* Fetches bridge info from Relay, then decodes the Yodl event from the destination chain.
|
|
108
|
-
* Extracts the original sender from the source chain's UserOperationEvent.
|
|
109
|
-
*
|
|
110
|
-
* SECURITY: Token data is extracted from onchain logs, not trusted from Relay.
|
|
111
|
-
* Relay is only used for bridge discovery (hashes + chain IDs).
|
|
112
|
-
*/
|
|
113
|
-
declare function decodeBridgePayment(hash: Hex, clients: ChainClients): Promise<Extract<PaymentInfo, {
|
|
114
|
-
type: "bridge";
|
|
115
|
-
}>>;
|
|
116
|
-
|
|
117
119
|
declare function decodeYodlPayment(txHash: Hex, chainId: number, clients: ChainClients, cachedReceipt?: TransactionReceipt): Promise<YodlPayment>;
|
|
118
120
|
|
|
119
121
|
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,46 +1,119 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
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
|
+
// src/errors.ts
|
|
3
10
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
11
|
+
AbiDecodingDataSizeTooSmallError,
|
|
12
|
+
AbiEventSignatureEmptyTopicsError,
|
|
13
|
+
AbiEventSignatureNotFoundError,
|
|
14
|
+
DecodeLogTopicsMismatch
|
|
6
15
|
} from "viem";
|
|
7
|
-
var
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
var ExpectedDecodeError = class extends Error {
|
|
17
|
+
constructor(message = "Expected decode error") {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "ExpectedDecodeError";
|
|
20
|
+
}
|
|
10
21
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
chain,
|
|
16
|
-
transport: http(rpcOverrides[chain.id])
|
|
17
|
-
});
|
|
22
|
+
var NoBridgeFoundError = class extends Error {
|
|
23
|
+
constructor(message = "No bridge transaction found") {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "NoBridgeFoundError";
|
|
18
26
|
}
|
|
19
|
-
|
|
27
|
+
};
|
|
28
|
+
var NoYodlEventError = class extends Error {
|
|
29
|
+
constructor(message = "No Yodl event found in logs") {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "NoYodlEventError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
function isExpectedDecodeError(error) {
|
|
35
|
+
return error instanceof AbiEventSignatureNotFoundError || error instanceof AbiEventSignatureEmptyTopicsError || error instanceof AbiDecodingDataSizeTooSmallError || error instanceof DecodeLogTopicsMismatch;
|
|
20
36
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
|
|
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);
|
|
25
81
|
}
|
|
26
|
-
async function
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const found = results.find(
|
|
38
|
-
(r) => r.status === "fulfilled"
|
|
39
|
-
);
|
|
40
|
-
if (!found) {
|
|
41
|
-
throw new Error(`Transaction ${hash} not found on any configured chain`);
|
|
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);
|
|
42
93
|
}
|
|
43
|
-
|
|
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);
|
|
44
117
|
}
|
|
45
118
|
|
|
46
119
|
// src/decode-utils.ts
|
|
@@ -85,35 +158,6 @@ var entryPointAbi = [
|
|
|
85
158
|
}
|
|
86
159
|
];
|
|
87
160
|
|
|
88
|
-
// src/errors.ts
|
|
89
|
-
import {
|
|
90
|
-
AbiDecodingDataSizeTooSmallError,
|
|
91
|
-
AbiEventSignatureEmptyTopicsError,
|
|
92
|
-
AbiEventSignatureNotFoundError,
|
|
93
|
-
DecodeLogTopicsMismatch
|
|
94
|
-
} from "viem";
|
|
95
|
-
var ExpectedDecodeError = class extends Error {
|
|
96
|
-
constructor(message = "Expected decode error") {
|
|
97
|
-
super(message);
|
|
98
|
-
this.name = "ExpectedDecodeError";
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
var NoBridgeFoundError = class extends Error {
|
|
102
|
-
constructor(message = "No bridge transaction found") {
|
|
103
|
-
super(message);
|
|
104
|
-
this.name = "NoBridgeFoundError";
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
var NoYodlEventError = class extends Error {
|
|
108
|
-
constructor(message = "No Yodl event found in logs") {
|
|
109
|
-
super(message);
|
|
110
|
-
this.name = "NoYodlEventError";
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
function isExpectedDecodeError(error) {
|
|
114
|
-
return error instanceof AbiEventSignatureNotFoundError || error instanceof AbiEventSignatureEmptyTopicsError || error instanceof AbiDecodingDataSizeTooSmallError || error instanceof DecodeLogTopicsMismatch;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
161
|
// src/utils.ts
|
|
118
162
|
import { getTokenByAddress } from "@yodlpay/tokenlists";
|
|
119
163
|
import {
|
|
@@ -121,7 +165,7 @@ import {
|
|
|
121
165
|
getContract,
|
|
122
166
|
hexToString
|
|
123
167
|
} from "viem";
|
|
124
|
-
import { z } from "zod";
|
|
168
|
+
import { z as z2 } from "zod";
|
|
125
169
|
function decodeMemo(memo) {
|
|
126
170
|
if (!memo || memo === "0x") return "";
|
|
127
171
|
try {
|
|
@@ -130,11 +174,11 @@ function decodeMemo(memo) {
|
|
|
130
174
|
return "";
|
|
131
175
|
}
|
|
132
176
|
}
|
|
133
|
-
var erc20String =
|
|
134
|
-
var erc20Decimals =
|
|
135
|
-
async function resolveToken(address,
|
|
177
|
+
var erc20String = z2.string().max(64).transform((s) => s.replace(/\p{Cc}/gu, ""));
|
|
178
|
+
var erc20Decimals = z2.number().int().nonnegative().max(36);
|
|
179
|
+
async function resolveToken(address, chainId2, client) {
|
|
136
180
|
try {
|
|
137
|
-
return getTokenByAddress(address,
|
|
181
|
+
return getTokenByAddress(address, chainId2);
|
|
138
182
|
} catch {
|
|
139
183
|
const token = getContract({ address, abi: erc20Abi, client });
|
|
140
184
|
const [name, symbol, decimals] = await Promise.all([
|
|
@@ -143,7 +187,7 @@ async function resolveToken(address, chainId, client) {
|
|
|
143
187
|
token.read.decimals()
|
|
144
188
|
]);
|
|
145
189
|
return {
|
|
146
|
-
chainId,
|
|
190
|
+
chainId: chainId2,
|
|
147
191
|
address,
|
|
148
192
|
name: erc20String.parse(name),
|
|
149
193
|
symbol: erc20String.parse(symbol),
|
|
@@ -311,6 +355,57 @@ function findInputTransfer(transfers, sender) {
|
|
|
311
355
|
if (!result) return void 0;
|
|
312
356
|
return { token: result.token, amount: result.amount };
|
|
313
357
|
}
|
|
358
|
+
function inferSwapFromTransfers(logs, sender, yodlToken) {
|
|
359
|
+
const transfers = extractTokenTransfers(logs);
|
|
360
|
+
const outgoingDifferentToken = transfers.filter(
|
|
361
|
+
(t) => isAddressEqual(t.from, sender) && !isAddressEqual(t.token, yodlToken)
|
|
362
|
+
);
|
|
363
|
+
if (outgoingDifferentToken.length === 0) return void 0;
|
|
364
|
+
const byToken = /* @__PURE__ */ new Map();
|
|
365
|
+
for (const t of outgoingDifferentToken) {
|
|
366
|
+
const key = getAddress(t.token);
|
|
367
|
+
const existing = byToken.get(key);
|
|
368
|
+
if (existing) {
|
|
369
|
+
existing.gross += t.amount;
|
|
370
|
+
} else {
|
|
371
|
+
byToken.set(key, { token: t.token, gross: t.amount, refund: 0n });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const incomingNonYodl = transfers.filter(
|
|
375
|
+
(t) => isAddressEqual(t.to, sender) && !isAddressEqual(t.token, yodlToken)
|
|
376
|
+
);
|
|
377
|
+
for (const t of incomingNonYodl) {
|
|
378
|
+
const key = getAddress(t.token);
|
|
379
|
+
const existing = byToken.get(key);
|
|
380
|
+
if (existing) {
|
|
381
|
+
existing.refund += t.amount;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
let tokenIn;
|
|
385
|
+
let tokenInAmount = 0n;
|
|
386
|
+
for (const { token, gross, refund } of byToken.values()) {
|
|
387
|
+
const net = gross - refund;
|
|
388
|
+
if (net > tokenInAmount) {
|
|
389
|
+
tokenIn = token;
|
|
390
|
+
tokenInAmount = net;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!tokenIn || tokenInAmount <= 0n) return void 0;
|
|
394
|
+
const incomingYodlToken = transfers.filter(
|
|
395
|
+
(t) => isAddressEqual(t.to, sender) && isAddressEqual(t.token, yodlToken)
|
|
396
|
+
);
|
|
397
|
+
const tokenOutAmount = incomingYodlToken.reduce(
|
|
398
|
+
(sum, t) => sum + t.amount,
|
|
399
|
+
0n
|
|
400
|
+
);
|
|
401
|
+
if (tokenOutAmount === 0n) return void 0;
|
|
402
|
+
return {
|
|
403
|
+
tokenIn,
|
|
404
|
+
tokenOut: yodlToken,
|
|
405
|
+
tokenInAmount,
|
|
406
|
+
tokenOutAmount
|
|
407
|
+
};
|
|
408
|
+
}
|
|
314
409
|
function toBlockTimestamp(block) {
|
|
315
410
|
return new Date(Number(block.timestamp) * 1e3);
|
|
316
411
|
}
|
|
@@ -323,29 +418,31 @@ function buildPaymentEvent(yodlEvent, webhooks, blockTimestamp, senderOverride)
|
|
|
323
418
|
};
|
|
324
419
|
}
|
|
325
420
|
|
|
326
|
-
// src/payment-decoder.ts
|
|
327
|
-
import { getRouter as getRouter2 } from "@yodlpay/tokenlists";
|
|
328
|
-
|
|
329
421
|
// src/embedded-params.ts
|
|
330
422
|
import { decodeFunctionData, toFunctionSelector } from "viem";
|
|
331
423
|
|
|
332
424
|
// src/validation.ts
|
|
333
|
-
import { chains as chains2 } from "@yodlpay/tokenlists";
|
|
334
425
|
import { isAddress, isHex } from "viem";
|
|
335
|
-
import { z as
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
426
|
+
import { z as z3 } from "zod";
|
|
427
|
+
|
|
428
|
+
// src/chains.ts
|
|
429
|
+
import { chains as allChains } from "@yodlpay/tokenlists";
|
|
430
|
+
var chains = allChains.filter((c) => !c.testnet);
|
|
431
|
+
|
|
432
|
+
// src/validation.ts
|
|
433
|
+
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), {
|
|
339
436
|
message: `Chain ID must be one of: ${validChainIds.join(", ")}`
|
|
340
437
|
});
|
|
341
|
-
var ArgsSchema =
|
|
342
|
-
|
|
343
|
-
|
|
438
|
+
var ArgsSchema = z3.union([
|
|
439
|
+
z3.tuple([txHashSchema, chainIdSchema]),
|
|
440
|
+
z3.tuple([txHashSchema])
|
|
344
441
|
]);
|
|
345
|
-
var WebhooksSchema =
|
|
346
|
-
|
|
347
|
-
webhookAddress:
|
|
348
|
-
payload:
|
|
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)))
|
|
349
446
|
}).transform((webhook) => ({
|
|
350
447
|
...webhook,
|
|
351
448
|
memo: webhook.payload[0] ? decodeMemo(webhook.payload[0]) : ""
|
|
@@ -390,12 +487,56 @@ import {
|
|
|
390
487
|
} from "viem";
|
|
391
488
|
import { entryPoint08Address } from "viem/account-abstraction";
|
|
392
489
|
|
|
490
|
+
// src/clients.ts
|
|
491
|
+
import {
|
|
492
|
+
createPublicClient,
|
|
493
|
+
http
|
|
494
|
+
} from "viem";
|
|
495
|
+
var rpcOverrides = {
|
|
496
|
+
1: "https://ethereum-rpc.publicnode.com",
|
|
497
|
+
// eth.merkle.io is down
|
|
498
|
+
137: "https://polygon-bor-rpc.publicnode.com"
|
|
499
|
+
// polygon-rpc.com is down
|
|
500
|
+
};
|
|
501
|
+
function createClients() {
|
|
502
|
+
const clients = {};
|
|
503
|
+
for (const chain of chains) {
|
|
504
|
+
clients[chain.id] = createPublicClient({
|
|
505
|
+
chain,
|
|
506
|
+
transport: http(rpcOverrides[chain.id])
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return clients;
|
|
510
|
+
}
|
|
511
|
+
function getClient(clients, chainId2) {
|
|
512
|
+
const client = clients[chainId2];
|
|
513
|
+
if (!client) throw new Error(`No client configured for chain ${chainId2}`);
|
|
514
|
+
return client;
|
|
515
|
+
}
|
|
516
|
+
async function detectChain(hash, clients) {
|
|
517
|
+
const entries = Object.entries(clients).map(([id, client]) => ({
|
|
518
|
+
chainId: Number(id),
|
|
519
|
+
client
|
|
520
|
+
}));
|
|
521
|
+
const results = await Promise.allSettled(
|
|
522
|
+
entries.map(async ({ chainId: chainId2, client }) => {
|
|
523
|
+
const receipt = await client.getTransactionReceipt({ hash });
|
|
524
|
+
return { chainId: chainId2, receipt };
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
const found = results.find(
|
|
528
|
+
(r) => r.status === "fulfilled"
|
|
529
|
+
);
|
|
530
|
+
if (!found) {
|
|
531
|
+
throw new Error(`Transaction ${hash} not found on any configured chain`);
|
|
532
|
+
}
|
|
533
|
+
return found.value;
|
|
534
|
+
}
|
|
535
|
+
|
|
393
536
|
// src/relay-client.ts
|
|
394
|
-
var
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const baseUrl = options?.testnet ? TESTNET_RELAY_API : MAINNET_RELAY_API;
|
|
398
|
-
const url = new URL(`${baseUrl}/requests/v2`);
|
|
537
|
+
var RELAY_API = "https://api.relay.link";
|
|
538
|
+
async function getRelayRequests(params) {
|
|
539
|
+
const url = new URL(`${RELAY_API}/requests/v2`);
|
|
399
540
|
for (const [key, value] of Object.entries(params)) {
|
|
400
541
|
if (value !== void 0) {
|
|
401
542
|
url.searchParams.set(key, String(value));
|
|
@@ -498,7 +639,7 @@ function parseRelayResponse(request) {
|
|
|
498
639
|
inputAmountGross: BigInt(data?.metadata?.currencyIn?.amount ?? 0)
|
|
499
640
|
};
|
|
500
641
|
}
|
|
501
|
-
async function
|
|
642
|
+
async function decodeRelayBridgePayment(hash, clients) {
|
|
502
643
|
const request = await fetchRelayRequest(hash);
|
|
503
644
|
const {
|
|
504
645
|
sourceChainId,
|
|
@@ -507,8 +648,11 @@ async function decodeBridgePayment(hash, clients) {
|
|
|
507
648
|
destinationTxHash,
|
|
508
649
|
inputAmountGross
|
|
509
650
|
} = parseRelayResponse(request);
|
|
510
|
-
const destProvider =
|
|
511
|
-
const sourceProvider =
|
|
651
|
+
const destProvider = clients[destinationChainId];
|
|
652
|
+
const sourceProvider = clients[sourceChainId];
|
|
653
|
+
if (!destProvider || !sourceProvider) {
|
|
654
|
+
throw new NoBridgeFoundError();
|
|
655
|
+
}
|
|
512
656
|
const { address: routerAddress } = getRouter(destinationChainId);
|
|
513
657
|
const [destReceipt, sourceReceipt] = await Promise.all([
|
|
514
658
|
destProvider.getTransactionReceipt({ hash: destinationTxHash }),
|
|
@@ -552,23 +696,161 @@ async function decodeBridgePayment(hash, clients) {
|
|
|
552
696
|
return { type: "bridge", ...payment, ...bridge };
|
|
553
697
|
}
|
|
554
698
|
|
|
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;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
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;
|
|
720
|
+
}
|
|
721
|
+
async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
|
|
722
|
+
const status = await resolveAcrossStatus(hash, fillReceipt);
|
|
723
|
+
if (status.status !== "filled" || !status.fillTxnRef) {
|
|
724
|
+
throw new NoBridgeFoundError();
|
|
725
|
+
}
|
|
726
|
+
if (status.actionsSucceeded === false) {
|
|
727
|
+
throw new NoBridgeFoundError();
|
|
728
|
+
}
|
|
729
|
+
const sourceChainId = status.originChainId;
|
|
730
|
+
const sourceTxHash = status.depositTxnRef;
|
|
731
|
+
const destinationChainId = status.destinationChainId;
|
|
732
|
+
const destinationTxHash = status.fillTxnRef;
|
|
733
|
+
const sourceProvider = clients[sourceChainId];
|
|
734
|
+
const destProvider = clients[destinationChainId];
|
|
735
|
+
if (!sourceProvider || !destProvider) {
|
|
736
|
+
throw new NoBridgeFoundError();
|
|
737
|
+
}
|
|
738
|
+
const { address: routerAddress } = getRouter2(destinationChainId);
|
|
739
|
+
const [sourceReceipt, destReceipt] = await Promise.all([
|
|
740
|
+
sourceProvider.getTransactionReceipt({ hash: sourceTxHash }),
|
|
741
|
+
destProvider.getTransactionReceipt({ hash: destinationTxHash })
|
|
742
|
+
]);
|
|
743
|
+
const yodlEvent = decodeYodlFromLogs(destReceipt.logs, routerAddress);
|
|
744
|
+
if (!yodlEvent) {
|
|
745
|
+
throw new NoBridgeFoundError();
|
|
746
|
+
}
|
|
747
|
+
const [destBlock, destTx, sender] = await Promise.all([
|
|
748
|
+
destProvider.getBlock({ blockNumber: destReceipt.blockNumber }),
|
|
749
|
+
destProvider.getTransaction({ hash: destinationTxHash }),
|
|
750
|
+
extractSenderFromSource(sourceReceipt, sourceProvider, sourceTxHash)
|
|
751
|
+
]);
|
|
752
|
+
const depositLog = parseDepositLogs(sourceReceipt.logs);
|
|
753
|
+
const depositData = depositLog && depositLog.depositId === BigInt(status.depositId) ? {
|
|
754
|
+
tokenIn: depositLog.inputToken,
|
|
755
|
+
inputAmount: depositLog.inputAmount,
|
|
756
|
+
outputAmount: depositLog.outputAmount
|
|
757
|
+
} : void 0;
|
|
758
|
+
const fillLog = parseFillLogs(destReceipt.logs, {
|
|
759
|
+
depositId: BigInt(status.depositId)
|
|
760
|
+
});
|
|
761
|
+
const fillOutputAmount = fillLog?.relayExecutionInfo?.updatedOutputAmount ?? fillLog?.outputAmount ?? 0n;
|
|
762
|
+
let tokens;
|
|
763
|
+
try {
|
|
764
|
+
tokens = await resolveBridgeTokens(
|
|
765
|
+
sourceReceipt,
|
|
766
|
+
destReceipt,
|
|
767
|
+
yodlEvent,
|
|
768
|
+
sender,
|
|
769
|
+
0n,
|
|
770
|
+
sourceChainId,
|
|
771
|
+
destinationChainId,
|
|
772
|
+
clients
|
|
773
|
+
);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
if (error instanceof ExpectedDecodeError && depositData?.tokenIn && depositData.inputAmount > 0n) {
|
|
776
|
+
tokens = {
|
|
777
|
+
tokenIn: depositData.tokenIn,
|
|
778
|
+
tokenInAmount: depositData.inputAmount,
|
|
779
|
+
tokenOut: yodlEvent.token,
|
|
780
|
+
tokenOutAmount: yodlEvent.amount,
|
|
781
|
+
tokenOutAmountGross: fillOutputAmount > 0n ? fillOutputAmount : depositData.outputAmount > 0n ? depositData.outputAmount : yodlEvent.amount
|
|
782
|
+
};
|
|
783
|
+
} else {
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const tokenOutAmountGross = fillOutputAmount > 0n ? fillOutputAmount : depositData?.outputAmount && depositData.outputAmount > 0n ? depositData.outputAmount : tokens.tokenOutAmountGross > 0n ? tokens.tokenOutAmountGross : tokens.tokenOutAmount;
|
|
788
|
+
const bridge = {
|
|
789
|
+
sourceChainId,
|
|
790
|
+
sourceTxHash,
|
|
791
|
+
destinationChainId,
|
|
792
|
+
destinationTxHash,
|
|
793
|
+
...tokens,
|
|
794
|
+
tokenIn: depositData?.tokenIn || tokens.tokenIn,
|
|
795
|
+
tokenInAmount: depositData?.inputAmount ?? tokens.tokenInAmount,
|
|
796
|
+
tokenOutAmountGross,
|
|
797
|
+
service: "across"
|
|
798
|
+
};
|
|
799
|
+
const payment = buildPaymentEvent(
|
|
800
|
+
yodlEvent,
|
|
801
|
+
extractEmbeddedParams(destTx.input),
|
|
802
|
+
toBlockTimestamp(destBlock),
|
|
803
|
+
sender
|
|
804
|
+
);
|
|
805
|
+
return { type: "bridge", ...payment, ...bridge };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/bridge-payment.ts
|
|
809
|
+
async function decodeBridgePayment(hash, clients, options = {}) {
|
|
810
|
+
const includeAcross = options.includeAcross ?? true;
|
|
811
|
+
let relayError;
|
|
812
|
+
try {
|
|
813
|
+
return await decodeRelayBridgePayment(hash, clients);
|
|
814
|
+
} catch (error) {
|
|
815
|
+
relayError = error;
|
|
816
|
+
if (!includeAcross) {
|
|
817
|
+
throw error;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
return await decodeAcrossBridgePayment(hash, clients, options.fillReceipt);
|
|
822
|
+
} catch (acrossError) {
|
|
823
|
+
if (!(relayError instanceof NoBridgeFoundError) && !(acrossError instanceof NoBridgeFoundError)) {
|
|
824
|
+
throw new Error("Both Relay and Across bridge decoders failed", {
|
|
825
|
+
cause: { relayError, acrossError }
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
if (relayError instanceof NoBridgeFoundError && acrossError instanceof NoBridgeFoundError) {
|
|
829
|
+
throw acrossError;
|
|
830
|
+
}
|
|
831
|
+
if (acrossError instanceof NoBridgeFoundError) {
|
|
832
|
+
throw relayError;
|
|
833
|
+
}
|
|
834
|
+
throw acrossError;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
555
838
|
// src/payment-decoder.ts
|
|
556
|
-
|
|
839
|
+
import { getRouter as getRouter3 } from "@yodlpay/tokenlists";
|
|
840
|
+
async function tryDecodeBridge(hash, clients, options = {}) {
|
|
557
841
|
try {
|
|
558
|
-
return await decodeBridgePayment(hash, clients);
|
|
842
|
+
return await decodeBridgePayment(hash, clients, options);
|
|
559
843
|
} catch (error) {
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
console.warn("Unexpected error checking bridge info:", error);
|
|
563
|
-
}
|
|
844
|
+
if (error instanceof NoBridgeFoundError) {
|
|
845
|
+
return void 0;
|
|
564
846
|
}
|
|
565
|
-
|
|
847
|
+
throw error;
|
|
566
848
|
}
|
|
567
849
|
}
|
|
568
|
-
async function decodePayment(hash,
|
|
569
|
-
const provider = getClient(clients,
|
|
850
|
+
async function decodePayment(hash, chainId2, clients, cachedReceipt) {
|
|
851
|
+
const provider = getClient(clients, chainId2);
|
|
570
852
|
const receipt = cachedReceipt ?? await provider.getTransactionReceipt({ hash });
|
|
571
|
-
const { address: routerAddress } =
|
|
853
|
+
const { address: routerAddress } = getRouter3(chainId2);
|
|
572
854
|
const [block, tx] = await Promise.all([
|
|
573
855
|
provider.getBlock({ blockNumber: receipt.blockNumber }),
|
|
574
856
|
provider.getTransaction({ hash })
|
|
@@ -583,8 +865,18 @@ async function decodePayment(hash, chainId, clients, cachedReceipt) {
|
|
|
583
865
|
if (swapEvent) {
|
|
584
866
|
return { type: "swap", ...paymentEvent, ...swapEvent };
|
|
585
867
|
}
|
|
586
|
-
const bridgeResult2 = await tryDecodeBridge(hash, clients
|
|
868
|
+
const bridgeResult2 = await tryDecodeBridge(hash, clients, {
|
|
869
|
+
fillReceipt: receipt
|
|
870
|
+
});
|
|
587
871
|
if (bridgeResult2) return bridgeResult2;
|
|
872
|
+
const inferredSwap = inferSwapFromTransfers(
|
|
873
|
+
swapLogs,
|
|
874
|
+
yodlEvent.sender,
|
|
875
|
+
yodlEvent.token
|
|
876
|
+
);
|
|
877
|
+
if (inferredSwap) {
|
|
878
|
+
return { type: "swap", ...paymentEvent, ...inferredSwap };
|
|
879
|
+
}
|
|
588
880
|
return { type: "direct", ...paymentEvent };
|
|
589
881
|
}
|
|
590
882
|
const bridgeResult = await tryDecodeBridge(hash, clients);
|
|
@@ -631,7 +923,7 @@ async function buildTokenInfo(params, clients) {
|
|
|
631
923
|
}
|
|
632
924
|
};
|
|
633
925
|
}
|
|
634
|
-
async function extractTokenInfo(paymentInfo,
|
|
926
|
+
async function extractTokenInfo(paymentInfo, chainId2, txHash2, clients) {
|
|
635
927
|
switch (paymentInfo.type) {
|
|
636
928
|
case "direct": {
|
|
637
929
|
return {
|
|
@@ -642,15 +934,15 @@ async function extractTokenInfo(paymentInfo, chainId, txHash, clients) {
|
|
|
642
934
|
tokenOut: paymentInfo.token,
|
|
643
935
|
tokenOutAmountGross: paymentInfo.amount,
|
|
644
936
|
tokenOutAmountNet: paymentInfo.amount,
|
|
645
|
-
inChainId:
|
|
646
|
-
outChainId:
|
|
937
|
+
inChainId: chainId2,
|
|
938
|
+
outChainId: chainId2
|
|
647
939
|
},
|
|
648
940
|
clients
|
|
649
941
|
),
|
|
650
|
-
sourceChainId:
|
|
651
|
-
sourceTxHash:
|
|
652
|
-
destinationChainId:
|
|
653
|
-
destinationTxHash:
|
|
942
|
+
sourceChainId: chainId2,
|
|
943
|
+
sourceTxHash: txHash2,
|
|
944
|
+
destinationChainId: chainId2,
|
|
945
|
+
destinationTxHash: txHash2,
|
|
654
946
|
solver: null
|
|
655
947
|
};
|
|
656
948
|
}
|
|
@@ -663,15 +955,15 @@ async function extractTokenInfo(paymentInfo, chainId, txHash, clients) {
|
|
|
663
955
|
tokenOut: paymentInfo.token,
|
|
664
956
|
tokenOutAmountGross: paymentInfo.tokenOutAmount,
|
|
665
957
|
tokenOutAmountNet: paymentInfo.amount,
|
|
666
|
-
inChainId:
|
|
667
|
-
outChainId:
|
|
958
|
+
inChainId: chainId2,
|
|
959
|
+
outChainId: chainId2
|
|
668
960
|
},
|
|
669
961
|
clients
|
|
670
962
|
),
|
|
671
|
-
sourceChainId:
|
|
672
|
-
sourceTxHash:
|
|
673
|
-
destinationChainId:
|
|
674
|
-
destinationTxHash:
|
|
963
|
+
sourceChainId: chainId2,
|
|
964
|
+
sourceTxHash: txHash2,
|
|
965
|
+
destinationChainId: chainId2,
|
|
966
|
+
destinationTxHash: txHash2,
|
|
675
967
|
solver: paymentInfo.service ?? null
|
|
676
968
|
};
|
|
677
969
|
}
|
|
@@ -698,10 +990,10 @@ async function extractTokenInfo(paymentInfo, chainId, txHash, clients) {
|
|
|
698
990
|
}
|
|
699
991
|
}
|
|
700
992
|
}
|
|
701
|
-
async function decodeYodlPayment(
|
|
993
|
+
async function decodeYodlPayment(txHash2, chainId2, clients, cachedReceipt) {
|
|
702
994
|
const paymentInfo = await decodePayment(
|
|
703
|
-
|
|
704
|
-
|
|
995
|
+
txHash2,
|
|
996
|
+
chainId2,
|
|
705
997
|
clients,
|
|
706
998
|
cachedReceipt
|
|
707
999
|
);
|
|
@@ -710,8 +1002,8 @@ async function decodeYodlPayment(txHash, chainId, clients, cachedReceipt) {
|
|
|
710
1002
|
const processorMemo = firstWebhook?.memo ?? "";
|
|
711
1003
|
const tokenInfo = await extractTokenInfo(
|
|
712
1004
|
paymentInfo,
|
|
713
|
-
|
|
714
|
-
|
|
1005
|
+
chainId2,
|
|
1006
|
+
txHash2,
|
|
715
1007
|
clients
|
|
716
1008
|
);
|
|
717
1009
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yodlpay/payment-decoder",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "Decode Yodl payment hashes into structured payment data",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"yodl",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"zod": "^4.3.6"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
+
"@across-protocol/app-sdk": "^0.5.0",
|
|
51
52
|
"@yodlpay/tokenlists": "^1.1.12"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|