mppx 0.6.27 → 0.6.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +16 -10
- package/dist/Challenge.js.map +1 -1
- package/dist/Method.d.ts +1 -1
- package/dist/Method.d.ts.map +1 -1
- package/dist/Store.d.ts +32 -9
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +42 -10
- package/dist/Store.js.map +1 -1
- package/dist/client/Methods.d.ts +1 -0
- package/dist/client/Methods.d.ts.map +1 -1
- package/dist/client/Methods.js +1 -0
- package/dist/client/Methods.js.map +1 -1
- package/dist/client/Mppx.d.ts +3 -3
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +1 -0
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/Transport.d.ts +10 -3
- package/dist/client/Transport.d.ts.map +1 -1
- package/dist/client/Transport.js +60 -7
- package/dist/client/Transport.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +3 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +12 -20
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/evm/Assets.d.ts +2 -0
- package/dist/evm/Assets.d.ts.map +1 -0
- package/dist/evm/Assets.js +2 -0
- package/dist/evm/Assets.js.map +1 -0
- package/dist/evm/Chains.d.ts +5 -0
- package/dist/evm/Chains.d.ts.map +1 -0
- package/dist/evm/Chains.js +5 -0
- package/dist/evm/Chains.js.map +1 -0
- package/dist/evm/Methods.d.ts +68 -0
- package/dist/evm/Methods.d.ts.map +1 -0
- package/dist/evm/Methods.js +28 -0
- package/dist/evm/Methods.js.map +1 -0
- package/dist/evm/Types.d.ts +143 -0
- package/dist/evm/Types.d.ts.map +1 -0
- package/dist/evm/Types.js +102 -0
- package/dist/evm/Types.js.map +1 -0
- package/dist/evm/client/Charge.d.ts +102 -0
- package/dist/evm/client/Charge.d.ts.map +1 -0
- package/dist/evm/client/Charge.js +141 -0
- package/dist/evm/client/Charge.js.map +1 -0
- package/dist/evm/client/Methods.d.ts +81 -0
- package/dist/evm/client/Methods.d.ts.map +1 -0
- package/dist/evm/client/Methods.js +16 -0
- package/dist/evm/client/Methods.js.map +1 -0
- package/dist/evm/client/index.d.ts +6 -0
- package/dist/evm/client/index.d.ts.map +1 -0
- package/dist/evm/client/index.js +6 -0
- package/dist/evm/client/index.js.map +1 -0
- package/dist/evm/index.d.ts +9 -0
- package/dist/evm/index.d.ts.map +1 -0
- package/dist/evm/index.js +8 -0
- package/dist/evm/index.js.map +1 -0
- package/dist/evm/server/Charge.d.ts +62 -0
- package/dist/evm/server/Charge.d.ts.map +1 -0
- package/dist/evm/server/Charge.js +172 -0
- package/dist/evm/server/Charge.js.map +1 -0
- package/dist/evm/server/Methods.d.ts +80 -0
- package/dist/evm/server/Methods.d.ts.map +1 -0
- package/dist/evm/server/Methods.js +16 -0
- package/dist/evm/server/Methods.js.map +1 -0
- package/dist/evm/server/index.d.ts +6 -0
- package/dist/evm/server/index.d.ts.map +1 -0
- package/dist/evm/server/index.js +6 -0
- package/dist/evm/server/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/HeaderCodec.d.ts +18 -0
- package/dist/internal/HeaderCodec.d.ts.map +1 -0
- package/dist/internal/HeaderCodec.js +31 -0
- package/dist/internal/HeaderCodec.js.map +1 -0
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +2 -3
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.js +2 -1
- package/dist/middlewares/express.js.map +1 -1
- package/dist/proxy/internal/Headers.d.ts +13 -1
- package/dist/proxy/internal/Headers.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.js +25 -2
- package/dist/proxy/internal/Headers.js.map +1 -1
- package/dist/proxy/services/openai.d.ts.map +1 -1
- package/dist/proxy/services/openai.js +2 -0
- package/dist/proxy/services/openai.js.map +1 -1
- package/dist/server/Methods.d.ts +1 -0
- package/dist/server/Methods.d.ts.map +1 -1
- package/dist/server/Methods.js +1 -0
- package/dist/server/Methods.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +90 -12
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts +10 -0
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +9 -0
- package/dist/server/Transport.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/stripe/server/Charge.d.ts +31 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +88 -11
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts +3 -3
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js +13 -6
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +8 -5
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +0 -1
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/client/Session.d.ts +2 -4
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js +10 -12
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts +3 -3
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +1 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/account.d.ts +5 -0
- package/dist/tempo/internal/account.d.ts.map +1 -1
- package/dist/tempo/internal/account.js +8 -0
- package/dist/tempo/internal/account.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +5 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +6 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +30 -3
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +6 -0
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +14 -13
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +6 -0
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js +1 -1
- package/dist/tempo/server/Subscription.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 +2 -0
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +8 -8
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts +4 -3
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js +71 -44
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/dist/tempo/session/Ws.d.ts.map +1 -1
- package/dist/tempo/session/Ws.js +15 -0
- package/dist/tempo/session/Ws.js.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +2 -2
- package/dist/x402/Assets.d.ts +29 -0
- package/dist/x402/Assets.d.ts.map +1 -0
- package/dist/x402/Assets.js +46 -0
- package/dist/x402/Assets.js.map +1 -0
- package/dist/x402/Header.d.ts +14 -0
- package/dist/x402/Header.d.ts.map +1 -0
- package/dist/x402/Header.js +18 -0
- package/dist/x402/Header.js.map +1 -0
- package/dist/x402/Types.d.ts +289 -0
- package/dist/x402/Types.d.ts.map +1 -0
- package/dist/x402/Types.js +139 -0
- package/dist/x402/Types.js.map +1 -0
- package/dist/x402/client/Exact.d.ts +38 -0
- package/dist/x402/client/Exact.d.ts.map +1 -0
- package/dist/x402/client/Exact.js +141 -0
- package/dist/x402/client/Exact.js.map +1 -0
- package/dist/x402/index.d.ts +6 -0
- package/dist/x402/index.d.ts.map +1 -0
- package/dist/x402/index.js +6 -0
- package/dist/x402/index.js.map +1 -0
- package/dist/x402/internal/ChallengeBrand.d.ts +5 -0
- package/dist/x402/internal/ChallengeBrand.d.ts.map +1 -0
- package/dist/x402/internal/ChallengeBrand.js +13 -0
- package/dist/x402/internal/ChallengeBrand.js.map +1 -0
- package/dist/x402/internal/RouteBinding.d.ts +8 -0
- package/dist/x402/internal/RouteBinding.d.ts.map +1 -0
- package/dist/x402/internal/RouteBinding.js +12 -0
- package/dist/x402/internal/RouteBinding.js.map +1 -0
- package/dist/x402/server/EvmCharge.d.ts +50 -0
- package/dist/x402/server/EvmCharge.d.ts.map +1 -0
- package/dist/x402/server/EvmCharge.js +301 -0
- package/dist/x402/server/EvmCharge.js.map +1 -0
- package/dist/x402/server/Facilitator.d.ts +12 -0
- package/dist/x402/server/Facilitator.d.ts.map +1 -0
- package/dist/x402/server/Facilitator.js +42 -0
- package/dist/x402/server/Facilitator.js.map +1 -0
- package/package.json +41 -21
- package/src/Challenge.test.ts +28 -0
- package/src/Challenge.ts +17 -10
- package/src/Method.ts +1 -1
- package/src/Store.test-d.ts +58 -0
- package/src/Store.test.ts +77 -0
- package/src/Store.ts +155 -74
- package/src/client/Methods.ts +1 -0
- package/src/client/Mppx.ts +4 -3
- package/src/client/Transport.test.ts +165 -30
- package/src/client/Transport.ts +76 -8
- package/src/client/index.ts +1 -1
- package/src/client/internal/Fetch.test.ts +31 -2
- package/src/client/internal/Fetch.ts +26 -19
- package/src/evm/Assets.ts +1 -0
- package/src/evm/Chains.ts +5 -0
- package/src/evm/Methods.ts +44 -0
- package/src/evm/PublicInterface.test-d.ts +109 -0
- package/src/evm/Types.ts +140 -0
- package/src/evm/client/Charge.test.ts +99 -0
- package/src/evm/client/Charge.ts +198 -0
- package/src/evm/client/Methods.ts +19 -0
- package/src/evm/client/index.ts +5 -0
- package/src/evm/index.ts +13 -0
- package/src/evm/server/Charge.test.ts +199 -0
- package/src/evm/server/Charge.ts +283 -0
- package/src/evm/server/Methods.ts +22 -0
- package/src/evm/server/index.ts +5 -0
- package/src/index.ts +2 -0
- package/src/internal/HeaderCodec.ts +36 -0
- package/src/middlewares/elysia.test.ts +25 -0
- package/src/middlewares/elysia.ts +1 -2
- package/src/middlewares/express.test.ts +28 -0
- package/src/middlewares/express.ts +1 -1
- package/src/middlewares/hono.test.ts +138 -2
- package/src/middlewares/nextjs.test.ts +22 -0
- package/src/proxy/internal/Headers.test.ts +38 -0
- package/src/proxy/internal/Headers.ts +26 -2
- package/src/proxy/services/openai.test.ts +57 -1
- package/src/proxy/services/openai.ts +2 -0
- package/src/server/Methods.ts +1 -0
- package/src/server/Mppx.test.ts +244 -1
- package/src/server/Mppx.ts +124 -11
- package/src/server/NodeListener.test.ts +28 -1
- package/src/server/Transport.test.ts +19 -0
- package/src/server/Transport.ts +20 -0
- package/src/server/index.ts +1 -1
- package/src/stripe/server/Charge.test.ts +215 -1
- package/src/stripe/server/Charge.ts +150 -20
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/AccessKeyAuthorization.test.ts +231 -0
- package/src/tempo/client/ChannelOps.test.ts +61 -7
- package/src/tempo/client/ChannelOps.ts +18 -7
- package/src/tempo/client/Charge.test.ts +126 -0
- package/src/tempo/client/Charge.ts +10 -6
- package/src/tempo/client/Session.test.ts +130 -1
- package/src/tempo/client/Session.ts +12 -19
- package/src/tempo/client/SessionManager.test.ts +69 -2
- package/src/tempo/client/SessionManager.ts +4 -4
- package/src/tempo/internal/account.ts +13 -0
- package/src/tempo/internal/fee-payer.test.ts +32 -2
- package/src/tempo/internal/fee-payer.ts +6 -2
- package/src/tempo/server/Charge.test.ts +91 -1
- package/src/tempo/server/Charge.ts +48 -2
- package/src/tempo/server/Session.test.ts +30 -0
- package/src/tempo/server/Session.ts +24 -17
- package/src/tempo/server/Subscription.ts +7 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/Chain.test.ts +4 -4
- package/src/tempo/session/Chain.ts +10 -6
- package/src/tempo/session/ChannelStore.test.ts +21 -0
- package/src/tempo/session/Voucher.test.ts +230 -1
- package/src/tempo/session/Voucher.ts +96 -48
- package/src/tempo/session/Ws.test.ts +71 -0
- package/src/tempo/session/Ws.ts +13 -0
- package/src/tempo/subscription/Store.test.ts +55 -0
- package/src/x402/Assets.ts +65 -0
- package/src/x402/Exact.e2e.test.ts +448 -0
- package/src/x402/Header.test.ts +73 -0
- package/src/x402/Header.ts +34 -0
- package/src/x402/PublicInterface.test-d.ts +39 -0
- package/src/x402/Types.ts +248 -0
- package/src/x402/client/Exact.test.ts +180 -0
- package/src/x402/client/Exact.ts +198 -0
- package/src/x402/index.ts +5 -0
- package/src/x402/internal/ChallengeBrand.ts +14 -0
- package/src/x402/internal/RouteBinding.ts +18 -0
- package/src/x402/server/EvmCharge.ts +394 -0
- package/src/x402/server/Facilitator.test.ts +111 -0
- package/src/x402/server/Facilitator.ts +56 -0
|
@@ -310,7 +310,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
310
310
|
expect(result.onChain.finalized).toBe(false)
|
|
311
311
|
})
|
|
312
312
|
|
|
313
|
-
test('fee-payer: rejects unauthorized calls', async () => {
|
|
313
|
+
test('fee-payer relay: rejects unauthorized open calls', async () => {
|
|
314
314
|
const salt = nextSalt()
|
|
315
315
|
const deposit = 5_000_000n
|
|
316
316
|
|
|
@@ -349,7 +349,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
349
349
|
channelId,
|
|
350
350
|
recipient,
|
|
351
351
|
currency,
|
|
352
|
-
|
|
352
|
+
isSponsored: true,
|
|
353
353
|
}),
|
|
354
354
|
).rejects.toThrow('fee-sponsored open transaction contains an unauthorized call')
|
|
355
355
|
})
|
|
@@ -798,7 +798,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
798
798
|
expect(result.newDeposit).toBe(deposit + topUpAmount)
|
|
799
799
|
})
|
|
800
800
|
|
|
801
|
-
test('fee-payer: rejects unauthorized calls', async () => {
|
|
801
|
+
test('fee-payer relay: rejects unauthorized topUp calls', async () => {
|
|
802
802
|
const salt = nextSalt()
|
|
803
803
|
const deposit = 5_000_000n
|
|
804
804
|
const topUpAmount = 3_000_000n
|
|
@@ -847,7 +847,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
847
847
|
currency: asset,
|
|
848
848
|
declaredDeposit: topUpAmount,
|
|
849
849
|
previousDeposit: deposit,
|
|
850
|
-
|
|
850
|
+
isSponsored: true,
|
|
851
851
|
}),
|
|
852
852
|
).rejects.toThrow('fee-sponsored topUp transaction contains an unauthorized call')
|
|
853
853
|
})
|
|
@@ -466,6 +466,7 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
466
466
|
challengeExpires?: string | undefined
|
|
467
467
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
468
468
|
feePayer?: Account | undefined
|
|
469
|
+
isSponsored?: boolean | undefined
|
|
469
470
|
beforeBroadcast?: ((onChain: OnChainChannel) => Promise<void> | void) | undefined
|
|
470
471
|
/** When false, simulates instead of waiting for confirmation and returns derived on-chain state. @default true */
|
|
471
472
|
waitForConfirmation?: boolean | undefined
|
|
@@ -480,11 +481,12 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
480
481
|
challengeExpires,
|
|
481
482
|
feePayerPolicy,
|
|
482
483
|
feePayer,
|
|
484
|
+
isSponsored = Boolean(feePayer),
|
|
483
485
|
beforeBroadcast,
|
|
484
486
|
waitForConfirmation = true,
|
|
485
487
|
} = parameters
|
|
486
488
|
|
|
487
|
-
if (
|
|
489
|
+
if (isSponsored && !FeePayer.isTempoTransaction(serializedTransaction))
|
|
488
490
|
throw new BadRequestError({
|
|
489
491
|
reason: 'Only Tempo (0x76/0x78) transactions are supported',
|
|
490
492
|
})
|
|
@@ -493,11 +495,11 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
493
495
|
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
494
496
|
)
|
|
495
497
|
|
|
496
|
-
if (
|
|
498
|
+
if (isSponsored) assertSenderSigned(transaction)
|
|
497
499
|
|
|
498
500
|
const calls = transaction.calls ?? []
|
|
499
501
|
|
|
500
|
-
const sponsoredOpenCall =
|
|
502
|
+
const sponsoredOpenCall = isSponsored
|
|
501
503
|
? validateSponsoredOpenCalls({
|
|
502
504
|
calls,
|
|
503
505
|
currency,
|
|
@@ -669,6 +671,7 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
669
671
|
challengeExpires?: string | undefined
|
|
670
672
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined
|
|
671
673
|
feePayer?: Account | undefined
|
|
674
|
+
isSponsored?: boolean | undefined
|
|
672
675
|
}): Promise<{ txHash: Hex; newDeposit: bigint }> {
|
|
673
676
|
const {
|
|
674
677
|
client,
|
|
@@ -681,9 +684,10 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
681
684
|
challengeExpires,
|
|
682
685
|
feePayerPolicy,
|
|
683
686
|
feePayer,
|
|
687
|
+
isSponsored = Boolean(feePayer),
|
|
684
688
|
} = parameters
|
|
685
689
|
|
|
686
|
-
if (
|
|
690
|
+
if (isSponsored && !FeePayer.isTempoTransaction(serializedTransaction))
|
|
687
691
|
throw new BadRequestError({
|
|
688
692
|
reason: 'Only Tempo (0x76/0x78) transactions are supported',
|
|
689
693
|
})
|
|
@@ -692,11 +696,11 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
692
696
|
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
693
697
|
)
|
|
694
698
|
|
|
695
|
-
if (
|
|
699
|
+
if (isSponsored) assertSenderSigned(transaction)
|
|
696
700
|
|
|
697
701
|
const calls = transaction.calls ?? []
|
|
698
702
|
|
|
699
|
-
const sponsoredTopUpCall =
|
|
703
|
+
const sponsoredTopUpCall = isSponsored
|
|
700
704
|
? validateSponsoredTopUpCalls({
|
|
701
705
|
calls,
|
|
702
706
|
currency,
|
|
@@ -117,6 +117,27 @@ describe('channelStore', () => {
|
|
|
117
117
|
expect(typeof loaded!.createdAt).toBe('string')
|
|
118
118
|
})
|
|
119
119
|
|
|
120
|
+
test('prefixes backing store keys when configured', async () => {
|
|
121
|
+
const rawStore = Store.memory()
|
|
122
|
+
const cs = ChannelStore.fromStore(Store.from(rawStore, { keyPrefix: 'tenant:' }))
|
|
123
|
+
await cs.updateChannel(channelId, () => makeChannel())
|
|
124
|
+
|
|
125
|
+
expect(await rawStore.get(`tenant:${channelId}`)).not.toBeNull()
|
|
126
|
+
expect(await rawStore.get(channelId)).toBeNull()
|
|
127
|
+
expect((await cs.getChannel(channelId))?.channelId).toBe(channelId)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('isolates prefixed store wrappers', async () => {
|
|
131
|
+
const rawStore = Store.memory()
|
|
132
|
+
const first = ChannelStore.fromStore(Store.from(rawStore, { keyPrefix: 'tenant-a:' }))
|
|
133
|
+
const second = ChannelStore.fromStore(Store.from(rawStore, { keyPrefix: 'tenant-b:' }))
|
|
134
|
+
await first.updateChannel(channelId, () => makeChannel())
|
|
135
|
+
|
|
136
|
+
expect(await second.getChannel(channelId)).toBeNull()
|
|
137
|
+
expect(await rawStore.get(`tenant-a:${channelId}`)).not.toBeNull()
|
|
138
|
+
expect(await rawStore.get(`tenant-b:${channelId}`)).toBeNull()
|
|
139
|
+
})
|
|
140
|
+
|
|
120
141
|
test('treats case-variant channelIds as the same record', async () => {
|
|
121
142
|
const cs = ChannelStore.fromStore(Store.memory())
|
|
122
143
|
await cs.updateChannel(mixedCaseAliasChannelId, () =>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { P256, Secp256k1, Signature } from 'ox'
|
|
2
|
+
import { SignatureEnvelope } from 'ox/tempo'
|
|
3
|
+
import { createClient, http, type Account, type Hex } from 'viem'
|
|
2
4
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
5
|
+
import { Account as TempoAccount, WebCryptoP256 } from 'viem/tempo'
|
|
3
6
|
import { describe, expect, test } from 'vp/test'
|
|
4
7
|
|
|
5
8
|
import { parseVoucherFromPayload, signVoucher, verifyVoucher } from './Voucher.js'
|
|
@@ -39,6 +42,232 @@ describe('Voucher', () => {
|
|
|
39
42
|
expect(isValid).toBe(true)
|
|
40
43
|
})
|
|
41
44
|
|
|
45
|
+
test('signVoucher rejects direct WebCrypto P256 voucher signatures', async () => {
|
|
46
|
+
const keyPair = await WebCryptoP256.createKeyPair()
|
|
47
|
+
const p256Account = TempoAccount.fromWebCryptoP256(keyPair)
|
|
48
|
+
const p256Client = createClient({
|
|
49
|
+
account: p256Account,
|
|
50
|
+
transport: http('http://127.0.0.1'),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
await expect(
|
|
54
|
+
signVoucher(
|
|
55
|
+
p256Client,
|
|
56
|
+
p256Account,
|
|
57
|
+
{ channelId, cumulativeAmount },
|
|
58
|
+
escrowContract,
|
|
59
|
+
chainId,
|
|
60
|
+
),
|
|
61
|
+
).rejects.toThrow('Session vouchers only support secp256k1 signatures')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('signVoucher rejects direct WebAuthn voucher signatures', async () => {
|
|
65
|
+
const webAuthnAccount = TempoAccount.fromHeadlessWebAuthn(P256.randomPrivateKey(), {
|
|
66
|
+
origin: 'https://example.com',
|
|
67
|
+
rpId: 'example.com',
|
|
68
|
+
})
|
|
69
|
+
const webAuthnClient = createClient({
|
|
70
|
+
account: webAuthnAccount,
|
|
71
|
+
transport: http('http://127.0.0.1'),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await expect(
|
|
75
|
+
signVoucher(
|
|
76
|
+
webAuthnClient,
|
|
77
|
+
webAuthnAccount,
|
|
78
|
+
{ channelId, cumulativeAmount },
|
|
79
|
+
escrowContract,
|
|
80
|
+
chainId,
|
|
81
|
+
),
|
|
82
|
+
).rejects.toThrow('Session vouchers only support secp256k1 signatures')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('signVoucher signs v1 secp256k1 access keys with raw signatures', async () => {
|
|
86
|
+
const accessKey = TempoAccount.fromSecp256k1(
|
|
87
|
+
'0x59c6995e998f97a5a0044966f09453863d462d2b3f1446a99f0a3d7b5d0f5a0d',
|
|
88
|
+
{ access: account, internal_version: 'v1' },
|
|
89
|
+
)
|
|
90
|
+
const accessKeyClient = createClient({
|
|
91
|
+
account: accessKey,
|
|
92
|
+
transport: http('http://127.0.0.1'),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const signature = await signVoucher(
|
|
96
|
+
accessKeyClient,
|
|
97
|
+
accessKey,
|
|
98
|
+
{ channelId, cumulativeAmount },
|
|
99
|
+
escrowContract,
|
|
100
|
+
chainId,
|
|
101
|
+
)
|
|
102
|
+
const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
|
|
103
|
+
|
|
104
|
+
expect(envelope.type).toBe('secp256k1')
|
|
105
|
+
expect(signature.length).toBe(132)
|
|
106
|
+
|
|
107
|
+
const isValid = await verifyVoucher(
|
|
108
|
+
escrowContract,
|
|
109
|
+
chainId,
|
|
110
|
+
{ channelId, cumulativeAmount, signature },
|
|
111
|
+
accessKey.accessKeyAddress,
|
|
112
|
+
)
|
|
113
|
+
expect(isValid).toBe(true)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('signVoucher unwraps legacy secp256k1 keychain signatures', async () => {
|
|
117
|
+
const privateKey = '0x59c6995e998f97a5a0044966f09453863d462d2b3f1446a99f0a3d7b5d0f5a0d'
|
|
118
|
+
const rawAccessKey = TempoAccount.fromSecp256k1(privateKey)
|
|
119
|
+
const keychainSigner = {
|
|
120
|
+
accessKeyAddress: rawAccessKey.address,
|
|
121
|
+
address: account.address,
|
|
122
|
+
async sign({ hash }: { hash: Hex }) {
|
|
123
|
+
const inner = SignatureEnvelope.from(
|
|
124
|
+
Signature.toHex(Secp256k1.sign({ payload: hash, privateKey })),
|
|
125
|
+
)
|
|
126
|
+
return SignatureEnvelope.serialize({
|
|
127
|
+
type: 'keychain',
|
|
128
|
+
version: 'v1',
|
|
129
|
+
userAddress: account.address,
|
|
130
|
+
inner,
|
|
131
|
+
})
|
|
132
|
+
},
|
|
133
|
+
} as unknown as Account
|
|
134
|
+
|
|
135
|
+
const signature = await signVoucher(
|
|
136
|
+
client,
|
|
137
|
+
keychainSigner,
|
|
138
|
+
{ channelId, cumulativeAmount },
|
|
139
|
+
escrowContract,
|
|
140
|
+
chainId,
|
|
141
|
+
)
|
|
142
|
+
const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
|
|
143
|
+
|
|
144
|
+
expect(envelope.type).toBe('secp256k1')
|
|
145
|
+
expect(signature.length).toBe(132)
|
|
146
|
+
|
|
147
|
+
const isValid = await verifyVoucher(
|
|
148
|
+
escrowContract,
|
|
149
|
+
chainId,
|
|
150
|
+
{ channelId, cumulativeAmount, signature },
|
|
151
|
+
rawAccessKey.address,
|
|
152
|
+
)
|
|
153
|
+
expect(isValid).toBe(true)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('signVoucher signs v2 secp256k1 access keys with raw signatures', async () => {
|
|
157
|
+
const accessKey = TempoAccount.fromSecp256k1(
|
|
158
|
+
'0x59c6995e998f97a5a0044966f09453863d462d2b3f1446a99f0a3d7b5d0f5a0d',
|
|
159
|
+
{ access: account },
|
|
160
|
+
)
|
|
161
|
+
const accessKeyClient = createClient({
|
|
162
|
+
account: accessKey,
|
|
163
|
+
transport: http('http://127.0.0.1'),
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const signature = await signVoucher(
|
|
167
|
+
accessKeyClient,
|
|
168
|
+
accessKey,
|
|
169
|
+
{ channelId, cumulativeAmount },
|
|
170
|
+
escrowContract,
|
|
171
|
+
chainId,
|
|
172
|
+
)
|
|
173
|
+
const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
|
|
174
|
+
|
|
175
|
+
expect(envelope.type).toBe('secp256k1')
|
|
176
|
+
expect(signature.length).toBe(132)
|
|
177
|
+
|
|
178
|
+
const isValid = await verifyVoucher(
|
|
179
|
+
escrowContract,
|
|
180
|
+
chainId,
|
|
181
|
+
{ channelId, cumulativeAmount, signature },
|
|
182
|
+
accessKey.accessKeyAddress,
|
|
183
|
+
)
|
|
184
|
+
expect(isValid).toBe(true)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('verifyVoucher rejects keychain envelopes', async () => {
|
|
188
|
+
const privateKey = '0x59c6995e998f97a5a0044966f09453863d462d2b3f1446a99f0a3d7b5d0f5a0d'
|
|
189
|
+
const inner = SignatureEnvelope.from(
|
|
190
|
+
Signature.toHex(Secp256k1.sign({ payload: channelId, privateKey })),
|
|
191
|
+
)
|
|
192
|
+
const keychainSignature = SignatureEnvelope.serialize({
|
|
193
|
+
type: 'keychain',
|
|
194
|
+
version: 'v2',
|
|
195
|
+
userAddress: account.address,
|
|
196
|
+
inner,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const isValid = await verifyVoucher(
|
|
200
|
+
escrowContract,
|
|
201
|
+
chainId,
|
|
202
|
+
{ channelId, cumulativeAmount, signature: keychainSignature },
|
|
203
|
+
account.address,
|
|
204
|
+
)
|
|
205
|
+
expect(isValid).toBe(false)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('signVoucher signs non-secp256k1 access keys without keychain envelopes', async () => {
|
|
209
|
+
const accessKey = TempoAccount.fromP256(P256.randomPrivateKey(), {
|
|
210
|
+
access: account,
|
|
211
|
+
internal_version: 'v1',
|
|
212
|
+
})
|
|
213
|
+
const accessKeyClient = createClient({
|
|
214
|
+
account: accessKey,
|
|
215
|
+
transport: http('http://127.0.0.1'),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
await expect(
|
|
219
|
+
signVoucher(
|
|
220
|
+
accessKeyClient,
|
|
221
|
+
accessKey,
|
|
222
|
+
{ channelId, cumulativeAmount },
|
|
223
|
+
escrowContract,
|
|
224
|
+
chainId,
|
|
225
|
+
),
|
|
226
|
+
).rejects.toThrow('Session vouchers only support secp256k1 signatures')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('verifyVoucher rejects magic-suffixed secp256k1 signatures', async () => {
|
|
230
|
+
const signature = await signVoucher(
|
|
231
|
+
client,
|
|
232
|
+
account,
|
|
233
|
+
{ channelId, cumulativeAmount },
|
|
234
|
+
escrowContract,
|
|
235
|
+
chainId,
|
|
236
|
+
)
|
|
237
|
+
const signatureWithMagic = SignatureEnvelope.serialize(SignatureEnvelope.from(signature), {
|
|
238
|
+
magic: true,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const isValid = await verifyVoucher(
|
|
242
|
+
escrowContract,
|
|
243
|
+
chainId,
|
|
244
|
+
{ channelId, cumulativeAmount, signature: signatureWithMagic },
|
|
245
|
+
account.address,
|
|
246
|
+
)
|
|
247
|
+
expect(isValid).toBe(false)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('verifyVoucher rejects EIP-155-style secp256k1 v values', async () => {
|
|
251
|
+
const signature = await signVoucher(
|
|
252
|
+
client,
|
|
253
|
+
account,
|
|
254
|
+
{ channelId, cumulativeAmount },
|
|
255
|
+
escrowContract,
|
|
256
|
+
chainId,
|
|
257
|
+
)
|
|
258
|
+
const signatureWithEip155V = `${signature.slice(0, -2)}${
|
|
259
|
+
signature.endsWith('1b') ? '23' : '24'
|
|
260
|
+
}` as `0x${string}`
|
|
261
|
+
|
|
262
|
+
const isValid = await verifyVoucher(
|
|
263
|
+
escrowContract,
|
|
264
|
+
chainId,
|
|
265
|
+
{ channelId, cumulativeAmount, signature: signatureWithEip155V },
|
|
266
|
+
account.address,
|
|
267
|
+
)
|
|
268
|
+
expect(isValid).toBe(false)
|
|
269
|
+
})
|
|
270
|
+
|
|
42
271
|
test('verifyVoucher rejects wrong signer', async () => {
|
|
43
272
|
const signature = await signVoucher(
|
|
44
273
|
client,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { type Address
|
|
1
|
+
import { Signature, type Address } from 'ox'
|
|
2
2
|
import { SignatureEnvelope } from 'ox/tempo'
|
|
3
3
|
import type { Account, Client, Hex } from 'viem'
|
|
4
|
-
import {
|
|
4
|
+
import { hashTypedData } from 'viem'
|
|
5
5
|
import { signTypedData } from 'viem/actions'
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { getAccountSignerAddress, isAccessKeyAccount } from '../internal/account.js'
|
|
8
8
|
import type { SignedVoucher, Voucher } from './Types.js'
|
|
9
9
|
|
|
10
10
|
/** Must match the on-chain TempoStreamChannel DOMAIN_SEPARATOR name. */
|
|
@@ -35,48 +35,110 @@ const voucherTypes = {
|
|
|
35
35
|
],
|
|
36
36
|
} as const
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const acceptTip1020VoucherSignatures = false
|
|
39
|
+
|
|
40
|
+
function getVoucherMessage(message: Voucher) {
|
|
41
|
+
return {
|
|
42
|
+
channelId: message.channelId,
|
|
43
|
+
cumulativeAmount: message.cumulativeAmount,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getVoucherDigest(escrowContract: Address.Address, chainId: number, message: Voucher) {
|
|
48
|
+
return hashTypedData({
|
|
49
|
+
domain: getVoucherDomain(escrowContract, chainId),
|
|
50
|
+
types: voucherTypes,
|
|
51
|
+
primaryType: 'Voucher',
|
|
52
|
+
message: getVoucherMessage(message),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function signVoucherTypedData(
|
|
42
57
|
client: Client,
|
|
43
58
|
account: Account,
|
|
44
59
|
message: Voucher,
|
|
45
60
|
escrowContract: Address.Address,
|
|
46
61
|
chainId: number,
|
|
47
|
-
authorizedSigner?: Address.Address | undefined,
|
|
48
62
|
): Promise<Hex> {
|
|
49
|
-
|
|
63
|
+
return signTypedData(client, {
|
|
50
64
|
account,
|
|
51
65
|
domain: getVoucherDomain(escrowContract, chainId),
|
|
52
66
|
types: voucherTypes,
|
|
53
67
|
primaryType: 'Voucher',
|
|
54
|
-
message:
|
|
55
|
-
channelId: message.channelId,
|
|
56
|
-
cumulativeAmount: message.cumulativeAmount,
|
|
57
|
-
},
|
|
68
|
+
message: getVoucherMessage(message),
|
|
58
69
|
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeVoucherSignature(signature: Hex): Hex {
|
|
73
|
+
const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (envelope.type === 'keychain' && envelope.inner.type === 'secp256k1')
|
|
68
|
-
return Signature.toHex(envelope.inner.signature)
|
|
69
|
-
} catch {}
|
|
75
|
+
if (envelope.type === 'keychain') {
|
|
76
|
+
if (envelope.inner.type !== 'secp256k1')
|
|
77
|
+
throw new Error(
|
|
78
|
+
'Session vouchers only unwrap secp256k1 keychain signatures; pass a direct voucherSigner for other key types.',
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return Signature.toHex(envelope.inner.signature)
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
|
|
84
|
+
// Tempo local accounts may append signature-envelope magic bytes for RPC
|
|
85
|
+
// routing. Voucher signatures are direct envelopes without magic bytes.
|
|
86
|
+
return SignatureEnvelope.serialize(envelope)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function acceptsVoucherEnvelope(envelope: SignatureEnvelope.SignatureEnvelope): boolean {
|
|
90
|
+
if (envelope.type === 'keychain') return false
|
|
91
|
+
if (envelope.type === 'secp256k1') return true
|
|
92
|
+
return acceptTip1020VoucherSignatures
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertSupportedVoucherEnvelope(envelope: SignatureEnvelope.SignatureEnvelope) {
|
|
96
|
+
if (acceptsVoucherEnvelope(envelope)) return
|
|
97
|
+
|
|
98
|
+
throw new Error(
|
|
99
|
+
'Session vouchers only support secp256k1 signatures until TIP-1020 voucher verification is enabled.',
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sign a voucher with an account.
|
|
105
|
+
*/
|
|
106
|
+
export async function signVoucher(
|
|
107
|
+
client: Client,
|
|
108
|
+
account: Account,
|
|
109
|
+
message: Voucher,
|
|
110
|
+
escrowContract: Address.Address,
|
|
111
|
+
chainId: number,
|
|
112
|
+
voucherSigner?: Account | undefined,
|
|
113
|
+
): Promise<Hex> {
|
|
114
|
+
const signer = voucherSigner ?? account
|
|
115
|
+
const expectedSigner = getAccountSignerAddress(signer)
|
|
116
|
+
|
|
117
|
+
const digest = getVoucherDigest(escrowContract, chainId, message)
|
|
118
|
+
const signature = isAccessKeyAccount(signer)
|
|
119
|
+
? await signer.sign({ hash: digest, raw: true })
|
|
120
|
+
: await signVoucherTypedData(client, signer, message, escrowContract, chainId)
|
|
121
|
+
const normalized = normalizeVoucherSignature(signature)
|
|
122
|
+
const envelope = SignatureEnvelope.from(normalized as SignatureEnvelope.Serialized)
|
|
123
|
+
assertSupportedVoucherEnvelope(envelope)
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
!SignatureEnvelope.verify(envelope, {
|
|
127
|
+
address: expectedSigner,
|
|
128
|
+
payload: digest,
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
throw new Error('voucher signature does not match voucher signer')
|
|
132
|
+
|
|
133
|
+
return normalized
|
|
73
134
|
}
|
|
74
135
|
|
|
75
136
|
/**
|
|
76
137
|
* Verify a voucher signature matches the expected signer.
|
|
77
138
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
139
|
+
* Accepts canonical raw secp256k1 voucher signatures.
|
|
140
|
+
*
|
|
141
|
+
* TIP-1020 voucher signatures will be enabled when onchain escrow verification ships.
|
|
80
142
|
*/
|
|
81
143
|
export async function verifyVoucher(
|
|
82
144
|
escrowContract: Address.Address,
|
|
@@ -85,31 +147,17 @@ export async function verifyVoucher(
|
|
|
85
147
|
expectedSigner: Address.Address,
|
|
86
148
|
): Promise<boolean> {
|
|
87
149
|
try {
|
|
88
|
-
const domain = getVoucherDomain(escrowContract, chainId)
|
|
89
|
-
const message = {
|
|
90
|
-
channelId: voucher.channelId,
|
|
91
|
-
cumulativeAmount: voucher.cumulativeAmount,
|
|
92
|
-
}
|
|
93
|
-
|
|
94
150
|
const envelope = SignatureEnvelope.from(voucher.signature)
|
|
95
151
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const signer = await recoverTypedDataAddress({
|
|
106
|
-
domain,
|
|
107
|
-
types: voucherTypes,
|
|
108
|
-
primaryType: 'Voucher',
|
|
109
|
-
message,
|
|
110
|
-
signature: voucher.signature,
|
|
152
|
+
if (!acceptsVoucherEnvelope(envelope)) return false
|
|
153
|
+
|
|
154
|
+
const canonical = SignatureEnvelope.serialize(envelope)
|
|
155
|
+
if (canonical.toLowerCase() !== voucher.signature.toLowerCase()) return false
|
|
156
|
+
|
|
157
|
+
return SignatureEnvelope.verify(envelope, {
|
|
158
|
+
address: expectedSigner,
|
|
159
|
+
payload: getVoucherDigest(escrowContract, chainId, voucher),
|
|
111
160
|
})
|
|
112
|
-
return TempoAddress.isEqual(signer, expectedSigner)
|
|
113
161
|
} catch {
|
|
114
162
|
return false
|
|
115
163
|
}
|
|
@@ -407,4 +407,75 @@ describe('isows', () => {
|
|
|
407
407
|
expect(channel?.spent).toBe(0n)
|
|
408
408
|
expect(channel?.units).toBe(0)
|
|
409
409
|
})
|
|
410
|
+
|
|
411
|
+
test('does not meter or emit application messages after close is requested on-chain', async () => {
|
|
412
|
+
const socket = new MockSocket()
|
|
413
|
+
const store = memoryChannelStore()
|
|
414
|
+
await store.updateChannel(channelId, () => ({
|
|
415
|
+
channelId,
|
|
416
|
+
payer: '0x0000000000000000000000000000000000000001' as Address,
|
|
417
|
+
payee: '0x0000000000000000000000000000000000000002' as Address,
|
|
418
|
+
token: '0x0000000000000000000000000000000000000003' as Address,
|
|
419
|
+
authorizedSigner: '0x0000000000000000000000000000000000000004' as Address,
|
|
420
|
+
chainId: 42431,
|
|
421
|
+
escrowContract: '0x0000000000000000000000000000000000000005' as Address,
|
|
422
|
+
deposit: 1n,
|
|
423
|
+
settledOnChain: 0n,
|
|
424
|
+
highestVoucherAmount: 1n,
|
|
425
|
+
highestVoucher: null,
|
|
426
|
+
spent: 0n,
|
|
427
|
+
units: 0,
|
|
428
|
+
closeRequestedAt: 1n,
|
|
429
|
+
finalized: false,
|
|
430
|
+
createdAt: new Date().toISOString(),
|
|
431
|
+
}))
|
|
432
|
+
|
|
433
|
+
await Ws.serve({
|
|
434
|
+
socket,
|
|
435
|
+
store,
|
|
436
|
+
url: 'ws://example.test/stream',
|
|
437
|
+
route: async () => ({
|
|
438
|
+
status: 200,
|
|
439
|
+
withReceipt(response = new Response(null, { status: 204 })) {
|
|
440
|
+
response.headers.set(
|
|
441
|
+
'Payment-Receipt',
|
|
442
|
+
serializeSessionReceipt(
|
|
443
|
+
createSessionReceipt({
|
|
444
|
+
challengeId: challenge.id,
|
|
445
|
+
channelId,
|
|
446
|
+
acceptedCumulative: 1n,
|
|
447
|
+
spent: 0n,
|
|
448
|
+
units: 0,
|
|
449
|
+
}),
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
return response
|
|
453
|
+
},
|
|
454
|
+
}),
|
|
455
|
+
generate: (async function* () {
|
|
456
|
+
yield 'should-not-reach'
|
|
457
|
+
})(),
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
socket.receive(
|
|
461
|
+
Ws.formatAuthorizationMessage(
|
|
462
|
+
makeCredential({
|
|
463
|
+
action: 'open',
|
|
464
|
+
channelId,
|
|
465
|
+
cumulativeAmount: '1',
|
|
466
|
+
signature: `0x${'77'.repeat(65)}`,
|
|
467
|
+
transaction: '0x01',
|
|
468
|
+
type: 'transaction',
|
|
469
|
+
}),
|
|
470
|
+
),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
await sleep(100)
|
|
474
|
+
|
|
475
|
+
expect(socket.closed).toBe(true)
|
|
476
|
+
expect(socket.sent.some((message) => message.includes('should-not-reach'))).toBe(false)
|
|
477
|
+
const channel = await store.getChannel(channelId)
|
|
478
|
+
expect(channel?.spent).toBe(0n)
|
|
479
|
+
expect(channel?.units).toBe(0)
|
|
480
|
+
})
|
|
410
481
|
})
|
package/src/tempo/session/Ws.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Credential from '../../Credential.js'
|
|
2
|
+
import { ChannelClosedError } from '../../Errors.js'
|
|
2
3
|
import * as ChannelStore from './ChannelStore.js'
|
|
3
4
|
import { deserializeSessionReceipt } from './Receipt.js'
|
|
4
5
|
import { createSessionReceipt } from './Receipt.js'
|
|
@@ -405,6 +406,7 @@ async function reserveChargeOrWait(options: {
|
|
|
405
406
|
|
|
406
407
|
let channel = await store.getChannel(channelId)
|
|
407
408
|
if (!channel) throw new Error('channel not found')
|
|
409
|
+
throwIfChannelClosed(channel)
|
|
408
410
|
|
|
409
411
|
const hasHeadroom = (state: ChannelStore.State) =>
|
|
410
412
|
state.highestVoucherAmount - state.spent - reservedAmount >= amount
|
|
@@ -424,6 +426,7 @@ async function reserveChargeOrWait(options: {
|
|
|
424
426
|
await waitForUpdate(store, channelId, pollIntervalMs, signal)
|
|
425
427
|
channel = await store.getChannel(channelId)
|
|
426
428
|
if (!channel) throw new Error('channel not found')
|
|
429
|
+
throwIfChannelClosed(channel)
|
|
427
430
|
}
|
|
428
431
|
}
|
|
429
432
|
|
|
@@ -440,6 +443,7 @@ async function commitReservedCharges(options: {
|
|
|
440
443
|
const channel = await store.updateChannel(channelId, (current) => {
|
|
441
444
|
if (!current) return null
|
|
442
445
|
if (current.finalized) return current
|
|
446
|
+
if (current.closeRequestedAt !== 0n) return current
|
|
443
447
|
if (current.highestVoucherAmount - current.spent < amount) return current
|
|
444
448
|
committed = true
|
|
445
449
|
return {
|
|
@@ -450,9 +454,18 @@ async function commitReservedCharges(options: {
|
|
|
450
454
|
})
|
|
451
455
|
|
|
452
456
|
if (!channel) throw new Error('channel not found')
|
|
457
|
+
if (channel.finalized) throw new ChannelClosedError({ reason: 'channel is finalized' })
|
|
458
|
+
if (channel.closeRequestedAt !== 0n)
|
|
459
|
+
throw new ChannelClosedError({ reason: 'channel has a pending close request' })
|
|
453
460
|
if (!committed) throw new Error('reserved voucher coverage is no longer available')
|
|
454
461
|
}
|
|
455
462
|
|
|
463
|
+
function throwIfChannelClosed(channel: ChannelStore.State): void {
|
|
464
|
+
if (channel.finalized) throw new ChannelClosedError({ reason: 'channel is finalized' })
|
|
465
|
+
if (channel.closeRequestedAt !== 0n)
|
|
466
|
+
throw new ChannelClosedError({ reason: 'channel has a pending close request' })
|
|
467
|
+
}
|
|
468
|
+
|
|
456
469
|
async function waitForUpdate(
|
|
457
470
|
store: ChannelStore.ChannelStore,
|
|
458
471
|
channelId: SessionCredentialPayload['channelId'],
|