mppx 0.3.8 → 0.3.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/README.md +3 -3
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +2 -0
- package/dist/Challenge.js.map +1 -1
- package/dist/Errors.d.ts +0 -2
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +1 -3
- package/dist/Errors.js.map +1 -1
- package/dist/internal/constantTimeEqual.d.ts.map +1 -1
- package/dist/internal/constantTimeEqual.js +4 -6
- package/dist/internal/constantTimeEqual.js.map +1 -1
- package/dist/internal/env.d.ts +2 -2
- package/dist/internal/env.d.ts.map +1 -1
- package/dist/internal/env.js +1 -2
- package/dist/internal/env.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +6 -2
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/server/Mppx.d.ts +13 -3
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +46 -3
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/internal/simulate.d.ts +21 -0
- package/dist/tempo/internal/simulate.d.ts.map +1 -0
- package/dist/tempo/internal/simulate.js +31 -0
- package/dist/tempo/internal/simulate.js.map +1 -0
- package/dist/tempo/server/Charge.d.ts +12 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +28 -6
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +18 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +66 -46
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts +5 -2
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +78 -10
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +1 -1
- package/src/Challenge.ts +2 -0
- package/src/Errors.test.ts +43 -18
- package/src/Errors.ts +1 -4
- package/src/client/Mppx.test.ts +1 -0
- package/src/internal/constantTimeEqual.ts +5 -4
- package/src/internal/env.test.ts +2 -2
- package/src/internal/env.ts +4 -5
- package/src/middlewares/express.test.ts +5 -0
- package/src/middlewares/hono.test.ts +5 -0
- package/src/middlewares/internal/mppx.ts +5 -2
- package/src/middlewares/nextjs.test.ts +5 -0
- package/src/proxy/Proxy.test.ts +3 -0
- package/src/proxy/services/openai.test.ts +3 -0
- package/src/server/Mppx.test.ts +93 -2
- package/src/server/Mppx.ts +81 -6
- package/src/tempo/internal/simulate.ts +49 -0
- package/src/tempo/server/Charge.test.ts +62 -0
- package/src/tempo/server/Charge.ts +44 -6
- package/src/tempo/server/Session.test.ts +51 -2
- package/src/tempo/server/Session.ts +97 -38
- package/src/tempo/session/Chain.test.ts +190 -0
- package/src/tempo/session/Chain.ts +109 -5
|
@@ -524,6 +524,68 @@ describe('tempo', () => {
|
|
|
524
524
|
})
|
|
525
525
|
})
|
|
526
526
|
|
|
527
|
+
describe('intent: charge; type: transaction; waitForConfirmation: false', () => {
|
|
528
|
+
test('returns receipt without waiting for confirmation', async () => {
|
|
529
|
+
const serverNoWait = Mppx_server.create({
|
|
530
|
+
methods: [
|
|
531
|
+
tempo_server.charge({
|
|
532
|
+
getClient() {
|
|
533
|
+
return client
|
|
534
|
+
},
|
|
535
|
+
currency: asset,
|
|
536
|
+
account: accounts[0],
|
|
537
|
+
waitForConfirmation: false,
|
|
538
|
+
}),
|
|
539
|
+
],
|
|
540
|
+
realm,
|
|
541
|
+
secretKey,
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
const mppx = Mppx_client.create({
|
|
545
|
+
polyfill: false,
|
|
546
|
+
methods: [
|
|
547
|
+
tempo_client({
|
|
548
|
+
account: accounts[1],
|
|
549
|
+
getClient() {
|
|
550
|
+
return client
|
|
551
|
+
},
|
|
552
|
+
}),
|
|
553
|
+
],
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
557
|
+
const result = await Mppx_server.toNodeListener(
|
|
558
|
+
serverNoWait.charge({
|
|
559
|
+
amount: '1',
|
|
560
|
+
currency: asset,
|
|
561
|
+
recipient: accounts[0].address,
|
|
562
|
+
}),
|
|
563
|
+
)(req, res)
|
|
564
|
+
if (result.status === 402) return
|
|
565
|
+
res.end('OK')
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
const response = await fetch(httpServer.url)
|
|
569
|
+
expect(response.status).toBe(402)
|
|
570
|
+
|
|
571
|
+
const credential = await mppx.createCredential(response)
|
|
572
|
+
|
|
573
|
+
{
|
|
574
|
+
const response = await fetch(httpServer.url, {
|
|
575
|
+
headers: { Authorization: credential },
|
|
576
|
+
})
|
|
577
|
+
expect(response.status).toBe(200)
|
|
578
|
+
|
|
579
|
+
const receipt = Receipt.fromResponse(response)
|
|
580
|
+
expect(receipt.status).toBe('success')
|
|
581
|
+
expect(receipt.method).toBe('tempo')
|
|
582
|
+
expect(receipt.reference).toBeDefined()
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
httpServer.close()
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
|
|
527
589
|
describe('intent: unknown', () => {
|
|
528
590
|
test('behavior: returns 402 for invalid payload schema', async () => {
|
|
529
591
|
const httpServer = await Http.createServer(async (req, res) => {
|
|
@@ -5,7 +5,12 @@ import {
|
|
|
5
5
|
type TransactionReceipt,
|
|
6
6
|
toFunctionSelector,
|
|
7
7
|
} from 'viem'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getTransactionReceipt,
|
|
10
|
+
sendRawTransaction,
|
|
11
|
+
sendRawTransactionSync,
|
|
12
|
+
signTransaction,
|
|
13
|
+
} from 'viem/actions'
|
|
9
14
|
import { tempo as tempo_chain } from 'viem/chains'
|
|
10
15
|
import { Abis, Transaction } from 'viem/tempo'
|
|
11
16
|
import { PaymentExpiredError } from '../../Errors.js'
|
|
@@ -14,6 +19,7 @@ import * as Method from '../../Method.js'
|
|
|
14
19
|
import * as Client from '../../viem/Client.js'
|
|
15
20
|
import * as Account from '../internal/account.js'
|
|
16
21
|
import * as defaults from '../internal/defaults.js'
|
|
22
|
+
import { simulateTransaction } from '../internal/simulate.js'
|
|
17
23
|
import type * as types from '../internal/types.js'
|
|
18
24
|
import * as Methods from '../Methods.js'
|
|
19
25
|
|
|
@@ -45,6 +51,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
45
51
|
description,
|
|
46
52
|
externalId,
|
|
47
53
|
memo,
|
|
54
|
+
waitForConfirmation = true,
|
|
48
55
|
} = parameters
|
|
49
56
|
|
|
50
57
|
const { recipient, feePayer } = Account.resolve(parameters)
|
|
@@ -250,11 +257,30 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
250
257
|
return serializedTransaction
|
|
251
258
|
})()
|
|
252
259
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
260
|
+
if (waitForConfirmation) {
|
|
261
|
+
const receipt = await sendRawTransactionSync(client, {
|
|
262
|
+
serializedTransaction: serializedTransaction_final,
|
|
263
|
+
})
|
|
264
|
+
return toReceipt(receipt)
|
|
265
|
+
} else {
|
|
266
|
+
// Optimistic path: simulate to catch obvious reverts, then broadcast
|
|
267
|
+
// without waiting for on-chain confirmation. The returned receipt
|
|
268
|
+
// assumes success — callers opt into this risk via waitForConfirmation: false.
|
|
269
|
+
await simulateTransaction(client, {
|
|
270
|
+
...transaction,
|
|
271
|
+
from: transaction.from as `0x${string}`,
|
|
272
|
+
calls,
|
|
273
|
+
})
|
|
274
|
+
const hash = await sendRawTransaction(client, {
|
|
275
|
+
serializedTransaction: serializedTransaction_final,
|
|
276
|
+
})
|
|
277
|
+
return {
|
|
278
|
+
method: 'tempo',
|
|
279
|
+
status: 'success',
|
|
280
|
+
timestamp: new Date().toISOString(),
|
|
281
|
+
reference: hash,
|
|
282
|
+
} as const
|
|
283
|
+
}
|
|
258
284
|
}
|
|
259
285
|
|
|
260
286
|
default:
|
|
@@ -270,6 +296,18 @@ export declare namespace charge {
|
|
|
270
296
|
type Parameters = {
|
|
271
297
|
/** Testnet mode. */
|
|
272
298
|
testnet?: boolean | undefined
|
|
299
|
+
/**
|
|
300
|
+
* Whether to wait for the charge transaction to confirm on-chain before
|
|
301
|
+
* responding. @default true
|
|
302
|
+
*
|
|
303
|
+
* When `false`, the transaction is simulated via `eth_estimateGas` and
|
|
304
|
+
* broadcast without waiting for inclusion. The receipt will optimistically
|
|
305
|
+
* report `status: 'success'` based on simulation alone — if the
|
|
306
|
+
* transaction reverts on-chain after broadcast (e.g. due to a state
|
|
307
|
+
* change between simulation and inclusion), the receipt will not reflect
|
|
308
|
+
* the failure.
|
|
309
|
+
*/
|
|
310
|
+
waitForConfirmation?: boolean | undefined
|
|
273
311
|
} & Client.getResolver.Parameters &
|
|
274
312
|
Account.resolve.Parameters &
|
|
275
313
|
Defaults
|
|
@@ -929,7 +929,7 @@ describe('session', () => {
|
|
|
929
929
|
request: makeRequest(),
|
|
930
930
|
})
|
|
931
931
|
|
|
932
|
-
const settleTxHash = await settle(store, client, channelId, escrowContract)
|
|
932
|
+
const settleTxHash = await settle(store, client, channelId, { escrowContract })
|
|
933
933
|
expect(settleTxHash).toMatch(/^0x/)
|
|
934
934
|
|
|
935
935
|
const ch = await store.getChannel(channelId)
|
|
@@ -939,7 +939,7 @@ describe('session', () => {
|
|
|
939
939
|
test('settle rejects when no channel found', async () => {
|
|
940
940
|
const fakeChannelId =
|
|
941
941
|
'0x0000000000000000000000000000000000000000000000000000000000000000' as Hex
|
|
942
|
-
await expect(settle(store, client, fakeChannelId, escrowContract)).rejects.toThrow(
|
|
942
|
+
await expect(settle(store, client, fakeChannelId, { escrowContract })).rejects.toThrow(
|
|
943
943
|
ChannelNotFoundError,
|
|
944
944
|
)
|
|
945
945
|
})
|
|
@@ -1101,6 +1101,55 @@ describe('session', () => {
|
|
|
1101
1101
|
expect(result).toBeUndefined()
|
|
1102
1102
|
})
|
|
1103
1103
|
|
|
1104
|
+
test('returns undefined for open POST with content-length > 0 (content request)', () => {
|
|
1105
|
+
const server = createServer()
|
|
1106
|
+
const result = server.respond!({
|
|
1107
|
+
credential: {
|
|
1108
|
+
challenge: makeChallenge({
|
|
1109
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
|
|
1110
|
+
}),
|
|
1111
|
+
payload: { action: 'open' },
|
|
1112
|
+
},
|
|
1113
|
+
input: new Request('http://localhost', {
|
|
1114
|
+
method: 'POST',
|
|
1115
|
+
headers: { 'content-length': '42' },
|
|
1116
|
+
}),
|
|
1117
|
+
} as any)
|
|
1118
|
+
expect(result).toBeUndefined()
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
test('returns undefined for open POST with transfer-encoding header (content request)', () => {
|
|
1122
|
+
const server = createServer()
|
|
1123
|
+
const result = server.respond!({
|
|
1124
|
+
credential: {
|
|
1125
|
+
challenge: makeChallenge({
|
|
1126
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
|
|
1127
|
+
}),
|
|
1128
|
+
payload: { action: 'open' },
|
|
1129
|
+
},
|
|
1130
|
+
input: new Request('http://localhost', {
|
|
1131
|
+
method: 'POST',
|
|
1132
|
+
headers: { 'transfer-encoding': 'chunked' },
|
|
1133
|
+
}),
|
|
1134
|
+
} as any)
|
|
1135
|
+
expect(result).toBeUndefined()
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
test('returns 204 for GET with topUp action', () => {
|
|
1139
|
+
const server = createServer()
|
|
1140
|
+
const result = server.respond!({
|
|
1141
|
+
credential: {
|
|
1142
|
+
challenge: makeChallenge({
|
|
1143
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
|
|
1144
|
+
}),
|
|
1145
|
+
payload: { action: 'topUp' },
|
|
1146
|
+
},
|
|
1147
|
+
input: new Request('http://localhost', { method: 'GET' }),
|
|
1148
|
+
} as any)
|
|
1149
|
+
expect(result).toBeInstanceOf(Response)
|
|
1150
|
+
expect((result as Response).status).toBe(204)
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1104
1153
|
test('returns undefined for voucher POST with content-length > 0 (content request)', () => {
|
|
1105
1154
|
const server = createServer()
|
|
1106
1155
|
const result = server.respond!({
|
|
@@ -85,13 +85,17 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
85
85
|
const parameters = p as parameters
|
|
86
86
|
const {
|
|
87
87
|
amount,
|
|
88
|
+
channelStateTtl = 60_000,
|
|
88
89
|
currency = defaults.resolveCurrency(parameters),
|
|
89
90
|
decimals = defaults.decimals,
|
|
90
91
|
store: rawStore = Store.memory(),
|
|
91
92
|
suggestedDeposit,
|
|
92
93
|
unitType,
|
|
94
|
+
waitForConfirmation = true,
|
|
93
95
|
} = parameters
|
|
94
96
|
|
|
97
|
+
const lastOnChainVerified = new Map<Hex, number>()
|
|
98
|
+
|
|
95
99
|
const store = ChannelStore.fromStore(rawStore)
|
|
96
100
|
|
|
97
101
|
const getClient = Client.getResolver({
|
|
@@ -187,7 +191,9 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
187
191
|
payload,
|
|
188
192
|
methodDetails,
|
|
189
193
|
resolvedFeePayer,
|
|
194
|
+
waitForConfirmation,
|
|
190
195
|
)
|
|
196
|
+
lastOnChainVerified.set(payload.channelId, Date.now())
|
|
191
197
|
break
|
|
192
198
|
|
|
193
199
|
case 'topUp':
|
|
@@ -199,6 +205,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
199
205
|
methodDetails,
|
|
200
206
|
resolvedFeePayer,
|
|
201
207
|
)
|
|
208
|
+
lastOnChainVerified.set(payload.channelId, Date.now())
|
|
202
209
|
break
|
|
203
210
|
|
|
204
211
|
case 'voucher':
|
|
@@ -209,6 +216,8 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
209
216
|
challenge,
|
|
210
217
|
payload,
|
|
211
218
|
methodDetails,
|
|
219
|
+
channelStateTtl,
|
|
220
|
+
lastOnChainVerified,
|
|
212
221
|
)
|
|
213
222
|
break
|
|
214
223
|
|
|
@@ -220,6 +229,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
220
229
|
payload,
|
|
221
230
|
methodDetails,
|
|
222
231
|
account,
|
|
232
|
+
resolvedFeePayer,
|
|
223
233
|
)
|
|
224
234
|
break
|
|
225
235
|
|
|
@@ -237,34 +247,30 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
237
247
|
// invoking the user's route handler. When it returns undefined, the
|
|
238
248
|
// user's handler runs normally and serves content.
|
|
239
249
|
//
|
|
240
|
-
//
|
|
241
|
-
// return 204 regardless of request method.
|
|
250
|
+
// close and topUp are always gated (204) — they are pure management.
|
|
242
251
|
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// single GET retry) receive content as expected.
|
|
252
|
+
// open and voucher are gated only for bodyless POSTs (management
|
|
253
|
+
// updates). POSTs with a body are content requests — the client's
|
|
254
|
+
// original request piggybacked on the credential — so they fall
|
|
255
|
+
// through to serve content. GETs always fall through so auto-mode
|
|
256
|
+
// clients (whose fetch wrapper bundles open+voucher into a single
|
|
257
|
+
// GET retry) receive content as expected.
|
|
250
258
|
respond({ credential, input }) {
|
|
251
259
|
const { payload } = credential as Credential.Credential<SessionCredentialPayload>
|
|
252
260
|
|
|
253
261
|
if (payload.action === 'close') return new Response(null, { status: 204 })
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (input.headers.has('transfer-encoding')) return undefined
|
|
267
|
-
return new Response(null, { status: 204 })
|
|
262
|
+
if (payload.action === 'topUp') return new Response(null, { status: 204 })
|
|
263
|
+
|
|
264
|
+
// open and voucher: gate only bodyless POSTs (management updates).
|
|
265
|
+
// POSTs with a body are content requests — fall through so the
|
|
266
|
+
// upstream response is returned to the client.
|
|
267
|
+
if (input.method === 'POST') {
|
|
268
|
+
const contentLength = input.headers.get('content-length')
|
|
269
|
+
if (contentLength !== null && contentLength !== '0') return undefined
|
|
270
|
+
if (input.headers.has('transfer-encoding')) return undefined
|
|
271
|
+
return new Response(null, { status: 204 })
|
|
272
|
+
}
|
|
273
|
+
return undefined
|
|
268
274
|
},
|
|
269
275
|
})
|
|
270
276
|
}
|
|
@@ -276,8 +282,22 @@ export declare namespace session {
|
|
|
276
282
|
>
|
|
277
283
|
|
|
278
284
|
type Parameters = {
|
|
285
|
+
/** TTL in milliseconds for cached on-chain channel state. After this duration, the server re-queries on-chain state during voucher handling to detect forced close requests. @default 60_000 */
|
|
286
|
+
channelStateTtl?: number | undefined
|
|
279
287
|
/** Minimum voucher delta to accept (numeric string, default: "0"). */
|
|
280
288
|
minVoucherDelta?: string | undefined
|
|
289
|
+
/**
|
|
290
|
+
* Whether to wait for the open transaction to confirm on-chain before
|
|
291
|
+
* responding. @default true
|
|
292
|
+
*
|
|
293
|
+
* When `false`, the transaction is simulated via `eth_estimateGas` and
|
|
294
|
+
* broadcast without waiting for inclusion. The receipt will optimistically
|
|
295
|
+
* report `status: 'success'` based on simulation alone — if the
|
|
296
|
+
* transaction reverts on-chain after broadcast (e.g. due to a state
|
|
297
|
+
* change between simulation and inclusion), the receipt will not reflect
|
|
298
|
+
* the failure.
|
|
299
|
+
*/
|
|
300
|
+
waitForConfirmation?: boolean | undefined
|
|
281
301
|
/** Store backend for channel state. */
|
|
282
302
|
store?: Store.Store | undefined
|
|
283
303
|
/**
|
|
@@ -310,7 +330,10 @@ export async function settle(
|
|
|
310
330
|
store: ChannelStore.ChannelStore,
|
|
311
331
|
client: viem_Client,
|
|
312
332
|
channelId: Hex,
|
|
313
|
-
|
|
333
|
+
options?: {
|
|
334
|
+
escrowContract?: Address | undefined
|
|
335
|
+
feePayer?: viem_Account | undefined
|
|
336
|
+
},
|
|
314
337
|
): Promise<Hex> {
|
|
315
338
|
const channel = await store.getChannel(channelId)
|
|
316
339
|
if (!channel) throw new ChannelNotFoundError({ reason: 'channel not found' })
|
|
@@ -318,11 +341,17 @@ export async function settle(
|
|
|
318
341
|
|
|
319
342
|
const chainId = client.chain?.id
|
|
320
343
|
const resolvedEscrow =
|
|
321
|
-
escrowContract ??
|
|
344
|
+
options?.escrowContract ??
|
|
345
|
+
defaults.escrowContract[chainId as keyof typeof defaults.escrowContract]
|
|
322
346
|
if (!resolvedEscrow) throw new Error(`No escrow contract for chainId ${chainId}.`)
|
|
323
347
|
|
|
324
348
|
const settledAmount = channel.highestVoucher.cumulativeAmount
|
|
325
|
-
const txHash = await settleOnChain(
|
|
349
|
+
const txHash = await settleOnChain(
|
|
350
|
+
client,
|
|
351
|
+
resolvedEscrow,
|
|
352
|
+
channel.highestVoucher,
|
|
353
|
+
options?.feePayer,
|
|
354
|
+
)
|
|
326
355
|
|
|
327
356
|
await store.updateChannel(channelId, (current) => {
|
|
328
357
|
if (!current) return null
|
|
@@ -482,7 +511,12 @@ async function verifyAndAcceptVoucher(parameters: {
|
|
|
482
511
|
}
|
|
483
512
|
|
|
484
513
|
/**
|
|
485
|
-
* Handle 'open' action -
|
|
514
|
+
* Handle 'open' action - verify voucher, create channel, and broadcast.
|
|
515
|
+
*
|
|
516
|
+
* When `waitForConfirmation` is true (default), the open transaction is
|
|
517
|
+
* broadcast and confirmed on-chain before responding. When false, the
|
|
518
|
+
* transaction is validated and simulated via `eth_estimateGas`; the receipt
|
|
519
|
+
* is returned immediately and the broadcast runs in the background.
|
|
486
520
|
*/
|
|
487
521
|
async function handleOpen(
|
|
488
522
|
store: ChannelStore.ChannelStore,
|
|
@@ -491,6 +525,7 @@ async function handleOpen(
|
|
|
491
525
|
payload: SessionCredentialPayload & { action: 'open' },
|
|
492
526
|
methodDetails: SessionMethodDetails,
|
|
493
527
|
feePayer: viem_Account | undefined,
|
|
528
|
+
waitForConfirmation: boolean,
|
|
494
529
|
): Promise<SessionReceipt> {
|
|
495
530
|
const voucher = parseVoucherFromPayload(
|
|
496
531
|
payload.channelId,
|
|
@@ -510,6 +545,7 @@ async function handleOpen(
|
|
|
510
545
|
recipient,
|
|
511
546
|
currency,
|
|
512
547
|
feePayer,
|
|
548
|
+
waitForConfirmation,
|
|
513
549
|
})
|
|
514
550
|
|
|
515
551
|
validateOnChainChannel(onChain, recipient, currency, amount)
|
|
@@ -621,6 +657,7 @@ async function handleTopUp(
|
|
|
621
657
|
serializedTransaction: payload.transaction,
|
|
622
658
|
escrowContract: methodDetails.escrowContract,
|
|
623
659
|
channelId: payload.channelId,
|
|
660
|
+
currency: challenge.request.currency as Address,
|
|
624
661
|
declaredDeposit,
|
|
625
662
|
previousDeposit: channel.deposit,
|
|
626
663
|
feePayer,
|
|
@@ -645,11 +682,13 @@ async function handleTopUp(
|
|
|
645
682
|
*/
|
|
646
683
|
async function handleVoucher(
|
|
647
684
|
store: ChannelStore.ChannelStore,
|
|
648
|
-
|
|
685
|
+
client: viem_Client,
|
|
649
686
|
minVoucherDelta: bigint,
|
|
650
687
|
challenge: Challenge.Challenge,
|
|
651
688
|
payload: SessionCredentialPayload & { action: 'voucher' },
|
|
652
689
|
methodDetails: SessionMethodDetails,
|
|
690
|
+
channelStateTtl: number,
|
|
691
|
+
lastOnChainVerified: Map<Hex, number>,
|
|
653
692
|
): Promise<SessionReceipt> {
|
|
654
693
|
const channel = await store.getChannel(payload.channelId)
|
|
655
694
|
if (!channel) {
|
|
@@ -671,15 +710,28 @@ async function handleVoucher(
|
|
|
671
710
|
// same session can safely use the cached deposit/signer values.
|
|
672
711
|
// This avoids an RPC round-trip per voucher, which is critical for
|
|
673
712
|
// high-frequency SSE streaming where vouchers arrive per-token.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
713
|
+
//
|
|
714
|
+
// To guard against the payer initiating a forced close while vouchers
|
|
715
|
+
// are still being accepted, re-query on-chain state when the cache
|
|
716
|
+
// exceeds the configured staleness TTL.
|
|
717
|
+
const lastVerified = lastOnChainVerified.get(payload.channelId) ?? 0
|
|
718
|
+
const isStale = Date.now() - lastVerified > channelStateTtl
|
|
719
|
+
|
|
720
|
+
let cachedOnChain: OnChainChannel
|
|
721
|
+
if (isStale) {
|
|
722
|
+
cachedOnChain = await getOnChainChannel(client, methodDetails.escrowContract, payload.channelId)
|
|
723
|
+
lastOnChainVerified.set(payload.channelId, Date.now())
|
|
724
|
+
} else {
|
|
725
|
+
cachedOnChain = {
|
|
726
|
+
payer: channel.payer,
|
|
727
|
+
payee: channel.payee,
|
|
728
|
+
token: channel.token,
|
|
729
|
+
deposit: channel.deposit,
|
|
730
|
+
settled: channel.settledOnChain,
|
|
731
|
+
finalized: channel.finalized,
|
|
732
|
+
authorizedSigner: channel.authorizedSigner,
|
|
733
|
+
closeRequestedAt: 0n,
|
|
734
|
+
}
|
|
683
735
|
}
|
|
684
736
|
|
|
685
737
|
return verifyAndAcceptVoucher({
|
|
@@ -704,6 +756,7 @@ async function handleClose(
|
|
|
704
756
|
payload: SessionCredentialPayload & { action: 'close' },
|
|
705
757
|
methodDetails: SessionMethodDetails,
|
|
706
758
|
account?: viem_Account,
|
|
759
|
+
feePayer?: viem_Account,
|
|
707
760
|
): Promise<SessionReceipt> {
|
|
708
761
|
const channel = await store.getChannel(payload.channelId)
|
|
709
762
|
if (!channel) {
|
|
@@ -754,7 +807,13 @@ async function handleClose(
|
|
|
754
807
|
throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
|
|
755
808
|
}
|
|
756
809
|
|
|
757
|
-
const txHash = await closeOnChain(
|
|
810
|
+
const txHash = await closeOnChain(
|
|
811
|
+
client,
|
|
812
|
+
methodDetails.escrowContract,
|
|
813
|
+
voucher,
|
|
814
|
+
account,
|
|
815
|
+
feePayer,
|
|
816
|
+
)
|
|
758
817
|
|
|
759
818
|
const updated = await store.updateChannel(payload.channelId, (current) => {
|
|
760
819
|
if (!current) return null
|