mppx 0.0.1 → 0.1.0
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/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/BodyDigest.d.ts +42 -0
- package/dist/BodyDigest.d.ts.map +1 -0
- package/dist/BodyDigest.js +40 -0
- package/dist/BodyDigest.js.map +1 -0
- package/dist/Challenge.d.ts +271 -0
- package/dist/Challenge.d.ts.map +1 -0
- package/dist/Challenge.js +291 -0
- package/dist/Challenge.js.map +1 -0
- package/dist/Credential.d.ts +91 -0
- package/dist/Credential.d.ts.map +1 -0
- package/dist/Credential.js +122 -0
- package/dist/Credential.js.map +1 -0
- package/dist/Errors.d.ts +243 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +201 -0
- package/dist/Errors.js.map +1 -0
- package/dist/Expires.d.ts +15 -0
- package/dist/Expires.d.ts.map +1 -0
- package/dist/Expires.js +29 -0
- package/dist/Expires.js.map +1 -0
- package/dist/Intent.d.ts +101 -0
- package/dist/Intent.d.ts.map +1 -0
- package/dist/Intent.js +83 -0
- package/dist/Intent.js.map +1 -0
- package/dist/Mcp.d.ts +74 -0
- package/dist/Mcp.d.ts.map +1 -0
- package/dist/Mcp.js +9 -0
- package/dist/Mcp.js.map +1 -0
- package/dist/MethodIntent.d.ts +225 -0
- package/dist/MethodIntent.d.ts.map +1 -0
- package/dist/MethodIntent.js +156 -0
- package/dist/MethodIntent.js.map +1 -0
- package/dist/PaymentRequest.d.ts +88 -0
- package/dist/PaymentRequest.d.ts.map +1 -0
- package/dist/PaymentRequest.js +81 -0
- package/dist/PaymentRequest.js.map +1 -0
- package/dist/Receipt.d.ts +110 -0
- package/dist/Receipt.d.ts.map +1 -0
- package/dist/Receipt.js +105 -0
- package/dist/Receipt.js.map +1 -0
- package/dist/Store.d.ts +28 -0
- package/dist/Store.d.ts.map +1 -0
- package/dist/Store.js +61 -0
- package/dist/Store.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1219 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/Methods.d.ts +4 -0
- package/dist/client/Methods.d.ts.map +1 -0
- package/dist/client/Methods.js +4 -0
- package/dist/client/Methods.js.map +1 -0
- package/dist/client/Mppx.d.ts +84 -0
- package/dist/client/Mppx.d.ts.map +1 -0
- package/dist/client/Mppx.js +64 -0
- package/dist/client/Mppx.js.map +1 -0
- package/dist/client/Transport.d.ts +56 -0
- package/dist/client/Transport.d.ts.map +1 -0
- package/dist/client/Transport.js +81 -0
- package/dist/client/Transport.js.map +1 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/internal/Fetch.d.ts +85 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -0
- package/dist/client/internal/Fetch.js +95 -0
- package/dist/client/internal/Fetch.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/types.d.ts +302 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +2 -0
- package/dist/internal/types.js.map +1 -0
- package/dist/mcp-sdk/client/McpClient.d.ts +78 -0
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -0
- package/dist/mcp-sdk/client/McpClient.js +98 -0
- package/dist/mcp-sdk/client/McpClient.js.map +1 -0
- package/dist/mcp-sdk/client/index.d.ts +3 -0
- package/dist/mcp-sdk/client/index.d.ts.map +1 -0
- package/dist/mcp-sdk/client/index.js +3 -0
- package/dist/mcp-sdk/client/index.js.map +1 -0
- package/dist/mcp-sdk/server/Transport.d.ts +43 -0
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -0
- package/dist/mcp-sdk/server/Transport.js +71 -0
- package/dist/mcp-sdk/server/Transport.js.map +1 -0
- package/dist/mcp-sdk/server/index.d.ts +2 -0
- package/dist/mcp-sdk/server/index.d.ts.map +1 -0
- package/dist/mcp-sdk/server/index.js +2 -0
- package/dist/mcp-sdk/server/index.js.map +1 -0
- package/dist/middlewares/elysia.d.ts +51 -0
- package/dist/middlewares/elysia.d.ts.map +1 -0
- package/dist/middlewares/elysia.js +59 -0
- package/dist/middlewares/elysia.js.map +1 -0
- package/dist/middlewares/express.d.ts +46 -0
- package/dist/middlewares/express.d.ts.map +1 -0
- package/dist/middlewares/express.js +69 -0
- package/dist/middlewares/express.js.map +1 -0
- package/dist/middlewares/hono.d.ts +46 -0
- package/dist/middlewares/hono.d.ts.map +1 -0
- package/dist/middlewares/hono.js +57 -0
- package/dist/middlewares/hono.js.map +1 -0
- package/dist/middlewares/internal/mppx.d.ts +16 -0
- package/dist/middlewares/internal/mppx.d.ts.map +1 -0
- package/dist/middlewares/internal/mppx.js +16 -0
- package/dist/middlewares/internal/mppx.js.map +1 -0
- package/dist/middlewares/nextjs.d.ts +45 -0
- package/dist/middlewares/nextjs.d.ts.map +1 -0
- package/dist/middlewares/nextjs.js +57 -0
- package/dist/middlewares/nextjs.js.map +1 -0
- package/dist/proxy/Proxy.d.ts +47 -0
- package/dist/proxy/Proxy.d.ts.map +1 -0
- package/dist/proxy/Proxy.js +126 -0
- package/dist/proxy/Proxy.js.map +1 -0
- package/dist/proxy/Service.d.ts +100 -0
- package/dist/proxy/Service.d.ts.map +1 -0
- package/dist/proxy/Service.js +147 -0
- package/dist/proxy/Service.js.map +1 -0
- package/dist/proxy/index.d.ts +7 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +7 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/internal/Headers.d.ts +3 -0
- package/dist/proxy/internal/Headers.d.ts.map +1 -0
- package/dist/proxy/internal/Headers.js +41 -0
- package/dist/proxy/internal/Headers.js.map +1 -0
- package/dist/proxy/internal/Route.d.ts +14 -0
- package/dist/proxy/internal/Route.d.ts.map +1 -0
- package/dist/proxy/internal/Route.js +47 -0
- package/dist/proxy/internal/Route.js.map +1 -0
- package/dist/proxy/services/anthropic.d.ts +29 -0
- package/dist/proxy/services/anthropic.d.ts.map +1 -0
- package/dist/proxy/services/anthropic.js +30 -0
- package/dist/proxy/services/anthropic.js.map +1 -0
- package/dist/proxy/services/openai.d.ts +29 -0
- package/dist/proxy/services/openai.d.ts.map +1 -0
- package/dist/proxy/services/openai.js +30 -0
- package/dist/proxy/services/openai.js.map +1 -0
- package/dist/proxy/services/stripe.d.ts +29 -0
- package/dist/proxy/services/stripe.d.ts.map +1 -0
- package/dist/proxy/services/stripe.js +30 -0
- package/dist/proxy/services/stripe.js.map +1 -0
- package/dist/server/Methods.d.ts +3 -0
- package/dist/server/Methods.d.ts.map +1 -0
- package/dist/server/Methods.js +3 -0
- package/dist/server/Methods.js.map +1 -0
- package/dist/server/Mppx.d.ts +116 -0
- package/dist/server/Mppx.d.ts.map +1 -0
- package/dist/server/Mppx.js +207 -0
- package/dist/server/Mppx.js.map +1 -0
- package/dist/server/NodeListener.d.ts +3 -0
- package/dist/server/NodeListener.d.ts.map +1 -0
- package/dist/server/NodeListener.js +3 -0
- package/dist/server/NodeListener.js.map +1 -0
- package/dist/server/Request.d.ts +24 -0
- package/dist/server/Request.d.ts.map +1 -0
- package/dist/server/Request.js +26 -0
- package/dist/server/Request.js.map +1 -0
- package/dist/server/Response.d.ts +10 -0
- package/dist/server/Response.d.ts.map +1 -0
- package/dist/server/Response.js +15 -0
- package/dist/server/Response.js.map +1 -0
- package/dist/server/Transport.d.ts +93 -0
- package/dist/server/Transport.d.ts.map +1 -0
- package/dist/server/Transport.js +132 -0
- package/dist/server/Transport.js.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -0
- package/dist/stripe/Intents.d.ts +54 -0
- package/dist/stripe/Intents.d.ts.map +1 -0
- package/dist/stripe/Intents.js +27 -0
- package/dist/stripe/Intents.js.map +1 -0
- package/dist/stripe/client/Charge.d.ts +114 -0
- package/dist/stripe/client/Charge.d.ts.map +1 -0
- package/dist/stripe/client/Charge.js +77 -0
- package/dist/stripe/client/Charge.js.map +1 -0
- package/dist/stripe/client/MethodIntents.d.ts +80 -0
- package/dist/stripe/client/MethodIntents.d.ts.map +1 -0
- package/dist/stripe/client/MethodIntents.js +34 -0
- package/dist/stripe/client/MethodIntents.js.map +1 -0
- package/dist/stripe/client/index.d.ts +3 -0
- package/dist/stripe/client/index.d.ts.map +1 -0
- package/dist/stripe/client/index.js +3 -0
- package/dist/stripe/client/index.js.map +1 -0
- package/dist/stripe/index.d.ts +2 -0
- package/dist/stripe/index.d.ts.map +1 -0
- package/dist/stripe/index.js +2 -0
- package/dist/stripe/index.js.map +1 -0
- package/dist/stripe/server/Charge.d.ts +74 -0
- package/dist/stripe/server/Charge.d.ts.map +1 -0
- package/dist/stripe/server/Charge.js +79 -0
- package/dist/stripe/server/Charge.js.map +1 -0
- package/dist/stripe/server/MethodIntents.d.ts +65 -0
- package/dist/stripe/server/MethodIntents.d.ts.map +1 -0
- package/dist/stripe/server/MethodIntents.js +21 -0
- package/dist/stripe/server/MethodIntents.js.map +1 -0
- package/dist/stripe/server/index.d.ts +3 -0
- package/dist/stripe/server/index.d.ts.map +1 -0
- package/dist/stripe/server/index.js +3 -0
- package/dist/stripe/server/index.js.map +1 -0
- package/dist/tempo/Attribution.d.ts +101 -0
- package/dist/tempo/Attribution.d.ts.map +1 -0
- package/dist/tempo/Attribution.js +124 -0
- package/dist/tempo/Attribution.js.map +1 -0
- package/dist/tempo/Intents.d.ts +132 -0
- package/dist/tempo/Intents.d.ts.map +1 -0
- package/dist/tempo/Intents.js +81 -0
- package/dist/tempo/Intents.js.map +1 -0
- package/dist/tempo/client/ChannelOps.d.ts +54 -0
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -0
- package/dist/tempo/client/ChannelOps.js +138 -0
- package/dist/tempo/client/ChannelOps.js.map +1 -0
- package/dist/tempo/client/Charge.d.ts +76 -0
- package/dist/tempo/client/Charge.d.ts.map +1 -0
- package/dist/tempo/client/Charge.js +69 -0
- package/dist/tempo/client/Charge.js.map +1 -0
- package/dist/tempo/client/MethodIntents.d.ts +157 -0
- package/dist/tempo/client/MethodIntents.d.ts.map +1 -0
- package/dist/tempo/client/MethodIntents.js +25 -0
- package/dist/tempo/client/MethodIntents.js.map +1 -0
- package/dist/tempo/client/Session.d.ts +159 -0
- package/dist/tempo/client/Session.d.ts.map +1 -0
- package/dist/tempo/client/Session.js +263 -0
- package/dist/tempo/client/Session.js.map +1 -0
- package/dist/tempo/client/SessionManager.d.ts +62 -0
- package/dist/tempo/client/SessionManager.d.ts.map +1 -0
- package/dist/tempo/client/SessionManager.js +196 -0
- package/dist/tempo/client/SessionManager.js.map +1 -0
- package/dist/tempo/client/index.d.ts +6 -0
- package/dist/tempo/client/index.d.ts.map +1 -0
- package/dist/tempo/client/index.js +5 -0
- package/dist/tempo/client/index.js.map +1 -0
- package/dist/tempo/index.d.ts +3 -0
- package/dist/tempo/index.d.ts.map +1 -0
- package/dist/tempo/index.js +3 -0
- package/dist/tempo/index.js.map +1 -0
- package/dist/tempo/internal/account.d.ts +32 -0
- package/dist/tempo/internal/account.d.ts.map +1 -0
- package/dist/tempo/internal/account.js +33 -0
- package/dist/tempo/internal/account.js.map +1 -0
- package/dist/tempo/internal/defaults.d.ts +18 -0
- package/dist/tempo/internal/defaults.d.ts.map +1 -0
- package/dist/tempo/internal/defaults.js +18 -0
- package/dist/tempo/internal/defaults.js.map +1 -0
- package/dist/tempo/internal/types.d.ts +11 -0
- package/dist/tempo/internal/types.d.ts.map +1 -0
- package/dist/tempo/internal/types.js +2 -0
- package/dist/tempo/internal/types.js.map +1 -0
- package/dist/tempo/server/Charge.d.ts +77 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -0
- package/dist/tempo/server/Charge.js +228 -0
- package/dist/tempo/server/Charge.js.map +1 -0
- package/dist/tempo/server/MethodIntents.d.ts +140 -0
- package/dist/tempo/server/MethodIntents.d.ts.map +1 -0
- package/dist/tempo/server/MethodIntents.js +26 -0
- package/dist/tempo/server/MethodIntents.js.map +1 -0
- package/dist/tempo/server/Session.d.ts +148 -0
- package/dist/tempo/server/Session.d.ts.map +1 -0
- package/dist/tempo/server/Session.js +529 -0
- package/dist/tempo/server/Session.js.map +1 -0
- package/dist/tempo/server/internal/transport.d.ts +47 -0
- package/dist/tempo/server/internal/transport.d.ts.map +1 -0
- package/dist/tempo/server/internal/transport.js +118 -0
- package/dist/tempo/server/internal/transport.js.map +1 -0
- package/dist/tempo/stream/Chain.d.ts +52 -0
- package/dist/tempo/stream/Chain.d.ts.map +1 -0
- package/dist/tempo/stream/Chain.js +215 -0
- package/dist/tempo/stream/Chain.js.map +1 -0
- package/dist/tempo/stream/Channel.d.ts +26 -0
- package/dist/tempo/stream/Channel.d.ts.map +1 -0
- package/dist/tempo/stream/Channel.js +27 -0
- package/dist/tempo/stream/Channel.js.map +1 -0
- package/dist/tempo/stream/ChannelStore.d.ts +103 -0
- package/dist/tempo/stream/ChannelStore.d.ts.map +1 -0
- package/dist/tempo/stream/ChannelStore.js +100 -0
- package/dist/tempo/stream/ChannelStore.js.map +1 -0
- package/dist/tempo/stream/Receipt.d.ts +22 -0
- package/dist/tempo/stream/Receipt.d.ts.map +1 -0
- package/dist/tempo/stream/Receipt.js +34 -0
- package/dist/tempo/stream/Receipt.js.map +1 -0
- package/dist/tempo/stream/Sse.d.ts +134 -0
- package/dist/tempo/stream/Sse.d.ts.map +1 -0
- package/dist/tempo/stream/Sse.js +288 -0
- package/dist/tempo/stream/Sse.js.map +1 -0
- package/dist/tempo/stream/Types.d.ts +78 -0
- package/dist/tempo/stream/Types.d.ts.map +1 -0
- package/dist/tempo/stream/Types.js +2 -0
- package/dist/tempo/stream/Types.js.map +1 -0
- package/dist/tempo/stream/Voucher.d.ts +20 -0
- package/dist/tempo/stream/Voucher.d.ts.map +1 -0
- package/dist/tempo/stream/Voucher.js +98 -0
- package/dist/tempo/stream/Voucher.js.map +1 -0
- package/dist/tempo/stream/escrow.abi.d.ts +598 -0
- package/dist/tempo/stream/escrow.abi.d.ts.map +1 -0
- package/dist/tempo/stream/escrow.abi.js +760 -0
- package/dist/tempo/stream/escrow.abi.js.map +1 -0
- package/dist/tempo/stream/index.d.ts +8 -0
- package/dist/tempo/stream/index.d.ts.map +1 -0
- package/dist/tempo/stream/index.js +8 -0
- package/dist/tempo/stream/index.js.map +1 -0
- package/dist/viem/Account.d.ts +12 -0
- package/dist/viem/Account.d.ts.map +1 -0
- package/dist/viem/Account.js +14 -0
- package/dist/viem/Account.js.map +1 -0
- package/dist/viem/Client.d.ts +21 -0
- package/dist/viem/Client.d.ts.map +1 -0
- package/dist/viem/Client.js +19 -0
- package/dist/viem/Client.js.map +1 -0
- package/dist/zod.d.ts +17 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +35 -0
- package/dist/zod.js.map +1 -0
- package/package.json +117 -4
- package/src/BodyDigest.test.ts +43 -0
- package/src/BodyDigest.ts +53 -0
- package/src/Challenge.test-d.ts +81 -0
- package/src/Challenge.test.ts +414 -0
- package/src/Challenge.ts +429 -0
- package/src/Credential.test.ts +227 -0
- package/src/Credential.ts +154 -0
- package/src/Errors.test.ts +402 -0
- package/src/Errors.ts +348 -0
- package/src/Expires.ts +34 -0
- package/src/Intent.test.ts +180 -0
- package/src/Intent.ts +109 -0
- package/src/Mcp.ts +81 -0
- package/src/MethodIntent.test.ts +303 -0
- package/src/MethodIntent.ts +388 -0
- package/src/PaymentRequest.test.ts +152 -0
- package/src/PaymentRequest.ts +107 -0
- package/src/Receipt.test.ts +98 -0
- package/src/Receipt.ts +129 -0
- package/src/Store.ts +84 -0
- package/src/cli.test.ts +542 -0
- package/src/cli.ts +1319 -0
- package/src/client/Methods.ts +3 -0
- package/src/client/Mppx.test-d.ts +90 -0
- package/src/client/Mppx.test.ts +468 -0
- package/src/client/Mppx.ts +149 -0
- package/src/client/Transport.test.ts +283 -0
- package/src/client/Transport.ts +115 -0
- package/src/client/index.ts +4 -0
- package/src/client/internal/Fetch.test-d.ts +57 -0
- package/src/client/internal/Fetch.test.ts +281 -0
- package/src/client/internal/Fetch.ts +157 -0
- package/src/env.d.ts +11 -0
- package/src/index.ts +12 -0
- package/src/internal/types.ts +403 -0
- package/src/mcp-sdk/client/McpClient.test-d.ts +109 -0
- package/src/mcp-sdk/client/McpClient.test.ts +219 -0
- package/src/mcp-sdk/client/McpClient.ts +187 -0
- package/src/mcp-sdk/client/index.ts +2 -0
- package/src/mcp-sdk/server/Transport.ts +94 -0
- package/src/mcp-sdk/server/index.ts +1 -0
- package/src/middlewares/elysia.ts +66 -0
- package/src/middlewares/express.test.ts +155 -0
- package/src/middlewares/express.ts +82 -0
- package/src/middlewares/hono.test.ts +148 -0
- package/src/middlewares/hono.ts +62 -0
- package/src/middlewares/internal/mppx.ts +30 -0
- package/src/middlewares/nextjs.test.ts +164 -0
- package/src/middlewares/nextjs.ts +66 -0
- package/src/proxy/Proxy.test.ts +472 -0
- package/src/proxy/Proxy.ts +175 -0
- package/src/proxy/Service.test.ts +125 -0
- package/src/proxy/Service.ts +227 -0
- package/src/proxy/index.ts +6 -0
- package/src/proxy/internal/Headers.test.ts +100 -0
- package/src/proxy/internal/Headers.ts +40 -0
- package/src/proxy/internal/Route.test.ts +143 -0
- package/src/proxy/internal/Route.ts +54 -0
- package/src/proxy/services/anthropic.ts +45 -0
- package/src/proxy/services/openai.test.ts +97 -0
- package/src/proxy/services/openai.ts +48 -0
- package/src/proxy/services/stripe.ts +49 -0
- package/src/server/Methods.ts +2 -0
- package/src/server/Mppx.test-d.ts +343 -0
- package/src/server/Mppx.test.ts +342 -0
- package/src/server/Mppx.ts +378 -0
- package/src/server/NodeListener.test.ts +188 -0
- package/src/server/NodeListener.ts +3 -0
- package/src/server/Request.test.ts +102 -0
- package/src/server/Request.ts +33 -0
- package/src/server/Response.test.ts +31 -0
- package/src/server/Response.ts +27 -0
- package/src/server/Transport.test.ts +294 -0
- package/src/server/Transport.ts +222 -0
- package/src/server/index.ts +8 -0
- package/src/stripe/Charge.integration.test.ts +326 -0
- package/src/stripe/Intents.test.ts +52 -0
- package/src/stripe/Intents.ts +27 -0
- package/src/stripe/client/Charge.ts +119 -0
- package/src/stripe/client/MethodIntents.ts +37 -0
- package/src/stripe/client/index.ts +2 -0
- package/src/stripe/index.ts +1 -0
- package/src/stripe/server/Charge.ts +121 -0
- package/src/stripe/server/MethodIntents.ts +24 -0
- package/src/stripe/server/index.ts +2 -0
- package/src/tempo/Attribution.test.ts +187 -0
- package/src/tempo/Attribution.ts +156 -0
- package/src/tempo/Intents.test.ts +84 -0
- package/src/tempo/Intents.ts +93 -0
- package/src/tempo/client/ChannelOps.ts +233 -0
- package/src/tempo/client/Charge.ts +84 -0
- package/src/tempo/client/MethodIntents.ts +28 -0
- package/src/tempo/client/Session.ts +369 -0
- package/src/tempo/client/SessionManager.test.ts +223 -0
- package/src/tempo/client/SessionManager.ts +270 -0
- package/src/tempo/client/index.ts +5 -0
- package/src/tempo/index.ts +2 -0
- package/src/tempo/internal/account.ts +47 -0
- package/src/tempo/internal/defaults.ts +20 -0
- package/src/tempo/internal/types.ts +8 -0
- package/src/tempo/server/Charge.test.ts +847 -0
- package/src/tempo/server/Charge.ts +309 -0
- package/src/tempo/server/MethodIntents.ts +29 -0
- package/src/tempo/server/Session.test.ts +1349 -0
- package/src/tempo/server/Session.ts +773 -0
- package/src/tempo/server/Sse.test.ts +289 -0
- package/src/tempo/server/index.ts +5 -0
- package/src/tempo/server/internal/transport.ts +153 -0
- package/src/tempo/stream/Chain.ts +333 -0
- package/src/tempo/stream/Channel.ts +50 -0
- package/src/tempo/stream/ChannelStore.test.ts +473 -0
- package/src/tempo/stream/ChannelStore.ts +202 -0
- package/src/tempo/stream/Receipt.test.ts +84 -0
- package/src/tempo/stream/Receipt.ts +45 -0
- package/src/tempo/stream/Sse.test.ts +401 -0
- package/src/tempo/stream/Sse.ts +375 -0
- package/src/tempo/stream/Types.ts +86 -0
- package/src/tempo/stream/Voucher.test.ts +134 -0
- package/src/tempo/stream/Voucher.ts +123 -0
- package/src/tempo/stream/escrow.abi.ts +759 -0
- package/src/tempo/stream/index.ts +7 -0
- package/src/tsconfig.json +10 -0
- package/src/viem/Account.test.ts +71 -0
- package/src/viem/Account.ts +30 -0
- package/src/viem/Client.test.ts +58 -0
- package/src/viem/Client.ts +33 -0
- package/src/zod.ts +47 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import type { Address, Hex } from 'viem'
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import * as Store from '../../Store.js'
|
|
4
|
+
import * as ChannelStore from './ChannelStore.js'
|
|
5
|
+
|
|
6
|
+
const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
7
|
+
const channelId2 = '0x0000000000000000000000000000000000000000000000000000000000000002' as Hex
|
|
8
|
+
|
|
9
|
+
function makeChannel(overrides?: Partial<ChannelStore.State>): ChannelStore.State {
|
|
10
|
+
return {
|
|
11
|
+
channelId,
|
|
12
|
+
payer: '0x0000000000000000000000000000000000000001' as Address,
|
|
13
|
+
payee: '0x0000000000000000000000000000000000000002' as Address,
|
|
14
|
+
token: '0x0000000000000000000000000000000000000003' as Address,
|
|
15
|
+
authorizedSigner: '0x0000000000000000000000000000000000000004' as Address,
|
|
16
|
+
chainId: 42431,
|
|
17
|
+
escrowContract: '0x542831e3E4Ace07559b7C8787395f4Fb99F70787' as Address,
|
|
18
|
+
deposit: 10_000_000n,
|
|
19
|
+
settledOnChain: 0n,
|
|
20
|
+
highestVoucherAmount: 10_000_000n,
|
|
21
|
+
highestVoucher: null,
|
|
22
|
+
spent: 0n,
|
|
23
|
+
units: 0,
|
|
24
|
+
finalized: false,
|
|
25
|
+
createdAt: '2025-01-01T00:00:00.000Z',
|
|
26
|
+
...overrides,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function seedChannel(
|
|
31
|
+
store: ChannelStore.ChannelStore,
|
|
32
|
+
overrides?: Partial<ChannelStore.State>,
|
|
33
|
+
): Promise<ChannelStore.State | null> {
|
|
34
|
+
return store.updateChannel(channelId, () => makeChannel(overrides))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stripUpdateMethod(store: Store.Store): Store.Store {
|
|
38
|
+
return {
|
|
39
|
+
get: store.get.bind(store),
|
|
40
|
+
put: store.put.bind(store),
|
|
41
|
+
delete: store.delete.bind(store),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function delayedStore(delayMs: number): Store.Store {
|
|
46
|
+
const store = new Map<string, unknown>()
|
|
47
|
+
return {
|
|
48
|
+
async get(key) {
|
|
49
|
+
await sleep(delayMs)
|
|
50
|
+
return (store.get(key) ?? null) as any
|
|
51
|
+
},
|
|
52
|
+
async put(key, value) {
|
|
53
|
+
await sleep(delayMs)
|
|
54
|
+
store.set(key, value)
|
|
55
|
+
},
|
|
56
|
+
async delete(key) {
|
|
57
|
+
await sleep(delayMs)
|
|
58
|
+
store.delete(key)
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sleep(ms: number): Promise<void> {
|
|
64
|
+
return new Promise((r) => setTimeout(r, ms))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------- Store.memory ----------
|
|
68
|
+
|
|
69
|
+
describe('Store.memory', () => {
|
|
70
|
+
test('get returns null for missing key', async () => {
|
|
71
|
+
const s = Store.memory()
|
|
72
|
+
expect(await s.get('missing')).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('put then get returns value', async () => {
|
|
76
|
+
const s = Store.memory()
|
|
77
|
+
const ch = makeChannel()
|
|
78
|
+
await s.put('k', ch)
|
|
79
|
+
const result = await s.get('k')
|
|
80
|
+
expect(result).toEqual(ch)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('delete removes key', async () => {
|
|
84
|
+
const s = Store.memory()
|
|
85
|
+
await s.put('k', makeChannel())
|
|
86
|
+
await s.delete('k')
|
|
87
|
+
expect(await s.get('k')).toBeNull()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// ---------- channelStore ----------
|
|
92
|
+
|
|
93
|
+
describe('channelStore', () => {
|
|
94
|
+
describe('getChannel', () => {
|
|
95
|
+
test('returns null for missing channel', async () => {
|
|
96
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
97
|
+
expect(await cs.getChannel(channelId)).toBeNull()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('returns channel after update', async () => {
|
|
101
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
102
|
+
const ch = makeChannel()
|
|
103
|
+
await cs.updateChannel(channelId, () => ch)
|
|
104
|
+
|
|
105
|
+
const loaded = await cs.getChannel(channelId)
|
|
106
|
+
expect(loaded).not.toBeNull()
|
|
107
|
+
expect(loaded!.channelId).toBe(channelId)
|
|
108
|
+
expect(loaded!.deposit).toBe(10_000_000n)
|
|
109
|
+
expect(typeof loaded!.deposit).toBe('bigint')
|
|
110
|
+
expect(typeof loaded!.createdAt).toBe('string')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('updateChannel', () => {
|
|
115
|
+
test('creates channel from null', async () => {
|
|
116
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
117
|
+
const result = await cs.updateChannel(channelId, (current) => {
|
|
118
|
+
expect(current).toBeNull()
|
|
119
|
+
return makeChannel()
|
|
120
|
+
})
|
|
121
|
+
expect(result).not.toBeNull()
|
|
122
|
+
expect(result!.deposit).toBe(10_000_000n)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('updates existing channel', async () => {
|
|
126
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
127
|
+
await seedChannel(cs)
|
|
128
|
+
|
|
129
|
+
const result = await cs.updateChannel(channelId, (current) => {
|
|
130
|
+
return { ...current!, spent: current!.spent + 1_000_000n, units: current!.units + 1 }
|
|
131
|
+
})
|
|
132
|
+
expect(result!.spent).toBe(1_000_000n)
|
|
133
|
+
expect(result!.units).toBe(1)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('returning null deletes channel', async () => {
|
|
137
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
138
|
+
await seedChannel(cs)
|
|
139
|
+
|
|
140
|
+
const result = await cs.updateChannel(channelId, () => null)
|
|
141
|
+
expect(result).toBeNull()
|
|
142
|
+
expect(await cs.getChannel(channelId)).toBeNull()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('preserves bigint fields', async () => {
|
|
146
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
147
|
+
const ch = makeChannel({
|
|
148
|
+
deposit: 999_999_999_999_999_999n,
|
|
149
|
+
settledOnChain: 123_456_789n,
|
|
150
|
+
highestVoucherAmount: 888_888_888n,
|
|
151
|
+
spent: 42n,
|
|
152
|
+
})
|
|
153
|
+
await cs.updateChannel(channelId, () => ch)
|
|
154
|
+
|
|
155
|
+
const loaded = await cs.getChannel(channelId)
|
|
156
|
+
expect(loaded!.deposit).toBe(999_999_999_999_999_999n)
|
|
157
|
+
expect(loaded!.settledOnChain).toBe(123_456_789n)
|
|
158
|
+
expect(loaded!.highestVoucherAmount).toBe(888_888_888n)
|
|
159
|
+
expect(loaded!.spent).toBe(42n)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('waitForUpdate', () => {
|
|
164
|
+
test('resolves on next updateChannel call', async () => {
|
|
165
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
166
|
+
await seedChannel(cs)
|
|
167
|
+
|
|
168
|
+
let resolved = false
|
|
169
|
+
const waiter = cs.waitForUpdate!(channelId).then(() => {
|
|
170
|
+
resolved = true
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
await sleep(10)
|
|
174
|
+
expect(resolved).toBe(false)
|
|
175
|
+
|
|
176
|
+
await cs.updateChannel(channelId, (c) => (c ? { ...c, spent: 1n } : null))
|
|
177
|
+
await waiter
|
|
178
|
+
expect(resolved).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('multiple waiters all resolve', async () => {
|
|
182
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
183
|
+
await seedChannel(cs)
|
|
184
|
+
|
|
185
|
+
let count = 0
|
|
186
|
+
const w1 = cs.waitForUpdate!(channelId).then(() => count++)
|
|
187
|
+
const w2 = cs.waitForUpdate!(channelId).then(() => count++)
|
|
188
|
+
const w3 = cs.waitForUpdate!(channelId).then(() => count++)
|
|
189
|
+
|
|
190
|
+
await cs.updateChannel(channelId, (c) => (c ? { ...c, spent: 1n } : null))
|
|
191
|
+
await Promise.all([w1, w2, w3])
|
|
192
|
+
expect(count).toBe(3)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('different channels are independent', async () => {
|
|
196
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
197
|
+
await seedChannel(cs)
|
|
198
|
+
await cs.updateChannel(channelId2, () => makeChannel({ channelId: channelId2 }))
|
|
199
|
+
|
|
200
|
+
let ch1Resolved = false
|
|
201
|
+
cs.waitForUpdate!(channelId).then(() => {
|
|
202
|
+
ch1Resolved = true
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
await cs.updateChannel(channelId2, (c) => (c ? { ...c, spent: 1n } : null))
|
|
206
|
+
await sleep(10)
|
|
207
|
+
expect(ch1Resolved).toBe(false)
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// ---------- ChannelStore.deductFromChannel ----------
|
|
213
|
+
|
|
214
|
+
describe('ChannelStore.deductFromChannel', () => {
|
|
215
|
+
test('deducts when balance is sufficient', async () => {
|
|
216
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
217
|
+
await seedChannel(cs, { highestVoucherAmount: 5_000_000n, spent: 0n })
|
|
218
|
+
|
|
219
|
+
const result = await ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)
|
|
220
|
+
expect(result.ok).toBe(true)
|
|
221
|
+
expect(result.channel.spent).toBe(1_000_000n)
|
|
222
|
+
expect(result.channel.units).toBe(1)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('returns ok: false when balance insufficient', async () => {
|
|
226
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
227
|
+
await seedChannel(cs, { highestVoucherAmount: 1_000_000n, spent: 500_000n })
|
|
228
|
+
|
|
229
|
+
const result = await ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)
|
|
230
|
+
expect(result.ok).toBe(false)
|
|
231
|
+
expect(result.channel.spent).toBe(500_000n)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('throws when channel does not exist', async () => {
|
|
235
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
236
|
+
await expect(ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)).rejects.toThrow(
|
|
237
|
+
'channel not found',
|
|
238
|
+
)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('exact balance succeeds', async () => {
|
|
242
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
243
|
+
await seedChannel(cs, { highestVoucherAmount: 1_000_000n, spent: 0n })
|
|
244
|
+
|
|
245
|
+
const result = await ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)
|
|
246
|
+
expect(result.ok).toBe(true)
|
|
247
|
+
expect(result.channel.spent).toBe(1_000_000n)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('sequential deductions accumulate correctly', async () => {
|
|
251
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
252
|
+
await seedChannel(cs, { highestVoucherAmount: 5_000_000n, spent: 0n })
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < 5; i++) {
|
|
255
|
+
const result = await ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)
|
|
256
|
+
expect(result.ok).toBe(true)
|
|
257
|
+
expect(result.channel.spent).toBe(BigInt((i + 1) * 1_000_000))
|
|
258
|
+
expect(result.channel.units).toBe(i + 1)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const final = await ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)
|
|
262
|
+
expect(final.ok).toBe(false)
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// ---------- Concurrency ----------
|
|
267
|
+
|
|
268
|
+
describe('concurrency', () => {
|
|
269
|
+
describe('with update (atomic backend)', () => {
|
|
270
|
+
test('concurrent deductions do not lose updates', async () => {
|
|
271
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
272
|
+
await seedChannel(cs, { highestVoucherAmount: 100_000_000n, spent: 0n })
|
|
273
|
+
|
|
274
|
+
const N = 50
|
|
275
|
+
const results = await Promise.all(
|
|
276
|
+
Array.from({ length: N }, () => ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
const successes = results.filter((r) => r.ok).length
|
|
280
|
+
expect(successes).toBe(N)
|
|
281
|
+
|
|
282
|
+
const channel = await cs.getChannel(channelId)
|
|
283
|
+
expect(channel!.spent).toBe(BigInt(N * 1_000_000))
|
|
284
|
+
expect(channel!.units).toBe(N)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('concurrent deductions respect balance limit', async () => {
|
|
288
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
289
|
+
await seedChannel(cs, { highestVoucherAmount: 3_000_000n, spent: 0n })
|
|
290
|
+
|
|
291
|
+
const N = 10
|
|
292
|
+
const results = await Promise.all(
|
|
293
|
+
Array.from({ length: N }, () => ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
const successes = results.filter((r) => r.ok).length
|
|
297
|
+
const failures = results.filter((r) => !r.ok).length
|
|
298
|
+
expect(successes).toBe(3)
|
|
299
|
+
expect(failures).toBe(7)
|
|
300
|
+
|
|
301
|
+
const channel = await cs.getChannel(channelId)
|
|
302
|
+
expect(channel!.spent).toBe(3_000_000n)
|
|
303
|
+
expect(channel!.units).toBe(3)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('concurrent updates to different channels are independent', async () => {
|
|
307
|
+
const cs = ChannelStore.fromStore(Store.memory())
|
|
308
|
+
await seedChannel(cs, { highestVoucherAmount: 10_000_000n, spent: 0n })
|
|
309
|
+
await cs.updateChannel(channelId2, () =>
|
|
310
|
+
makeChannel({ channelId: channelId2, highestVoucherAmount: 10_000_000n, spent: 0n }),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
const N = 20
|
|
314
|
+
const [results1, results2] = await Promise.all([
|
|
315
|
+
Promise.all(
|
|
316
|
+
Array.from({ length: N }, () =>
|
|
317
|
+
ChannelStore.deductFromChannel(cs, channelId, 1_000_000n),
|
|
318
|
+
),
|
|
319
|
+
),
|
|
320
|
+
Promise.all(
|
|
321
|
+
Array.from({ length: N }, () =>
|
|
322
|
+
ChannelStore.deductFromChannel(cs, channelId2, 1_000_000n),
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
])
|
|
326
|
+
|
|
327
|
+
expect(results1.filter((r) => r.ok).length).toBe(10)
|
|
328
|
+
expect(results2.filter((r) => r.ok).length).toBe(10)
|
|
329
|
+
|
|
330
|
+
const ch1 = await cs.getChannel(channelId)
|
|
331
|
+
const ch2 = await cs.getChannel(channelId2)
|
|
332
|
+
expect(ch1!.spent).toBe(10_000_000n)
|
|
333
|
+
expect(ch2!.spent).toBe(10_000_000n)
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('with mutex fallback (no update method)', () => {
|
|
338
|
+
test('concurrent deductions do not lose updates', async () => {
|
|
339
|
+
const cs = ChannelStore.fromStore(stripUpdateMethod(Store.memory()))
|
|
340
|
+
await seedChannel(cs, { highestVoucherAmount: 100_000_000n, spent: 0n })
|
|
341
|
+
|
|
342
|
+
const N = 50
|
|
343
|
+
const results = await Promise.all(
|
|
344
|
+
Array.from({ length: N }, () => ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
const successes = results.filter((r) => r.ok).length
|
|
348
|
+
expect(successes).toBe(N)
|
|
349
|
+
|
|
350
|
+
const channel = await cs.getChannel(channelId)
|
|
351
|
+
expect(channel!.spent).toBe(BigInt(N * 1_000_000))
|
|
352
|
+
expect(channel!.units).toBe(N)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test('concurrent deductions respect balance limit', async () => {
|
|
356
|
+
const cs = ChannelStore.fromStore(stripUpdateMethod(Store.memory()))
|
|
357
|
+
await seedChannel(cs, { highestVoucherAmount: 3_000_000n, spent: 0n })
|
|
358
|
+
|
|
359
|
+
const N = 10
|
|
360
|
+
const results = await Promise.all(
|
|
361
|
+
Array.from({ length: N }, () => ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
const successes = results.filter((r) => r.ok).length
|
|
365
|
+
const failures = results.filter((r) => !r.ok).length
|
|
366
|
+
expect(successes).toBe(3)
|
|
367
|
+
expect(failures).toBe(7)
|
|
368
|
+
|
|
369
|
+
const channel = await cs.getChannel(channelId)
|
|
370
|
+
expect(channel!.spent).toBe(3_000_000n)
|
|
371
|
+
expect(channel!.units).toBe(3)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test('mutex serializes async operations', async () => {
|
|
375
|
+
const s = delayedStore(5)
|
|
376
|
+
const cs = ChannelStore.fromStore(s)
|
|
377
|
+
await seedChannel(cs, { highestVoucherAmount: 100_000_000n, spent: 0n })
|
|
378
|
+
|
|
379
|
+
const N = 10
|
|
380
|
+
const results = await Promise.all(
|
|
381
|
+
Array.from({ length: N }, () => ChannelStore.deductFromChannel(cs, channelId, 1_000_000n)),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
const successes = results.filter((r) => r.ok).length
|
|
385
|
+
expect(successes).toBe(N)
|
|
386
|
+
|
|
387
|
+
const channel = await cs.getChannel(channelId)
|
|
388
|
+
expect(channel!.spent).toBe(BigInt(N * 1_000_000))
|
|
389
|
+
expect(channel!.units).toBe(N)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test('mutex does not block different channels', async () => {
|
|
393
|
+
const cs = ChannelStore.fromStore(stripUpdateMethod(Store.memory()))
|
|
394
|
+
await seedChannel(cs, { highestVoucherAmount: 10_000_000n, spent: 0n })
|
|
395
|
+
await cs.updateChannel(channelId2, () =>
|
|
396
|
+
makeChannel({ channelId: channelId2, highestVoucherAmount: 10_000_000n, spent: 0n }),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
const N = 20
|
|
400
|
+
const [results1, results2] = await Promise.all([
|
|
401
|
+
Promise.all(
|
|
402
|
+
Array.from({ length: N }, () =>
|
|
403
|
+
ChannelStore.deductFromChannel(cs, channelId, 1_000_000n),
|
|
404
|
+
),
|
|
405
|
+
),
|
|
406
|
+
Promise.all(
|
|
407
|
+
Array.from({ length: N }, () =>
|
|
408
|
+
ChannelStore.deductFromChannel(cs, channelId2, 1_000_000n),
|
|
409
|
+
),
|
|
410
|
+
),
|
|
411
|
+
])
|
|
412
|
+
|
|
413
|
+
expect(results1.filter((r) => r.ok).length).toBe(10)
|
|
414
|
+
expect(results2.filter((r) => r.ok).length).toBe(10)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test('mutex releases on callback error', async () => {
|
|
418
|
+
const cs = ChannelStore.fromStore(stripUpdateMethod(Store.memory()))
|
|
419
|
+
await seedChannel(cs)
|
|
420
|
+
|
|
421
|
+
await expect(
|
|
422
|
+
cs.updateChannel(channelId, () => {
|
|
423
|
+
throw new Error('callback error')
|
|
424
|
+
}),
|
|
425
|
+
).rejects.toThrow('callback error')
|
|
426
|
+
|
|
427
|
+
const result = await cs.updateChannel(channelId, (c) => (c ? { ...c, spent: 1n } : null))
|
|
428
|
+
expect(result!.spent).toBe(1n)
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
describe('parity: atomic vs mutex produce same results', () => {
|
|
433
|
+
test('same final state after N concurrent deductions', async () => {
|
|
434
|
+
const N = 30
|
|
435
|
+
const balance = 20_000_000n
|
|
436
|
+
const deduction = 1_000_000n
|
|
437
|
+
|
|
438
|
+
const atomicCs = ChannelStore.fromStore(Store.memory())
|
|
439
|
+
await atomicCs.updateChannel(channelId, () =>
|
|
440
|
+
makeChannel({ highestVoucherAmount: balance, spent: 0n }),
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
const mutexCs = ChannelStore.fromStore(stripUpdateMethod(Store.memory()))
|
|
444
|
+
await mutexCs.updateChannel(channelId, () =>
|
|
445
|
+
makeChannel({ highestVoucherAmount: balance, spent: 0n }),
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
const [atomicResults, mutexResults] = await Promise.all([
|
|
449
|
+
Promise.all(
|
|
450
|
+
Array.from({ length: N }, () =>
|
|
451
|
+
ChannelStore.deductFromChannel(atomicCs, channelId, deduction),
|
|
452
|
+
),
|
|
453
|
+
),
|
|
454
|
+
Promise.all(
|
|
455
|
+
Array.from({ length: N }, () =>
|
|
456
|
+
ChannelStore.deductFromChannel(mutexCs, channelId, deduction),
|
|
457
|
+
),
|
|
458
|
+
),
|
|
459
|
+
])
|
|
460
|
+
|
|
461
|
+
const atomicSuccesses = atomicResults.filter((r) => r.ok).length
|
|
462
|
+
const mutexSuccesses = mutexResults.filter((r) => r.ok).length
|
|
463
|
+
expect(atomicSuccesses).toBe(mutexSuccesses)
|
|
464
|
+
expect(atomicSuccesses).toBe(20)
|
|
465
|
+
|
|
466
|
+
const atomicChannel = await atomicCs.getChannel(channelId)
|
|
467
|
+
const mutexChannel = await mutexCs.getChannel(channelId)
|
|
468
|
+
expect(atomicChannel!.spent).toBe(mutexChannel!.spent)
|
|
469
|
+
expect(atomicChannel!.units).toBe(mutexChannel!.units)
|
|
470
|
+
expect(atomicChannel!.spent).toBe(balance)
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
})
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Address, Hex } from 'viem'
|
|
2
|
+
import type * as Store from '../../Store.js'
|
|
3
|
+
import type { SignedVoucher } from './Types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* State for an on-chain payment channel, including per-session accounting.
|
|
7
|
+
*
|
|
8
|
+
* Tracks the channel's identity, on-chain balance, the highest voucher
|
|
9
|
+
* the server has accepted, and the current session's spend counters.
|
|
10
|
+
* A channel is created when a payer opens an escrow on-chain and persists
|
|
11
|
+
* until the channel is finalized (closed/settled).
|
|
12
|
+
*
|
|
13
|
+
* One channel = one session. The client owns the key and can't race with
|
|
14
|
+
* itself, so concurrent session support is unnecessary.
|
|
15
|
+
*
|
|
16
|
+
* Monotonicity invariants (enforced by update callbacks):
|
|
17
|
+
* - `highestVoucherAmount` only increases
|
|
18
|
+
* - `settledOnChain` only increases
|
|
19
|
+
* - `deposit` reflects the latest on-chain value
|
|
20
|
+
*/
|
|
21
|
+
export interface State {
|
|
22
|
+
/** Address authorized to sign vouchers on behalf of the payer. */
|
|
23
|
+
authorizedSigner: Address
|
|
24
|
+
/** Chain ID the channel was opened on. */
|
|
25
|
+
chainId: number
|
|
26
|
+
/** Escrow contract address the channel was opened on. */
|
|
27
|
+
escrowContract: Address
|
|
28
|
+
/** Unique identifier for this payment channel. */
|
|
29
|
+
channelId: Hex
|
|
30
|
+
/** ISO 8601 timestamp when the channel was created. */
|
|
31
|
+
createdAt: string
|
|
32
|
+
/** Current on-chain deposit in the escrow contract. */
|
|
33
|
+
deposit: bigint
|
|
34
|
+
/** Whether the channel has been finalized (closed) on-chain. */
|
|
35
|
+
finalized: boolean
|
|
36
|
+
/** The signed voucher corresponding to `highestVoucherAmount`. */
|
|
37
|
+
highestVoucher: SignedVoucher | null
|
|
38
|
+
/** Highest cumulative voucher amount accepted by the server. */
|
|
39
|
+
highestVoucherAmount: bigint
|
|
40
|
+
/** Address of the payment recipient. */
|
|
41
|
+
payee: Address
|
|
42
|
+
/** Address of the payment sender. */
|
|
43
|
+
payer: Address
|
|
44
|
+
/** Cumulative amount settled on-chain so far. */
|
|
45
|
+
settledOnChain: bigint
|
|
46
|
+
/** Cumulative amount spent (charged) against this channel's current session. */
|
|
47
|
+
spent: bigint
|
|
48
|
+
/** Token contract address used for payments. */
|
|
49
|
+
token: Address
|
|
50
|
+
/** Number of charge operations (API requests) fulfilled in the current session. */
|
|
51
|
+
units: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Internal store interface for channel state persistence.
|
|
56
|
+
*
|
|
57
|
+
* ## Atomicity contract
|
|
58
|
+
*
|
|
59
|
+
* The `updateChannel` method uses an atomic read-modify-write callback.
|
|
60
|
+
* The callback receives the current state (or `null` if none exists), and
|
|
61
|
+
* returns the next state (or `null` to delete). Implementations must
|
|
62
|
+
* guarantee that no concurrent mutation occurs between reading `current`
|
|
63
|
+
* and writing the return value.
|
|
64
|
+
*
|
|
65
|
+
* Backends implement this via their native mechanisms:
|
|
66
|
+
* - **In-memory / JS single-thread**: Synchronous callback execution
|
|
67
|
+
* - **Durable Objects**: Single-threaded execution model
|
|
68
|
+
* - **D1 / SQL**: Database transactions
|
|
69
|
+
*/
|
|
70
|
+
export type ChannelStore = {
|
|
71
|
+
getChannel(channelId: Hex): Promise<State | null>
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Atomic read-modify-write for channel state.
|
|
75
|
+
* Return `null` from `fn` to delete the channel.
|
|
76
|
+
*/
|
|
77
|
+
updateChannel(channelId: Hex, fn: (current: State | null) => State | null): Promise<State | null>
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Wait for the next update to a channel.
|
|
81
|
+
*
|
|
82
|
+
* Returns a `Promise` that resolves once `updateChannel` is called for
|
|
83
|
+
* `channelId`. Implementations should resolve immediately if the channel
|
|
84
|
+
* was updated between the call to `waitForUpdate` and the `Promise`
|
|
85
|
+
* being awaited.
|
|
86
|
+
*
|
|
87
|
+
* When not implemented, callers fall back to polling.
|
|
88
|
+
*/
|
|
89
|
+
waitForUpdate?(channelId: Hex): Promise<void>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type DeductResult = { ok: true; channel: State } | { ok: false; channel: State }
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Atomically deduct `amount` from a channel's available balance.
|
|
96
|
+
*
|
|
97
|
+
* Returns `{ ok: true, channel }` if the deduction succeeded, or
|
|
98
|
+
* `{ ok: false, channel }` with the unchanged state if balance is
|
|
99
|
+
* insufficient. Throws if the channel does not exist.
|
|
100
|
+
*/
|
|
101
|
+
export async function deductFromChannel(
|
|
102
|
+
store: ChannelStore,
|
|
103
|
+
channelId: Hex,
|
|
104
|
+
amount: bigint,
|
|
105
|
+
): Promise<DeductResult> {
|
|
106
|
+
let deducted = false
|
|
107
|
+
const channel = await store.updateChannel(channelId, (current) => {
|
|
108
|
+
deducted = false
|
|
109
|
+
if (!current) return null
|
|
110
|
+
if (current.highestVoucherAmount - current.spent >= amount) {
|
|
111
|
+
deducted = true
|
|
112
|
+
return { ...current, spent: current.spent + amount, units: current.units + 1 }
|
|
113
|
+
}
|
|
114
|
+
return current
|
|
115
|
+
})
|
|
116
|
+
if (!channel) throw new Error('channel not found')
|
|
117
|
+
return { ok: deducted, channel }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Wraps a generic {@link Store} into the internal {@link Store}
|
|
122
|
+
* interface used by server handlers and the SSE metering loop.
|
|
123
|
+
*
|
|
124
|
+
* Provides `waitForUpdate` notifications so the SSE `chargeOrWait` loop
|
|
125
|
+
* can wake up without polling.
|
|
126
|
+
*
|
|
127
|
+
* ## Atomicity
|
|
128
|
+
*
|
|
129
|
+
* Mutations use `get` → `fn` → `set` guarded by a per-key in-process
|
|
130
|
+
* mutex. This serializes concurrent `updateChannel` calls within a
|
|
131
|
+
* single JS runtime but does **not** protect against races across
|
|
132
|
+
* multiple processes or instances.
|
|
133
|
+
*
|
|
134
|
+
* Backends that need true atomicity (e.g., Durable Objects, D1)
|
|
135
|
+
* should implement {@link Store} directly.
|
|
136
|
+
*/
|
|
137
|
+
const storeCache = new WeakMap<Store.Store, ChannelStore>()
|
|
138
|
+
|
|
139
|
+
export function fromStore(store: Store.Store): ChannelStore {
|
|
140
|
+
const cached = storeCache.get(store)
|
|
141
|
+
if (cached) return cached
|
|
142
|
+
|
|
143
|
+
const waiters = new Map<string, Set<() => void>>()
|
|
144
|
+
const locks = new Map<string, Promise<void>>()
|
|
145
|
+
|
|
146
|
+
function notify(channelId: string) {
|
|
147
|
+
const set = waiters.get(channelId)
|
|
148
|
+
if (!set) return
|
|
149
|
+
for (const resolve of set) resolve()
|
|
150
|
+
waiters.delete(channelId)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function update(
|
|
154
|
+
channelId: Hex,
|
|
155
|
+
fn: (current: State | null) => State | null,
|
|
156
|
+
): Promise<State | null> {
|
|
157
|
+
while (locks.has(channelId)) await locks.get(channelId)
|
|
158
|
+
|
|
159
|
+
let release!: () => void
|
|
160
|
+
locks.set(
|
|
161
|
+
channelId,
|
|
162
|
+
new Promise<void>((r) => {
|
|
163
|
+
release = r
|
|
164
|
+
}),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const current = await store.get<State | null>(channelId)
|
|
169
|
+
const next = fn(current)
|
|
170
|
+
if (next) await store.put(channelId, next)
|
|
171
|
+
else await store.delete(channelId)
|
|
172
|
+
return next
|
|
173
|
+
} finally {
|
|
174
|
+
locks.delete(channelId)
|
|
175
|
+
release()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const cs: ChannelStore = {
|
|
180
|
+
async getChannel(channelId) {
|
|
181
|
+
return store.get<State | null>(channelId)
|
|
182
|
+
},
|
|
183
|
+
async updateChannel(channelId, fn) {
|
|
184
|
+
const result = await update(channelId, fn)
|
|
185
|
+
notify(channelId)
|
|
186
|
+
return result
|
|
187
|
+
},
|
|
188
|
+
waitForUpdate(channelId) {
|
|
189
|
+
return new Promise<void>((resolve) => {
|
|
190
|
+
let set = waiters.get(channelId)
|
|
191
|
+
if (!set) {
|
|
192
|
+
set = new Set()
|
|
193
|
+
waiters.set(channelId, set)
|
|
194
|
+
}
|
|
195
|
+
set.add(resolve)
|
|
196
|
+
})
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
storeCache.set(store, cs)
|
|
201
|
+
return cs
|
|
202
|
+
}
|