From 15ed7d6358a05e0994941b256bc646331cfad321 Mon Sep 17 00:00:00 2001 From: Brendan Ryan <1572504+brendanjryan@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:26:32 -0700 Subject: [PATCH] fix: support P-256 V2 session vouchers --- .changelog/fix-p256-v2-session-vouchers.md | 5 + package.json | 4 +- pnpm-lock.yaml | 108 ++++++++++++++++++--- src/commands/fund.ts | 10 +- src/commands/request.ts | 12 ++- src/commands/transfer.ts | 2 +- test/request.test.ts | 61 +++++++++++- 7 files changed, 174 insertions(+), 28 deletions(-) create mode 100644 .changelog/fix-p256-v2-session-vouchers.md diff --git a/.changelog/fix-p256-v2-session-vouchers.md b/.changelog/fix-p256-v2-session-vouchers.md new file mode 100644 index 0000000..2b82d2b --- /dev/null +++ b/.changelog/fix-p256-v2-session-vouchers.md @@ -0,0 +1,5 @@ +--- +wallet-cli: patch +--- + +Support P-256 access keys for V2 MPP session vouchers. diff --git a/package.json b/package.json index 14dc982..85cc4e8 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,9 @@ "dependencies": { "accounts": "0.14.11", "incur": "0.4.8", - "mppx": "0.7.0", + "mppx": "0.8.9", "undici": "8.5.0", - "viem": "2.53.1" + "viem": "2.54.0" }, "devDependencies": { "@types/node": "25.9.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd3ee6d..9b524c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,19 +14,19 @@ importers: dependencies: accounts: specifier: 0.14.11 - version: 0.14.11(typescript@6.0.3)(viem@2.53.1(typescript@6.0.3)(zod@4.4.3)) + version: 0.14.11(typescript@6.0.3)(viem@2.54.0(typescript@6.0.3)(zod@4.4.3)) incur: specifier: 0.4.8 version: 0.4.8 mppx: - specifier: 0.7.0 - version: 0.7.0(hono@4.12.25)(typescript@6.0.3)(viem@2.53.1(typescript@6.0.3)(zod@4.4.3)) + specifier: 0.8.9 + version: 0.8.9(hono@4.12.25)(typescript@6.0.3)(viem@2.54.0(typescript@6.0.3)(zod@4.4.3)) undici: specifier: 8.5.0 version: 8.5.0 viem: - specifier: 2.53.1 - version: 2.53.1(typescript@6.0.3)(zod@4.4.3) + specifier: 2.54.0 + version: 2.54.0(typescript@6.0.3)(zod@4.4.3) devDependencies: '@types/node': specifier: 25.9.2 @@ -728,6 +728,10 @@ packages: resolution: {integrity: sha512-r1ElolvWXM4aYnZZVHvKW3EDL8JcwEuIgTuWxlB5lvC+YsvjkQ0gX35x9d8dTDubX395fViLVqkaolVs1PmIQQ==} engines: {node: '>=12.16'} + '@stripe/stripe-js@9.9.0': + resolution: {integrity: sha512-Vwqe6Q5cU4i82tPyAv2BpaW/fQSNdOSO4/J8EeDLPp5/oIZiMmdB+Hgh863zFH+rtoxpuWGvD1L7QPh8k1Rdvw==} + engines: {node: '>=12.16'} + '@toon-format/toon@2.3.0': resolution: {integrity: sha512-/Ew9etdRQKVMnm9fDaCG0JjyAOK/O7T0M97oum1aW4W+UR8ZhVVPBanIV7oWgHBiGlnVxV9M55PWQCHofDV07w==} @@ -1153,6 +1157,10 @@ packages: events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -1216,6 +1224,11 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + incur@0.4.13: + resolution: {integrity: sha512-BeKlYFLIsRCgC8IxsUd3S2/4kPeZaNf1Rbz+Kz41jQII4jOgr7j5w4KYe00aAEQa3V0Ur57BVxE5JDTx8L2s5Q==} + engines: {node: '>=22'} + hasBin: true + incur@0.4.8: resolution: {integrity: sha512-SjW2QNtY7Bcvqjj0KvOJ3qiuFATlC3mEpYQyUzWdLb9MAzakkQ1KQg5WZ/yy2tDGBmHLN/zUJNimIWEqkRfqdw==} engines: {node: '>=22'} @@ -1383,6 +1396,25 @@ packages: hono: optional: true + mppx@0.8.9: + resolution: {integrity: sha512-OkDecYp/nHjyGKBzEmOebMlW/gXRxQRvTVjPX5f6indEhkQ0rlXl+8v2aRfbS9ckMWY8i5ulJzYfLipJ82TEkw==} + hasBin: true + peerDependencies: + '@modelcontextprotocol/sdk': '>=1.25.0' + elysia: '>=1' + express: '>=5' + hono: '>=4.12.25' + viem: '>=2.54.0' + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + elysia: + optional: true + express: + optional: true + hono: + optional: true + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -1440,6 +1472,14 @@ packages: typescript: optional: true + ox@0.14.30: + resolution: {integrity: sha512-LI11uu+8iiM1B3CLckgd++YF1a0A2k5wDoM9ZeQMiL21BOzQs6L//BLS6hb1HSEKCyycdDIQLsVQx9MjpcC0hA==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + oxfmt@0.52.0: resolution: {integrity: sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1692,8 +1732,8 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - viem@2.53.1: - resolution: {integrity: sha512-FhfJ/SW73CVosiyVLmIMVgKDRKYV1AGCLzZoHYvmNayyVff63Qi1ocPCk59LqC/cNw244RbBJjHnmxqXkE7NpA==} + viem@2.54.0: + resolution: {integrity: sha512-DW6KW3m89+3MLozFNPuI9Nhz2hJw375hUo1bpbo3qXipBvjdjF26B/d3U5adVyLGKOfqK4XeA1wPDWQTFQ/ltA==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -2281,6 +2321,8 @@ snapshots: '@stripe/stripe-js@9.7.0': {} + '@stripe/stripe-js@9.9.0': {} + '@toon-format/toon@2.3.0': {} '@tybys/wasm-util@0.10.2': @@ -2474,20 +2516,20 @@ snapshots: typescript: 6.0.3 zod: 4.4.3 - accounts@0.14.11(typescript@6.0.3)(viem@2.53.1(typescript@6.0.3)(zod@4.4.3)): + accounts@0.14.11(typescript@6.0.3)(viem@2.54.0(typescript@6.0.3)(zod@4.4.3)): dependencies: hono: 4.12.25 idb-keyval: 6.2.5 jose: 6.2.3 mipd: 0.0.7(typescript@6.0.3) - mppx: 0.7.0(hono@4.12.25)(typescript@6.0.3)(viem@2.53.1(typescript@6.0.3)(zod@4.4.3)) + mppx: 0.7.0(hono@4.12.25)(typescript@6.0.3)(viem@2.54.0(typescript@6.0.3)(zod@4.4.3)) ox: 0.14.29(typescript@6.0.3)(zod@4.4.3) wata: 0.4.0(typescript@6.0.3) webauthx: 0.1.2(typescript@6.0.3)(zod@4.4.3) zod: 4.4.3 zustand: 5.0.14 optionalDependencies: - viem: 2.53.1(typescript@6.0.3)(zod@4.4.3) + viem: 2.54.0(typescript@6.0.3)(zod@4.4.3) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - '@types/react' @@ -2656,6 +2698,8 @@ snapshots: transitivePeerDependencies: - bare-abort-controller + eventsource-parser@3.1.0: {} + expand-template@2.0.3: {} expect-type@1.3.0: {} @@ -2702,6 +2746,16 @@ snapshots: ieee754@1.2.1: {} + incur@0.4.13: + dependencies: + '@cfworker/json-schema': 4.1.1 + '@modelcontextprotocol/server': 2.0.0-alpha.2(@cfworker/json-schema@4.1.1) + '@scalar/openapi-types': 0.8.0 + '@toon-format/toon': 2.3.0 + tokenx: 1.3.0 + yaml: 2.9.0 + zod: 4.4.3 + incur@0.4.8: dependencies: '@cfworker/json-schema': 4.1.1 @@ -2811,12 +2865,25 @@ snapshots: mkdirp-classic@0.5.3: {} - mppx@0.7.0(hono@4.12.25)(typescript@6.0.3)(viem@2.53.1(typescript@6.0.3)(zod@4.4.3)): + mppx@0.7.0(hono@4.12.25)(typescript@6.0.3)(viem@2.54.0(typescript@6.0.3)(zod@4.4.3)): dependencies: '@stripe/stripe-js': 9.7.0 incur: 0.4.8 ox: 0.14.27(typescript@6.0.3)(zod@4.4.3) - viem: 2.53.1(typescript@6.0.3)(zod@4.4.3) + viem: 2.54.0(typescript@6.0.3)(zod@4.4.3) + zod: 4.4.3 + optionalDependencies: + hono: 4.12.25 + transitivePeerDependencies: + - typescript + + mppx@0.8.9(hono@4.12.25)(typescript@6.0.3)(viem@2.54.0(typescript@6.0.3)(zod@4.4.3)): + dependencies: + '@stripe/stripe-js': 9.9.0 + eventsource-parser: 3.1.0 + incur: 0.4.13 + ox: 0.14.30(typescript@6.0.3)(zod@4.4.3) + viem: 2.54.0(typescript@6.0.3)(zod@4.4.3) zod: 4.4.3 optionalDependencies: hono: 4.12.25 @@ -2882,6 +2949,21 @@ snapshots: transitivePeerDependencies: - zod + ox@0.14.30(typescript@6.0.3)(zod@4.4.3): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.4(typescript@6.0.3)(zod@4.4.3) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - zod + oxfmt@0.52.0(vite-plus@0.1.24(@types/node@25.9.2)(esbuild@0.28.1)(tsx@4.22.4)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.2)(esbuild@0.28.1)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0)): dependencies: tinypool: 2.1.0 @@ -3215,7 +3297,7 @@ snapshots: util-deprecate@1.0.2: {} - viem@2.53.1(typescript@6.0.3)(zod@4.4.3): + viem@2.54.0(typescript@6.0.3)(zod@4.4.3): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 diff --git a/src/commands/fund.ts b/src/commands/fund.ts index 25b54a4..b429efe 100644 --- a/src/commands/fund.ts +++ b/src/commands/fund.ts @@ -101,10 +101,12 @@ async function fundingBalance(options: { const provider = createProvider({ network: options.chainId === 42431 ? "testnet" : undefined, }) as CoreProvider.Provider & { getClient: () => unknown }; - const rawBalance = (await Actions.token.getBalance(provider.getClient() as never, { - account: options.walletAddress as `0x${string}`, - token: tokenAddress(options.chainId), - })) as bigint; + const rawBalance = ( + await Actions.token.getBalance(provider.getClient() as never, { + account: options.walletAddress as `0x${string}`, + token: tokenAddress(options.chainId), + }) + ).amount; return { balance: formatMicroUnits(rawBalance.toString()), diff --git a/src/commands/request.ts b/src/commands/request.ts index 0d10d99..6f67df6 100644 --- a/src/commands/request.ts +++ b/src/commands/request.ts @@ -1056,10 +1056,12 @@ async function assertSufficientSessionBalance( depositRaw: bigint, ) { const client = createTempoPublicClient(details.chainId === 42431 ? "testnet" : undefined); - const balance = (await Actions.token.getBalance(client as never, { - account: payer as `0x${string}`, - token: details.token as `0x${string}`, - })) as bigint; + const balance = ( + await Actions.token.getBalance(client as never, { + account: payer as `0x${string}`, + token: details.token as `0x${string}`, + }) + ).amount; if (balance >= depositRaw) return; throw paymentError( `Insufficient balance for session deposit: available=${formatTokenAmount(balance)} required=${formatTokenAmount(depositRaw)}`, @@ -1570,4 +1572,4 @@ function parseSimpleToon(value: string) { function write(stdout: Pick, text: string) { stdout.write(text); -} \ No newline at end of file +} diff --git a/src/commands/transfer.ts b/src/commands/transfer.ts index 3dff29c..4c921c5 100644 --- a/src/commands/transfer.ts +++ b/src/commands/transfer.ts @@ -52,7 +52,7 @@ export async function transferTokens(options: { } const provider = createProvider({ network: options.options.network }); - const call = Actions.token.transfer.call({ + const call = Actions.token.transfer.call(provider.getClient() as never, { amount: parseUnits(args.amount, tokenDecimals()), token, to, diff --git a/test/request.test.ts b/test/request.test.ts index 438488c..e532992 100644 --- a/test/request.test.ts +++ b/test/request.test.ts @@ -3,9 +3,9 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Challenge, Credential, Method, z } from "mppx"; -import { Mppx } from "mppx/client"; +import { Mppx, session as tempoSession } from "mppx/client"; import { Keystore } from "accounts"; -import { decodeFunctionData } from "viem"; +import { createClient, custom, decodeFunctionData } from "viem"; import { Abis as TempoAbis, Channel as TempoChannel, KeyAuthorizationManager } from "viem/tempo"; import { afterEach, describe, expect, it } from "vitest"; @@ -489,6 +489,61 @@ describe("request command", () => { account: { accessKeyAddress: account.accessKeyAddress, keyType: "p256" }, mode: "pull", }); + + if (!("account" in identity)) throw new Error("expected a stored access-key identity"); + const client = createClient({ + account: identity.account, + chain: { id: 4217 } as never, + transport: custom({ + async request({ method }) { + if (method === "eth_chainId") return "0x1079"; + throw new Error(`unexpected RPC request: ${method}`); + }, + }), + }); + const payment = tempoSession({ + account: identity.account, + decimals: 0, + getClient: () => client, + }); + const descriptor = { + authorizedSigner: account.accessKeyAddress, + expiringNonceHash: `0x${"22".repeat(32)}` as `0x${string}`, + operator: "0x0000000000000000000000000000000000000000", + payee: "0x0000000000000000000000000000000000000002", + payer: account.address, + salt: `0x${"11".repeat(32)}` as `0x${string}`, + token: "0x20C000000000000000000000b9537d11c60E8b50", + } as const; + const credential = await payment.createCredential({ + challenge: { + id: "test", + intent: "session", + method: "tempo", + realm: "rpc.mpp.tempo.xyz", + request: { + amount: "1", + currency: descriptor.token, + methodDetails: { + chainId: 4217, + escrowContract: "0x4d50500000000000000000000000000000000000", + sessionProtocol: "v2", + }, + recipient: descriptor.payee, + }, + } as never, + context: { + action: "voucher", + cumulativeAmountRaw: "1", + descriptor, + }, + }); + const payload = Credential.deserialize>(credential).payload; + + expect(payload).toMatchObject({ + action: "voucher", + descriptor: { authorizedSigner: account.accessKeyAddress }, + }); }); it("keeps v2 session descriptors for reuse", async () => { @@ -667,4 +722,4 @@ async function readSeenRequest(request: IncomingMessage): Promise { method: request.method ?? "", url: request.url ?? "", }; -} \ No newline at end of file +}