mppx 0.8.0 → 0.8.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/CHANGELOG.md +18 -0
- package/dist/Errors.d.ts +13 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +12 -0
- package/dist/Errors.js.map +1 -1
- package/dist/cli/cli.d.ts +2 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +35 -4
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/internal.d.ts +1 -0
- package/dist/cli/internal.d.ts.map +1 -1
- package/dist/cli/internal.js +1 -15
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts +15 -0
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +124 -14
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/client/Mppx.d.ts +2 -0
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +2 -1
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/Transport.d.ts.map +1 -1
- package/dist/client/Transport.js +11 -12
- package/dist/client/Transport.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +55 -50
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/protocols/Mcp.d.ts +6 -0
- package/dist/client/internal/protocols/Mcp.d.ts.map +1 -1
- package/dist/client/internal/protocols/Mcp.js +28 -10
- package/dist/client/internal/protocols/Mcp.js.map +1 -1
- package/dist/client/internal/protocols/X402.d.ts.map +1 -1
- package/dist/client/internal/protocols/X402.js +18 -12
- package/dist/client/internal/protocols/X402.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +17 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +105 -25
- 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 +4 -0
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +47 -5
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +6 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +13 -0
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/x402/Header.d.ts +10 -1
- package/dist/x402/Header.d.ts.map +1 -1
- package/dist/x402/Header.js +27 -1
- package/dist/x402/Header.js.map +1 -1
- package/package.json +3 -3
- package/src/Errors.test.ts +28 -0
- package/src/Errors.ts +19 -0
- package/src/cli/cli.test.ts +194 -0
- package/src/cli/cli.ts +43 -7
- package/src/cli/config.test.ts +13 -7
- package/src/cli/internal.ts +1 -16
- package/src/cli/plugins/tempo.ts +160 -17
- package/src/client/Mppx.test-d.ts +9 -0
- package/src/client/Mppx.test.ts +41 -0
- package/src/client/Mppx.ts +4 -0
- package/src/client/Transport.test.ts +124 -0
- package/src/client/Transport.ts +11 -9
- package/src/client/internal/Fetch.test-d.ts +9 -0
- package/src/client/internal/Fetch.test.ts +186 -4
- package/src/client/internal/Fetch.ts +85 -76
- package/src/client/internal/protocols/Mcp.ts +39 -15
- package/src/client/internal/protocols/X402.ts +9 -5
- package/src/server/Mppx.test.ts +3 -0
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/internal/fee-payer.test.ts +130 -0
- package/src/tempo/internal/fee-payer.ts +157 -36
- package/src/tempo/server/Charge.ts +4 -0
- package/src/tempo/server/internal/html/package.json +2 -2
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/client/SessionManager.test.ts +57 -4
- package/src/tempo/session/client/SessionManager.ts +50 -6
- package/src/tempo/session/server/CredentialVerification.test.ts +36 -0
- package/src/tempo/session/server/CredentialVerification.ts +18 -0
- package/src/x402/Header.ts +45 -0
|
@@ -499,6 +499,10 @@ describe('fillHostedFeePayerTransaction', () => {
|
|
|
499
499
|
signature: { r: 1n, s: 1n, yParity: 0 } as any,
|
|
500
500
|
validBefore: Math.floor(Date.now() / 1_000) + 300,
|
|
501
501
|
} as const
|
|
502
|
+
const hostedContext = {
|
|
503
|
+
chainId: defaults.chainId.mainnet,
|
|
504
|
+
details,
|
|
505
|
+
} as const
|
|
502
506
|
|
|
503
507
|
test('uses hosted fillTransaction and preserves sender-committed fields', async () => {
|
|
504
508
|
// Sign over the payload built from the actual RPC request body so this
|
|
@@ -569,6 +573,7 @@ describe('fillHostedFeePayerTransaction', () => {
|
|
|
569
573
|
|
|
570
574
|
const result = await fillHostedFeePayerTransaction({
|
|
571
575
|
allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
|
|
576
|
+
...hostedContext,
|
|
572
577
|
transaction: hostedTransaction as any,
|
|
573
578
|
url: 'https://sponsor.example/tp_key',
|
|
574
579
|
})
|
|
@@ -621,6 +626,7 @@ describe('fillHostedFeePayerTransaction', () => {
|
|
|
621
626
|
await expect(
|
|
622
627
|
fillHostedFeePayerTransaction({
|
|
623
628
|
allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
|
|
629
|
+
...hostedContext,
|
|
624
630
|
transaction: hostedTransaction as any,
|
|
625
631
|
url: 'https://sponsor.example/tp_key',
|
|
626
632
|
}),
|
|
@@ -643,6 +649,7 @@ describe('fillHostedFeePayerTransaction', () => {
|
|
|
643
649
|
await expect(
|
|
644
650
|
fillHostedFeePayerTransaction({
|
|
645
651
|
allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
|
|
652
|
+
...hostedContext,
|
|
646
653
|
transaction: hostedTransaction as any,
|
|
647
654
|
url: 'https://sponsor.example/tp_key',
|
|
648
655
|
}),
|
|
@@ -663,11 +670,69 @@ describe('fillHostedFeePayerTransaction', () => {
|
|
|
663
670
|
await expect(
|
|
664
671
|
fillHostedFeePayerTransaction({
|
|
665
672
|
allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
|
|
673
|
+
...hostedContext,
|
|
666
674
|
transaction: hostedTransaction as any,
|
|
667
675
|
url: 'https://sponsor.example/tp_key',
|
|
668
676
|
}),
|
|
669
677
|
).rejects.toThrow('Invalid or revoked API key')
|
|
670
678
|
})
|
|
679
|
+
|
|
680
|
+
test('error: enforces sponsor policy before requesting hosted fill', async () => {
|
|
681
|
+
const fetchMock = vi.fn()
|
|
682
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
683
|
+
|
|
684
|
+
await expect(
|
|
685
|
+
fillHostedFeePayerTransaction({
|
|
686
|
+
allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
|
|
687
|
+
...hostedContext,
|
|
688
|
+
policy: { maxGas: hostedTransaction.gas - 1n },
|
|
689
|
+
transaction: hostedTransaction as any,
|
|
690
|
+
url: 'https://sponsor.example/tp_key',
|
|
691
|
+
}),
|
|
692
|
+
).rejects.toThrow('gas exceeds sponsor policy')
|
|
693
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
test('error: rejects non-empty access lists before requesting hosted fill', async () => {
|
|
697
|
+
const fetchMock = vi.fn()
|
|
698
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
699
|
+
|
|
700
|
+
await expect(
|
|
701
|
+
fillHostedFeePayerTransaction({
|
|
702
|
+
allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
|
|
703
|
+
...hostedContext,
|
|
704
|
+
transaction: {
|
|
705
|
+
...hostedTransaction,
|
|
706
|
+
accessList: [{ address: bogus, storageKeys: [] }],
|
|
707
|
+
} as any,
|
|
708
|
+
url: 'https://sponsor.example/tp_key',
|
|
709
|
+
}),
|
|
710
|
+
).rejects.toThrow('accessList is not allowed')
|
|
711
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
test('error: rejects padded calldata before requesting hosted fill', async () => {
|
|
715
|
+
const fetchMock = vi.fn()
|
|
716
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
717
|
+
|
|
718
|
+
await expect(
|
|
719
|
+
fillHostedFeePayerTransaction({
|
|
720
|
+
allowedFeeTokens: defaultAllowedFeeTokens(defaults.chainId.mainnet),
|
|
721
|
+
...hostedContext,
|
|
722
|
+
transaction: {
|
|
723
|
+
...hostedTransaction,
|
|
724
|
+
calls: [
|
|
725
|
+
{
|
|
726
|
+
...hostedTransaction.calls[0],
|
|
727
|
+
data: `${hostedTransaction.calls[0]!.data}01`,
|
|
728
|
+
},
|
|
729
|
+
],
|
|
730
|
+
} as any,
|
|
731
|
+
url: 'https://sponsor.example/tp_key',
|
|
732
|
+
}),
|
|
733
|
+
).rejects.toThrow('calldata is not canonical')
|
|
734
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
735
|
+
})
|
|
671
736
|
})
|
|
672
737
|
|
|
673
738
|
describe('simulationTransaction', () => {
|
|
@@ -948,4 +1013,69 @@ describe('prepareSponsoredTransaction', () => {
|
|
|
948
1013
|
}),
|
|
949
1014
|
).toThrow('validity window exceeds sponsor policy')
|
|
950
1015
|
})
|
|
1016
|
+
|
|
1017
|
+
test('error: rejects non-empty access lists', () => {
|
|
1018
|
+
expect(() =>
|
|
1019
|
+
prepareSponsoredTransaction({
|
|
1020
|
+
account: sponsor,
|
|
1021
|
+
chainId: 42431,
|
|
1022
|
+
details,
|
|
1023
|
+
allowedFeeTokens: [bogus],
|
|
1024
|
+
transaction: {
|
|
1025
|
+
...baseTransaction,
|
|
1026
|
+
accessList: [{ address: bogus, storageKeys: [] }],
|
|
1027
|
+
} as any,
|
|
1028
|
+
}),
|
|
1029
|
+
).toThrow('accessList is not allowed')
|
|
1030
|
+
})
|
|
1031
|
+
|
|
1032
|
+
test('error: rejects padded calldata', () => {
|
|
1033
|
+
expect(() =>
|
|
1034
|
+
prepareSponsoredTransaction({
|
|
1035
|
+
account: sponsor,
|
|
1036
|
+
chainId: 42431,
|
|
1037
|
+
details,
|
|
1038
|
+
allowedFeeTokens: [bogus],
|
|
1039
|
+
transaction: {
|
|
1040
|
+
...baseTransaction,
|
|
1041
|
+
calls: [
|
|
1042
|
+
{
|
|
1043
|
+
...baseTransaction.calls[0],
|
|
1044
|
+
data: `${baseTransaction.calls[0]!.data}01`,
|
|
1045
|
+
},
|
|
1046
|
+
],
|
|
1047
|
+
} as any,
|
|
1048
|
+
}),
|
|
1049
|
+
).toThrow('calldata is not canonical')
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
test('error: rejects missing calls', () => {
|
|
1053
|
+
expect(() =>
|
|
1054
|
+
prepareSponsoredTransaction({
|
|
1055
|
+
account: sponsor,
|
|
1056
|
+
chainId: 42431,
|
|
1057
|
+
details,
|
|
1058
|
+
allowedFeeTokens: [bogus],
|
|
1059
|
+
transaction: {
|
|
1060
|
+
...baseTransaction,
|
|
1061
|
+
calls: undefined,
|
|
1062
|
+
} as any,
|
|
1063
|
+
}),
|
|
1064
|
+
).toThrow('must declare calls')
|
|
1065
|
+
})
|
|
1066
|
+
|
|
1067
|
+
test('error: rejects nonzero call value', () => {
|
|
1068
|
+
expect(() =>
|
|
1069
|
+
prepareSponsoredTransaction({
|
|
1070
|
+
account: sponsor,
|
|
1071
|
+
chainId: 42431,
|
|
1072
|
+
details,
|
|
1073
|
+
allowedFeeTokens: [bogus],
|
|
1074
|
+
transaction: {
|
|
1075
|
+
...baseTransaction,
|
|
1076
|
+
calls: [{ ...baseTransaction.calls[0], value: 1n }],
|
|
1077
|
+
} as any,
|
|
1078
|
+
}),
|
|
1079
|
+
).toThrow('call value is not allowed')
|
|
1080
|
+
})
|
|
951
1081
|
})
|
|
@@ -3,7 +3,7 @@ import type { TempoAddress } from 'ox/tempo'
|
|
|
3
3
|
import { TxEnvelopeTempo } from 'ox/tempo'
|
|
4
4
|
import type { Hex } from 'viem'
|
|
5
5
|
import type { Account } from 'viem'
|
|
6
|
-
import { decodeFunctionData, maxUint256, toHex } from 'viem'
|
|
6
|
+
import { decodeFunctionData, encodeFunctionData, maxUint256, toHex } from 'viem'
|
|
7
7
|
import { Abis, Addresses, Transaction } from 'viem/tempo'
|
|
8
8
|
|
|
9
9
|
import * as TempoAddress_internal from './address.js'
|
|
@@ -181,10 +181,22 @@ function hostedFeePayerRequest(transaction: SponsoredTransaction) {
|
|
|
181
181
|
*/
|
|
182
182
|
export async function fillHostedFeePayerTransaction(parameters: {
|
|
183
183
|
allowedFeeTokens: readonly TempoAddress.Address[]
|
|
184
|
+
challengeExpires?: string | undefined
|
|
185
|
+
chainId: number
|
|
186
|
+
details: Record<string, string>
|
|
187
|
+
policy?: Partial<Policy> | undefined
|
|
184
188
|
transaction: SponsoredTransaction
|
|
185
189
|
url: string
|
|
186
190
|
}) {
|
|
187
|
-
const { allowedFeeTokens, transaction, url } =
|
|
191
|
+
const { allowedFeeTokens, challengeExpires, chainId, details, policy, transaction, url } =
|
|
192
|
+
parameters
|
|
193
|
+
assertTransactionPolicy({
|
|
194
|
+
challengeExpires,
|
|
195
|
+
chainId,
|
|
196
|
+
details,
|
|
197
|
+
policy,
|
|
198
|
+
transaction,
|
|
199
|
+
})
|
|
188
200
|
const response = await fetch(url, {
|
|
189
201
|
body: JSON.stringify(
|
|
190
202
|
{
|
|
@@ -307,6 +319,74 @@ function isExpiringNonceKey(nonceKey: SponsoredTransaction['nonceKey']): boolean
|
|
|
307
319
|
return nonceKey === 'expiring' || nonceKey === maxUint256
|
|
308
320
|
}
|
|
309
321
|
|
|
322
|
+
const sponsoredCallAbi = [...Abis.tip20, ...Abis.stablecoinDex] as const
|
|
323
|
+
const sponsoredCallSelectors = new Set([
|
|
324
|
+
Selectors.approve,
|
|
325
|
+
Selectors.transfer,
|
|
326
|
+
Selectors.transferWithMemo,
|
|
327
|
+
Selectors.swapExactAmountOut,
|
|
328
|
+
])
|
|
329
|
+
|
|
330
|
+
function canonicalSponsoredCallData(
|
|
331
|
+
data: Hex,
|
|
332
|
+
call: string,
|
|
333
|
+
fail: (reason: string, extra?: Record<string, string>) => never,
|
|
334
|
+
) {
|
|
335
|
+
if (!sponsoredCallSelectors.has(data.slice(0, 10) as Hex)) return undefined
|
|
336
|
+
|
|
337
|
+
let decoded: ReturnType<typeof decodeFunctionData<typeof sponsoredCallAbi>>
|
|
338
|
+
try {
|
|
339
|
+
decoded = decodeFunctionData({ abi: sponsoredCallAbi, data })
|
|
340
|
+
} catch {
|
|
341
|
+
fail('fee-sponsored transaction call calldata is invalid', { call })
|
|
342
|
+
}
|
|
343
|
+
return encodeFunctionData({
|
|
344
|
+
abi: sponsoredCallAbi,
|
|
345
|
+
functionName: decoded.functionName,
|
|
346
|
+
args: decoded.args,
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Rejects sponsored transaction fields that increase intrinsic gas without
|
|
352
|
+
* changing the decoded payment intent.
|
|
353
|
+
*/
|
|
354
|
+
function assertCanonicalSponsoredTransaction(
|
|
355
|
+
transaction: SponsoredTransaction,
|
|
356
|
+
fail: (reason: string, extra?: Record<string, string>) => never,
|
|
357
|
+
) {
|
|
358
|
+
if (transaction.accessList !== undefined && transaction.accessList.length > 0)
|
|
359
|
+
fail('fee-sponsored transaction accessList is not allowed', {
|
|
360
|
+
accessListEntries: String(transaction.accessList.length),
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const calls = transaction.calls
|
|
364
|
+
if (!Array.isArray(calls) || calls.length === 0)
|
|
365
|
+
fail('fee-sponsored transaction must declare calls')
|
|
366
|
+
|
|
367
|
+
for (const [index, call] of calls.entries()) {
|
|
368
|
+
const callIndex = String(index)
|
|
369
|
+
const value = call.value
|
|
370
|
+
if (value !== undefined && value !== 0n)
|
|
371
|
+
fail('fee-sponsored transaction call value is not allowed', {
|
|
372
|
+
call: callIndex,
|
|
373
|
+
value: value.toString(),
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
const data = call.data
|
|
377
|
+
if (!data)
|
|
378
|
+
fail('fee-sponsored transaction call is missing calldata', {
|
|
379
|
+
call: callIndex,
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const canonical = canonicalSponsoredCallData(data, callIndex, fail)
|
|
383
|
+
if (canonical && data.toLowerCase() !== canonical.toLowerCase())
|
|
384
|
+
fail('fee-sponsored transaction call calldata is not canonical', {
|
|
385
|
+
call: callIndex,
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
310
390
|
/** Validates that a set of transaction calls matches an allowed fee-payer pattern. */
|
|
311
391
|
export function validateCalls(
|
|
312
392
|
calls: readonly { data?: `0x${string}` | undefined; to?: TempoAddress.Address | undefined }[],
|
|
@@ -433,9 +513,8 @@ export function validateCalls(
|
|
|
433
513
|
}
|
|
434
514
|
}
|
|
435
515
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
allowedFeeTokens?: readonly TempoAddress.Address[] | undefined
|
|
516
|
+
/** Validates sponsor fee policy limits for a fee-payer transaction. */
|
|
517
|
+
export function assertTransactionPolicy(parameters: {
|
|
439
518
|
challengeExpires?: string | undefined
|
|
440
519
|
chainId: number
|
|
441
520
|
details: Record<string, string>
|
|
@@ -444,8 +523,6 @@ export function prepareSponsoredTransaction(parameters: {
|
|
|
444
523
|
transaction: SponsoredTransaction
|
|
445
524
|
}) {
|
|
446
525
|
const {
|
|
447
|
-
account,
|
|
448
|
-
allowedFeeTokens,
|
|
449
526
|
challengeExpires,
|
|
450
527
|
chainId,
|
|
451
528
|
details,
|
|
@@ -454,22 +531,12 @@ export function prepareSponsoredTransaction(parameters: {
|
|
|
454
531
|
transaction,
|
|
455
532
|
} = parameters
|
|
456
533
|
const policy = getPolicy(chainId, policyOverrides)
|
|
457
|
-
const transactionRecord = transaction as Record<string, unknown>
|
|
458
|
-
|
|
459
534
|
const {
|
|
460
|
-
accessList,
|
|
461
|
-
calls,
|
|
462
535
|
chainId: transactionChainId,
|
|
463
|
-
feeToken,
|
|
464
|
-
from,
|
|
465
536
|
gas,
|
|
466
|
-
keyAuthorization,
|
|
467
537
|
maxFeePerGas,
|
|
468
538
|
maxPriorityFeePerGas,
|
|
469
|
-
nonce,
|
|
470
539
|
nonceKey,
|
|
471
|
-
signature,
|
|
472
|
-
validAfter,
|
|
473
540
|
validBefore,
|
|
474
541
|
} = transaction
|
|
475
542
|
|
|
@@ -477,25 +544,6 @@ export function prepareSponsoredTransaction(parameters: {
|
|
|
477
544
|
throw new FeePayerValidationError(reason, { ...details, ...extra })
|
|
478
545
|
}
|
|
479
546
|
|
|
480
|
-
const unsupportedKeys = Object.entries(transaction).flatMap(([key, value]) => {
|
|
481
|
-
if (value === undefined) return []
|
|
482
|
-
if (supportedTransactionKeys.has(key)) return []
|
|
483
|
-
return [key]
|
|
484
|
-
})
|
|
485
|
-
if (unsupportedKeys.length > 0)
|
|
486
|
-
fail('fee-sponsored transaction contains unsupported fields', {
|
|
487
|
-
unsupportedFields: unsupportedKeys.join(', '),
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
const rejectedKeys = rejectedTransactionKeys.filter((key) => {
|
|
491
|
-
const value = transactionRecord[key]
|
|
492
|
-
return value !== undefined && value !== null
|
|
493
|
-
})
|
|
494
|
-
if (rejectedKeys.length > 0)
|
|
495
|
-
fail('fee-sponsored transaction contains rejected fields', {
|
|
496
|
-
rejectedFields: rejectedKeys.join(', '),
|
|
497
|
-
})
|
|
498
|
-
|
|
499
547
|
if (transaction.type !== undefined && transaction.type !== 'tempo')
|
|
500
548
|
fail('fee-sponsored transaction type is invalid', {
|
|
501
549
|
type: String(transaction.type),
|
|
@@ -506,6 +554,8 @@ export function prepareSponsoredTransaction(parameters: {
|
|
|
506
554
|
chainId: String(transactionChainId),
|
|
507
555
|
})
|
|
508
556
|
|
|
557
|
+
assertCanonicalSponsoredTransaction(transaction, fail)
|
|
558
|
+
|
|
509
559
|
if (gas === undefined || gas <= 0n) fail('fee-sponsored transaction must declare gas')
|
|
510
560
|
const gasLimit = gas
|
|
511
561
|
if (gasLimit > policy.maxGas)
|
|
@@ -562,6 +612,77 @@ export function prepareSponsoredTransaction(parameters: {
|
|
|
562
612
|
validBefore: String(validBeforeValue),
|
|
563
613
|
})
|
|
564
614
|
|
|
615
|
+
return { gasLimit, maxFeePerGasValue, validBeforeValue }
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function prepareSponsoredTransaction(parameters: {
|
|
619
|
+
account: Account
|
|
620
|
+
allowedFeeTokens?: readonly TempoAddress.Address[] | undefined
|
|
621
|
+
challengeExpires?: string | undefined
|
|
622
|
+
chainId: number
|
|
623
|
+
details: Record<string, string>
|
|
624
|
+
now?: Date | undefined
|
|
625
|
+
policy?: Partial<Policy> | undefined
|
|
626
|
+
transaction: SponsoredTransaction
|
|
627
|
+
}) {
|
|
628
|
+
const {
|
|
629
|
+
account,
|
|
630
|
+
allowedFeeTokens,
|
|
631
|
+
challengeExpires,
|
|
632
|
+
chainId,
|
|
633
|
+
details,
|
|
634
|
+
now = new Date(),
|
|
635
|
+
policy: policyOverrides,
|
|
636
|
+
transaction,
|
|
637
|
+
} = parameters
|
|
638
|
+
const transactionRecord = transaction as Record<string, unknown>
|
|
639
|
+
|
|
640
|
+
const {
|
|
641
|
+
accessList,
|
|
642
|
+
calls,
|
|
643
|
+
chainId: transactionChainId,
|
|
644
|
+
feeToken,
|
|
645
|
+
from,
|
|
646
|
+
keyAuthorization,
|
|
647
|
+
maxPriorityFeePerGas,
|
|
648
|
+
nonce,
|
|
649
|
+
nonceKey,
|
|
650
|
+
signature,
|
|
651
|
+
validAfter,
|
|
652
|
+
} = transaction
|
|
653
|
+
|
|
654
|
+
const fail = (reason: string, extra: Record<string, string> = {}) => {
|
|
655
|
+
throw new FeePayerValidationError(reason, { ...details, ...extra })
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const unsupportedKeys = Object.entries(transaction).flatMap(([key, value]) => {
|
|
659
|
+
if (value === undefined) return []
|
|
660
|
+
if (supportedTransactionKeys.has(key)) return []
|
|
661
|
+
return [key]
|
|
662
|
+
})
|
|
663
|
+
if (unsupportedKeys.length > 0)
|
|
664
|
+
fail('fee-sponsored transaction contains unsupported fields', {
|
|
665
|
+
unsupportedFields: unsupportedKeys.join(', '),
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
const rejectedKeys = rejectedTransactionKeys.filter((key) => {
|
|
669
|
+
const value = transactionRecord[key]
|
|
670
|
+
return value !== undefined && value !== null
|
|
671
|
+
})
|
|
672
|
+
if (rejectedKeys.length > 0)
|
|
673
|
+
fail('fee-sponsored transaction contains rejected fields', {
|
|
674
|
+
rejectedFields: rejectedKeys.join(', '),
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
const { gasLimit, maxFeePerGasValue, validBeforeValue } = assertTransactionPolicy({
|
|
678
|
+
challengeExpires,
|
|
679
|
+
chainId,
|
|
680
|
+
details,
|
|
681
|
+
now,
|
|
682
|
+
policy: policyOverrides,
|
|
683
|
+
transaction,
|
|
684
|
+
})
|
|
685
|
+
|
|
565
686
|
const normalizedFeeToken = (() => {
|
|
566
687
|
if (feeToken === undefined) return undefined
|
|
567
688
|
if (typeof feeToken !== 'string') fail('fee-sponsored transaction feeToken is invalid')
|
|
@@ -441,6 +441,10 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
441
441
|
if (feePayerUrl && isFeePayerTx) {
|
|
442
442
|
const hosted = await FeePayer.fillHostedFeePayerTransaction({
|
|
443
443
|
allowedFeeTokens,
|
|
444
|
+
challengeExpires: expires,
|
|
445
|
+
chainId: chainId ?? client.chain!.id,
|
|
446
|
+
details: { amount, currency, recipient },
|
|
447
|
+
policy: feePayerPolicy,
|
|
444
448
|
transaction,
|
|
445
449
|
url: feePayerUrl,
|
|
446
450
|
})
|