mppx 0.5.13 → 0.5.14
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/CHANGELOG.md +8 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +7 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +6 -0
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +4 -0
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +36 -28
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts +5 -0
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +202 -63
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +1 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +38 -15
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/package.json +2 -2
- package/src/tempo/internal/fee-payer.test.ts +48 -12
- package/src/tempo/internal/fee-payer.ts +13 -2
- package/src/tempo/server/Charge.test.ts +73 -0
- package/src/tempo/server/Charge.ts +6 -0
- package/src/tempo/server/Session.test.ts +130 -1
- package/src/tempo/server/Session.ts +41 -35
- package/src/tempo/session/Chain.test.ts +225 -2
- package/src/tempo/session/Chain.ts +250 -65
- package/src/tempo/session/ChannelStore.test.ts +23 -0
- package/src/tempo/session/ChannelStore.ts +46 -13
|
@@ -37,6 +37,7 @@ import * as Store from '../../Store.js'
|
|
|
37
37
|
import * as Client from '../../viem/Client.js'
|
|
38
38
|
import * as Account from '../internal/account.js'
|
|
39
39
|
import * as defaults from '../internal/defaults.js'
|
|
40
|
+
import * as FeePayer from '../internal/fee-payer.js'
|
|
40
41
|
import type * as types from '../internal/types.js'
|
|
41
42
|
import * as Methods from '../Methods.js'
|
|
42
43
|
import {
|
|
@@ -92,6 +93,7 @@ export function session<const parameters extends session.Parameters>(
|
|
|
92
93
|
channelStateTtl = 5_000,
|
|
93
94
|
currency = defaults.resolveCurrency(parameters),
|
|
94
95
|
decimals = defaults.decimals,
|
|
96
|
+
feePayerPolicy,
|
|
95
97
|
store: rawStore = Store.memory(),
|
|
96
98
|
suggestedDeposit,
|
|
97
99
|
unitType,
|
|
@@ -203,9 +205,10 @@ export function session<const parameters extends session.Parameters>(
|
|
|
203
205
|
payload,
|
|
204
206
|
methodDetails,
|
|
205
207
|
resolvedFeePayer,
|
|
208
|
+
feePayerPolicy,
|
|
206
209
|
waitForConfirmation,
|
|
207
210
|
)
|
|
208
|
-
lastOnChainVerified.set(
|
|
211
|
+
lastOnChainVerified.set(sessionReceipt.channelId, Date.now())
|
|
209
212
|
break
|
|
210
213
|
|
|
211
214
|
case 'topUp':
|
|
@@ -216,8 +219,9 @@ export function session<const parameters extends session.Parameters>(
|
|
|
216
219
|
payload,
|
|
217
220
|
methodDetails,
|
|
218
221
|
resolvedFeePayer,
|
|
222
|
+
feePayerPolicy,
|
|
219
223
|
)
|
|
220
|
-
lastOnChainVerified.set(
|
|
224
|
+
lastOnChainVerified.set(sessionReceipt.channelId, Date.now())
|
|
221
225
|
break
|
|
222
226
|
|
|
223
227
|
case 'voucher':
|
|
@@ -293,9 +297,13 @@ export declare namespace session {
|
|
|
293
297
|
'feePayer' | 'recipient'
|
|
294
298
|
>
|
|
295
299
|
|
|
300
|
+
type FeePayerPolicy = Partial<FeePayer.Policy>
|
|
301
|
+
|
|
296
302
|
type Parameters = {
|
|
297
303
|
/** TTL in milliseconds for cached on-chain channel state. After this duration, the server re-queries on-chain state during voucher handling to detect forced close requests. @default 5_000 */
|
|
298
304
|
channelStateTtl?: number | undefined
|
|
305
|
+
/** Override the fee-sponsor policy used for sponsored open/topUp transactions. */
|
|
306
|
+
feePayerPolicy?: FeePayerPolicy | undefined
|
|
299
307
|
/** Minimum voucher delta to accept (numeric string, default: "0"). */
|
|
300
308
|
minVoucherDelta?: string | undefined
|
|
301
309
|
/**
|
|
@@ -561,13 +569,11 @@ async function handleOpen(
|
|
|
561
569
|
payload: SessionCredentialPayload & { action: 'open' },
|
|
562
570
|
methodDetails: SessionMethodDetails,
|
|
563
571
|
feePayer: viem_Account | undefined,
|
|
572
|
+
feePayerPolicy: session.FeePayerPolicy | undefined,
|
|
564
573
|
waitForConfirmation: boolean,
|
|
565
574
|
): Promise<SessionReceipt> {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
payload.cumulativeAmount,
|
|
569
|
-
payload.signature,
|
|
570
|
-
)
|
|
575
|
+
const channelId = ChannelStore.normalizeChannelId(payload.channelId)
|
|
576
|
+
const voucher = parseVoucherFromPayload(channelId, payload.cumulativeAmount, payload.signature)
|
|
571
577
|
|
|
572
578
|
const recipient = challenge.request.recipient as Address
|
|
573
579
|
const currency = challenge.request.currency as Address
|
|
@@ -577,9 +583,11 @@ async function handleOpen(
|
|
|
577
583
|
client,
|
|
578
584
|
serializedTransaction: payload.transaction,
|
|
579
585
|
escrowContract: methodDetails.escrowContract,
|
|
580
|
-
channelId
|
|
586
|
+
channelId,
|
|
581
587
|
recipient,
|
|
582
588
|
currency,
|
|
589
|
+
challengeExpires: challenge.expires,
|
|
590
|
+
feePayerPolicy,
|
|
583
591
|
feePayer,
|
|
584
592
|
waitForConfirmation,
|
|
585
593
|
})
|
|
@@ -612,7 +620,7 @@ async function handleOpen(
|
|
|
612
620
|
throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
|
|
613
621
|
}
|
|
614
622
|
|
|
615
|
-
const updated = await store.updateChannel(
|
|
623
|
+
const updated = await store.updateChannel(channelId, (existing) => {
|
|
616
624
|
if (existing) {
|
|
617
625
|
if (voucher.cumulativeAmount <= existing.settledOnChain) {
|
|
618
626
|
throw new VerificationFailedError({
|
|
@@ -644,7 +652,7 @@ async function handleOpen(
|
|
|
644
652
|
}
|
|
645
653
|
}
|
|
646
654
|
return {
|
|
647
|
-
channelId
|
|
655
|
+
channelId,
|
|
648
656
|
chainId: methodDetails.chainId,
|
|
649
657
|
escrowContract: methodDetails.escrowContract,
|
|
650
658
|
closeRequestedAt: onChain.closeRequestedAt,
|
|
@@ -667,7 +675,7 @@ async function handleOpen(
|
|
|
667
675
|
|
|
668
676
|
return createSessionReceipt({
|
|
669
677
|
challengeId: challenge.id,
|
|
670
|
-
channelId:
|
|
678
|
+
channelId: updated.channelId,
|
|
671
679
|
acceptedCumulative: updated.highestVoucherAmount,
|
|
672
680
|
spent: updated.spent,
|
|
673
681
|
units: updated.units,
|
|
@@ -689,8 +697,10 @@ async function handleTopUp(
|
|
|
689
697
|
payload: SessionCredentialPayload & { action: 'topUp' },
|
|
690
698
|
methodDetails: SessionMethodDetails,
|
|
691
699
|
feePayer: viem_Account | undefined,
|
|
700
|
+
feePayerPolicy: session.FeePayerPolicy | undefined,
|
|
692
701
|
): Promise<SessionReceipt> {
|
|
693
|
-
const
|
|
702
|
+
const channelId = ChannelStore.normalizeChannelId(payload.channelId)
|
|
703
|
+
const channel = await store.getChannel(channelId)
|
|
694
704
|
if (!channel) {
|
|
695
705
|
throw new ChannelNotFoundError({ reason: 'channel not found' })
|
|
696
706
|
}
|
|
@@ -701,21 +711,23 @@ async function handleTopUp(
|
|
|
701
711
|
client,
|
|
702
712
|
serializedTransaction: payload.transaction,
|
|
703
713
|
escrowContract: methodDetails.escrowContract,
|
|
704
|
-
channelId
|
|
714
|
+
channelId,
|
|
705
715
|
currency: challenge.request.currency as Address,
|
|
706
716
|
declaredDeposit,
|
|
707
717
|
previousDeposit: channel.deposit,
|
|
718
|
+
challengeExpires: challenge.expires,
|
|
719
|
+
feePayerPolicy,
|
|
708
720
|
feePayer,
|
|
709
721
|
})
|
|
710
722
|
|
|
711
|
-
const updated = await store.updateChannel(
|
|
723
|
+
const updated = await store.updateChannel(channelId, (current) => {
|
|
712
724
|
if (!current) throw new ChannelNotFoundError({ reason: 'channel not found' })
|
|
713
725
|
return { ...current, deposit: onChainDeposit }
|
|
714
726
|
})
|
|
715
727
|
|
|
716
728
|
return createSessionReceipt({
|
|
717
729
|
challengeId: challenge.id,
|
|
718
|
-
channelId:
|
|
730
|
+
channelId: updated?.channelId ?? channel.channelId,
|
|
719
731
|
acceptedCumulative: updated?.highestVoucherAmount ?? channel.highestVoucherAmount,
|
|
720
732
|
spent: updated?.spent ?? channel.spent,
|
|
721
733
|
units: updated?.units ?? channel.units,
|
|
@@ -735,7 +747,8 @@ async function handleVoucher(
|
|
|
735
747
|
channelStateTtl: number,
|
|
736
748
|
lastOnChainVerified: Map<Hex, number>,
|
|
737
749
|
): Promise<SessionReceipt> {
|
|
738
|
-
const
|
|
750
|
+
const channelId = ChannelStore.normalizeChannelId(payload.channelId)
|
|
751
|
+
const channel = await store.getChannel(channelId)
|
|
739
752
|
if (!channel) {
|
|
740
753
|
throw new ChannelNotFoundError({ reason: 'channel not found' })
|
|
741
754
|
}
|
|
@@ -743,11 +756,7 @@ async function handleVoucher(
|
|
|
743
756
|
throw new ChannelClosedError({ reason: 'channel is finalized' })
|
|
744
757
|
}
|
|
745
758
|
|
|
746
|
-
const voucher = parseVoucherFromPayload(
|
|
747
|
-
payload.channelId,
|
|
748
|
-
payload.cumulativeAmount,
|
|
749
|
-
payload.signature,
|
|
750
|
-
)
|
|
759
|
+
const voucher = parseVoucherFromPayload(channelId, payload.cumulativeAmount, payload.signature)
|
|
751
760
|
|
|
752
761
|
// Use locally-stored channel state as a trusted cache instead of
|
|
753
762
|
// reading on-chain for every voucher. The on-chain state is verified
|
|
@@ -759,7 +768,7 @@ async function handleVoucher(
|
|
|
759
768
|
// To guard against the payer initiating a forced close while vouchers
|
|
760
769
|
// are still being accepted, re-query on-chain state when the cache
|
|
761
770
|
// exceeds the configured staleness TTL (default: 5s).
|
|
762
|
-
const lastVerified = lastOnChainVerified.get(
|
|
771
|
+
const lastVerified = lastOnChainVerified.get(channelId) ?? 0
|
|
763
772
|
const isStale = Date.now() - lastVerified > channelStateTtl
|
|
764
773
|
|
|
765
774
|
const onChain = await (async () => {
|
|
@@ -767,13 +776,13 @@ async function handleVoucher(
|
|
|
767
776
|
const onChainChannel = await getOnChainChannel(
|
|
768
777
|
client,
|
|
769
778
|
methodDetails.escrowContract,
|
|
770
|
-
|
|
779
|
+
channelId,
|
|
771
780
|
)
|
|
772
|
-
lastOnChainVerified.set(
|
|
781
|
+
lastOnChainVerified.set(channelId, Date.now())
|
|
773
782
|
// Persist closeRequestedAt so the cached path detects force-close
|
|
774
783
|
// between re-queries.
|
|
775
784
|
if (onChainChannel.closeRequestedAt !== 0n) {
|
|
776
|
-
await store.updateChannel(
|
|
785
|
+
await store.updateChannel(channelId, (current) =>
|
|
777
786
|
current ? { ...current, closeRequestedAt: onChainChannel.closeRequestedAt } : current,
|
|
778
787
|
)
|
|
779
788
|
}
|
|
@@ -796,7 +805,7 @@ async function handleVoucher(
|
|
|
796
805
|
minVoucherDelta,
|
|
797
806
|
challenge,
|
|
798
807
|
channel,
|
|
799
|
-
channelId
|
|
808
|
+
channelId,
|
|
800
809
|
voucher,
|
|
801
810
|
onChain,
|
|
802
811
|
methodDetails,
|
|
@@ -815,7 +824,8 @@ async function handleClose(
|
|
|
815
824
|
account?: viem_Account,
|
|
816
825
|
feePayer?: viem_Account,
|
|
817
826
|
): Promise<SessionReceipt> {
|
|
818
|
-
const
|
|
827
|
+
const channelId = ChannelStore.normalizeChannelId(payload.channelId)
|
|
828
|
+
const channel = await store.getChannel(channelId)
|
|
819
829
|
if (!channel) {
|
|
820
830
|
throw new ChannelNotFoundError({ reason: 'channel not found' })
|
|
821
831
|
}
|
|
@@ -823,13 +833,9 @@ async function handleClose(
|
|
|
823
833
|
throw new ChannelClosedError({ reason: 'channel is already finalized' })
|
|
824
834
|
}
|
|
825
835
|
|
|
826
|
-
const voucher = parseVoucherFromPayload(
|
|
827
|
-
payload.channelId,
|
|
828
|
-
payload.cumulativeAmount,
|
|
829
|
-
payload.signature,
|
|
830
|
-
)
|
|
836
|
+
const voucher = parseVoucherFromPayload(channelId, payload.cumulativeAmount, payload.signature)
|
|
831
837
|
|
|
832
|
-
const onChain = await getOnChainChannel(client, methodDetails.escrowContract,
|
|
838
|
+
const onChain = await getOnChainChannel(client, methodDetails.escrowContract, channelId)
|
|
833
839
|
|
|
834
840
|
if (onChain.finalized) {
|
|
835
841
|
throw new ChannelClosedError({ reason: 'channel is finalized on-chain' })
|
|
@@ -867,7 +873,7 @@ async function handleClose(
|
|
|
867
873
|
...(feePayer && account ? { feePayer, account } : { account }),
|
|
868
874
|
})
|
|
869
875
|
|
|
870
|
-
const updated = await store.updateChannel(
|
|
876
|
+
const updated = await store.updateChannel(channelId, (current) => {
|
|
871
877
|
if (!current) return null
|
|
872
878
|
const updateVoucher = voucher.cumulativeAmount > current.highestVoucherAmount
|
|
873
879
|
return {
|
|
@@ -883,7 +889,7 @@ async function handleClose(
|
|
|
883
889
|
|
|
884
890
|
return createSessionReceipt({
|
|
885
891
|
challengeId: challenge.id,
|
|
886
|
-
channelId:
|
|
892
|
+
channelId: updated?.channelId ?? channel.channelId,
|
|
887
893
|
acceptedCumulative: voucher.cumulativeAmount,
|
|
888
894
|
spent: updated?.spent ?? channel.spent,
|
|
889
895
|
units: updated?.units ?? channel.units,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Address, encodeFunctionData, erc20Abi, type Hex } from 'viem'
|
|
2
|
-
import { waitForTransactionReceipt } from 'viem/actions'
|
|
1
|
+
import { type Address, encodeFunctionData, erc20Abi, type Hex, zeroAddress } from 'viem'
|
|
2
|
+
import { prepareTransactionRequest, signTransaction, waitForTransactionReceipt } from 'viem/actions'
|
|
3
3
|
import { Addresses, Transaction } from 'viem/tempo'
|
|
4
4
|
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
5
5
|
import { nodeEnv } from '~test/config.js'
|
|
@@ -17,10 +17,12 @@ import {
|
|
|
17
17
|
broadcastOpenTransaction,
|
|
18
18
|
broadcastTopUpTransaction,
|
|
19
19
|
closeOnChain,
|
|
20
|
+
escrowAbi,
|
|
20
21
|
getOnChainChannel,
|
|
21
22
|
settleOnChain,
|
|
22
23
|
verifyTopUpTransaction,
|
|
23
24
|
} from './Chain.js'
|
|
25
|
+
import * as Channel from './Channel.js'
|
|
24
26
|
import { signVoucher } from './Voucher.js'
|
|
25
27
|
|
|
26
28
|
const isLocalnet = nodeEnv === 'localnet'
|
|
@@ -397,6 +399,116 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
397
399
|
).rejects.toThrow('Only Tempo (0x76/0x78) transactions are supported')
|
|
398
400
|
})
|
|
399
401
|
|
|
402
|
+
test('fee-payer: rejects transactions whose gas budget exceeds sponsor policy', async () => {
|
|
403
|
+
const salt = nextSalt()
|
|
404
|
+
const deposit = 5_000_000n
|
|
405
|
+
|
|
406
|
+
const approveData = encodeFunctionData({
|
|
407
|
+
abi: erc20Abi,
|
|
408
|
+
functionName: 'approve',
|
|
409
|
+
args: [escrowContract, deposit],
|
|
410
|
+
})
|
|
411
|
+
const openData = encodeFunctionData({
|
|
412
|
+
abi: escrowAbi,
|
|
413
|
+
functionName: 'open',
|
|
414
|
+
args: [recipient, currency, deposit, salt, zeroAddress],
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const channelId = Channel.computeId({
|
|
418
|
+
authorizedSigner: zeroAddress,
|
|
419
|
+
chainId: chain.id,
|
|
420
|
+
escrowContract,
|
|
421
|
+
payee: recipient,
|
|
422
|
+
payer: payer.address,
|
|
423
|
+
salt,
|
|
424
|
+
token: currency,
|
|
425
|
+
}) as Hex
|
|
426
|
+
|
|
427
|
+
const prepared = await prepareTransactionRequest(client, {
|
|
428
|
+
account: payer,
|
|
429
|
+
calls: [
|
|
430
|
+
{ to: currency, data: approveData },
|
|
431
|
+
{ to: escrowContract, data: openData },
|
|
432
|
+
],
|
|
433
|
+
feePayer: true,
|
|
434
|
+
feeToken: currency,
|
|
435
|
+
} as never)
|
|
436
|
+
prepared.gas = 2_000_001n
|
|
437
|
+
|
|
438
|
+
const serializedTransaction = await signTransaction(client, prepared as never)
|
|
439
|
+
|
|
440
|
+
await expect(
|
|
441
|
+
broadcastOpenTransaction({
|
|
442
|
+
client,
|
|
443
|
+
serializedTransaction: serializedTransaction as Hex,
|
|
444
|
+
escrowContract,
|
|
445
|
+
channelId,
|
|
446
|
+
recipient,
|
|
447
|
+
currency,
|
|
448
|
+
feePayer: accounts[0],
|
|
449
|
+
}),
|
|
450
|
+
).rejects.toThrow('gas exceeds sponsor policy')
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
test('fee-payer: rejects smuggled second open call', async () => {
|
|
454
|
+
const deposit = 5_000_000n
|
|
455
|
+
const smuggledDeposit = 7_000_000n
|
|
456
|
+
const salt = nextSalt()
|
|
457
|
+
const smuggledSalt = nextSalt()
|
|
458
|
+
|
|
459
|
+
const approveData = encodeFunctionData({
|
|
460
|
+
abi: erc20Abi,
|
|
461
|
+
functionName: 'approve',
|
|
462
|
+
args: [escrowContract, deposit + smuggledDeposit],
|
|
463
|
+
})
|
|
464
|
+
const openData = encodeFunctionData({
|
|
465
|
+
abi: escrowAbi,
|
|
466
|
+
functionName: 'open',
|
|
467
|
+
args: [recipient, currency, deposit, salt, zeroAddress],
|
|
468
|
+
})
|
|
469
|
+
const smuggledOpenData = encodeFunctionData({
|
|
470
|
+
abi: escrowAbi,
|
|
471
|
+
functionName: 'open',
|
|
472
|
+
args: [accounts[3].address, currency, smuggledDeposit, smuggledSalt, zeroAddress],
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
const channelId = Channel.computeId({
|
|
476
|
+
authorizedSigner: zeroAddress,
|
|
477
|
+
chainId: chain.id,
|
|
478
|
+
escrowContract,
|
|
479
|
+
payee: recipient,
|
|
480
|
+
payer: payer.address,
|
|
481
|
+
salt,
|
|
482
|
+
token: currency,
|
|
483
|
+
}) as Hex
|
|
484
|
+
|
|
485
|
+
const prepared = await prepareTransactionRequest(client, {
|
|
486
|
+
account: payer,
|
|
487
|
+
calls: [
|
|
488
|
+
{ to: currency, data: approveData },
|
|
489
|
+
{ to: escrowContract, data: openData },
|
|
490
|
+
{ to: escrowContract, data: smuggledOpenData },
|
|
491
|
+
],
|
|
492
|
+
feePayer: true,
|
|
493
|
+
feeToken: currency,
|
|
494
|
+
} as never)
|
|
495
|
+
prepared.gas = prepared.gas! + 5_000n
|
|
496
|
+
|
|
497
|
+
const serializedTransaction = await signTransaction(client, prepared as never)
|
|
498
|
+
|
|
499
|
+
await expect(
|
|
500
|
+
broadcastOpenTransaction({
|
|
501
|
+
client,
|
|
502
|
+
serializedTransaction: serializedTransaction as Hex,
|
|
503
|
+
escrowContract,
|
|
504
|
+
channelId,
|
|
505
|
+
recipient,
|
|
506
|
+
currency,
|
|
507
|
+
feePayer: accounts[0],
|
|
508
|
+
}),
|
|
509
|
+
).rejects.toThrow('fee-sponsored open transaction contains a smuggled call')
|
|
510
|
+
})
|
|
511
|
+
|
|
400
512
|
test('duplicate broadcast returns fallback with txHash undefined', async () => {
|
|
401
513
|
const salt = nextSalt()
|
|
402
514
|
const deposit = 5_000_000n
|
|
@@ -727,6 +839,117 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
727
839
|
}),
|
|
728
840
|
).rejects.toThrow('Only Tempo (0x76/0x78) transactions are supported')
|
|
729
841
|
})
|
|
842
|
+
|
|
843
|
+
test('fee-payer: rejects topUp transactions whose gas budget exceeds sponsor policy', async () => {
|
|
844
|
+
const salt = nextSalt()
|
|
845
|
+
const deposit = 5_000_000n
|
|
846
|
+
const topUpAmount = 3_000_000n
|
|
847
|
+
|
|
848
|
+
const { channelId } = await openChannel({
|
|
849
|
+
escrow: escrowContract,
|
|
850
|
+
payer,
|
|
851
|
+
payee: recipient,
|
|
852
|
+
token: currency,
|
|
853
|
+
deposit,
|
|
854
|
+
salt,
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
const approveData = encodeFunctionData({
|
|
858
|
+
abi: erc20Abi,
|
|
859
|
+
functionName: 'approve',
|
|
860
|
+
args: [escrowContract, topUpAmount],
|
|
861
|
+
})
|
|
862
|
+
const topUpData = encodeFunctionData({
|
|
863
|
+
abi: escrowAbi,
|
|
864
|
+
functionName: 'topUp',
|
|
865
|
+
args: [channelId, topUpAmount],
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
const prepared = await prepareTransactionRequest(client, {
|
|
869
|
+
account: payer,
|
|
870
|
+
calls: [
|
|
871
|
+
{ to: currency, data: approveData },
|
|
872
|
+
{ to: escrowContract, data: topUpData },
|
|
873
|
+
],
|
|
874
|
+
feePayer: true,
|
|
875
|
+
feeToken: currency,
|
|
876
|
+
} as never)
|
|
877
|
+
prepared.gas = 2_000_001n
|
|
878
|
+
|
|
879
|
+
const serializedTransaction = await signTransaction(client, prepared as never)
|
|
880
|
+
|
|
881
|
+
await expect(
|
|
882
|
+
broadcastTopUpTransaction({
|
|
883
|
+
client,
|
|
884
|
+
serializedTransaction: serializedTransaction as Hex,
|
|
885
|
+
escrowContract,
|
|
886
|
+
channelId,
|
|
887
|
+
currency: asset,
|
|
888
|
+
declaredDeposit: topUpAmount,
|
|
889
|
+
previousDeposit: deposit,
|
|
890
|
+
feePayer: accounts[0],
|
|
891
|
+
}),
|
|
892
|
+
).rejects.toThrow('gas exceeds sponsor policy')
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
test('fee-payer: rejects smuggled second topUp call', async () => {
|
|
896
|
+
const salt = nextSalt()
|
|
897
|
+
const deposit = 5_000_000n
|
|
898
|
+
const topUpAmount = 3_000_000n
|
|
899
|
+
const smuggledAmount = 4_000_000n
|
|
900
|
+
|
|
901
|
+
const { channelId } = await openChannel({
|
|
902
|
+
escrow: escrowContract,
|
|
903
|
+
payer,
|
|
904
|
+
payee: recipient,
|
|
905
|
+
token: currency,
|
|
906
|
+
deposit,
|
|
907
|
+
salt,
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
const approveData = encodeFunctionData({
|
|
911
|
+
abi: erc20Abi,
|
|
912
|
+
functionName: 'approve',
|
|
913
|
+
args: [escrowContract, topUpAmount + smuggledAmount],
|
|
914
|
+
})
|
|
915
|
+
const topUpData = encodeFunctionData({
|
|
916
|
+
abi: escrowAbi,
|
|
917
|
+
functionName: 'topUp',
|
|
918
|
+
args: [channelId, topUpAmount],
|
|
919
|
+
})
|
|
920
|
+
const smuggledTopUpData = encodeFunctionData({
|
|
921
|
+
abi: escrowAbi,
|
|
922
|
+
functionName: 'topUp',
|
|
923
|
+
args: [channelId, smuggledAmount],
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
const prepared = await prepareTransactionRequest(client, {
|
|
927
|
+
account: payer,
|
|
928
|
+
calls: [
|
|
929
|
+
{ to: currency, data: approveData },
|
|
930
|
+
{ to: escrowContract, data: topUpData },
|
|
931
|
+
{ to: escrowContract, data: smuggledTopUpData },
|
|
932
|
+
],
|
|
933
|
+
feePayer: true,
|
|
934
|
+
feeToken: currency,
|
|
935
|
+
} as never)
|
|
936
|
+
prepared.gas = prepared.gas! + 5_000n
|
|
937
|
+
|
|
938
|
+
const serializedTransaction = await signTransaction(client, prepared as never)
|
|
939
|
+
|
|
940
|
+
await expect(
|
|
941
|
+
broadcastTopUpTransaction({
|
|
942
|
+
client,
|
|
943
|
+
serializedTransaction: serializedTransaction as Hex,
|
|
944
|
+
escrowContract,
|
|
945
|
+
channelId,
|
|
946
|
+
currency: asset,
|
|
947
|
+
declaredDeposit: topUpAmount,
|
|
948
|
+
previousDeposit: deposit,
|
|
949
|
+
feePayer: accounts[0],
|
|
950
|
+
}),
|
|
951
|
+
).rejects.toThrow('fee-sponsored topUp transaction contains a smuggled call')
|
|
952
|
+
})
|
|
730
953
|
})
|
|
731
954
|
|
|
732
955
|
describe('settleOnChain', () => {
|