diff --git a/.changelog/accounts-store-owner.md b/.changelog/accounts-store-owner.md new file mode 100644 index 0000000..cc3ee8c --- /dev/null +++ b/.changelog/accounts-store-owner.md @@ -0,0 +1,5 @@ +--- +wallet-cli: patch +--- + +Delegated CLI wallet persistence and access-key selection to the Accounts SDK while retaining legacy `keys.toml` migration. diff --git a/src/commands/identity.ts b/src/commands/identity.ts index 42cabf5..27445cf 100644 --- a/src/commands/identity.ts +++ b/src/commands/identity.ts @@ -255,7 +255,7 @@ export async function currentWhoamiOutput(options: { network?: string | undefined; }) { const token = - options.accessKeys[0]?.limits[0]?.token ?? + options.accessKeys[0]?.limits?.[0]?.token ?? tokenAddress(options.chain ?? chainId(options.network)); const balance = await tokenBalance({ token, @@ -288,7 +288,7 @@ export async function currentKeysOutput(options: { const balances = new Map(); const keys = []; for (const key of options.accessKeys) { - const token = key.limits[0]?.token ?? tokenAddress(key.chainId); + const token = key.limits?.[0]?.token ?? tokenAddress(key.chainId); const balance = balances.has(token.toLowerCase()) ? (balances.get(token.toLowerCase()) ?? null) : await tokenBalance({ @@ -321,7 +321,7 @@ function currentKeyOutput(options: { status: string | null; }) { if (!options.key) return null; - const limit = options.key.limits[0]; + const limit = options.key.limits?.[0]; const token = limit?.token ?? tokenAddress(options.chain ?? options.key.chainId); const spendingLimits = accessKeyLimitsOutput(options.key); return { @@ -350,7 +350,7 @@ function currentKeyOutput(options: { } function accessKeyLimitsOutput(key: WalletState["accessKeys"][number]) { - return key.limits.map((limit) => ({ + return (key.limits ?? []).map((limit) => ({ unlimited: false, symbol: tokenSymbol(limit.token), token: limit.token.toLowerCase(), @@ -365,11 +365,11 @@ function accessKeyScopesOutput(key: WalletState["accessKeys"][number]) { return accessKeyScopes(key).map((scope) => ({ address: scope.address.toLowerCase(), selector: scope.selector ?? null, - recipients: scope.recipients.map((recipient) => recipient.toLowerCase()), + recipients: (scope.recipients ?? []).map((recipient) => recipient.toLowerCase()), })); } -function accessKeyScopes(key: WalletState["accessKeys"][number]) { +function accessKeyScopes(key: WalletState["accessKeys"][number]): readonly AccessKeyScope[] { if (key.scopes !== undefined) return key.scopes; return parseKeyAuthorizationScopes(key.keyAuthorization); } diff --git a/src/commands/request.ts b/src/commands/request.ts index 6f67df6..cd112c8 100644 --- a/src/commands/request.ts +++ b/src/commands/request.ts @@ -7,7 +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 { Store } from "accounts"; import { Session as TempoSession } from "mppx/tempo"; import { Agent, @@ -26,7 +26,6 @@ import { Actions, Chain, Channel as TempoChannel, - KeyAuthorizationManager, } from "viem/tempo"; import { withSessionLock } from "../payment/session-lock.js"; @@ -51,7 +50,6 @@ import { rpcUrl, } from "../shared/network.js"; import { getRecord, nowSeconds, parseOnChainBigInt, stringValue } from "../shared/utils.js"; -import { loadWalletState, type WalletState } from "../wallet/store.js"; export type RequestOptions = { bearer?: string | undefined; @@ -663,13 +661,14 @@ export 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 } }; + store: StoredAccessKeyStore; }; + await Store.waitForHydration(providerState.store as never); + const storedIdentity = await storedAccessKeyIdentity(providerState.store, options); + if (storedIdentity) return storedIdentity; + await ensureProviderAccounts(provider); const getClient = ({ chainId }: { chainId?: number | undefined }) => { const client = provider.getClient({ chainId }); @@ -705,69 +704,44 @@ async function ensureProviderAccounts(provider: Parameters[0]) { await connect(provider); } -export async function storedAccessKeyIdentity(walletState: WalletState, options: RequestOptions) { - const activeAccount = walletState.accounts[walletState.activeAccount ?? 0]; +type StoredAccessKeyStore = { + accessKeys: { + select(options: { + account: `0x${string}`; + chainId: number; + }): Promise; + }; + getState(): { + accounts: readonly { address: string }[]; + activeAccount: number; + }; +}; + +async function storedAccessKeyIdentity(store: StoredAccessKeyStore, options: RequestOptions) { + const state = store.getState(); + const activeAccount = state.accounts[state.activeAccount]; if (!activeAccount) return undefined; const expectedChain = chainId(options.network); - for (const key of walletState.accessKeys) { - if (key.chainId !== expectedChain) continue; - if (key.keyType && key.keyType !== "secp256k1" && key.keyType !== "p256") continue; - - const keyAuthorizationManager = KeyAuthorizationManager.memory(); - if (key.keyAuthorization) { - await keyAuthorizationManager.set( - { - address: activeAccount.address as `0x${string}`, - accessKey: key.address as `0x${string}`, - chainId: expectedChain, - }, - key.keyAuthorization as never, - ); - } - - 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 account = await store.accessKeys.select({ + account: activeAccount.address as `0x${string}`, + chainId: expectedChain, + }); + if (!account) return undefined; - const getClient = ({ chainId }: { chainId?: number | undefined }) => - createWalletClient({ - account, - chain: chainId === 42431 ? Chain.tempoModerato : Chain.tempo, - transport: http(rpcUrl(chainId === 42431 ? "testnet" : "mainnet")), - }); - return { + const getClient = ({ chainId }: { chainId?: number | undefined }) => + createWalletClient({ account, - address: account.address, - getClient, - methodOptions: { account, getClient, mode: "pull" as const }, - signerAddress: account.accessKeyAddress, - }; - } - - return undefined; + chain: chainId === 42431 ? Chain.tempoModerato : Chain.tempo, + transport: http(rpcUrl(chainId === 42431 ? "testnet" : "mainnet")), + }); + return { + account, + address: account.address, + getClient, + methodOptions: { account, getClient, mode: "pull" as const }, + signerAddress: account.accessKeyAddress, + }; } type PaymentIdentity = Awaited>; diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 48c0eb2..591b0b3 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -11,8 +11,8 @@ export function stringValue(value: unknown) { return typeof value === "string" ? value : ""; } -export function cleanStoredScalar(value: string) { - return value.replace(/#__bigint$/, ""); +export function cleanStoredScalar(value: bigint | string) { + return value.toString().replace(/#__bigint$/, ""); } export function sleep(ms: number) { diff --git a/src/wallet/store.ts b/src/wallet/store.ts index d8cd3c3..cc29dfa 100644 --- a/src/wallet/store.ts +++ b/src/wallet/store.ts @@ -1,20 +1,16 @@ -import { randomUUID } from "node:crypto"; -import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; +import { readFile } from "node:fs/promises"; import { homedir } from "node:os"; -import { dirname, join } from "node:path"; +import { join } from "node:path"; -import { getArray, getRecord } from "../shared/utils.js"; +import { Store } from "accounts"; +import { Storage } from "accounts/cli"; -export type AccessKeyLimit = { - token: string; - limit: string; - period?: number | undefined; -}; +import { createProvider } from "../provider.js"; export type AccessKeyScope = { address: string; selector?: string | undefined; - recipients: readonly string[]; + recipients?: readonly string[] | undefined; }; export type WalletState = { @@ -30,156 +26,81 @@ export type WalletState = { keyType?: string | undefined; privateKey?: string | undefined; publicKey?: string | undefined; - limits: readonly AccessKeyLimit[]; + limits?: readonly { + token: string; + limit: bigint | string; + period?: number | undefined; + }[]; scopes?: readonly AccessKeyScope[] | undefined; }[]; activeAccount?: number | undefined; chainId?: number | undefined; }; -const privateDirMode = 0o700; -const privateFileMode = 0o600; - +/** + * Loads the wallet state through the Accounts SDK provider store. + * + * The Accounts SDK owns the current `store.json` path, envelope, validation, + * hydration, serialization, and filesystem safety. Wallet CLI only retains the + * one-time migration from its legacy `keys.toml` format. + */ export async function loadWalletState(): Promise { - const path = walletStorePath(); - let text: string; - - try { - text = await readFile(path, "utf8"); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") return await migrateLegacyWalletState(); - throw error; - } + const persisted = await Storage.filesystem().getItem("store"); + const store = await loadWalletStore(); + const state = store.getState(); + if (persisted !== null || state.accounts.length || state.accessKeys.length) + return state as unknown as WalletState; - const value = JSON.parse(text) as unknown; - const envelope = getRecord(value)["tempo-cli.store"]; - const state = getRecord(envelope).state; - if (!state) return emptyWalletState(); - - const record = getRecord(state); - const accounts = getArray(record.accounts).flatMap((account) => { - const item = getRecord(account); - return typeof item.address === "string" ? [{ address: item.address }] : []; - }); - const accessKeys = getArray(record.accessKeys).flatMap((key) => { - const item = getRecord(key); - if ( - typeof item.address !== "string" || - typeof item.access !== "string" || - typeof item.chainId !== "number" - ) - return []; - - return [ - { - address: item.address, - access: item.access, - chainId: item.chainId, - expiry: typeof item.expiry === "number" ? item.expiry : undefined, - ...(isRecord(item.handle) ? { handle: reviveBigInts(item.handle) } : {}), - ...(isRecord(item.keyPair) ? { keyPair: reviveBigInts(item.keyPair) } : {}), - keyAuthorization: reviveBigInts(item.keyAuthorization), - keyType: typeof item.keyType === "string" ? item.keyType : undefined, - privateKey: typeof item.privateKey === "string" ? item.privateKey : undefined, - publicKey: typeof item.publicKey === "string" ? item.publicKey : undefined, - limits: parseAccessKeyLimits(item.limits), - scopes: parseAccessKeyScopes(item.scopes), - }, - ]; - }); - - return { - accounts, - accessKeys, - activeAccount: typeof record.activeAccount === "number" ? record.activeAccount : undefined, - chainId: typeof record.chainId === "number" ? record.chainId : undefined, - }; -} - -function reviveBigInts(value: unknown): unknown { - if (typeof value === "string" && value.endsWith("#__bigint")) { - return BigInt(value.slice(0, -"#__bigint".length)); - } - if (Array.isArray(value)) return value.map(reviveBigInts); - if (!value || typeof value !== "object") return value; - return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, reviveBigInts(item)])); -} - -function isRecord(value: unknown): value is Record { - return Boolean(value && typeof value === "object" && !Array.isArray(value)); -} - -function parseAccessKeyLimits(value: unknown): AccessKeyLimit[] { - return getArray(value).flatMap((limit) => { - const item = getRecord(limit); - if (typeof item.token !== "string" || typeof item.limit !== "string") return []; - if (item.period !== undefined && typeof item.period !== "number") return []; - return [ - { - token: item.token, - limit: item.limit, - period: typeof item.period === "number" ? item.period : undefined, - }, - ]; - }); -} - -function parseAccessKeyScopes(value: unknown): AccessKeyScope[] | undefined { - if (!Array.isArray(value)) return undefined; - const scopes = getArray(value).flatMap((scope) => { - const item = getRecord(scope); - if (typeof item.address !== "string") return []; - return [ - { - address: item.address, - selector: typeof item.selector === "string" ? item.selector : undefined, - recipients: getArray(item.recipients).flatMap((recipient) => - typeof recipient === "string" ? [recipient] : [], - ), - }, - ]; - }); - return scopes; + const legacy = await loadLegacyWalletState(); + if (!legacy.accounts.length && !legacy.accessKeys.length) return state as unknown as WalletState; + await replaceWalletState(store, legacy); + return store.getState() as unknown as WalletState; } +/** Replaces the Accounts SDK provider state and waits for filesystem persistence. */ export async function saveWalletState(state: WalletState) { - const path = walletStorePath(); - await ensurePrivateWalletDirectory(path); - await writePrivateFile( - path, - JSON.stringify( - { - "tempo-cli.store": { - state: { - accounts: state.accounts, - accessKeys: state.accessKeys, - activeAccount: state.activeAccount ?? 0, - chainId: state.chainId ?? 4217, - }, - version: 0, - }, - }, - (_key, value: unknown) => (typeof value === "bigint" ? `${value}#__bigint` : value), - 2, - ), - ); + const store = await loadWalletStore(); + await replaceWalletState(store, state); } +/** Returns the Accounts SDK's default CLI storage path. */ export function walletStorePath() { - return join(homedir(), ".tempo", "wallet", "store.json"); + return Storage.defaultPath(); } export function emptyWalletState(): WalletState { return { accounts: [], accessKeys: [], + activeAccount: 0, + chainId: 4217, }; } -async function migrateLegacyWalletState() { - const state = await loadLegacyWalletState(); - if (state.accounts.length || state.accessKeys.length) await saveWalletState(state); - return state; +type WalletStore = { + getState(): Store.State; + setState(state: Store.State): void; +}; + +async function loadWalletStore(): Promise { + const provider = createProvider() as unknown as { store: WalletStore }; + await Store.waitForHydration(provider.store as never); + return provider.store; +} + +async function replaceWalletState(store: WalletStore, state: WalletState) { + const current = store.getState(); + store.setState({ + ...current, + accounts: state.accounts as Store.State["accounts"], + accessKeys: state.accessKeys as Store.State["accessKeys"], + activeAccount: state.activeAccount ?? 0, + chainId: state.chainId ?? 4217, + }); + + // Filesystem storage serializes operations per path. Queueing a read after + // Zustand's write gives CLI commands an explicit persistence barrier. + await Storage.filesystem().getItem("store"); } async function loadLegacyWalletState(): Promise { @@ -195,7 +116,7 @@ async function loadLegacyWalletState(): Promise { const accounts = [...new Set(keys.map((key) => key.access))].map((address) => ({ address })); return { accounts, - accessKeys: keys, + accessKeys: keys as unknown as WalletState["accessKeys"], ...(accounts.length ? { activeAccount: 0 } : {}), ...(keys[0] ? { chainId: keys[0].chainId } : {}), }; @@ -205,7 +126,7 @@ function legacyKeysPath() { return join(homedir(), ".tempo", "wallet", "keys.toml"); } -function parseLegacyKeys(text: string): WalletState["accessKeys"] { +function parseLegacyKeys(text: string): Store.State["accessKeys"] { const keys: LegacyKey[] = []; let key: LegacyKey | undefined; let limit: LegacyLimit | undefined; @@ -248,7 +169,7 @@ function parseLegacyKeys(text: string): WalletState["accessKeys"] { if (field === "key_address" && typeof value === "string") key.address = value; if (field === "key" && typeof value === "string") key.privateKey = value; if (field === "key_authorization" && typeof value === "string") key.keyAuthorization = value; - if (field === "key_type" && typeof value === "string") key.keyType = value; + if (field === "key_type" && (value === "p256" || value === "secp256k1")) key.keyType = value; if (field === "expiry" && typeof value === "number") key.expiry = value; } @@ -269,12 +190,12 @@ function parseLegacyKeys(text: string): WalletState["accessKeys"] { keyAuthorization: key.keyAuthorization, keyType: key.keyType ?? "secp256k1", privateKey: key.privateKey, - limits: (key.limits ?? []).flatMap((limit) => { - if (typeof limit.token !== "string" || typeof limit.limit !== "string") return []; - return [{ token: limit.token, limit: `${limit.limit}#__bigint` }]; + limits: (key.limits ?? []).flatMap((item) => { + if (typeof item.token !== "string" || typeof item.limit !== "string") return []; + return [{ token: item.token, limit: BigInt(item.limit) }]; }), }, - ]; + ] as Store.State["accessKeys"]; }); } @@ -307,47 +228,13 @@ function parseTomlValue(value: string) { return trimmed; } -async function ensurePrivateWalletDirectory(path: string) { - const walletDir = dirname(path); - const tempoDir = dirname(walletDir); - await mkdir(tempoDir, { recursive: true, mode: privateDirMode }); - await chmodIfSupported(tempoDir, privateDirMode); - await mkdir(walletDir, { recursive: true, mode: privateDirMode }); - await chmodIfSupported(walletDir, privateDirMode); -} - -async function writePrivateFile(path: string, contents: string) { - const tempPath = join( - dirname(path), - `.store.json.${process.pid}.${Date.now()}.${randomUUID()}.tmp`, - ); - try { - await writeFile(tempPath, contents, { mode: privateFileMode }); - await chmodIfSupported(tempPath, privateFileMode); - await rename(tempPath, path); - await chmodIfSupported(path, privateFileMode); - } catch (error) { - await unlink(tempPath).catch(() => undefined); - throw error; - } -} - -async function chmodIfSupported(path: string, mode: number) { - try { - await chmod(path, mode); - } catch (error) { - if (process.platform === "win32") return; - throw error; - } -} - type LegacyKey = { access?: string | undefined; address?: string | undefined; chainId?: number | undefined; expiry?: number | undefined; keyAuthorization?: string | undefined; - keyType?: string | undefined; + keyType?: "p256" | "secp256k1" | undefined; privateKey?: string | undefined; limits?: LegacyLimit[] | undefined; }; diff --git a/test/helpers.ts b/test/helpers.ts index 639b15c..6227043 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -104,10 +104,10 @@ export function walletState(overrides: Partial = {}): WalletState { address: testAccessKey, access: testWallet, chainId: 4217, - expiry: 1783809942, + expiry: 2_000_000_000, keyType: "secp256k1", privateKey: testPrivateKey, - limits: [{ token: usdc, limit: "100000000#__bigint" }], + limits: [{ token: usdc, limit: 100000000n }], }, ], activeAccount: 0, diff --git a/test/identity.test.ts b/test/identity.test.ts index dd5284b..0e7e48c 100644 --- a/test/identity.test.ts +++ b/test/identity.test.ts @@ -91,7 +91,7 @@ key_address = "${testAccessKey}" key = "${testPrivateKey}" key_authorization = "0x1234" provisioned = true -expiry = 1783809942 +expiry = 2000000000 [[keys.limits]] currency = "0x20C000000000000000000000b9537d11c60E8b50" @@ -138,7 +138,7 @@ wallet_address = "${testWallet}" # inline comments are ignored chain_id = 4217 key_address = "${testAccessKey}" key = "${testPrivateKey}" -expiry = 1783809942 +expiry = 2000000000 [[keys.limits]] currency = "${usdc}" @@ -173,13 +173,13 @@ key = "${testPrivateKey2}" access: testWallet, address: testAccessKey, chainId: 4217, - expiry: 1783809942, + expiry: 2_000_000_000, keyAuthorization: undefined, keyType: "secp256k1", privateKey: testPrivateKey, limits: [ - { token: usdc, limit: "100000000#__bigint" }, - { token: "0x1111111111111111111111111111111111111111", limit: "2500000#__bigint" }, + { token: usdc, limit: 100000000n }, + { token: "0x1111111111111111111111111111111111111111", limit: 2500000n }, ], }, { @@ -232,7 +232,7 @@ key_address = "${testAccessKey}" key = "${testPrivateKey}" `); - await expect(loadWalletState()).rejects.toThrow(SyntaxError); + await expect(loadWalletState()).rejects.toThrow("not valid JSON"); }); it("prefers store.json over legacy keys.toml", async () => { @@ -373,10 +373,10 @@ limit = "100000000" ...walletState().accessKeys[0]!, expiry: 4_102_444_800, limits: [ - { token: usdc, limit: "100000000#__bigint", period: 86_400 }, + { token: usdc, limit: 100000000n, period: 86_400 }, { token: "0x1111111111111111111111111111111111111111", - limit: "2500000#__bigint", + limit: 2500000n, }, ], scopes: [ diff --git a/test/request.test.ts b/test/request.test.ts index e532992..5621e19 100644 --- a/test/request.test.ts +++ b/test/request.test.ts @@ -15,7 +15,6 @@ import { parseRequestArgs, resolvePaymentIdentity, runRequest, - storedAccessKeyIdentity, tempoPaymentChallengeResponse, } from "../src/commands/request.js"; import { @@ -33,7 +32,6 @@ import { walletState, writeWalletState, } from "./helpers.js"; -import { loadWalletState } from "../src/wallet/store.js"; type SeenRequest = { body: string; @@ -425,7 +423,7 @@ describe("request command", () => { const keyAuthorization = { address: testAccessKey, chainId: 4217n, - expiry: 1783809942, + expiry: 2_000_000_000, limits: [{ token: "0x20C000000000000000000000b9537d11c60E8b50", limit: 100000000n }], signature: { type: "secp256k1", signature: "0x1234" }, type: "secp256k1", @@ -441,11 +439,8 @@ describe("request command", () => { }), ); - const state = await loadWalletState(); - const identity = await storedAccessKeyIdentity( - state, - requestOptions("https://paid.example.com"), - ); + const identity = await resolvePaymentIdentity(requestOptions("https://paid.example.com")); + if (!("account" in identity)) throw new Error("expected a stored access-key identity"); const stored = await identity?.account.keyAuthorizationManager?.get({ address: testWallet, accessKey: testAccessKey, @@ -471,7 +466,7 @@ describe("request command", () => { address: account.accessKeyAddress, access: testWallet, chainId: 4217, - expiry: 1783809942, + expiry: 2_000_000_000, handle, keyType: "p256", limits: [], diff --git a/test/wallet-store.test.ts b/test/wallet-store.test.ts index 3c0c933..0b48d7f 100644 --- a/test/wallet-store.test.ts +++ b/test/wallet-store.test.ts @@ -23,7 +23,6 @@ import { walletState, walletStoreExists, writeLegacyKeysToml, - writeRawWalletStore, } from "./helpers.js"; describe("wallet store file", () => { @@ -33,7 +32,7 @@ describe("wallet store file", () => { expect(walletStorePath()).toBe(join(home, ".tempo", "wallet", "store.json")); }); - it("saves the current envelope with default active account and chain", async () => { + it("persists through the Accounts SDK filesystem storage", async () => { const home = await useTempHome(); await saveWalletState({ accounts: [{ address: testWallet }], @@ -60,7 +59,6 @@ describe("wallet store file", () => { const home = await useTempHome(); await saveWalletState(walletState()); - await expectMode(join(home, ".tempo"), 0o700); await expectMode(join(home, ".tempo", "wallet"), 0o700); await expectMode(join(home, ".tempo", "wallet", "store.json"), 0o600); }); @@ -77,112 +75,14 @@ describe("wallet store file", () => { await saveWalletState(walletState()); - await expectMode(tempoDir, 0o700); await expectMode(walletDir, 0o700); await expectMode(storePath, 0o600); }); - it("loads an empty state from missing or malformed envelopes", async () => { + it("loads the Accounts SDK default state when storage is missing", async () => { await useTempHome(); expect(await loadWalletState()).toEqual(emptyWalletState()); - - for (const body of [ - "{}", - '{"tempo-cli.store": null}', - '{"tempo-cli.store": {}}', - '{"tempo-cli.store": {"state": null}}', - '{"tempo-cli.store": {"state": "not-an-object"}}', - ]) { - await writeRawWalletStore(body); - expect(await loadWalletState()).toEqual(emptyWalletState()); - } - }); - - it("filters malformed accounts, keys, limits, and optional scalar fields", async () => { - await useTempHome(); - await writeRawWalletStore( - JSON.stringify({ - "tempo-cli.store": { - state: { - accounts: [{ address: testWallet }, { address: 42 }, null, { other: testWallet2 }], - activeAccount: "0", - chainId: "4217", - accessKeys: [ - { - access: testWallet, - address: testAccessKey, - chainId: 4217, - expiry: "1783809942", - keyAuthorization: { - address: testAccessKey, - nested: ["100#__bigint", { limit: "250#__bigint" }], - }, - keyType: 1, - privateKey: false, - limits: [ - { token: usdc, limit: "100000000#__bigint", period: 86_400 }, - { token: usdc, limit: 100000000 }, - { token: 1, limit: "100000000#__bigint" }, - { token: usdc, limit: "250000000#__bigint", period: "86400" }, - null, - ], - scopes: [ - { - address: usdc, - selector: "transfer(address,uint256)", - recipients: [testWallet2, 42], - }, - { address: 42 }, - null, - ], - }, - { - access: testWallet, - address: testAccessKey2, - chainId: "4217", - limits: [], - }, - { - access: testWallet, - chainId: 4217, - limits: [], - }, - null, - ], - }, - version: 0, - }, - }), - ); - - expect(await loadWalletState()).toEqual({ - accounts: [{ address: testWallet }], - accessKeys: [ - { - access: testWallet, - address: testAccessKey, - chainId: 4217, - expiry: undefined, - keyAuthorization: { - address: testAccessKey, - nested: [100n, { limit: 250n }], - }, - keyType: undefined, - privateKey: undefined, - limits: [{ token: usdc, limit: "100000000#__bigint", period: 86_400 }], - scopes: [ - { - address: usdc, - selector: "transfer(address,uint256)", - recipients: [testWallet2], - }, - ], - }, - ], - activeAccount: undefined, - chainId: undefined, - }); }); it("round trips nested key authorization bigint values", async () => { @@ -221,7 +121,6 @@ describe("wallet store file", () => { ...walletState().accessKeys[0]!, handle: { jwk: { crv: "P-256", kty: "EC" }, kind: "webcrypto-p256" }, keyType: "p256", - privateKey: undefined, publicKey: "0x04abcd", }; @@ -237,10 +136,10 @@ describe("wallet store file", () => { { ...walletState().accessKeys[0]!, limits: [ - { token: usdc, limit: "100000000#__bigint", period: 86_400 }, + { token: usdc, limit: 100000000n, period: 86_400 }, { token: "0x1111111111111111111111111111111111111111", - limit: "2500000#__bigint", + limit: 2500000n, }, ], scopes: [ @@ -279,37 +178,6 @@ describe("wallet store file", () => { expect(await loadWalletState()).toEqual(state); }); - it("can save a state after loading revived bigint authorizations", async () => { - await useTempHome(); - await writeRawWalletStore( - JSON.stringify({ - "tempo-cli.store": { - state: { - accounts: [{ address: testWallet }], - activeAccount: 0, - accessKeys: [ - { - access: testWallet, - address: testAccessKey, - chainId: 4217, - keyAuthorization: { chainId: "4217#__bigint" }, - limits: [{ token: usdc, limit: "100000000#__bigint" }], - privateKey: testPrivateKey, - }, - ], - chainId: 4217, - }, - version: 0, - }, - }), - ); - - const loaded = await loadWalletState(); - await saveWalletState(loaded); - - expect(await loadWalletState()).toEqual(loaded); - }); - it("migrates legacy TOML with comments, escaped strings, booleans, and CRLF line endings", async () => { await useTempHome(); await writeLegacyKeysToml( @@ -323,7 +191,7 @@ describe("wallet store file", () => { `key = "${testPrivateKey}"`, 'key_authorization = "0x12#34"', "provisioned = true", - "expiry = 1783809942", + "expiry = 2000000000", "", "[[keys.limits]]", `currency = "${usdc}"`,