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
package/src/cli.ts
ADDED
|
@@ -0,0 +1,1319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as child from 'node:child_process'
|
|
3
|
+
import * as fs from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import * as os from 'node:os'
|
|
6
|
+
import * as path from 'node:path'
|
|
7
|
+
import * as readline from 'node:readline'
|
|
8
|
+
import { cac } from 'cac'
|
|
9
|
+
import { Base64 } from 'ox'
|
|
10
|
+
import type { Chain } from 'viem'
|
|
11
|
+
import { type Address, createClient, http } from 'viem'
|
|
12
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
13
|
+
import { tempo as tempoMainnet, tempoModerato } from 'viem/chains'
|
|
14
|
+
import { type ZodMiniType, z } from 'zod/mini'
|
|
15
|
+
import * as Challenge from './Challenge.js'
|
|
16
|
+
import * as Credential from './Credential.js'
|
|
17
|
+
import * as Mppx from './client/Mppx.js'
|
|
18
|
+
import { tempo } from './tempo/client/index.js'
|
|
19
|
+
import type { StreamCredentialPayload } from './tempo/stream/Types.js'
|
|
20
|
+
import { signVoucher } from './tempo/stream/Voucher.js'
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url)
|
|
23
|
+
const { name, version } = require('../package.json') as { name: string; version: string }
|
|
24
|
+
|
|
25
|
+
const cli = cac(name)
|
|
26
|
+
|
|
27
|
+
cli
|
|
28
|
+
.command('[url]', 'Make HTTP request with automatic payment')
|
|
29
|
+
.option('-a, --account <name>', 'Account name (env: MPPX_ACCOUNT)')
|
|
30
|
+
.option('-d, --data <data>', 'Send request body (implies POST unless -X is set)')
|
|
31
|
+
.option('-f, --fail', 'Fail silently on HTTP errors (exit 22)')
|
|
32
|
+
.option('-i, --include', 'Include response headers in output')
|
|
33
|
+
.option('-k, --insecure', 'Skip TLS certificate verification (true for localhost/.local)')
|
|
34
|
+
.option(
|
|
35
|
+
'-r, --rpc-url <url>',
|
|
36
|
+
'RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)',
|
|
37
|
+
)
|
|
38
|
+
.option('-s, --silent', 'Silent mode (suppress progress and info)')
|
|
39
|
+
.option('-v, --verbose', 'Show request/response headers')
|
|
40
|
+
.option('-A, --user-agent <ua>', 'Set User-Agent header')
|
|
41
|
+
.option('-H, --header <header>', 'Add header (repeatable)')
|
|
42
|
+
.option('-L, --location', 'Follow redirects')
|
|
43
|
+
.option('-X, --method <method>', 'HTTP method')
|
|
44
|
+
.option('--channel <id>', 'Reuse existing stream channel ID')
|
|
45
|
+
.option('--deposit <amount>', 'Deposit amount for stream payments (human-readable units)')
|
|
46
|
+
.option('--json <json>', 'Send JSON body (sets Content-Type and Accept, implies POST)')
|
|
47
|
+
.option('--yes', 'Skip confirmation prompts')
|
|
48
|
+
.example(`${name} example.com/content`)
|
|
49
|
+
.example(`${name} example.com/api --json '{"key":"value"}'`)
|
|
50
|
+
.action(async (rawUrl: string | undefined, rawOptions: unknown) => {
|
|
51
|
+
const options = parseOptions(
|
|
52
|
+
z.object({
|
|
53
|
+
account: z.optional(z.string()),
|
|
54
|
+
channel: z.optional(z.coerce.string()),
|
|
55
|
+
data: z.optional(z.string()),
|
|
56
|
+
deposit: z.optional(z.union([z.string(), z.number()])),
|
|
57
|
+
fail: z.optional(z.boolean()),
|
|
58
|
+
header: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
59
|
+
include: z.optional(z.boolean()),
|
|
60
|
+
insecure: z.optional(z.boolean()),
|
|
61
|
+
json: z.optional(z.string()),
|
|
62
|
+
location: z.optional(z.boolean()),
|
|
63
|
+
method: z.optional(z.string()),
|
|
64
|
+
rpcUrl: z.optional(z.string()),
|
|
65
|
+
silent: z.optional(z.boolean()),
|
|
66
|
+
userAgent: z.optional(z.string()),
|
|
67
|
+
verbose: z.optional(z.boolean()),
|
|
68
|
+
yes: z.optional(z.boolean()),
|
|
69
|
+
}),
|
|
70
|
+
rawOptions,
|
|
71
|
+
)
|
|
72
|
+
if (!rawUrl) {
|
|
73
|
+
cli.outputHelp()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const silent = options.silent ?? false
|
|
78
|
+
const info = silent ? (_msg: string) => {} : (msg: string) => process.stderr.write(msg)
|
|
79
|
+
if (silent) options.yes = true
|
|
80
|
+
|
|
81
|
+
const accountName = resolveAccountName(options.account)
|
|
82
|
+
const privateKey = process.env.MPPX_PRIVATE_KEY ?? (await createKeychain(accountName).get())
|
|
83
|
+
if (!privateKey) {
|
|
84
|
+
if (options.account) console.log(`Account "${accountName}" not found.`)
|
|
85
|
+
else console.log(`No account found.`)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const headers: Record<string, string> = {}
|
|
90
|
+
if (options.header) {
|
|
91
|
+
const headerList = Array.isArray(options.header) ? options.header : [options.header]
|
|
92
|
+
for (const header of headerList) {
|
|
93
|
+
const index = header.indexOf(':')
|
|
94
|
+
if (index === -1) {
|
|
95
|
+
console.error(`Invalid header format: ${header}`)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
headers[header.slice(0, index).trim()] = header.slice(index + 1).trim()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
headers['User-Agent'] = options.userAgent ?? `${name}/${version}`
|
|
102
|
+
|
|
103
|
+
const url = (() => {
|
|
104
|
+
const hasProtocol = /^https?:\/\//.test(rawUrl)
|
|
105
|
+
const isLocal = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?/.test(rawUrl)
|
|
106
|
+
return hasProtocol ? rawUrl : `${isLocal ? 'http' : 'https'}://${rawUrl}`
|
|
107
|
+
})()
|
|
108
|
+
const { hostname } = new URL(url)
|
|
109
|
+
if (options.insecure || hostname === 'localhost' || hostname.endsWith('.local')) {
|
|
110
|
+
process.removeAllListeners('warning')
|
|
111
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const fetchInit: RequestInit = { redirect: options.location ? 'follow' : 'manual' }
|
|
116
|
+
if (options.json) {
|
|
117
|
+
fetchInit.body = options.json
|
|
118
|
+
headers['Content-Type'] ??= 'application/json'
|
|
119
|
+
headers.Accept ??= 'application/json'
|
|
120
|
+
} else if (options.data) {
|
|
121
|
+
fetchInit.body = options.data
|
|
122
|
+
}
|
|
123
|
+
if (options.method) fetchInit.method = options.method.toUpperCase()
|
|
124
|
+
else if (fetchInit.body) fetchInit.method = 'POST'
|
|
125
|
+
if (Object.keys(headers).length > 0) fetchInit.headers = headers
|
|
126
|
+
|
|
127
|
+
const verbose = options.verbose ?? false
|
|
128
|
+
|
|
129
|
+
const printRequestHeaders = (reqUrl: string, init: RequestInit) => {
|
|
130
|
+
if (!verbose) return
|
|
131
|
+
const { pathname, host } = new URL(reqUrl)
|
|
132
|
+
const method = (init.method ?? 'GET').toUpperCase()
|
|
133
|
+
info(`> ${method} ${pathname} HTTP/1.1\n`)
|
|
134
|
+
info(`> Host: ${host}\n`)
|
|
135
|
+
for (const [k, v] of Object.entries((init.headers ?? {}) as Record<string, string>))
|
|
136
|
+
info(`> ${k}: ${v}\n`)
|
|
137
|
+
info('>\n')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const printResponseHeaders = (res: Response) => {
|
|
141
|
+
if (!options.include && !verbose) return
|
|
142
|
+
if (silent) return
|
|
143
|
+
const status = `HTTP/1.1 ${res.status} ${res.statusText}`
|
|
144
|
+
const out = verbose ? process.stderr : process.stdout
|
|
145
|
+
const prefix = verbose ? '< ' : ''
|
|
146
|
+
out.write(`${prefix}${status}\n`)
|
|
147
|
+
for (const [k, v] of res.headers) out.write(`${prefix}${k}: ${v}\n`)
|
|
148
|
+
out.write(verbose ? '<\n' : '\n')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
printRequestHeaders(url, fetchInit)
|
|
152
|
+
const challengeResponse = await globalThis.fetch(url, fetchInit)
|
|
153
|
+
if (challengeResponse.status !== 402) {
|
|
154
|
+
if (options.fail && challengeResponse.status >= 400) process.exit(22)
|
|
155
|
+
printResponseHeaders(challengeResponse)
|
|
156
|
+
console.log((await challengeResponse.text()).replace(/\n+$/, ''))
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const account = privateKeyToAccount(privateKey as `0x${string}`)
|
|
161
|
+
const rpcUrl = options.rpcUrl ?? process.env.RPC_URL
|
|
162
|
+
const client = createClient({
|
|
163
|
+
chain: await resolveChain({ ...options, rpcUrl }),
|
|
164
|
+
transport: http(rpcUrl),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const challenge = Challenge.fromResponse(challengeResponse)
|
|
168
|
+
const explorerUrl = client.chain?.blockExplorers?.default?.url
|
|
169
|
+
const shownKeys = new Set<string>()
|
|
170
|
+
const challengeRequest = challenge.request as Record<string, unknown>
|
|
171
|
+
const currency = challengeRequest.currency as string | undefined
|
|
172
|
+
const tokenInfo = currency
|
|
173
|
+
? await fetchTokenInfo(client, currency as Address, account.address).catch(() => undefined)
|
|
174
|
+
: undefined
|
|
175
|
+
const tokenSymbol = tokenInfo?.symbol ?? currency ?? ''
|
|
176
|
+
const tokenDecimals =
|
|
177
|
+
tokenInfo?.decimals ?? (challengeRequest.decimals as number | undefined) ?? 6
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
printResponseHeaders(challengeResponse)
|
|
181
|
+
const request = challengeRequest
|
|
182
|
+
|
|
183
|
+
const balanceKeys = new Set(['amount', 'suggestedDeposit', 'minVoucherDelta'])
|
|
184
|
+
const skipKeys = new Set(['decimals', 'currency', 'methodDetails'])
|
|
185
|
+
const fmtRequestValue = (key: string, value: unknown): string => {
|
|
186
|
+
if (balanceKeys.has(key) && typeof value === 'string') {
|
|
187
|
+
return `${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`
|
|
188
|
+
}
|
|
189
|
+
if (key === 'chainId' && typeof value === 'number') {
|
|
190
|
+
const name = chainName({ id: value, name: '' })
|
|
191
|
+
return name ? `${value} ${pc.dim(`(${name})`)}` : String(value)
|
|
192
|
+
}
|
|
193
|
+
if (typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value))
|
|
194
|
+
return explorerUrl ? pc.link(`${explorerUrl}/address/${value}`, value) : value
|
|
195
|
+
if (typeof value === 'string' && /^https?:\/\//.test(value)) return pc.link(value, value)
|
|
196
|
+
return String(value)
|
|
197
|
+
}
|
|
198
|
+
const decodeMemo = (hex: string): string | undefined => {
|
|
199
|
+
try {
|
|
200
|
+
const stripped = hex.replace(/^0x0*/, '')
|
|
201
|
+
if (!stripped) return undefined
|
|
202
|
+
const bytes = Uint8Array.from(
|
|
203
|
+
stripped.match(/.{1,2}/g)!.map((b) => Number.parseInt(b, 16)),
|
|
204
|
+
)
|
|
205
|
+
const decoded = new TextDecoder().decode(bytes)
|
|
206
|
+
return /^[\x20-\x7e]+$/.test(decoded) ? decoded : undefined
|
|
207
|
+
} catch {
|
|
208
|
+
return undefined
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const skipChallengeKeys = new Set(['id', 'request'])
|
|
213
|
+
const fmtChallengeValue = (key: string, value: unknown): string => {
|
|
214
|
+
if (key === 'realm' && typeof value === 'string') {
|
|
215
|
+
try {
|
|
216
|
+
const realmUrl = new URL(value.includes('://') ? value : `https://${value}`)
|
|
217
|
+
return pc.link(realmUrl.href, value)
|
|
218
|
+
} catch {}
|
|
219
|
+
}
|
|
220
|
+
return String(value)
|
|
221
|
+
}
|
|
222
|
+
const challengeRows: [string, string][] = []
|
|
223
|
+
for (const [key, value] of Object.entries(challenge)) {
|
|
224
|
+
if (skipChallengeKeys.has(key) || value === undefined) continue
|
|
225
|
+
challengeRows.push([key, fmtChallengeValue(key, value)])
|
|
226
|
+
}
|
|
227
|
+
challengeRows.sort(([a], [b]) => a.localeCompare(b))
|
|
228
|
+
|
|
229
|
+
const requestRows: [string, string][] = []
|
|
230
|
+
for (const [key, value] of Object.entries(request)) {
|
|
231
|
+
if (skipKeys.has(key) || value === undefined) continue
|
|
232
|
+
requestRows.push([key, fmtRequestValue(key, value)])
|
|
233
|
+
}
|
|
234
|
+
requestRows.sort(([a], [b]) => a.localeCompare(b))
|
|
235
|
+
|
|
236
|
+
const detailRows: [string, string, string?][] = []
|
|
237
|
+
const methodDetails = request.methodDetails as Record<string, unknown> | undefined
|
|
238
|
+
if (methodDetails) {
|
|
239
|
+
for (const [key, value] of Object.entries(methodDetails)) {
|
|
240
|
+
if (value === undefined) continue
|
|
241
|
+
if (key === 'memo' && typeof value === 'string') {
|
|
242
|
+
const decoded = decodeMemo(value)
|
|
243
|
+
detailRows.push([key, decoded ? `${decoded}\n${pc.dim(value)}` : value])
|
|
244
|
+
} else {
|
|
245
|
+
detailRows.push([key, fmtRequestValue(key, value)])
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
detailRows.sort(([a], [b]) => a.localeCompare(b))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const sections: [string, [string, string][]][] = [
|
|
252
|
+
['Challenge', challengeRows],
|
|
253
|
+
['Request', requestRows],
|
|
254
|
+
...(detailRows.length ? [['Details', detailRows] as [string, [string, string][]]] : []),
|
|
255
|
+
]
|
|
256
|
+
for (const [, rows] of sections) for (const [key] of rows) shownKeys.add(key)
|
|
257
|
+
const pad = Math.max(...sections.flatMap(([, rows]) => rows.map(([k]) => k.length)))
|
|
258
|
+
const indent = ` ${''.padEnd(pad)} `
|
|
259
|
+
|
|
260
|
+
info(`${pc.bold(pc.yellow('Payment Required'))}\n`)
|
|
261
|
+
for (const [title, rows] of sections) {
|
|
262
|
+
info(`${pc.bold(title)}\n`)
|
|
263
|
+
for (const [label, value] of rows) {
|
|
264
|
+
const [first, ...rest] = value.split('\n')
|
|
265
|
+
info(` ${pc.dim(label.padEnd(pad))} ${first}\n`)
|
|
266
|
+
for (const line of rest) info(`${indent}${line}\n`)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
info('\n')
|
|
270
|
+
|
|
271
|
+
if (!options.yes) {
|
|
272
|
+
const ok = await confirm(`Proceed with ${challenge.intent}?`)
|
|
273
|
+
if (!ok) {
|
|
274
|
+
info('Aborted.\n')
|
|
275
|
+
process.exit(0)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const mppx = Mppx.create({
|
|
281
|
+
methods: tempo({
|
|
282
|
+
account,
|
|
283
|
+
getClient: () => client,
|
|
284
|
+
deposit: (() => {
|
|
285
|
+
if (challenge.intent !== 'session') return undefined
|
|
286
|
+
const suggestedDeposit = (challenge.request as Record<string, unknown>)
|
|
287
|
+
.suggestedDeposit as string | undefined
|
|
288
|
+
const cliDeposit = options.deposit !== undefined ? String(options.deposit) : undefined
|
|
289
|
+
const resolved =
|
|
290
|
+
suggestedDeposit ?? cliDeposit ?? (isTestnet(client.chain!) ? '10' : undefined)
|
|
291
|
+
if (!resolved) {
|
|
292
|
+
console.error(
|
|
293
|
+
'Stream payment requires a deposit. Use --deposit <amount> or connect to testnet.',
|
|
294
|
+
)
|
|
295
|
+
process.exit(1)
|
|
296
|
+
}
|
|
297
|
+
return resolved
|
|
298
|
+
})(),
|
|
299
|
+
}),
|
|
300
|
+
polyfill: false,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const credential = await mppx.createCredential(
|
|
304
|
+
challengeResponse,
|
|
305
|
+
(() => {
|
|
306
|
+
if (!options.channel) return undefined
|
|
307
|
+
const idx = process.argv.indexOf('--channel')
|
|
308
|
+
const channelId = idx !== -1 ? process.argv[idx + 1]! : String(options.channel)
|
|
309
|
+
const saved = readChannelCumulative(channelId)
|
|
310
|
+
return {
|
|
311
|
+
channelId,
|
|
312
|
+
...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
|
|
313
|
+
}
|
|
314
|
+
})(),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
const streamMd = challenge.request.methodDetails as
|
|
318
|
+
| { escrowContract?: string; chainId?: number }
|
|
319
|
+
| undefined
|
|
320
|
+
let streamChannelId: `0x${string}` | undefined
|
|
321
|
+
let streamEscrowContract: Address | undefined
|
|
322
|
+
let streamChainId = 0
|
|
323
|
+
let streamCumulativeAmount = 0n
|
|
324
|
+
|
|
325
|
+
if (challenge.intent === 'session') {
|
|
326
|
+
const parsed = Credential.deserialize<StreamCredentialPayload>(credential)
|
|
327
|
+
streamChannelId = parsed.payload.channelId
|
|
328
|
+
streamChainId = streamMd?.chainId ?? client.chain?.id ?? 0
|
|
329
|
+
streamEscrowContract = streamMd?.escrowContract as Address | undefined
|
|
330
|
+
if ('cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount)
|
|
331
|
+
streamCumulativeAmount = BigInt(parsed.payload.cumulativeAmount)
|
|
332
|
+
|
|
333
|
+
if (parsed.payload.action === 'open') {
|
|
334
|
+
const depositRaw =
|
|
335
|
+
(challengeRequest.suggestedDeposit as string | undefined) ?? options.deposit
|
|
336
|
+
const depositDisplay = depositRaw
|
|
337
|
+
? ` ${pc.dim(`(deposit ${depositRaw} ${tokenSymbol})`)}`
|
|
338
|
+
: ''
|
|
339
|
+
info(`${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const credentialFetchInit = {
|
|
344
|
+
...fetchInit,
|
|
345
|
+
headers: { ...(fetchInit.headers as Record<string, string>), Authorization: credential },
|
|
346
|
+
}
|
|
347
|
+
printRequestHeaders(url, credentialFetchInit)
|
|
348
|
+
const credentialResponse = await globalThis.fetch(url, credentialFetchInit)
|
|
349
|
+
|
|
350
|
+
if (options.fail && credentialResponse.status >= 400) process.exit(22)
|
|
351
|
+
|
|
352
|
+
if (credentialResponse.status === 402) {
|
|
353
|
+
const body = await credentialResponse.text()
|
|
354
|
+
info(`${pc.bold(pc.red('Payment Rejected'))}\n`)
|
|
355
|
+
try {
|
|
356
|
+
const problem = JSON.parse(body) as Record<string, unknown>
|
|
357
|
+
const rows: [string, string][] = []
|
|
358
|
+
for (const [key, value] of Object.entries(problem)) {
|
|
359
|
+
if (value === undefined) continue
|
|
360
|
+
rows.push([key, String(value)])
|
|
361
|
+
}
|
|
362
|
+
rows.sort(([a], [b]) => a.localeCompare(b))
|
|
363
|
+
const pad = Math.max(...rows.map(([k]) => k.length))
|
|
364
|
+
for (const [label, value] of rows) info(` ${pc.dim(label.padEnd(pad))} ${value}\n`)
|
|
365
|
+
} catch {
|
|
366
|
+
if (body) info(` ${body}\n`)
|
|
367
|
+
}
|
|
368
|
+
process.exit(1)
|
|
369
|
+
} else {
|
|
370
|
+
printResponseHeaders(credentialResponse)
|
|
371
|
+
|
|
372
|
+
const receiptHeader = credentialResponse.headers.get('Payment-Receipt')
|
|
373
|
+
if (receiptHeader) {
|
|
374
|
+
try {
|
|
375
|
+
const receiptJson = JSON.parse(Base64.toString(receiptHeader)) as Record<
|
|
376
|
+
string,
|
|
377
|
+
unknown
|
|
378
|
+
>
|
|
379
|
+
if (
|
|
380
|
+
typeof receiptJson.acceptedCumulative === 'string' &&
|
|
381
|
+
receiptJson.acceptedCumulative
|
|
382
|
+
) {
|
|
383
|
+
streamCumulativeAmount = BigInt(receiptJson.acceptedCumulative)
|
|
384
|
+
if (streamChannelId) writeChannelCumulative(streamChannelId, streamCumulativeAmount)
|
|
385
|
+
}
|
|
386
|
+
info(`\n${pc.bold(pc.green('Payment Receipt'))}\n`)
|
|
387
|
+
const rows: [string, string][] = []
|
|
388
|
+
for (const [key, value] of Object.entries(receiptJson)) {
|
|
389
|
+
if (value === undefined || shownKeys.has(key)) continue
|
|
390
|
+
if (key === 'reference' && typeof value === 'string' && explorerUrl)
|
|
391
|
+
rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)])
|
|
392
|
+
else rows.push([key, String(value)])
|
|
393
|
+
}
|
|
394
|
+
rows.sort(([a], [b]) => a.localeCompare(b))
|
|
395
|
+
const pad = Math.max(...rows.map(([k]) => k.length))
|
|
396
|
+
for (const [label, value] of rows) info(` ${pc.dim(label.padEnd(pad))} ${value}\n`)
|
|
397
|
+
info('\n')
|
|
398
|
+
} catch {}
|
|
399
|
+
}
|
|
400
|
+
const contentType = credentialResponse.headers.get('Content-Type') ?? ''
|
|
401
|
+
if (contentType.includes('text/event-stream')) {
|
|
402
|
+
const reader = credentialResponse.body?.getReader()
|
|
403
|
+
if (!reader) {
|
|
404
|
+
console.error('No response body')
|
|
405
|
+
process.exit(1)
|
|
406
|
+
}
|
|
407
|
+
const decoder = new TextDecoder()
|
|
408
|
+
let buffer = ''
|
|
409
|
+
let currentEvent = ''
|
|
410
|
+
|
|
411
|
+
const streamCred =
|
|
412
|
+
challenge.intent === 'session'
|
|
413
|
+
? Credential.deserialize<StreamCredentialPayload>(credential)
|
|
414
|
+
: undefined
|
|
415
|
+
const channelId = streamCred?.payload.channelId
|
|
416
|
+
const md = challenge.request.methodDetails as
|
|
417
|
+
| { escrowContract?: string; chainId?: number }
|
|
418
|
+
| undefined
|
|
419
|
+
const streamChainId = md?.chainId ?? client.chain?.id ?? 0
|
|
420
|
+
const escrowContract = md?.escrowContract as Address | undefined
|
|
421
|
+
let cumulativeAmount =
|
|
422
|
+
streamCred?.payload &&
|
|
423
|
+
'cumulativeAmount' in streamCred.payload &&
|
|
424
|
+
streamCred.payload.cumulativeAmount
|
|
425
|
+
? BigInt(streamCred.payload.cumulativeAmount)
|
|
426
|
+
: 0n
|
|
427
|
+
let _voucherSeq = 0
|
|
428
|
+
|
|
429
|
+
const termBg = await detectTerminalBg()
|
|
430
|
+
const chunkBgs = (() => {
|
|
431
|
+
if (!termBg || !pc.isColorSupported) return undefined
|
|
432
|
+
const clamp = (n: number) => Math.max(0, Math.min(255, Math.round(n)))
|
|
433
|
+
const isDark = 0.299 * termBg.r + 0.587 * termBg.g + 0.114 * termBg.b < 128
|
|
434
|
+
const offset = isDark ? 1 : -1
|
|
435
|
+
const bgRgb = (d: number) => (s: string) => {
|
|
436
|
+
const r = clamp(termBg.r + d * offset)
|
|
437
|
+
const g = clamp(termBg.g + d * offset)
|
|
438
|
+
const b = clamp(termBg.b + d * offset)
|
|
439
|
+
return `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`
|
|
440
|
+
}
|
|
441
|
+
return [bgRgb(12), bgRgb(24)] as const
|
|
442
|
+
})()
|
|
443
|
+
let chunkIdx = 0
|
|
444
|
+
|
|
445
|
+
const writeContent = (chunk: string) => {
|
|
446
|
+
if (chunkBgs) {
|
|
447
|
+
const bgFn = chunkBgs[chunkIdx % chunkBgs.length]!
|
|
448
|
+
process.stdout.write(chunk.replace(/[^\n]+/g, (m) => bgFn(m)))
|
|
449
|
+
chunkIdx++
|
|
450
|
+
} else {
|
|
451
|
+
process.stdout.write(chunk)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const processLines = async (lines: string[]) => {
|
|
456
|
+
for (const line of lines) {
|
|
457
|
+
if (line.startsWith('event: ')) {
|
|
458
|
+
currentEvent = line.slice(7).trim()
|
|
459
|
+
continue
|
|
460
|
+
}
|
|
461
|
+
if (!line.startsWith('data: ')) {
|
|
462
|
+
if (line === '') currentEvent = ''
|
|
463
|
+
continue
|
|
464
|
+
}
|
|
465
|
+
const data = line.slice(6)
|
|
466
|
+
if (data.trim() === '[DONE]') continue
|
|
467
|
+
if (
|
|
468
|
+
currentEvent === 'payment-need-voucher' &&
|
|
469
|
+
channelId &&
|
|
470
|
+
escrowContract &&
|
|
471
|
+
streamChainId
|
|
472
|
+
) {
|
|
473
|
+
try {
|
|
474
|
+
const event = JSON.parse(data) as {
|
|
475
|
+
channelId: string
|
|
476
|
+
requiredCumulative: string
|
|
477
|
+
}
|
|
478
|
+
const required = BigInt(event.requiredCumulative)
|
|
479
|
+
cumulativeAmount = cumulativeAmount > required ? cumulativeAmount : required
|
|
480
|
+
|
|
481
|
+
const signature = await signVoucher(
|
|
482
|
+
client,
|
|
483
|
+
account,
|
|
484
|
+
{ channelId, cumulativeAmount },
|
|
485
|
+
escrowContract,
|
|
486
|
+
streamChainId,
|
|
487
|
+
)
|
|
488
|
+
const voucherCred = Credential.serialize({
|
|
489
|
+
challenge,
|
|
490
|
+
payload: {
|
|
491
|
+
action: 'voucher',
|
|
492
|
+
channelId,
|
|
493
|
+
cumulativeAmount: cumulativeAmount.toString(),
|
|
494
|
+
signature,
|
|
495
|
+
},
|
|
496
|
+
source: `did:pkh:eip155:${streamChainId}:${account.address}`,
|
|
497
|
+
})
|
|
498
|
+
await globalThis.fetch(url, {
|
|
499
|
+
method: 'POST',
|
|
500
|
+
headers: { Authorization: voucherCred },
|
|
501
|
+
})
|
|
502
|
+
_voucherSeq++
|
|
503
|
+
} catch (e) {
|
|
504
|
+
info(
|
|
505
|
+
pc.dim(pc.yellow(` [voucher failed: ${e instanceof Error ? e.message : e}]`)),
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
currentEvent = ''
|
|
509
|
+
continue
|
|
510
|
+
}
|
|
511
|
+
if (currentEvent === 'payment-receipt') {
|
|
512
|
+
try {
|
|
513
|
+
const receipt = JSON.parse(data) as Record<string, unknown>
|
|
514
|
+
info(`\n\n${pc.bold(pc.green('Payment Receipt'))}\n`)
|
|
515
|
+
const rows: [string, string][] = []
|
|
516
|
+
for (const [key, value] of Object.entries(receipt)) {
|
|
517
|
+
if (value === undefined || shownKeys.has(key)) continue
|
|
518
|
+
if (key === 'channelId' && value === receipt.reference) continue
|
|
519
|
+
const receiptBalanceKeys = ['acceptedCumulative', 'spent']
|
|
520
|
+
if (receiptBalanceKeys.includes(key) && typeof value === 'string') {
|
|
521
|
+
rows.push([
|
|
522
|
+
key,
|
|
523
|
+
`${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`,
|
|
524
|
+
])
|
|
525
|
+
} else if (key === 'reference' && typeof value === 'string' && explorerUrl)
|
|
526
|
+
rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)])
|
|
527
|
+
else rows.push([key, String(value)])
|
|
528
|
+
}
|
|
529
|
+
rows.sort(([a], [b]) => a.localeCompare(b))
|
|
530
|
+
const rpad = Math.max(...rows.map(([k]) => k.length))
|
|
531
|
+
for (const [label, value] of rows)
|
|
532
|
+
info(` ${pc.dim(label.padEnd(rpad))} ${value}\n`)
|
|
533
|
+
} catch {}
|
|
534
|
+
currentEvent = ''
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
if (data.length === 0) {
|
|
538
|
+
writeContent('\n')
|
|
539
|
+
} else {
|
|
540
|
+
try {
|
|
541
|
+
const parsed = JSON.parse(data) as {
|
|
542
|
+
token?: string
|
|
543
|
+
choices?: { delta?: { content?: string } }[]
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
writeContent(parsed.token ?? parsed.choices?.[0]?.delta?.content ?? data)
|
|
547
|
+
} catch {
|
|
548
|
+
writeContent(data)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
currentEvent = ''
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
while (true) {
|
|
556
|
+
const { done, value } = await reader.read()
|
|
557
|
+
if (done) break
|
|
558
|
+
buffer += decoder.decode(value, { stream: true })
|
|
559
|
+
const lines = buffer.split('\n')
|
|
560
|
+
buffer = lines.pop()!
|
|
561
|
+
await processLines(lines)
|
|
562
|
+
}
|
|
563
|
+
if (buffer.trim()) await processLines([buffer])
|
|
564
|
+
|
|
565
|
+
if (channelId && escrowContract && streamChainId) {
|
|
566
|
+
const signature = await signVoucher(
|
|
567
|
+
client,
|
|
568
|
+
account,
|
|
569
|
+
{ channelId, cumulativeAmount },
|
|
570
|
+
escrowContract,
|
|
571
|
+
streamChainId,
|
|
572
|
+
)
|
|
573
|
+
const closePayload: StreamCredentialPayload = {
|
|
574
|
+
action: 'close',
|
|
575
|
+
channelId,
|
|
576
|
+
cumulativeAmount: cumulativeAmount.toString(),
|
|
577
|
+
signature,
|
|
578
|
+
}
|
|
579
|
+
const closeCred = Credential.serialize({
|
|
580
|
+
challenge,
|
|
581
|
+
payload: closePayload,
|
|
582
|
+
source: `did:pkh:eip155:${streamChainId}:${account.address}`,
|
|
583
|
+
})
|
|
584
|
+
const closeRes = await globalThis.fetch(url, {
|
|
585
|
+
method: 'POST',
|
|
586
|
+
headers: { Authorization: closeCred },
|
|
587
|
+
})
|
|
588
|
+
if (closeRes.ok) {
|
|
589
|
+
info(
|
|
590
|
+
`\n${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(cumulativeAmount, tokenSymbol, tokenDecimals)}.`)}\n`,
|
|
591
|
+
)
|
|
592
|
+
} else {
|
|
593
|
+
info(
|
|
594
|
+
`\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`,
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
const body = (await credentialResponse.text()).replace(/\n+$/, '')
|
|
600
|
+
console.log(body)
|
|
601
|
+
|
|
602
|
+
const shouldClose =
|
|
603
|
+
challenge.intent === 'session' &&
|
|
604
|
+
credentialResponse.ok &&
|
|
605
|
+
streamChannelId &&
|
|
606
|
+
streamEscrowContract &&
|
|
607
|
+
streamChainId
|
|
608
|
+
if (shouldClose && !options.yes) {
|
|
609
|
+
info('\n')
|
|
610
|
+
}
|
|
611
|
+
if (shouldClose && !(options.yes || (await confirm('Close channel?')))) {
|
|
612
|
+
info(`${pc.dim('Kept channel open.')}\n`)
|
|
613
|
+
} else if (shouldClose) {
|
|
614
|
+
const signature = await signVoucher(
|
|
615
|
+
client,
|
|
616
|
+
account,
|
|
617
|
+
{ channelId: streamChannelId!, cumulativeAmount: streamCumulativeAmount },
|
|
618
|
+
streamEscrowContract!,
|
|
619
|
+
streamChainId,
|
|
620
|
+
)
|
|
621
|
+
const closePayload: StreamCredentialPayload = {
|
|
622
|
+
action: 'close',
|
|
623
|
+
channelId: streamChannelId!,
|
|
624
|
+
cumulativeAmount: streamCumulativeAmount.toString(),
|
|
625
|
+
signature,
|
|
626
|
+
}
|
|
627
|
+
const closeCred = Credential.serialize({
|
|
628
|
+
challenge,
|
|
629
|
+
payload: closePayload,
|
|
630
|
+
source: `did:pkh:eip155:${streamChainId}:${account.address}`,
|
|
631
|
+
})
|
|
632
|
+
const closeRes = await globalThis.fetch(url, {
|
|
633
|
+
...fetchInit,
|
|
634
|
+
headers: {
|
|
635
|
+
...(fetchInit.headers as Record<string, string>),
|
|
636
|
+
Authorization: closeCred,
|
|
637
|
+
},
|
|
638
|
+
})
|
|
639
|
+
if (closeRes.ok) {
|
|
640
|
+
deleteChannelState(streamChannelId!)
|
|
641
|
+
info(
|
|
642
|
+
`${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(streamCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}\n`,
|
|
643
|
+
)
|
|
644
|
+
} else {
|
|
645
|
+
const closeBody = await closeRes.text().catch(() => '')
|
|
646
|
+
info(
|
|
647
|
+
`${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`,
|
|
648
|
+
)
|
|
649
|
+
info(
|
|
650
|
+
`${pc.dim(` channelId: ${streamChannelId}`)}\n` +
|
|
651
|
+
`${pc.dim(` cumulativeAmount: ${streamCumulativeAmount}`)}\n` +
|
|
652
|
+
`${pc.dim(` escrowContract: ${streamEscrowContract}`)}\n` +
|
|
653
|
+
`${pc.dim(` chainId: ${streamChainId}`)}\n` +
|
|
654
|
+
`${pc.dim(` account: ${account.address}`)}\n` +
|
|
655
|
+
`${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`,
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
} catch (err) {
|
|
662
|
+
// TODO: revert cast when https://github.com/wevm/zile/pull/26 is merged
|
|
663
|
+
const errCause =
|
|
664
|
+
err instanceof Error ? (err as unknown as Record<string, unknown>).cause : undefined
|
|
665
|
+
const cause = errCause instanceof Error ? errCause : undefined
|
|
666
|
+
|
|
667
|
+
if (cause && 'code' in cause) {
|
|
668
|
+
const code = cause.code as string
|
|
669
|
+
if (code === 'ENOTFOUND')
|
|
670
|
+
console.error(`Could not resolve host "${hostname}". Check the URL and try again.`)
|
|
671
|
+
else if (code === 'ECONNREFUSED')
|
|
672
|
+
console.error(`Connection refused by "${hostname}". Is the server running?`)
|
|
673
|
+
else if (code === 'ECONNRESET') console.error(`Connection to "${hostname}" was reset.`)
|
|
674
|
+
else if (code === 'ETIMEDOUT') console.error(`Connection to "${hostname}" timed out.`)
|
|
675
|
+
else if (code === 'CERT_HAS_EXPIRED' || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE')
|
|
676
|
+
console.error(
|
|
677
|
+
`TLS certificate error for "${hostname}". Use --insecure to skip verification.`,
|
|
678
|
+
)
|
|
679
|
+
else {
|
|
680
|
+
console.error(`Request to "${hostname}" failed: ${cause.message}`)
|
|
681
|
+
}
|
|
682
|
+
} else {
|
|
683
|
+
console.error('Request failed:', err instanceof Error ? err.message : err)
|
|
684
|
+
if (cause) console.error('Cause:', cause.message)
|
|
685
|
+
}
|
|
686
|
+
process.exit(1)
|
|
687
|
+
}
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
const accountOptionsSchema = z.object({
|
|
691
|
+
account: z.optional(z.string()),
|
|
692
|
+
rpcUrl: z.optional(z.string()),
|
|
693
|
+
yes: z.optional(z.boolean()),
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
cli
|
|
697
|
+
.command('account [action]', 'Manage accounts (create, default, delete, fund, list, view)')
|
|
698
|
+
.option('-a, --account <name>', 'Account name (env: MPPX_ACCOUNT)')
|
|
699
|
+
.option(
|
|
700
|
+
'-r, --rpc-url <url>',
|
|
701
|
+
'RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)',
|
|
702
|
+
)
|
|
703
|
+
.option('--yes', 'DANGER!! Skip confirmation prompts')
|
|
704
|
+
.action(async (action: string | undefined, rawOptions: unknown) => {
|
|
705
|
+
if (!action) {
|
|
706
|
+
cli.outputHelp()
|
|
707
|
+
return
|
|
708
|
+
}
|
|
709
|
+
const options = parseOptions(accountOptionsSchema, rawOptions)
|
|
710
|
+
switch (action) {
|
|
711
|
+
case 'create': {
|
|
712
|
+
let resolvedName = options.account
|
|
713
|
+
if (!resolvedName) {
|
|
714
|
+
const existing = await createKeychain().list()
|
|
715
|
+
if (existing.length === 0) resolvedName = 'main'
|
|
716
|
+
else {
|
|
717
|
+
const input = await prompt('Account name')
|
|
718
|
+
if (!input) return
|
|
719
|
+
resolvedName = input
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
let keychain = createKeychain(resolvedName)
|
|
723
|
+
while (await keychain.get()) {
|
|
724
|
+
process.stderr.write(`${pc.dim(`Account "${resolvedName}" already exists.`)}\n\n`)
|
|
725
|
+
const input = await prompt('Enter different name')
|
|
726
|
+
if (!input) return
|
|
727
|
+
resolvedName = input
|
|
728
|
+
keychain = createKeychain(resolvedName)
|
|
729
|
+
}
|
|
730
|
+
const privateKey = generatePrivateKey()
|
|
731
|
+
const account = privateKeyToAccount(privateKey)
|
|
732
|
+
await keychain.set(privateKey)
|
|
733
|
+
const accounts = await createKeychain().list()
|
|
734
|
+
if (accounts.length === 1) createDefaultStore().set(resolvedName)
|
|
735
|
+
console.log(`Account "${resolvedName}" saved to keychain.`)
|
|
736
|
+
console.log(pc.dim(`Address ${account.address}`))
|
|
737
|
+
resolveChain(options)
|
|
738
|
+
.then((chain) => createClient({ chain, transport: http(options.rpcUrl) }))
|
|
739
|
+
.then((client) =>
|
|
740
|
+
import('viem/tempo').then(({ Actions }) =>
|
|
741
|
+
Actions.faucet.fund(client, { account }).catch(() => {}),
|
|
742
|
+
),
|
|
743
|
+
)
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
case 'default': {
|
|
747
|
+
const accountName = options.account
|
|
748
|
+
if (!accountName) {
|
|
749
|
+
console.error('-a, --account <name> is required for default.')
|
|
750
|
+
process.exit(1)
|
|
751
|
+
}
|
|
752
|
+
const key = await createKeychain(accountName).get()
|
|
753
|
+
if (!key) {
|
|
754
|
+
console.log(`Account "${accountName}" not found.`)
|
|
755
|
+
process.exit(1)
|
|
756
|
+
}
|
|
757
|
+
createDefaultStore().set(accountName)
|
|
758
|
+
console.log(`Default account set to "${accountName}"`)
|
|
759
|
+
return
|
|
760
|
+
}
|
|
761
|
+
case 'delete': {
|
|
762
|
+
if (!options.account) {
|
|
763
|
+
console.error('-a, --account <name> is required for delete.')
|
|
764
|
+
process.exit(1)
|
|
765
|
+
}
|
|
766
|
+
const keychain = createKeychain(options.account)
|
|
767
|
+
const key = await keychain.get()
|
|
768
|
+
if (!key) {
|
|
769
|
+
console.log(`Account "${options.account}" not found.`)
|
|
770
|
+
process.exit(1)
|
|
771
|
+
}
|
|
772
|
+
const account = privateKeyToAccount(key as `0x${string}`)
|
|
773
|
+
const balanceLines = await fetchBalanceLines(account.address, { includeTestnet: false })
|
|
774
|
+
if (!options.yes) {
|
|
775
|
+
process.stderr.write(pc.dim(`Delete account "${options.account}"\n`))
|
|
776
|
+
process.stderr.write(pc.dim(` Address ${account.address}\n`))
|
|
777
|
+
for (let i = 0; i < balanceLines.length; i++)
|
|
778
|
+
process.stderr.write(
|
|
779
|
+
pc.dim(` ${i === 0 ? 'Balance' : ' '} ${balanceLines[i]}\n`),
|
|
780
|
+
)
|
|
781
|
+
process.stderr.write(pc.dim('This action cannot be undone\n\n'))
|
|
782
|
+
const confirmed = await confirm('Confirm delete?')
|
|
783
|
+
if (!confirmed) {
|
|
784
|
+
console.log('Canceled')
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
await keychain.delete()
|
|
789
|
+
const currentDefault = createDefaultStore().get()
|
|
790
|
+
if (currentDefault === options.account) {
|
|
791
|
+
const remaining = await createKeychain().list()
|
|
792
|
+
if (remaining.length > 0) {
|
|
793
|
+
createDefaultStore().set(remaining[0]!)
|
|
794
|
+
console.log(`Default account set to "${remaining[0]}"`)
|
|
795
|
+
} else {
|
|
796
|
+
createDefaultStore().clear()
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
console.log(`Account "${options.account}" deleted`)
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
case 'fund': {
|
|
803
|
+
const accountName = resolveAccountName(options.account)
|
|
804
|
+
const keychain = createKeychain(accountName)
|
|
805
|
+
const key = await keychain.get()
|
|
806
|
+
if (!key) {
|
|
807
|
+
if (options.account) console.log(`Account "${accountName}" not found.`)
|
|
808
|
+
else console.log(`No account found.`)
|
|
809
|
+
process.exit(1)
|
|
810
|
+
}
|
|
811
|
+
const account = privateKeyToAccount(key as `0x${string}`)
|
|
812
|
+
const chain = await resolveChain(options)
|
|
813
|
+
const client = createClient({ chain, transport: http(options.rpcUrl) })
|
|
814
|
+
console.log(`Funding "${accountName}" on ${chainName(chain)}`)
|
|
815
|
+
try {
|
|
816
|
+
const { Actions } = await import('viem/tempo')
|
|
817
|
+
const hashes = await Actions.faucet.fund(client, { account })
|
|
818
|
+
const explorerUrl = chain.blockExplorers?.default?.url
|
|
819
|
+
for (const hash of hashes) {
|
|
820
|
+
const label = explorerUrl ? pc.link(`${explorerUrl}/tx/${hash}`, pc.gray(hash)) : hash
|
|
821
|
+
console.log(` ${label}`)
|
|
822
|
+
}
|
|
823
|
+
const { waitForTransactionReceipt } = await import('viem/actions')
|
|
824
|
+
await Promise.all(hashes.map((hash) => waitForTransactionReceipt(client, { hash })))
|
|
825
|
+
console.log('Funded successfully')
|
|
826
|
+
} catch (err) {
|
|
827
|
+
console.error('Funding failed:', err instanceof Error ? err.message : err)
|
|
828
|
+
}
|
|
829
|
+
return
|
|
830
|
+
}
|
|
831
|
+
case 'list': {
|
|
832
|
+
const currentDefault = createDefaultStore().get()
|
|
833
|
+
const accounts = (await createKeychain().list()).sort()
|
|
834
|
+
if (accounts.length === 0) {
|
|
835
|
+
console.log(`No accounts found.`)
|
|
836
|
+
return
|
|
837
|
+
}
|
|
838
|
+
const entries = await Promise.all(
|
|
839
|
+
accounts.map(async (accountName) => {
|
|
840
|
+
const key = await createKeychain(accountName).get()
|
|
841
|
+
if (!key) return undefined
|
|
842
|
+
return {
|
|
843
|
+
name: accountName,
|
|
844
|
+
address: privateKeyToAccount(key as `0x${string}`).address,
|
|
845
|
+
}
|
|
846
|
+
}),
|
|
847
|
+
)
|
|
848
|
+
const resolved = entries.filter((e) => e !== undefined)
|
|
849
|
+
const maxWidth = Math.max(
|
|
850
|
+
...resolved.map((e) => e.name.length + (e.name === currentDefault ? 1 : 0)),
|
|
851
|
+
)
|
|
852
|
+
for (const entry of resolved) {
|
|
853
|
+
const isDefault = entry.name === currentDefault
|
|
854
|
+
const label = isDefault ? `${entry.name}${pc.dim('*')}` : entry.name
|
|
855
|
+
const width = entry.name.length + (isDefault ? 1 : 0)
|
|
856
|
+
console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(entry.address)}`)
|
|
857
|
+
}
|
|
858
|
+
return
|
|
859
|
+
}
|
|
860
|
+
case 'view': {
|
|
861
|
+
const accountName = resolveAccountName(options.account)
|
|
862
|
+
const keychain = createKeychain(accountName)
|
|
863
|
+
const key = await keychain.get()
|
|
864
|
+
if (!key) {
|
|
865
|
+
if (options.account) console.log(`Account "${accountName}" not found.`)
|
|
866
|
+
else console.log(`No account found.`)
|
|
867
|
+
process.exit(1)
|
|
868
|
+
}
|
|
869
|
+
const account = privateKeyToAccount(key as `0x${string}`)
|
|
870
|
+
console.log(`${pc.dim('Address')} ${account.address}`)
|
|
871
|
+
|
|
872
|
+
const rpcUrl = options.rpcUrl ?? process.env.RPC_URL
|
|
873
|
+
const chain = rpcUrl ? await resolveChain({ rpcUrl }) : undefined
|
|
874
|
+
const balanceLines = await fetchBalanceLines(
|
|
875
|
+
account.address,
|
|
876
|
+
chain && rpcUrl ? { chain, rpcUrl } : undefined,
|
|
877
|
+
)
|
|
878
|
+
for (let i = 0; i < balanceLines.length; i++)
|
|
879
|
+
console.log(`${pc.dim(i === 0 ? 'Balance' : ' ')} ${balanceLines[i]}`)
|
|
880
|
+
|
|
881
|
+
console.log(`${pc.dim('Name')} ${accountName}`)
|
|
882
|
+
return
|
|
883
|
+
}
|
|
884
|
+
default:
|
|
885
|
+
console.error(`Unknown action: ${action}`)
|
|
886
|
+
console.error('Available: create, default, delete, fund, list, view')
|
|
887
|
+
process.exit(1)
|
|
888
|
+
}
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
cli.version(version, '-V, --version')
|
|
892
|
+
|
|
893
|
+
cli.help((sections) => {
|
|
894
|
+
const isAccount = sections.some((s: { body?: string }) => s.body?.includes('$ mppx account'))
|
|
895
|
+
if (isAccount) {
|
|
896
|
+
const actionsSection = {
|
|
897
|
+
title: 'Actions',
|
|
898
|
+
body: [
|
|
899
|
+
' create Create new account',
|
|
900
|
+
' default Set default account',
|
|
901
|
+
' delete Delete account',
|
|
902
|
+
' fund Fund account with testnet tokens',
|
|
903
|
+
' list List all accounts',
|
|
904
|
+
' view View account address',
|
|
905
|
+
].join('\n'),
|
|
906
|
+
}
|
|
907
|
+
const optionsIndex = sections.findIndex((s: { title?: string }) => s.title === 'Options')
|
|
908
|
+
if (optionsIndex !== -1) sections.splice(optionsIndex, 0, actionsSection)
|
|
909
|
+
else sections.push(actionsSection)
|
|
910
|
+
}
|
|
911
|
+
return sections
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
cli.parse()
|
|
916
|
+
} catch (err) {
|
|
917
|
+
console.error(err instanceof Error ? err.message : err)
|
|
918
|
+
process.exit(1)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
922
|
+
|
|
923
|
+
function parseOptions<const schema extends ZodMiniType>(
|
|
924
|
+
schema: schema,
|
|
925
|
+
rawOptions: unknown,
|
|
926
|
+
): z.output<schema> {
|
|
927
|
+
const result = schema.safeParse(rawOptions ?? {})
|
|
928
|
+
if (result.success) return result.data
|
|
929
|
+
const summary = result.error.issues
|
|
930
|
+
.map((issue) => {
|
|
931
|
+
const path = issue.path.length ? issue.path.join('.') : 'options'
|
|
932
|
+
return `${path}: ${issue.message}`
|
|
933
|
+
})
|
|
934
|
+
.join(', ')
|
|
935
|
+
throw new Error(`Invalid CLI options (${summary})`)
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function execCommand(
|
|
939
|
+
command: string,
|
|
940
|
+
args: string[],
|
|
941
|
+
): Promise<{ stdout: string; stderr: string; error: Error | null }> {
|
|
942
|
+
return new Promise((resolve) => {
|
|
943
|
+
child.execFile(command, args, (error, stdout, stderr) => {
|
|
944
|
+
resolve({ stdout: stdout.trim(), stderr: stderr.trim(), error })
|
|
945
|
+
})
|
|
946
|
+
})
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function channelStateDir() {
|
|
950
|
+
return path.join(
|
|
951
|
+
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
|
|
952
|
+
'mppx',
|
|
953
|
+
'channels',
|
|
954
|
+
)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function readChannelCumulative(channelId: string): bigint | undefined {
|
|
958
|
+
try {
|
|
959
|
+
const raw = fs.readFileSync(path.join(channelStateDir(), channelId), 'utf-8').trim()
|
|
960
|
+
return raw ? BigInt(raw) : undefined
|
|
961
|
+
} catch {
|
|
962
|
+
return undefined
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function writeChannelCumulative(channelId: string, cumulative: bigint): void {
|
|
967
|
+
const dir = channelStateDir()
|
|
968
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
969
|
+
fs.writeFileSync(path.join(dir, channelId), cumulative.toString(), 'utf-8')
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function deleteChannelState(channelId: string): void {
|
|
973
|
+
try {
|
|
974
|
+
fs.unlinkSync(path.join(channelStateDir(), channelId))
|
|
975
|
+
} catch {}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function createDefaultStore() {
|
|
979
|
+
const configPath = path.join(
|
|
980
|
+
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
|
|
981
|
+
'mppx',
|
|
982
|
+
'default',
|
|
983
|
+
)
|
|
984
|
+
return {
|
|
985
|
+
get(): string {
|
|
986
|
+
try {
|
|
987
|
+
return fs.readFileSync(configPath, 'utf-8').trim() || 'main'
|
|
988
|
+
} catch {
|
|
989
|
+
return 'main'
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
set(value: string): void {
|
|
993
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
994
|
+
fs.writeFileSync(configPath, value, 'utf-8')
|
|
995
|
+
},
|
|
996
|
+
clear(): void {
|
|
997
|
+
try {
|
|
998
|
+
fs.unlinkSync(configPath)
|
|
999
|
+
} catch {}
|
|
1000
|
+
},
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function resolveAccountName(explicit?: string): string {
|
|
1005
|
+
if (explicit) return explicit
|
|
1006
|
+
if (process.env.MPPX_ACCOUNT) return process.env.MPPX_ACCOUNT
|
|
1007
|
+
return createDefaultStore().get()
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// biome-ignore format: compact shell commands
|
|
1011
|
+
function createKeychain(account = 'main') {
|
|
1012
|
+
const service = name
|
|
1013
|
+
return {
|
|
1014
|
+
async list(): Promise<string[]> {
|
|
1015
|
+
const platform = os.platform()
|
|
1016
|
+
if (platform === 'darwin') {
|
|
1017
|
+
const { stdout, error } = await execCommand('security', ['dump-keychain'])
|
|
1018
|
+
if (error) return []
|
|
1019
|
+
const accounts: string[] = []
|
|
1020
|
+
const blocks = stdout.split('keychain:')
|
|
1021
|
+
for (const block of blocks) {
|
|
1022
|
+
const serviceMatch = block.match(/"svce"<blob>="([^"]*)"/)
|
|
1023
|
+
const accountMatch = block.match(/"acct"<blob>="([^"]*)"/)
|
|
1024
|
+
if (serviceMatch?.[1] === service && accountMatch?.[1]) accounts.push(accountMatch[1])
|
|
1025
|
+
}
|
|
1026
|
+
return accounts
|
|
1027
|
+
}
|
|
1028
|
+
if (platform === 'linux') {
|
|
1029
|
+
const { stdout, stderr, error } = await execCommand('secret-tool', ['search', '--all', '--unlock', 'service', service])
|
|
1030
|
+
if (error) return []
|
|
1031
|
+
const combined = `${stdout}\n${stderr}`
|
|
1032
|
+
const accounts: string[] = []
|
|
1033
|
+
const matches = combined.matchAll(/\baccount = (.+)/g)
|
|
1034
|
+
for (const match of matches) if (match[1]) accounts.push(match[1])
|
|
1035
|
+
return accounts
|
|
1036
|
+
}
|
|
1037
|
+
throw new Error(`Unsupported platform: ${platform}`)
|
|
1038
|
+
},
|
|
1039
|
+
async get(): Promise<string | undefined> {
|
|
1040
|
+
const platform = os.platform()
|
|
1041
|
+
if (platform === 'darwin') {
|
|
1042
|
+
const { stdout, error } = await execCommand('security', ['find-generic-password', '-s', service, '-a', account, '-w'])
|
|
1043
|
+
return error ? undefined : stdout
|
|
1044
|
+
}
|
|
1045
|
+
if (platform === 'linux') {
|
|
1046
|
+
const { stdout, error } = await execCommand('secret-tool', ['lookup', 'service', service, 'account', account])
|
|
1047
|
+
return error ? undefined : stdout || undefined
|
|
1048
|
+
}
|
|
1049
|
+
throw new Error(`Unsupported platform: ${platform}`)
|
|
1050
|
+
},
|
|
1051
|
+
async set(value: string): Promise<void> {
|
|
1052
|
+
const platform = os.platform()
|
|
1053
|
+
if (platform === 'darwin') {
|
|
1054
|
+
await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
|
|
1055
|
+
const { error } = await execCommand('security', ['add-generic-password', '-s', service, '-a', account, '-w', value])
|
|
1056
|
+
if (error) throw error
|
|
1057
|
+
return
|
|
1058
|
+
}
|
|
1059
|
+
if (platform === 'linux') {
|
|
1060
|
+
const proc = child.execFile('secret-tool', ['store', '--label', `${service} ${account}`, 'service', service, 'account', account])
|
|
1061
|
+
proc.stdin?.write(value)
|
|
1062
|
+
proc.stdin?.end()
|
|
1063
|
+
return new Promise((resolve, reject) => {
|
|
1064
|
+
proc.on('close', (code) => {
|
|
1065
|
+
if (code === 0) resolve()
|
|
1066
|
+
else reject(new Error(`secret-tool exited with code ${code}`))
|
|
1067
|
+
})
|
|
1068
|
+
proc.on('error', reject)
|
|
1069
|
+
})
|
|
1070
|
+
}
|
|
1071
|
+
throw new Error(`Unsupported platform: ${platform}`)
|
|
1072
|
+
},
|
|
1073
|
+
async delete(): Promise<void> {
|
|
1074
|
+
const platform = os.platform()
|
|
1075
|
+
if (platform === 'darwin') {
|
|
1076
|
+
await execCommand('security', ['delete-generic-password', '-s', service, '-a', account])
|
|
1077
|
+
return
|
|
1078
|
+
}
|
|
1079
|
+
if (platform === 'linux') {
|
|
1080
|
+
await execCommand('secret-tool', ['clear', 'service', service, 'account', account])
|
|
1081
|
+
return
|
|
1082
|
+
}
|
|
1083
|
+
throw new Error(`Unsupported platform: ${platform}`)
|
|
1084
|
+
},
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function prompt(message: string): Promise<string | undefined> {
|
|
1089
|
+
const reader = readline.createInterface({ input: process.stdin, output: process.stderr })
|
|
1090
|
+
return new Promise((resolve) => {
|
|
1091
|
+
reader.on('close', () => resolve(undefined))
|
|
1092
|
+
reader.question(`${pc.bold(`▸ ${message}:`)} `, (answer) => {
|
|
1093
|
+
reader.close()
|
|
1094
|
+
const value = answer.trim()
|
|
1095
|
+
resolve(value || undefined)
|
|
1096
|
+
})
|
|
1097
|
+
})
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function confirm(prompt: string): Promise<boolean> {
|
|
1101
|
+
const reader = readline.createInterface({ input: process.stdin, output: process.stderr })
|
|
1102
|
+
return new Promise((resolve) => {
|
|
1103
|
+
reader.question(`${pc.bold(`▸ ${prompt}`)} ${pc.dim('(y/N)')} `, (answer) => {
|
|
1104
|
+
reader.close()
|
|
1105
|
+
resolve(answer.trim().toLowerCase() === 'y')
|
|
1106
|
+
})
|
|
1107
|
+
})
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Inlined from https://github.com/alexeyraspopov/picocolors (ISC License)
|
|
1111
|
+
const pc = (() => {
|
|
1112
|
+
const p = process || ({} as NodeJS.Process)
|
|
1113
|
+
const argv = p.argv || []
|
|
1114
|
+
const env = p.env || {}
|
|
1115
|
+
const isColorSupported =
|
|
1116
|
+
!(!!env.NO_COLOR || argv.includes('--no-color')) &&
|
|
1117
|
+
(!!env.FORCE_COLOR ||
|
|
1118
|
+
argv.includes('--color') ||
|
|
1119
|
+
((p.stdout || ({} as NodeJS.WriteStream)).isTTY && env.TERM !== 'dumb') ||
|
|
1120
|
+
!!env.CI)
|
|
1121
|
+
|
|
1122
|
+
const replaceClose = (string: string, close: string, replace: string, index: number): string => {
|
|
1123
|
+
let result = ''
|
|
1124
|
+
let cursor = 0
|
|
1125
|
+
let i = index
|
|
1126
|
+
do {
|
|
1127
|
+
result += string.substring(cursor, i) + replace
|
|
1128
|
+
cursor = i + close.length
|
|
1129
|
+
i = string.indexOf(close, cursor)
|
|
1130
|
+
} while (~i)
|
|
1131
|
+
return result + string.substring(cursor)
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const formatter =
|
|
1135
|
+
(open: string, close: string, replace = open) =>
|
|
1136
|
+
(input: unknown) => {
|
|
1137
|
+
const string = `${input}`
|
|
1138
|
+
const index = string.indexOf(close, open.length)
|
|
1139
|
+
return ~index
|
|
1140
|
+
? open + replaceClose(string, close, replace, index) + close
|
|
1141
|
+
: open + string + close
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const f = isColorSupported ? formatter : () => String
|
|
1145
|
+
return {
|
|
1146
|
+
isColorSupported,
|
|
1147
|
+
reset: f('\x1b[0m', '\x1b[0m'),
|
|
1148
|
+
bold: f('\x1b[1m', '\x1b[22m', '\x1b[22m\x1b[1m'),
|
|
1149
|
+
dim: f('\x1b[2m', '\x1b[22m', '\x1b[22m\x1b[2m'),
|
|
1150
|
+
italic: f('\x1b[3m', '\x1b[23m'),
|
|
1151
|
+
underline: f('\x1b[4m', '\x1b[24m'),
|
|
1152
|
+
inverse: f('\x1b[7m', '\x1b[27m'),
|
|
1153
|
+
hidden: f('\x1b[8m', '\x1b[28m'),
|
|
1154
|
+
strikethrough: f('\x1b[9m', '\x1b[29m'),
|
|
1155
|
+
black: f('\x1b[30m', '\x1b[39m'),
|
|
1156
|
+
red: f('\x1b[31m', '\x1b[39m'),
|
|
1157
|
+
green: f('\x1b[32m', '\x1b[39m'),
|
|
1158
|
+
yellow: f('\x1b[33m', '\x1b[39m'),
|
|
1159
|
+
blue: f('\x1b[34m', '\x1b[39m'),
|
|
1160
|
+
magenta: f('\x1b[35m', '\x1b[39m'),
|
|
1161
|
+
cyan: f('\x1b[36m', '\x1b[39m'),
|
|
1162
|
+
white: f('\x1b[37m', '\x1b[39m'),
|
|
1163
|
+
gray: f('\x1b[90m', '\x1b[39m'),
|
|
1164
|
+
bgBlack: f('\x1b[40m', '\x1b[49m'),
|
|
1165
|
+
bgRed: f('\x1b[41m', '\x1b[49m'),
|
|
1166
|
+
bgGreen: f('\x1b[42m', '\x1b[49m'),
|
|
1167
|
+
bgYellow: f('\x1b[43m', '\x1b[49m'),
|
|
1168
|
+
bgBlue: f('\x1b[44m', '\x1b[49m'),
|
|
1169
|
+
bgMagenta: f('\x1b[45m', '\x1b[49m'),
|
|
1170
|
+
bgCyan: f('\x1b[46m', '\x1b[49m'),
|
|
1171
|
+
bgWhite: f('\x1b[47m', '\x1b[49m'),
|
|
1172
|
+
blackBright: f('\x1b[90m', '\x1b[39m'),
|
|
1173
|
+
redBright: f('\x1b[91m', '\x1b[39m'),
|
|
1174
|
+
greenBright: f('\x1b[92m', '\x1b[39m'),
|
|
1175
|
+
yellowBright: f('\x1b[93m', '\x1b[39m'),
|
|
1176
|
+
blueBright: f('\x1b[94m', '\x1b[39m'),
|
|
1177
|
+
magentaBright: f('\x1b[95m', '\x1b[39m'),
|
|
1178
|
+
cyanBright: f('\x1b[96m', '\x1b[39m'),
|
|
1179
|
+
whiteBright: f('\x1b[97m', '\x1b[39m'),
|
|
1180
|
+
bgBlackBright: f('\x1b[100m', '\x1b[49m'),
|
|
1181
|
+
bgRedBright: f('\x1b[101m', '\x1b[49m'),
|
|
1182
|
+
bgGreenBright: f('\x1b[102m', '\x1b[49m'),
|
|
1183
|
+
bgYellowBright: f('\x1b[103m', '\x1b[49m'),
|
|
1184
|
+
bgBlueBright: f('\x1b[104m', '\x1b[49m'),
|
|
1185
|
+
bgMagentaBright: f('\x1b[105m', '\x1b[49m'),
|
|
1186
|
+
bgCyanBright: f('\x1b[106m', '\x1b[49m'),
|
|
1187
|
+
bgWhiteBright: f('\x1b[107m', '\x1b[49m'),
|
|
1188
|
+
link(url: string, text: string) {
|
|
1189
|
+
if (!isColorSupported) return text
|
|
1190
|
+
return `\x1b]8;;${url}\x07${pc.underline(text)}\x1b]8;;\x07`
|
|
1191
|
+
},
|
|
1192
|
+
}
|
|
1193
|
+
})()
|
|
1194
|
+
|
|
1195
|
+
async function resolveChain(opts: { rpcUrl?: string | undefined } = {}): Promise<Chain> {
|
|
1196
|
+
if (!opts.rpcUrl) return tempoModerato
|
|
1197
|
+
const { getChainId } = await import('viem/actions')
|
|
1198
|
+
const chainId = await getChainId(createClient({ transport: http(opts.rpcUrl) }))
|
|
1199
|
+
const allExports = Object.values(await import('viem/chains')) as unknown[]
|
|
1200
|
+
const candidates = allExports.filter(
|
|
1201
|
+
(c): c is Chain =>
|
|
1202
|
+
typeof c === 'object' && c !== null && 'id' in c && (c as Chain).id === chainId,
|
|
1203
|
+
)
|
|
1204
|
+
const found = candidates.find((c) => 'serializers' in c && c.serializers) ?? candidates[0]
|
|
1205
|
+
if (!found) throw new Error(`Unknown chain ID ${chainId} from RPC ${opts.rpcUrl}`)
|
|
1206
|
+
return found
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function chainName(chain: { id: number; name: string }) {
|
|
1210
|
+
const chainNames: Record<number, string> = {
|
|
1211
|
+
[tempoMainnet.id]: 'mainnet',
|
|
1212
|
+
[tempoModerato.id]: 'testnet',
|
|
1213
|
+
}
|
|
1214
|
+
return chainNames[chain.id] ?? chain.name
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const pathUsd = '0x20c0000000000000000000000000000000000000' as Address
|
|
1218
|
+
const testnetTokens = [
|
|
1219
|
+
'0x20c0000000000000000000000000000000000000',
|
|
1220
|
+
'0x20c0000000000000000000000000000000000001',
|
|
1221
|
+
'0x20c0000000000000000000000000000000000002',
|
|
1222
|
+
'0x20c0000000000000000000000000000000000003',
|
|
1223
|
+
] as const
|
|
1224
|
+
|
|
1225
|
+
function fmtBalance(b: bigint, symbol: string, decimals = 6) {
|
|
1226
|
+
const value = Number(b) / 10 ** decimals
|
|
1227
|
+
const [int, dec] = value.toString().split('.')
|
|
1228
|
+
const formatted = int!.replace(/\B(?=(\d{3})+(?!\d))/g, '_')
|
|
1229
|
+
return `${dec ? `${formatted}.${dec}` : formatted} ${pc.dim(symbol)}`
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function isTestnet(chain: Chain) {
|
|
1233
|
+
return chain.id !== tempoMainnet.id
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
async function fetchTokenInfo(
|
|
1237
|
+
client: ReturnType<typeof createClient>,
|
|
1238
|
+
token: Address,
|
|
1239
|
+
account: Address,
|
|
1240
|
+
) {
|
|
1241
|
+
const { Actions } = await import('viem/tempo')
|
|
1242
|
+
const [balance, metadata] = await Promise.all([
|
|
1243
|
+
Actions.token.getBalance(client, { account, token }).catch(() => 0n),
|
|
1244
|
+
Actions.token.getMetadata(client, { token }).catch(() => ({ symbol: token as string })),
|
|
1245
|
+
])
|
|
1246
|
+
const symbol = token === pathUsd ? 'PathUSD' : metadata.symbol
|
|
1247
|
+
const decimals = 'decimals' in metadata ? metadata.decimals : 6
|
|
1248
|
+
return { balance, symbol, decimals }
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function detectTerminalBg(
|
|
1252
|
+
timeoutMs = 100,
|
|
1253
|
+
): Promise<{ r: number; g: number; b: number } | undefined> {
|
|
1254
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return Promise.resolve(undefined)
|
|
1255
|
+
return new Promise((resolve) => {
|
|
1256
|
+
const wasRaw = process.stdin.isRaw
|
|
1257
|
+
let buf = ''
|
|
1258
|
+
const cleanup = () => {
|
|
1259
|
+
clearTimeout(timer)
|
|
1260
|
+
process.stdin.removeListener('data', onData)
|
|
1261
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false)
|
|
1262
|
+
process.stdin.pause()
|
|
1263
|
+
}
|
|
1264
|
+
const timer = setTimeout(() => {
|
|
1265
|
+
cleanup()
|
|
1266
|
+
resolve(undefined)
|
|
1267
|
+
}, timeoutMs)
|
|
1268
|
+
const onData = (data: Buffer) => {
|
|
1269
|
+
buf += data.toString()
|
|
1270
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence for terminal background detection
|
|
1271
|
+
const match = buf.match(/\x1b\]11;rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i)
|
|
1272
|
+
if (!match) return
|
|
1273
|
+
cleanup()
|
|
1274
|
+
const parse = (hex: string) => Number.parseInt(hex.slice(0, 2), 16)
|
|
1275
|
+
resolve({ r: parse(match[1]!), g: parse(match[2]!), b: parse(match[3]!) })
|
|
1276
|
+
}
|
|
1277
|
+
process.stdin.setRawMode(true)
|
|
1278
|
+
process.stdin.resume()
|
|
1279
|
+
process.stdin.on('data', onData)
|
|
1280
|
+
process.stdout.write('\x1b]11;?\x07')
|
|
1281
|
+
})
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async function fetchBalanceLines(
|
|
1285
|
+
address: Address,
|
|
1286
|
+
opts?: { chain?: Chain; rpcUrl?: string; includeTestnet?: boolean },
|
|
1287
|
+
): Promise<string[]> {
|
|
1288
|
+
if (opts?.chain) {
|
|
1289
|
+
const client = createClient({ chain: opts.chain, transport: http(opts.rpcUrl) })
|
|
1290
|
+
const label = pc.dim(`(${chainName(opts.chain)})`)
|
|
1291
|
+
if (isTestnet(opts.chain)) {
|
|
1292
|
+
const results = await Promise.all(
|
|
1293
|
+
testnetTokens.map((token) => fetchTokenInfo(client, token, address)),
|
|
1294
|
+
)
|
|
1295
|
+
return results
|
|
1296
|
+
.filter((t) => t.balance > 0n)
|
|
1297
|
+
.map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals)} ${label}`)
|
|
1298
|
+
}
|
|
1299
|
+
const { balance, symbol, decimals } = await fetchTokenInfo(client, pathUsd, address)
|
|
1300
|
+
return [`${fmtBalance(balance, symbol, decimals)} ${label}`]
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const mainnetClient = createClient({ chain: tempoMainnet, transport: http() })
|
|
1304
|
+
const mainnetInfo = await fetchTokenInfo(mainnetClient, pathUsd, address)
|
|
1305
|
+
const lines = [fmtBalance(mainnetInfo.balance, mainnetInfo.symbol, mainnetInfo.decimals)]
|
|
1306
|
+
|
|
1307
|
+
if (opts?.includeTestnet !== false) {
|
|
1308
|
+
const testnetClient = createClient({ chain: tempoModerato, transport: http() })
|
|
1309
|
+
const testnetResults = await Promise.all(
|
|
1310
|
+
testnetTokens.map((token) => fetchTokenInfo(testnetClient, token, address)),
|
|
1311
|
+
)
|
|
1312
|
+
for (const t of testnetResults) {
|
|
1313
|
+
if (t.balance > 0n)
|
|
1314
|
+
lines.push(`${fmtBalance(t.balance, t.symbol, t.decimals)} ${pc.dim('(testnet)')}`)
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
return lines
|
|
1319
|
+
}
|