@yodlpay/payment-decoder 1.3.0 → 1.3.2
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.js +176 -64
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -44,9 +44,11 @@ async function detectChain(hash, clients) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// src/decode-utils.ts
|
|
47
|
+
import { tokenlist } from "@yodlpay/tokenlists";
|
|
47
48
|
import {
|
|
48
49
|
decodeEventLog,
|
|
49
|
-
erc20Abi,
|
|
50
|
+
erc20Abi as erc20Abi2,
|
|
51
|
+
getAddress,
|
|
50
52
|
isAddressEqual
|
|
51
53
|
} from "viem";
|
|
52
54
|
|
|
@@ -113,7 +115,13 @@ function isExpectedDecodeError(error) {
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
// src/utils.ts
|
|
116
|
-
import {
|
|
118
|
+
import { getTokenByAddress } from "@yodlpay/tokenlists";
|
|
119
|
+
import {
|
|
120
|
+
erc20Abi,
|
|
121
|
+
getContract,
|
|
122
|
+
hexToString
|
|
123
|
+
} from "viem";
|
|
124
|
+
import { z } from "zod";
|
|
117
125
|
function decodeMemo(memo) {
|
|
118
126
|
if (!memo || memo === "0x") return "";
|
|
119
127
|
try {
|
|
@@ -122,6 +130,27 @@ function decodeMemo(memo) {
|
|
|
122
130
|
return "";
|
|
123
131
|
}
|
|
124
132
|
}
|
|
133
|
+
var erc20String = z.string().max(64).transform((s) => s.replace(/\p{Cc}/gu, ""));
|
|
134
|
+
var erc20Decimals = z.number().int().nonnegative().max(36);
|
|
135
|
+
async function resolveToken(address, chainId, client) {
|
|
136
|
+
try {
|
|
137
|
+
return getTokenByAddress(address, chainId);
|
|
138
|
+
} catch {
|
|
139
|
+
const token = getContract({ address, abi: erc20Abi, client });
|
|
140
|
+
const [name, symbol, decimals] = await Promise.all([
|
|
141
|
+
token.read.name(),
|
|
142
|
+
token.read.symbol(),
|
|
143
|
+
token.read.decimals()
|
|
144
|
+
]);
|
|
145
|
+
return {
|
|
146
|
+
chainId,
|
|
147
|
+
address,
|
|
148
|
+
name: erc20String.parse(name),
|
|
149
|
+
symbol: erc20String.parse(symbol),
|
|
150
|
+
decimals: erc20Decimals.parse(decimals)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
125
154
|
|
|
126
155
|
// src/decode-utils.ts
|
|
127
156
|
function* matchingEvents(logs, options) {
|
|
@@ -204,7 +233,7 @@ function decodeSwapFromLogs(logs) {
|
|
|
204
233
|
function extractTokenTransfers(logs) {
|
|
205
234
|
return collectEventsFromLogs(
|
|
206
235
|
logs,
|
|
207
|
-
{ abi:
|
|
236
|
+
{ abi: erc20Abi2, eventName: "Transfer", context: "ERC20 Transfer" },
|
|
208
237
|
(decoded, log) => {
|
|
209
238
|
const args = decoded.args;
|
|
210
239
|
return {
|
|
@@ -216,13 +245,71 @@ function extractTokenTransfers(logs) {
|
|
|
216
245
|
}
|
|
217
246
|
);
|
|
218
247
|
}
|
|
248
|
+
function isKnownToken(address) {
|
|
249
|
+
return tokenlist.some((t) => isAddressEqual(t.address, address));
|
|
250
|
+
}
|
|
251
|
+
function transferSignature(t) {
|
|
252
|
+
return `${getAddress(t.from)}:${getAddress(t.to)}:${t.amount}`;
|
|
253
|
+
}
|
|
254
|
+
function areMirrorTransfers(a, b) {
|
|
255
|
+
if (a.length !== b.length) return false;
|
|
256
|
+
const counts = /* @__PURE__ */ new Map();
|
|
257
|
+
for (const t of a) {
|
|
258
|
+
const key = transferSignature(t);
|
|
259
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
260
|
+
}
|
|
261
|
+
for (const t of b) {
|
|
262
|
+
const key = transferSignature(t);
|
|
263
|
+
const count = counts.get(key);
|
|
264
|
+
if (!count) return false;
|
|
265
|
+
if (count === 1) counts.delete(key);
|
|
266
|
+
else counts.set(key, count - 1);
|
|
267
|
+
}
|
|
268
|
+
return counts.size === 0;
|
|
269
|
+
}
|
|
270
|
+
function detectMirrorTokens(allTransfers) {
|
|
271
|
+
const mirrors = /* @__PURE__ */ new Map();
|
|
272
|
+
const byToken = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const t of allTransfers) {
|
|
274
|
+
const key = getAddress(t.token);
|
|
275
|
+
const group = byToken.get(key);
|
|
276
|
+
group ? group.push(t) : byToken.set(key, [t]);
|
|
277
|
+
}
|
|
278
|
+
const tokens = [...byToken.keys()];
|
|
279
|
+
for (const [i, tokenA] of tokens.entries()) {
|
|
280
|
+
if (mirrors.has(tokenA)) continue;
|
|
281
|
+
const transfersA = byToken.get(tokenA) ?? [];
|
|
282
|
+
for (const tokenB of tokens.slice(i + 1)) {
|
|
283
|
+
if (mirrors.has(tokenB)) continue;
|
|
284
|
+
const transfersB = byToken.get(tokenB) ?? [];
|
|
285
|
+
if (!areMirrorTransfers(transfersA, transfersB)) continue;
|
|
286
|
+
if (isKnownToken(tokenB) && !isKnownToken(tokenA)) {
|
|
287
|
+
mirrors.set(tokenA, tokenB);
|
|
288
|
+
} else {
|
|
289
|
+
mirrors.set(tokenB, tokenA);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return mirrors;
|
|
294
|
+
}
|
|
219
295
|
function findInputTransfer(transfers, sender) {
|
|
220
296
|
const fromSender = transfers.filter((t) => isAddressEqual(t.from, sender));
|
|
221
297
|
if (fromSender.length === 0) return void 0;
|
|
222
|
-
const
|
|
223
|
-
(max, t) => t.amount > max
|
|
298
|
+
const maxAmount = fromSender.reduce(
|
|
299
|
+
(max, t) => t.amount > max ? t.amount : max,
|
|
300
|
+
0n
|
|
224
301
|
);
|
|
225
|
-
|
|
302
|
+
let candidates = fromSender.filter((t) => t.amount === maxAmount);
|
|
303
|
+
if (candidates.length > 1) {
|
|
304
|
+
const mirrors = detectMirrorTokens(transfers);
|
|
305
|
+
const filtered = candidates.filter(
|
|
306
|
+
(t) => !mirrors.has(getAddress(t.token))
|
|
307
|
+
);
|
|
308
|
+
if (filtered.length > 0) candidates = filtered;
|
|
309
|
+
}
|
|
310
|
+
const result = candidates.find((t) => isKnownToken(t.token)) ?? candidates[0];
|
|
311
|
+
if (!result) return void 0;
|
|
312
|
+
return { token: result.token, amount: result.amount };
|
|
226
313
|
}
|
|
227
314
|
function toBlockTimestamp(block) {
|
|
228
315
|
return new Date(Number(block.timestamp) * 1e3);
|
|
@@ -245,20 +332,20 @@ import { decodeFunctionData, toFunctionSelector } from "viem";
|
|
|
245
332
|
// src/validation.ts
|
|
246
333
|
import { chains as chains2 } from "@yodlpay/tokenlists";
|
|
247
334
|
import { isAddress, isHex } from "viem";
|
|
248
|
-
import { z } from "zod";
|
|
335
|
+
import { z as z2 } from "zod";
|
|
249
336
|
var validChainIds = chains2.map((c) => c.id);
|
|
250
|
-
var txHashSchema =
|
|
251
|
-
var chainIdSchema =
|
|
337
|
+
var txHashSchema = z2.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash").transform((val) => val);
|
|
338
|
+
var chainIdSchema = z2.coerce.number().int().refine((id) => validChainIds.includes(id), {
|
|
252
339
|
message: `Chain ID must be one of: ${validChainIds.join(", ")}`
|
|
253
340
|
});
|
|
254
|
-
var ArgsSchema =
|
|
255
|
-
|
|
256
|
-
|
|
341
|
+
var ArgsSchema = z2.union([
|
|
342
|
+
z2.tuple([txHashSchema, chainIdSchema]),
|
|
343
|
+
z2.tuple([txHashSchema])
|
|
257
344
|
]);
|
|
258
|
-
var WebhooksSchema =
|
|
259
|
-
|
|
260
|
-
webhookAddress:
|
|
261
|
-
payload:
|
|
345
|
+
var WebhooksSchema = z2.array(
|
|
346
|
+
z2.object({
|
|
347
|
+
webhookAddress: z2.string().refine((val) => isAddress(val)),
|
|
348
|
+
payload: z2.array(z2.string().refine((val) => isHex(val)))
|
|
262
349
|
}).transform((webhook) => ({
|
|
263
350
|
...webhook,
|
|
264
351
|
memo: webhook.payload[0] ? decodeMemo(webhook.payload[0]) : ""
|
|
@@ -296,7 +383,7 @@ function extractEmbeddedParams(data) {
|
|
|
296
383
|
}
|
|
297
384
|
|
|
298
385
|
// src/relay-bridge.ts
|
|
299
|
-
import { getRouter
|
|
386
|
+
import { getRouter } from "@yodlpay/tokenlists";
|
|
300
387
|
import {
|
|
301
388
|
decodeEventLog as decodeEventLog2,
|
|
302
389
|
isAddressEqual as isAddressEqual2
|
|
@@ -330,15 +417,15 @@ async function fetchRelayRequest(hash) {
|
|
|
330
417
|
}
|
|
331
418
|
|
|
332
419
|
// src/relay-bridge.ts
|
|
333
|
-
function calculateOutputAmountGross(inputAmount, inputToken, inputChainId, outputToken, outputChainId) {
|
|
334
|
-
const { decimals: inputDecimals } =
|
|
335
|
-
inputToken,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
);
|
|
420
|
+
async function calculateOutputAmountGross(inputAmount, inputToken, inputChainId, outputToken, outputChainId, clients) {
|
|
421
|
+
const [{ decimals: inputDecimals }, { decimals: outputDecimals }] = await Promise.all([
|
|
422
|
+
resolveToken(inputToken, inputChainId, getClient(clients, inputChainId)),
|
|
423
|
+
resolveToken(
|
|
424
|
+
outputToken,
|
|
425
|
+
outputChainId,
|
|
426
|
+
getClient(clients, outputChainId)
|
|
427
|
+
)
|
|
428
|
+
]);
|
|
342
429
|
const decimalDiff = inputDecimals - outputDecimals;
|
|
343
430
|
if (decimalDiff === 0) return inputAmount;
|
|
344
431
|
if (decimalDiff > 0) return inputAmount / 10n ** BigInt(decimalDiff);
|
|
@@ -362,7 +449,7 @@ async function extractSenderFromSource(sourceReceipt, sourceProvider, sourceHash
|
|
|
362
449
|
const sourceTx = await sourceProvider.getTransaction({ hash: sourceHash });
|
|
363
450
|
return sourceTx.from;
|
|
364
451
|
}
|
|
365
|
-
function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inputAmountGross, inputChainId, outputChainId) {
|
|
452
|
+
async function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inputAmountGross, inputChainId, outputChainId, clients) {
|
|
366
453
|
const sourceTransfers = extractTokenTransfers(sourceReceipt.logs);
|
|
367
454
|
const inputTransfer = findInputTransfer(sourceTransfers, sender);
|
|
368
455
|
const tokenIn = inputTransfer?.token;
|
|
@@ -379,12 +466,13 @@ function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inpu
|
|
|
379
466
|
if (swapEvent !== void 0) {
|
|
380
467
|
tokenOutAmountGross = swapEvent.tokenOutAmount;
|
|
381
468
|
} else if (inputAmountGross > 0n && tokenIn && tokenOut) {
|
|
382
|
-
tokenOutAmountGross = calculateOutputAmountGross(
|
|
469
|
+
tokenOutAmountGross = await calculateOutputAmountGross(
|
|
383
470
|
inputAmountGross,
|
|
384
471
|
tokenIn,
|
|
385
472
|
inputChainId,
|
|
386
473
|
tokenOut,
|
|
387
|
-
outputChainId
|
|
474
|
+
outputChainId,
|
|
475
|
+
clients
|
|
388
476
|
);
|
|
389
477
|
}
|
|
390
478
|
return {
|
|
@@ -437,14 +525,15 @@ async function decodeBridgePayment(hash, clients) {
|
|
|
437
525
|
destProvider.getTransaction({ hash: destinationTxHash }),
|
|
438
526
|
extractSenderFromSource(sourceReceipt, sourceProvider, sourceTxHash)
|
|
439
527
|
]);
|
|
440
|
-
const tokens = resolveBridgeTokens(
|
|
528
|
+
const tokens = await resolveBridgeTokens(
|
|
441
529
|
sourceReceipt,
|
|
442
530
|
destReceipt,
|
|
443
531
|
yodlEvent,
|
|
444
532
|
sender,
|
|
445
533
|
inputAmountGross,
|
|
446
534
|
sourceChainId,
|
|
447
|
-
destinationChainId
|
|
535
|
+
destinationChainId,
|
|
536
|
+
clients
|
|
448
537
|
);
|
|
449
538
|
const bridge = {
|
|
450
539
|
sourceChainId,
|
|
@@ -504,14 +593,23 @@ async function decodePayment(hash, chainId, clients, cachedReceipt) {
|
|
|
504
593
|
}
|
|
505
594
|
|
|
506
595
|
// src/yodl-payment.ts
|
|
507
|
-
import { getTokenByAddress as getTokenByAddress2 } from "@yodlpay/tokenlists";
|
|
508
596
|
import {
|
|
509
597
|
formatUnits,
|
|
510
598
|
zeroAddress
|
|
511
599
|
} from "viem";
|
|
512
|
-
function buildTokenInfo(params) {
|
|
513
|
-
const inToken =
|
|
514
|
-
|
|
600
|
+
async function buildTokenInfo(params, clients) {
|
|
601
|
+
const [inToken, outToken] = await Promise.all([
|
|
602
|
+
resolveToken(
|
|
603
|
+
params.tokenIn,
|
|
604
|
+
params.inChainId,
|
|
605
|
+
getClient(clients, params.inChainId)
|
|
606
|
+
),
|
|
607
|
+
resolveToken(
|
|
608
|
+
params.tokenOut,
|
|
609
|
+
params.outChainId,
|
|
610
|
+
getClient(clients, params.outChainId)
|
|
611
|
+
)
|
|
612
|
+
]);
|
|
515
613
|
return {
|
|
516
614
|
tokenIn: {
|
|
517
615
|
...inToken,
|
|
@@ -533,19 +631,22 @@ function buildTokenInfo(params) {
|
|
|
533
631
|
}
|
|
534
632
|
};
|
|
535
633
|
}
|
|
536
|
-
function extractTokenInfo(paymentInfo, chainId, txHash) {
|
|
634
|
+
async function extractTokenInfo(paymentInfo, chainId, txHash, clients) {
|
|
537
635
|
switch (paymentInfo.type) {
|
|
538
636
|
case "direct": {
|
|
539
637
|
return {
|
|
540
|
-
...buildTokenInfo(
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
638
|
+
...await buildTokenInfo(
|
|
639
|
+
{
|
|
640
|
+
tokenIn: paymentInfo.token,
|
|
641
|
+
tokenInAmount: paymentInfo.amount,
|
|
642
|
+
tokenOut: paymentInfo.token,
|
|
643
|
+
tokenOutAmountGross: paymentInfo.amount,
|
|
644
|
+
tokenOutAmountNet: paymentInfo.amount,
|
|
645
|
+
inChainId: chainId,
|
|
646
|
+
outChainId: chainId
|
|
647
|
+
},
|
|
648
|
+
clients
|
|
649
|
+
),
|
|
549
650
|
sourceChainId: chainId,
|
|
550
651
|
sourceTxHash: txHash,
|
|
551
652
|
destinationChainId: chainId,
|
|
@@ -555,15 +656,18 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
|
|
|
555
656
|
}
|
|
556
657
|
case "swap": {
|
|
557
658
|
return {
|
|
558
|
-
...buildTokenInfo(
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
659
|
+
...await buildTokenInfo(
|
|
660
|
+
{
|
|
661
|
+
tokenIn: paymentInfo.tokenIn,
|
|
662
|
+
tokenInAmount: paymentInfo.tokenInAmount,
|
|
663
|
+
tokenOut: paymentInfo.token,
|
|
664
|
+
tokenOutAmountGross: paymentInfo.tokenOutAmount,
|
|
665
|
+
tokenOutAmountNet: paymentInfo.amount,
|
|
666
|
+
inChainId: chainId,
|
|
667
|
+
outChainId: chainId
|
|
668
|
+
},
|
|
669
|
+
clients
|
|
670
|
+
),
|
|
567
671
|
sourceChainId: chainId,
|
|
568
672
|
sourceTxHash: txHash,
|
|
569
673
|
destinationChainId: chainId,
|
|
@@ -573,15 +677,18 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
|
|
|
573
677
|
}
|
|
574
678
|
case "bridge": {
|
|
575
679
|
return {
|
|
576
|
-
...buildTokenInfo(
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
680
|
+
...await buildTokenInfo(
|
|
681
|
+
{
|
|
682
|
+
tokenIn: paymentInfo.tokenIn,
|
|
683
|
+
tokenInAmount: paymentInfo.tokenInAmount,
|
|
684
|
+
tokenOut: paymentInfo.tokenOut,
|
|
685
|
+
tokenOutAmountGross: paymentInfo.tokenOutAmountGross,
|
|
686
|
+
tokenOutAmountNet: paymentInfo.amount,
|
|
687
|
+
inChainId: paymentInfo.sourceChainId,
|
|
688
|
+
outChainId: paymentInfo.destinationChainId
|
|
689
|
+
},
|
|
690
|
+
clients
|
|
691
|
+
),
|
|
585
692
|
sourceChainId: paymentInfo.sourceChainId,
|
|
586
693
|
sourceTxHash: paymentInfo.sourceTxHash,
|
|
587
694
|
destinationChainId: paymentInfo.destinationChainId,
|
|
@@ -601,7 +708,12 @@ async function decodeYodlPayment(txHash, chainId, clients, cachedReceipt) {
|
|
|
601
708
|
const firstWebhook = paymentInfo.webhooks[0];
|
|
602
709
|
const processorAddress = firstWebhook?.webhookAddress ?? zeroAddress;
|
|
603
710
|
const processorMemo = firstWebhook?.memo ?? "";
|
|
604
|
-
const tokenInfo = extractTokenInfo(
|
|
711
|
+
const tokenInfo = await extractTokenInfo(
|
|
712
|
+
paymentInfo,
|
|
713
|
+
chainId,
|
|
714
|
+
txHash,
|
|
715
|
+
clients
|
|
716
|
+
);
|
|
605
717
|
return {
|
|
606
718
|
senderAddress: paymentInfo.sender,
|
|
607
719
|
receiverAddress: paymentInfo.receiver,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yodlpay/payment-decoder",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
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.
|
|
42
|
-
"@relayprotocol/relay-sdk": "^5.
|
|
41
|
+
"@biomejs/biome": "^2.4.4",
|
|
42
|
+
"@relayprotocol/relay-sdk": "^5.2.0",
|
|
43
43
|
"@semantic-release/changelog": "^6.0.3",
|
|
44
44
|
"@semantic-release/git": "^10.0.1",
|
|
45
45
|
"@types/bun": "latest",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"zod": "^4.3.6"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@yodlpay/tokenlists": "^1.1.
|
|
51
|
+
"@yodlpay/tokenlists": "^1.1.12"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"typescript": "^5",
|