mppx 0.6.13 → 0.6.15
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 +14 -0
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +24 -1
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +3 -0
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +3 -0
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +3 -0
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +22 -0
- package/dist/middlewares/express.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +26 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +11 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +71 -6
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +53 -10
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +76 -29
- package/dist/tempo/server/Session.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/server/internal/request-body.d.ts +1 -1
- package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
- package/dist/tempo/server/internal/request-body.js +3 -0
- package/dist/tempo/server/internal/request-body.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +7 -0
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +16 -0
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts +1 -0
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +28 -11
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts +1 -0
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js +19 -12
- package/dist/tempo/session/Sse.js.map +1 -1
- package/package.json +3 -3
- package/src/cli/plugins/tempo.ts +28 -1
- package/src/client/Mppx.test.ts +39 -3
- package/src/client/Mppx.ts +2 -0
- package/src/client/internal/Fetch.test.ts +22 -3
- package/src/client/internal/Fetch.ts +2 -0
- package/src/mcp-sdk/client/McpClient.test.ts +39 -2
- package/src/mcp-sdk/client/McpClient.ts +3 -0
- package/src/middlewares/express.test.ts +27 -0
- package/src/middlewares/express.ts +24 -0
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/client/SessionManager.ts +26 -1
- package/src/tempo/internal/fee-payer.test.ts +139 -0
- package/src/tempo/internal/fee-payer.ts +85 -6
- package/src/tempo/server/Charge.test.ts +119 -0
- package/src/tempo/server/Charge.ts +70 -10
- package/src/tempo/server/Session.test.ts +327 -0
- package/src/tempo/server/Session.ts +88 -39
- package/src/tempo/server/internal/html/main.ts +10 -3
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +26 -0
- package/src/tempo/server/internal/request-body.ts +4 -1
- package/src/tempo/server/internal/transport.test.ts +28 -2
- package/src/tempo/server/internal/transport.ts +23 -0
- package/src/tempo/session/Chain.test.ts +140 -1
- package/src/tempo/session/Chain.ts +34 -10
- package/src/tempo/session/Sse.test.ts +25 -0
- package/src/tempo/session/Sse.ts +9 -2
|
@@ -82,6 +82,17 @@ describe('request-body', () => {
|
|
|
82
82
|
}),
|
|
83
83
|
).toBe(false)
|
|
84
84
|
})
|
|
85
|
+
|
|
86
|
+
test('treats bodyless POST requests with query parameters as content requests', () => {
|
|
87
|
+
expect(
|
|
88
|
+
isSessionContentRequest({
|
|
89
|
+
hasBody: false,
|
|
90
|
+
headers: new Headers(),
|
|
91
|
+
method: 'POST',
|
|
92
|
+
url: new URL('https://api.example.com/search?q=paid'),
|
|
93
|
+
}),
|
|
94
|
+
).toBe(true)
|
|
95
|
+
})
|
|
85
96
|
})
|
|
86
97
|
|
|
87
98
|
describe('shouldChargePlainResponse', () => {
|
|
@@ -121,6 +132,20 @@ describe('request-body', () => {
|
|
|
121
132
|
),
|
|
122
133
|
).toBe(false)
|
|
123
134
|
})
|
|
135
|
+
|
|
136
|
+
test('charges bodyless POST query requests', () => {
|
|
137
|
+
expect(
|
|
138
|
+
shouldChargePlainResponse(
|
|
139
|
+
{
|
|
140
|
+
hasBody: false,
|
|
141
|
+
headers: new Headers(),
|
|
142
|
+
method: 'POST',
|
|
143
|
+
url: new URL('https://api.example.com/search?q=paid'),
|
|
144
|
+
},
|
|
145
|
+
{ action: 'voucher' },
|
|
146
|
+
),
|
|
147
|
+
).toBe(true)
|
|
148
|
+
})
|
|
124
149
|
})
|
|
125
150
|
|
|
126
151
|
describe('captureRequestBodyProbe', () => {
|
|
@@ -136,6 +161,7 @@ describe('request-body', () => {
|
|
|
136
161
|
headers: request.headers,
|
|
137
162
|
hasBody: true,
|
|
138
163
|
method: 'POST',
|
|
164
|
+
url: new URL('https://example.com/'),
|
|
139
165
|
})
|
|
140
166
|
})
|
|
141
167
|
})
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type * as Method from '../../../Method.js'
|
|
2
2
|
import type { SessionCredentialPayload } from '../../session/Types.js'
|
|
3
3
|
|
|
4
|
-
export type RequestBodyProbe = Pick<Method.CapturedRequest, 'headers' | 'hasBody' | 'method'>
|
|
4
|
+
export type RequestBodyProbe = Pick<Method.CapturedRequest, 'headers' | 'hasBody' | 'method'> &
|
|
5
|
+
Partial<Pick<Method.CapturedRequest, 'url'>>
|
|
5
6
|
|
|
6
7
|
export function captureRequestBodyProbe(input: Request): RequestBodyProbe {
|
|
7
8
|
return {
|
|
8
9
|
headers: input.headers,
|
|
9
10
|
hasBody: input.body !== null,
|
|
10
11
|
method: input.method,
|
|
12
|
+
url: new URL(input.url),
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -25,6 +27,7 @@ export function hasCapturedRequestBody(
|
|
|
25
27
|
export function isSessionContentRequest(input: RequestBodyProbe): boolean {
|
|
26
28
|
if (input.method === 'HEAD') return false
|
|
27
29
|
if (input.method !== 'POST') return true
|
|
30
|
+
if (input.url?.search) return true
|
|
28
31
|
return hasCapturedRequestBody(input)
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -7,7 +7,8 @@ import { chainId, escrowContract as escrowContractDefaults } from '../../interna
|
|
|
7
7
|
import * as ChannelStore from '../../session/ChannelStore.js'
|
|
8
8
|
import { deserializeSessionReceipt } from '../../session/Receipt.js'
|
|
9
9
|
import { parseEvent } from '../../session/Sse.js'
|
|
10
|
-
import {
|
|
10
|
+
import type { SessionReceipt } from '../../session/Types.js'
|
|
11
|
+
import { markPrepaidSessionTick, sse } from './transport.js'
|
|
11
12
|
|
|
12
13
|
const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
13
14
|
const challengeId = 'challenge-1'
|
|
@@ -129,7 +130,7 @@ type ReceiptOverrides = Partial<{
|
|
|
129
130
|
units: number
|
|
130
131
|
}>
|
|
131
132
|
|
|
132
|
-
function makeReceipt(overrides: ReceiptOverrides = {}) {
|
|
133
|
+
function makeReceipt(overrides: ReceiptOverrides = {}): SessionReceipt {
|
|
133
134
|
return {
|
|
134
135
|
method: 'tempo',
|
|
135
136
|
intent: 'session' as const,
|
|
@@ -666,6 +667,31 @@ describe('sse transport', () => {
|
|
|
666
667
|
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
667
668
|
})
|
|
668
669
|
|
|
670
|
+
test('respondReceipt with prepaid plain Response does not deduct again', async () => {
|
|
671
|
+
const store = memoryStore()
|
|
672
|
+
await seedChannel(store, 10000000n)
|
|
673
|
+
const transport = sse({ store })
|
|
674
|
+
|
|
675
|
+
const response = transport.respondReceipt({
|
|
676
|
+
credential: makeCredential(),
|
|
677
|
+
input: makeAuthorizedRequest(),
|
|
678
|
+
receipt: markPrepaidSessionTick(makeReceipt({ spent: '1000000', units: 1 })),
|
|
679
|
+
response: new Response('ok', {
|
|
680
|
+
headers: { 'Content-Type': 'application/json' },
|
|
681
|
+
}),
|
|
682
|
+
challengeId,
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
expect(await response.text()).toBe('ok')
|
|
686
|
+
const receipt = deserializeSessionReceipt(response.headers.get('Payment-Receipt')!)
|
|
687
|
+
const channel = await store.getChannel(channelId)
|
|
688
|
+
|
|
689
|
+
expect(channel!.spent).toBe(0n)
|
|
690
|
+
expect(channel!.units).toBe(0)
|
|
691
|
+
expect(receipt.spent).toBe('1000000')
|
|
692
|
+
expect(receipt.units).toBe(1)
|
|
693
|
+
})
|
|
694
|
+
|
|
669
695
|
test('respondReceipt no longer depends on prior getCredential side effects', async () => {
|
|
670
696
|
const store = memoryStore()
|
|
671
697
|
await seedChannel(store, 10000000n)
|
|
@@ -13,9 +13,28 @@ import * as Sse_core from '../../session/Sse.js'
|
|
|
13
13
|
import type { SessionCredentialPayload, SessionReceipt } from '../../session/Types.js'
|
|
14
14
|
import { captureRequestBodyProbe, shouldChargePlainResponse } from './request-body.js'
|
|
15
15
|
|
|
16
|
+
const prepaidSessionTick = Symbol('mppx.prepaidSessionTick')
|
|
17
|
+
|
|
16
18
|
/** SSE transport with Tempo session controller. */
|
|
17
19
|
export type Sse = Transport.Sse<Sse_core.SessionController>
|
|
18
20
|
|
|
21
|
+
export type PrepaidSessionReceipt = SessionReceipt & {
|
|
22
|
+
[prepaidSessionTick]?: true | undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function markPrepaidSessionTick(receipt: SessionReceipt): SessionReceipt {
|
|
26
|
+
Object.defineProperty(receipt, prepaidSessionTick, {
|
|
27
|
+
configurable: false,
|
|
28
|
+
enumerable: false,
|
|
29
|
+
value: true,
|
|
30
|
+
})
|
|
31
|
+
return receipt
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasPrepaidSessionTick(receipt: SessionReceipt): boolean {
|
|
35
|
+
return (receipt as PrepaidSessionReceipt)[prepaidSessionTick] === true
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
/**
|
|
20
39
|
* Creates a Tempo-metered SSE transport.
|
|
21
40
|
*
|
|
@@ -93,6 +112,7 @@ export function sse(options: sse.Options & { store: ChannelStore.ChannelStore })
|
|
|
93
112
|
tickCost,
|
|
94
113
|
pollIntervalMs: pollingInterval,
|
|
95
114
|
generate,
|
|
115
|
+
prepaidUnits: hasPrepaidSessionTick(receipt as SessionReceipt) ? 1 : 0,
|
|
96
116
|
signal: input.signal,
|
|
97
117
|
})
|
|
98
118
|
return Sse_core.toResponse(stream)
|
|
@@ -113,6 +133,9 @@ export function sse(options: sse.Options & { store: ChannelStore.ChannelStore })
|
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
const currentReceipt = receipt as SessionReceipt
|
|
136
|
+
if (hasPrepaidSessionTick(currentReceipt)) {
|
|
137
|
+
return baseResponse
|
|
138
|
+
}
|
|
116
139
|
const available = BigInt(currentReceipt.acceptedCumulative) - BigInt(currentReceipt.spent)
|
|
117
140
|
if (available < tickCost) {
|
|
118
141
|
const error = new Errors.InsufficientBalanceError({
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Address,
|
|
3
|
+
createClient,
|
|
4
|
+
custom,
|
|
5
|
+
encodeFunctionData,
|
|
6
|
+
erc20Abi,
|
|
7
|
+
type Hex,
|
|
8
|
+
zeroAddress,
|
|
9
|
+
} from 'viem'
|
|
2
10
|
import { prepareTransactionRequest, signTransaction, waitForTransactionReceipt } from 'viem/actions'
|
|
3
11
|
import { Addresses, Transaction } from 'viem/tempo'
|
|
4
12
|
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
@@ -450,6 +458,70 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
450
458
|
).rejects.toThrow('gas exceeds sponsor policy')
|
|
451
459
|
})
|
|
452
460
|
|
|
461
|
+
test('fee-payer: simulates open before broadcasting', async () => {
|
|
462
|
+
const rpcMethods: string[] = []
|
|
463
|
+
const interceptingClient = createClient({
|
|
464
|
+
account: accounts[0],
|
|
465
|
+
chain: client.chain,
|
|
466
|
+
transport: custom({
|
|
467
|
+
async request(args: any) {
|
|
468
|
+
rpcMethods.push(args.method)
|
|
469
|
+
return client.transport.request(args)
|
|
470
|
+
},
|
|
471
|
+
}),
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
const salt = nextSalt()
|
|
475
|
+
const deposit = 5_000_000n
|
|
476
|
+
const approveData = encodeFunctionData({
|
|
477
|
+
abi: erc20Abi,
|
|
478
|
+
functionName: 'approve',
|
|
479
|
+
args: [escrowContract, deposit],
|
|
480
|
+
})
|
|
481
|
+
const openData = encodeFunctionData({
|
|
482
|
+
abi: escrowAbi,
|
|
483
|
+
functionName: 'open',
|
|
484
|
+
args: [recipient, currency, deposit, salt, zeroAddress],
|
|
485
|
+
})
|
|
486
|
+
const channelId = Channel.computeId({
|
|
487
|
+
authorizedSigner: zeroAddress,
|
|
488
|
+
chainId: chain.id,
|
|
489
|
+
escrowContract,
|
|
490
|
+
payee: recipient,
|
|
491
|
+
payer: payer.address,
|
|
492
|
+
salt,
|
|
493
|
+
token: currency,
|
|
494
|
+
}) as Hex
|
|
495
|
+
const prepared = await prepareTransactionRequest(client, {
|
|
496
|
+
account: payer,
|
|
497
|
+
calls: [
|
|
498
|
+
{ to: currency, data: approveData },
|
|
499
|
+
{ to: escrowContract, data: openData },
|
|
500
|
+
],
|
|
501
|
+
feePayer: true,
|
|
502
|
+
feeToken: currency,
|
|
503
|
+
} as never)
|
|
504
|
+
prepared.gas = prepared.gas! + 5_000n
|
|
505
|
+
const serializedTransaction = await signTransaction(client, prepared as never)
|
|
506
|
+
|
|
507
|
+
await broadcastOpenTransaction({
|
|
508
|
+
client: interceptingClient,
|
|
509
|
+
serializedTransaction,
|
|
510
|
+
escrowContract,
|
|
511
|
+
channelId,
|
|
512
|
+
recipient,
|
|
513
|
+
currency,
|
|
514
|
+
feePayer: accounts[0],
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
const broadcastIndex = rpcMethods.indexOf('eth_sendRawTransactionSync')
|
|
518
|
+
const simulationIndex = rpcMethods.indexOf('eth_call')
|
|
519
|
+
|
|
520
|
+
expect(broadcastIndex).toBeGreaterThan(-1)
|
|
521
|
+
expect(simulationIndex).toBeGreaterThan(-1)
|
|
522
|
+
expect(simulationIndex).toBeLessThan(broadcastIndex)
|
|
523
|
+
})
|
|
524
|
+
|
|
453
525
|
test('fee-payer: rejects smuggled second open call', async () => {
|
|
454
526
|
const deposit = 5_000_000n
|
|
455
527
|
const smuggledDeposit = 7_000_000n
|
|
@@ -892,6 +964,73 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
892
964
|
).rejects.toThrow('gas exceeds sponsor policy')
|
|
893
965
|
})
|
|
894
966
|
|
|
967
|
+
test('fee-payer: simulates topUp before broadcasting', async () => {
|
|
968
|
+
const rpcMethods: string[] = []
|
|
969
|
+
const interceptingClient = createClient({
|
|
970
|
+
account: accounts[0],
|
|
971
|
+
chain: client.chain,
|
|
972
|
+
transport: custom({
|
|
973
|
+
async request(args: any) {
|
|
974
|
+
rpcMethods.push(args.method)
|
|
975
|
+
return client.transport.request(args)
|
|
976
|
+
},
|
|
977
|
+
}),
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
const salt = nextSalt()
|
|
981
|
+
const deposit = 5_000_000n
|
|
982
|
+
const topUpAmount = 3_000_000n
|
|
983
|
+
|
|
984
|
+
const { channelId } = await openChannel({
|
|
985
|
+
escrow: escrowContract,
|
|
986
|
+
payer,
|
|
987
|
+
payee: recipient,
|
|
988
|
+
token: currency,
|
|
989
|
+
deposit,
|
|
990
|
+
salt,
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
const approveData = encodeFunctionData({
|
|
994
|
+
abi: erc20Abi,
|
|
995
|
+
functionName: 'approve',
|
|
996
|
+
args: [escrowContract, topUpAmount],
|
|
997
|
+
})
|
|
998
|
+
const topUpData = encodeFunctionData({
|
|
999
|
+
abi: escrowAbi,
|
|
1000
|
+
functionName: 'topUp',
|
|
1001
|
+
args: [channelId, topUpAmount],
|
|
1002
|
+
})
|
|
1003
|
+
const prepared = await prepareTransactionRequest(client, {
|
|
1004
|
+
account: payer,
|
|
1005
|
+
calls: [
|
|
1006
|
+
{ to: currency, data: approveData },
|
|
1007
|
+
{ to: escrowContract, data: topUpData },
|
|
1008
|
+
],
|
|
1009
|
+
feePayer: true,
|
|
1010
|
+
feeToken: currency,
|
|
1011
|
+
} as never)
|
|
1012
|
+
prepared.gas = prepared.gas! + 5_000n
|
|
1013
|
+
const serializedTransaction = await signTransaction(client, prepared as never)
|
|
1014
|
+
|
|
1015
|
+
await broadcastTopUpTransaction({
|
|
1016
|
+
client: interceptingClient,
|
|
1017
|
+
serializedTransaction,
|
|
1018
|
+
escrowContract,
|
|
1019
|
+
channelId,
|
|
1020
|
+
currency: asset,
|
|
1021
|
+
declaredDeposit: topUpAmount,
|
|
1022
|
+
previousDeposit: deposit,
|
|
1023
|
+
feePayer: accounts[0],
|
|
1024
|
+
})
|
|
1025
|
+
|
|
1026
|
+
const broadcastIndex = rpcMethods.indexOf('eth_sendRawTransactionSync')
|
|
1027
|
+
const simulationIndex = rpcMethods.indexOf('eth_call')
|
|
1028
|
+
|
|
1029
|
+
expect(broadcastIndex).toBeGreaterThan(-1)
|
|
1030
|
+
expect(simulationIndex).toBeGreaterThan(-1)
|
|
1031
|
+
expect(simulationIndex).toBeLessThan(broadcastIndex)
|
|
1032
|
+
})
|
|
1033
|
+
|
|
895
1034
|
test('fee-payer: rejects smuggled second topUp call', async () => {
|
|
896
1035
|
const salt = nextSalt()
|
|
897
1036
|
const deposit = 5_000_000n
|
|
@@ -449,6 +449,7 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
449
449
|
challengeExpires?: string | undefined
|
|
450
450
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
451
451
|
feePayer?: Account | undefined
|
|
452
|
+
beforeBroadcast?: ((onChain: OnChainChannel) => Promise<void> | void) | undefined
|
|
452
453
|
/** When false, simulates instead of waiting for confirmation and returns derived on-chain state. @default true */
|
|
453
454
|
waitForConfirmation?: boolean | undefined
|
|
454
455
|
}): Promise<BroadcastResult> {
|
|
@@ -462,6 +463,7 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
462
463
|
challengeExpires,
|
|
463
464
|
feePayerPolicy,
|
|
464
465
|
feePayer,
|
|
466
|
+
beforeBroadcast,
|
|
465
467
|
waitForConfirmation = true,
|
|
466
468
|
} = parameters
|
|
467
469
|
|
|
@@ -551,6 +553,19 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
551
553
|
const resolvedFeeToken =
|
|
552
554
|
transaction.feeToken ?? defaults.currency[client.chain?.id as keyof typeof defaults.currency]
|
|
553
555
|
|
|
556
|
+
const pendingOnChain = {
|
|
557
|
+
finalized: false,
|
|
558
|
+
closeRequestedAt: 0n,
|
|
559
|
+
payer: transaction.from,
|
|
560
|
+
payee,
|
|
561
|
+
token,
|
|
562
|
+
authorizedSigner,
|
|
563
|
+
deposit,
|
|
564
|
+
settled: 0n,
|
|
565
|
+
} as OnChainChannel
|
|
566
|
+
|
|
567
|
+
await beforeBroadcast?.(pendingOnChain)
|
|
568
|
+
|
|
554
569
|
const serializedTransaction_final = await (async () => {
|
|
555
570
|
if (feePayer) {
|
|
556
571
|
if (!sponsoredOpenCall)
|
|
@@ -588,21 +603,20 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
588
603
|
|
|
589
604
|
return {
|
|
590
605
|
txHash,
|
|
591
|
-
onChain:
|
|
592
|
-
finalized: false,
|
|
593
|
-
closeRequestedAt: 0n,
|
|
594
|
-
payer: transaction.from,
|
|
595
|
-
payee,
|
|
596
|
-
token,
|
|
597
|
-
authorizedSigner,
|
|
598
|
-
deposit,
|
|
599
|
-
settled: 0n,
|
|
600
|
-
} as OnChainChannel,
|
|
606
|
+
onChain: pendingOnChain,
|
|
601
607
|
}
|
|
602
608
|
}
|
|
603
609
|
|
|
604
610
|
let txHash: Hex | undefined
|
|
605
611
|
try {
|
|
612
|
+
if (feePayer)
|
|
613
|
+
await call(client, {
|
|
614
|
+
...transaction,
|
|
615
|
+
account: transaction.from,
|
|
616
|
+
feeToken: resolvedFeeToken,
|
|
617
|
+
calls,
|
|
618
|
+
} as never)
|
|
619
|
+
|
|
606
620
|
const receipt = await sendRawTransactionSync(client, {
|
|
607
621
|
serializedTransaction: serializedTransaction_final as Transaction.TransactionSerializedTempo,
|
|
608
622
|
})
|
|
@@ -732,6 +746,16 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
732
746
|
return serializedTransaction
|
|
733
747
|
})()
|
|
734
748
|
|
|
749
|
+
if (feePayer)
|
|
750
|
+
await call(client, {
|
|
751
|
+
...transaction,
|
|
752
|
+
account: transaction.from,
|
|
753
|
+
feeToken:
|
|
754
|
+
transaction.feeToken ??
|
|
755
|
+
defaults.currency[client.chain?.id as keyof typeof defaults.currency],
|
|
756
|
+
calls,
|
|
757
|
+
} as never)
|
|
758
|
+
|
|
735
759
|
const receipt = await sendRawTransactionSync(client, {
|
|
736
760
|
serializedTransaction: serializedTransaction_final as Transaction.TransactionSerializedTempo,
|
|
737
761
|
})
|
|
@@ -258,6 +258,31 @@ describe('serve', () => {
|
|
|
258
258
|
expect(channel!.units).toBe(3)
|
|
259
259
|
})
|
|
260
260
|
|
|
261
|
+
test('uses a prepaid unit for the first generated value', async () => {
|
|
262
|
+
const storage = memoryStore()
|
|
263
|
+
await seedChannel(storage, 3000000n)
|
|
264
|
+
await storage.updateChannel(channelId, (current) =>
|
|
265
|
+
current ? { ...current, spent: 1000000n, units: 1 } : current,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
const stream = serve({
|
|
269
|
+
store: storage,
|
|
270
|
+
channelId,
|
|
271
|
+
challengeId,
|
|
272
|
+
tickCost: 1000000n,
|
|
273
|
+
generate: generate(['hello', 'world']),
|
|
274
|
+
prepaidUnits: 1,
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const output = await readStream(stream)
|
|
278
|
+
expect(output).toContain('event: message\ndata: hello\n\n')
|
|
279
|
+
expect(output).toContain('event: message\ndata: world\n\n')
|
|
280
|
+
|
|
281
|
+
const channel = await storage.getChannel(channelId)
|
|
282
|
+
expect(channel!.spent).toBe(2000000n)
|
|
283
|
+
expect(channel!.units).toBe(2)
|
|
284
|
+
})
|
|
285
|
+
|
|
261
286
|
test('emits multiline message values as a single SSE message event', async () => {
|
|
262
287
|
const storage = memoryStore()
|
|
263
288
|
await seedChannel(storage, 1000000n)
|
package/src/tempo/session/Sse.ts
CHANGED
|
@@ -141,11 +141,16 @@ export function serve(options: serve.Options): ReadableStream<Uint8Array> {
|
|
|
141
141
|
async start(controller) {
|
|
142
142
|
const aborted = () => signal?.aborted ?? false
|
|
143
143
|
const emit = (event: string) => controller.enqueue(encoder.encode(event))
|
|
144
|
+
let prepaidUnits = options.prepaidUnits ?? 0
|
|
144
145
|
let reservedAmount = 0n
|
|
145
146
|
let reservedUnits = 0
|
|
146
147
|
|
|
147
|
-
const charge = () =>
|
|
148
|
-
|
|
148
|
+
const charge = () => {
|
|
149
|
+
if (prepaidUnits > 0) {
|
|
150
|
+
prepaidUnits -= 1
|
|
151
|
+
return Promise.resolve()
|
|
152
|
+
}
|
|
153
|
+
return reserveChargeOrWait({
|
|
149
154
|
store,
|
|
150
155
|
channelId,
|
|
151
156
|
amount: tickCost,
|
|
@@ -157,6 +162,7 @@ export function serve(options: serve.Options): ReadableStream<Uint8Array> {
|
|
|
157
162
|
reservedAmount += tickCost
|
|
158
163
|
reservedUnits += 1
|
|
159
164
|
})
|
|
165
|
+
}
|
|
160
166
|
|
|
161
167
|
const iterable: AsyncIterable<string> =
|
|
162
168
|
typeof generate === 'function' ? generate({ charge }) : generate
|
|
@@ -207,6 +213,7 @@ export declare namespace serve {
|
|
|
207
213
|
tickCost: bigint
|
|
208
214
|
generate: AsyncIterable<string> | ((stream: SessionController) => AsyncIterable<string>)
|
|
209
215
|
pollIntervalMs?: number | undefined
|
|
216
|
+
prepaidUnits?: number | undefined
|
|
210
217
|
signal?: AbortSignal | undefined
|
|
211
218
|
}
|
|
212
219
|
}
|