mppx 0.4.7 → 0.4.9
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 +15 -3
- package/README.md +13 -13
- package/dist/BodyDigest.d.ts.map +1 -1
- package/dist/BodyDigest.js.map +1 -1
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js.map +1 -1
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js.map +1 -1
- package/dist/Errors.js +64 -67
- package/dist/Errors.js.map +1 -1
- package/dist/PaymentRequest.d.ts.map +1 -1
- package/dist/PaymentRequest.js.map +1 -1
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js.map +1 -1
- package/dist/Store.d.ts +14 -4
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +17 -0
- package/dist/Store.js.map +1 -1
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +40 -5
- package/dist/cli/account.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +24 -8
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/internal.d.ts.map +1 -1
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +11 -23
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +1 -1
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +1 -1
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
- package/dist/mcp-sdk/server/Transport.js.map +1 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +5 -1
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +5 -2
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +3 -1
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.js +1 -1
- package/dist/proxy/Service.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts +2 -2
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +4 -2
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +47 -11
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Request.d.ts.map +1 -1
- package/dist/server/Request.js.map +1 -1
- package/dist/stripe/Methods.d.ts.map +1 -1
- package/dist/stripe/Methods.js.map +1 -1
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js.map +1 -1
- 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/address.d.ts +3 -0
- package/dist/tempo/internal/address.d.ts.map +1 -0
- package/dist/tempo/internal/address.js +4 -0
- package/dist/tempo/internal/address.js.map +1 -0
- package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
- package/dist/tempo/internal/auto-swap.js +4 -4
- package/dist/tempo/internal/auto-swap.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +4 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +12 -4
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +11 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +110 -51
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +31 -23
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +41 -1
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +51 -10
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +2 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +4 -2
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Receipt.d.ts.map +1 -1
- package/dist/tempo/session/Receipt.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js +3 -2
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +2 -2
- package/src/BodyDigest.ts +1 -0
- package/src/Challenge.test-d.ts +1 -0
- package/src/Challenge.ts +1 -0
- package/src/Credential.ts +1 -0
- package/src/Errors.test.ts +27 -39
- package/src/Expires.test.ts +1 -0
- package/src/PaymentRequest.ts +1 -0
- package/src/Receipt.ts +1 -0
- package/src/Store.test-d.ts +59 -0
- package/src/Store.test.ts +56 -6
- package/src/Store.ts +31 -4
- package/src/cli/account.ts +65 -30
- package/src/cli/cli.test.ts +127 -1
- package/src/cli/cli.ts +23 -8
- package/src/cli/config.test.ts +1 -0
- package/src/cli/internal.ts +1 -0
- package/src/cli/plugins/stripe.ts +1 -0
- package/src/cli/plugins/tempo.ts +21 -24
- package/src/cli/utils.ts +1 -0
- package/src/client/Mppx.test-d.ts +1 -0
- package/src/client/internal/Fetch.browser.test.ts +1 -0
- package/src/client/internal/Fetch.test-d.ts +1 -0
- package/src/client/internal/Fetch.test.ts +1 -0
- package/src/client/internal/Fetch.ts +1 -1
- package/src/internal/constantTimeEqual.test.ts +1 -0
- package/src/internal/types.ts +1 -3
- package/src/mcp-sdk/client/McpClient.test-d.ts +1 -0
- package/src/mcp-sdk/client/McpClient.test.ts +1 -0
- package/src/mcp-sdk/client/McpClient.ts +2 -0
- package/src/mcp-sdk/server/Transport.test.ts +1 -0
- package/src/mcp-sdk/server/Transport.ts +1 -0
- package/src/middlewares/elysia.test.ts +90 -0
- package/src/middlewares/elysia.ts +5 -1
- package/src/middlewares/express.test.ts +62 -2
- package/src/middlewares/express.ts +6 -2
- package/src/middlewares/hono.ts +1 -0
- package/src/middlewares/internal/mppx.test.ts +1 -0
- package/src/middlewares/nextjs.test.ts +1 -0
- package/src/proxy/Proxy.test.ts +57 -0
- package/src/proxy/Proxy.ts +8 -1
- package/src/proxy/Service.test.ts +1 -0
- package/src/proxy/Service.ts +8 -2
- package/src/proxy/internal/Headers.test.ts +1 -0
- package/src/proxy/internal/Route.test.ts +57 -0
- package/src/proxy/internal/Route.ts +3 -1
- package/src/proxy/services/openai.test.ts +1 -0
- package/src/server/Mppx.test.ts +438 -0
- package/src/server/Mppx.ts +51 -13
- package/src/server/Request.test.ts +1 -0
- package/src/server/Request.ts +1 -0
- package/src/server/Response.test.ts +1 -0
- package/src/server/Transport.test.ts +1 -0
- package/src/stripe/Methods.ts +1 -0
- package/src/stripe/client/Charge.test.ts +1 -0
- package/src/stripe/server/Charge.test.ts +1 -0
- package/src/tempo/Attribution.test.ts +1 -0
- package/src/tempo/Methods.ts +1 -0
- package/src/tempo/client/ChannelOps.test.ts +1 -0
- package/src/tempo/client/ChannelOps.ts +1 -0
- package/src/tempo/client/Charge.ts +1 -0
- package/src/tempo/client/Session.test.ts +1 -0
- package/src/tempo/client/Session.ts +1 -0
- package/src/tempo/client/SessionManager.test.ts +28 -0
- package/src/tempo/client/SessionManager.ts +2 -1
- package/src/tempo/internal/address.ts +6 -0
- package/src/tempo/internal/auto-swap.test.ts +1 -0
- package/src/tempo/internal/auto-swap.ts +4 -3
- package/src/tempo/internal/defaults.test.ts +1 -0
- package/src/tempo/internal/fee-payer.test.ts +1 -0
- package/src/tempo/internal/fee-payer.ts +19 -4
- package/src/tempo/server/Charge.test.ts +1081 -31
- package/src/tempo/server/Charge.ts +159 -63
- package/src/tempo/server/Session.test.ts +896 -107
- package/src/tempo/server/Session.ts +41 -23
- package/src/tempo/server/Sse.test.ts +2 -0
- package/src/tempo/server/internal/transport.test.ts +30 -0
- package/src/tempo/server/internal/transport.ts +41 -2
- package/src/tempo/session/Chain.test.ts +145 -0
- package/src/tempo/session/Chain.ts +59 -10
- package/src/tempo/session/Channel.test.ts +1 -0
- package/src/tempo/session/ChannelStore.test.ts +11 -0
- package/src/tempo/session/ChannelStore.ts +7 -3
- package/src/tempo/session/Receipt.test.ts +1 -0
- package/src/tempo/session/Receipt.ts +1 -0
- package/src/tempo/session/Sse.test.ts +2 -0
- package/src/tempo/session/Sse.ts +1 -0
- package/src/tempo/session/Voucher.test.ts +1 -0
- package/src/tempo/session/Voucher.ts +4 -2
- package/src/viem/Account.test.ts +1 -0
- package/src/viem/Client.test.ts +1 -0
- package/src/viem/Client.ts +1 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type Client as viem_Client,
|
|
19
19
|
} from 'viem'
|
|
20
20
|
import { tempo as tempo_chain } from 'viem/chains'
|
|
21
|
+
|
|
21
22
|
import {
|
|
22
23
|
AmountExceedsDepositError,
|
|
23
24
|
BadRequestError,
|
|
@@ -85,7 +86,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
85
86
|
const parameters = p as parameters
|
|
86
87
|
const {
|
|
87
88
|
amount,
|
|
88
|
-
channelStateTtl =
|
|
89
|
+
channelStateTtl = 5_000,
|
|
89
90
|
currency = defaults.resolveCurrency(parameters),
|
|
90
91
|
decimals = defaults.decimals,
|
|
91
92
|
store: rawStore = Store.memory(),
|
|
@@ -284,7 +285,7 @@ export declare namespace session {
|
|
|
284
285
|
>
|
|
285
286
|
|
|
286
287
|
type Parameters = {
|
|
287
|
-
/** 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
|
|
288
|
+
/** 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 5_000 */
|
|
288
289
|
channelStateTtl?: number | undefined
|
|
289
290
|
/** Minimum voucher delta to accept (numeric string, default: "0"). */
|
|
290
291
|
minVoucherDelta?: string | undefined
|
|
@@ -451,7 +452,7 @@ async function verifyAndAcceptVoucher(parameters: {
|
|
|
451
452
|
throw new ChannelClosedError({ reason: 'channel has a pending close request' })
|
|
452
453
|
}
|
|
453
454
|
|
|
454
|
-
if (voucher.cumulativeAmount
|
|
455
|
+
if (voucher.cumulativeAmount <= onChain.settled) {
|
|
455
456
|
throw new VerificationFailedError({
|
|
456
457
|
reason: 'voucher cumulativeAmount is below on-chain settled amount',
|
|
457
458
|
})
|
|
@@ -462,12 +463,8 @@ async function verifyAndAcceptVoucher(parameters: {
|
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
if (voucher.cumulativeAmount <= channel.highestVoucherAmount) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
channelId,
|
|
468
|
-
acceptedCumulative: channel.highestVoucherAmount,
|
|
469
|
-
spent: channel.spent,
|
|
470
|
-
units: channel.units,
|
|
466
|
+
throw new VerificationFailedError({
|
|
467
|
+
reason: 'voucher cumulativeAmount must be strictly greater than highest accepted voucher',
|
|
471
468
|
})
|
|
472
469
|
}
|
|
473
470
|
|
|
@@ -561,7 +558,7 @@ async function handleOpen(
|
|
|
561
558
|
throw new AmountExceedsDepositError({ reason: 'voucher amount exceeds on-chain deposit' })
|
|
562
559
|
}
|
|
563
560
|
|
|
564
|
-
if (voucher.cumulativeAmount
|
|
561
|
+
if (voucher.cumulativeAmount <= onChain.settled) {
|
|
565
562
|
throw new VerificationFailedError({
|
|
566
563
|
reason: 'voucher cumulativeAmount is below on-chain settled amount',
|
|
567
564
|
})
|
|
@@ -580,16 +577,22 @@ async function handleOpen(
|
|
|
580
577
|
|
|
581
578
|
const updated = await store.updateChannel(payload.channelId, (existing) => {
|
|
582
579
|
if (existing) {
|
|
583
|
-
if (voucher.cumulativeAmount
|
|
580
|
+
if (voucher.cumulativeAmount <= existing.settledOnChain) {
|
|
584
581
|
throw new VerificationFailedError({
|
|
585
582
|
reason: 'voucher amount is below settled on-chain amount',
|
|
586
583
|
})
|
|
587
584
|
}
|
|
588
585
|
|
|
586
|
+
const settledOnChain =
|
|
587
|
+
onChain.settled > existing.settledOnChain ? onChain.settled : existing.settledOnChain
|
|
588
|
+
const spent = settledOnChain > existing.spent ? settledOnChain : existing.spent
|
|
589
|
+
|
|
589
590
|
if (voucher.cumulativeAmount > existing.highestVoucherAmount) {
|
|
590
591
|
return {
|
|
591
592
|
...existing,
|
|
592
593
|
deposit: onChain.deposit,
|
|
594
|
+
settledOnChain,
|
|
595
|
+
spent,
|
|
593
596
|
highestVoucherAmount: voucher.cumulativeAmount,
|
|
594
597
|
highestVoucher: voucher,
|
|
595
598
|
authorizedSigner,
|
|
@@ -598,6 +601,8 @@ async function handleOpen(
|
|
|
598
601
|
return {
|
|
599
602
|
...existing,
|
|
600
603
|
deposit: onChain.deposit,
|
|
604
|
+
settledOnChain,
|
|
605
|
+
spent,
|
|
601
606
|
authorizedSigner,
|
|
602
607
|
}
|
|
603
608
|
}
|
|
@@ -605,15 +610,16 @@ async function handleOpen(
|
|
|
605
610
|
channelId: payload.channelId,
|
|
606
611
|
chainId: methodDetails.chainId,
|
|
607
612
|
escrowContract: methodDetails.escrowContract,
|
|
613
|
+
closeRequestedAt: onChain.closeRequestedAt,
|
|
608
614
|
payer: onChain.payer,
|
|
609
615
|
payee: onChain.payee,
|
|
610
616
|
token: onChain.token,
|
|
611
617
|
authorizedSigner,
|
|
612
618
|
deposit: onChain.deposit,
|
|
613
|
-
settledOnChain:
|
|
619
|
+
settledOnChain: onChain.settled,
|
|
614
620
|
highestVoucherAmount: voucher.cumulativeAmount,
|
|
615
621
|
highestVoucher: voucher,
|
|
616
|
-
spent:
|
|
622
|
+
spent: onChain.settled,
|
|
617
623
|
units: 0,
|
|
618
624
|
finalized: false,
|
|
619
625
|
createdAt: new Date().toISOString(),
|
|
@@ -715,18 +721,30 @@ async function handleVoucher(
|
|
|
715
721
|
//
|
|
716
722
|
// To guard against the payer initiating a forced close while vouchers
|
|
717
723
|
// are still being accepted, re-query on-chain state when the cache
|
|
718
|
-
// exceeds the configured staleness TTL.
|
|
724
|
+
// exceeds the configured staleness TTL (default: 5s).
|
|
719
725
|
const lastVerified = lastOnChainVerified.get(payload.channelId) ?? 0
|
|
720
726
|
const isStale = Date.now() - lastVerified > channelStateTtl
|
|
721
727
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
+
const onChain = await (async () => {
|
|
729
|
+
if (isStale) {
|
|
730
|
+
const onChainChannel = await getOnChainChannel(
|
|
731
|
+
client,
|
|
732
|
+
methodDetails.escrowContract,
|
|
733
|
+
payload.channelId,
|
|
734
|
+
)
|
|
735
|
+
lastOnChainVerified.set(payload.channelId, Date.now())
|
|
736
|
+
// Persist closeRequestedAt so the cached path detects force-close
|
|
737
|
+
// between re-queries.
|
|
738
|
+
if (onChainChannel.closeRequestedAt !== 0n) {
|
|
739
|
+
await store.updateChannel(payload.channelId, (current) =>
|
|
740
|
+
current ? { ...current, closeRequestedAt: onChainChannel.closeRequestedAt } : current,
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
return onChainChannel
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
728
746
|
finalized: channel.finalized,
|
|
729
|
-
closeRequestedAt:
|
|
747
|
+
closeRequestedAt: channel.closeRequestedAt,
|
|
730
748
|
payer: channel.payer,
|
|
731
749
|
payee: channel.payee,
|
|
732
750
|
token: channel.token,
|
|
@@ -734,7 +752,7 @@ async function handleVoucher(
|
|
|
734
752
|
deposit: channel.deposit,
|
|
735
753
|
settled: channel.settledOnChain,
|
|
736
754
|
}
|
|
737
|
-
}
|
|
755
|
+
})()
|
|
738
756
|
|
|
739
757
|
return verifyAndAcceptVoucher({
|
|
740
758
|
store,
|
|
@@ -743,7 +761,7 @@ async function handleVoucher(
|
|
|
743
761
|
channel,
|
|
744
762
|
channelId: payload.channelId,
|
|
745
763
|
voucher,
|
|
746
|
-
onChain
|
|
764
|
+
onChain,
|
|
747
765
|
methodDetails,
|
|
748
766
|
})
|
|
749
767
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
2
|
import { describe, expect, test } from 'vitest'
|
|
3
|
+
|
|
3
4
|
import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
|
|
4
5
|
import type * as ChannelStore from '../session/ChannelStore.js'
|
|
5
6
|
import { serve, toResponse } from '../session/Sse.js'
|
|
@@ -41,6 +42,7 @@ function seedChannel(
|
|
|
41
42
|
highestVoucher: null,
|
|
42
43
|
spent: 0n,
|
|
43
44
|
units: 0,
|
|
45
|
+
closeRequestedAt: 0n,
|
|
44
46
|
finalized: false,
|
|
45
47
|
createdAt: new Date().toISOString(),
|
|
46
48
|
}))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Challenge, Credential } from 'mppx'
|
|
2
2
|
import type { Address, Hex } from 'viem'
|
|
3
3
|
import { describe, expect, test } from 'vitest'
|
|
4
|
+
|
|
4
5
|
import * as Store from '../../../Store.js'
|
|
5
6
|
import { chainId, escrowContract as escrowContractDefaults } from '../../internal/defaults.js'
|
|
6
7
|
import * as ChannelStore from '../../session/ChannelStore.js'
|
|
@@ -31,6 +32,7 @@ function seedChannel(
|
|
|
31
32
|
highestVoucher: null,
|
|
32
33
|
spent: 0n,
|
|
33
34
|
units: 0,
|
|
35
|
+
closeRequestedAt: 0n,
|
|
34
36
|
finalized: false,
|
|
35
37
|
createdAt: new Date().toISOString(),
|
|
36
38
|
}))
|
|
@@ -262,6 +264,34 @@ describe('sse transport', () => {
|
|
|
262
264
|
).toThrow('No SSE context available')
|
|
263
265
|
})
|
|
264
266
|
|
|
267
|
+
test('respondReceipt with non-SSE upstream Response still deducts from channel', async () => {
|
|
268
|
+
const store = memoryStore()
|
|
269
|
+
await seedChannel(store, 10000000n)
|
|
270
|
+
const transport = sse({ store })
|
|
271
|
+
|
|
272
|
+
transport.getCredential(makeAuthorizedRequest())
|
|
273
|
+
|
|
274
|
+
const plainResponse = new Response(JSON.stringify({ content: 'hello' }), {
|
|
275
|
+
headers: { 'Content-Type': 'application/json' },
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const response = transport.respondReceipt({
|
|
279
|
+
receipt: makeReceipt(),
|
|
280
|
+
response: plainResponse,
|
|
281
|
+
challengeId,
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const body = await response.text()
|
|
285
|
+
|
|
286
|
+
const channel = await store.getChannel(channelId)
|
|
287
|
+
expect(channel!.spent).toBe(1000000n)
|
|
288
|
+
expect(channel!.units).toBe(1)
|
|
289
|
+
|
|
290
|
+
expect(JSON.parse(body)).toEqual({ content: 'hello' })
|
|
291
|
+
expect(response.headers.get('Content-Type')).toBe('application/json')
|
|
292
|
+
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
293
|
+
})
|
|
294
|
+
|
|
265
295
|
test('poll: true strips waitForUpdate from store', async () => {
|
|
266
296
|
const store = memoryStore()
|
|
267
297
|
;(store as any).waitForUpdate = async () => {}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @internal
|
|
7
7
|
*/
|
|
8
8
|
import * as Transport from '../../../server/Transport.js'
|
|
9
|
-
import
|
|
9
|
+
import * as ChannelStore from '../../session/ChannelStore.js'
|
|
10
10
|
import * as Sse_core from '../../session/Sse.js'
|
|
11
11
|
|
|
12
12
|
/** SSE transport with Tempo session controller. */
|
|
@@ -89,7 +89,46 @@ export function sse(options: sse.Options & { store: ChannelStore.ChannelStore })
|
|
|
89
89
|
return Sse_core.toResponse(stream)
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
const baseResponse = base.respondReceipt({
|
|
93
|
+
receipt,
|
|
94
|
+
response: response as Response,
|
|
95
|
+
challengeId,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Non-SSE response (e.g. upstream returned JSON instead of event-stream).
|
|
99
|
+
// Need to deduct tickCost so request isn't free.
|
|
100
|
+
const ctx = contextMap.get(challengeId)
|
|
101
|
+
if (ctx) {
|
|
102
|
+
contextMap.delete(challengeId)
|
|
103
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
104
|
+
async start(controller) {
|
|
105
|
+
// deduction completes before consumer reads
|
|
106
|
+
await ChannelStore.deductFromChannel(store, ctx.channelId, ctx.tickCost)
|
|
107
|
+
if (!baseResponse.body) {
|
|
108
|
+
controller.close()
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
const reader = baseResponse.body.getReader()
|
|
112
|
+
try {
|
|
113
|
+
while (true) {
|
|
114
|
+
const { done, value } = await reader.read()
|
|
115
|
+
if (done) break
|
|
116
|
+
controller.enqueue(value)
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
reader.releaseLock()
|
|
120
|
+
controller.close()
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
return new Response(stream, {
|
|
125
|
+
status: baseResponse.status,
|
|
126
|
+
statusText: baseResponse.statusText,
|
|
127
|
+
headers: baseResponse.headers,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return baseResponse
|
|
93
132
|
},
|
|
94
133
|
})
|
|
95
134
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
topUpChannel,
|
|
12
12
|
} from '~test/tempo/session.js'
|
|
13
13
|
import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
|
|
14
|
+
|
|
14
15
|
import {
|
|
15
16
|
broadcastOpenTransaction,
|
|
16
17
|
broadcastTopUpTransaction,
|
|
@@ -242,6 +243,33 @@ describe('on-chain', () => {
|
|
|
242
243
|
).rejects.toThrow('open transaction token does not match server currency')
|
|
243
244
|
})
|
|
244
245
|
|
|
246
|
+
test('rejects when transaction channelId does not match claimed channelId', async () => {
|
|
247
|
+
const salt = nextSalt()
|
|
248
|
+
|
|
249
|
+
const { serializedTransaction } = await signOpenChannel({
|
|
250
|
+
escrow: escrowContract,
|
|
251
|
+
payer,
|
|
252
|
+
payee: recipient,
|
|
253
|
+
token: currency,
|
|
254
|
+
deposit: 5_000_000n,
|
|
255
|
+
salt,
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
const fakeChannelId =
|
|
259
|
+
'0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
260
|
+
|
|
261
|
+
await expect(
|
|
262
|
+
broadcastOpenTransaction({
|
|
263
|
+
client,
|
|
264
|
+
serializedTransaction,
|
|
265
|
+
escrowContract,
|
|
266
|
+
channelId: fakeChannelId,
|
|
267
|
+
recipient,
|
|
268
|
+
currency,
|
|
269
|
+
}),
|
|
270
|
+
).rejects.toThrow('open transaction does not match claimed channelId')
|
|
271
|
+
})
|
|
272
|
+
|
|
245
273
|
test('successful broadcast returns txHash and onChain state', async () => {
|
|
246
274
|
const salt = nextSalt()
|
|
247
275
|
const deposit = 10_000_000n
|
|
@@ -313,6 +341,59 @@ describe('on-chain', () => {
|
|
|
313
341
|
).rejects.toThrow('fee-sponsored open transaction contains an unauthorized call')
|
|
314
342
|
})
|
|
315
343
|
|
|
344
|
+
test('fee-payer: rejects unsigned transaction', async () => {
|
|
345
|
+
const salt = nextSalt()
|
|
346
|
+
const deposit = 5_000_000n
|
|
347
|
+
|
|
348
|
+
const { channelId, serializedTransaction } = await signOpenChannel({
|
|
349
|
+
escrow: escrowContract,
|
|
350
|
+
payer,
|
|
351
|
+
payee: recipient,
|
|
352
|
+
token: currency,
|
|
353
|
+
deposit,
|
|
354
|
+
salt,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
// Strip the sender signature to simulate the POC attack
|
|
358
|
+
const deserialized = Transaction.deserialize(
|
|
359
|
+
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
360
|
+
)
|
|
361
|
+
const unsigned = await Transaction.serialize({
|
|
362
|
+
...deserialized,
|
|
363
|
+
signature: undefined,
|
|
364
|
+
from: undefined,
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
await expect(
|
|
368
|
+
broadcastOpenTransaction({
|
|
369
|
+
client,
|
|
370
|
+
serializedTransaction: unsigned,
|
|
371
|
+
escrowContract,
|
|
372
|
+
channelId,
|
|
373
|
+
recipient,
|
|
374
|
+
currency,
|
|
375
|
+
feePayer: accounts[0],
|
|
376
|
+
}),
|
|
377
|
+
).rejects.toThrow('Transaction must be signed by the sender before fee payer co-signing')
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
test('fee-payer: rejects non-Tempo transaction', async () => {
|
|
381
|
+
const fakeEip1559 =
|
|
382
|
+
'0x02f8650182a5bf843b9aca00843b9aca008252089400000000000000000000000000000000000000008080c001a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000' as Hex
|
|
383
|
+
|
|
384
|
+
await expect(
|
|
385
|
+
broadcastOpenTransaction({
|
|
386
|
+
client,
|
|
387
|
+
serializedTransaction: fakeEip1559,
|
|
388
|
+
escrowContract,
|
|
389
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000000' as Hex,
|
|
390
|
+
recipient,
|
|
391
|
+
currency,
|
|
392
|
+
feePayer: accounts[0],
|
|
393
|
+
}),
|
|
394
|
+
).rejects.toThrow('Only Tempo (0x76/0x78) transactions are supported')
|
|
395
|
+
})
|
|
396
|
+
|
|
316
397
|
test('duplicate broadcast returns fallback with txHash undefined', async () => {
|
|
317
398
|
const salt = nextSalt()
|
|
318
399
|
const deposit = 5_000_000n
|
|
@@ -544,6 +625,70 @@ describe('on-chain', () => {
|
|
|
544
625
|
}),
|
|
545
626
|
).rejects.toThrow('fee-sponsored topUp transaction contains an unauthorized call')
|
|
546
627
|
})
|
|
628
|
+
|
|
629
|
+
test('fee-payer: rejects unsigned transaction', async () => {
|
|
630
|
+
const salt = nextSalt()
|
|
631
|
+
const deposit = 5_000_000n
|
|
632
|
+
const topUpAmount = 3_000_000n
|
|
633
|
+
|
|
634
|
+
const { channelId } = await openChannel({
|
|
635
|
+
escrow: escrowContract,
|
|
636
|
+
payer,
|
|
637
|
+
payee: recipient,
|
|
638
|
+
token: currency,
|
|
639
|
+
deposit,
|
|
640
|
+
salt,
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
const { serializedTransaction } = await signTopUpChannel({
|
|
644
|
+
escrow: escrowContract,
|
|
645
|
+
payer,
|
|
646
|
+
channelId,
|
|
647
|
+
token: currency,
|
|
648
|
+
amount: topUpAmount,
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
// Strip the sender signature to simulate the POC attack
|
|
652
|
+
const deserialized = Transaction.deserialize(
|
|
653
|
+
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
654
|
+
)
|
|
655
|
+
const unsigned = await Transaction.serialize({
|
|
656
|
+
...deserialized,
|
|
657
|
+
signature: undefined,
|
|
658
|
+
from: undefined,
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
await expect(
|
|
662
|
+
broadcastTopUpTransaction({
|
|
663
|
+
client,
|
|
664
|
+
serializedTransaction: unsigned,
|
|
665
|
+
escrowContract,
|
|
666
|
+
channelId,
|
|
667
|
+
currency: asset,
|
|
668
|
+
declaredDeposit: topUpAmount,
|
|
669
|
+
previousDeposit: deposit,
|
|
670
|
+
feePayer: accounts[0],
|
|
671
|
+
}),
|
|
672
|
+
).rejects.toThrow('Transaction must be signed by the sender before fee payer co-signing')
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
test('fee-payer: rejects non-Tempo transaction', async () => {
|
|
676
|
+
const fakeEip1559 =
|
|
677
|
+
'0x02f8650182a5bf843b9aca00843b9aca008252089400000000000000000000000000000000000000008080c001a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000' as Hex
|
|
678
|
+
|
|
679
|
+
await expect(
|
|
680
|
+
broadcastTopUpTransaction({
|
|
681
|
+
client,
|
|
682
|
+
serializedTransaction: fakeEip1559,
|
|
683
|
+
escrowContract,
|
|
684
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000000' as Hex,
|
|
685
|
+
currency: asset,
|
|
686
|
+
declaredDeposit: 1_000_000n,
|
|
687
|
+
previousDeposit: 0n,
|
|
688
|
+
feePayer: accounts[0],
|
|
689
|
+
}),
|
|
690
|
+
).rejects.toThrow('Only Tempo (0x76/0x78) transactions are supported')
|
|
691
|
+
})
|
|
547
692
|
})
|
|
548
693
|
|
|
549
694
|
describe('settleOnChain', () => {
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
encodeFunctionData,
|
|
7
7
|
getAbiItem,
|
|
8
8
|
type Hex,
|
|
9
|
-
isAddressEqual,
|
|
10
9
|
type ReadContractReturnType,
|
|
11
10
|
toFunctionSelector,
|
|
12
11
|
} from 'viem'
|
|
@@ -20,13 +19,29 @@ import {
|
|
|
20
19
|
writeContract,
|
|
21
20
|
} from 'viem/actions'
|
|
22
21
|
import { Transaction } from 'viem/tempo'
|
|
22
|
+
|
|
23
23
|
import { BadRequestError, ChannelClosedError, VerificationFailedError } from '../../Errors.js'
|
|
24
|
+
import * as TempoAddress from '../internal/address.js'
|
|
24
25
|
import * as defaults from '../internal/defaults.js'
|
|
26
|
+
import { isTempoTransaction } from '../internal/fee-payer.js'
|
|
27
|
+
import * as Channel from './Channel.js'
|
|
25
28
|
import { escrowAbi } from './escrow.abi.js'
|
|
26
29
|
import type { SignedVoucher } from './Types.js'
|
|
27
30
|
|
|
28
31
|
export { escrowAbi }
|
|
29
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Asserts that a deserialized transaction has an existing sender signature —
|
|
35
|
+
* required before fee payer co-signing to prevent the fee payer from becoming
|
|
36
|
+
* the sender.
|
|
37
|
+
*/
|
|
38
|
+
function assertSenderSigned(transaction: any): void {
|
|
39
|
+
if (!transaction.signature || !transaction.from)
|
|
40
|
+
throw new BadRequestError({
|
|
41
|
+
reason: 'Transaction must be signed by the sender before fee payer co-signing',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
const UINT128_MAX = 2n ** 128n - 1n
|
|
31
46
|
|
|
32
47
|
/**
|
|
@@ -221,13 +236,21 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
221
236
|
waitForConfirmation = true,
|
|
222
237
|
} = parameters
|
|
223
238
|
|
|
239
|
+
if (feePayer && !isTempoTransaction(serializedTransaction))
|
|
240
|
+
throw new BadRequestError({
|
|
241
|
+
reason: 'Only Tempo (0x76/0x78) transactions are supported',
|
|
242
|
+
})
|
|
243
|
+
|
|
224
244
|
const transaction = Transaction.deserialize(
|
|
225
245
|
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
226
246
|
)
|
|
247
|
+
|
|
248
|
+
if (feePayer) assertSenderSigned(transaction)
|
|
249
|
+
|
|
227
250
|
const calls = transaction.calls ?? []
|
|
228
251
|
|
|
229
252
|
const openCall = calls.find((call) => {
|
|
230
|
-
if (!call.to || !
|
|
253
|
+
if (!call.to || !TempoAddress.isEqual(call.to, escrowContract)) return false
|
|
231
254
|
if (!call.data) return false
|
|
232
255
|
return call.data.slice(0, 10) === escrowOpenSelector
|
|
233
256
|
})
|
|
@@ -246,8 +269,9 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
246
269
|
}
|
|
247
270
|
const selector = call.data.slice(0, 10)
|
|
248
271
|
const isEscrowOpen =
|
|
249
|
-
|
|
250
|
-
const isTokenApprove =
|
|
272
|
+
TempoAddress.isEqual(call.to, escrowContract) && selector === escrowOpenSelector
|
|
273
|
+
const isTokenApprove =
|
|
274
|
+
TempoAddress.isEqual(call.to, currency) && selector === erc20ApproveSelector
|
|
251
275
|
if (!isEscrowOpen && !isTokenApprove) {
|
|
252
276
|
throw new BadRequestError({
|
|
253
277
|
reason: 'fee-sponsored open transaction contains an unauthorized call',
|
|
@@ -257,7 +281,7 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
257
281
|
}
|
|
258
282
|
|
|
259
283
|
const { args: openArgs } = decodeFunctionData({ abi: escrowAbi, data: openCall.data! })
|
|
260
|
-
const [payee, token, deposit, , authorizedSigner] = openArgs as readonly [
|
|
284
|
+
const [payee, token, deposit, salt, authorizedSigner] = openArgs as readonly [
|
|
261
285
|
Address,
|
|
262
286
|
Address,
|
|
263
287
|
bigint,
|
|
@@ -265,17 +289,33 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
265
289
|
Address,
|
|
266
290
|
]
|
|
267
291
|
|
|
268
|
-
if (!
|
|
292
|
+
if (!TempoAddress.isEqual(payee, recipient)) {
|
|
269
293
|
throw new VerificationFailedError({
|
|
270
294
|
reason: 'open transaction payee does not match server recipient',
|
|
271
295
|
})
|
|
272
296
|
}
|
|
273
|
-
if (!
|
|
297
|
+
if (!TempoAddress.isEqual(token, currency)) {
|
|
274
298
|
throw new VerificationFailedError({
|
|
275
299
|
reason: 'open transaction token does not match server currency',
|
|
276
300
|
})
|
|
277
301
|
}
|
|
278
302
|
|
|
303
|
+
if (!transaction.from) throw new BadRequestError({ reason: 'open transaction has no sender' })
|
|
304
|
+
|
|
305
|
+
const derivedChannelId = Channel.computeId({
|
|
306
|
+
payer: transaction.from as `0x${string}`,
|
|
307
|
+
payee,
|
|
308
|
+
token,
|
|
309
|
+
salt,
|
|
310
|
+
authorizedSigner,
|
|
311
|
+
escrowContract,
|
|
312
|
+
chainId: client.chain!.id,
|
|
313
|
+
})
|
|
314
|
+
if (derivedChannelId.toLowerCase() !== channelId.toLowerCase())
|
|
315
|
+
throw new VerificationFailedError({
|
|
316
|
+
reason: 'open transaction does not match claimed channelId',
|
|
317
|
+
})
|
|
318
|
+
|
|
279
319
|
const resolvedFeeToken =
|
|
280
320
|
transaction.feeToken ?? defaults.currency[client.chain?.id as keyof typeof defaults.currency]
|
|
281
321
|
|
|
@@ -364,13 +404,21 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
364
404
|
feePayer,
|
|
365
405
|
} = parameters
|
|
366
406
|
|
|
407
|
+
if (feePayer && !isTempoTransaction(serializedTransaction))
|
|
408
|
+
throw new BadRequestError({
|
|
409
|
+
reason: 'Only Tempo (0x76/0x78) transactions are supported',
|
|
410
|
+
})
|
|
411
|
+
|
|
367
412
|
const transaction = Transaction.deserialize(
|
|
368
413
|
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
369
414
|
)
|
|
415
|
+
|
|
416
|
+
if (feePayer) assertSenderSigned(transaction)
|
|
417
|
+
|
|
370
418
|
const calls = transaction.calls ?? []
|
|
371
419
|
|
|
372
420
|
const topUpCall = calls.find((call) => {
|
|
373
|
-
if (!call.to || !
|
|
421
|
+
if (!call.to || !TempoAddress.isEqual(call.to, escrowContract)) return false
|
|
374
422
|
if (!call.data) return false
|
|
375
423
|
return call.data.slice(0, 10) === escrowTopUpSelector
|
|
376
424
|
})
|
|
@@ -389,8 +437,9 @@ export async function broadcastTopUpTransaction(parameters: {
|
|
|
389
437
|
}
|
|
390
438
|
const selector = call.data.slice(0, 10)
|
|
391
439
|
const isEscrowTopUp =
|
|
392
|
-
|
|
393
|
-
const isTokenApprove =
|
|
440
|
+
TempoAddress.isEqual(call.to, escrowContract) && selector === escrowTopUpSelector
|
|
441
|
+
const isTokenApprove =
|
|
442
|
+
TempoAddress.isEqual(call.to, currency) && selector === erc20ApproveSelector
|
|
394
443
|
if (!isEscrowTopUp && !isTokenApprove) {
|
|
395
444
|
throw new BadRequestError({
|
|
396
445
|
reason: 'fee-sponsored topUp transaction contains an unauthorized call',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
2
|
import { describe, expect, test } from 'vitest'
|
|
3
|
+
|
|
3
4
|
import * as Store from '../../Store.js'
|
|
4
5
|
import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
|
|
5
6
|
import * as ChannelStore from './ChannelStore.js'
|
|
@@ -22,6 +23,7 @@ function makeChannel(overrides?: Partial<ChannelStore.State>): ChannelStore.Stat
|
|
|
22
23
|
highestVoucher: null,
|
|
23
24
|
spent: 0n,
|
|
24
25
|
units: 0,
|
|
26
|
+
closeRequestedAt: 0n,
|
|
25
27
|
finalized: false,
|
|
26
28
|
createdAt: '2025-01-01T00:00:00.000Z',
|
|
27
29
|
...overrides,
|
|
@@ -239,6 +241,15 @@ describe('ChannelStore.deductFromChannel', () => {
|
|
|
239
241
|
)
|
|
240
242
|
})
|
|
241
243
|
|
|
244
|
+
test('rejects deduction when channel is finalized', async () => {
|
|
245
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
246
|
+
await seedChannel(cs, { highestVoucherAmount: 10_000_000n, spent: 0n, finalized: true })
|
|
247
|
+
|
|
248
|
+
const result = await ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)
|
|
249
|
+
expect(result.ok).toBe(false)
|
|
250
|
+
expect(result.channel.spent).toBe(0n)
|
|
251
|
+
})
|
|
252
|
+
|
|
242
253
|
test('exact balance succeeds', async () => {
|
|
243
254
|
const cs = ChannelStore.fromStore(Store.memory())
|
|
244
255
|
await seedChannel(cs, { highestVoucherAmount: 1_000_000n, spent: 0n })
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
|
+
|
|
2
3
|
import type * as Store from '../../Store.js'
|
|
3
4
|
import type { SignedVoucher } from './Types.js'
|
|
4
5
|
|
|
@@ -27,6 +28,8 @@ export interface State {
|
|
|
27
28
|
escrowContract: Address
|
|
28
29
|
/** Unique identifier for this payment channel. */
|
|
29
30
|
channelId: Hex
|
|
31
|
+
/** On-chain timestamp when a force-close was requested (0n if not requested). */
|
|
32
|
+
closeRequestedAt: bigint
|
|
30
33
|
/** ISO 8601 timestamp when the channel was created. */
|
|
31
34
|
createdAt: string
|
|
32
35
|
/** Current on-chain deposit in the escrow contract. */
|
|
@@ -107,6 +110,7 @@ export async function deductFromChannel(
|
|
|
107
110
|
const channel = await store.updateChannel(channelId, (current) => {
|
|
108
111
|
deducted = false
|
|
109
112
|
if (!current) return null
|
|
113
|
+
if (current.finalized) return current
|
|
110
114
|
if (current.highestVoucherAmount - current.spent >= amount) {
|
|
111
115
|
deducted = true
|
|
112
116
|
return { ...current, spent: current.spent + amount, units: current.units + 1 }
|
|
@@ -165,9 +169,9 @@ export function fromStore(store: Store.Store): ChannelStore {
|
|
|
165
169
|
)
|
|
166
170
|
|
|
167
171
|
try {
|
|
168
|
-
const current = await store.get
|
|
172
|
+
const current = (await store.get(channelId)) as State | null
|
|
169
173
|
const next = fn(current)
|
|
170
|
-
if (next) await store.put(channelId, next)
|
|
174
|
+
if (next) await store.put(channelId, next as never)
|
|
171
175
|
else await store.delete(channelId)
|
|
172
176
|
return next
|
|
173
177
|
} finally {
|
|
@@ -178,7 +182,7 @@ export function fromStore(store: Store.Store): ChannelStore {
|
|
|
178
182
|
|
|
179
183
|
const cs: ChannelStore = {
|
|
180
184
|
async getChannel(channelId) {
|
|
181
|
-
return store.get
|
|
185
|
+
return (await store.get(channelId)) as State | null
|
|
182
186
|
},
|
|
183
187
|
async updateChannel(channelId, fn) {
|
|
184
188
|
const result = await update(channelId, fn)
|