@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 +11 -4
- package/dist/index.js +289 -284
- package/package.json +4 -4
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-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
178
|
-
var erc20Decimals =
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 {
|
|
375
|
-
import {
|
|
376
|
-
|
|
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 =
|
|
379
|
-
var chainIdSchema =
|
|
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 =
|
|
383
|
-
|
|
384
|
-
|
|
278
|
+
var ArgsSchema = z2.union([
|
|
279
|
+
z2.tuple([txHashSchema, chainIdSchema]),
|
|
280
|
+
z2.tuple([txHashSchema])
|
|
385
281
|
]);
|
|
386
|
-
var WebhooksSchema =
|
|
387
|
-
|
|
388
|
-
webhookAddress:
|
|
389
|
-
payload:
|
|
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/
|
|
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
|
-
|
|
430
|
-
|
|
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
|
|
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
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
+
"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.
|
|
42
|
-
"@relayprotocol/relay-sdk": "^5.2.
|
|
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.
|
|
52
|
+
"@yodlpay/tokenlists": "^1.1.13"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"typescript": "^5",
|