mppx 0.5.13 → 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 +8 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +7 -2
- 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 +6 -0
- package/dist/tempo/server/Charge.js.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/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/tempo/internal/fee-payer.test.ts +48 -12
- package/src/tempo/internal/fee-payer.ts +13 -2
- package/src/tempo/server/Charge.test.ts +73 -0
- package/src/tempo/server/Charge.ts +6 -0
- package/src/tempo/server/Session.test.ts +130 -1
- package/src/tempo/server/Session.ts +41 -35
- 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
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type Client,
|
|
5
5
|
decodeFunctionData,
|
|
6
6
|
encodeFunctionData,
|
|
7
|
+
erc20Abi,
|
|
7
8
|
getAbiItem,
|
|
8
9
|
type Hex,
|
|
9
10
|
type ReadContractReturnType,
|
|
@@ -23,7 +24,7 @@ import { Transaction } from 'viem/tempo'
|
|
|
23
24
|
import { BadRequestError, ChannelClosedError, VerificationFailedError } from '../../Errors.js'
|
|
24
25
|
import * as TempoAddress from '../internal/address.js'
|
|
25
26
|
import * as defaults from '../internal/defaults.js'
|
|
26
|
-
import
|
|
27
|
+
import * as FeePayer from '../internal/fee-payer.js'
|
|
27
28
|
import * as Channel from './Channel.js'
|
|
28
29
|
import { escrowAbi } from './escrow.abi.js'
|
|
29
30
|
import type { SignedVoucher } from './Types.js'
|
|
@@ -230,6 +231,159 @@ export type BroadcastResult = {
|
|
|
230
231
|
onChain: OnChainChannel
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
type TempoCall = NonNullable<ReturnType<(typeof Transaction)['deserialize']>['calls']>[number]
|
|
235
|
+
|
|
236
|
+
function assertCallHasTargetAndData(call: TempoCall): { to: Address; data: Hex } {
|
|
237
|
+
if (!call.to || !call.data) {
|
|
238
|
+
throw new BadRequestError({
|
|
239
|
+
reason: 'fee-sponsored transactions must not contain calls without target or data',
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
return { to: call.to, data: call.data }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function validateSponsoredApproveCall(parameters: {
|
|
246
|
+
action: 'open' | 'topUp'
|
|
247
|
+
call: TempoCall
|
|
248
|
+
currency: Address
|
|
249
|
+
escrowContract: Address
|
|
250
|
+
expectedAmount: bigint
|
|
251
|
+
}) {
|
|
252
|
+
const { action, call, currency, escrowContract, expectedAmount } = parameters
|
|
253
|
+
const { to, data } = assertCallHasTargetAndData(call)
|
|
254
|
+
|
|
255
|
+
if (!TempoAddress.isEqual(to, currency) || data.slice(0, 10) !== erc20ApproveSelector) {
|
|
256
|
+
throw new BadRequestError({
|
|
257
|
+
reason: `fee-sponsored ${action} transaction contains an unauthorized call`,
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const { args } = decodeFunctionData({ abi: erc20Abi, data })
|
|
262
|
+
const [spender, amount] = args as readonly [Address, bigint]
|
|
263
|
+
|
|
264
|
+
if (!TempoAddress.isEqual(spender, escrowContract)) {
|
|
265
|
+
throw new BadRequestError({
|
|
266
|
+
reason: `fee-sponsored ${action} transaction approve spender does not match escrow contract`,
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (amount !== expectedAmount) {
|
|
271
|
+
throw new BadRequestError({
|
|
272
|
+
reason: `fee-sponsored ${action} transaction approve amount does not match requested amount`,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function validateSponsoredOpenCalls(parameters: {
|
|
278
|
+
calls: readonly TempoCall[]
|
|
279
|
+
currency: Address
|
|
280
|
+
escrowContract: Address
|
|
281
|
+
deposit: bigint
|
|
282
|
+
}) {
|
|
283
|
+
const { calls, currency, escrowContract, deposit } = parameters
|
|
284
|
+
|
|
285
|
+
let openCall: TempoCall | undefined
|
|
286
|
+
let approveCall: TempoCall | undefined
|
|
287
|
+
|
|
288
|
+
for (const call of calls) {
|
|
289
|
+
const { to, data } = assertCallHasTargetAndData(call)
|
|
290
|
+
const selector = data.slice(0, 10)
|
|
291
|
+
const isOpen = TempoAddress.isEqual(to, escrowContract) && selector === escrowOpenSelector
|
|
292
|
+
const isApprove = TempoAddress.isEqual(to, currency) && selector === erc20ApproveSelector
|
|
293
|
+
|
|
294
|
+
if (isApprove) {
|
|
295
|
+
if (approveCall || openCall) {
|
|
296
|
+
throw new BadRequestError({
|
|
297
|
+
reason: 'fee-sponsored open transaction contains a smuggled call',
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
approveCall = call
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (isOpen) {
|
|
305
|
+
if (openCall) {
|
|
306
|
+
throw new BadRequestError({
|
|
307
|
+
reason: 'fee-sponsored open transaction contains a smuggled call',
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
openCall = call
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
throw new BadRequestError({
|
|
315
|
+
reason: 'fee-sponsored open transaction contains an unauthorized call',
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (approveCall) {
|
|
320
|
+
validateSponsoredApproveCall({
|
|
321
|
+
action: 'open',
|
|
322
|
+
call: approveCall,
|
|
323
|
+
currency,
|
|
324
|
+
escrowContract,
|
|
325
|
+
expectedAmount: deposit,
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return openCall
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function validateSponsoredTopUpCalls(parameters: {
|
|
333
|
+
calls: readonly TempoCall[]
|
|
334
|
+
currency: Address
|
|
335
|
+
escrowContract: Address
|
|
336
|
+
topUpAmount: bigint
|
|
337
|
+
}) {
|
|
338
|
+
const { calls, currency, escrowContract, topUpAmount } = parameters
|
|
339
|
+
|
|
340
|
+
let topUpCall: TempoCall | undefined
|
|
341
|
+
let approveCall: TempoCall | undefined
|
|
342
|
+
|
|
343
|
+
for (const call of calls) {
|
|
344
|
+
const { to, data } = assertCallHasTargetAndData(call)
|
|
345
|
+
const selector = data.slice(0, 10)
|
|
346
|
+
const isTopUp = TempoAddress.isEqual(to, escrowContract) && selector === escrowTopUpSelector
|
|
347
|
+
const isApprove = TempoAddress.isEqual(to, currency) && selector === erc20ApproveSelector
|
|
348
|
+
|
|
349
|
+
if (isApprove) {
|
|
350
|
+
if (approveCall || topUpCall) {
|
|
351
|
+
throw new BadRequestError({
|
|
352
|
+
reason: 'fee-sponsored topUp transaction contains a smuggled call',
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
approveCall = call
|
|
356
|
+
continue
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (isTopUp) {
|
|
360
|
+
if (topUpCall) {
|
|
361
|
+
throw new BadRequestError({
|
|
362
|
+
reason: 'fee-sponsored topUp transaction contains a smuggled call',
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
topUpCall = call
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
throw new BadRequestError({
|
|
370
|
+
reason: 'fee-sponsored topUp transaction contains an unauthorized call',
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (approveCall) {
|
|
375
|
+
validateSponsoredApproveCall({
|
|
376
|
+
action: 'topUp',
|
|
377
|
+
call: approveCall,
|
|
378
|
+
currency,
|
|
379
|
+
escrowContract,
|
|
380
|
+
expectedAmount: topUpAmount,
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return topUpCall
|
|
385
|
+
}
|
|
386
|
+
|
|
233
387
|
export async function broadcastOpenTransaction(parameters: {
|
|
234
388
|
client: Client
|
|
235
389
|
serializedTransaction: Hex
|
|
@@ -237,6 +391,8 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
237
391
|
channelId: Hex
|
|
238
392
|
recipient: Address
|
|
239
393
|
currency: Address
|
|
394
|
+
challengeExpires?: string | undefined
|
|
395
|
+
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
240
396
|
feePayer?: Account | undefined
|
|
241
397
|
/** When false, simulates instead of waiting for confirmation and returns derived on-chain state. @default true */
|
|
242
398
|
waitForConfirmation?: boolean | undefined
|
|
@@ -248,11 +404,13 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
248
404
|
channelId,
|
|
249
405
|
recipient,
|
|
250
406
|
currency,
|
|
407
|
+
challengeExpires,
|
|
408
|
+
feePayerPolicy,
|
|
251
409
|
feePayer,
|
|
252
410
|
waitForConfirmation = true,
|
|
253
411
|
} = parameters
|
|
254
412
|
|
|
255
|
-
if (feePayer && !isTempoTransaction(serializedTransaction))
|
|
413
|
+
if (feePayer && !FeePayer.isTempoTransaction(serializedTransaction))
|
|
256
414
|
throw new BadRequestError({
|
|
257
415
|
reason: 'Only Tempo (0x76/0x78) transactions are supported',
|
|
258
416
|
})
|
|
@@ -265,37 +423,40 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
265
423
|
|
|
266
424
|
const calls = transaction.calls ?? []
|
|
267
425
|
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
426
|
+
const sponsoredOpenCall = feePayer
|
|
427
|
+
? validateSponsoredOpenCalls({
|
|
428
|
+
calls,
|
|
429
|
+
currency,
|
|
430
|
+
escrowContract,
|
|
431
|
+
deposit: (() => {
|
|
432
|
+
const candidate = calls.find((call) => {
|
|
433
|
+
if (!call.to || !TempoAddress.isEqual(call.to, escrowContract)) return false
|
|
434
|
+
if (!call.data) return false
|
|
435
|
+
return call.data.slice(0, 10) === escrowOpenSelector
|
|
436
|
+
})
|
|
437
|
+
if (!candidate?.data)
|
|
438
|
+
throw new BadRequestError({
|
|
439
|
+
reason: 'transaction does not contain a valid escrow open call',
|
|
440
|
+
})
|
|
441
|
+
const { args } = decodeFunctionData({ abi: escrowAbi, data: candidate.data })
|
|
442
|
+
return (args as readonly [Address, Address, bigint, Hex, Address])[2]
|
|
443
|
+
})(),
|
|
444
|
+
})
|
|
445
|
+
: undefined
|
|
446
|
+
|
|
447
|
+
const openCall =
|
|
448
|
+
sponsoredOpenCall ??
|
|
449
|
+
calls.find((call) => {
|
|
450
|
+
if (!call.to || !TempoAddress.isEqual(call.to, escrowContract)) return false
|
|
451
|
+
if (!call.data) return false
|
|
452
|
+
return call.data.slice(0, 10) === escrowOpenSelector
|
|
453
|
+
})
|
|
273
454
|
|
|
274
455
|
if (!openCall)
|
|
275
456
|
throw new BadRequestError({
|
|
276
457
|
reason: 'transaction does not contain a valid escrow open call',
|
|
277
458
|
})
|
|
278
459
|
|
|
279
|
-
if (feePayer) {
|
|
280
|
-
for (const call of calls) {
|
|
281
|
-
if (!call.to || !call.data) {
|
|
282
|
-
throw new BadRequestError({
|
|
283
|
-
reason: 'fee-sponsored transactions must not contain calls without target or data',
|
|
284
|
-
})
|
|
285
|
-
}
|
|
286
|
-
const selector = call.data.slice(0, 10)
|
|
287
|
-
const isEscrowOpen =
|
|
288
|
-
TempoAddress.isEqual(call.to, escrowContract) && selector === escrowOpenSelector
|
|
289
|
-
const isTokenApprove =
|
|
290
|
-
TempoAddress.isEqual(call.to, currency) && selector === erc20ApproveSelector
|
|
291
|
-
if (!isEscrowOpen && !isTokenApprove) {
|
|
292
|
-
throw new BadRequestError({
|
|
293
|
-
reason: 'fee-sponsored open transaction contains an unauthorized call',
|
|
294
|
-
})
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
460
|
const { args: openArgs } = decodeFunctionData({ abi: escrowAbi, data: openCall.data! })
|
|
300
461
|
const [payee, token, deposit, salt, authorizedSigner] = openArgs as readonly [
|
|
301
462
|
Address,
|
|
@@ -337,12 +498,24 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
337
498
|
|
|
338
499
|
const serializedTransaction_final = await (async () => {
|
|
339
500
|
if (feePayer) {
|
|
340
|
-
|
|
341
|
-
|
|
501
|
+
if (!sponsoredOpenCall)
|
|
502
|
+
throw new BadRequestError({
|
|
503
|
+
reason: 'transaction does not contain a valid escrow open call',
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
const sponsored = FeePayer.prepareSponsoredTransaction({
|
|
342
507
|
account: feePayer,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
508
|
+
challengeExpires,
|
|
509
|
+
chainId: client.chain!.id,
|
|
510
|
+
details: { channelId, currency, recipient },
|
|
511
|
+
expectedFeeToken: defaults.currency[client.chain?.id as keyof typeof defaults.currency],
|
|
512
|
+
policy: feePayerPolicy,
|
|
513
|
+
transaction: {
|
|
514
|
+
...transaction,
|
|
515
|
+
...(resolvedFeeToken ? { feeToken: resolvedFeeToken } : {}),
|
|
516
|
+
},
|
|
517
|
+
})
|
|
518
|
+
return signTransaction(client, sponsored as never)
|
|
346
519
|
}
|
|
347
520
|
return serializedTransaction
|
|
348
521
|
})()
|
|
@@ -407,6 +580,8 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
407
580
|
currency: Address
|
|
408
581
|
declaredDeposit: bigint
|
|
409
582
|
previousDeposit: bigint
|
|
583
|
+
challengeExpires?: string | undefined
|
|
584
|
+
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
410
585
|
feePayer?: Account | undefined
|
|
411
586
|
}): Promise<{ txHash: Hex; newDeposit: bigint }> {
|
|
412
587
|
const {
|
|
@@ -417,10 +592,12 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
417
592
|
currency,
|
|
418
593
|
declaredDeposit,
|
|
419
594
|
previousDeposit,
|
|
595
|
+
challengeExpires,
|
|
596
|
+
feePayerPolicy,
|
|
420
597
|
feePayer,
|
|
421
598
|
} = parameters
|
|
422
599
|
|
|
423
|
-
if (feePayer && !isTempoTransaction(serializedTransaction))
|
|
600
|
+
if (feePayer && !FeePayer.isTempoTransaction(serializedTransaction))
|
|
424
601
|
throw new BadRequestError({
|
|
425
602
|
reason: 'Only Tempo (0x76/0x78) transactions are supported',
|
|
426
603
|
})
|
|
@@ -433,37 +610,28 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
433
610
|
|
|
434
611
|
const calls = transaction.calls ?? []
|
|
435
612
|
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
613
|
+
const sponsoredTopUpCall = feePayer
|
|
614
|
+
? validateSponsoredTopUpCalls({
|
|
615
|
+
calls,
|
|
616
|
+
currency,
|
|
617
|
+
escrowContract,
|
|
618
|
+
topUpAmount: declaredDeposit,
|
|
619
|
+
})
|
|
620
|
+
: undefined
|
|
621
|
+
|
|
622
|
+
const topUpCall =
|
|
623
|
+
sponsoredTopUpCall ??
|
|
624
|
+
calls.find((call) => {
|
|
625
|
+
if (!call.to || !TempoAddress.isEqual(call.to, escrowContract)) return false
|
|
626
|
+
if (!call.data) return false
|
|
627
|
+
return call.data.slice(0, 10) === escrowTopUpSelector
|
|
628
|
+
})
|
|
441
629
|
|
|
442
630
|
if (!topUpCall)
|
|
443
631
|
throw new BadRequestError({
|
|
444
632
|
reason: 'transaction does not contain a valid escrow topUp call',
|
|
445
633
|
})
|
|
446
634
|
|
|
447
|
-
if (feePayer) {
|
|
448
|
-
for (const call of calls) {
|
|
449
|
-
if (!call.to || !call.data) {
|
|
450
|
-
throw new BadRequestError({
|
|
451
|
-
reason: 'fee-sponsored transactions must not contain calls without target or data',
|
|
452
|
-
})
|
|
453
|
-
}
|
|
454
|
-
const selector = call.data.slice(0, 10)
|
|
455
|
-
const isEscrowTopUp =
|
|
456
|
-
TempoAddress.isEqual(call.to, escrowContract) && selector === escrowTopUpSelector
|
|
457
|
-
const isTokenApprove =
|
|
458
|
-
TempoAddress.isEqual(call.to, currency) && selector === erc20ApproveSelector
|
|
459
|
-
if (!isEscrowTopUp && !isTokenApprove) {
|
|
460
|
-
throw new BadRequestError({
|
|
461
|
-
reason: 'fee-sponsored topUp transaction contains an unauthorized call',
|
|
462
|
-
})
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
635
|
const { args: topUpArgs } = decodeFunctionData({ abi: escrowAbi, data: topUpCall.data! })
|
|
468
636
|
const [txChannelId, txAmount] = topUpArgs as [Hex, bigint]
|
|
469
637
|
|
|
@@ -480,14 +648,31 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
480
648
|
|
|
481
649
|
const serializedTransaction_final = await (async () => {
|
|
482
650
|
if (feePayer) {
|
|
483
|
-
|
|
484
|
-
|
|
651
|
+
if (!sponsoredTopUpCall)
|
|
652
|
+
throw new BadRequestError({
|
|
653
|
+
reason: 'transaction does not contain a valid escrow topUp call',
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
const expectedFeeToken = defaults.currency[client.chain?.id as keyof typeof defaults.currency]
|
|
657
|
+
const sponsored = FeePayer.prepareSponsoredTransaction({
|
|
485
658
|
account: feePayer,
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
659
|
+
challengeExpires,
|
|
660
|
+
chainId: client.chain!.id,
|
|
661
|
+
details: {
|
|
662
|
+
additionalDeposit: declaredDeposit.toString(),
|
|
663
|
+
channelId,
|
|
664
|
+
currency,
|
|
665
|
+
},
|
|
666
|
+
expectedFeeToken,
|
|
667
|
+
policy: feePayerPolicy,
|
|
668
|
+
transaction: {
|
|
669
|
+
...transaction,
|
|
670
|
+
...((transaction.feeToken ?? expectedFeeToken)
|
|
671
|
+
? { feeToken: transaction.feeToken ?? expectedFeeToken }
|
|
672
|
+
: {}),
|
|
673
|
+
},
|
|
674
|
+
})
|
|
675
|
+
return signTransaction(client, sponsored as never)
|
|
491
676
|
}
|
|
492
677
|
return serializedTransaction
|
|
493
678
|
})()
|
|
@@ -7,6 +7,10 @@ import * as ChannelStore from './ChannelStore.js'
|
|
|
7
7
|
|
|
8
8
|
const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
9
9
|
const channelId2 = '0x0000000000000000000000000000000000000000000000000000000000000002' as Hex
|
|
10
|
+
const lowerCaseAliasChannelId = `0x${'ab'.repeat(31)}cd` as Hex
|
|
11
|
+
const mixedCaseAliasChannelId = lowerCaseAliasChannelId.replace(/[a-f]/g, (character, index) =>
|
|
12
|
+
index % 2 === 0 ? character.toUpperCase() : character,
|
|
13
|
+
) as Hex
|
|
10
14
|
|
|
11
15
|
function makeChannel(overrides?: Partial<ChannelStore.State>): ChannelStore.State {
|
|
12
16
|
return {
|
|
@@ -112,6 +116,25 @@ describe('channelStore', () => {
|
|
|
112
116
|
expect(typeof loaded!.deposit).toBe('bigint')
|
|
113
117
|
expect(typeof loaded!.createdAt).toBe('string')
|
|
114
118
|
})
|
|
119
|
+
|
|
120
|
+
test('treats case-variant channelIds as the same record', async () => {
|
|
121
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
122
|
+
await cs.updateChannel(mixedCaseAliasChannelId, () =>
|
|
123
|
+
makeChannel({ channelId: mixedCaseAliasChannelId }),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const loaded = await cs.getChannel(lowerCaseAliasChannelId)
|
|
127
|
+
expect(loaded).not.toBeNull()
|
|
128
|
+
expect(loaded!.channelId).toBe(lowerCaseAliasChannelId)
|
|
129
|
+
|
|
130
|
+
await cs.updateChannel(lowerCaseAliasChannelId, (current) =>
|
|
131
|
+
current ? { ...current, spent: 1_000_000n } : null,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const aliased = await cs.getChannel(mixedCaseAliasChannelId)
|
|
135
|
+
expect(aliased!.channelId).toBe(lowerCaseAliasChannelId)
|
|
136
|
+
expect(aliased!.spent).toBe(1_000_000n)
|
|
137
|
+
})
|
|
115
138
|
})
|
|
116
139
|
|
|
117
140
|
describe('updateChannel', () => {
|
|
@@ -109,6 +109,18 @@ export type ChannelStore = {
|
|
|
109
109
|
|
|
110
110
|
export type DeductResult = { ok: true; channel: State } | { ok: false; channel: State }
|
|
111
111
|
|
|
112
|
+
export function normalizeChannelId(channelId: Hex): Hex {
|
|
113
|
+
return channelId.toLowerCase() as Hex
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeState(channelId: Hex, state: State): State {
|
|
117
|
+
return state.channelId === channelId ? state : { ...state, channelId }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeMaybeState(channelId: Hex, state: State | null): State | null {
|
|
121
|
+
return state ? normalizeState(channelId, state) : null
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
/**
|
|
113
125
|
* Atomically deduct `amount` from a channel's available balance.
|
|
114
126
|
*
|
|
@@ -208,54 +220,75 @@ export function fromStore(store: Store.Store | Store.AtomicStore): ChannelStore
|
|
|
208
220
|
channelId: Hex,
|
|
209
221
|
fn: (current: State | null) => Store.Change<State, result>,
|
|
210
222
|
): Promise<result> {
|
|
223
|
+
const normalizedChannelId = normalizeChannelId(channelId)
|
|
211
224
|
let change: Store.Change<State, result> | undefined
|
|
212
225
|
|
|
213
226
|
if (atomicUpdate) {
|
|
214
|
-
const result = await atomicUpdate(
|
|
215
|
-
change = fn((current as State | null) ?? null)
|
|
227
|
+
const result = await atomicUpdate(normalizedChannelId, (current) => {
|
|
228
|
+
change = fn(normalizeMaybeState(normalizedChannelId, (current as State | null) ?? null))
|
|
229
|
+
if (change.op === 'set') {
|
|
230
|
+
change = {
|
|
231
|
+
...change,
|
|
232
|
+
value: normalizeState(normalizedChannelId, change.value),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
216
235
|
if (change.op !== 'set') return change
|
|
217
236
|
return { ...change, value: change.value as never }
|
|
218
237
|
})
|
|
219
|
-
if (change?.op !== 'noop') notify(
|
|
238
|
+
if (change?.op !== 'noop') notify(normalizedChannelId)
|
|
220
239
|
return result
|
|
221
240
|
}
|
|
222
241
|
|
|
223
|
-
while (locks.has(
|
|
242
|
+
while (locks.has(normalizedChannelId)) await locks.get(normalizedChannelId)
|
|
224
243
|
|
|
225
244
|
let release!: () => void
|
|
226
245
|
locks.set(
|
|
227
|
-
|
|
246
|
+
normalizedChannelId,
|
|
228
247
|
new Promise<void>((r) => {
|
|
229
248
|
release = r
|
|
230
249
|
}),
|
|
231
250
|
)
|
|
232
251
|
|
|
233
252
|
try {
|
|
234
|
-
const current = (
|
|
253
|
+
const current = normalizeMaybeState(
|
|
254
|
+
normalizedChannelId,
|
|
255
|
+
(await store.get(normalizedChannelId)) as State | null,
|
|
256
|
+
)
|
|
235
257
|
change = fn(current)
|
|
236
|
-
if (change.op === 'set')
|
|
237
|
-
|
|
238
|
-
|
|
258
|
+
if (change.op === 'set') {
|
|
259
|
+
change = {
|
|
260
|
+
...change,
|
|
261
|
+
value: normalizeState(normalizedChannelId, change.value),
|
|
262
|
+
}
|
|
263
|
+
await store.put(normalizedChannelId, change.value as never)
|
|
264
|
+
}
|
|
265
|
+
if (change.op === 'delete') await store.delete(normalizedChannelId)
|
|
266
|
+
if (change.op !== 'noop') notify(normalizedChannelId)
|
|
239
267
|
return change.result
|
|
240
268
|
} finally {
|
|
241
|
-
locks.delete(
|
|
269
|
+
locks.delete(normalizedChannelId)
|
|
242
270
|
release()
|
|
243
271
|
}
|
|
244
272
|
}
|
|
245
273
|
|
|
246
274
|
const cs: ChannelStore = {
|
|
247
275
|
async getChannel(channelId) {
|
|
248
|
-
|
|
276
|
+
const normalizedChannelId = normalizeChannelId(channelId)
|
|
277
|
+
return normalizeMaybeState(
|
|
278
|
+
normalizedChannelId,
|
|
279
|
+
(await store.get(normalizedChannelId)) as State | null,
|
|
280
|
+
)
|
|
249
281
|
},
|
|
250
282
|
async updateChannel(channelId, fn) {
|
|
251
283
|
return update(channelId, fn)
|
|
252
284
|
},
|
|
253
285
|
waitForUpdate(channelId) {
|
|
254
286
|
return new Promise<void>((resolve) => {
|
|
255
|
-
|
|
287
|
+
const normalizedChannelId = normalizeChannelId(channelId)
|
|
288
|
+
let set = waiters.get(normalizedChannelId)
|
|
256
289
|
if (!set) {
|
|
257
290
|
set = new Set()
|
|
258
|
-
waiters.set(
|
|
291
|
+
waiters.set(normalizedChannelId, set)
|
|
259
292
|
}
|
|
260
293
|
set.add(resolve)
|
|
261
294
|
})
|