mppx 0.8.9 → 0.8.11
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 +17 -0
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.d.ts.map +1 -1
- package/dist/cli/validate/challenge.js +21 -9
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/discovery.d.ts.map +1 -1
- package/dist/cli/validate/discovery.js +50 -19
- package/dist/cli/validate/discovery.js.map +1 -1
- package/dist/cli/validate/helpers.d.ts +1 -0
- package/dist/cli/validate/helpers.d.ts.map +1 -1
- package/dist/cli/validate/helpers.js +11 -0
- package/dist/cli/validate/helpers.js.map +1 -1
- package/dist/cli/validate/index.d.ts +1 -0
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +4 -0
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +1 -0
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +2 -2
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +39 -2
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +45 -1
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/internal/fee-token.d.ts +8 -0
- package/dist/tempo/internal/fee-token.d.ts.map +1 -1
- package/dist/tempo/internal/fee-token.js +19 -6
- package/dist/tempo/internal/fee-token.js.map +1 -1
- package/dist/tempo/legacy/session/Chain.d.ts.map +1 -1
- package/dist/tempo/legacy/session/Chain.js +53 -50
- package/dist/tempo/legacy/session/Chain.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +8 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +49 -45
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +7 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js +24 -16
- package/dist/tempo/server/Subscription.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 +3 -0
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +1 -0
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +20 -0
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +23 -11
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +25 -19
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/server/index.d.ts +2 -0
- package/dist/tempo/session/server/index.d.ts.map +1 -1
- package/dist/tempo/session/server/index.js +2 -0
- package/dist/tempo/session/server/index.js.map +1 -1
- package/dist/validation/core.d.ts +1 -0
- package/dist/validation/core.d.ts.map +1 -1
- package/dist/validation/core.js +5 -0
- package/dist/validation/core.js.map +1 -1
- package/dist/x402/Header.js +1 -1
- package/dist/x402/Header.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/validate/challenge.ts +20 -8
- package/src/cli/validate/discovery.test.ts +128 -0
- package/src/cli/validate/discovery.ts +50 -15
- package/src/cli/validate/helpers.ts +10 -0
- package/src/cli/validate/index.ts +4 -0
- package/src/cli/validate/payment.ts +4 -1
- package/src/cli/validate.test.ts +23 -2
- package/src/client/Mppx.test-d.ts +3 -1
- package/src/tempo/PublicExports.test-d.ts +5 -0
- package/src/tempo/internal/fee-payer.test.ts +93 -0
- package/src/tempo/internal/fee-payer.ts +64 -2
- package/src/tempo/internal/fee-token.test.ts +37 -0
- package/src/tempo/internal/fee-token.ts +26 -5
- package/src/tempo/legacy/session/Chain.test.ts +78 -2
- package/src/tempo/legacy/session/Chain.ts +56 -52
- package/src/tempo/server/Charge.test.ts +121 -6
- package/src/tempo/server/Charge.ts +61 -48
- package/src/tempo/server/Methods.ts +6 -0
- package/src/tempo/server/Subscription.test.ts +79 -4
- package/src/tempo/server/Subscription.ts +24 -16
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/client/SessionManager.test.ts +12 -1
- package/src/tempo/session/client/SessionManager.ts +3 -0
- package/src/tempo/session/client/Transports.test.ts +34 -0
- package/src/tempo/session/client/Transports.ts +47 -20
- package/src/tempo/session/precompile/Chain.test.ts +9 -0
- package/src/tempo/session/precompile/Chain.ts +28 -20
- package/src/tempo/session/server/Session.test.ts +23 -3
- package/src/tempo/session/server/index.ts +2 -0
- package/src/validation/core.ts +6 -0
- package/src/x402/Header.ts +1 -1
|
@@ -1619,8 +1619,10 @@ describe('tempo', () => {
|
|
|
1619
1619
|
httpServer.close()
|
|
1620
1620
|
})
|
|
1621
1621
|
|
|
1622
|
-
test('behavior: fee payer
|
|
1623
|
-
//
|
|
1622
|
+
test('behavior: fee payer simulates the sender and final sponsored envelopes', async () => {
|
|
1623
|
+
// First simulate execution as the sender with fee fields omitted. This
|
|
1624
|
+
// catches call-level reverts without requiring the sender to fund fees.
|
|
1625
|
+
// The second simulation must reflect the FINAL co-signed envelope
|
|
1624
1626
|
// (concrete sponsor fee payer), not the pre-cosign 0x78 (`feePayer: true`).
|
|
1625
1627
|
const callRequests: any[] = []
|
|
1626
1628
|
const interceptingClient = createClient({
|
|
@@ -1628,7 +1630,8 @@ describe('tempo', () => {
|
|
|
1628
1630
|
chain: client.chain,
|
|
1629
1631
|
transport: custom({
|
|
1630
1632
|
async request(args: any) {
|
|
1631
|
-
if (args.method === 'eth_call'
|
|
1633
|
+
if (args.method === 'eth_call' && args.params?.[0]?.calls)
|
|
1634
|
+
callRequests.push(args.params[0])
|
|
1632
1635
|
return client.transport.request(args)
|
|
1633
1636
|
},
|
|
1634
1637
|
}),
|
|
@@ -1682,14 +1685,16 @@ describe('tempo', () => {
|
|
|
1682
1685
|
})
|
|
1683
1686
|
expect(authResponse.status).toBe(200)
|
|
1684
1687
|
|
|
1685
|
-
expect(callRequests
|
|
1686
|
-
|
|
1688
|
+
expect(callRequests).toHaveLength(2)
|
|
1689
|
+
expect(callRequests[0]).not.toHaveProperty('feePayer')
|
|
1690
|
+
const simRequest = callRequests[1]
|
|
1687
1691
|
// The co-signed envelope names a concrete sponsor as fee payer. The
|
|
1688
1692
|
// pre-cosign 0x78 instead carries `feePayer: true`; asserting the address
|
|
1689
1693
|
// proves we simulate the transaction the sponsor actually broadcasts.
|
|
1690
1694
|
expect(simRequest.feePayer).not.toBe(true)
|
|
1691
1695
|
expect(typeof simRequest.feePayer).toBe('string')
|
|
1692
1696
|
expect((simRequest.feePayer as string).toLowerCase()).toBe(accounts[0].address.toLowerCase())
|
|
1697
|
+
expect(simRequest.feeToken?.toLowerCase()).toBe(defaults.tokens.pathUsd.toLowerCase())
|
|
1693
1698
|
// Execution runs as the sender, not the sponsor.
|
|
1694
1699
|
expect((simRequest.from as string).toLowerCase()).toBe(accounts[1].address.toLowerCase())
|
|
1695
1700
|
expect(simRequest.calls?.length).toBeGreaterThan(0)
|
|
@@ -1770,6 +1775,77 @@ describe('tempo', () => {
|
|
|
1770
1775
|
httpServer.close()
|
|
1771
1776
|
})
|
|
1772
1777
|
|
|
1778
|
+
test('behavior: fee payer does not broadcast when final simulation reverts', async () => {
|
|
1779
|
+
const rpcMethods: string[] = []
|
|
1780
|
+
let simulations = 0
|
|
1781
|
+
const interceptingClient = createClient({
|
|
1782
|
+
account: accounts[0],
|
|
1783
|
+
chain: client.chain,
|
|
1784
|
+
transport: custom({
|
|
1785
|
+
async request(args: any) {
|
|
1786
|
+
rpcMethods.push(args.method)
|
|
1787
|
+
if (args.method === 'eth_call' && ++simulations >= 2)
|
|
1788
|
+
throw new Error('execution reverted: final simulation fixture')
|
|
1789
|
+
return client.transport.request(args)
|
|
1790
|
+
},
|
|
1791
|
+
}),
|
|
1792
|
+
})
|
|
1793
|
+
|
|
1794
|
+
const serverWithRevert = Mppx_server.create({
|
|
1795
|
+
methods: [
|
|
1796
|
+
tempo_server.charge({
|
|
1797
|
+
getClient() {
|
|
1798
|
+
return interceptingClient
|
|
1799
|
+
},
|
|
1800
|
+
currency: asset,
|
|
1801
|
+
account: accounts[0],
|
|
1802
|
+
}),
|
|
1803
|
+
],
|
|
1804
|
+
realm,
|
|
1805
|
+
secretKey,
|
|
1806
|
+
})
|
|
1807
|
+
|
|
1808
|
+
const mppx = Mppx_client.create({
|
|
1809
|
+
polyfill: false,
|
|
1810
|
+
methods: [
|
|
1811
|
+
tempo_client({
|
|
1812
|
+
account: accounts[1],
|
|
1813
|
+
getClient() {
|
|
1814
|
+
return client
|
|
1815
|
+
},
|
|
1816
|
+
}),
|
|
1817
|
+
],
|
|
1818
|
+
})
|
|
1819
|
+
|
|
1820
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
1821
|
+
const result = await Mppx_server.toNodeListener(
|
|
1822
|
+
serverWithRevert.charge({
|
|
1823
|
+
feePayer: accounts[0],
|
|
1824
|
+
amount: '1',
|
|
1825
|
+
currency: asset,
|
|
1826
|
+
recipient: accounts[0].address,
|
|
1827
|
+
}),
|
|
1828
|
+
)(req, res)
|
|
1829
|
+
if (result.status === 402) return
|
|
1830
|
+
res.end('OK')
|
|
1831
|
+
})
|
|
1832
|
+
|
|
1833
|
+
const challengeResponse = await fetch(httpServer.url)
|
|
1834
|
+
const credential = await mppx.createCredential(challengeResponse)
|
|
1835
|
+
rpcMethods.length = 0
|
|
1836
|
+
|
|
1837
|
+
const authResponse = await fetch(httpServer.url, {
|
|
1838
|
+
headers: { Authorization: credential },
|
|
1839
|
+
})
|
|
1840
|
+
|
|
1841
|
+
expect(authResponse.status).not.toBe(200)
|
|
1842
|
+
expect(simulations).toBeGreaterThanOrEqual(2)
|
|
1843
|
+
expect(rpcMethods).not.toContain('eth_sendRawTransactionSync')
|
|
1844
|
+
expect(rpcMethods).not.toContain('eth_sendRawTransaction')
|
|
1845
|
+
|
|
1846
|
+
httpServer.close()
|
|
1847
|
+
})
|
|
1848
|
+
|
|
1773
1849
|
test('behavior: fee payer fails closed when simulation reverts (optimistic mode)', async () => {
|
|
1774
1850
|
const rpcMethods: string[] = []
|
|
1775
1851
|
const interceptingClient = createClient({
|
|
@@ -2054,11 +2130,30 @@ describe('tempo', () => {
|
|
|
2054
2130
|
|
|
2055
2131
|
test('behavior: fee payer URL (withFeePayer transport)', async () => {
|
|
2056
2132
|
const feePayerRequests: any[] = []
|
|
2133
|
+
const sequence: string[] = []
|
|
2134
|
+
let rejectFinalSimulation = false
|
|
2135
|
+
let simulations = 0
|
|
2136
|
+
const interceptingClient = createClient({
|
|
2137
|
+
account: accounts[0],
|
|
2138
|
+
chain: client.chain,
|
|
2139
|
+
transport: custom({
|
|
2140
|
+
async request(args: any) {
|
|
2141
|
+
if (args.method === 'eth_call' && args.params?.[0]?.calls) {
|
|
2142
|
+
sequence.push('simulate')
|
|
2143
|
+
if (rejectFinalSimulation && ++simulations >= 2)
|
|
2144
|
+
throw new Error('hosted final simulation reverted')
|
|
2145
|
+
}
|
|
2146
|
+
if (args.method === 'eth_sendRawTransactionSync') sequence.push('broadcast')
|
|
2147
|
+
return client.transport.request(args)
|
|
2148
|
+
},
|
|
2149
|
+
}),
|
|
2150
|
+
})
|
|
2057
2151
|
const feePayerServer = await Http.createServer(async (req, res) => {
|
|
2058
2152
|
let requestBody = ''
|
|
2059
2153
|
for await (const chunk of req) requestBody += chunk
|
|
2060
2154
|
const request = JSON.parse(requestBody)
|
|
2061
2155
|
feePayerRequests.push(request)
|
|
2156
|
+
sequence.push('complete')
|
|
2062
2157
|
|
|
2063
2158
|
const transaction = request.params[0]
|
|
2064
2159
|
const quantity = (value: unknown) =>
|
|
@@ -2116,7 +2211,7 @@ describe('tempo', () => {
|
|
|
2116
2211
|
methods: [
|
|
2117
2212
|
tempo_server.charge({
|
|
2118
2213
|
feePayer: feePayerServer.url,
|
|
2119
|
-
getClient: () =>
|
|
2214
|
+
getClient: () => interceptingClient,
|
|
2120
2215
|
currency: asset,
|
|
2121
2216
|
}),
|
|
2122
2217
|
],
|
|
@@ -2149,6 +2244,7 @@ describe('tempo', () => {
|
|
|
2149
2244
|
const response = await mppx.fetch(httpServer.url)
|
|
2150
2245
|
expect(response.status).toBe(200)
|
|
2151
2246
|
expect(feePayerRequests.map(({ method }) => method)).toEqual(['eth_fillTransaction'])
|
|
2247
|
+
expect(sequence).toEqual(['simulate', 'complete', 'simulate', 'broadcast'])
|
|
2152
2248
|
|
|
2153
2249
|
const receipt = Receipt.fromResponse(response)
|
|
2154
2250
|
expect(receipt.status).toBe('success')
|
|
@@ -2158,10 +2254,29 @@ describe('tempo', () => {
|
|
|
2158
2254
|
})
|
|
2159
2255
|
expect((txReceipt as any).feePayer).toBe(accounts[0].address.toLowerCase())
|
|
2160
2256
|
|
|
2257
|
+
sequence.length = 0
|
|
2258
|
+
simulations = 0
|
|
2259
|
+
rejectFinalSimulation = true
|
|
2260
|
+
const rejected = await mppx.fetch(httpServer.url)
|
|
2261
|
+
|
|
2262
|
+
expect(rejected.status).not.toBe(200)
|
|
2263
|
+
expect(feePayerRequests).toHaveLength(2)
|
|
2264
|
+
expect(sequence.slice(0, 2)).toEqual(['simulate', 'complete'])
|
|
2265
|
+
expect(sequence).not.toContain('broadcast')
|
|
2266
|
+
|
|
2161
2267
|
httpServer.close()
|
|
2162
2268
|
feePayerServer.close()
|
|
2163
2269
|
})
|
|
2164
2270
|
|
|
2271
|
+
test('error: rejects a fee token configured for a remote fee payer', () => {
|
|
2272
|
+
expect(() =>
|
|
2273
|
+
tempo_server.charge({
|
|
2274
|
+
feePayer: 'https://fee-payer.example',
|
|
2275
|
+
feeToken: defaults.tokens.pathUsd,
|
|
2276
|
+
}),
|
|
2277
|
+
).toThrow('`feeToken` can only be configured for a local fee payer.')
|
|
2278
|
+
})
|
|
2279
|
+
|
|
2165
2280
|
test('behavior: access keys', async () => {
|
|
2166
2281
|
const rootAccount = accounts[1]
|
|
2167
2282
|
const accessKey = Account.fromSecp256k1(Secp256k1.randomPrivateKey(), {
|
|
@@ -31,6 +31,7 @@ import * as TempoAddress from '../internal/address.js'
|
|
|
31
31
|
import * as Charge_internal from '../internal/charge.js'
|
|
32
32
|
import * as defaults from '../internal/defaults.js'
|
|
33
33
|
import * as FeePayer from '../internal/fee-payer.js'
|
|
34
|
+
import { resolveFeeToken } from '../internal/fee-token.js'
|
|
34
35
|
import * as Proof from '../internal/proof.js'
|
|
35
36
|
import * as Selectors from '../internal/selectors.js'
|
|
36
37
|
import type * as types from '../internal/types.js'
|
|
@@ -59,6 +60,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
59
60
|
decimals = defaults.decimals,
|
|
60
61
|
description,
|
|
61
62
|
externalId,
|
|
63
|
+
feeToken: configuredFeeToken,
|
|
62
64
|
feePayerPolicy,
|
|
63
65
|
html,
|
|
64
66
|
memo,
|
|
@@ -79,6 +81,8 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
79
81
|
: undefined
|
|
80
82
|
|
|
81
83
|
const { recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
|
|
84
|
+
if (configuredFeeToken && feePayerUrl)
|
|
85
|
+
throw new Error('`feeToken` can only be configured for a local fee payer.')
|
|
82
86
|
|
|
83
87
|
const getClient = Client.getResolver({
|
|
84
88
|
chain: { ...tempo_chain, experimental_preconfirmationTime: 500 },
|
|
@@ -405,67 +409,68 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
405
409
|
|
|
406
410
|
const allowedFeeTokens = FeePayer.defaultAllowedFeeTokens(chainId)
|
|
407
411
|
if (isFeePayerTx) FeePayer.assertAllowedFeeToken(transaction, allowedFeeTokens)
|
|
408
|
-
|
|
409
|
-
// Request for the pre-broadcast simulation; for sponsored payments
|
|
410
|
-
// this is overwritten below with the co-signed shape.
|
|
411
|
-
let simulationRequest: Record<string, unknown> = FeePayer.simulationTransaction(
|
|
412
|
-
transaction,
|
|
413
|
-
{ feePayer: isFeePayerTx },
|
|
414
|
-
)
|
|
412
|
+
const selectableFeeTokens = allowedFeeTokens as readonly `0x${string}`[]
|
|
415
413
|
|
|
416
414
|
const serializedTransaction_final = await (async () => {
|
|
417
415
|
if (feePayerAccount && methodDetails?.feePayer !== false) {
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
416
|
+
const completed = await FeePayer.preflightSponsorship({
|
|
417
|
+
transaction,
|
|
418
|
+
simulate: (request) => viem_call(client, request as never),
|
|
419
|
+
async complete() {
|
|
420
|
+
const feeToken =
|
|
421
|
+
configuredFeeToken ??
|
|
422
|
+
(await resolveFeeToken({
|
|
423
|
+
account: feePayerAccount.address,
|
|
424
|
+
allowedTokens: selectableFeeTokens,
|
|
425
|
+
candidateTokens: selectableFeeTokens,
|
|
426
|
+
client,
|
|
427
|
+
prioritizeCandidates: true,
|
|
428
|
+
}))
|
|
429
|
+
const sponsored = FeePayer.prepareSponsoredTransaction({
|
|
430
|
+
account: feePayerAccount,
|
|
431
|
+
allowedFeeTokens,
|
|
432
|
+
challengeExpires: expires,
|
|
433
|
+
chainId: chainId ?? client.chain!.id,
|
|
434
|
+
details: { amount, currency, recipient },
|
|
435
|
+
policy: feePayerPolicy,
|
|
436
|
+
transaction: {
|
|
437
|
+
...transaction,
|
|
438
|
+
...(feeToken ? { feeToken } : {}),
|
|
439
|
+
},
|
|
440
|
+
})
|
|
441
|
+
return { feePayer: feePayerAccount.address, transaction: sponsored }
|
|
428
442
|
},
|
|
429
443
|
})
|
|
430
|
-
|
|
431
|
-
// sponsor that pays gas.
|
|
432
|
-
simulationRequest = {
|
|
433
|
-
...sponsored,
|
|
434
|
-
account: transaction.from,
|
|
435
|
-
feePayer: feePayerAccount.address,
|
|
436
|
-
feePayerSignature: undefined,
|
|
437
|
-
signature: undefined,
|
|
438
|
-
}
|
|
439
|
-
return signTransaction(client, sponsored as never)
|
|
444
|
+
return signTransaction(client, completed.transaction as never)
|
|
440
445
|
}
|
|
441
446
|
if (feePayerUrl && isFeePayerTx) {
|
|
442
|
-
const
|
|
443
|
-
allowedFeeTokens,
|
|
444
|
-
challengeExpires: expires,
|
|
445
|
-
chainId: chainId ?? client.chain!.id,
|
|
446
|
-
details: { amount, currency, recipient },
|
|
447
|
-
policy: feePayerPolicy,
|
|
447
|
+
const completed = await FeePayer.preflightSponsorship({
|
|
448
448
|
transaction,
|
|
449
|
-
|
|
449
|
+
simulate: (request) => viem_call(client, request as never),
|
|
450
|
+
async complete() {
|
|
451
|
+
const hosted = await FeePayer.fillHostedFeePayerTransaction({
|
|
452
|
+
allowedFeeTokens,
|
|
453
|
+
challengeExpires: expires,
|
|
454
|
+
chainId: chainId ?? client.chain!.id,
|
|
455
|
+
details: { amount, currency, recipient },
|
|
456
|
+
policy: feePayerPolicy,
|
|
457
|
+
transaction,
|
|
458
|
+
url: feePayerUrl,
|
|
459
|
+
})
|
|
460
|
+
return { ...hosted, transaction }
|
|
461
|
+
},
|
|
450
462
|
})
|
|
451
|
-
|
|
452
|
-
// fee token), mirroring the local-sponsor path.
|
|
453
|
-
simulationRequest = {
|
|
454
|
-
...transaction,
|
|
455
|
-
account: transaction.from,
|
|
456
|
-
feePayer: hosted.feePayer,
|
|
457
|
-
feePayerSignature: undefined,
|
|
458
|
-
feeToken: hosted.feeToken,
|
|
459
|
-
signature: undefined,
|
|
460
|
-
}
|
|
461
|
-
return hosted.serializedTransaction
|
|
463
|
+
return completed.serializedTransaction
|
|
462
464
|
}
|
|
463
465
|
return serializedTransaction
|
|
464
466
|
})()
|
|
465
467
|
|
|
466
|
-
// Pre-broadcast simulation
|
|
467
|
-
|
|
468
|
-
|
|
468
|
+
// Pre-broadcast simulation for non-sponsored transactions.
|
|
469
|
+
if (!isFeePayerTx)
|
|
470
|
+
await viem_call(
|
|
471
|
+
client,
|
|
472
|
+
FeePayer.simulationTransaction(transaction, { feePayer: false }) as never,
|
|
473
|
+
)
|
|
469
474
|
|
|
470
475
|
if (waitForConfirmation) {
|
|
471
476
|
const receipt = await sendRawTransactionSync(client, {
|
|
@@ -563,6 +568,14 @@ export declare namespace charge {
|
|
|
563
568
|
* `maxTotalFee` so the combined fee budget remains valid.
|
|
564
569
|
*/
|
|
565
570
|
feePayerPolicy?: FeePayerPolicy | undefined
|
|
571
|
+
/**
|
|
572
|
+
* Token a local fee payer uses to pay gas. If omitted, mppx selects a
|
|
573
|
+
* funded allowed token, preferring pathUSD.
|
|
574
|
+
*
|
|
575
|
+
* This option is not supported with a remote fee-payer URL, which selects
|
|
576
|
+
* its own token.
|
|
577
|
+
*/
|
|
578
|
+
feeToken?: `0x${string}` | undefined
|
|
566
579
|
/** Testnet mode. */
|
|
567
580
|
testnet?: boolean | undefined
|
|
568
581
|
/**
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
settle as settle_,
|
|
6
6
|
settleBatch as settleBatch_,
|
|
7
7
|
} from '../session/server/Session.js'
|
|
8
|
+
import type { SessionController as SessionController_ } from '../session/server/Sse.js'
|
|
8
9
|
import * as Ws_ from '../session/server/Ws.js'
|
|
9
10
|
import { charge as charge_ } from './Charge.js'
|
|
10
11
|
import { renew as renewSubscription_, subscription as subscription_ } from './Subscription.js'
|
|
@@ -83,6 +84,11 @@ export namespace tempo {
|
|
|
83
84
|
export const settle = settle_
|
|
84
85
|
/** Batch-settle precompile-backed session channels. */
|
|
85
86
|
export const settleBatch = settleBatch_
|
|
87
|
+
/** Types for Tempo session streams. */
|
|
88
|
+
export namespace Sse {
|
|
89
|
+
/** Controller passed to manual-charge SSE generators. */
|
|
90
|
+
export type SessionController = SessionController_
|
|
91
|
+
}
|
|
86
92
|
/** Experimental websocket helpers for Tempo sessions. */
|
|
87
93
|
export const Ws = Ws_
|
|
88
94
|
}
|
|
@@ -112,22 +112,76 @@ async function createCredential(
|
|
|
112
112
|
})
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function createBillingClient(
|
|
115
|
+
function createBillingClient(
|
|
116
|
+
hashes: readonly string[],
|
|
117
|
+
options: { account?: typeof rootAccount; failSimulation?: number | undefined } = {},
|
|
118
|
+
) {
|
|
119
|
+
const callRequests: Record<string, unknown>[] = []
|
|
116
120
|
const rpcMethods: string[] = []
|
|
117
121
|
let nextHash = 0
|
|
122
|
+
let simulations = 0
|
|
118
123
|
const client = createClient({
|
|
124
|
+
account: options.account,
|
|
119
125
|
chain: { ...tempo_chain, id: chainId },
|
|
120
126
|
transport: custom({
|
|
121
|
-
async request({ method }) {
|
|
127
|
+
async request({ method, params }) {
|
|
122
128
|
rpcMethods.push(method)
|
|
123
129
|
if (method === 'eth_chainId') return `0x${chainId.toString(16)}`
|
|
124
|
-
if (method === '
|
|
130
|
+
if (method === 'eth_getTransactionCount') return '0x0'
|
|
131
|
+
if (method === 'eth_estimateGas') return '0x5208'
|
|
132
|
+
if (method === 'eth_maxPriorityFeePerGas') return '0x1'
|
|
133
|
+
if (method === 'eth_getBlockByNumber') return { baseFeePerGas: '0x1' }
|
|
134
|
+
if (method === 'eth_call') {
|
|
135
|
+
callRequests.push((params as [Record<string, unknown>])[0])
|
|
136
|
+
if (options.failSimulation !== undefined && ++simulations >= options.failSimulation)
|
|
137
|
+
throw new Error('final sponsored simulation reverted')
|
|
138
|
+
return '0x'
|
|
139
|
+
}
|
|
125
140
|
if (method === 'eth_sendRawTransaction') return hashes[nextHash++] ?? hashActivate
|
|
126
141
|
throw new Error(`unexpected rpc method: ${method}`)
|
|
127
142
|
},
|
|
128
143
|
}),
|
|
129
144
|
})
|
|
130
|
-
return { client, rpcMethods }
|
|
145
|
+
return { callRequests, client, rpcMethods }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function activateFeeSponsoredSubscription(options?: { failSimulation?: number | undefined }) {
|
|
149
|
+
const store = Store.memory()
|
|
150
|
+
const billing = createBillingClient([hashActivate], {
|
|
151
|
+
account: rootAccount,
|
|
152
|
+
...options,
|
|
153
|
+
})
|
|
154
|
+
const method = subscription({
|
|
155
|
+
amount: subscriptionAmount,
|
|
156
|
+
chainId,
|
|
157
|
+
currency: subscriptionCurrency,
|
|
158
|
+
feePayer: rootAccount,
|
|
159
|
+
getClient: async () => billing.client,
|
|
160
|
+
periodCount: subscriptionPeriodCount,
|
|
161
|
+
periodUnit: subscriptionPeriodUnit,
|
|
162
|
+
recipient: subscriptionRecipient,
|
|
163
|
+
resolve: async () => ({ key: subscriptionKey }),
|
|
164
|
+
store,
|
|
165
|
+
subscriptionExpires: activeSubscriptionExpires,
|
|
166
|
+
waitForConfirmation: false,
|
|
167
|
+
})
|
|
168
|
+
const mppx = Mppx.create({ methods: [method], realm, secretKey })
|
|
169
|
+
const challengeResult = await mppx.tempo.subscription({})(
|
|
170
|
+
new Request('https://example.com/resource'),
|
|
171
|
+
)
|
|
172
|
+
if (challengeResult.status !== 402) throw new Error('expected activation challenge')
|
|
173
|
+
const challenge = Challenge.fromResponse(challengeResult.challenge)
|
|
174
|
+
const accessKey = (
|
|
175
|
+
challenge.request as ReturnType<typeof Methods.subscription.schema.request.parse>
|
|
176
|
+
).methodDetails?.accessKey
|
|
177
|
+
if (!accessKey) throw new Error('expected generated access key')
|
|
178
|
+
const credential = await createCredential(challenge, rootAccount.address, accessKey)
|
|
179
|
+
const result = await mppx.tempo.subscription({})(
|
|
180
|
+
new Request('https://example.com/resource', {
|
|
181
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
182
|
+
}),
|
|
183
|
+
)
|
|
184
|
+
return { ...billing, result }
|
|
131
185
|
}
|
|
132
186
|
|
|
133
187
|
const confirmedBlockHash = `0x${'f'.repeat(64)}` as const
|
|
@@ -234,6 +288,27 @@ function createConfirmingBillingClient(options?: {
|
|
|
234
288
|
}
|
|
235
289
|
|
|
236
290
|
describe('tempo.subscription', () => {
|
|
291
|
+
test('preflights sender and final envelopes for fee-sponsored activation', async () => {
|
|
292
|
+
const { callRequests, result, rpcMethods } = await activateFeeSponsoredSubscription()
|
|
293
|
+
|
|
294
|
+
expect(result.status).toBe(200)
|
|
295
|
+
expect(callRequests).toHaveLength(2)
|
|
296
|
+
expect(callRequests[0]).not.toHaveProperty('feePayer')
|
|
297
|
+
expect(callRequests[1]?.feePayer).toBe(rootAccount.address)
|
|
298
|
+
expect(rpcMethods).toContain('eth_sendRawTransaction')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('does not broadcast a fee-sponsored activation after final simulation failure', async () => {
|
|
302
|
+
const { callRequests, result, rpcMethods } = await activateFeeSponsoredSubscription({
|
|
303
|
+
failSimulation: 2,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
expect(result.status).not.toBe(200)
|
|
307
|
+
expect(callRequests.length).toBeGreaterThanOrEqual(2)
|
|
308
|
+
expect(rpcMethods).not.toContain('eth_sendRawTransaction')
|
|
309
|
+
expect(rpcMethods).not.toContain('eth_sendRawTransactionSync')
|
|
310
|
+
})
|
|
311
|
+
|
|
237
312
|
test('stores an activated subscription and reuses it on later requests', async () => {
|
|
238
313
|
const store = Store.memory()
|
|
239
314
|
let activationCount = 0
|
|
@@ -965,28 +965,36 @@ async function submitSubscriptionPayment(parameters: {
|
|
|
965
965
|
const userTransaction = Transaction.deserialize(
|
|
966
966
|
userSerialized as Transaction.TransactionSerializedTempo,
|
|
967
967
|
)
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
968
|
+
const completed = await FeePayer.preflightSponsorship({
|
|
969
|
+
transaction: userTransaction,
|
|
970
|
+
simulate: (request) => viem_call(client, request as never),
|
|
971
|
+
async complete() {
|
|
972
|
+
const sponsored = FeePayer.prepareSponsoredTransaction({
|
|
973
|
+
account: feePayer,
|
|
974
|
+
chainId: chainId ?? client.chain!.id,
|
|
975
|
+
details: {
|
|
976
|
+
amount: String(request.amount),
|
|
977
|
+
currency: String(request.currency),
|
|
978
|
+
recipient: String(request.recipient),
|
|
979
|
+
},
|
|
980
|
+
...(feePayerPolicy ? { policy: feePayerPolicy } : {}),
|
|
981
|
+
transaction: userTransaction as never,
|
|
982
|
+
})
|
|
983
|
+
return { feePayer: feePayer.address, transaction: sponsored }
|
|
975
984
|
},
|
|
976
|
-
...(feePayerPolicy ? { policy: feePayerPolicy } : {}),
|
|
977
|
-
transaction: userTransaction as never,
|
|
978
985
|
})
|
|
979
|
-
return await signTransaction(client,
|
|
986
|
+
return await signTransaction(client, completed.transaction as never)
|
|
980
987
|
})()
|
|
981
988
|
const transaction = Transaction.deserialize(
|
|
982
989
|
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
983
990
|
)
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
991
|
+
if (!feePayer)
|
|
992
|
+
await viem_call(client, {
|
|
993
|
+
...transaction,
|
|
994
|
+
account: transaction.from,
|
|
995
|
+
calls: transaction.calls,
|
|
996
|
+
feePayerSignature: undefined,
|
|
997
|
+
} as never)
|
|
990
998
|
|
|
991
999
|
if (!waitForConfirmation) {
|
|
992
1000
|
// Optimistic mode has no receipt to inspect, so it cannot detect a T6
|