mppx 0.8.11 → 0.8.13
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 +25 -0
- package/README.md +16 -1
- package/dist/cli/account.d.ts +88 -0
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +39 -9
- package/dist/cli/account.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 +59 -2
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/sessions/Manager.d.ts +31 -0
- package/dist/cli/sessions/Manager.d.ts.map +1 -0
- package/dist/cli/sessions/Manager.js +56 -0
- package/dist/cli/sessions/Manager.js.map +1 -0
- package/dist/cli/sessions/commands.d.ts +36 -0
- package/dist/cli/sessions/commands.d.ts.map +1 -0
- package/dist/cli/sessions/commands.js +407 -0
- package/dist/cli/sessions/commands.js.map +1 -0
- package/dist/cli/sessions/request.d.ts +35 -0
- package/dist/cli/sessions/request.d.ts.map +1 -0
- package/dist/cli/sessions/request.js +271 -0
- package/dist/cli/sessions/request.js.map +1 -0
- package/dist/cli/sessions/store.d.ts +134 -0
- package/dist/cli/sessions/store.d.ts.map +1 -0
- package/dist/cli/sessions/store.js +723 -0
- package/dist/cli/sessions/store.js.map +1 -0
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +12 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +12 -6
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/messages.d.ts +2 -0
- package/dist/cli/validate/messages.d.ts.map +1 -0
- package/dist/cli/validate/messages.js +8 -0
- package/dist/cli/validate/messages.js.map +1 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +35 -6
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodResponse.d.ts +23 -0
- package/dist/client/internal/MethodResponse.d.ts.map +1 -0
- package/dist/client/internal/MethodResponse.js +15 -0
- package/dist/client/internal/MethodResponse.js.map +1 -0
- package/dist/client/node.d.ts +29 -0
- package/dist/client/node.d.ts.map +1 -0
- package/dist/client/node.js +177 -0
- package/dist/client/node.js.map +1 -0
- package/dist/internal/types.d.ts +24 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts +8 -4
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +1 -1
- package/dist/tempo/internal/types.d.ts +10 -1
- package/dist/tempo/internal/types.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.d.ts +2 -0
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.js +4 -0
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
- package/dist/tempo/legacy/client/Session.d.ts +2 -2
- package/dist/tempo/legacy/client/Session.d.ts.map +1 -1
- package/dist/tempo/legacy/client/Session.js +7 -0
- package/dist/tempo/legacy/client/Session.js.map +1 -1
- package/dist/tempo/legacy/server/Session.d.ts +3 -3
- package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
- package/dist/tempo/legacy/server/Session.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +3 -2
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +2 -23
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +2 -2
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +3 -2
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js.map +1 -1
- package/dist/tempo/server/index.d.ts +1 -1
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.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/Snapshot.d.ts +9 -0
- package/dist/tempo/session/Snapshot.d.ts.map +1 -1
- package/dist/tempo/session/Snapshot.js +6 -0
- package/dist/tempo/session/Snapshot.js.map +1 -1
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +11 -2
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +21 -1
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +66 -1
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +1 -1
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +53 -1
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +2 -2
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +130 -30
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +21 -2
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +129 -18
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
- package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
- package/dist/tempo/session/client/internal/SessionManager.js +13 -0
- package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
- package/dist/tempo/session/precompile/Chain.js +13 -4
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +3 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +14 -0
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
- package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
- package/dist/tempo/session/server/MeteredStream.js +5 -4
- package/dist/tempo/session/server/MeteredStream.js.map +1 -1
- package/dist/tempo/session/server/RequestState.d.ts +5 -1
- package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
- package/dist/tempo/session/server/RequestState.js +10 -4
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts +21 -6
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +4 -1
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/tempo/session/server/Settlement.d.ts +20 -0
- package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
- package/dist/tempo/session/server/Settlement.js +20 -0
- package/dist/tempo/session/server/Settlement.js.map +1 -1
- package/dist/tempo/session/server/Ws.d.ts +2 -0
- package/dist/tempo/session/server/Ws.d.ts.map +1 -1
- package/dist/tempo/session/server/Ws.js.map +1 -1
- package/dist/tempo/session/server/index.d.ts +1 -1
- package/dist/tempo/session/server/index.d.ts.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
- package/dist/validation/core.d.ts.map +1 -1
- package/dist/validation/core.js +6 -2
- package/dist/validation/core.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js +24 -17
- package/dist/viem/Client.js.map +1 -1
- package/package.json +6 -1
- package/src/cli/account.ts +45 -10
- package/src/cli/cli.test.ts +165 -70
- package/src/cli/cli.ts +67 -2
- package/src/cli/mcp.test.ts +11 -0
- package/src/cli/sessions/Manager.test.ts +249 -0
- package/src/cli/sessions/Manager.ts +93 -0
- package/src/cli/sessions/commands.ts +444 -0
- package/src/cli/sessions/request.test.ts +51 -0
- package/src/cli/sessions/request.ts +353 -0
- package/src/cli/sessions/store.test.ts +581 -0
- package/src/cli/sessions/store.ts +940 -0
- package/src/cli/utils.test.ts +23 -0
- package/src/cli/utils.ts +10 -0
- package/src/cli/validate/index.ts +12 -14
- package/src/cli/validate/messages.ts +7 -0
- package/src/cli/validate.test.ts +38 -5
- package/src/client/internal/Fetch.test.ts +29 -0
- package/src/client/internal/Fetch.ts +51 -6
- package/src/client/internal/MethodResponse.ts +39 -0
- package/src/client/node.test.ts +115 -0
- package/src/client/node.ts +247 -0
- package/src/internal/types.test-d.ts +21 -0
- package/src/internal/types.ts +33 -0
- package/src/server/Mppx.test-d.ts +42 -0
- package/src/stripe/Methods.test.ts +10 -0
- package/src/stripe/server/Charge.test-d.ts +66 -0
- package/src/stripe/server/Charge.ts +8 -5
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/internal/types.ts +13 -2
- package/src/tempo/legacy/client/ChannelOps.test.ts +21 -0
- package/src/tempo/legacy/client/ChannelOps.ts +7 -0
- package/src/tempo/legacy/client/Session.test.ts +102 -1
- package/src/tempo/legacy/client/Session.ts +10 -0
- package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
- package/src/tempo/legacy/server/Session.ts +6 -5
- package/src/tempo/server/Charge.ts +6 -4
- package/src/tempo/server/Methods.ts +5 -4
- package/src/tempo/server/Subscription.ts +6 -4
- package/src/tempo/server/index.ts +5 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/README.md +201 -0
- package/src/tempo/session/Snapshot.ts +21 -0
- package/src/tempo/session/client/ChannelOps.test.ts +40 -0
- package/src/tempo/session/client/ChannelOps.ts +12 -2
- package/src/tempo/session/client/CredentialState.test.ts +2 -2
- package/src/tempo/session/client/CredentialState.ts +103 -1
- package/src/tempo/session/client/Session.test.ts +86 -4
- package/src/tempo/session/client/Session.ts +73 -1
- package/src/tempo/session/client/SessionManager.test.ts +137 -4
- package/src/tempo/session/client/SessionManager.ts +141 -33
- package/src/tempo/session/client/Transports.test.ts +225 -8
- package/src/tempo/session/client/Transports.ts +156 -21
- package/src/tempo/session/client/internal/SessionManager.ts +36 -0
- package/src/tempo/session/precompile/Chain.test.ts +26 -0
- package/src/tempo/session/precompile/Chain.ts +17 -4
- package/src/tempo/session/server/CredentialVerification.ts +18 -1
- package/src/tempo/session/server/MeteredStream.ts +12 -5
- package/src/tempo/session/server/RequestState.test.ts +39 -3
- package/src/tempo/session/server/RequestState.ts +13 -6
- package/src/tempo/session/server/Session.test.ts +461 -0
- package/src/tempo/session/server/Session.ts +31 -8
- package/src/tempo/session/server/Settlement.ts +45 -0
- package/src/tempo/session/server/Ws.test.ts +113 -0
- package/src/tempo/session/server/Ws.ts +2 -0
- package/src/tempo/session/server/index.ts +2 -0
- package/src/validation/core.ts +7 -1
- package/src/viem/Client.test.ts +44 -1
- package/src/viem/Client.ts +31 -17
|
@@ -9,6 +9,7 @@ import type { ChannelEntry } from '../client/ChannelOps.js'
|
|
|
9
9
|
import type { SessionContext } from '../client/CredentialState.js'
|
|
10
10
|
import {
|
|
11
11
|
createSessionReceipt,
|
|
12
|
+
formatNeedVoucherEvent,
|
|
12
13
|
serializeSessionReceipt,
|
|
13
14
|
tip20ChannelEscrow,
|
|
14
15
|
type ChannelDescriptor,
|
|
@@ -18,6 +19,7 @@ import { initialState, type SessionState } from './Runtime.js'
|
|
|
18
19
|
import {
|
|
19
20
|
applyTopUpResult,
|
|
20
21
|
closeHttpSession,
|
|
22
|
+
consumeSseSessionResponse,
|
|
21
23
|
createActiveSocketSession,
|
|
22
24
|
driveSseResponse,
|
|
23
25
|
isExpectedSocketReceipt,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
validateSocketCloseReadyReceipt,
|
|
33
35
|
validateSocketPaymentReceipt,
|
|
34
36
|
webSocketProbeUrl,
|
|
37
|
+
wrapSseResponse,
|
|
35
38
|
type TempoSessionChallenge,
|
|
36
39
|
type TopUpRequirement,
|
|
37
40
|
} from './Transports.js'
|
|
@@ -111,9 +114,15 @@ describe('HttpManagement', () => {
|
|
|
111
114
|
})
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
function receiptHeader(acceptedCumulative: bigint, spent: bigint) {
|
|
117
|
+
function receiptHeader(acceptedCumulative: bigint, spent: bigint, challengeId = 'challenge-1') {
|
|
115
118
|
return serializeSessionReceipt(
|
|
116
|
-
createSessionReceipt({
|
|
119
|
+
createSessionReceipt({
|
|
120
|
+
acceptedCumulative,
|
|
121
|
+
challengeId,
|
|
122
|
+
channelId,
|
|
123
|
+
spent,
|
|
124
|
+
txHash: `0x${'aa'.repeat(32)}`,
|
|
125
|
+
}),
|
|
117
126
|
)
|
|
118
127
|
}
|
|
119
128
|
|
|
@@ -138,9 +147,12 @@ describe('HttpManagement', () => {
|
|
|
138
147
|
)
|
|
139
148
|
})
|
|
140
149
|
|
|
141
|
-
test('
|
|
150
|
+
test('preserves query state and strips fragments from management URLs', () => {
|
|
142
151
|
expect(managementInput('https://example.test/resource?cursor=abc').toString()).toBe(
|
|
143
|
-
'https://example.test/resource',
|
|
152
|
+
'https://example.test/resource?cursor=abc',
|
|
153
|
+
)
|
|
154
|
+
expect(managementInput('https://example.test/resource?cursor=abc#result').toString()).toBe(
|
|
155
|
+
'https://example.test/resource?cursor=abc',
|
|
144
156
|
)
|
|
145
157
|
})
|
|
146
158
|
|
|
@@ -184,7 +196,7 @@ describe('HttpManagement', () => {
|
|
|
184
196
|
return 'top-up-credential'
|
|
185
197
|
})
|
|
186
198
|
const fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
187
|
-
expect(input.toString()).toBe('https://example.test/resource')
|
|
199
|
+
expect(input.toString()).toBe('https://example.test/resource?cursor=abc')
|
|
188
200
|
expect(init?.method).toBe('POST')
|
|
189
201
|
expect(authorizationHeader(init)).toBe('top-up-credential')
|
|
190
202
|
return new Response(null, {
|
|
@@ -300,16 +312,20 @@ describe('HttpManagement', () => {
|
|
|
300
312
|
const entry = channel()
|
|
301
313
|
const retryChallenge = { ...challenge(), id: 'challenge-2' } as TempoSessionChallenge
|
|
302
314
|
const setChallenge = vi.fn()
|
|
303
|
-
const
|
|
315
|
+
const closeAmounts: string[] = []
|
|
316
|
+
const createSessionCredential = vi.fn(async (challenge_, context: SessionContext) => {
|
|
317
|
+
if (context.action !== 'close') throw new Error('expected close context')
|
|
318
|
+
closeAmounts.push(context.cumulativeAmountRaw!)
|
|
304
319
|
return `close-${challenge_.id}`
|
|
305
320
|
})
|
|
321
|
+
const resolveSignedCloseAmount = vi.fn(() => '6')
|
|
306
322
|
const fetch = vi
|
|
307
323
|
.fn()
|
|
308
324
|
.mockResolvedValueOnce(response402(retryChallenge))
|
|
309
325
|
.mockResolvedValueOnce(
|
|
310
326
|
new Response(null, {
|
|
311
327
|
status: 200,
|
|
312
|
-
headers: { [Constants.Headers.paymentReceipt]: receiptHeader(
|
|
328
|
+
headers: { [Constants.Headers.paymentReceipt]: receiptHeader(6n, 6n, 'challenge-2') },
|
|
313
329
|
}),
|
|
314
330
|
)
|
|
315
331
|
|
|
@@ -317,18 +333,71 @@ describe('HttpManagement', () => {
|
|
|
317
333
|
createSessionCredential,
|
|
318
334
|
fetch,
|
|
319
335
|
lastUrl: 'https://example.test/resource',
|
|
336
|
+
resolveSignedCloseAmount,
|
|
320
337
|
signedCloseAmount: '5',
|
|
321
338
|
setChallenge,
|
|
322
339
|
target: { challenge: challenge(), channel: entry, channelId },
|
|
323
340
|
})
|
|
324
341
|
|
|
325
|
-
expect(receipt?.spent).toBe('
|
|
342
|
+
expect(receipt?.spent).toBe('6')
|
|
326
343
|
expect(setChallenge).toHaveBeenCalledWith(retryChallenge)
|
|
344
|
+
expect(resolveSignedCloseAmount).toHaveBeenCalledOnce()
|
|
345
|
+
expect(resolveSignedCloseAmount).toHaveBeenCalledWith(retryChallenge)
|
|
346
|
+
expect(closeAmounts).toEqual(['5', '6'])
|
|
327
347
|
expect(createSessionCredential).toHaveBeenCalledTimes(2)
|
|
328
348
|
expect(createSessionCredential.mock.calls[1]?.[0]).toMatchObject({ id: retryChallenge.id })
|
|
329
349
|
expect(authorizationHeader(fetch.mock.calls[1]?.[1])).toBe('close-challenge-2')
|
|
330
350
|
})
|
|
331
351
|
|
|
352
|
+
test('closeHttpSession rejects a receipt without settlement proof', async () => {
|
|
353
|
+
const receipt = serializeSessionReceipt(
|
|
354
|
+
createSessionReceipt({
|
|
355
|
+
acceptedCumulative: 5n,
|
|
356
|
+
challengeId: 'challenge-1',
|
|
357
|
+
channelId,
|
|
358
|
+
spent: 5n,
|
|
359
|
+
}),
|
|
360
|
+
)
|
|
361
|
+
await expect(
|
|
362
|
+
closeHttpSession({
|
|
363
|
+
createSessionCredential: async () => 'close-credential',
|
|
364
|
+
fetch: async () =>
|
|
365
|
+
new Response(null, {
|
|
366
|
+
headers: { [Constants.Headers.paymentReceipt]: receipt },
|
|
367
|
+
}),
|
|
368
|
+
lastUrl: 'https://example.test/resource',
|
|
369
|
+
signedCloseAmount: '5',
|
|
370
|
+
target: { challenge: challenge(), channel: channel(), channelId },
|
|
371
|
+
}),
|
|
372
|
+
).rejects.toThrow('Session close response included a mismatched payment receipt.')
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test('closeHttpSession rejects an invalid settlement transaction hash', async () => {
|
|
376
|
+
for (const txHash of ['0x1234', `0x${'gg'.repeat(32)}`]) {
|
|
377
|
+
const receipt = serializeSessionReceipt(
|
|
378
|
+
createSessionReceipt({
|
|
379
|
+
acceptedCumulative: 5n,
|
|
380
|
+
challengeId: 'challenge-1',
|
|
381
|
+
channelId,
|
|
382
|
+
spent: 5n,
|
|
383
|
+
txHash: txHash as Hex.Hex,
|
|
384
|
+
}),
|
|
385
|
+
)
|
|
386
|
+
await expect(
|
|
387
|
+
closeHttpSession({
|
|
388
|
+
createSessionCredential: async () => 'close-credential',
|
|
389
|
+
fetch: async () =>
|
|
390
|
+
new Response(null, {
|
|
391
|
+
headers: { [Constants.Headers.paymentReceipt]: receipt },
|
|
392
|
+
}),
|
|
393
|
+
lastUrl: 'https://example.test/resource',
|
|
394
|
+
signedCloseAmount: '5',
|
|
395
|
+
target: { challenge: challenge(), channel: channel(), channelId },
|
|
396
|
+
}),
|
|
397
|
+
).rejects.toThrow('Session close response included a mismatched payment receipt.')
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
|
|
332
401
|
test('closeHttpSession includes problem detail and challenge header on failure', async () => {
|
|
333
402
|
await expect(
|
|
334
403
|
closeHttpSession({
|
|
@@ -672,6 +741,154 @@ describe('SseDriver', () => {
|
|
|
672
741
|
])
|
|
673
742
|
expect(acceptReceipt).toHaveBeenCalledWith(receipt)
|
|
674
743
|
})
|
|
744
|
+
|
|
745
|
+
test('wraps standard CRLF streams and preserves response metadata', async () => {
|
|
746
|
+
const receipt = createSessionReceipt({
|
|
747
|
+
acceptedCumulative: 2n,
|
|
748
|
+
challengeId: 'challenge-1',
|
|
749
|
+
channelId: `0x${'01'.repeat(32)}`,
|
|
750
|
+
spent: 2n,
|
|
751
|
+
})
|
|
752
|
+
const encoder = new TextEncoder()
|
|
753
|
+
const source = new Response(
|
|
754
|
+
new ReadableStream({
|
|
755
|
+
start(controller) {
|
|
756
|
+
controller.enqueue(encoder.encode(': keepalive\r\n\r'))
|
|
757
|
+
controller.enqueue(
|
|
758
|
+
encoder.encode(
|
|
759
|
+
[
|
|
760
|
+
'\nevent: custom\r\ndata: hello\r\n\r\n',
|
|
761
|
+
`event: payment-receipt\r\ndata: ${JSON.stringify(receipt)}\r\n\r\n`,
|
|
762
|
+
].join(''),
|
|
763
|
+
),
|
|
764
|
+
)
|
|
765
|
+
controller.close()
|
|
766
|
+
},
|
|
767
|
+
}),
|
|
768
|
+
{
|
|
769
|
+
headers: { 'content-length': '100', 'content-type': 'text/event-stream' },
|
|
770
|
+
status: 201,
|
|
771
|
+
statusText: 'Created',
|
|
772
|
+
},
|
|
773
|
+
)
|
|
774
|
+
Object.defineProperty(source, 'url', { value: 'https://example.com/stream' })
|
|
775
|
+
|
|
776
|
+
const response = wrapSseResponse({
|
|
777
|
+
async onNeedVoucher() {},
|
|
778
|
+
onReceipt() {},
|
|
779
|
+
response: source,
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
expect(response.status).toBe(201)
|
|
783
|
+
expect(response.statusText).toBe('Created')
|
|
784
|
+
expect(response.url).toBe('https://example.com/stream')
|
|
785
|
+
expect(response.headers.get('content-type')).toBe('text/event-stream')
|
|
786
|
+
expect(response.headers.has('content-length')).toBe(false)
|
|
787
|
+
expect(await response.text()).toBe(': keepalive\r\n\r\nevent: custom\r\ndata: hello\r\n\r\n')
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
test.each(['consumer', 'signal'] as const)(
|
|
791
|
+
'cancels the source stream from %s',
|
|
792
|
+
async (source) => {
|
|
793
|
+
const abortController = new AbortController()
|
|
794
|
+
const cancel = vi.fn()
|
|
795
|
+
const sourceResponse = new Response(
|
|
796
|
+
new ReadableStream({
|
|
797
|
+
cancel,
|
|
798
|
+
start(controller) {
|
|
799
|
+
controller.enqueue(new TextEncoder().encode('data: first\n\n'))
|
|
800
|
+
},
|
|
801
|
+
}),
|
|
802
|
+
)
|
|
803
|
+
const response = wrapSseResponse({
|
|
804
|
+
async onNeedVoucher() {},
|
|
805
|
+
onReceipt() {},
|
|
806
|
+
response: sourceResponse,
|
|
807
|
+
signal: abortController.signal,
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
const reader = response.body!.getReader()
|
|
811
|
+
await reader.read()
|
|
812
|
+
if (source === 'signal') abortController.abort()
|
|
813
|
+
else await reader.cancel()
|
|
814
|
+
|
|
815
|
+
await vi.waitFor(() => expect(cancel).toHaveBeenCalledOnce())
|
|
816
|
+
},
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
test('keeps voucher events bound to the challenge that opened the stream', async () => {
|
|
820
|
+
const channelId = `0x${'01'.repeat(32)}` as Hex.Hex
|
|
821
|
+
const token = '0x20c0000000000000000000000000000000000001' as Address
|
|
822
|
+
const payee = '0x0000000000000000000000000000000000000002' as Address
|
|
823
|
+
const descriptor: ChannelDescriptor = {
|
|
824
|
+
authorizedSigner: '0x0000000000000000000000000000000000000001',
|
|
825
|
+
expiringNonceHash: `0x${'03'.repeat(32)}` as Hex.Hex,
|
|
826
|
+
operator: '0x0000000000000000000000000000000000000000',
|
|
827
|
+
payee,
|
|
828
|
+
payer: '0x0000000000000000000000000000000000000001',
|
|
829
|
+
salt: `0x${'02'.repeat(32)}` as Hex.Hex,
|
|
830
|
+
token,
|
|
831
|
+
}
|
|
832
|
+
const makeChallenge = (id: string) =>
|
|
833
|
+
Challenge.from({
|
|
834
|
+
id,
|
|
835
|
+
intent: Constants.Intents.session,
|
|
836
|
+
method: Constants.Methods.tempo,
|
|
837
|
+
realm: 'example.test',
|
|
838
|
+
request: {
|
|
839
|
+
amount: '1',
|
|
840
|
+
currency: token,
|
|
841
|
+
methodDetails: { chainId: 4217, escrowContract: tip20ChannelEscrow },
|
|
842
|
+
recipient: payee,
|
|
843
|
+
unitType: 'request',
|
|
844
|
+
},
|
|
845
|
+
}) as TempoSessionChallenge
|
|
846
|
+
const streamChallenge = makeChallenge('challenge-1')
|
|
847
|
+
let currentChallenge = streamChallenge
|
|
848
|
+
const createSessionCredential = vi.fn(async () => 'Payment credential')
|
|
849
|
+
const channel: ChannelEntry = {
|
|
850
|
+
chainId: 4217,
|
|
851
|
+
channelId,
|
|
852
|
+
cumulativeAmount: 1n,
|
|
853
|
+
deposit: 10n,
|
|
854
|
+
descriptor,
|
|
855
|
+
escrow: tip20ChannelEscrow,
|
|
856
|
+
opened: true,
|
|
857
|
+
}
|
|
858
|
+
const response = new Response(
|
|
859
|
+
formatNeedVoucherEvent({
|
|
860
|
+
acceptedCumulative: '1',
|
|
861
|
+
channelId,
|
|
862
|
+
deposit: '10',
|
|
863
|
+
requiredCumulative: '2',
|
|
864
|
+
}),
|
|
865
|
+
{ headers: { 'Content-Type': 'text/event-stream' } },
|
|
866
|
+
)
|
|
867
|
+
const stream = consumeSseSessionResponse('https://example.test/stream', response, undefined, {
|
|
868
|
+
acceptReceipt() {},
|
|
869
|
+
assertVoucherWithinLocalLimit() {},
|
|
870
|
+
createSessionCredential,
|
|
871
|
+
async doFetch() {
|
|
872
|
+
throw new Error('unexpected resource fetch')
|
|
873
|
+
},
|
|
874
|
+
fetch: vi.fn(async () => new Response(null, { status: 204 })),
|
|
875
|
+
getChallenge: () => currentChallenge,
|
|
876
|
+
getChannel: () => channel,
|
|
877
|
+
managementInput: (input) => input,
|
|
878
|
+
async topUpIfNeeded() {},
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
currentChallenge = makeChallenge('challenge-2')
|
|
882
|
+
const frames: string[] = []
|
|
883
|
+
for await (const frame of stream) frames.push(frame)
|
|
884
|
+
|
|
885
|
+
expect(frames).toEqual([])
|
|
886
|
+
expect(createSessionCredential).toHaveBeenCalledOnce()
|
|
887
|
+
expect(createSessionCredential).toHaveBeenCalledWith(
|
|
888
|
+
streamChallenge,
|
|
889
|
+
expect.objectContaining({ action: 'voucher', cumulativeAmountRaw: '2' }),
|
|
890
|
+
)
|
|
891
|
+
})
|
|
675
892
|
})
|
|
676
893
|
|
|
677
894
|
describe('WsDriver', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Hex } from 'ox'
|
|
2
2
|
|
|
3
3
|
import * as Challenge from '../../../Challenge.js'
|
|
4
4
|
import * as Fetch from '../../../client/internal/Fetch.js'
|
|
@@ -502,6 +502,8 @@ export type CloseHttpSessionParameters = {
|
|
|
502
502
|
fetch: typeof globalThis.fetch
|
|
503
503
|
/** Last paid resource URL; used as the management endpoint base. */
|
|
504
504
|
lastUrl: RequestInfo | URL | null
|
|
505
|
+
/** Resolves the close amount again when the server refreshes the challenge. */
|
|
506
|
+
resolveSignedCloseAmount?: ((challenge: TempoSessionChallenge) => string) | undefined
|
|
505
507
|
/** Final cumulative amount the client is willing to sign. */
|
|
506
508
|
signedCloseAmount: string
|
|
507
509
|
/** Stores a fresh close challenge when the first close credential expired. */
|
|
@@ -544,7 +546,7 @@ export function requestInitWithAuthorization(
|
|
|
544
546
|
}
|
|
545
547
|
}
|
|
546
548
|
|
|
547
|
-
/** Returns the URL used for out-of-band management POSTs,
|
|
549
|
+
/** Returns the exact resource URL used for out-of-band management POSTs, without its fragment. */
|
|
548
550
|
export function managementInput(input: RequestInfo | URL): RequestInfo | URL {
|
|
549
551
|
try {
|
|
550
552
|
const base =
|
|
@@ -555,7 +557,7 @@ export function managementInput(input: RequestInfo | URL): RequestInfo | URL {
|
|
|
555
557
|
: input instanceof URL
|
|
556
558
|
? new URL(input)
|
|
557
559
|
: new URL(String(input), base)
|
|
558
|
-
url.
|
|
560
|
+
url.hash = ''
|
|
559
561
|
return url
|
|
560
562
|
} catch {
|
|
561
563
|
return input
|
|
@@ -682,12 +684,18 @@ export async function closeHttpSession(
|
|
|
682
684
|
)
|
|
683
685
|
}
|
|
684
686
|
|
|
685
|
-
|
|
687
|
+
let closeChallenge = parameters.target.challenge
|
|
688
|
+
let signedCloseAmount = parameters.signedCloseAmount
|
|
689
|
+
const postClose = async (challenge: TempoSessionChallenge, refreshed = false) => {
|
|
690
|
+
closeChallenge = challenge
|
|
691
|
+
signedCloseAmount =
|
|
692
|
+
(refreshed ? parameters.resolveSignedCloseAmount?.(challenge) : undefined) ??
|
|
693
|
+
parameters.signedCloseAmount
|
|
686
694
|
const credential = await parameters.createSessionCredential(challenge, {
|
|
687
695
|
action: 'close',
|
|
688
696
|
channelId: parameters.target.channelId,
|
|
689
697
|
descriptor: parameters.target.channel.descriptor,
|
|
690
|
-
cumulativeAmountRaw:
|
|
698
|
+
cumulativeAmountRaw: signedCloseAmount,
|
|
691
699
|
})
|
|
692
700
|
return parameters.fetch(parameters.lastUrl!, {
|
|
693
701
|
method: 'POST',
|
|
@@ -700,7 +708,7 @@ export async function closeHttpSession(
|
|
|
700
708
|
const challenge = Challenge.fromResponseList(response).find(isTempoSessionChallenge)
|
|
701
709
|
if (challenge) {
|
|
702
710
|
parameters.setChallenge?.(challenge)
|
|
703
|
-
response = await postClose(challenge)
|
|
711
|
+
response = await postClose(challenge, true)
|
|
704
712
|
}
|
|
705
713
|
}
|
|
706
714
|
if (!response.ok) {
|
|
@@ -712,7 +720,21 @@ export async function closeHttpSession(
|
|
|
712
720
|
}
|
|
713
721
|
|
|
714
722
|
const receiptHeader = response.headers.get(Constants.Headers.paymentReceipt)
|
|
715
|
-
|
|
723
|
+
if (!receiptHeader) return undefined
|
|
724
|
+
const receipt = deserializeSessionReceipt(receiptHeader)
|
|
725
|
+
if (
|
|
726
|
+
!receipt.txHash ||
|
|
727
|
+
!Hex.validate(receipt.txHash, { strict: true }) ||
|
|
728
|
+
Hex.size(receipt.txHash) !== 32 ||
|
|
729
|
+
!isExpectedCloseReceipt({
|
|
730
|
+
challengeId: closeChallenge.id,
|
|
731
|
+
channelId: parameters.target.channelId,
|
|
732
|
+
expectedCloseAmount: signedCloseAmount,
|
|
733
|
+
receipt,
|
|
734
|
+
})
|
|
735
|
+
)
|
|
736
|
+
throw new Error('Session close response included a mismatched payment receipt.')
|
|
737
|
+
return receipt
|
|
716
738
|
}
|
|
717
739
|
|
|
718
740
|
/** Options accepted by the auto-driving SSE session flow. */
|
|
@@ -723,6 +745,12 @@ export type SseDriverOptions = RequestInit & {
|
|
|
723
745
|
signal?: AbortSignal | undefined
|
|
724
746
|
}
|
|
725
747
|
|
|
748
|
+
/** @internal Options used when consuming an already-paid SSE response. */
|
|
749
|
+
export type SseResponseOptions = {
|
|
750
|
+
onReceipt?: ((receipt: SessionReceipt) => void) | undefined
|
|
751
|
+
signal?: AbortSignal | undefined
|
|
752
|
+
}
|
|
753
|
+
|
|
726
754
|
/** Dependencies the SSE driver needs from `SessionManager`. */
|
|
727
755
|
export type OpenSseSessionParameters = {
|
|
728
756
|
/** Creates a session credential for the selected challenge/context. */
|
|
@@ -748,6 +776,17 @@ export type OpenSseSessionParameters = {
|
|
|
748
776
|
topUpIfNeeded(parameters: TopUpRequirement): Promise<void>
|
|
749
777
|
}
|
|
750
778
|
|
|
779
|
+
/** Session payment operations shared with fetch-backed streams. */
|
|
780
|
+
export type SsePaymentDriver = Pick<
|
|
781
|
+
OpenSseSessionParameters,
|
|
782
|
+
| 'assertVoucherWithinLocalLimit'
|
|
783
|
+
| 'createSessionCredential'
|
|
784
|
+
| 'fetch'
|
|
785
|
+
| 'getChannel'
|
|
786
|
+
| 'managementInput'
|
|
787
|
+
| 'topUpIfNeeded'
|
|
788
|
+
>
|
|
789
|
+
|
|
751
790
|
/**
|
|
752
791
|
* Opens an auto-driving paid SSE stream.
|
|
753
792
|
*
|
|
@@ -773,18 +812,28 @@ export async function openSseSession(
|
|
|
773
812
|
if (!isEventStream(response) && driver.getChannel()?.opened)
|
|
774
813
|
response = await driver.doFetch(input, sseInit)
|
|
775
814
|
|
|
776
|
-
|
|
815
|
+
return consumeSseSessionResponse(input, response, { onReceipt, signal }, driver)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** @internal Consumes an already-paid SSE response with the session transport driver. */
|
|
819
|
+
export function consumeSseSessionResponse(
|
|
820
|
+
input: RequestInfo | URL,
|
|
821
|
+
response: Response,
|
|
822
|
+
options: SseResponseOptions | undefined,
|
|
823
|
+
driver: OpenSseSessionParameters,
|
|
824
|
+
): AsyncIterable<string> {
|
|
777
825
|
if (!isEventStream(response)) throw new Error('SSE response is not an event stream.')
|
|
778
826
|
if (!response.body) throw new Error('Response has no body.')
|
|
827
|
+
const challenge = driver.getChallenge()
|
|
779
828
|
|
|
780
829
|
return iterateSseMessages({
|
|
781
830
|
onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input }, event),
|
|
782
831
|
onReceipt(receipt) {
|
|
783
832
|
driver.acceptReceipt(receipt)
|
|
784
|
-
onReceipt?.(receipt)
|
|
833
|
+
options?.onReceipt?.(receipt)
|
|
785
834
|
},
|
|
786
835
|
response,
|
|
787
|
-
signal,
|
|
836
|
+
signal: options?.signal,
|
|
788
837
|
})
|
|
789
838
|
}
|
|
790
839
|
|
|
@@ -822,22 +871,32 @@ export async function* driveSseResponse(
|
|
|
822
871
|
const reader = parameters.response.body.getReader()
|
|
823
872
|
const decoder = new TextDecoder()
|
|
824
873
|
let buffer = ''
|
|
874
|
+
let complete = false
|
|
875
|
+
const cancel = () => {
|
|
876
|
+
void reader.cancel(parameters.signal?.reason).catch(() => undefined)
|
|
877
|
+
}
|
|
878
|
+
parameters.signal?.addEventListener('abort', cancel, { once: true })
|
|
879
|
+
if (parameters.signal?.aborted) cancel()
|
|
825
880
|
|
|
826
881
|
try {
|
|
827
882
|
while (true) {
|
|
828
883
|
if (parameters.signal?.aborted) break
|
|
829
884
|
|
|
830
885
|
const { done, value } = await reader.read()
|
|
831
|
-
if (done)
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
886
|
+
if (done) {
|
|
887
|
+
complete = true
|
|
888
|
+
buffer += decoder.decode()
|
|
889
|
+
} else buffer += decoder.decode(value, { stream: true })
|
|
890
|
+
|
|
891
|
+
while (true) {
|
|
892
|
+
const separator = findSseSeparator(buffer, done)
|
|
893
|
+
if (!separator) break
|
|
894
|
+
const end = separator.index + separator.length
|
|
895
|
+
const part = buffer.slice(0, separator.index)
|
|
896
|
+
const raw = buffer.slice(0, end)
|
|
897
|
+
buffer = buffer.slice(end)
|
|
838
898
|
if (!part.trim()) continue
|
|
839
|
-
const event = parseEvent(part)
|
|
840
|
-
const raw = `${part}\n\n`
|
|
899
|
+
const event = parseEvent(part.replace(/\r\n|\r/g, '\n'))
|
|
841
900
|
if (!event) {
|
|
842
901
|
yield { raw }
|
|
843
902
|
continue
|
|
@@ -855,16 +914,92 @@ export async function* driveSseResponse(
|
|
|
855
914
|
break
|
|
856
915
|
}
|
|
857
916
|
}
|
|
917
|
+
if (done) break
|
|
858
918
|
}
|
|
859
919
|
} finally {
|
|
920
|
+
parameters.signal?.removeEventListener('abort', cancel)
|
|
921
|
+
if (!complete) await reader.cancel().catch(() => undefined)
|
|
860
922
|
reader.releaseLock()
|
|
861
923
|
}
|
|
862
924
|
}
|
|
863
925
|
|
|
864
|
-
|
|
926
|
+
function findSseSeparator(
|
|
927
|
+
value: string,
|
|
928
|
+
complete: boolean,
|
|
929
|
+
): { index: number; length: number } | undefined {
|
|
930
|
+
for (let index = 0; index < value.length; index++) {
|
|
931
|
+
const first = lineEndingLength(value, index, complete)
|
|
932
|
+
if (!first) continue
|
|
933
|
+
const second = lineEndingLength(value, index + first, complete)
|
|
934
|
+
if (second) return { index, length: first + second }
|
|
935
|
+
index += first - 1
|
|
936
|
+
}
|
|
937
|
+
return undefined
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function lineEndingLength(value: string, index: number, complete: boolean): number {
|
|
941
|
+
if (value[index] === '\n') return 1
|
|
942
|
+
if (value[index] !== '\r') return 0
|
|
943
|
+
if (value[index + 1] === '\n') return 2
|
|
944
|
+
if (index + 1 === value.length && !complete) return 0
|
|
945
|
+
return 1
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Wraps an SSE response while handling session payment frames in-band. */
|
|
949
|
+
export function wrapSseResponse(parameters: DriveSseResponseParameters): Response {
|
|
950
|
+
const abortController = new AbortController()
|
|
951
|
+
const abort = () => abortController.abort(parameters.signal?.reason)
|
|
952
|
+
parameters.signal?.addEventListener('abort', abort, { once: true })
|
|
953
|
+
if (parameters.signal?.aborted) abort()
|
|
954
|
+
|
|
955
|
+
const iterator = driveSseResponse({ ...parameters, signal: abortController.signal })
|
|
956
|
+
const encoder = new TextEncoder()
|
|
957
|
+
const headers = new Headers(parameters.response.headers)
|
|
958
|
+
headers.delete('content-length')
|
|
959
|
+
const cleanup = () => parameters.signal?.removeEventListener('abort', abort)
|
|
960
|
+
|
|
961
|
+
const response = new Response(
|
|
962
|
+
new ReadableStream({
|
|
963
|
+
async pull(controller) {
|
|
964
|
+
try {
|
|
965
|
+
const frame = await iterator.next()
|
|
966
|
+
if (frame.done) {
|
|
967
|
+
cleanup()
|
|
968
|
+
return controller.close()
|
|
969
|
+
}
|
|
970
|
+
controller.enqueue(encoder.encode(frame.value.raw))
|
|
971
|
+
} catch (error) {
|
|
972
|
+
cleanup()
|
|
973
|
+
throw error
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
async cancel() {
|
|
977
|
+
cleanup()
|
|
978
|
+
abortController.abort()
|
|
979
|
+
await iterator.return(undefined)
|
|
980
|
+
},
|
|
981
|
+
}),
|
|
982
|
+
{
|
|
983
|
+
headers,
|
|
984
|
+
status: parameters.response.status,
|
|
985
|
+
statusText: parameters.response.statusText,
|
|
986
|
+
},
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
for (const property of ['redirected', 'type', 'url'] as const)
|
|
990
|
+
Object.defineProperty(response, property, {
|
|
991
|
+
configurable: true,
|
|
992
|
+
get: () => parameters.response[property],
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
return response
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/** Handles an in-band voucher request using the shared SSE payment driver. */
|
|
999
|
+
export async function handleSseNeedVoucher(
|
|
865
1000
|
parameters: {
|
|
866
1001
|
challenge: TempoSessionChallenge | null
|
|
867
|
-
driver:
|
|
1002
|
+
driver: SsePaymentDriver
|
|
868
1003
|
input: RequestInfo | URL
|
|
869
1004
|
},
|
|
870
1005
|
event: NeedVoucherEvent,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ChannelEntry } from '../ChannelOps.js'
|
|
2
|
+
import type { PaymentResponse, SessionManager } from '../SessionManager.js'
|
|
3
|
+
import type { SseResponseOptions, TempoSessionChallenge } from '../Transports.js'
|
|
4
|
+
|
|
5
|
+
type RehydrateParameters = {
|
|
6
|
+
channel: ChannelEntry
|
|
7
|
+
challenge: TempoSessionChallenge
|
|
8
|
+
input: RequestInfo | URL
|
|
9
|
+
spent: bigint
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type SessionManagerInternals = {
|
|
13
|
+
consumeSseResponse(
|
|
14
|
+
input: RequestInfo | URL,
|
|
15
|
+
response: PaymentResponse,
|
|
16
|
+
options?: SseResponseOptions | undefined,
|
|
17
|
+
): AsyncIterable<string>
|
|
18
|
+
rehydrate(parameters: RehydrateParameters): void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const internals = new WeakMap<SessionManager, SessionManagerInternals>()
|
|
22
|
+
|
|
23
|
+
/** @internal Registers private transport and recovery hooks for a session manager. */
|
|
24
|
+
export function registerSessionManagerInternals(
|
|
25
|
+
manager: SessionManager,
|
|
26
|
+
value: SessionManagerInternals,
|
|
27
|
+
): void {
|
|
28
|
+
internals.set(manager, value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @internal Returns private transport and recovery hooks for a session manager. */
|
|
32
|
+
export function getSessionManagerInternals(manager: SessionManager): SessionManagerInternals {
|
|
33
|
+
const value = internals.get(manager)
|
|
34
|
+
if (!value) throw new Error('Session manager internals are unavailable.')
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
@@ -62,6 +62,7 @@ function createMockClient(
|
|
|
62
62
|
parameters.rpcMethods?.push(args.method)
|
|
63
63
|
parameters.onRequest?.(args.method, args.params)
|
|
64
64
|
if (args.method === 'eth_chainId') return `0x${chainId.toString(16)}`
|
|
65
|
+
if (args.method === 'eth_sendTransaction') return txHash
|
|
65
66
|
if (args.method === 'eth_sendRawTransaction') return txHash
|
|
66
67
|
if (args.method === 'eth_sendRawTransactionSync') return parameters.receipt ?? receipt([])
|
|
67
68
|
if (args.method === 'eth_getTransactionReceipt') return parameters.receipt ?? null
|
|
@@ -290,6 +291,31 @@ describe('precompile receipt wait', () => {
|
|
|
290
291
|
})
|
|
291
292
|
})
|
|
292
293
|
|
|
294
|
+
describe('precompile server transactions', () => {
|
|
295
|
+
test('does not add a fee-payer signature when the sender and fee payer are the same account', async () => {
|
|
296
|
+
const rpcMethods: string[] = []
|
|
297
|
+
const client = createMockClient({ rpcMethods })
|
|
298
|
+
const sender = { address: feePayer.address, type: 'json-rpc' } as const
|
|
299
|
+
|
|
300
|
+
await Chain.closeOnChain(
|
|
301
|
+
client,
|
|
302
|
+
descriptor,
|
|
303
|
+
1n,
|
|
304
|
+
1n,
|
|
305
|
+
`0x${'11'.repeat(65)}`,
|
|
306
|
+
tip20ChannelEscrow,
|
|
307
|
+
{
|
|
308
|
+
account: sender,
|
|
309
|
+
candidateFeeTokens: [descriptor.token],
|
|
310
|
+
feePayer,
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
expect(rpcMethods).toContain('eth_sendTransaction')
|
|
315
|
+
expect(rpcMethods).not.toContain('eth_sendRawTransactionSync')
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
293
319
|
describe('precompile open calldata parsing', () => {
|
|
294
320
|
test('parseOpenCall accepts TIP-1034 open calldata', () => {
|
|
295
321
|
const data = encodeFunctionData({
|