mppx 0.5.12 → 0.5.14
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 +16 -0
- package/dist/server/internal/html/config.d.ts.map +1 -1
- package/dist/server/internal/html/config.js +8 -1
- package/dist/server/internal/html/config.js.map +1 -1
- package/dist/tempo/Methods.d.ts +8 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +6 -2
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +11 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +14 -2
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +6 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +8 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +31 -4
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +17 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +13 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +6 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Session.d.ts +4 -0
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +36 -28
- 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/session/Chain.d.ts +5 -0
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +202 -63
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +1 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +38 -15
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/package.json +2 -2
- package/src/server/Transport.test.ts +20 -0
- package/src/server/internal/html/config.ts +9 -1
- package/src/tempo/Methods.test.ts +25 -0
- package/src/tempo/Methods.ts +30 -22
- package/src/tempo/client/Charge.ts +20 -6
- package/src/tempo/internal/fee-payer.test.ts +122 -12
- package/src/tempo/internal/fee-payer.ts +49 -4
- package/src/tempo/server/Charge.test.ts +259 -1
- package/src/tempo/server/Charge.ts +31 -0
- package/src/tempo/server/Session.test.ts +130 -1
- package/src/tempo/server/Session.ts +41 -35
- package/src/tempo/server/internal/html/main.ts +2 -2
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/Chain.test.ts +225 -2
- package/src/tempo/session/Chain.ts +250 -65
- package/src/tempo/session/ChannelStore.test.ts +23 -0
- package/src/tempo/session/ChannelStore.ts +46 -13
|
@@ -15,7 +15,7 @@ import { Abis, Account, Actions, Addresses, Secp256k1, Tick, Transaction } from
|
|
|
15
15
|
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
16
16
|
import * as Http from '~test/Http.js'
|
|
17
17
|
import { closeChannelOnChain, deployEscrow, openChannel } from '~test/tempo/session.js'
|
|
18
|
-
import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
|
|
18
|
+
import { accounts, asset, chain, client, fundAccount, http } from '~test/tempo/viem.js'
|
|
19
19
|
|
|
20
20
|
import * as Store from '../../Store.js'
|
|
21
21
|
import * as Attribution from '../Attribution.js'
|
|
@@ -124,6 +124,166 @@ describe('tempo', () => {
|
|
|
124
124
|
httpServer.close()
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
+
test('behavior: client rejects unsupported explicit push mode', async () => {
|
|
128
|
+
const mppx = Mppx_client.create({
|
|
129
|
+
polyfill: false,
|
|
130
|
+
methods: [
|
|
131
|
+
tempo_client({
|
|
132
|
+
account: accounts[1],
|
|
133
|
+
mode: 'push',
|
|
134
|
+
getClient: () => client,
|
|
135
|
+
}),
|
|
136
|
+
],
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
140
|
+
const result = await Mppx_server.toNodeListener(
|
|
141
|
+
server.charge({ amount: '1', decimals: 6, supportedModes: ['pull'] }),
|
|
142
|
+
)(req, res)
|
|
143
|
+
if (result.status === 402) return
|
|
144
|
+
res.end('OK')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const response = await fetch(httpServer.url)
|
|
148
|
+
expect(response.status).toBe(402)
|
|
149
|
+
|
|
150
|
+
await expect(mppx.createCredential(response)).rejects.toThrow(
|
|
151
|
+
'Challenge does not support push mode.',
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
httpServer.close()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('behavior: falls back to pull when push is not advertised', async () => {
|
|
158
|
+
const jsonRpcClient = createClient({
|
|
159
|
+
account: accounts[0].address,
|
|
160
|
+
chain,
|
|
161
|
+
transport: http(),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const mppx = Mppx_client.create({
|
|
165
|
+
polyfill: false,
|
|
166
|
+
methods: [
|
|
167
|
+
tempo_client({
|
|
168
|
+
getClient: () => jsonRpcClient,
|
|
169
|
+
}),
|
|
170
|
+
],
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
174
|
+
const result = await Mppx_server.toNodeListener(
|
|
175
|
+
server.charge({
|
|
176
|
+
amount: '1',
|
|
177
|
+
decimals: 6,
|
|
178
|
+
recipient: accounts[2].address,
|
|
179
|
+
supportedModes: ['pull'],
|
|
180
|
+
}),
|
|
181
|
+
)(req, res)
|
|
182
|
+
if (result.status === 402) return
|
|
183
|
+
res.end('OK')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const response = await fetch(httpServer.url)
|
|
187
|
+
expect(response.status).toBe(402)
|
|
188
|
+
|
|
189
|
+
const credential = Credential.deserialize<{ type: 'hash' | 'proof' | 'transaction' }>(
|
|
190
|
+
await mppx.createCredential(response),
|
|
191
|
+
)
|
|
192
|
+
expect(credential.payload.type).toBe('transaction')
|
|
193
|
+
|
|
194
|
+
const authResponse = await fetch(httpServer.url, {
|
|
195
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
196
|
+
})
|
|
197
|
+
expect(authResponse.status).toBe(200)
|
|
198
|
+
|
|
199
|
+
httpServer.close()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('behavior: rejects hash credential when challenge supports only pull', async () => {
|
|
203
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
204
|
+
const result = await Mppx_server.toNodeListener(
|
|
205
|
+
server.charge({ amount: '1', decimals: 6, supportedModes: ['pull'] }),
|
|
206
|
+
)(req, res)
|
|
207
|
+
if (result.status === 402) return
|
|
208
|
+
res.end('OK')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const response = await fetch(httpServer.url)
|
|
212
|
+
expect(response.status).toBe(402)
|
|
213
|
+
|
|
214
|
+
const challenge = Challenge.fromResponse(response, {
|
|
215
|
+
methods: [tempo_client.charge()],
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const { receipt } = await Actions.token.transferSync(client, {
|
|
219
|
+
account: accounts[1],
|
|
220
|
+
amount: BigInt(challenge.request.amount),
|
|
221
|
+
to: challenge.request.recipient as Hex.Hex,
|
|
222
|
+
token: challenge.request.currency as Hex.Hex,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const credential = Credential.from({
|
|
226
|
+
challenge,
|
|
227
|
+
payload: { hash: receipt.transactionHash, type: 'hash' as const },
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const rejected = await fetch(httpServer.url, {
|
|
231
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
232
|
+
})
|
|
233
|
+
expect(rejected.status).toBe(402)
|
|
234
|
+
|
|
235
|
+
const body = (await rejected.json()) as { detail: string }
|
|
236
|
+
expect(body.detail).toContain('Hash credentials are not supported for this challenge.')
|
|
237
|
+
|
|
238
|
+
httpServer.close()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('behavior: rejects transaction credential when challenge supports only push', async () => {
|
|
242
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
243
|
+
const result = await Mppx_server.toNodeListener(
|
|
244
|
+
server.charge({ amount: '1', decimals: 6, supportedModes: ['push'] }),
|
|
245
|
+
)(req, res)
|
|
246
|
+
if (result.status === 402) return
|
|
247
|
+
res.end('OK')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const response = await fetch(httpServer.url)
|
|
251
|
+
expect(response.status).toBe(402)
|
|
252
|
+
|
|
253
|
+
const challenge = Challenge.fromResponse(response, {
|
|
254
|
+
methods: [tempo_client.charge()],
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const prepared = await prepareTransactionRequest(client, {
|
|
258
|
+
account: accounts[1]!,
|
|
259
|
+
calls: [
|
|
260
|
+
Actions.token.transfer.call({
|
|
261
|
+
amount: BigInt(challenge.request.amount),
|
|
262
|
+
to: challenge.request.recipient as Hex.Hex,
|
|
263
|
+
token: challenge.request.currency as Hex.Hex,
|
|
264
|
+
}),
|
|
265
|
+
],
|
|
266
|
+
nonceKey: 'expiring',
|
|
267
|
+
} as never)
|
|
268
|
+
prepared.gas = prepared.gas! + 5_000n
|
|
269
|
+
const signature = await signTransaction(client, prepared as never)
|
|
270
|
+
|
|
271
|
+
const credential = Credential.from({
|
|
272
|
+
challenge,
|
|
273
|
+
payload: { signature, type: 'transaction' as const },
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const rejected = await fetch(httpServer.url, {
|
|
277
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
278
|
+
})
|
|
279
|
+
expect(rejected.status).toBe(402)
|
|
280
|
+
|
|
281
|
+
const body = (await rejected.json()) as { detail: string }
|
|
282
|
+
expect(body.detail).toContain('Transaction credentials are not supported for this challenge.')
|
|
283
|
+
|
|
284
|
+
httpServer.close()
|
|
285
|
+
})
|
|
286
|
+
|
|
127
287
|
test('behavior: rejects replayed transaction hash', async () => {
|
|
128
288
|
const dedupServer = Mppx_server.create({
|
|
129
289
|
methods: [
|
|
@@ -1344,6 +1504,79 @@ describe('tempo', () => {
|
|
|
1344
1504
|
httpServer.close()
|
|
1345
1505
|
})
|
|
1346
1506
|
|
|
1507
|
+
test('behavior: fee payer simulates before broadcasting in confirmation mode', async () => {
|
|
1508
|
+
const rpcMethods: string[] = []
|
|
1509
|
+
const interceptingClient = createClient({
|
|
1510
|
+
account: accounts[0],
|
|
1511
|
+
chain: client.chain,
|
|
1512
|
+
transport: custom({
|
|
1513
|
+
async request(args: any) {
|
|
1514
|
+
rpcMethods.push(args.method)
|
|
1515
|
+
return client.transport.request(args)
|
|
1516
|
+
},
|
|
1517
|
+
}),
|
|
1518
|
+
})
|
|
1519
|
+
|
|
1520
|
+
const serverWithRpcTrace = Mppx_server.create({
|
|
1521
|
+
methods: [
|
|
1522
|
+
tempo_server.charge({
|
|
1523
|
+
getClient() {
|
|
1524
|
+
return interceptingClient
|
|
1525
|
+
},
|
|
1526
|
+
currency: asset,
|
|
1527
|
+
account: accounts[0],
|
|
1528
|
+
}),
|
|
1529
|
+
],
|
|
1530
|
+
realm,
|
|
1531
|
+
secretKey,
|
|
1532
|
+
})
|
|
1533
|
+
|
|
1534
|
+
const mppx = Mppx_client.create({
|
|
1535
|
+
polyfill: false,
|
|
1536
|
+
methods: [
|
|
1537
|
+
tempo_client({
|
|
1538
|
+
account: accounts[1],
|
|
1539
|
+
getClient() {
|
|
1540
|
+
return client
|
|
1541
|
+
},
|
|
1542
|
+
}),
|
|
1543
|
+
],
|
|
1544
|
+
})
|
|
1545
|
+
|
|
1546
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
1547
|
+
const result = await Mppx_server.toNodeListener(
|
|
1548
|
+
serverWithRpcTrace.charge({
|
|
1549
|
+
feePayer: accounts[0],
|
|
1550
|
+
amount: '1',
|
|
1551
|
+
currency: asset,
|
|
1552
|
+
recipient: accounts[0].address,
|
|
1553
|
+
}),
|
|
1554
|
+
)(req, res)
|
|
1555
|
+
if (result.status === 402) return
|
|
1556
|
+
res.end('OK')
|
|
1557
|
+
})
|
|
1558
|
+
|
|
1559
|
+
const challengeResponse = await fetch(httpServer.url)
|
|
1560
|
+
expect(challengeResponse.status).toBe(402)
|
|
1561
|
+
|
|
1562
|
+
const credential = await mppx.createCredential(challengeResponse)
|
|
1563
|
+
rpcMethods.length = 0
|
|
1564
|
+
|
|
1565
|
+
const authResponse = await fetch(httpServer.url, {
|
|
1566
|
+
headers: { Authorization: credential },
|
|
1567
|
+
})
|
|
1568
|
+
expect(authResponse.status).toBe(200)
|
|
1569
|
+
|
|
1570
|
+
const broadcastIndex = rpcMethods.indexOf('eth_sendRawTransactionSync')
|
|
1571
|
+
const simulationIndex = rpcMethods.indexOf('eth_call')
|
|
1572
|
+
|
|
1573
|
+
expect(broadcastIndex).toBeGreaterThan(-1)
|
|
1574
|
+
expect(simulationIndex).toBeGreaterThan(-1)
|
|
1575
|
+
expect(simulationIndex).toBeLessThan(broadcastIndex)
|
|
1576
|
+
|
|
1577
|
+
httpServer.close()
|
|
1578
|
+
})
|
|
1579
|
+
|
|
1347
1580
|
test('behavior: fee payer with splits', async () => {
|
|
1348
1581
|
const mppx = Mppx_client.create({
|
|
1349
1582
|
polyfill: false,
|
|
@@ -3116,6 +3349,31 @@ describe('tempo', () => {
|
|
|
3116
3349
|
})
|
|
3117
3350
|
expect(challenge.request.currency).toBe(asset)
|
|
3118
3351
|
})
|
|
3352
|
+
|
|
3353
|
+
test('challenge contains supportedModes when configured', async () => {
|
|
3354
|
+
const handler = Mppx_server.create({
|
|
3355
|
+
methods: [
|
|
3356
|
+
tempo_server.charge({
|
|
3357
|
+
getClient: () => client,
|
|
3358
|
+
account: accounts[0].address,
|
|
3359
|
+
currency: asset,
|
|
3360
|
+
}),
|
|
3361
|
+
],
|
|
3362
|
+
realm,
|
|
3363
|
+
secretKey,
|
|
3364
|
+
})
|
|
3365
|
+
|
|
3366
|
+
const result = await handler.charge({ amount: '1', supportedModes: ['pull'] })(
|
|
3367
|
+
new Request('https://example.com'),
|
|
3368
|
+
)
|
|
3369
|
+
expect(result.status).toBe(402)
|
|
3370
|
+
if (result.status !== 402) throw new Error()
|
|
3371
|
+
|
|
3372
|
+
const challenge = Challenge.fromResponse(result.challenge, {
|
|
3373
|
+
methods: [tempo_client.charge()],
|
|
3374
|
+
})
|
|
3375
|
+
expect(challenge.request.methodDetails?.supportedModes).toEqual(['pull'])
|
|
3376
|
+
})
|
|
3119
3377
|
})
|
|
3120
3378
|
|
|
3121
3379
|
describe('attribution memo', () => {
|
|
@@ -60,6 +60,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
60
60
|
decimals = defaults.decimals,
|
|
61
61
|
description,
|
|
62
62
|
externalId,
|
|
63
|
+
feePayerPolicy,
|
|
63
64
|
html,
|
|
64
65
|
memo,
|
|
65
66
|
waitForConfirmation = true,
|
|
@@ -162,6 +163,9 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
162
163
|
|
|
163
164
|
const { amount, methodDetails } = resolvedRequest
|
|
164
165
|
const expires = challenge.expires
|
|
166
|
+
const supportedModes = methodDetails?.supportedModes as
|
|
167
|
+
| readonly Methods.ChargeMode[]
|
|
168
|
+
| undefined
|
|
165
169
|
|
|
166
170
|
const currency = resolvedRequest.currency as `0x${string}`
|
|
167
171
|
const recipient = resolvedRequest.recipient as `0x${string}`
|
|
@@ -178,6 +182,9 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
178
182
|
|
|
179
183
|
switch (payload.type) {
|
|
180
184
|
case 'hash': {
|
|
185
|
+
if (supportedModes && !supportedModes.includes('push'))
|
|
186
|
+
throw new MismatchError('Hash credentials are not supported for this challenge.', {})
|
|
187
|
+
|
|
181
188
|
const hash = payload.hash as `0x${string}`
|
|
182
189
|
if (!(await markHashUsed(store, hash))) {
|
|
183
190
|
throw new VerificationFailedError({ reason: 'Transaction hash has already been used' })
|
|
@@ -258,6 +265,12 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
258
265
|
}
|
|
259
266
|
|
|
260
267
|
case 'transaction': {
|
|
268
|
+
if (supportedModes && !supportedModes.includes('pull'))
|
|
269
|
+
throw new MismatchError(
|
|
270
|
+
'Transaction credentials are not supported for this challenge.',
|
|
271
|
+
{},
|
|
272
|
+
)
|
|
273
|
+
|
|
261
274
|
const serializedTransaction = payload.signature as Transaction.TransactionSerializedTempo
|
|
262
275
|
|
|
263
276
|
// Pre-broadcast dedup: catch exact byte-for-byte replays early.
|
|
@@ -301,6 +314,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
301
314
|
chainId: chainId ?? client.chain!.id,
|
|
302
315
|
details: { amount, currency, recipient },
|
|
303
316
|
expectedFeeToken,
|
|
317
|
+
policy: feePayerPolicy,
|
|
304
318
|
transaction: {
|
|
305
319
|
...transaction,
|
|
306
320
|
...(resolvedFeeToken ? { feeToken: resolvedFeeToken } : {}),
|
|
@@ -312,6 +326,12 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
312
326
|
})()
|
|
313
327
|
|
|
314
328
|
if (waitForConfirmation) {
|
|
329
|
+
await viem_call(client, {
|
|
330
|
+
...transaction,
|
|
331
|
+
account: transaction.from,
|
|
332
|
+
feeToken: resolvedFeeToken,
|
|
333
|
+
calls: transaction.calls,
|
|
334
|
+
} as never)
|
|
315
335
|
const receipt = await sendRawTransactionSync(client, {
|
|
316
336
|
serializedTransaction: serializedTransaction_final,
|
|
317
337
|
})
|
|
@@ -385,6 +405,15 @@ export declare namespace charge {
|
|
|
385
405
|
type Parameters = {
|
|
386
406
|
/** Render payment page when Accept header is text/html (e.g. in browsers) */
|
|
387
407
|
html?: boolean | Html.Config | undefined
|
|
408
|
+
/**
|
|
409
|
+
* Override the fee-sponsor policy used when co-signing Tempo charge
|
|
410
|
+
* transactions. Defaults resolve per chain, including a higher
|
|
411
|
+
* priority-fee ceiling on Moderato.
|
|
412
|
+
*
|
|
413
|
+
* If you increase `maxGas` or `maxFeePerGas`, you may also need to raise
|
|
414
|
+
* `maxTotalFee` so the combined fee budget remains valid.
|
|
415
|
+
*/
|
|
416
|
+
feePayerPolicy?: FeePayerPolicy | undefined
|
|
388
417
|
/** Testnet mode. */
|
|
389
418
|
testnet?: boolean | undefined
|
|
390
419
|
/**
|
|
@@ -424,6 +453,8 @@ export declare namespace charge {
|
|
|
424
453
|
> & {
|
|
425
454
|
decimals: number
|
|
426
455
|
}
|
|
456
|
+
|
|
457
|
+
type FeePayerPolicy = Partial<FeePayer.Policy>
|
|
427
458
|
}
|
|
428
459
|
|
|
429
460
|
type ExpectedTransfer = {
|
|
@@ -149,6 +149,40 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
149
149
|
expect(ch!.highestVoucherAmount).toBe(1000000n)
|
|
150
150
|
})
|
|
151
151
|
|
|
152
|
+
test('fee-payer policy override is enforced for sponsored open', async () => {
|
|
153
|
+
const salt = nextSalt()
|
|
154
|
+
const { channelId, serializedTransaction } = await signOpenChannel({
|
|
155
|
+
escrow: escrowContract,
|
|
156
|
+
payer,
|
|
157
|
+
payee: recipient,
|
|
158
|
+
token: currency,
|
|
159
|
+
deposit: 10000000n,
|
|
160
|
+
salt,
|
|
161
|
+
feePayer: true,
|
|
162
|
+
})
|
|
163
|
+
const server = createServer({
|
|
164
|
+
feePayer: recipientAccount,
|
|
165
|
+
feePayerPolicy: { maxGas: 1n },
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await expect(
|
|
169
|
+
server.verify({
|
|
170
|
+
credential: {
|
|
171
|
+
challenge: makeChallenge({ channelId }),
|
|
172
|
+
payload: {
|
|
173
|
+
action: 'open' as const,
|
|
174
|
+
type: 'transaction' as const,
|
|
175
|
+
channelId,
|
|
176
|
+
transaction: serializedTransaction,
|
|
177
|
+
cumulativeAmount: '1000000',
|
|
178
|
+
signature: await signTestVoucher(channelId, 1000000n),
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
request: makeRequest({ feePayer: true }),
|
|
182
|
+
}),
|
|
183
|
+
).rejects.toThrow('gas exceeds sponsor policy')
|
|
184
|
+
})
|
|
185
|
+
|
|
152
186
|
test('rejects open when payee mismatch', async () => {
|
|
153
187
|
const wrongPayee = accounts[3].address
|
|
154
188
|
const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n, {
|
|
@@ -299,6 +333,66 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
299
333
|
expect(ch!.highestVoucherAmount).toBe(1000000n)
|
|
300
334
|
})
|
|
301
335
|
|
|
336
|
+
test('reopen with a case-variant channelId does not reset available balance', async () => {
|
|
337
|
+
let open:
|
|
338
|
+
| {
|
|
339
|
+
channelId: Hex
|
|
340
|
+
serializedTransaction: Hex
|
|
341
|
+
}
|
|
342
|
+
| undefined
|
|
343
|
+
for (let i = 0; i < 10; i++) {
|
|
344
|
+
const candidate = await createSignedOpenTransaction(10000000n)
|
|
345
|
+
if (/[a-f]/.test(candidate.channelId)) {
|
|
346
|
+
open = candidate
|
|
347
|
+
break
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (!open) throw new Error('failed to generate channelId with alphabetic hex characters')
|
|
351
|
+
|
|
352
|
+
const { channelId, serializedTransaction } = open
|
|
353
|
+
const caseVariantChannelId = channelId.replace(/[a-f]/, (character) =>
|
|
354
|
+
character.toUpperCase(),
|
|
355
|
+
) as Hex
|
|
356
|
+
const server = createServer()
|
|
357
|
+
|
|
358
|
+
await server.verify({
|
|
359
|
+
credential: {
|
|
360
|
+
challenge: makeChallenge({ id: 'open-1', channelId }),
|
|
361
|
+
payload: {
|
|
362
|
+
action: 'open' as const,
|
|
363
|
+
type: 'transaction' as const,
|
|
364
|
+
channelId,
|
|
365
|
+
transaction: serializedTransaction,
|
|
366
|
+
cumulativeAmount: '5000000',
|
|
367
|
+
signature: await signTestVoucher(channelId, 5000000n),
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
request: makeRequest(),
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
await charge(store, channelId, 4000000n)
|
|
374
|
+
|
|
375
|
+
const reopenReceipt = (await server.verify({
|
|
376
|
+
credential: {
|
|
377
|
+
challenge: makeChallenge({ id: 'open-2', channelId: caseVariantChannelId }),
|
|
378
|
+
payload: {
|
|
379
|
+
action: 'open' as const,
|
|
380
|
+
type: 'transaction' as const,
|
|
381
|
+
channelId: caseVariantChannelId,
|
|
382
|
+
transaction: serializedTransaction,
|
|
383
|
+
cumulativeAmount: '5000000',
|
|
384
|
+
signature: await signTestVoucher(caseVariantChannelId, 5000000n),
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
request: makeRequest(),
|
|
388
|
+
})) as SessionReceipt
|
|
389
|
+
|
|
390
|
+
expect(reopenReceipt.spent).toBe('4000000')
|
|
391
|
+
await expect(charge(store, caseVariantChannelId, 2000000n)).rejects.toThrow(
|
|
392
|
+
'requested 2000000, available 1000000',
|
|
393
|
+
)
|
|
394
|
+
})
|
|
395
|
+
|
|
302
396
|
test('rejects voucher below settledOnChain', async () => {
|
|
303
397
|
const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
|
|
304
398
|
const server = createServer()
|
|
@@ -1036,6 +1130,40 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
1036
1130
|
expect(ch!.deposit).toBe(20000000n)
|
|
1037
1131
|
})
|
|
1038
1132
|
|
|
1133
|
+
test('fee-payer policy override is enforced for sponsored topUp', async () => {
|
|
1134
|
+
const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
|
|
1135
|
+
const server = createServer({
|
|
1136
|
+
feePayer: recipientAccount,
|
|
1137
|
+
feePayerPolicy: { maxGas: 1n },
|
|
1138
|
+
})
|
|
1139
|
+
await openServerChannel(server, channelId, serializedTransaction)
|
|
1140
|
+
|
|
1141
|
+
const { serializedTransaction: topUpTx } = await signTopUpChannel({
|
|
1142
|
+
escrow: escrowContract,
|
|
1143
|
+
payer,
|
|
1144
|
+
channelId,
|
|
1145
|
+
token: currency,
|
|
1146
|
+
amount: 10000000n,
|
|
1147
|
+
feePayer: true,
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1150
|
+
await expect(
|
|
1151
|
+
server.verify({
|
|
1152
|
+
credential: {
|
|
1153
|
+
challenge: makeChallenge({ id: 'challenge-topup-policy', channelId }),
|
|
1154
|
+
payload: {
|
|
1155
|
+
action: 'topUp' as const,
|
|
1156
|
+
type: 'transaction' as const,
|
|
1157
|
+
channelId,
|
|
1158
|
+
transaction: topUpTx,
|
|
1159
|
+
additionalDeposit: '10000000',
|
|
1160
|
+
},
|
|
1161
|
+
},
|
|
1162
|
+
request: makeRequest({ feePayer: true }),
|
|
1163
|
+
}),
|
|
1164
|
+
).rejects.toThrow('gas exceeds sponsor policy')
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1039
1167
|
test('topUp receipt preserves spent and units from prior charges', async () => {
|
|
1040
1168
|
const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
|
|
1041
1169
|
const server = createServer()
|
|
@@ -4514,7 +4642,7 @@ function makeChallenge(opts: { id?: string; channelId: Hex }) {
|
|
|
4514
4642
|
} as Challenge.Challenge<z.output<typeof Methods.session.schema.request>, 'session', 'tempo'>
|
|
4515
4643
|
}
|
|
4516
4644
|
|
|
4517
|
-
function makeRequest() {
|
|
4645
|
+
function makeRequest(overrides: Partial<Record<string, unknown>> = {}) {
|
|
4518
4646
|
return {
|
|
4519
4647
|
amount: '1000000',
|
|
4520
4648
|
unitType: 'token',
|
|
@@ -4523,6 +4651,7 @@ function makeRequest() {
|
|
|
4523
4651
|
recipient: recipient as string,
|
|
4524
4652
|
escrowContract: escrowContract as string,
|
|
4525
4653
|
chainId: chain.id,
|
|
4654
|
+
...overrides,
|
|
4526
4655
|
}
|
|
4527
4656
|
}
|
|
4528
4657
|
|