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
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
defaultAllowedFeeTokens,
|
|
12
12
|
FeePayerValidationError,
|
|
13
13
|
fillHostedFeePayerTransaction,
|
|
14
|
+
preflightSponsorship,
|
|
14
15
|
prepareSponsoredTransaction,
|
|
15
16
|
simulationTransaction,
|
|
17
|
+
sponsoredSimulationTransaction,
|
|
16
18
|
validateCalls,
|
|
17
19
|
} from './fee-payer.js'
|
|
18
20
|
import * as Selectors from './selectors.js'
|
|
@@ -768,6 +770,97 @@ describe('simulationTransaction', () => {
|
|
|
768
770
|
feePayerSignature: undefined,
|
|
769
771
|
})
|
|
770
772
|
})
|
|
773
|
+
|
|
774
|
+
test('builds the final sponsored envelope without either signature', () => {
|
|
775
|
+
const transaction = {
|
|
776
|
+
calls: [{ to: bogus }],
|
|
777
|
+
feePayerSignature,
|
|
778
|
+
feeToken: swapTokenIn,
|
|
779
|
+
from: bogus,
|
|
780
|
+
signature: { r: 1n, s: 2n, yParity: 0 },
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
expect(
|
|
784
|
+
sponsoredSimulationTransaction(transaction as any, {
|
|
785
|
+
feePayer: swapTokenOut,
|
|
786
|
+
feeToken: bogus,
|
|
787
|
+
}),
|
|
788
|
+
).toEqual({
|
|
789
|
+
...transaction,
|
|
790
|
+
account: bogus,
|
|
791
|
+
calls: transaction.calls,
|
|
792
|
+
feePayer: swapTokenOut,
|
|
793
|
+
feePayerSignature: undefined,
|
|
794
|
+
feeToken: bogus,
|
|
795
|
+
signature: undefined,
|
|
796
|
+
})
|
|
797
|
+
})
|
|
798
|
+
})
|
|
799
|
+
|
|
800
|
+
describe('preflightSponsorship', () => {
|
|
801
|
+
const transaction = {
|
|
802
|
+
calls: [{ to: bogus }],
|
|
803
|
+
feeToken: swapTokenIn,
|
|
804
|
+
from: bogus,
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
test('simulates the sender before completion and the completed envelope after', async () => {
|
|
808
|
+
const simulations: Record<string, unknown>[] = []
|
|
809
|
+
let completions = 0
|
|
810
|
+
|
|
811
|
+
const result = await preflightSponsorship({
|
|
812
|
+
transaction: transaction as any,
|
|
813
|
+
async simulate(request) {
|
|
814
|
+
simulations.push(request)
|
|
815
|
+
},
|
|
816
|
+
async complete() {
|
|
817
|
+
completions += 1
|
|
818
|
+
return { feePayer: swapTokenOut, transaction: transaction as any }
|
|
819
|
+
},
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
expect(completions).toBe(1)
|
|
823
|
+
expect(result.feePayer).toBe(swapTokenOut)
|
|
824
|
+
expect(simulations).toEqual([
|
|
825
|
+
{ account: bogus, calls: transaction.calls },
|
|
826
|
+
{
|
|
827
|
+
...transaction,
|
|
828
|
+
account: bogus,
|
|
829
|
+
calls: transaction.calls,
|
|
830
|
+
feePayer: swapTokenOut,
|
|
831
|
+
feePayerSignature: undefined,
|
|
832
|
+
signature: undefined,
|
|
833
|
+
},
|
|
834
|
+
])
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
test.each([
|
|
838
|
+
{ completeCalls: 0, failingSimulation: 1, simulationCalls: 1 },
|
|
839
|
+
{ completeCalls: 1, failingSimulation: 2, simulationCalls: 2 },
|
|
840
|
+
])(
|
|
841
|
+
'stops at simulation $failingSimulation',
|
|
842
|
+
async ({ completeCalls, failingSimulation, simulationCalls }) => {
|
|
843
|
+
let completions = 0
|
|
844
|
+
let simulations = 0
|
|
845
|
+
|
|
846
|
+
await expect(
|
|
847
|
+
preflightSponsorship({
|
|
848
|
+
transaction: transaction as any,
|
|
849
|
+
async simulate() {
|
|
850
|
+
simulations += 1
|
|
851
|
+
if (simulations === failingSimulation) throw new Error('simulation failed')
|
|
852
|
+
},
|
|
853
|
+
async complete() {
|
|
854
|
+
completions += 1
|
|
855
|
+
return { feePayer: swapTokenOut, transaction: transaction as any }
|
|
856
|
+
},
|
|
857
|
+
}),
|
|
858
|
+
).rejects.toThrow('simulation failed')
|
|
859
|
+
|
|
860
|
+
expect(completions).toBe(completeCalls)
|
|
861
|
+
expect(simulations).toBe(simulationCalls)
|
|
862
|
+
},
|
|
863
|
+
)
|
|
771
864
|
})
|
|
772
865
|
|
|
773
866
|
describe('prepareSponsoredTransaction', () => {
|
|
@@ -228,7 +228,7 @@ export async function fillHostedFeePayerTransaction(parameters: {
|
|
|
228
228
|
...(filled.feePayerSignature as Record<string, unknown>),
|
|
229
229
|
yParity: Number((filled.feePayerSignature as { yParity?: unknown }).yParity),
|
|
230
230
|
}
|
|
231
|
-
const feeToken = filled.feeToken
|
|
231
|
+
const feeToken = filled.feeToken as TempoAddress.Address
|
|
232
232
|
|
|
233
233
|
// Recover the concrete sponsor address so the simulation can use a concrete
|
|
234
234
|
// `feePayer` (the node rejects `eth_call` with `feePayer: true`).
|
|
@@ -266,7 +266,14 @@ export async function fillHostedFeePayerTransaction(parameters: {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
/**
|
|
269
|
+
/**
|
|
270
|
+
* Returns a transaction shape suitable for pre-broadcast simulation.
|
|
271
|
+
*
|
|
272
|
+
* Sponsored transactions are first simulated as calls from the sender with no
|
|
273
|
+
* fee fields or signatures. This checks call execution without requiring the
|
|
274
|
+
* sender to hold the fee token; transferred value and call-level balances are
|
|
275
|
+
* still checked by the RPC.
|
|
276
|
+
*/
|
|
270
277
|
export function simulationTransaction(
|
|
271
278
|
transaction: SponsoredTransaction,
|
|
272
279
|
options: { feePayer: boolean },
|
|
@@ -284,6 +291,61 @@ export function simulationTransaction(
|
|
|
284
291
|
}
|
|
285
292
|
}
|
|
286
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Returns the final fee-sponsored transaction shape for pre-broadcast
|
|
296
|
+
* simulation. RPC `eth_call` does not carry either transaction signature.
|
|
297
|
+
*/
|
|
298
|
+
export function sponsoredSimulationTransaction(
|
|
299
|
+
transaction: SponsoredTransaction,
|
|
300
|
+
options: { feePayer: TempoAddress.Address; feeToken?: TempoAddress.Address | undefined },
|
|
301
|
+
) {
|
|
302
|
+
const { feePayer, feeToken } = options
|
|
303
|
+
return {
|
|
304
|
+
...transaction,
|
|
305
|
+
account: transaction.from,
|
|
306
|
+
calls: transaction.calls,
|
|
307
|
+
feePayer,
|
|
308
|
+
feePayerSignature: undefined,
|
|
309
|
+
...(feeToken !== undefined ? { feeToken } : {}),
|
|
310
|
+
signature: undefined,
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** A completed sponsorship envelope that can be simulated before signing or broadcast. */
|
|
315
|
+
export type PreflightSponsorship = {
|
|
316
|
+
feePayer: TempoAddress.Address
|
|
317
|
+
feeToken?: TempoAddress.Address | undefined
|
|
318
|
+
transaction: SponsoredTransaction
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Runs execution-only sender and final-envelope simulations around sponsorship.
|
|
323
|
+
*
|
|
324
|
+
* First, it simulates the calls with the sender as `from`, omitting fee fields
|
|
325
|
+
* and signatures so the sender's fee balance is irrelevant. Next, `complete`
|
|
326
|
+
* resolves the sponsor and produces the co-signed transaction. Finally, it
|
|
327
|
+
* simulates that transaction with its concrete fee payer and fee token.
|
|
328
|
+
*
|
|
329
|
+
* `complete` runs only after sender-context execution succeeds, so a reverting
|
|
330
|
+
* transaction never reaches a local signer or hosted fee-payer.
|
|
331
|
+
*/
|
|
332
|
+
export async function preflightSponsorship<sponsorship extends PreflightSponsorship>(parameters: {
|
|
333
|
+
complete: () => Promise<sponsorship>
|
|
334
|
+
simulate: (request: Record<string, unknown>) => Promise<unknown>
|
|
335
|
+
transaction: SponsoredTransaction
|
|
336
|
+
}): Promise<sponsorship> {
|
|
337
|
+
const { complete, simulate, transaction } = parameters
|
|
338
|
+
await simulate(simulationTransaction(transaction, { feePayer: true }))
|
|
339
|
+
const completed = await complete()
|
|
340
|
+
await simulate(
|
|
341
|
+
sponsoredSimulationTransaction(completed.transaction, {
|
|
342
|
+
feePayer: completed.feePayer,
|
|
343
|
+
feeToken: completed.feeToken,
|
|
344
|
+
}),
|
|
345
|
+
)
|
|
346
|
+
return completed
|
|
347
|
+
}
|
|
348
|
+
|
|
287
349
|
/**
|
|
288
350
|
* maxTotalFee must be high enough to cover `transferWithMemo` and
|
|
289
351
|
* swap transactions at peak gas prices. Bumped from 0.01 ETH in #327.
|
|
@@ -77,6 +77,43 @@ describe.runIf(isLocalnet)('resolveFeeToken', () => {
|
|
|
77
77
|
expectAddress(feeToken, asset)
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
test('prioritizes funded allowed candidates', async () => {
|
|
81
|
+
const account = testAccount()
|
|
82
|
+
const client = clientFor(account)
|
|
83
|
+
await fundAccount({ address: account.address, token: asset })
|
|
84
|
+
await fundAccount({ address: account.address, token: Addresses.pathUsd })
|
|
85
|
+
await Actions.fee.setUserTokenSync(client, {
|
|
86
|
+
feeToken: asset,
|
|
87
|
+
token: asset,
|
|
88
|
+
} as never)
|
|
89
|
+
|
|
90
|
+
const feeToken = await resolveFeeToken({
|
|
91
|
+
account: account.address,
|
|
92
|
+
allowedTokens: [Addresses.pathUsd, asset],
|
|
93
|
+
candidateTokens: [Addresses.pathUsd, asset],
|
|
94
|
+
client,
|
|
95
|
+
prioritizeCandidates: true,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
expectAddress(feeToken, Addresses.pathUsd)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('falls through prioritized candidates when pathUSD is unfunded', async () => {
|
|
102
|
+
const account = testAccount()
|
|
103
|
+
const client = clientFor(account)
|
|
104
|
+
await fundAccount({ address: account.address, token: asset })
|
|
105
|
+
|
|
106
|
+
const feeToken = await resolveFeeToken({
|
|
107
|
+
account: account.address,
|
|
108
|
+
allowedTokens: [Addresses.pathUsd, asset],
|
|
109
|
+
candidateTokens: [Addresses.pathUsd, asset],
|
|
110
|
+
client,
|
|
111
|
+
prioritizeCandidates: true,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expectAddress(feeToken, asset)
|
|
115
|
+
})
|
|
116
|
+
|
|
80
117
|
test('uses a funded chain fee token when configured', async () => {
|
|
81
118
|
const account = testAccount()
|
|
82
119
|
const client = createClient({
|
|
@@ -4,8 +4,17 @@ import { Actions, TokenId } from 'viem/tempo'
|
|
|
4
4
|
import * as TempoAddress from './address.js'
|
|
5
5
|
import * as defaults from './defaults.js'
|
|
6
6
|
|
|
7
|
-
function pushUnique(
|
|
7
|
+
function pushUnique(
|
|
8
|
+
tokens: Address[],
|
|
9
|
+
token: Address | undefined,
|
|
10
|
+
allowedTokens?: readonly Address[] | undefined,
|
|
11
|
+
) {
|
|
8
12
|
if (!token) return
|
|
13
|
+
if (
|
|
14
|
+
allowedTokens &&
|
|
15
|
+
!allowedTokens.some((allowedToken) => TempoAddress.isEqual(allowedToken, token))
|
|
16
|
+
)
|
|
17
|
+
return
|
|
9
18
|
if (tokens.some((t) => TempoAddress.isEqual(t, token))) return
|
|
10
19
|
tokens.push(token)
|
|
11
20
|
}
|
|
@@ -27,21 +36,33 @@ function getChainFeeToken(client: Client): Address | undefined {
|
|
|
27
36
|
return chainId ? defaults.currency[chainId as keyof typeof defaults.currency] : undefined
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Resolves a funded fee token from account, chain, and caller-supplied preferences.
|
|
41
|
+
*
|
|
42
|
+
* `prioritizeCandidates` checks candidate tokens before account and chain
|
|
43
|
+
* preferences. `allowedTokens` limits every preference to the caller's policy.
|
|
44
|
+
*/
|
|
30
45
|
export async function resolveFeeToken(parameters: {
|
|
31
46
|
account: Address
|
|
47
|
+
allowedTokens?: readonly Address[] | undefined
|
|
32
48
|
candidateTokens?: readonly Address[] | undefined
|
|
33
49
|
client: Client
|
|
50
|
+
prioritizeCandidates?: boolean | undefined
|
|
34
51
|
}): Promise<Address | undefined> {
|
|
35
|
-
const { account, candidateTokens, client } = parameters
|
|
52
|
+
const { account, allowedTokens, candidateTokens, client, prioritizeCandidates } = parameters
|
|
36
53
|
const tokens: Address[] = []
|
|
37
54
|
|
|
55
|
+
if (prioritizeCandidates)
|
|
56
|
+
for (const token of candidateTokens ?? []) pushUnique(tokens, token, allowedTokens)
|
|
57
|
+
|
|
38
58
|
const userToken = await Actions.fee
|
|
39
59
|
.getUserToken(client as never, { account })
|
|
40
60
|
.then((token) => token?.address as Address | undefined)
|
|
41
61
|
.catch(() => undefined)
|
|
42
|
-
pushUnique(tokens, userToken)
|
|
43
|
-
pushUnique(tokens, getChainFeeToken(client))
|
|
44
|
-
|
|
62
|
+
pushUnique(tokens, userToken, allowedTokens)
|
|
63
|
+
pushUnique(tokens, getChainFeeToken(client), allowedTokens)
|
|
64
|
+
if (!prioritizeCandidates)
|
|
65
|
+
for (const token of candidateTokens ?? []) pushUnique(tokens, token, allowedTokens)
|
|
45
66
|
|
|
46
67
|
for (const token of tokens) {
|
|
47
68
|
if (await hasBalance(client, account, token)) return token
|
|
@@ -457,7 +457,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
457
457
|
).rejects.toThrow('gas exceeds sponsor policy')
|
|
458
458
|
})
|
|
459
459
|
|
|
460
|
-
test('fee-payer: simulates open before broadcasting', async () => {
|
|
460
|
+
test('fee-payer: simulates open before broadcasting and recovers duplicate opens', async () => {
|
|
461
461
|
const rpcMethods: string[] = []
|
|
462
462
|
const interceptingClient = createClient({
|
|
463
463
|
account: accounts[0],
|
|
@@ -521,6 +521,22 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
521
521
|
expect(broadcastIndex).toBeGreaterThan(-1)
|
|
522
522
|
expect(simulationIndex).toBeGreaterThan(-1)
|
|
523
523
|
expect(simulationIndex).toBeLessThan(broadcastIndex)
|
|
524
|
+
expect(
|
|
525
|
+
rpcMethods.slice(0, broadcastIndex).filter((method) => method === 'eth_call'),
|
|
526
|
+
).toHaveLength(2)
|
|
527
|
+
|
|
528
|
+
const recovered = await broadcastOpenTransaction({
|
|
529
|
+
client: interceptingClient,
|
|
530
|
+
serializedTransaction,
|
|
531
|
+
escrowContract,
|
|
532
|
+
channelId,
|
|
533
|
+
recipient,
|
|
534
|
+
currency,
|
|
535
|
+
feePayer: accounts[0],
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
expect(recovered.txHash).toBeUndefined()
|
|
539
|
+
expect(recovered.onChain.deposit).toBe(deposit)
|
|
524
540
|
})
|
|
525
541
|
|
|
526
542
|
test('fee-payer: rejects smuggled second open call', async () => {
|
|
@@ -620,6 +636,17 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
620
636
|
})
|
|
621
637
|
|
|
622
638
|
test('waitForConfirmation: false returns derived on-chain state', async () => {
|
|
639
|
+
const rpcMethods: string[] = []
|
|
640
|
+
const interceptingClient = createClient({
|
|
641
|
+
account: accounts[0],
|
|
642
|
+
chain: client.chain,
|
|
643
|
+
transport: custom({
|
|
644
|
+
async request(args: any) {
|
|
645
|
+
rpcMethods.push(args.method)
|
|
646
|
+
return client.transport.request(args)
|
|
647
|
+
},
|
|
648
|
+
}),
|
|
649
|
+
})
|
|
623
650
|
const salt = nextSalt()
|
|
624
651
|
const deposit = 10_000_000n
|
|
625
652
|
|
|
@@ -633,7 +660,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
633
660
|
})
|
|
634
661
|
|
|
635
662
|
const result = await broadcastOpenTransaction({
|
|
636
|
-
client,
|
|
663
|
+
client: interceptingClient,
|
|
637
664
|
serializedTransaction,
|
|
638
665
|
escrowContract,
|
|
639
666
|
channelId,
|
|
@@ -649,6 +676,52 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
649
676
|
expect(result.onChain.deposit).toBe(deposit)
|
|
650
677
|
expect(result.onChain.settled).toBe(0n)
|
|
651
678
|
expect(result.onChain.finalized).toBe(false)
|
|
679
|
+
const simulationIndex = rpcMethods.indexOf('eth_call')
|
|
680
|
+
const broadcastIndex = rpcMethods.indexOf('eth_sendRawTransaction')
|
|
681
|
+
expect(simulationIndex).toBeGreaterThan(-1)
|
|
682
|
+
expect(broadcastIndex).toBeGreaterThan(-1)
|
|
683
|
+
expect(simulationIndex).toBeLessThan(broadcastIndex)
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
test('fee-payer relay: optimistic open simulates before broadcasting', async () => {
|
|
687
|
+
const rpcMethods: string[] = []
|
|
688
|
+
const interceptingClient = createClient({
|
|
689
|
+
account: accounts[0],
|
|
690
|
+
chain: client.chain,
|
|
691
|
+
transport: custom({
|
|
692
|
+
async request(args: any) {
|
|
693
|
+
rpcMethods.push(args.method)
|
|
694
|
+
return client.transport.request(args)
|
|
695
|
+
},
|
|
696
|
+
}),
|
|
697
|
+
})
|
|
698
|
+
const salt = nextSalt()
|
|
699
|
+
const deposit = 10_000_000n
|
|
700
|
+
const { channelId, serializedTransaction } = await signOpenChannel({
|
|
701
|
+
escrow: escrowContract,
|
|
702
|
+
payer,
|
|
703
|
+
payee: recipient,
|
|
704
|
+
token: currency,
|
|
705
|
+
deposit,
|
|
706
|
+
salt,
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
await broadcastOpenTransaction({
|
|
710
|
+
client: interceptingClient,
|
|
711
|
+
serializedTransaction,
|
|
712
|
+
escrowContract,
|
|
713
|
+
channelId,
|
|
714
|
+
recipient,
|
|
715
|
+
currency,
|
|
716
|
+
isSponsored: true,
|
|
717
|
+
waitForConfirmation: false,
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
const simulationIndex = rpcMethods.indexOf('eth_call')
|
|
721
|
+
const broadcastIndex = rpcMethods.indexOf('eth_sendRawTransaction')
|
|
722
|
+
expect(simulationIndex).toBeGreaterThan(-1)
|
|
723
|
+
expect(broadcastIndex).toBeGreaterThan(-1)
|
|
724
|
+
expect(simulationIndex).toBeLessThan(broadcastIndex)
|
|
652
725
|
})
|
|
653
726
|
})
|
|
654
727
|
|
|
@@ -1034,6 +1107,9 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
1034
1107
|
expect(broadcastIndex).toBeGreaterThan(-1)
|
|
1035
1108
|
expect(simulationIndex).toBeGreaterThan(-1)
|
|
1036
1109
|
expect(simulationIndex).toBeLessThan(broadcastIndex)
|
|
1110
|
+
expect(
|
|
1111
|
+
rpcMethods.slice(0, broadcastIndex).filter((method) => method === 'eth_call'),
|
|
1112
|
+
).toHaveLength(2)
|
|
1037
1113
|
})
|
|
1038
1114
|
|
|
1039
1115
|
test('fee-payer: rejects smuggled second topUp call', async () => {
|
|
@@ -626,37 +626,47 @@ export async function broadcastOpenTransaction(
|
|
|
626
626
|
|
|
627
627
|
await beforeBroadcast?.(pendingOnChain)
|
|
628
628
|
|
|
629
|
-
const
|
|
629
|
+
const completeTransaction = async () => {
|
|
630
630
|
if (feePayer) {
|
|
631
631
|
if (!sponsoredOpenCall)
|
|
632
632
|
throw new BadRequestError({
|
|
633
633
|
reason: 'transaction does not contain a valid escrow open call',
|
|
634
634
|
})
|
|
635
635
|
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
636
|
+
const completed = await FeePayer.preflightSponsorship({
|
|
637
|
+
transaction,
|
|
638
|
+
simulate: (request) => call(client, request as never),
|
|
639
|
+
async complete() {
|
|
640
|
+
const sponsored = FeePayer.prepareSponsoredTransaction({
|
|
641
|
+
account: feePayer,
|
|
642
|
+
allowedFeeTokens: defaultFeeToken ? [defaultFeeToken] : undefined,
|
|
643
|
+
challengeExpires,
|
|
644
|
+
chainId: client.chain!.id,
|
|
645
|
+
details: { channelId, currency, recipient },
|
|
646
|
+
policy: feePayerPolicy,
|
|
647
|
+
transaction: {
|
|
648
|
+
...transaction,
|
|
649
|
+
...(resolvedFeeToken ? { feeToken: resolvedFeeToken } : {}),
|
|
650
|
+
},
|
|
651
|
+
})
|
|
652
|
+
return { feePayer: feePayer.address, transaction: sponsored }
|
|
646
653
|
},
|
|
647
654
|
})
|
|
648
|
-
return signTransaction(client,
|
|
655
|
+
return signTransaction(client, completed.transaction as never)
|
|
649
656
|
}
|
|
650
657
|
return serializedTransaction
|
|
651
|
-
}
|
|
658
|
+
}
|
|
652
659
|
|
|
653
660
|
if (!waitForConfirmation) {
|
|
654
|
-
await
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
661
|
+
const serializedTransaction_final = await completeTransaction()
|
|
662
|
+
// Local sponsorship already ran sender-context preflight above. Every
|
|
663
|
+
// other optimistic path must still simulate before returning calldata as
|
|
664
|
+
// pending on-chain state, including hosted sponsorship (`isSponsored`).
|
|
665
|
+
if (!feePayer)
|
|
666
|
+
await call(
|
|
667
|
+
client,
|
|
668
|
+
FeePayer.simulationTransaction(transaction, { feePayer: isSponsored }) as never,
|
|
669
|
+
)
|
|
660
670
|
const txHash = await sendRawTransaction(client, {
|
|
661
671
|
serializedTransaction: serializedTransaction_final as Transaction.TransactionSerializedTempo,
|
|
662
672
|
})
|
|
@@ -669,14 +679,9 @@ export async function broadcastOpenTransaction(
|
|
|
669
679
|
|
|
670
680
|
let txHash: Hex | undefined
|
|
671
681
|
try {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
account: transaction.from,
|
|
676
|
-
calls,
|
|
677
|
-
feePayerSignature: undefined,
|
|
678
|
-
} as never)
|
|
679
|
-
|
|
682
|
+
// Keep local preflight inside recovery: a retry can legitimately revert
|
|
683
|
+
// during simulation after the original open was mined.
|
|
684
|
+
const serializedTransaction_final = await completeTransaction()
|
|
680
685
|
const receipt = await sendRawTransactionSync(client, {
|
|
681
686
|
serializedTransaction: serializedTransaction_final as Transaction.TransactionSerializedTempo,
|
|
682
687
|
})
|
|
@@ -775,37 +780,36 @@ export async function broadcastTopUpTransaction(
|
|
|
775
780
|
})
|
|
776
781
|
|
|
777
782
|
const defaultFeeToken = defaults.currency[client.chain?.id as keyof typeof defaults.currency]
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
: {
|
|
783
|
+
const completed = await FeePayer.preflightSponsorship({
|
|
784
|
+
transaction,
|
|
785
|
+
simulate: (request) => call(client, request as never),
|
|
786
|
+
async complete() {
|
|
787
|
+
const sponsored = FeePayer.prepareSponsoredTransaction({
|
|
788
|
+
account: feePayer,
|
|
789
|
+
allowedFeeTokens: defaultFeeToken ? [defaultFeeToken] : undefined,
|
|
790
|
+
challengeExpires,
|
|
791
|
+
chainId: client.chain!.id,
|
|
792
|
+
details: {
|
|
793
|
+
additionalDeposit: declaredDeposit.toString(),
|
|
794
|
+
channelId,
|
|
795
|
+
currency,
|
|
796
|
+
},
|
|
797
|
+
policy: feePayerPolicy,
|
|
798
|
+
transaction: {
|
|
799
|
+
...transaction,
|
|
800
|
+
...((transaction.feeToken ?? defaultFeeToken)
|
|
801
|
+
? { feeToken: transaction.feeToken ?? defaultFeeToken }
|
|
802
|
+
: {}),
|
|
803
|
+
},
|
|
804
|
+
})
|
|
805
|
+
return { feePayer: feePayer.address, transaction: sponsored }
|
|
794
806
|
},
|
|
795
807
|
})
|
|
796
|
-
return signTransaction(client,
|
|
808
|
+
return signTransaction(client, completed.transaction as never)
|
|
797
809
|
}
|
|
798
810
|
return serializedTransaction
|
|
799
811
|
})()
|
|
800
812
|
|
|
801
|
-
if (feePayer)
|
|
802
|
-
await call(client, {
|
|
803
|
-
...transaction,
|
|
804
|
-
account: transaction.from,
|
|
805
|
-
calls,
|
|
806
|
-
feePayerSignature: undefined,
|
|
807
|
-
} as never)
|
|
808
|
-
|
|
809
813
|
const receipt = await sendRawTransactionSync(client, {
|
|
810
814
|
serializedTransaction: serializedTransaction_final as Transaction.TransactionSerializedTempo,
|
|
811
815
|
})
|