@yodlpay/payment-decoder 1.3.1 → 1.3.3
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 +10 -13
- package/dist/index.js +356 -120
- 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[];
|
|
@@ -75,6 +75,15 @@ interface YodlPayment {
|
|
|
75
75
|
blockTimestamp: Date;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
interface DecodeBridgeOptions {
|
|
79
|
+
includeAcross?: boolean;
|
|
80
|
+
/** Receipt of the fill (destination) tx, used for Across fill-side lookups. */
|
|
81
|
+
fillReceipt?: TransactionReceipt;
|
|
82
|
+
}
|
|
83
|
+
declare function decodeBridgePayment(hash: Hex, clients: ChainClients, options?: DecodeBridgeOptions): Promise<Extract<PaymentInfo, {
|
|
84
|
+
type: "bridge";
|
|
85
|
+
}>>;
|
|
86
|
+
|
|
78
87
|
/**
|
|
79
88
|
* Decoded Yodl event data from onchain logs.
|
|
80
89
|
* Does not include blockTimestamp or webhooks — callers provide those.
|
|
@@ -102,18 +111,6 @@ declare class NoYodlEventError extends Error {
|
|
|
102
111
|
|
|
103
112
|
declare function decodePayment(hash: Hex, chainId: number, clients: ChainClients, cachedReceipt?: TransactionReceipt): Promise<PaymentInfo>;
|
|
104
113
|
|
|
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
114
|
declare function decodeYodlPayment(txHash: Hex, chainId: number, clients: ChainClients, cachedReceipt?: TransactionReceipt): Promise<YodlPayment>;
|
|
118
115
|
|
|
119
116
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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;
|
|
25
36
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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");
|
|
42
65
|
}
|
|
43
|
-
return
|
|
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);
|
|
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),
|
|
@@ -269,7 +313,12 @@ function areMirrorTransfers(a, b) {
|
|
|
269
313
|
}
|
|
270
314
|
function detectMirrorTokens(allTransfers) {
|
|
271
315
|
const mirrors = /* @__PURE__ */ new Map();
|
|
272
|
-
const byToken =
|
|
316
|
+
const byToken = /* @__PURE__ */ new Map();
|
|
317
|
+
for (const t of allTransfers) {
|
|
318
|
+
const key = getAddress(t.token);
|
|
319
|
+
const group = byToken.get(key);
|
|
320
|
+
group ? group.push(t) : byToken.set(key, [t]);
|
|
321
|
+
}
|
|
273
322
|
const tokens = [...byToken.keys()];
|
|
274
323
|
for (const [i, tokenA] of tokens.entries()) {
|
|
275
324
|
if (mirrors.has(tokenA)) continue;
|
|
@@ -318,29 +367,26 @@ function buildPaymentEvent(yodlEvent, webhooks, blockTimestamp, senderOverride)
|
|
|
318
367
|
};
|
|
319
368
|
}
|
|
320
369
|
|
|
321
|
-
// src/payment-decoder.ts
|
|
322
|
-
import { getRouter as getRouter2 } from "@yodlpay/tokenlists";
|
|
323
|
-
|
|
324
370
|
// src/embedded-params.ts
|
|
325
371
|
import { decodeFunctionData, toFunctionSelector } from "viem";
|
|
326
372
|
|
|
327
373
|
// src/validation.ts
|
|
328
|
-
import { chains
|
|
374
|
+
import { chains } from "@yodlpay/tokenlists";
|
|
329
375
|
import { isAddress, isHex } from "viem";
|
|
330
|
-
import { z as
|
|
331
|
-
var validChainIds =
|
|
332
|
-
var txHashSchema =
|
|
333
|
-
var chainIdSchema =
|
|
376
|
+
import { z as z3 } from "zod";
|
|
377
|
+
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), {
|
|
334
380
|
message: `Chain ID must be one of: ${validChainIds.join(", ")}`
|
|
335
381
|
});
|
|
336
|
-
var ArgsSchema =
|
|
337
|
-
|
|
338
|
-
|
|
382
|
+
var ArgsSchema = z3.union([
|
|
383
|
+
z3.tuple([txHashSchema, chainIdSchema]),
|
|
384
|
+
z3.tuple([txHashSchema])
|
|
339
385
|
]);
|
|
340
|
-
var WebhooksSchema =
|
|
341
|
-
|
|
342
|
-
webhookAddress:
|
|
343
|
-
payload:
|
|
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)))
|
|
344
390
|
}).transform((webhook) => ({
|
|
345
391
|
...webhook,
|
|
346
392
|
memo: webhook.payload[0] ? decodeMemo(webhook.payload[0]) : ""
|
|
@@ -385,6 +431,53 @@ import {
|
|
|
385
431
|
} from "viem";
|
|
386
432
|
import { entryPoint08Address } from "viem/account-abstraction";
|
|
387
433
|
|
|
434
|
+
// src/clients.ts
|
|
435
|
+
import { chains as chains2 } from "@yodlpay/tokenlists";
|
|
436
|
+
import {
|
|
437
|
+
createPublicClient,
|
|
438
|
+
http
|
|
439
|
+
} from "viem";
|
|
440
|
+
var rpcOverrides = {
|
|
441
|
+
1: "https://ethereum-rpc.publicnode.com",
|
|
442
|
+
// eth.merkle.io is down
|
|
443
|
+
137: "https://polygon-bor-rpc.publicnode.com"
|
|
444
|
+
// polygon-rpc.com is down
|
|
445
|
+
};
|
|
446
|
+
function createClients() {
|
|
447
|
+
const clients = {};
|
|
448
|
+
for (const chain of chains2) {
|
|
449
|
+
clients[chain.id] = createPublicClient({
|
|
450
|
+
chain,
|
|
451
|
+
transport: http(rpcOverrides[chain.id])
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return clients;
|
|
455
|
+
}
|
|
456
|
+
function getClient(clients, chainId2) {
|
|
457
|
+
const client = clients[chainId2];
|
|
458
|
+
if (!client) throw new Error(`No client configured for chain ${chainId2}`);
|
|
459
|
+
return client;
|
|
460
|
+
}
|
|
461
|
+
async function detectChain(hash, clients) {
|
|
462
|
+
const entries = Object.entries(clients).map(([id, client]) => ({
|
|
463
|
+
chainId: Number(id),
|
|
464
|
+
client
|
|
465
|
+
}));
|
|
466
|
+
const results = await Promise.allSettled(
|
|
467
|
+
entries.map(async ({ chainId: chainId2, client }) => {
|
|
468
|
+
const receipt = await client.getTransactionReceipt({ hash });
|
|
469
|
+
return { chainId: chainId2, receipt };
|
|
470
|
+
})
|
|
471
|
+
);
|
|
472
|
+
const found = results.find(
|
|
473
|
+
(r) => r.status === "fulfilled"
|
|
474
|
+
);
|
|
475
|
+
if (!found) {
|
|
476
|
+
throw new Error(`Transaction ${hash} not found on any configured chain`);
|
|
477
|
+
}
|
|
478
|
+
return found.value;
|
|
479
|
+
}
|
|
480
|
+
|
|
388
481
|
// src/relay-client.ts
|
|
389
482
|
var MAINNET_RELAY_API = "https://api.relay.link";
|
|
390
483
|
var TESTNET_RELAY_API = "https://api.testnets.relay.link";
|
|
@@ -493,7 +586,7 @@ function parseRelayResponse(request) {
|
|
|
493
586
|
inputAmountGross: BigInt(data?.metadata?.currencyIn?.amount ?? 0)
|
|
494
587
|
};
|
|
495
588
|
}
|
|
496
|
-
async function
|
|
589
|
+
async function decodeRelayBridgePayment(hash, clients) {
|
|
497
590
|
const request = await fetchRelayRequest(hash);
|
|
498
591
|
const {
|
|
499
592
|
sourceChainId,
|
|
@@ -502,8 +595,11 @@ async function decodeBridgePayment(hash, clients) {
|
|
|
502
595
|
destinationTxHash,
|
|
503
596
|
inputAmountGross
|
|
504
597
|
} = parseRelayResponse(request);
|
|
505
|
-
const destProvider =
|
|
506
|
-
const sourceProvider =
|
|
598
|
+
const destProvider = clients[destinationChainId];
|
|
599
|
+
const sourceProvider = clients[sourceChainId];
|
|
600
|
+
if (!destProvider || !sourceProvider) {
|
|
601
|
+
throw new NoBridgeFoundError();
|
|
602
|
+
}
|
|
507
603
|
const { address: routerAddress } = getRouter(destinationChainId);
|
|
508
604
|
const [destReceipt, sourceReceipt] = await Promise.all([
|
|
509
605
|
destProvider.getTransactionReceipt({ hash: destinationTxHash }),
|
|
@@ -547,23 +643,161 @@ async function decodeBridgePayment(hash, clients) {
|
|
|
547
643
|
return { type: "bridge", ...payment, ...bridge };
|
|
548
644
|
}
|
|
549
645
|
|
|
646
|
+
// src/across-bridge.ts
|
|
647
|
+
async function resolveAcrossStatus(hash, fillReceipt) {
|
|
648
|
+
try {
|
|
649
|
+
return await fetchAcrossDepositByTx(hash);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
if (!(error instanceof NoBridgeFoundError) || !fillReceipt) {
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const fillLog = parseFillLogs(fillReceipt.logs);
|
|
656
|
+
if (!fillLog) {
|
|
657
|
+
throw new NoBridgeFoundError();
|
|
658
|
+
}
|
|
659
|
+
const status = await fetchAcrossDepositByDepositId(
|
|
660
|
+
Number(fillLog.originChainId),
|
|
661
|
+
String(fillLog.depositId)
|
|
662
|
+
);
|
|
663
|
+
if (status.fillTxnRef !== hash) {
|
|
664
|
+
throw new NoBridgeFoundError();
|
|
665
|
+
}
|
|
666
|
+
return status;
|
|
667
|
+
}
|
|
668
|
+
async function decodeAcrossBridgePayment(hash, clients, fillReceipt) {
|
|
669
|
+
const status = await resolveAcrossStatus(hash, fillReceipt);
|
|
670
|
+
if (status.status !== "filled" || !status.fillTxnRef) {
|
|
671
|
+
throw new NoBridgeFoundError();
|
|
672
|
+
}
|
|
673
|
+
if (status.actionsSucceeded === false) {
|
|
674
|
+
throw new NoBridgeFoundError();
|
|
675
|
+
}
|
|
676
|
+
const sourceChainId = status.originChainId;
|
|
677
|
+
const sourceTxHash = status.depositTxnRef;
|
|
678
|
+
const destinationChainId = status.destinationChainId;
|
|
679
|
+
const destinationTxHash = status.fillTxnRef;
|
|
680
|
+
const sourceProvider = clients[sourceChainId];
|
|
681
|
+
const destProvider = clients[destinationChainId];
|
|
682
|
+
if (!sourceProvider || !destProvider) {
|
|
683
|
+
throw new NoBridgeFoundError();
|
|
684
|
+
}
|
|
685
|
+
const { address: routerAddress } = getRouter2(destinationChainId);
|
|
686
|
+
const [sourceReceipt, destReceipt] = await Promise.all([
|
|
687
|
+
sourceProvider.getTransactionReceipt({ hash: sourceTxHash }),
|
|
688
|
+
destProvider.getTransactionReceipt({ hash: destinationTxHash })
|
|
689
|
+
]);
|
|
690
|
+
const yodlEvent = decodeYodlFromLogs(destReceipt.logs, routerAddress);
|
|
691
|
+
if (!yodlEvent) {
|
|
692
|
+
throw new NoBridgeFoundError();
|
|
693
|
+
}
|
|
694
|
+
const [destBlock, destTx, sender] = await Promise.all([
|
|
695
|
+
destProvider.getBlock({ blockNumber: destReceipt.blockNumber }),
|
|
696
|
+
destProvider.getTransaction({ hash: destinationTxHash }),
|
|
697
|
+
extractSenderFromSource(sourceReceipt, sourceProvider, sourceTxHash)
|
|
698
|
+
]);
|
|
699
|
+
const depositLog = parseDepositLogs(sourceReceipt.logs);
|
|
700
|
+
const depositData = depositLog && depositLog.depositId === BigInt(status.depositId) ? {
|
|
701
|
+
tokenIn: depositLog.inputToken,
|
|
702
|
+
inputAmount: depositLog.inputAmount,
|
|
703
|
+
outputAmount: depositLog.outputAmount
|
|
704
|
+
} : void 0;
|
|
705
|
+
const fillLog = parseFillLogs(destReceipt.logs, {
|
|
706
|
+
depositId: BigInt(status.depositId)
|
|
707
|
+
});
|
|
708
|
+
const fillOutputAmount = fillLog?.relayExecutionInfo?.updatedOutputAmount ?? fillLog?.outputAmount ?? 0n;
|
|
709
|
+
let tokens;
|
|
710
|
+
try {
|
|
711
|
+
tokens = await resolveBridgeTokens(
|
|
712
|
+
sourceReceipt,
|
|
713
|
+
destReceipt,
|
|
714
|
+
yodlEvent,
|
|
715
|
+
sender,
|
|
716
|
+
0n,
|
|
717
|
+
sourceChainId,
|
|
718
|
+
destinationChainId,
|
|
719
|
+
clients
|
|
720
|
+
);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
if (error instanceof ExpectedDecodeError && depositData?.tokenIn && depositData.inputAmount > 0n) {
|
|
723
|
+
tokens = {
|
|
724
|
+
tokenIn: depositData.tokenIn,
|
|
725
|
+
tokenInAmount: depositData.inputAmount,
|
|
726
|
+
tokenOut: yodlEvent.token,
|
|
727
|
+
tokenOutAmount: yodlEvent.amount,
|
|
728
|
+
tokenOutAmountGross: fillOutputAmount > 0n ? fillOutputAmount : depositData.outputAmount > 0n ? depositData.outputAmount : yodlEvent.amount
|
|
729
|
+
};
|
|
730
|
+
} else {
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const tokenOutAmountGross = fillOutputAmount > 0n ? fillOutputAmount : depositData?.outputAmount && depositData.outputAmount > 0n ? depositData.outputAmount : tokens.tokenOutAmountGross > 0n ? tokens.tokenOutAmountGross : tokens.tokenOutAmount;
|
|
735
|
+
const bridge = {
|
|
736
|
+
sourceChainId,
|
|
737
|
+
sourceTxHash,
|
|
738
|
+
destinationChainId,
|
|
739
|
+
destinationTxHash,
|
|
740
|
+
...tokens,
|
|
741
|
+
tokenIn: depositData?.tokenIn || tokens.tokenIn,
|
|
742
|
+
tokenInAmount: depositData?.inputAmount ?? tokens.tokenInAmount,
|
|
743
|
+
tokenOutAmountGross,
|
|
744
|
+
service: "across"
|
|
745
|
+
};
|
|
746
|
+
const payment = buildPaymentEvent(
|
|
747
|
+
yodlEvent,
|
|
748
|
+
extractEmbeddedParams(destTx.input),
|
|
749
|
+
toBlockTimestamp(destBlock),
|
|
750
|
+
sender
|
|
751
|
+
);
|
|
752
|
+
return { type: "bridge", ...payment, ...bridge };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/bridge-payment.ts
|
|
756
|
+
async function decodeBridgePayment(hash, clients, options = {}) {
|
|
757
|
+
const includeAcross = options.includeAcross ?? true;
|
|
758
|
+
let relayError;
|
|
759
|
+
try {
|
|
760
|
+
return await decodeRelayBridgePayment(hash, clients);
|
|
761
|
+
} catch (error) {
|
|
762
|
+
relayError = error;
|
|
763
|
+
if (!includeAcross) {
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
return await decodeAcrossBridgePayment(hash, clients, options.fillReceipt);
|
|
769
|
+
} catch (acrossError) {
|
|
770
|
+
if (!(relayError instanceof NoBridgeFoundError) && !(acrossError instanceof NoBridgeFoundError)) {
|
|
771
|
+
throw new Error("Both Relay and Across bridge decoders failed", {
|
|
772
|
+
cause: { relayError, acrossError }
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
if (relayError instanceof NoBridgeFoundError && acrossError instanceof NoBridgeFoundError) {
|
|
776
|
+
throw acrossError;
|
|
777
|
+
}
|
|
778
|
+
if (acrossError instanceof NoBridgeFoundError) {
|
|
779
|
+
throw relayError;
|
|
780
|
+
}
|
|
781
|
+
throw acrossError;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
550
785
|
// src/payment-decoder.ts
|
|
551
|
-
|
|
786
|
+
import { getRouter as getRouter3 } from "@yodlpay/tokenlists";
|
|
787
|
+
async function tryDecodeBridge(hash, clients, options = {}) {
|
|
552
788
|
try {
|
|
553
|
-
return await decodeBridgePayment(hash, clients);
|
|
789
|
+
return await decodeBridgePayment(hash, clients, options);
|
|
554
790
|
} catch (error) {
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
console.warn("Unexpected error checking bridge info:", error);
|
|
558
|
-
}
|
|
791
|
+
if (error instanceof NoBridgeFoundError) {
|
|
792
|
+
return void 0;
|
|
559
793
|
}
|
|
560
|
-
|
|
794
|
+
throw error;
|
|
561
795
|
}
|
|
562
796
|
}
|
|
563
|
-
async function decodePayment(hash,
|
|
564
|
-
const provider = getClient(clients,
|
|
797
|
+
async function decodePayment(hash, chainId2, clients, cachedReceipt) {
|
|
798
|
+
const provider = getClient(clients, chainId2);
|
|
565
799
|
const receipt = cachedReceipt ?? await provider.getTransactionReceipt({ hash });
|
|
566
|
-
const { address: routerAddress } =
|
|
800
|
+
const { address: routerAddress } = getRouter3(chainId2);
|
|
567
801
|
const [block, tx] = await Promise.all([
|
|
568
802
|
provider.getBlock({ blockNumber: receipt.blockNumber }),
|
|
569
803
|
provider.getTransaction({ hash })
|
|
@@ -578,7 +812,9 @@ async function decodePayment(hash, chainId, clients, cachedReceipt) {
|
|
|
578
812
|
if (swapEvent) {
|
|
579
813
|
return { type: "swap", ...paymentEvent, ...swapEvent };
|
|
580
814
|
}
|
|
581
|
-
const bridgeResult2 = await tryDecodeBridge(hash, clients
|
|
815
|
+
const bridgeResult2 = await tryDecodeBridge(hash, clients, {
|
|
816
|
+
fillReceipt: receipt
|
|
817
|
+
});
|
|
582
818
|
if (bridgeResult2) return bridgeResult2;
|
|
583
819
|
return { type: "direct", ...paymentEvent };
|
|
584
820
|
}
|
|
@@ -626,7 +862,7 @@ async function buildTokenInfo(params, clients) {
|
|
|
626
862
|
}
|
|
627
863
|
};
|
|
628
864
|
}
|
|
629
|
-
async function extractTokenInfo(paymentInfo,
|
|
865
|
+
async function extractTokenInfo(paymentInfo, chainId2, txHash2, clients) {
|
|
630
866
|
switch (paymentInfo.type) {
|
|
631
867
|
case "direct": {
|
|
632
868
|
return {
|
|
@@ -637,15 +873,15 @@ async function extractTokenInfo(paymentInfo, chainId, txHash, clients) {
|
|
|
637
873
|
tokenOut: paymentInfo.token,
|
|
638
874
|
tokenOutAmountGross: paymentInfo.amount,
|
|
639
875
|
tokenOutAmountNet: paymentInfo.amount,
|
|
640
|
-
inChainId:
|
|
641
|
-
outChainId:
|
|
876
|
+
inChainId: chainId2,
|
|
877
|
+
outChainId: chainId2
|
|
642
878
|
},
|
|
643
879
|
clients
|
|
644
880
|
),
|
|
645
|
-
sourceChainId:
|
|
646
|
-
sourceTxHash:
|
|
647
|
-
destinationChainId:
|
|
648
|
-
destinationTxHash:
|
|
881
|
+
sourceChainId: chainId2,
|
|
882
|
+
sourceTxHash: txHash2,
|
|
883
|
+
destinationChainId: chainId2,
|
|
884
|
+
destinationTxHash: txHash2,
|
|
649
885
|
solver: null
|
|
650
886
|
};
|
|
651
887
|
}
|
|
@@ -658,15 +894,15 @@ async function extractTokenInfo(paymentInfo, chainId, txHash, clients) {
|
|
|
658
894
|
tokenOut: paymentInfo.token,
|
|
659
895
|
tokenOutAmountGross: paymentInfo.tokenOutAmount,
|
|
660
896
|
tokenOutAmountNet: paymentInfo.amount,
|
|
661
|
-
inChainId:
|
|
662
|
-
outChainId:
|
|
897
|
+
inChainId: chainId2,
|
|
898
|
+
outChainId: chainId2
|
|
663
899
|
},
|
|
664
900
|
clients
|
|
665
901
|
),
|
|
666
|
-
sourceChainId:
|
|
667
|
-
sourceTxHash:
|
|
668
|
-
destinationChainId:
|
|
669
|
-
destinationTxHash:
|
|
902
|
+
sourceChainId: chainId2,
|
|
903
|
+
sourceTxHash: txHash2,
|
|
904
|
+
destinationChainId: chainId2,
|
|
905
|
+
destinationTxHash: txHash2,
|
|
670
906
|
solver: paymentInfo.service ?? null
|
|
671
907
|
};
|
|
672
908
|
}
|
|
@@ -693,10 +929,10 @@ async function extractTokenInfo(paymentInfo, chainId, txHash, clients) {
|
|
|
693
929
|
}
|
|
694
930
|
}
|
|
695
931
|
}
|
|
696
|
-
async function decodeYodlPayment(
|
|
932
|
+
async function decodeYodlPayment(txHash2, chainId2, clients, cachedReceipt) {
|
|
697
933
|
const paymentInfo = await decodePayment(
|
|
698
|
-
|
|
699
|
-
|
|
934
|
+
txHash2,
|
|
935
|
+
chainId2,
|
|
700
936
|
clients,
|
|
701
937
|
cachedReceipt
|
|
702
938
|
);
|
|
@@ -705,8 +941,8 @@ async function decodeYodlPayment(txHash, chainId, clients, cachedReceipt) {
|
|
|
705
941
|
const processorMemo = firstWebhook?.memo ?? "";
|
|
706
942
|
const tokenInfo = await extractTokenInfo(
|
|
707
943
|
paymentInfo,
|
|
708
|
-
|
|
709
|
-
|
|
944
|
+
chainId2,
|
|
945
|
+
txHash2,
|
|
710
946
|
clients
|
|
711
947
|
);
|
|
712
948
|
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.3",
|
|
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": {
|