Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changelog/fix-p256-mpp-payment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
wallet-cli: patch
---

Use stored P-256 access keys when creating MPP payment credentials.
32 changes: 25 additions & 7 deletions src/commands/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { setTimeout as sleep } from "node:timers/promises";

import { Challenge, Credential, PaymentRequest } from "mppx";
import { Mppx, session as tempoSession, tempo } from "mppx/client";
import { Keystore } from "accounts";
import { Session as TempoSession } from "mppx/tempo";
import {
Agent,
Expand Down Expand Up @@ -50,7 +51,7 @@ import {
rpcUrl,
} from "../shared/network.js";
import { getRecord, nowSeconds, parseOnChainBigInt, stringValue } from "../shared/utils.js";
import type { WalletState } from "../wallet/store.js";
import { loadWalletState, type WalletState } from "../wallet/store.js";

export type RequestOptions = {
bearer?: string | undefined;
Expand Down Expand Up @@ -644,7 +645,7 @@ async function paySessionAndRetryRequest(
}
}

async function resolvePaymentIdentity(options: RequestOptions) {
export async function resolvePaymentIdentity(options: RequestOptions) {
const privateKey = options.privateKey ?? process.env.TEMPO_PRIVATE_KEY;
if (privateKey) {
const account = privateKeyToAccount(privateKey as `0x${string}`);
Expand All @@ -662,6 +663,9 @@ async function resolvePaymentIdentity(options: RequestOptions) {
};
}

const storedIdentity = await storedAccessKeyIdentity(await loadWalletState(), options);
if (storedIdentity) return storedIdentity;

const provider = createProvider({ network: options.network });
const providerState = provider as unknown as {
store: { getState(): { accounts: { address: string }[]; activeAccount: number } };
Expand Down Expand Up @@ -707,7 +711,7 @@ export async function storedAccessKeyIdentity(walletState: WalletState, options:

const expectedChain = chainId(options.network);
for (const key of walletState.accessKeys) {
if (key.chainId !== expectedChain || !key.privateKey) continue;
if (key.chainId !== expectedChain) continue;
if (key.keyType && key.keyType !== "secp256k1" && key.keyType !== "p256") continue;

const keyAuthorizationManager = KeyAuthorizationManager.memory();
Expand All @@ -722,16 +726,30 @@ export async function storedAccessKeyIdentity(walletState: WalletState, options:
);
}

const account =
key.keyType === "p256"
const account = key.privateKey
? key.keyType === "p256"
? TempoAccount.fromP256(key.privateKey as `0x${string}`, {
access: activeAccount.address as `0x${string}`,
keyAuthorizationManager,
})
: TempoAccount.fromSecp256k1(key.privateKey as `0x${string}`, {
access: activeAccount.address as `0x${string}`,
keyAuthorizationManager,
});
})
: key.keyType === "p256" && key.handle && key.publicKey
? await Keystore.webCryptoP256({ extractable: true }).toAccount(
{
handle: key.handle as Keystore.Handle,
keyType: key.keyType,
publicKey: key.publicKey as `0x${string}`,
},
{
access: activeAccount.address as `0x${string}`,
keyAuthorizationManager,
},
)
: undefined;
if (!account) continue;
if (key.address.toLowerCase() !== account.accessKeyAddress.toLowerCase()) continue;

const getClient = ({ chainId }: { chainId?: number | undefined }) =>
Expand Down Expand Up @@ -1552,4 +1570,4 @@ function parseSimpleToon(value: string) {

function write(stdout: Pick<NodeJS.WriteStream, "write">, text: string) {
stdout.write(text);
}
}
41 changes: 39 additions & 2 deletions test/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { join } from "node:path";

import { Challenge, Credential, Method, z } from "mppx";
import { Mppx } from "mppx/client";
import { Keystore } from "accounts";
import { decodeFunctionData } from "viem";
import { Abis as TempoAbis, Channel as TempoChannel } from "viem/tempo";
import { Abis as TempoAbis, Channel as TempoChannel, KeyAuthorizationManager } from "viem/tempo";
import { afterEach, describe, expect, it } from "vitest";

import {
buildTopUpTransactionRequest,
isSessionInvalidationResponse,
parseRequestArgs,
resolvePaymentIdentity,
runRequest,
storedAccessKeyIdentity,
tempoPaymentChallengeResponse,
Expand Down Expand Up @@ -454,6 +456,41 @@ describe("request command", () => {
expect(stored).toStrictEqual(keyAuthorization);
});

it("uses stored P-256 access keys for payment identity resolution", async () => {
await useTempHome();
const keystore = Keystore.webCryptoP256({ extractable: true });
const { handle, publicKey } = await keystore.createKey();
const account = await keystore.toAccount(
{ handle, keyType: "p256", publicKey },
{ access: testWallet, keyAuthorizationManager: KeyAuthorizationManager.memory() },
);
await writeWalletState(
walletState({
accessKeys: [
{
address: account.accessKeyAddress,
access: testWallet,
chainId: 4217,
expiry: 1783809942,
handle,
keyType: "p256",
limits: [],
publicKey,
},
],
}),
);

const identity = await resolvePaymentIdentity(requestOptions("https://paid.example.com"));

expect(identity.address.toLowerCase()).toBe(testWallet.toLowerCase());
expect(identity.signerAddress.toLowerCase()).toBe(account.accessKeyAddress.toLowerCase());
expect(identity.methodOptions).toMatchObject({
account: { accessKeyAddress: account.accessKeyAddress, keyType: "p256" },
mode: "pull",
});
});

it("keeps v2 session descriptors for reuse", async () => {
await useTempHome();
const origin = "https://paid.example.com";
Expand Down Expand Up @@ -630,4 +667,4 @@ async function readSeenRequest(request: IncomingMessage): Promise<SeenRequest> {
method: request.method ?? "",
url: request.url ?? "",
};
}
}