@yodlpay/payment-decoder 1.3.0 → 1.3.1
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 +171 -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,66 @@ 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 = Map.groupBy(allTransfers, (t) => getAddress(t.token));
|
|
273
|
+
const tokens = [...byToken.keys()];
|
|
274
|
+
for (const [i, tokenA] of tokens.entries()) {
|
|
275
|
+
if (mirrors.has(tokenA)) continue;
|
|
276
|
+
const transfersA = byToken.get(tokenA) ?? [];
|
|
277
|
+
for (const tokenB of tokens.slice(i + 1)) {
|
|
278
|
+
if (mirrors.has(tokenB)) continue;
|
|
279
|
+
const transfersB = byToken.get(tokenB) ?? [];
|
|
280
|
+
if (!areMirrorTransfers(transfersA, transfersB)) continue;
|
|
281
|
+
if (isKnownToken(tokenB) && !isKnownToken(tokenA)) {
|
|
282
|
+
mirrors.set(tokenA, tokenB);
|
|
283
|
+
} else {
|
|
284
|
+
mirrors.set(tokenB, tokenA);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return mirrors;
|
|
289
|
+
}
|
|
219
290
|
function findInputTransfer(transfers, sender) {
|
|
220
291
|
const fromSender = transfers.filter((t) => isAddressEqual(t.from, sender));
|
|
221
292
|
if (fromSender.length === 0) return void 0;
|
|
222
|
-
const
|
|
223
|
-
(max, t) => t.amount > max
|
|
293
|
+
const maxAmount = fromSender.reduce(
|
|
294
|
+
(max, t) => t.amount > max ? t.amount : max,
|
|
295
|
+
0n
|
|
224
296
|
);
|
|
225
|
-
|
|
297
|
+
let candidates = fromSender.filter((t) => t.amount === maxAmount);
|
|
298
|
+
if (candidates.length > 1) {
|
|
299
|
+
const mirrors = detectMirrorTokens(transfers);
|
|
300
|
+
const filtered = candidates.filter(
|
|
301
|
+
(t) => !mirrors.has(getAddress(t.token))
|
|
302
|
+
);
|
|
303
|
+
if (filtered.length > 0) candidates = filtered;
|
|
304
|
+
}
|
|
305
|
+
const result = candidates.find((t) => isKnownToken(t.token)) ?? candidates[0];
|
|
306
|
+
if (!result) return void 0;
|
|
307
|
+
return { token: result.token, amount: result.amount };
|
|
226
308
|
}
|
|
227
309
|
function toBlockTimestamp(block) {
|
|
228
310
|
return new Date(Number(block.timestamp) * 1e3);
|
|
@@ -245,20 +327,20 @@ import { decodeFunctionData, toFunctionSelector } from "viem";
|
|
|
245
327
|
// src/validation.ts
|
|
246
328
|
import { chains as chains2 } from "@yodlpay/tokenlists";
|
|
247
329
|
import { isAddress, isHex } from "viem";
|
|
248
|
-
import { z } from "zod";
|
|
330
|
+
import { z as z2 } from "zod";
|
|
249
331
|
var validChainIds = chains2.map((c) => c.id);
|
|
250
|
-
var txHashSchema =
|
|
251
|
-
var chainIdSchema =
|
|
332
|
+
var txHashSchema = z2.string().regex(/^0x[a-fA-F0-9]{64}$/, "Invalid transaction hash").transform((val) => val);
|
|
333
|
+
var chainIdSchema = z2.coerce.number().int().refine((id) => validChainIds.includes(id), {
|
|
252
334
|
message: `Chain ID must be one of: ${validChainIds.join(", ")}`
|
|
253
335
|
});
|
|
254
|
-
var ArgsSchema =
|
|
255
|
-
|
|
256
|
-
|
|
336
|
+
var ArgsSchema = z2.union([
|
|
337
|
+
z2.tuple([txHashSchema, chainIdSchema]),
|
|
338
|
+
z2.tuple([txHashSchema])
|
|
257
339
|
]);
|
|
258
|
-
var WebhooksSchema =
|
|
259
|
-
|
|
260
|
-
webhookAddress:
|
|
261
|
-
payload:
|
|
340
|
+
var WebhooksSchema = z2.array(
|
|
341
|
+
z2.object({
|
|
342
|
+
webhookAddress: z2.string().refine((val) => isAddress(val)),
|
|
343
|
+
payload: z2.array(z2.string().refine((val) => isHex(val)))
|
|
262
344
|
}).transform((webhook) => ({
|
|
263
345
|
...webhook,
|
|
264
346
|
memo: webhook.payload[0] ? decodeMemo(webhook.payload[0]) : ""
|
|
@@ -296,7 +378,7 @@ function extractEmbeddedParams(data) {
|
|
|
296
378
|
}
|
|
297
379
|
|
|
298
380
|
// src/relay-bridge.ts
|
|
299
|
-
import { getRouter
|
|
381
|
+
import { getRouter } from "@yodlpay/tokenlists";
|
|
300
382
|
import {
|
|
301
383
|
decodeEventLog as decodeEventLog2,
|
|
302
384
|
isAddressEqual as isAddressEqual2
|
|
@@ -330,15 +412,15 @@ async function fetchRelayRequest(hash) {
|
|
|
330
412
|
}
|
|
331
413
|
|
|
332
414
|
// src/relay-bridge.ts
|
|
333
|
-
function calculateOutputAmountGross(inputAmount, inputToken, inputChainId, outputToken, outputChainId) {
|
|
334
|
-
const { decimals: inputDecimals } =
|
|
335
|
-
inputToken,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
);
|
|
415
|
+
async function calculateOutputAmountGross(inputAmount, inputToken, inputChainId, outputToken, outputChainId, clients) {
|
|
416
|
+
const [{ decimals: inputDecimals }, { decimals: outputDecimals }] = await Promise.all([
|
|
417
|
+
resolveToken(inputToken, inputChainId, getClient(clients, inputChainId)),
|
|
418
|
+
resolveToken(
|
|
419
|
+
outputToken,
|
|
420
|
+
outputChainId,
|
|
421
|
+
getClient(clients, outputChainId)
|
|
422
|
+
)
|
|
423
|
+
]);
|
|
342
424
|
const decimalDiff = inputDecimals - outputDecimals;
|
|
343
425
|
if (decimalDiff === 0) return inputAmount;
|
|
344
426
|
if (decimalDiff > 0) return inputAmount / 10n ** BigInt(decimalDiff);
|
|
@@ -362,7 +444,7 @@ async function extractSenderFromSource(sourceReceipt, sourceProvider, sourceHash
|
|
|
362
444
|
const sourceTx = await sourceProvider.getTransaction({ hash: sourceHash });
|
|
363
445
|
return sourceTx.from;
|
|
364
446
|
}
|
|
365
|
-
function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inputAmountGross, inputChainId, outputChainId) {
|
|
447
|
+
async function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inputAmountGross, inputChainId, outputChainId, clients) {
|
|
366
448
|
const sourceTransfers = extractTokenTransfers(sourceReceipt.logs);
|
|
367
449
|
const inputTransfer = findInputTransfer(sourceTransfers, sender);
|
|
368
450
|
const tokenIn = inputTransfer?.token;
|
|
@@ -379,12 +461,13 @@ function resolveBridgeTokens(sourceReceipt, destReceipt, yodlEvent, sender, inpu
|
|
|
379
461
|
if (swapEvent !== void 0) {
|
|
380
462
|
tokenOutAmountGross = swapEvent.tokenOutAmount;
|
|
381
463
|
} else if (inputAmountGross > 0n && tokenIn && tokenOut) {
|
|
382
|
-
tokenOutAmountGross = calculateOutputAmountGross(
|
|
464
|
+
tokenOutAmountGross = await calculateOutputAmountGross(
|
|
383
465
|
inputAmountGross,
|
|
384
466
|
tokenIn,
|
|
385
467
|
inputChainId,
|
|
386
468
|
tokenOut,
|
|
387
|
-
outputChainId
|
|
469
|
+
outputChainId,
|
|
470
|
+
clients
|
|
388
471
|
);
|
|
389
472
|
}
|
|
390
473
|
return {
|
|
@@ -437,14 +520,15 @@ async function decodeBridgePayment(hash, clients) {
|
|
|
437
520
|
destProvider.getTransaction({ hash: destinationTxHash }),
|
|
438
521
|
extractSenderFromSource(sourceReceipt, sourceProvider, sourceTxHash)
|
|
439
522
|
]);
|
|
440
|
-
const tokens = resolveBridgeTokens(
|
|
523
|
+
const tokens = await resolveBridgeTokens(
|
|
441
524
|
sourceReceipt,
|
|
442
525
|
destReceipt,
|
|
443
526
|
yodlEvent,
|
|
444
527
|
sender,
|
|
445
528
|
inputAmountGross,
|
|
446
529
|
sourceChainId,
|
|
447
|
-
destinationChainId
|
|
530
|
+
destinationChainId,
|
|
531
|
+
clients
|
|
448
532
|
);
|
|
449
533
|
const bridge = {
|
|
450
534
|
sourceChainId,
|
|
@@ -504,14 +588,23 @@ async function decodePayment(hash, chainId, clients, cachedReceipt) {
|
|
|
504
588
|
}
|
|
505
589
|
|
|
506
590
|
// src/yodl-payment.ts
|
|
507
|
-
import { getTokenByAddress as getTokenByAddress2 } from "@yodlpay/tokenlists";
|
|
508
591
|
import {
|
|
509
592
|
formatUnits,
|
|
510
593
|
zeroAddress
|
|
511
594
|
} from "viem";
|
|
512
|
-
function buildTokenInfo(params) {
|
|
513
|
-
const inToken =
|
|
514
|
-
|
|
595
|
+
async function buildTokenInfo(params, clients) {
|
|
596
|
+
const [inToken, outToken] = await Promise.all([
|
|
597
|
+
resolveToken(
|
|
598
|
+
params.tokenIn,
|
|
599
|
+
params.inChainId,
|
|
600
|
+
getClient(clients, params.inChainId)
|
|
601
|
+
),
|
|
602
|
+
resolveToken(
|
|
603
|
+
params.tokenOut,
|
|
604
|
+
params.outChainId,
|
|
605
|
+
getClient(clients, params.outChainId)
|
|
606
|
+
)
|
|
607
|
+
]);
|
|
515
608
|
return {
|
|
516
609
|
tokenIn: {
|
|
517
610
|
...inToken,
|
|
@@ -533,19 +626,22 @@ function buildTokenInfo(params) {
|
|
|
533
626
|
}
|
|
534
627
|
};
|
|
535
628
|
}
|
|
536
|
-
function extractTokenInfo(paymentInfo, chainId, txHash) {
|
|
629
|
+
async function extractTokenInfo(paymentInfo, chainId, txHash, clients) {
|
|
537
630
|
switch (paymentInfo.type) {
|
|
538
631
|
case "direct": {
|
|
539
632
|
return {
|
|
540
|
-
...buildTokenInfo(
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
633
|
+
...await buildTokenInfo(
|
|
634
|
+
{
|
|
635
|
+
tokenIn: paymentInfo.token,
|
|
636
|
+
tokenInAmount: paymentInfo.amount,
|
|
637
|
+
tokenOut: paymentInfo.token,
|
|
638
|
+
tokenOutAmountGross: paymentInfo.amount,
|
|
639
|
+
tokenOutAmountNet: paymentInfo.amount,
|
|
640
|
+
inChainId: chainId,
|
|
641
|
+
outChainId: chainId
|
|
642
|
+
},
|
|
643
|
+
clients
|
|
644
|
+
),
|
|
549
645
|
sourceChainId: chainId,
|
|
550
646
|
sourceTxHash: txHash,
|
|
551
647
|
destinationChainId: chainId,
|
|
@@ -555,15 +651,18 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
|
|
|
555
651
|
}
|
|
556
652
|
case "swap": {
|
|
557
653
|
return {
|
|
558
|
-
...buildTokenInfo(
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
654
|
+
...await buildTokenInfo(
|
|
655
|
+
{
|
|
656
|
+
tokenIn: paymentInfo.tokenIn,
|
|
657
|
+
tokenInAmount: paymentInfo.tokenInAmount,
|
|
658
|
+
tokenOut: paymentInfo.token,
|
|
659
|
+
tokenOutAmountGross: paymentInfo.tokenOutAmount,
|
|
660
|
+
tokenOutAmountNet: paymentInfo.amount,
|
|
661
|
+
inChainId: chainId,
|
|
662
|
+
outChainId: chainId
|
|
663
|
+
},
|
|
664
|
+
clients
|
|
665
|
+
),
|
|
567
666
|
sourceChainId: chainId,
|
|
568
667
|
sourceTxHash: txHash,
|
|
569
668
|
destinationChainId: chainId,
|
|
@@ -573,15 +672,18 @@ function extractTokenInfo(paymentInfo, chainId, txHash) {
|
|
|
573
672
|
}
|
|
574
673
|
case "bridge": {
|
|
575
674
|
return {
|
|
576
|
-
...buildTokenInfo(
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
675
|
+
...await buildTokenInfo(
|
|
676
|
+
{
|
|
677
|
+
tokenIn: paymentInfo.tokenIn,
|
|
678
|
+
tokenInAmount: paymentInfo.tokenInAmount,
|
|
679
|
+
tokenOut: paymentInfo.tokenOut,
|
|
680
|
+
tokenOutAmountGross: paymentInfo.tokenOutAmountGross,
|
|
681
|
+
tokenOutAmountNet: paymentInfo.amount,
|
|
682
|
+
inChainId: paymentInfo.sourceChainId,
|
|
683
|
+
outChainId: paymentInfo.destinationChainId
|
|
684
|
+
},
|
|
685
|
+
clients
|
|
686
|
+
),
|
|
585
687
|
sourceChainId: paymentInfo.sourceChainId,
|
|
586
688
|
sourceTxHash: paymentInfo.sourceTxHash,
|
|
587
689
|
destinationChainId: paymentInfo.destinationChainId,
|
|
@@ -601,7 +703,12 @@ async function decodeYodlPayment(txHash, chainId, clients, cachedReceipt) {
|
|
|
601
703
|
const firstWebhook = paymentInfo.webhooks[0];
|
|
602
704
|
const processorAddress = firstWebhook?.webhookAddress ?? zeroAddress;
|
|
603
705
|
const processorMemo = firstWebhook?.memo ?? "";
|
|
604
|
-
const tokenInfo = extractTokenInfo(
|
|
706
|
+
const tokenInfo = await extractTokenInfo(
|
|
707
|
+
paymentInfo,
|
|
708
|
+
chainId,
|
|
709
|
+
txHash,
|
|
710
|
+
clients
|
|
711
|
+
);
|
|
605
712
|
return {
|
|
606
713
|
senderAddress: paymentInfo.sender,
|
|
607
714
|
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.1",
|
|
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",
|